Skip to content

Commit 61f16de

Browse files
All file access permission for terminal (#1426)
* feat. all file access permission for terminal * format * feat. added script to toggle permission * remove declaration
1 parent 2b0c0eb commit 61f16de

File tree

8 files changed

+341
-115
lines changed

8 files changed

+341
-115
lines changed

config.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@
6868
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
6969
<uses-permission android:name="android.permission.WRITE_MEDIA_STORAGE" />
7070
<uses-permission android:name="android.permission.VIBRATE" />
71-
<!-- <uses-permission android:name="com.termux.permission.RUN_COMMAND" /> -->
7271
</config-file>
7372

7473
<hook type="before_prepare" src="hooks/modify-java-files.js" />

src/lang/en-us.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@
126126
"animation": "Animation",
127127
"backup": "Backup",
128128
"restore": "Restore",
129+
"allFileAccess": "All file access",
129130
"backup successful": "Backup successful",
130131
"invalid backup file": "Invalid backup file",
131132
"add path": "Add path",

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

Lines changed: 179 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,19 @@
7777
import java.io.OutputStream;
7878
import java.io.IOException;
7979

80+
import android.os.Build;
81+
import android.os.Environment;
82+
import android.Manifest;
83+
import android.content.Context;
84+
import android.content.Intent;
85+
import android.content.pm.PackageInfo;
86+
import android.content.pm.PackageManager;
87+
import android.net.Uri;
88+
import android.provider.Settings;
89+
90+
import androidx.core.content.ContextCompat;
91+
92+
8093

8194
public class System extends CordovaPlugin {
8295

@@ -240,6 +253,65 @@ public void run() {
240253

241254
callbackContext.success(arch);
242255
return true;
256+
257+
case "requestStorageManager":
258+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
259+
try {
260+
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
261+
intent.setData(Uri.parse("package:" + context.getPackageName()));
262+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
263+
context.startActivity(intent);
264+
callbackContext.success("true");
265+
} catch (Exception e) {
266+
// Fallback to general settings if specific one fails
267+
Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
268+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
269+
context.startActivity(intent);
270+
callbackContext.success("true");
271+
}
272+
} else {
273+
callbackContext.success("false"); // Not needed on Android < 11
274+
}
275+
return true;
276+
277+
278+
case "hasGrantedStorageManager":
279+
boolean granted;
280+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
281+
granted = Environment.isExternalStorageManager();
282+
} else {
283+
// Fallback for Android 10 and below
284+
granted = ContextCompat.checkSelfPermission(
285+
context,
286+
Manifest.permission.READ_EXTERNAL_STORAGE
287+
) == PackageManager.PERMISSION_GRANTED;
288+
}
289+
callbackContext.success(String.valueOf(granted));
290+
return true;
291+
292+
case "isManageExternalStorageDeclared":
293+
PackageManager pm = context.getPackageManager();
294+
try {
295+
PackageInfo info = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS);
296+
String[] permissions = info.requestedPermissions;
297+
String isDeclared = "false";
298+
299+
if (permissions != null) {
300+
for (String perm: permissions) {
301+
if (perm.equals("android.permission.MANAGE_EXTERNAL_STORAGE")) {
302+
isDeclared = "true";
303+
break;
304+
}
305+
}
306+
}
307+
callbackContext.success(isDeclared);
308+
309+
} catch (PackageManager.NameNotFoundException e) {
310+
e.printStackTrace();
311+
callbackContext.error(e.toString());
312+
}
313+
314+
return true;
243315
case "mkdirs":
244316
File file = new File(args.getString(0));
245317
if (file.mkdirs()) {
@@ -260,106 +332,104 @@ public void run() {
260332
public void run() {
261333
switch (action) {
262334
case "copyToUri":
263-
try {
264-
//srcUri is a file
265-
Uri srcUri = Uri.parse(args.getString(0));
266-
267-
//destUri is a directory
268-
Uri destUri = Uri.parse(args.getString(1));
269-
270-
//create a file named this into the dest Directory and copy the srcUri into it
271-
String fileName = args.getString(2);
272-
273-
InputStream in = null;
274-
OutputStream out = null;
275-
try {
276-
// Open input stream from the source URI
277-
if ("file".equalsIgnoreCase(srcUri.getScheme())) {
278-
File file = new File(srcUri.getPath());
279-
in = new FileInputStream(file);
280-
} else {
281-
in = context.getContentResolver().openInputStream(srcUri);
282-
}
283-
284-
// Create the destination file using DocumentFile for better URI handling
285-
DocumentFile destFile = null;
286-
287-
if ("file".equalsIgnoreCase(destUri.getScheme())) {
288-
// Handle file:// scheme using DocumentFile
289-
File destDir = new File(destUri.getPath());
290-
if (!destDir.exists()) {
291-
destDir.mkdirs(); // Create directory if it doesn't exist
292-
}
293-
DocumentFile destDocDir = DocumentFile.fromFile(destDir);
294-
295-
// Check if file already exists and delete it
296-
DocumentFile existingFile = destDocDir.findFile(fileName);
297-
if (existingFile != null && existingFile.exists()) {
298-
existingFile.delete();
299-
}
300-
301-
// Create new file
302-
String mimeType = getMimeTypeFromExtension(fileName);
303-
destFile = destDocDir.createFile(mimeType, fileName);
304-
} else {
305-
// Handle content:// scheme using DocumentFile
306-
DocumentFile destDocDir = DocumentFile.fromTreeUri(context, destUri);
307-
308-
if (destDocDir == null || !destDocDir.exists() || !destDocDir.isDirectory()) {
309-
callbackContext.error("Destination directory does not exist or is not accessible");
310-
return;
311-
}
312-
313-
// Check if file already exists and delete it
314-
DocumentFile existingFile = destDocDir.findFile(fileName);
315-
if (existingFile != null && existingFile.exists()) {
316-
existingFile.delete();
317-
}
318-
319-
// Create new file
320-
String mimeType = getMimeTypeFromExtension(fileName);
321-
destFile = destDocDir.createFile(mimeType, fileName);
322-
}
323-
324-
if (destFile == null || !destFile.exists()) {
325-
callbackContext.error("Failed to create destination file");
326-
return;
327-
}
328-
329-
// Open output stream to the created file
330-
out = context.getContentResolver().openOutputStream(destFile.getUri());
331-
332-
if (in == null || out == null) {
333-
callbackContext.error("uri streams are null");
334-
return;
335-
}
336-
337-
// Copy stream
338-
byte[] buffer = new byte[8192];
339-
int len;
340-
while ((len = in.read(buffer)) > 0) {
341-
out.write(buffer, 0, len);
342-
}
343-
344-
out.flush();
345-
callbackContext.success();
346-
} catch (IOException e) {
347-
e.printStackTrace();
348-
callbackContext.error(e.toString());
349-
} finally {
350-
try {
351-
if (in != null) in.close();
352-
if (out != null) out.close();
353-
} catch (IOException e) {
354-
e.printStackTrace();
355-
callbackContext.error(e.toString());
356-
}
357-
}
358-
} catch (Exception e) {
359-
e.printStackTrace();
360-
callbackContext.error(e.toString());
361-
}
362-
break;
335+
try {
336+
//srcUri is a file
337+
Uri srcUri = Uri.parse(args.getString(0));
338+
339+
//destUri is a directory
340+
Uri destUri = Uri.parse(args.getString(1));
341+
342+
//create a file named this into the dest Directory and copy the srcUri into it
343+
String fileName = args.getString(2);
344+
345+
InputStream in = null;
346+
OutputStream out = null;
347+
try {
348+
// Open input stream from the source URI
349+
if ("file".equalsIgnoreCase(srcUri.getScheme())) {
350+
File file = new File(srcUri.getPath()); in = new FileInputStream(file);
351+
} else { in = context.getContentResolver().openInputStream(srcUri);
352+
}
353+
354+
// Create the destination file using DocumentFile for better URI handling
355+
DocumentFile destFile = null;
356+
357+
if ("file".equalsIgnoreCase(destUri.getScheme())) {
358+
// Handle file:// scheme using DocumentFile
359+
File destDir = new File(destUri.getPath());
360+
if (!destDir.exists()) {
361+
destDir.mkdirs(); // Create directory if it doesn't exist
362+
}
363+
DocumentFile destDocDir = DocumentFile.fromFile(destDir);
364+
365+
// Check if file already exists and delete it
366+
DocumentFile existingFile = destDocDir.findFile(fileName);
367+
if (existingFile != null && existingFile.exists()) {
368+
existingFile.delete();
369+
}
370+
371+
// Create new file
372+
String mimeType = getMimeTypeFromExtension(fileName);
373+
destFile = destDocDir.createFile(mimeType, fileName);
374+
} else {
375+
// Handle content:// scheme using DocumentFile
376+
DocumentFile destDocDir = DocumentFile.fromTreeUri(context, destUri);
377+
378+
if (destDocDir == null || !destDocDir.exists() || !destDocDir.isDirectory()) {
379+
callbackContext.error("Destination directory does not exist or is not accessible");
380+
return;
381+
}
382+
383+
// Check if file already exists and delete it
384+
DocumentFile existingFile = destDocDir.findFile(fileName);
385+
if (existingFile != null && existingFile.exists()) {
386+
existingFile.delete();
387+
}
388+
389+
// Create new file
390+
String mimeType = getMimeTypeFromExtension(fileName);
391+
destFile = destDocDir.createFile(mimeType, fileName);
392+
}
393+
394+
if (destFile == null || !destFile.exists()) {
395+
callbackContext.error("Failed to create destination file");
396+
return;
397+
}
398+
399+
// Open output stream to the created file
400+
out = context.getContentResolver().openOutputStream(destFile.getUri());
401+
402+
if ( in == null || out == null) {
403+
callbackContext.error("uri streams are null");
404+
return;
405+
}
406+
407+
// Copy stream
408+
byte[] buffer = new byte[8192];
409+
int len;
410+
while ((len = in .read(buffer)) > 0) {
411+
out.write(buffer, 0, len);
412+
}
413+
414+
out.flush();
415+
callbackContext.success();
416+
} catch (IOException e) {
417+
e.printStackTrace();
418+
callbackContext.error(e.toString());
419+
} finally {
420+
try {
421+
if ( in != null) in .close();
422+
if (out != null) out.close();
423+
} catch (IOException e) {
424+
e.printStackTrace();
425+
callbackContext.error(e.toString());
426+
}
427+
}
428+
} catch (Exception e) {
429+
e.printStackTrace();
430+
callbackContext.error(e.toString());
431+
}
432+
break;
363433
case "get-webkit-info":
364434
getWebkitInfo(callbackContext);
365435
break;
@@ -445,16 +515,16 @@ public void run() {
445515
}
446516

447517
// Helper method to determine MIME type using Android's built-in MimeTypeMap
448-
private String getMimeTypeFromExtension(String fileName) {
449-
String extension = "";
450-
int lastDotIndex = fileName.lastIndexOf('.');
451-
if (lastDotIndex > 0 && lastDotIndex < fileName.length() - 1) {
452-
extension = fileName.substring(lastDotIndex + 1).toLowerCase();
518+
private String getMimeTypeFromExtension(String fileName) {
519+
String extension = "";
520+
int lastDotIndex = fileName.lastIndexOf('.');
521+
if (lastDotIndex > 0 && lastDotIndex < fileName.length() - 1) {
522+
extension = fileName.substring(lastDotIndex + 1).toLowerCase();
523+
}
524+
525+
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
526+
return mimeType != null ? mimeType : "application/octet-stream";
453527
}
454-
455-
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
456-
return mimeType != null ? mimeType : "application/octet-stream";
457-
}
458528

459529
private void getConfiguration(CallbackContext callback) {
460530
try {

src/plugins/system/www/plugin.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
module.exports = {
2+
isManageExternalStorageDeclared: function (success, error) {
3+
cordova.exec(success, error, 'System', 'isManageExternalStorageDeclared', []);
4+
},
5+
hasGrantedStorageManager: function (success, error) {
6+
cordova.exec(success, error, 'System', 'hasGrantedStorageManager', []);
7+
},
8+
requestStorageManager: function (success, error) {
9+
cordova.exec(success, error, 'System', 'requestStorageManager', []);
10+
},
211
copyToUri: function (srcUri,destUri,fileName, success, error) {
312
cordova.exec(success, error, 'System', 'copyToUri', [srcUri,destUri,fileName]);
413
},

src/plugins/terminal/plugin.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@
2323

2424
<source-file src="scripts/init-sandbox.sh" target-dir="assets"/>
2525
<source-file src="scripts/init-alpine.sh" target-dir="assets"/>
26+
27+
<config-file target="AndroidManifest.xml" parent="/manifest">
28+
29+
</config-file>
30+
2631

2732
</platform>
2833
</plugin>

src/settings/terminalSettings.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ export default function terminalSettings() {
2727
const terminalValues = values.terminalSettings;
2828

2929
const items = [
30+
{
31+
key: "all_file_access",
32+
text: strings.allFileAccess.capitalize(),
33+
info: "Enable access of /sdcard and /storage in terminal",
34+
},
3035
{
3136
key: "fontSize",
3237
text: strings["font size"],
@@ -179,6 +184,20 @@ export default function terminalSettings() {
179184
*/
180185
function callback(key, value) {
181186
switch (key) {
187+
case "all_file_access":
188+
if (ANDROID_SDK_INT >= 30) {
189+
system.isManageExternalStorageDeclared((boolStr) => {
190+
if (boolStr === "true") {
191+
system.requestStorageManager(console.log, console.error);
192+
} else {
193+
alert("This feature is not available.");
194+
}
195+
}, alert);
196+
} else {
197+
alert("This feature is not available.");
198+
}
199+
200+
return;
182201
case "backup":
183202
terminalBackup();
184203
return;

0 commit comments

Comments
 (0)