From 669e754f5bdc9f9130a68eec6966babe9a85ecae Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sat, 18 Nov 2023 14:15:55 +0800 Subject: [PATCH] Offer to grant storage permissions if absent * java/org/gnu/emacs/EmacsService.java (externalStorageAvailable) (requestStorageAccess23, requestStorageAccess30) (requestStorageAccess): New functions. * lisp/startup.el (fancy-startup-tail, normal-splash-screen): Call android-win functions for inserting the new storage permission notice. * lisp/term/android-win.el (android-display-storage-permission-popup) (android-after-splash-screen): New functions. * src/android.c (android_init_emacs_service): Link to new Java functions. (android_external_storage_available_p) (android_request_storage_access): New functions. * src/android.h: Update prototypes. * src/androidfns.c (Fandroid_external_storage_available_p) (Fandroid_request_storage_access): New functions. (syms_of_androidfns): Register new subrs. --- java/org/gnu/emacs/EmacsService.java | 122 +++++++++++++++++++++++++++ lisp/startup.el | 10 ++- lisp/term/android-win.el | 86 +++++++++++++++++++ src/android.c | 55 ++++++++++++ src/android.h | 4 + src/androidfns.c | 38 +++++++++ 6 files changed, 314 insertions(+), 1 deletion(-) diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 5bd1dcc5a88..3cc37dd992d 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -63,6 +63,7 @@ import android.net.Uri; import android.os.BatteryManager; import android.os.Build; +import android.os.Environment; import android.os.Looper; import android.os.IBinder; import android.os.Handler; @@ -73,6 +74,7 @@ import android.os.VibrationEffect; import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; +import android.provider.Settings; import android.util.Log; import android.util.DisplayMetrics; @@ -1909,4 +1911,124 @@ public final class EmacsService extends Service return false; } + + + + /* Functions for detecting and requesting storage permissions. */ + + public boolean + externalStorageAvailable () + { + final String readPermission; + + readPermission = "android.permission.READ_EXTERNAL_STORAGE"; + + return (Build.VERSION.SDK_INT < Build.VERSION_CODES.R + ? (checkSelfPermission (readPermission) + == PackageManager.PERMISSION_GRANTED) + : Environment.isExternalStorageManager ()); + } + + private void + requestStorageAccess23 () + { + Runnable runnable; + + runnable = new Runnable () { + @Override + public void + run () + { + EmacsActivity activity; + String permission, permission1; + + permission = "android.permission.READ_EXTERNAL_STORAGE"; + permission1 = "android.permission.WRITE_EXTERNAL_STORAGE"; + + /* Find an activity that is entitled to display a permission + request dialog. */ + + if (EmacsActivity.focusedActivities.isEmpty ()) + { + /* If focusedActivities is empty then this dialog may + have been displayed immediately after another popup + dialog was dismissed. Try the EmacsActivity to be + focused. */ + + activity = EmacsActivity.lastFocusedActivity; + + if (activity == null) + { + /* Still no luck. Return failure. */ + return; + } + } + else + activity = EmacsActivity.focusedActivities.get (0); + + /* Now request these permissions. */ + activity.requestPermissions (new String[] { permission, + permission1, }, + 0); + } + }; + + runOnUiThread (runnable); + } + + private void + requestStorageAccess30 () + { + Runnable runnable; + final Intent intent; + + intent + = new Intent (Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, + Uri.parse ("package:org.gnu.emacs")); + + runnable = new Runnable () { + @Override + public void + run () + { + EmacsActivity activity; + + /* Find an activity that is entitled to display a permission + request dialog. */ + + if (EmacsActivity.focusedActivities.isEmpty ()) + { + /* If focusedActivities is empty then this dialog may + have been displayed immediately after another popup + dialog was dismissed. Try the EmacsActivity to be + focused. */ + + activity = EmacsActivity.lastFocusedActivity; + + if (activity == null) + { + /* Still no luck. Return failure. */ + return; + } + } + else + activity = EmacsActivity.focusedActivities.get (0); + + /* Now request these permissions. */ + + activity.startActivity (intent); + } + }; + + runOnUiThread (runnable); + } + + public void + requestStorageAccess () + { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) + requestStorageAccess23 (); + else + requestStorageAccess30 (); + } }; diff --git a/lisp/startup.el b/lisp/startup.el index 37843eab176..e40c316a8e8 100644 --- a/lisp/startup.el +++ b/lisp/startup.el @@ -2036,7 +2036,10 @@ a face or button specification." (call-interactively 'recover-session))) " to recover the files you were editing.")))) - + ;; Insert the permissions notice if the user has yet to grant Emacs + ;; storage permissions. + (when (fboundp 'android-after-splash-screen) + (funcall 'android-after-splash-screen t)) (when concise (fancy-splash-insert :face 'variable-pitch "\n" @@ -2238,6 +2241,11 @@ splash screen in another window." "type M-x recover-session RET\nto recover" " the files you were editing.\n")) + ;; Insert the permissions notice if the user has yet to grant + ;; Emacs storage permissions. + (when (fboundp 'android-after-splash-screen) + (funcall 'android-after-splash-screen nil)) + (use-local-map splash-screen-keymap) ;; Display the input that we set up in the buffer. diff --git a/lisp/term/android-win.el b/lisp/term/android-win.el index 7d9a033d723..bcf49da1225 100644 --- a/lisp/term/android-win.el +++ b/lisp/term/android-win.el @@ -338,6 +338,92 @@ the `stop-selecting-text' editing key." (global-set-key [start-selecting-text] 'set-mark-command) (global-set-key [stop-selecting-text] 'android-deactivate-mark-command) + +;; Splash screen notice. Users are frequently left scratching their +;; heads when they overlook the Android appendex in the Emacs manual +;; and discover that external storage is not accessible; worse yet, +;; Android 11 and later veil the settings panel controlling such +;; permissions behind layer upon layer of largely immaterial settings +;; panels, such that several modified copies of the Android Settings +;; app have omitted them altogether after their developers conducted +;; their own interface simplifications. Display a button on the +;; splash screen that instructs users on granting these permissions +;; when they are denied. + +(declare-function android-external-storage-available-p "androidfns.c") +(declare-function android-request-storage-access "androidfns.c") +(declare-function android-request-directory-access "androidfns.c") + +(defun android-display-storage-permission-popup (&optional _ignored) + "Display a dialog regarding storage permissions. +Display a buffer explaining the need for storage permissions and +offering to grant them." + (interactive) + (with-current-buffer (get-buffer-create "*Android Permissions*") + (setq buffer-read-only nil) + (erase-buffer) + (insert (propertize "Storage Access Permissions" + 'face '(bold (:height 1.2)))) + (insert " + +Before Emacs can access your device's external storage +directories, such as /sdcard and /storage/emulated/0, you must +grant it permission to do so. + +Alternatively, you can request access to a particular directory +in external storage, whereafter it will be available under the +directory /content/storage. + +") + (insert-button "Grant storage permissions" + 'action (lambda (_) + (android-request-storage-access) + (quit-window))) + (newline) + (newline) + (insert-button "Request access to directory" + 'action (lambda (_) + (android-request-directory-access))) + (newline) + (special-mode) + (setq buffer-read-only t)) + (let ((window (display-buffer "*Android Permissions*"))) + (when (windowp window) + (with-selected-window window + ;; Fill the text to the width of this window in columns if it + ;; does not exceed 72, that the text might not be wrapped or + ;; truncated. + (when (<= (window-width window) 72) + (let ((fill-column (window-width window)) + (inhibit-read-only t)) + (fill-region (point-min) (point-max)))))))) + +(defun android-after-splash-screen (fancy-p) + "Insert a brief notice on the absence of storage permissions. +If storage permissions are as yet denied to Emacs, insert a short +notice to that effect, followed by a button that enables the user +to grant such permissions. + +FANCY-P controls if the inserted notice should be displayed in a +variable space consequent on its being incorporated within the +fancy splash screen." + (unless (android-external-storage-available-p) + (if fancy-p + (fancy-splash-insert + :face '(variable-pitch + font-lock-function-call-face) + "\nPermissions necessary to access external storage directories have +been denied. Click " + :link '("here" android-display-storage-permission-popup) + " to grant them.") + (insert + "Permissions necessary to access external storage directories have been +denied. ") + (insert-button "Click here to grant them." + 'action #'android-display-storage-permission-popup + 'follow-link t) + (newline)))) + (provide 'android-win) ;; android-win.el ends here. diff --git a/src/android.c b/src/android.c index e116426ca05..7ca5eab817c 100644 --- a/src/android.c +++ b/src/android.c @@ -1628,6 +1628,10 @@ android_init_emacs_service (void) "Ljava/lang/String;)Ljava/lang/String;"); FIND_METHOD (valid_authority, "validAuthority", "(Ljava/lang/String;)Z"); + FIND_METHOD (external_storage_available, + "externalStorageAvailable", "()Z"); + FIND_METHOD (request_storage_access, + "requestStorageAccess", "()V"); #undef FIND_METHOD } @@ -6558,6 +6562,57 @@ android_request_directory_access (void) return rc; } +/* Return whether Emacs is entitled to access external storage. + + On Android 5.1 and earlier, such permissions as are declared within + an application's manifest are granted during installation and are + irrevocable. + + On Android 6.0 through Android 10.0, the right to read external + storage is a regular permission granted from the Permissions + panel. + + On Android 11.0 and later, that right must be granted through an + independent ``Special App Access'' settings panel. */ + +bool +android_external_storage_available_p (void) +{ + jboolean rc; + jmethodID method; + + if (android_api_level <= 22) /* LOLLIPOP_MR1 */ + return true; + + method = service_class.external_storage_available; + rc = (*android_java_env)->CallNonvirtualBooleanMethod (android_java_env, + emacs_service, + service_class.class, + method); + android_exception_check (); + + return rc; +} + +/* Display a dialog from which the aforementioned rights can be + granted. */ + +void +android_request_storage_access (void) +{ + jmethodID method; + + if (android_api_level <= 22) /* LOLLIPOP_MR1 */ + return; + + method = service_class.request_storage_access; + (*android_java_env)->CallNonvirtualVoidMethod (android_java_env, + emacs_service, + service_class.class, + method); + android_exception_check (); +} + /* The thread from which a query against a thread is currently being diff --git a/src/android.h b/src/android.h index 28d9d25930e..12f9472836f 100644 --- a/src/android.h +++ b/src/android.h @@ -123,6 +123,8 @@ extern void android_wait_event (void); extern void android_toggle_on_screen_keyboard (android_window, bool); extern _Noreturn void android_restart_emacs (void); extern int android_request_directory_access (void); +extern bool android_external_storage_available_p (void); +extern void android_request_storage_access (void); extern int android_get_current_api_level (void) __attribute__ ((pure)); @@ -289,6 +291,8 @@ struct android_emacs_service jmethodID rename_document; jmethodID move_document; jmethodID valid_authority; + jmethodID external_storage_available; + jmethodID request_storage_access; }; extern JNIEnv *android_java_env; diff --git a/src/androidfns.c b/src/androidfns.c index 772a4f51e78..785587d9282 100644 --- a/src/androidfns.c +++ b/src/androidfns.c @@ -3096,6 +3096,42 @@ within the directory `/content/storage'. */) +/* Functions concerning storage permissions. */ + +DEFUN ("android-external-storage-available-p", + Fandroid_external_storage_available_p, + Sandroid_external_storage_available_p, 0, 0, 0, + doc: /* Return whether Emacs is entitled to access external storage. +Return nil if the requisite permissions for external storage access +have not been granted to Emacs, t otherwise. Such permissions can be +requested by means of the `android-request-storage-access' +command. + +External storage on Android encompasses the `/sdcard' and +`/storage/emulated' directories, access to which is denied to programs +absent these permissions. */) + (void) +{ + return android_external_storage_available_p () ? Qt : Qnil; +} + +DEFUN ("android-request-storage-access", Fandroid_request_storage_access, + Sandroid_request_storage_access, 0, 0, "", + doc: /* Request rights to access external storage. + +Return nil whether access is accorded or not, immediately subsequent +to displaying the permissions request dialog. + +`android-external-storage-available-p' (which see) ascertains if Emacs +has received such rights. */) + (void) +{ + android_request_storage_access (); + return Qnil; +} + + + /* Miscellaneous input method related stuff. */ /* Report X, Y, by the phys cursor width and height as the cursor @@ -3302,6 +3338,8 @@ bell being rung. */); #ifndef ANDROID_STUBIFY defsubr (&Sandroid_query_battery); defsubr (&Sandroid_request_directory_access); + defsubr (&Sandroid_external_storage_available_p); + defsubr (&Sandroid_request_storage_access); tip_timer = Qnil; staticpro (&tip_timer); -- 2.39.2