Skip to content

Commit 4072ce2

Browse files
authored
fix: improve file sharing and URI handling (#1662)
* 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. * fix: open with of file browser
1 parent 54b06e7 commit 4072ce2

File tree

2 files changed

+247
-28
lines changed

2 files changed

+247
-28
lines changed

src/pages/fileBrowser/fileBrowser.js

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,20 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
748748
});
749749
}
750750

751+
async function getShareableUri(fileUrl) {
752+
if (!fileUrl) return null;
753+
try {
754+
const fs = fsOperation(fileUrl);
755+
if (/^s?ftp:/.test(fileUrl)) {
756+
return fs.localName;
757+
}
758+
const stat = await fs.stat();
759+
return stat?.url || null;
760+
} catch (error) {
761+
return null;
762+
}
763+
}
764+
751765
async function contextMenuHandler() {
752766
if (appSettings.value.vibrateOnTap) {
753767
navigator.vibrate(constants.VIBRATION_TIME);
@@ -824,19 +838,20 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) {
824838

825839
case "open_with":
826840
try {
827-
let mimeType = mimeTypes.lookup(name || "text/plain");
828-
const fs = fsOperation(url);
829-
if (/^s?ftp:/.test(url)) return fs.localName;
830-
831-
system.fileAction(
832-
(await fs.stat()).url,
833-
name,
834-
"VIEW",
835-
mimeType,
836-
() => {
837-
toast(strings["no app found to handle this file"]);
838-
},
839-
);
841+
const shareableUri = await getShareableUri(url);
842+
if (!shareableUri) {
843+
toast(strings["no app found to handle this file"]);
844+
break;
845+
}
846+
847+
const mimeType =
848+
mimeTypes.lookup(name) ||
849+
mimeTypes.lookup(shareableUri) ||
850+
"text/plain";
851+
852+
system.fileAction(shareableUri, name, "VIEW", mimeType, () => {
853+
toast(strings["no app found to handle this file"]);
854+
});
840855
} catch (error) {
841856
console.error(error);
842857
toast(strings.error);

src/plugins/system/android/com/foxdebug/system/System.java

Lines changed: 219 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ public class System extends CordovaPlugin {
104104
private Theme theme;
105105
private CallbackContext intentHandler;
106106
private CordovaWebView webView;
107+
private String fileProviderAuthority;
107108

108109
public void initialize(CordovaInterface cordova, CordovaWebView webView) {
109110
super.initialize(cordova, webView);
@@ -879,20 +880,46 @@ private void fileAction(
879880
) {
880881
Activity activity = this.activity;
881882
Context context = this.context;
882-
Uri uri = this.getContentProviderUri(fileURI);
883+
Uri uri = this.getContentProviderUri(fileURI, filename);
884+
if (uri == null) {
885+
callback.error("Unable to access file for action " + action);
886+
return;
887+
}
883888
try {
884889
Intent intent = new Intent(action);
885890

886891
if (mimeType.equals("")) {
887892
mimeType = "text/plain";
888893
}
889894

895+
mimeType = resolveMimeType(mimeType, uri, filename);
896+
897+
String clipLabel = null;
898+
if (filename != null && !filename.isEmpty()) {
899+
clipLabel = new File(filename).getName();
900+
}
901+
if (clipLabel == null || clipLabel.isEmpty()) {
902+
clipLabel = uri.getLastPathSegment();
903+
}
904+
if (clipLabel == null || clipLabel.isEmpty()) {
905+
clipLabel = "shared-file";
906+
}
890907
if (action.equals(Intent.ACTION_SEND)) {
908+
intent.setType(mimeType);
909+
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
910+
intent.setClipData(
911+
ClipData.newUri(
912+
context.getContentResolver(),
913+
clipLabel,
914+
uri
915+
)
916+
);
891917
intent.putExtra(Intent.EXTRA_STREAM, uri);
892-
if (!filename.equals("")) {
918+
intent.putExtra(Intent.EXTRA_TITLE, clipLabel);
919+
intent.putExtra(Intent.EXTRA_SUBJECT, clipLabel);
920+
if (filename != null && !filename.isEmpty()) {
893921
intent.putExtra(Intent.EXTRA_TEXT, filename);
894922
}
895-
intent.setType(mimeType);
896923
} else {
897924
int flags =
898925
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION |
@@ -904,9 +931,42 @@ private void fileAction(
904931

905932
intent.setFlags(flags);
906933
intent.setDataAndType(uri, mimeType);
934+
intent.setClipData(
935+
ClipData.newUri(
936+
context.getContentResolver(),
937+
clipLabel,
938+
uri
939+
)
940+
);
941+
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
942+
if (!clipLabel.equals("shared-file")) {
943+
intent.putExtra(Intent.EXTRA_TITLE, clipLabel);
944+
}
945+
if (action.equals(Intent.ACTION_EDIT)) {
946+
intent.putExtra(Intent.EXTRA_STREAM, uri);
947+
}
907948
}
908949

909-
activity.startActivity(intent);
950+
int permissionFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION;
951+
if (action.equals(Intent.ACTION_EDIT)) {
952+
permissionFlags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
953+
}
954+
grantUriPermissions(intent, uri, permissionFlags);
955+
956+
if (action.equals(Intent.ACTION_SEND)) {
957+
Intent chooserIntent = Intent.createChooser(intent, null);
958+
chooserIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
959+
activity.startActivity(chooserIntent);
960+
} else if (action.equals(Intent.ACTION_EDIT) || action.equals(Intent.ACTION_VIEW)) {
961+
Intent chooserIntent = Intent.createChooser(intent, null);
962+
chooserIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
963+
if (action.equals(Intent.ACTION_EDIT)) {
964+
chooserIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
965+
}
966+
activity.startActivity(chooserIntent);
967+
} else {
968+
activity.startActivity(intent);
969+
}
910970
callback.success(uri.toString());
911971
} catch (Exception e) {
912972
callback.error(e.getMessage());
@@ -1263,24 +1323,168 @@ private Uri getContentProviderUri(String fileUri) {
12631323
}
12641324

12651325
private Uri getContentProviderUri(String fileUri, String filename) {
1326+
if (fileUri == null || fileUri.isEmpty()) {
1327+
return null;
1328+
}
1329+
12661330
Uri uri = Uri.parse(fileUri);
1267-
String Id = context.getPackageName();
1268-
if (fileUri.matches("file:///(.*)")) {
1269-
File file = new File(uri.getPath());
1270-
if (filename.equals("")) {
1271-
return FileProvider.getUriForFile(context, Id + ".provider", file);
1331+
if (uri == null) {
1332+
return null;
1333+
}
1334+
1335+
if ("file".equalsIgnoreCase(uri.getScheme())) {
1336+
File originalFile = new File(uri.getPath());
1337+
if (!originalFile.exists()) {
1338+
Log.e("System", "File does not exist for URI: " + fileUri);
1339+
return null;
12721340
}
12731341

1274-
return FileProvider.getUriForFile(
1275-
context,
1276-
Id + ".provider",
1277-
file,
1278-
filename
1279-
);
1342+
String authority = getFileProviderAuthority();
1343+
if (authority == null) {
1344+
Log.e("System", "No FileProvider authority available.");
1345+
return null;
1346+
}
1347+
1348+
try {
1349+
return FileProvider.getUriForFile(context, authority, originalFile);
1350+
} catch (IllegalArgumentException | SecurityException ex) {
1351+
try {
1352+
File cacheCopy = ensureShareableCopy(originalFile, filename);
1353+
return FileProvider.getUriForFile(context, authority, cacheCopy);
1354+
} catch (Exception copyError) {
1355+
Log.e("System", "Failed to expose file via FileProvider", copyError);
1356+
return null;
1357+
}
1358+
}
12801359
}
12811360
return uri;
12821361
}
12831362

1363+
private File ensureShareableCopy(File source, String displayName) throws IOException {
1364+
File cacheRoot = new File(context.getCacheDir(), "shared");
1365+
if (!cacheRoot.exists() && !cacheRoot.mkdirs()) {
1366+
throw new IOException("Unable to create shared cache directory");
1367+
}
1368+
1369+
if (displayName != null && !displayName.isEmpty()) {
1370+
displayName = new File(displayName).getName();
1371+
}
1372+
if (displayName == null || displayName.isEmpty()) {
1373+
displayName = source.getName();
1374+
}
1375+
if (displayName == null || displayName.isEmpty()) {
1376+
displayName = "shared-file";
1377+
}
1378+
1379+
File target = new File(cacheRoot, displayName);
1380+
target = ensureUniqueFile(target);
1381+
copyFile(source, target);
1382+
return target;
1383+
}
1384+
1385+
private File ensureUniqueFile(File target) {
1386+
if (!target.exists()) {
1387+
return target;
1388+
}
1389+
1390+
String name = target.getName();
1391+
String prefix = name;
1392+
String suffix = "";
1393+
int dotIndex = name.lastIndexOf('.');
1394+
if (dotIndex > 0) {
1395+
prefix = name.substring(0, dotIndex);
1396+
suffix = name.substring(dotIndex);
1397+
}
1398+
1399+
int index = 1;
1400+
File candidate = target;
1401+
while (candidate.exists()) {
1402+
candidate = new File(target.getParentFile(), prefix + "-" + index + suffix);
1403+
index++;
1404+
}
1405+
return candidate;
1406+
}
1407+
1408+
private void copyFile(File source, File destination) throws IOException {
1409+
try (
1410+
InputStream in = new FileInputStream(source);
1411+
OutputStream out = new FileOutputStream(destination)
1412+
) {
1413+
byte[] buffer = new byte[8192];
1414+
int length;
1415+
while ((length = in.read(buffer)) != -1) {
1416+
out.write(buffer, 0, length);
1417+
}
1418+
out.flush();
1419+
}
1420+
}
1421+
1422+
private void grantUriPermissions(Intent intent, Uri uri, int flags) {
1423+
if (uri == null) return;
1424+
PackageManager pm = context.getPackageManager();
1425+
List<ResolveInfo> resInfoList = pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
1426+
for (ResolveInfo resolveInfo: resInfoList) {
1427+
String packageName = resolveInfo.activityInfo.packageName;
1428+
context.grantUriPermission(packageName, uri, flags);
1429+
}
1430+
}
1431+
1432+
private String resolveMimeType(String currentMime, Uri uri, String filename) {
1433+
if (currentMime != null && !currentMime.isEmpty() && !currentMime.equals("*/*")) {
1434+
return currentMime;
1435+
}
1436+
1437+
String mime = null;
1438+
if (uri != null) {
1439+
mime = context.getContentResolver().getType(uri);
1440+
}
1441+
1442+
if ((mime == null || mime.isEmpty()) && filename != null) {
1443+
mime = getMimeTypeFromExtension(filename);
1444+
}
1445+
1446+
if ((mime == null || mime.isEmpty()) && uri != null) {
1447+
String path = uri.getPath();
1448+
if (path != null) {
1449+
mime = getMimeTypeFromExtension(path);
1450+
}
1451+
}
1452+
1453+
return (mime != null && !mime.isEmpty()) ? mime : "*/*";
1454+
}
1455+
1456+
private String getFileProviderAuthority() {
1457+
if (fileProviderAuthority != null && !fileProviderAuthority.isEmpty()) {
1458+
return fileProviderAuthority;
1459+
}
1460+
1461+
try {
1462+
PackageManager pm = context.getPackageManager();
1463+
PackageInfo packageInfo = pm.getPackageInfo(
1464+
context.getPackageName(),
1465+
PackageManager.GET_PROVIDERS
1466+
);
1467+
if (packageInfo.providers != null) {
1468+
for (ProviderInfo providerInfo: packageInfo.providers) {
1469+
if (
1470+
providerInfo != null &&
1471+
providerInfo.name != null &&
1472+
providerInfo.name.equals(FileProvider.class.getName())
1473+
) {
1474+
fileProviderAuthority = providerInfo.authority;
1475+
break;
1476+
}
1477+
}
1478+
}
1479+
} catch (PackageManager.NameNotFoundException ignored) {}
1480+
1481+
if (fileProviderAuthority == null || fileProviderAuthority.isEmpty()) {
1482+
fileProviderAuthority = context.getPackageName() + ".provider";
1483+
}
1484+
1485+
return fileProviderAuthority;
1486+
}
1487+
12841488
private boolean isPackageInstalled(
12851489
String packageName,
12861490
PackageManager packageManager,

0 commit comments

Comments
 (0)