Skip to content

Commit 588d65d

Browse files
authored
Merge pull request kivy#1818 from gbm001/gbm_permissions_callback
Allows registering the onRequestPermissionsResult callback. Adds an interface in PythonActivity and a method to register a Python function which will be called when the onRequestPermissionsResult callback is received. In android/permissions.py, a new function 'register_permissions_callback' is added to register a Python function (that takes three arguments) which will receive the three arguments of onRequestPermissionsResult.
2 parents 40b1164 + 76b947c commit 588d65d

File tree

2 files changed

+194
-12
lines changed

2 files changed

+194
-12
lines changed

pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/PythonActivity.java

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,34 @@ public void onWindowFocusChanged(boolean hasFocus) {
608608
// call native function (since it's not yet loaded)
609609
}
610610
considerLoadingScreenRemoval();
611-
}
611+
}
612+
613+
/**
614+
* Used by android.permissions p4a module to register a call back after
615+
* requesting runtime permissions
616+
**/
617+
public interface PermissionsCallback {
618+
void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults);
619+
}
620+
621+
private PermissionsCallback permissionCallback;
622+
private boolean havePermissionsCallback = false;
623+
624+
public void addPermissionsCallback(PermissionsCallback callback) {
625+
permissionCallback = callback;
626+
havePermissionsCallback = true;
627+
Log.v(TAG, "addPermissionsCallback(): Added callback for onRequestPermissionsResult");
628+
}
629+
630+
@Override
631+
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
632+
Log.v(TAG, "onRequestPermissionsResult()");
633+
if (havePermissionsCallback) {
634+
Log.v(TAG, "onRequestPermissionsResult passed to callback");
635+
permissionCallback.onRequestPermissionsResult(requestCode, permissions, grantResults);
636+
}
637+
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
638+
}
612639

613640
/**
614641
* Used by android.permissions p4a module to check a permission
@@ -619,9 +646,9 @@ public boolean checkCurrentPermission(String permission) {
619646

620647
try {
621648
java.lang.reflect.Method methodCheckPermission =
622-
Activity.class.getMethod("checkSelfPermission", java.lang.String.class);
649+
Activity.class.getMethod("checkSelfPermission", java.lang.String.class);
623650
Object resultObj = methodCheckPermission.invoke(this, permission);
624-
int result = Integer.parseInt(resultObj.toString());
651+
int result = Integer.parseInt(resultObj.toString());
625652
if (result == PackageManager.PERMISSION_GRANTED)
626653
return true;
627654
} catch (IllegalAccessException | NoSuchMethodException |
@@ -633,16 +660,20 @@ public boolean checkCurrentPermission(String permission) {
633660
/**
634661
* Used by android.permissions p4a module to request runtime permissions
635662
**/
636-
public void requestPermissions(String[] permissions) {
663+
public void requestPermissionsWithRequestCode(String[] permissions, int requestCode) {
637664
if (android.os.Build.VERSION.SDK_INT < 23)
638665
return;
639666
try {
640667
java.lang.reflect.Method methodRequestPermission =
641668
Activity.class.getMethod("requestPermissions",
642-
java.lang.String[].class, int.class);
643-
methodRequestPermission.invoke(this, permissions, 1);
669+
java.lang.String[].class, int.class);
670+
methodRequestPermission.invoke(this, permissions, requestCode);
644671
} catch (IllegalAccessException | NoSuchMethodException |
645672
InvocationTargetException e) {
646673
}
647674
}
675+
676+
public void requestPermissions(String[] permissions) {
677+
requestPermissionsWithRequestCode(permissions, 1);
678+
}
648679
}

pythonforandroid/recipes/android/src/android/permissions.py

Lines changed: 157 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import threading
12

23
try:
3-
from jnius import autoclass
4+
from jnius import autoclass, PythonJavaClass, java_method
45
except ImportError:
56
# To allow importing by build/manifest-creating code without
67
# pyjnius being present:
@@ -422,16 +423,166 @@ class Permission:
422423
)
423424

424425

