Skip to content

Commit 4e8da1b

Browse files
authored
Merge branch 'Acode-Foundation:main' into ai-test
2 parents fa65877 + d17459d commit 4e8da1b

File tree

3 files changed

+248
-29
lines changed

3 files changed

+248
-29
lines changed

.github/workflows/close-inactive-issues.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
2525
days-before-pr-stale: -1
2626
days-before-pr-close: -1
27-
exempt-issue-labels: "new plugin idea, todo"
27+
exempt-issue-labels: "new plugin idea, todo, enhancement, bug, Low priority, documentation"
2828
operations-per-run: 100
2929
repo-token: ${{ secrets.GITHUB_TOKEN }}
3030

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)