From 4380aada1168565a66dfe14469c1b8b42b495470 Mon Sep 17 00:00:00 2001 From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com> Date: Fri, 24 Oct 2025 14:22:02 +0530 Subject: [PATCH 1/2] fix: improve file sharing and URI handling Enhances file sharing by improving URI handling, MIME type resolution, and permission granting for intents. Adds robust support for FileProvider authority detection, ensures files are shareable by copying to cache if needed, and uses ClipData for better compatibility. Also improves error handling and chooser intent usage for SEND, VIEW, and EDIT actions. --- .../android/com/foxdebug/system/System.java | 234 ++++++++++++++++-- 1 file changed, 219 insertions(+), 15 deletions(-) diff --git a/src/plugins/system/android/com/foxdebug/system/System.java b/src/plugins/system/android/com/foxdebug/system/System.java index 087ef1d16..517550fc4 100644 --- a/src/plugins/system/android/com/foxdebug/system/System.java +++ b/src/plugins/system/android/com/foxdebug/system/System.java @@ -104,6 +104,7 @@ public class System extends CordovaPlugin { private Theme theme; private CallbackContext intentHandler; private CordovaWebView webView; + private String fileProviderAuthority; public void initialize(CordovaInterface cordova, CordovaWebView webView) { super.initialize(cordova, webView); @@ -879,7 +880,11 @@ private void fileAction( ) { Activity activity = this.activity; Context context = this.context; - Uri uri = this.getContentProviderUri(fileURI); + Uri uri = this.getContentProviderUri(fileURI, filename); + if (uri == null) { + callback.error("Unable to access file for action " + action); + return; + } try { Intent intent = new Intent(action); @@ -887,12 +892,34 @@ private void fileAction( mimeType = "text/plain"; } + mimeType = resolveMimeType(mimeType, uri, filename); + + String clipLabel = null; + if (filename != null && !filename.isEmpty()) { + clipLabel = new File(filename).getName(); + } + if (clipLabel == null || clipLabel.isEmpty()) { + clipLabel = uri.getLastPathSegment(); + } + if (clipLabel == null || clipLabel.isEmpty()) { + clipLabel = "shared-file"; + } if (action.equals(Intent.ACTION_SEND)) { + intent.setType(mimeType); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.setClipData( + ClipData.newUri( + context.getContentResolver(), + clipLabel, + uri + ) + ); intent.putExtra(Intent.EXTRA_STREAM, uri); - if (!filename.equals("")) { + intent.putExtra(Intent.EXTRA_TITLE, clipLabel); + intent.putExtra(Intent.EXTRA_SUBJECT, clipLabel); + if (filename != null && !filename.isEmpty()) { intent.putExtra(Intent.EXTRA_TEXT, filename); } - intent.setType(mimeType); } else { int flags = Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | @@ -904,9 +931,42 @@ private void fileAction( intent.setFlags(flags); intent.setDataAndType(uri, mimeType); + intent.setClipData( + ClipData.newUri( + context.getContentResolver(), + clipLabel, + uri + ) + ); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + if (!clipLabel.equals("shared-file")) { + intent.putExtra(Intent.EXTRA_TITLE, clipLabel); + } + if (action.equals(Intent.ACTION_EDIT)) { + intent.putExtra(Intent.EXTRA_STREAM, uri); + } } - activity.startActivity(intent); + int permissionFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION; + if (action.equals(Intent.ACTION_EDIT)) { + permissionFlags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION; + } + grantUriPermissions(intent, uri, permissionFlags); + + if (action.equals(Intent.ACTION_SEND)) { + Intent chooserIntent = Intent.createChooser(intent, null); + chooserIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + activity.startActivity(chooserIntent); + } else if (action.equals(Intent.ACTION_EDIT) || action.equals(Intent.ACTION_VIEW)) { + Intent chooserIntent = Intent.createChooser(intent, null); + chooserIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + if (action.equals(Intent.ACTION_EDIT)) { + chooserIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + } + activity.startActivity(chooserIntent); + } else { + activity.startActivity(intent); + } callback.success(uri.toString()); } catch (Exception e) { callback.error(e.getMessage()); @@ -1263,24 +1323,168 @@ private Uri getContentProviderUri(String fileUri) { } private Uri getContentProviderUri(String fileUri, String filename) { + if (fileUri == null || fileUri.isEmpty()) { + return null; + } + Uri uri = Uri.parse(fileUri); - String Id = context.getPackageName(); - if (fileUri.matches("file:///(.*)")) { - File file = new File(uri.getPath()); - if (filename.equals("")) { - return FileProvider.getUriForFile(context, Id + ".provider", file); + if (uri == null) { + return null; + } + + if ("file".equalsIgnoreCase(uri.getScheme())) { + File originalFile = new File(uri.getPath()); + if (!originalFile.exists()) { + Log.e("System", "File does not exist for URI: " + fileUri); + return null; } - return FileProvider.getUriForFile( - context, - Id + ".provider", - file, - filename - ); + String authority = getFileProviderAuthority(); + if (authority == null) { + Log.e("System", "No FileProvider authority available."); + return null; + } + + try { + return FileProvider.getUriForFile(context, authority, originalFile); + } catch (IllegalArgumentException | SecurityException ex) { + try { + File cacheCopy = ensureShareableCopy(originalFile, filename); + return FileProvider.getUriForFile(context, authority, cacheCopy); + } catch (Exception copyError) { + Log.e("System", "Failed to expose file via FileProvider", copyError); + return null; + } + } } return uri; } + private File ensureShareableCopy(File source, String displayName) throws IOException { + File cacheRoot = new File(context.getCacheDir(), "shared"); + if (!cacheRoot.exists() && !cacheRoot.mkdirs()) { + throw new IOException("Unable to create shared cache directory"); + } + + if (displayName != null && !displayName.isEmpty()) { + displayName = new File(displayName).getName(); + } + if (displayName == null || displayName.isEmpty()) { + displayName = source.getName(); + } + if (displayName == null || displayName.isEmpty()) { + displayName = "shared-file"; + } + + File target = new File(cacheRoot, displayName); + target = ensureUniqueFile(target); + copyFile(source, target); + return target; + } + + private File ensureUniqueFile(File target) { + if (!target.exists()) { + return target; + } + + String name = target.getName(); + String prefix = name; + String suffix = ""; + int dotIndex = name.lastIndexOf('.'); + if (dotIndex > 0) { + prefix = name.substring(0, dotIndex); + suffix = name.substring(dotIndex); + } + + int index = 1; + File candidate = target; + while (candidate.exists()) { + candidate = new File(target.getParentFile(), prefix + "-" + index + suffix); + index++; + } + return candidate; + } + + private void copyFile(File source, File destination) throws IOException { + try ( + InputStream in = new FileInputStream(source); + OutputStream out = new FileOutputStream(destination) + ) { + byte[] buffer = new byte[8192]; + int length; + while ((length = in.read(buffer)) != -1) { + out.write(buffer, 0, length); + } + out.flush(); + } + } + + private void grantUriPermissions(Intent intent, Uri uri, int flags) { + if (uri == null) return; + PackageManager pm = context.getPackageManager(); + List resInfoList = pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); + for (ResolveInfo resolveInfo: resInfoList) { + String packageName = resolveInfo.activityInfo.packageName; + context.grantUriPermission(packageName, uri, flags); + } + } + + private String resolveMimeType(String currentMime, Uri uri, String filename) { + if (currentMime != null && !currentMime.isEmpty() && !currentMime.equals("*/*")) { + return currentMime; + } + + String mime = null; + if (uri != null) { + mime = context.getContentResolver().getType(uri); + } + + if ((mime == null || mime.isEmpty()) && filename != null) { + mime = getMimeTypeFromExtension(filename); + } + + if ((mime == null || mime.isEmpty()) && uri != null) { + String path = uri.getPath(); + if (path != null) { + mime = getMimeTypeFromExtension(path); + } + } + + return (mime != null && !mime.isEmpty()) ? mime : "*/*"; + } + + private String getFileProviderAuthority() { + if (fileProviderAuthority != null && !fileProviderAuthority.isEmpty()) { + return fileProviderAuthority; + } + + try { + PackageManager pm = context.getPackageManager(); + PackageInfo packageInfo = pm.getPackageInfo( + context.getPackageName(), + PackageManager.GET_PROVIDERS + ); + if (packageInfo.providers != null) { + for (ProviderInfo providerInfo: packageInfo.providers) { + if ( + providerInfo != null && + providerInfo.name != null && + providerInfo.name.equals(FileProvider.class.getName()) + ) { + fileProviderAuthority = providerInfo.authority; + break; + } + } + } + } catch (PackageManager.NameNotFoundException ignored) {} + + if (fileProviderAuthority == null || fileProviderAuthority.isEmpty()) { + fileProviderAuthority = context.getPackageName() + ".provider"; + } + + return fileProviderAuthority; + } + private boolean isPackageInstalled( String packageName, PackageManager packageManager, From 7cdd2649f2a7e55ab8531d9a74c5779d58623b19 Mon Sep 17 00:00:00 2001 From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com> Date: Fri, 24 Oct 2025 14:37:23 +0530 Subject: [PATCH 2/2] fix: open with of file browser --- src/pages/fileBrowser/fileBrowser.js | 41 +++++++++++++++++++--------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/src/pages/fileBrowser/fileBrowser.js b/src/pages/fileBrowser/fileBrowser.js index cfe5c0675..f034f1c22 100644 --- a/src/pages/fileBrowser/fileBrowser.js +++ b/src/pages/fileBrowser/fileBrowser.js @@ -748,6 +748,20 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { }); } + async function getShareableUri(fileUrl) { + if (!fileUrl) return null; + try { + const fs = fsOperation(fileUrl); + if (/^s?ftp:/.test(fileUrl)) { + return fs.localName; + } + const stat = await fs.stat(); + return stat?.url || null; + } catch (error) { + return null; + } + } + async function contextMenuHandler() { if (appSettings.value.vibrateOnTap) { navigator.vibrate(constants.VIBRATION_TIME); @@ -824,19 +838,20 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { case "open_with": try { - let mimeType = mimeTypes.lookup(name || "text/plain"); - const fs = fsOperation(url); - if (/^s?ftp:/.test(url)) return fs.localName; - - system.fileAction( - (await fs.stat()).url, - name, - "VIEW", - mimeType, - () => { - toast(strings["no app found to handle this file"]); - }, - ); + const shareableUri = await getShareableUri(url); + if (!shareableUri) { + toast(strings["no app found to handle this file"]); + break; + } + + const mimeType = + mimeTypes.lookup(name) || + mimeTypes.lookup(shareableUri) || + "text/plain"; + + system.fileAction(shareableUri, name, "VIEW", mimeType, () => { + toast(strings["no app found to handle this file"]); + }); } catch (error) { console.error(error); toast(strings.error);