Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a require_permissions decorator to use on android methods #629

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
==================

Expand Down
32 changes: 0 additions & 32 deletions examples/gps/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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):
Expand Down
73 changes: 73 additions & 0 deletions plyer/platforms/android/__init__.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,91 @@
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
ns = config.JAVA_NAMESPACE
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
2 changes: 2 additions & 0 deletions plyer/platforms/android/audio.py
Original file line number Diff line number Diff line change
@@ -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')
Expand All @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion plyer/platforms/android/battery.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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}

Expand Down
5 changes: 3 additions & 2 deletions plyer/platforms/android/brightness.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand All @@ -15,14 +15,15 @@ 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(
mActivity.getContentResolver(),
System.SCREEN_BRIGHTNESS)
return (cr_level / 255.) * 100

@require_permissions("WRITE_SETTINGS")
def _set_level(self, level):
System.putInt(
mActivity.getContentResolver(),
Expand Down
4 changes: 3 additions & 1 deletion plyer/platforms/android/call.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,23 @@

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')


class AndroidCall(Call):

@require_permissions("CALL_PHONE")
def _makecall(self, **kwargs):

intent = Intent(Intent.ACTION_CALL)
tel = kwargs.get('tel')
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_)
Expand Down
1 change: 0 additions & 1 deletion plyer/platforms/android/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down
3 changes: 2 additions & 1 deletion plyer/platforms/android/flash.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion plyer/platforms/android/gps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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')
Expand Down
2 changes: 2 additions & 0 deletions plyer/platforms/android/sms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
3 changes: 2 additions & 1 deletion plyer/platforms/android/stt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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(
Expand Down
5 changes: 3 additions & 2 deletions plyer/platforms/android/vibrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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:
Expand All @@ -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]

Expand Down