425-
def request_permissions(permissions):
426-
python_activity = autoclass('org.kivy.android.PythonActivity')
427-
python_activity.requestPermissions(permissions)
426+
PERMISSION_GRANTED = 0
427+
PERMISSION_DENIED = -1
428+
429+
430+
class _onRequestPermissionsCallback(PythonJavaClass):
431+
"""Callback class for registering a Python callback from
432+
onRequestPermissionsResult in PythonActivity.
433+
"""
434+
__javainterfaces__ = ['org.kivy.android.PythonActivity$PermissionsCallback']
435+
__javacontext__ = 'app'
436+
437+
def __init__(self, func):
438+
self.func = func
439+
super().__init__()
440+
441+
@java_method('(I[Ljava/lang/String;[I)V')
442+
def onRequestPermissionsResult(self, requestCode,
443+
permissions, grantResults):
444+
self.func(requestCode, permissions, grantResults)
445+
446+
447+
class _RequestPermissionsManager:
448+
"""Internal class for requesting Android permissions.
449+
450+
Permissions are requested through the method 'request_permissions' which
451+
accepts a list of permissions and an optional callback.
452+
453+
Any callback will asynchronously receive arguments from
454+
onRequestPermissionsResult on PythonActivity after requestPermissions is
455+
called.
456+
457+
The callback supplied must accept two arguments: 'permissions' and
458+
'grantResults' (as supplied to onPermissionsCallbackResult).
459+
460+
Note that for SDK_INT < 23, run-time permissions are not required, and so
461+
the callback will be called immediately.
462+
463+
The attribute '_java_callback' is initially None, but is set when the first
464+
permissions request is made. It is set to an instance of
465+
onRequestPermissionsCallback, which allows the Java callback to be
466+
propagated to the class method 'python_callback'. This is then, in turn,
467+
used to call an application callback if provided to request_permissions.
468+
469+
The attribute '_callback_id' is incremented with each call to
470+
request_permissions which has a callback (the value '1' is used for any
471+
call which does not pass a callback). This is passed to requestCode in
472+
the Java call, and used to identify (via the _callbacks dictionary)
473+
the matching call.
474+
"""
475+
_SDK_INT = None
476+
_java_callback = None
477+
_callbacks = {1: None}
478+
_callback_id = 1
479+
# Lock to prevent multiple calls to request_permissions being handled
480+
# simultaneously (as incrementing _callback_id is not atomic)
481+
_lock = threading.Lock()
482+
483+
@classmethod
484+
def register_callback(cls):
485+
"""Register Java callback for requestPermissions."""
486+
cls._java_callback = _onRequestPermissionsCallback(cls.python_callback)
487+
python_activity = autoclass('org.kivy.android.PythonActivity')
488+
python_activity.addPermissionsCallback(cls._java_callback)
489+
490+
@classmethod
491+
def request_permissions(cls, permissions, callback=None):
492+
"""Requests Android permissions from PythonActivity.
493+
If 'callback' is supplied, the request is made with a new requestCode
494+
and the callback is stored in the _callbacks dict. When a Java callback
495+
with the matching requestCode is received, callback will be called
496+
with arguments of 'permissions' and 'grant_results'.
497+
"""
498+
if not cls._SDK_INT:
499+
# Get the Android build version and store it
500+
VERSION = autoclass('android.os.Build$VERSION')
501+
cls.SDK_INT = VERSION.SDK_INT
502+
if cls.SDK_INT < 23:
503+
# No run-time permissions needed, return immediately.
504+
if callback:
505+
callback(permissions, [True for x in permissions])
506+
return
507+
# Request permissions
508+
with cls._lock:
509+
if not cls._java_callback:
510+
cls.register_callback()
511+
python_activity = autoclass('org.kivy.android.PythonActivity')
512+
if not callback:
513+
python_activity.requestPermissions(permissions)
514+
else:
515+
cls._callback_id += 1
516+
python_activity.requestPermissionsWithRequestCode(
517+
permissions, cls._callback_id)
518+
cls._callbacks[cls._callback_id] = callback
428519

520+
@classmethod
521+
def python_callback(cls, requestCode, permissions, grantResults):
522+
"""Calls the relevant callback with arguments of 'permissions'
523+
and 'grantResults'."""
524+
# Convert from Android codes to True/False
525+
grant_results = [x == PERMISSION_GRANTED for x in grantResults]
526+
if cls._callbacks.get(requestCode):
527+
cls._callbacks[requestCode](permissions, grant_results)
429528

430-
def request_permission(permission):
431-
request_permissions([permission])
529+
530+
# Public API methods for requesting permissions
531+
532+
def request_permissions(permissions, callback=None):
533+
"""Requests Android permissions.
534+
535+
Args:
536+
permissions (str): A list of permissions to requests (str)
537+
callback (callable, optional): A function to call when the request
538+
is completed (callable)
539+
540+
Returns:
541+
None
542+
543+
Notes:
544+
545+
Permission strings can be imported from the 'Permission' class in this
546+
module. For example:
547+
548+
from android import Permission
549+
permissions_list = [Permission.CAMERA,
550+
Permission.WRITE_EXTERNAL_STORAGE]
551+
552+
See the p4a source file 'permissions.py' for a list of valid permission
553+
strings (pythonforandroid/recipes/android/src/android/permissions.py).
554+
555+
Any callback supplied must accept two arguments:
556+
permissions (list of str): A list of permission strings
557+
grant_results (list of bool): A list of bools indicating whether the
558+
respective permission was granted.
559+
See Android documentation for onPermissionsCallbackResult for
560+
further information.
561+
562+
Note that if the request is interupted the callback may contain an empty
563+
list of permissions, without permissions being granted; the App should
564+
check that each permission requested has been granted.
565+
566+
Also note that when calling request_permission on SDK_INT < 23, the
567+
callback will be returned immediately as requesting permissions is not
568+
required.
569+
"""
570+
_RequestPermissionsManager.request_permissions(permissions, callback)
571+
572+
573+
def request_permission(permission, callback=None):
574+
request_permissions([permission], callback)
432575

433576

434577
def check_permission(permission):
578+
"""Checks if an app holds the passed permission.
579+
580+
Args:
581+
- permission An Android permission (str)
582+
583+
Returns:
584+
bool: True if the app holds the permission given, False otherwise.
585+
"""
435586
python_activity = autoclass('org.kivy.android.PythonActivity')
436587
result = bool(python_activity.checkCurrentPermission(
437588
permission + ""

0 commit comments

Comments
 (0)