]> git.eshelyaron.com Git - emacs.git/commitdiff
Offer to grant storage permissions if absent
authorPo Lu <luangruo@yahoo.com>
Sat, 18 Nov 2023 06:15:55 +0000 (14:15 +0800)
committerPo Lu <luangruo@yahoo.com>
Sat, 18 Nov 2023 06:15:55 +0000 (14:15 +0800)
* 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
lisp/startup.el
lisp/term/android-win.el
src/android.c
src/android.h
src/androidfns.c

index 5bd1dcc5a88269c43cd9565c124fb958be1daf5a..3cc37dd992dabc6efce86cb6d46aab4b5f46562a 100644 (file)
@@ -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;
   }
+
+\f
+
+  /* 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 ();
+  }
 };
index 37843eab176fa2355ebd59f8a24e002fd87ea760..e40c316a8e8161f093aa6b0138dd6a55ecfb0465 100644 (file)
@@ -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.
index 7d9a033d72375731782b86f79222cb7c108e8825..bcf49da12254f67984e32e7a1d4a4976712f589a 100644 (file)
@@ -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)
 
+\f
+;; 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))))
+
 \f
 (provide 'android-win)
 ;; android-win.el ends here.
index e116426ca055edc893e30a11ea05d016d9d0edbe..7ca5eab817ce61c499e21463fb9add52dd766394 100644 (file)
@@ -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 ();
+}
+
 \f
 
 /* The thread from which a query against a thread is currently being
index 28d9d25930e2aa030ed1b6067dcc7dbd0b4c00ff..12f9472836ff18df024c5a4df79f85ad3f9c5d14 100644 (file)
@@ -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;
index 772a4f51e78ebbd3a8a7a54d8c5f1710d6f0388f..785587d92821c0e98893338edec9c77a6848e684 100644 (file)
@@ -3096,6 +3096,42 @@ within the directory `/content/storage'.  */)
 
 \f
 
+/* 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;
+}
+
+\f
+
 /* 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);