summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--java/org/gnu/emacs/EmacsService.java122
-rw-r--r--lisp/startup.el10
-rw-r--r--lisp/term/android-win.el86
-rw-r--r--src/android.c55
-rw-r--r--src/android.h4
-rw-r--r--src/androidfns.c38
6 files changed, 314 insertions, 1 deletions
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
@@ -339,5 +339,91 @@ the `stop-selecting-text' editing key."
(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);