diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index 5c377395..1b8d3e6a 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -113,5 +113,15 @@ android:name="android.appwidget.provider" android:resource="@xml/category_time_widget_info" /> + + + + \ No newline at end of file diff --git a/mobile/src/main/java/net/activitywatch/android/fragments/WebUIFragment.kt b/mobile/src/main/java/net/activitywatch/android/fragments/WebUIFragment.kt index ba2fdc43..87604e1c 100644 --- a/mobile/src/main/java/net/activitywatch/android/fragments/WebUIFragment.kt +++ b/mobile/src/main/java/net/activitywatch/android/fragments/WebUIFragment.kt @@ -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 @@ -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 @@ -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) } } @@ -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) + } + } +} diff --git a/mobile/src/main/res/xml/file_paths.xml b/mobile/src/main/res/xml/file_paths.xml new file mode 100644 index 00000000..6377fe7e --- /dev/null +++ b/mobile/src/main/res/xml/file_paths.xml @@ -0,0 +1,4 @@ + + + +