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 1 commit
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
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
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_permission

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_permission("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