diff --git a/docs/source/index.rst b/docs/source/index.rst index 20ae9d6f9..41d799a6e 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -8,12 +8,45 @@ Welcome to Plyer Plyer is a Python library for accessing features of your hardware / platforms. +Each feature is defined by a facade, and provided by platform specific +implementations, they are used by importing them directly from the `plyer` +package. + +For example, to get an implementation of the `gps` facade, and start it you can do: + +```python +from plyer import gps + +gps.start() +``` + +Please consult the :mod:`plyer.facades` documentation for the available methods. + +.. note:: + + Android manage permissions at runtime, and in granular way. Each feature + can require one or multiple permissions. Plyer will try to ask for the + necessary permissions the moment they are needed, but they still need to be + declared at compile time through python-for-android command line, or in + buildozer.spec. + + Also, there are implications to requesting a permission, as it will briefly + pause your application. For this reason, it's advised to avoid: + - starting a plyer feature that require permissions before the app is done + starting + - calling multiple features that require different permissions in the same + frame, unless you previously requested all the necessary permissions. + + If needed, you can normally import the `android` module to manually request + permissions. Make sure this import is only done when running on Android. + .. automodule:: plyer :members: .. automodule:: plyer.facades :members: + Indices and tables ================== diff --git a/examples/gps/main.py b/examples/gps/main.py index eba684019..ddad8e7fb 100644 --- a/examples/gps/main.py +++ b/examples/gps/main.py @@ -33,34 +33,6 @@ class GpsTest(App): gps_location = StringProperty() gps_status = StringProperty('Click Start to get GPS location updates') - def request_android_permissions(self): - """ - Since API 23, Android requires permission to be requested at runtime. - This function requests permission and handles the response via a - callback. - - The request will produce a popup if permissions have not already been - been granted, otherwise it will do nothing. - """ - from android.permissions import request_permissions, Permission - - def callback(permissions, results): - """ - Defines the callback to be fired when runtime permission - has been granted or denied. This is not strictly required, - but added for the sake of completeness. - """ - if all([res for res in results]): - print("callback. All permissions granted.") - else: - print("callback. Some permissions refused.") - - request_permissions([Permission.ACCESS_COARSE_LOCATION, - Permission.ACCESS_FINE_LOCATION], callback) - # # To request permissions without a callback, do: - # request_permissions([Permission.ACCESS_COARSE_LOCATION, - # Permission.ACCESS_FINE_LOCATION]) - def build(self): try: gps.configure(on_location=self.on_location, @@ -70,10 +42,6 @@ def build(self): traceback.print_exc() self.gps_status = 'GPS is not implemented for your platform' - if platform == "android": - print("gps.py: Android detected. Requesting permissions") - self.request_android_permissions() - return Builder.load_string(kv) def start(self, minTime, minDistance): diff --git a/plyer/platforms/android/__init__.py b/plyer/platforms/android/__init__.py index 8fe126353..ddd4f7b81 100644 --- a/plyer/platforms/android/__init__.py +++ b/plyer/platforms/android/__init__.py @@ -1,8 +1,13 @@ from os import environ +from logging import getLogger + +from functools import wraps from jnius import autoclass ANDROID_VERSION = autoclass('android.os.Build$VERSION') SDK_INT = ANDROID_VERSION.SDK_INT +LOG = getLogger(__name__) + try: from android import config @@ -10,9 +15,77 @@ except (ImportError, AttributeError): ns = 'org.renpy.android' + if 'PYTHON_SERVICE_ARGUMENT' in environ: PythonService = autoclass(ns + '.PythonService') activity = PythonService.mService else: PythonActivity = autoclass(ns + '.PythonActivity') activity = PythonActivity.mActivity + + +def resolve_permission(permission): + """Helper method to allow passing a permission by name + """ + from android.permissions import Permission + if hasattr(Permission, permission): + return getattr(Permission, permission) + return permission + + +def require_permissions(*permissions, handle_denied=None): + """ + A decorator for android plyer functions allowing to automatically request + necessary permissions when a method is called. + + usage: + @require_permissions(Permission.ACCESS_COARSE_LOCATION, Permission.ACCESS_FINE_LOCATION) + def start_gps(...): + ... + + + if the permissions haven't been granted yet, the require_permissions method + will be called first, and the actual method will be set as a callback to + execute when the user accept or refuse permissions, if you want to handle + the cases where some of the permissions are denied, you can set a callback + method to `handle_denied`. When set, and if some permissions are refused + this function will be called with the list of permissions that were refused + as a parameter. If you don't set such a handler, the decorated method will + be called in all the cases. + """ + + def decorator(function): + LOG.debug(f"decorating function {function.__name__}") + @wraps(function) + def wrapper(*args, **kwargs): + nonlocal permissions + from android.permissions import request_permissions, check_permission + + def callback(permissions, grant_results): + LOG.debug(f"callback called with {dict(zip(permissions, grant_results))}") + if handle_denied and not all(grant_results): + handle_denied([ + permission + for (granted, permission) in zip(grant_results, permissions) + if granted + ]) + else: + function(*args, **kwargs) + + permissions = [resolve_permission(permission) for permission in permissions] + permissions = [ + permission + for permission in permissions + if not check_permission(permission) + ] + LOG.debug(f"needed permissions: {permissions}") + + if permissions: + LOG.debug("calling request_permissions with callback") + request_permissions(permissions, callback) + else: + LOG.debug("no missing permissiong calling function directly") + function(*args, **kwargs) + + return wrapper + return decorator diff --git a/plyer/platforms/android/audio.py b/plyer/platforms/android/audio.py index 9f000ff41..959cd684b 100644 --- a/plyer/platforms/android/audio.py +++ b/plyer/platforms/android/audio.py @@ -1,6 +1,7 @@ from jnius import autoclass from plyer.facades.audio import Audio +from plyer.platforms.android import require_permissions # Recorder Classes MediaRecorder = autoclass('android.media.MediaRecorder') @@ -26,6 +27,7 @@ def __init__(self, file_path=None): self._recorder = None self._player = None + @require_permissions("RECORD_AUDIO") def _start(self): self._recorder = MediaRecorder() self._recorder.setAudioSource(AudioSource.DEFAULT) diff --git a/plyer/platforms/android/battery.py b/plyer/platforms/android/battery.py index 4a58f1f0b..20fe9e856 100644 --- a/plyer/platforms/android/battery.py +++ b/plyer/platforms/android/battery.py @@ -3,7 +3,7 @@ ''' from jnius import autoclass, cast -from plyer.platforms.android import activity +from plyer.platforms.android import activity, require_permissions from plyer.facades import Battery Intent = autoclass('android.content.Intent') @@ -16,6 +16,7 @@ class AndroidBattery(Battery): Implementation of Android battery API. ''' + @require_permissions('BATTERY_STATS') def _get_state(self): status = {"isCharging": None, "percentage": None} diff --git a/plyer/platforms/android/brightness.py b/plyer/platforms/android/brightness.py index d0a1157fd..fd12f3b9d 100755 --- a/plyer/platforms/android/brightness.py +++ b/plyer/platforms/android/brightness.py @@ -5,7 +5,7 @@ from jnius import autoclass from plyer.facades import Brightness -from android import mActivity +from plyer.platform.android import activity, require_permissions System = autoclass('android.provider.Settings$System') @@ -15,7 +15,7 @@ class AndroidBrightness(Brightness): def _current_level(self): System.putInt( - mActivity.getContentResolver(), + activity.getContentResolver(), System.SCREEN_BRIGHTNESS_MODE, System.SCREEN_BRIGHTNESS_MODE_MANUAL) cr_level = System.getInt( @@ -23,6 +23,7 @@ def _current_level(self): System.SCREEN_BRIGHTNESS) return (cr_level / 255.) * 100 + @require_permissions("WRITE_SETTINGS") def _set_level(self, level): System.putInt( mActivity.getContentResolver(), diff --git a/plyer/platforms/android/call.py b/plyer/platforms/android/call.py index 2a1388c7d..18a80ba2f 100644 --- a/plyer/platforms/android/call.py +++ b/plyer/platforms/android/call.py @@ -5,7 +5,7 @@ from jnius import autoclass from plyer.facades import Call -from plyer.platforms.android import activity +from plyer.platforms.android import activity, require_permissions Intent = autoclass('android.content.Intent') uri = autoclass('android.net.Uri') @@ -13,6 +13,7 @@ class AndroidCall(Call): + @require_permissions("CALL_PHONE") def _makecall(self, **kwargs): intent = Intent(Intent.ACTION_CALL) @@ -20,6 +21,7 @@ def _makecall(self, **kwargs): intent.setData(uri.parse("tel:{}".format(tel))) activity.startActivity(intent) + @require_permissions("CALL_PHONE") def _dialcall(self, **kwargs): intent_ = Intent(Intent.ACTION_DIAL) activity.startActivity(intent_) diff --git a/plyer/platforms/android/camera.py b/plyer/platforms/android/camera.py index be259aa01..e1e83e0b3 100644 --- a/plyer/platforms/android/camera.py +++ b/plyer/platforms/android/camera.py @@ -6,7 +6,6 @@ from plyer.platforms.android import activity Intent = autoclass('android.content.Intent') -PythonActivity = autoclass('org.renpy.android.PythonActivity') MediaStore = autoclass('android.provider.MediaStore') Uri = autoclass('android.net.Uri') diff --git a/plyer/platforms/android/flash.py b/plyer/platforms/android/flash.py index eec1ae385..b53bcbef2 100644 --- a/plyer/platforms/android/flash.py +++ b/plyer/platforms/android/flash.py @@ -6,7 +6,7 @@ from plyer.facades import Flash from jnius import autoclass -from plyer.platforms.android import activity +from plyer.platforms.android import activity, require_permissions Camera = autoclass("android.hardware.Camera") CameraParameters = autoclass("android.hardware.Camera$Parameters") @@ -38,6 +38,7 @@ def _release(self): self._camera.release() self._camera = None + @require_permissions("CAMERA", "FLASHLIGHT") def _camera_open(self): if not flash_available: return diff --git a/plyer/platforms/android/gps.py b/plyer/platforms/android/gps.py index 17fd86e97..ba07721eb 100644 --- a/plyer/platforms/android/gps.py +++ b/plyer/platforms/android/gps.py @@ -4,7 +4,9 @@ ''' from plyer.facades import GPS -from plyer.platforms.android import activity +from plyer.platforms.android import activity, require_permissions +from android.permissions import Permission + from jnius import autoclass, java_method, PythonJavaClass Looper = autoclass('android.os.Looper') @@ -62,6 +64,7 @@ def _configure(self): ) self._location_listener = _LocationListener(self) + @require_permissions("ACCESS_COARSE_LOCATION", "ACCESS_FINE_LOCATION") def _start(self, **kwargs): min_time = kwargs.get('minTime') min_distance = kwargs.get('minDistance') diff --git a/plyer/platforms/android/sms.py b/plyer/platforms/android/sms.py index 8650968cf..43e1302d5 100644 --- a/plyer/platforms/android/sms.py +++ b/plyer/platforms/android/sms.py @@ -5,12 +5,14 @@ from jnius import autoclass from plyer.facades import Sms +from plyer.platform.android import require_permissions SmsManager = autoclass('android.telephony.SmsManager') class AndroidSms(Sms): + @require_permissions("SEND_SMS") def _send(self, **kwargs): sms = SmsManager.getDefault() diff --git a/plyer/platforms/android/stt.py b/plyer/platforms/android/stt.py index 51681e049..18bedb691 100644 --- a/plyer/platforms/android/stt.py +++ b/plyer/platforms/android/stt.py @@ -5,7 +5,7 @@ from jnius import PythonJavaClass from plyer.facades import STT -from plyer.platforms.android import activity +from plyer.platforms.android import activity, require_permissions ArrayList = autoclass('java.util.ArrayList') Bundle = autoclass('android.os.Bundle') @@ -197,6 +197,7 @@ def _on_partial(self, messages): self.partial_results.extend(messages) @run_on_ui_thread + @require_permissions("RECORD_AUDIO") def _start(self): intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH) intent.putExtra( diff --git a/plyer/platforms/android/vibrator.py b/plyer/platforms/android/vibrator.py index a318c3c43..a414ee29e 100644 --- a/plyer/platforms/android/vibrator.py +++ b/plyer/platforms/android/vibrator.py @@ -2,8 +2,7 @@ from jnius import autoclass, cast from plyer.facades import Vibrator -from plyer.platforms.android import activity -from plyer.platforms.android import SDK_INT +from plyer.platforms.android import activity, SDK_INT, require_permission Context = autoclass("android.content.Context") vibrator_service = activity.getSystemService(Context.VIBRATOR_SERVICE) @@ -22,6 +21,7 @@ class AndroidVibrator(Vibrator): * check whether Vibrator exists. """ + @require_permissions("VIBRATE") def _vibrate(self, time=None, **kwargs): if vibrator: if SDK_INT >= 26: @@ -33,6 +33,7 @@ def _vibrate(self, time=None, **kwargs): else: vibrator.vibrate(int(1000 * time)) + @require_permissions("VIBRATE") def _pattern(self, pattern=None, repeat=None, **kwargs): pattern = [int(1000 * time) for time in pattern]