From a0c1cf964b8b47b9fcc13364f1e1cfefae8f5f15 Mon Sep 17 00:00:00 2001 From: MartinBraquet Date: Fri, 13 Feb 2026 17:59:13 +0100 Subject: [PATCH] Allow download on native mobile --- .../compassconnections/app/MainActivity.java | 44 +++++++++++++++++-- web/lib/util/webview.ts | 1 - web/pages/_document.tsx | 8 ++++ web/pages/settings.tsx | 28 +++++++----- 4 files changed, 64 insertions(+), 17 deletions(-) diff --git a/android/app/src/main/java/com/compassconnections/app/MainActivity.java b/android/app/src/main/java/com/compassconnections/app/MainActivity.java index f00979fa..0022ca54 100644 --- a/android/app/src/main/java/com/compassconnections/app/MainActivity.java +++ b/android/app/src/main/java/com/compassconnections/app/MainActivity.java @@ -1,6 +1,7 @@ package com.compassconnections.app; import android.Manifest; +import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.os.Build; @@ -13,8 +14,10 @@ import android.webkit.WebView; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.core.content.ContextCompat; +import androidx.core.content.FileProvider; import com.capacitorjs.plugins.pushnotifications.PushNotificationsPlugin; +import com.compassconnections.app.MainActivity.WebAppInterface; import com.getcapacitor.BridgeActivity; import com.getcapacitor.BridgeWebViewClient; import com.getcapacitor.Plugin; @@ -29,6 +32,10 @@ import com.google.android.play.core.install.model.UpdateAvailability; import org.json.JSONException; import org.json.JSONObject; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + import ee.forgr.capacitor.social.login.GoogleProvider; import ee.forgr.capacitor.social.login.ModifiedMainActivityForSocialLoginPlugin; import ee.forgr.capacitor.social.login.SocialLoginPlugin; @@ -57,13 +64,42 @@ public class MainActivity extends BridgeActivity implements ModifiedMainActivity } } - public static class NativeBridge { + public static class WebAppInterface { + private final Context context; + + public WebAppInterface(Context context) { + this.context = context; + } + @JavascriptInterface - public boolean isNativeApp() { - return true; + public void downloadFile(String filename, String content) { + try { + // Create file in app-specific external storage + File file = new File(context.getExternalFilesDir(null), filename); + + // Write content to file + FileOutputStream fos = new FileOutputStream(file); + fos.write(content.getBytes()); + fos.close(); + + // Get URI via FileProvider + String authority = context.getPackageName() + ".provider"; + android.net.Uri uri = FileProvider.getUriForFile(context, authority, file); + + // Launch intent to view/share file + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(uri, "application/json"); + intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_ACTIVITY_NEW_TASK); + + context.startActivity(intent); + + } catch (IOException e) { + Log.i("CompassApp", "Failed to download file", e); + } } } + @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); @@ -100,7 +136,7 @@ public class MainActivity extends BridgeActivity implements ModifiedMainActivity settings.setUserAgentString(settings.getUserAgentString() + " CompassAppWebView"); settings.setJavaScriptEnabled(true); - webView.addJavascriptInterface(new NativeBridge(), "AndroidBridge"); + webView.addJavascriptInterface(new WebAppInterface(this), "AndroidBridge"); registerPlugin(PushNotificationsPlugin.class); // Initialize the Bridge with Push Notifications plugin diff --git a/web/lib/util/webview.ts b/web/lib/util/webview.ts index a0219d9e..aaae7758 100644 --- a/web/lib/util/webview.ts +++ b/web/lib/util/webview.ts @@ -4,7 +4,6 @@ import {IS_WEBVIEW} from "common/hosting/constants"; export function isAndroidApp() { try { // Detect if Android bridge exists - // return typeof (window as any).AndroidBridge?.isNativeApp === 'function'; return Capacitor.isNativePlatform() || IS_WEBVIEW } catch { return false; diff --git a/web/pages/_document.tsx b/web/pages/_document.tsx index 606b1e27..a622a285 100644 --- a/web/pages/_document.tsx +++ b/web/pages/_document.tsx @@ -4,6 +4,14 @@ import Script from 'next/script' import clsx from "clsx"; import {IS_DEPLOYED} from "common/hosting/constants"; +declare global { + interface Window { + AndroidBridge?: { + downloadFile: (filename: string, content: string) => void + } + } +} + export default function Document() { return ( diff --git a/web/pages/settings.tsx b/web/pages/settings.tsx index 01ac0a4d..1b65ccc7 100644 --- a/web/pages/settings.tsx +++ b/web/pages/settings.tsx @@ -25,6 +25,7 @@ import HiddenProfilesModal from 'web/components/settings/hidden-profiles-modal' import {EmailVerificationButton} from "web/components/email-verification-button"; import {api} from 'web/lib/api' import {useUser} from "web/hooks/use-user"; +import {isNativeMobile} from "web/lib/util/webview"; export default function NotificationsPage() { const t = useT() @@ -209,17 +210,21 @@ const DataPrivacySettings = () => { try { setIsDownloading(true) const data = await api('me/data', {}) - const blob = new Blob([JSON.stringify(data, null, 2)], { - type: 'application/json', - }) - const url = URL.createObjectURL(blob) - const link = document.createElement('a') - link.href = url - link.download = `compass-data-export${user?.username ? `-${user.username}` : ''}.json` - document.body.appendChild(link) - link.click() - document.body.removeChild(link) - URL.revokeObjectURL(url) + const jsonString = JSON.stringify(data, null, 2) + const filename = `compass-data-export${user?.username ? `-${user.username}` : ''}.json`; + if (isNativeMobile() && window.AndroidBridge && window.AndroidBridge.downloadFile) { + window.AndroidBridge.downloadFile(filename, jsonString) + } else { + const blob = new Blob([jsonString], {type: 'application/json'}) + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = filename + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(url) + } toast.success( t( 'settings.data_privacy.download.success', @@ -256,4 +261,3 @@ const DataPrivacySettings = () => { ) } -