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.
This commit is contained in:
Po Lu 2023-11-18 14:15:55 +08:00
parent 05213345c0
commit 669e754f5b
6 changed files with 314 additions and 1 deletions

View file

@ -63,6 +63,7 @@
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.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.provider.Settings;
import android.util.Log;
import android.util.DisplayMetrics;
@ -1909,4 +1911,124 @@ In addition, arbitrary runtime exceptions (such as
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 ();
}
};

View 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.

View 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)
;; 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.

View 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 ();
}
/* The thread from which a query against a thread is currently being

View 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;

View file

@ -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);