Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions mobile/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -113,5 +113,15 @@
android:name="android.appwidget.provider"
android:resource="@xml/category_time_widget_info" />
</receiver>

<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package net.activitywatch.android.fragments

import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.net.Uri
import android.os.Bundle
import android.webkit.JavascriptInterface
import androidx.core.content.FileProvider
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
Expand All @@ -18,6 +21,7 @@ import android.webkit.URLUtil
import android.webkit.WebResourceRequest
import android.webkit.WebViewClient
import net.activitywatch.android.R
import java.io.File
import java.lang.Thread.sleep
import java.net.URI

Expand Down Expand Up @@ -117,6 +121,7 @@ class WebUIFragment : Fragment() {

myWebView.settings.javaScriptEnabled = true
myWebView.settings.domStorageEnabled = true
myWebView.addJavascriptInterface(WebAppInterface(requireContext()), "Android")
arguments?.let {
it.getString(ARG_URL)?.let { it1 -> myWebView.loadUrl(it1) }
}
Expand Down Expand Up @@ -165,3 +170,45 @@ class WebUIFragment : Fragment() {
}
}
}

class WebAppInterface(private val mContext: Context) {
@JavascriptInterface
fun downloadCSV(csv: String, filename: String) {
downloadFile(csv, filename, "text/csv")
}

@JavascriptInterface
fun downloadJSON(json: String, filename: String) {
downloadFile(json, filename, "application/json")
}

private fun downloadFile(content: String, filename: String, mimetype: String) {
// Strip path components from the JS-supplied name to prevent export-root escape
val safeName = File(filename).name.takeIf { it.isNotEmpty() } ?: "export"
val externalDir = mContext.getExternalFilesDir(null) ?: run {
Log.e(TAG, "External files directory unavailable")
return
}
val file = File(externalDir, safeName)
try {
file.writeText(content)
} catch (e: Exception) {
Log.e(TAG, "Failed to write export file: ${e.message}")
return
}
// FileProvider required on API 24+: Uri.fromFile() throws FileUriExposedException
val uri = FileProvider.getUriForFile(
mContext,
"${mContext.packageName}.provider",
file
)
val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(uri, mimetype)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NO_HISTORY)
try {
mContext.startActivity(intent)
} catch (e: ActivityNotFoundException) {
Log.e(TAG, "No viewer app found for $mimetype", e)
}
}
}
4 changes: 4 additions & 0 deletions mobile/src/main/res/xml/file_paths.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-files-path name="external_files" path="." />
</paths>
Loading