Skip to content

Commit a636b88

Browse files
authored
Support permission properties (maxSdkVersion and usesPermissionFlags) + remove WRITE_EXTERNAL_STORAGE permission, which has been previously declared by default (kivy#2725)
* Support permission properties (maxSdkVersion and usesPermissionFlags) + remove WRITE_EXTERNAL_STORAGE permission, which has been previously required by default * Fix test for ValueError
1 parent 7d473a9 commit a636b88

File tree

8 files changed

+173
-31
lines changed

8 files changed

+173
-31
lines changed

Makefile

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ testapps-with-numpy: virtualenv
4040
. $(ACTIVATE) && cd testapps/on_device_unit_tests/ && \
4141
python setup.py apk --sdk-dir $(ANDROID_SDK_HOME) --ndk-dir $(ANDROID_NDK_HOME) \
4242
--requirements libffi,sdl2,pyjnius,kivy,python3,openssl,requests,urllib3,chardet,idna,sqlite3,setuptools,numpy \
43-
--arch=armeabi-v7a --arch=arm64-v8a --arch=x86_64 --arch=x86
43+
--arch=armeabi-v7a --arch=arm64-v8a --arch=x86_64 --arch=x86 \
44+
--permission "(name=android.permission.WRITE_EXTERNAL_STORAGE;maxSdkVersion=18)" --permission "(name=android.permission.INTERNET)"
4445

4546
testapps-with-scipy: virtualenv
4647
. $(ACTIVATE) && cd testapps/on_device_unit_tests/ && \
@@ -53,7 +54,8 @@ testapps-with-numpy-aab: virtualenv
5354
. $(ACTIVATE) && cd testapps/on_device_unit_tests/ && \
5455
python setup.py aab --sdk-dir $(ANDROID_SDK_HOME) --ndk-dir $(ANDROID_NDK_HOME) \
5556
--requirements libffi,sdl2,pyjnius,kivy,python3,openssl,requests,urllib3,chardet,idna,sqlite3,setuptools,numpy \
56-
--arch=armeabi-v7a --arch=arm64-v8a --arch=x86_64 --arch=x86 --release
57+
--arch=armeabi-v7a --arch=arm64-v8a --arch=x86_64 --arch=x86 --release \
58+
--permission "(name=android.permission.WRITE_EXTERNAL_STORAGE;maxSdkVersion=18)" --permission "(name=android.permission.INTERNET)"
5759

5860
testapps-service_library-aar: virtualenv
5961
. $(ACTIVATE) && cd testapps/on_device_unit_tests/ && \

doc/source/buildoptions.rst

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,24 @@ options (this list may not be exhaustive):
6464
``android:screenOrientation`` in the `Android documentation
6565
<https://developer.android.com/guide/topics/manifest/activity-element.html>`__.
6666
- ``--icon``: A path to the png file to use as the application icon.
67-
- ``--permission``: A permission name for the app,
68-
e.g. ``--permission VIBRATE``. For multiple permissions, add
69-
multiple ``--permission`` arguments.
67+
- ``--permission``: A permission that needs to be declared into the App ``AndroidManifest.xml``.
68+
For multiple permissions, add multiple ``--permission`` arguments.
69+
70+
.. Note ::
71+
``--permission`` accepts the following syntaxes:
72+
``--permission (name=android.permission.WRITE_EXTERNAL_STORAGE;maxSdkVersion=18)``
73+
or ``--permission android.permission.WRITE_EXTERNAL_STORAGE``.
74+
75+
The first syntax is used to set additional properties to the permission
76+
(``android:maxSdkVersion`` and ``android:usesPermissionFlags`` are the only ones supported for now).
77+
78+
The second one can be used when there's no need to add any additional properties.
79+
80+
.. Warning ::
81+
The syntax ``--permission VIBRATE`` (only the permission name, without the prefix),
82+
is also supported for backward compatibility, but it will be removed in the future.
83+
84+
7085
- ``--meta-data``: Custom key=value pairs to add in the application metadata.
7186
- ``--presplash``: A path to the image file to use as a screen while
7287
the application is loading.

pythonforandroid/bootstraps/common/build/build.py

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,6 @@ def get_bootstrap_name():
5353

5454
curdir = dirname(__file__)
5555

56-
PYTHON = get_hostpython()
57-
if PYTHON is not None and not exists(PYTHON):
58-
PYTHON = None
59-
6056
BLACKLIST_PATTERNS = [
6157
# code versionning
6258
'^*.hg/*',
@@ -75,9 +71,19 @@ def get_bootstrap_name():
7571
]
7672

7773
WHITELIST_PATTERNS = []
78-
if get_bootstrap_name() in ('sdl2', 'webview', 'service_only'):
79-
WHITELIST_PATTERNS.append('pyconfig.h')
8074

75+
if os.environ.get("P4A_BUILD_IS_RUNNING_UNITTESTS", "0") != "1":
76+
PYTHON = get_hostpython()
77+
_bootstrap_name = get_bootstrap_name()
78+
else:
79+
PYTHON = "python3"
80+
_bootstrap_name = "sdl2"
81+
82+
if PYTHON is not None and not exists(PYTHON):
83+
PYTHON = None
84+
85+
if _bootstrap_name in ('sdl2', 'webview', 'service_only'):
86+
WHITELIST_PATTERNS.append('pyconfig.h')
8187

8288
environment = jinja2.Environment(loader=jinja2.FileSystemLoader(
8389
join(curdir, 'templates')))
@@ -646,6 +652,44 @@ def make_package(args):
646652
subprocess.check_output(patch_command)
647653

648654

655+
def parse_permissions(args_permissions):
656+
if args_permissions and isinstance(args_permissions[0], list):
657+
args_permissions = [p for perm in args_permissions for p in perm]
658+
659+
def _is_advanced_permission(permission):
660+
return permission.startswith("(") and permission.endswith(")")
661+
662+
def _decode_advanced_permission(permission):
663+
SUPPORTED_PERMISSION_PROPERTIES = ["name", "maxSdkVersion", "usesPermissionFlags"]
664+
_permission_args = permission[1:-1].split(";")
665+
_permission_args = (arg.split("=") for arg in _permission_args)
666+
advanced_permission = dict(_permission_args)
667+
668+
if "name" not in advanced_permission:
669+
raise ValueError("Advanced permission must have a name property")
670+
671+
for key in advanced_permission.keys():
672+
if key not in SUPPORTED_PERMISSION_PROPERTIES:
673+
raise ValueError(
674+
f"Property '{key}' is not supported. "
675+
"Advanced permission only supports: "
676+
f"{', '.join(SUPPORTED_PERMISSION_PROPERTIES)} properties"
677+
)
678+
679+
return advanced_permission
680+
681+
_permissions = []
682+
for permission in args_permissions:
683+
if _is_advanced_permission(permission):
684+
_permissions.append(_decode_advanced_permission(permission))
685+
else:
686+
if "." in permission:
687+
_permissions.append(dict(name=permission))
688+
else:
689+
_permissions.append(dict(name=f"android.permission.{permission}"))
690+
return _permissions
691+
692+
649693
def parse_args_and_make_package(args=None):
650694
global BLACKLIST_PATTERNS, WHITELIST_PATTERNS, PYTHON
651695

@@ -918,8 +962,7 @@ def _read_configuration():
918962
'deprecated and does nothing.')
919963
args.sdk_version = -1 # ensure it is not used
920964

921-
if args.permissions and isinstance(args.permissions[0], list):
922-
args.permissions = [p for perm in args.permissions for p in perm]
965+
args.permissions = parse_permissions(args.permissions)
923966

924967
if args.res_xmls and isinstance(args.res_xmls[0], list):
925968
args.res_xmls = [x for res in args.res_xmls for x in res]
@@ -959,4 +1002,6 @@ def _read_configuration():
9591002

9601003

9611004
if __name__ == "__main__":
1005+
if get_bootstrap_name() in ('sdl2', 'webview', 'service_only'):
1006+
WHITELIST_PATTERNS.append('pyconfig.h')
9621007
parse_args_and_make_package()

pythonforandroid/bootstraps/sdl2/build/templates/AndroidManifest.tmpl.xml

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,9 @@
2424
<!-- OpenGL ES 2.0 -->
2525
<uses-feature android:glEsVersion="0x00020000" />
2626

27-
<!-- Allow writing to external storage -->
28-
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29"/>
27+
<!-- Set permissions -->
2928
{% for perm in args.permissions %}
30-
{% if '.' in perm %}
31-
<uses-permission android:name="{{ perm }}" />
32-
{% else %}
33-
<uses-permission android:name="android.permission.{{ perm }}" />
34-
{% endif %}
29+
<uses-permission android:name="{{ perm.name }}"{% if perm.maxSdkVersion %} android:maxSdkVersion="{{ perm.maxSdkVersion }}"{% endif %}{% if perm.usesPermissionFlags %} android:usesPermissionFlags="{{ perm.usesPermissionFlags }}"{% endif %} />
3530
{% endfor %}
3631

3732
{% if args.wakelock %}

pythonforandroid/bootstraps/service_only/build/templates/AndroidManifest.tmpl.xml

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,7 @@
2020

2121
<!-- Set permissions -->
2222
{% for perm in args.permissions %}
23-
{% if '.' in perm %}
24-
<uses-permission android:name="{{ perm }}" />
25-
{% else %}
26-
<uses-permission android:name="android.permission.{{ perm }}" />
27-
{% endif %}
23+
<uses-permission android:name="{{ perm.name }}"{% if perm.maxSdkVersion %} android:maxSdkVersion="{{ perm.maxSdkVersion }}"{% endif %}{% if perm.usesPermissionFlags %} android:usesPermissionFlags="{{ perm.usesPermissionFlags }}"{% endif %} />
2824
{% endfor %}
2925

3026
{% if args.wakelock %}

pythonforandroid/bootstraps/webview/build/templates/AndroidManifest.tmpl.xml

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,7 @@
2121
<!-- Allow writing to external storage -->
2222
<uses-permission android:name="android.permission.INTERNET" />
2323
{% for perm in args.permissions %}
24-
{% if '.' in perm %}
25-
<uses-permission android:name="{{ perm }}" />
26-
{% else %}
27-
<uses-permission android:name="android.permission.{{ perm }}" />
28-
{% endif %}
24+
<uses-permission android:name="{{ perm.name }}"{% if perm.maxSdkVersion %} android:maxSdkVersion="{{ perm.maxSdkVersion }}"{% endif %}{% if perm.usesPermissionFlags %} android:usesPermissionFlags="{{ perm.usesPermissionFlags }}"{% endif %} />
2925
{% endfor %}
3026

3127
{% if args.wakelock %}

tests/test_bootstrap_build.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import unittest
2+
import pytest
3+
import os
4+
import argparse
5+
6+
from pythonforandroid.util import load_source
7+
8+
9+
class TestParsePermissions(unittest.TestCase):
10+
def test_parse_permissions_with_migrations(self):
11+
# Test that permissions declared in the old format are migrated to the
12+
# new format.
13+
# (Users can new declare permissions in both formats, even a mix)
14+
os.environ["P4A_BUILD_IS_RUNNING_UNITTESTS"] = "1"
15+
16+
ap = argparse.ArgumentParser()
17+
ap.add_argument(
18+
"--permission",
19+
dest="permissions",
20+
action="append",
21+
default=[],
22+
help="The permissions to give this app.",
23+
nargs="+",
24+
)
25+
26+
args = [
27+
"--permission",
28+
"INTERNET",
29+
"--permission",
30+
"com.android.voicemail.permission.ADD_VOICEMAIL",
31+
"--permission",
32+
"(name=android.permission.WRITE_EXTERNAL_STORAGE;maxSdkVersion=18)",
33+
"--permission",
34+
"(name=android.permission.BLUETOOTH_SCAN;usesPermissionFlags=neverForLocation)",
35+
]
36+
37+
args = ap.parse_args(args)
38+
39+
build_src = os.path.join(
40+
os.path.dirname(os.path.abspath(__file__)),
41+
"../pythonforandroid/bootstraps/common/build/build.py",
42+
)
43+
44+
buildpy = load_source("buildpy", build_src)
45+
parsed_permissions = buildpy.parse_permissions(args.permissions)
46+
47+
assert parsed_permissions == [
48+
dict(name="android.permission.INTERNET"),
49+
dict(name="com.android.voicemail.permission.ADD_VOICEMAIL"),
50+
dict(name="android.permission.WRITE_EXTERNAL_STORAGE", maxSdkVersion="18"),
51+
dict(
52+
name="android.permission.BLUETOOTH_SCAN",
53+
usesPermissionFlags="neverForLocation",
54+
),
55+
]
56+
57+
def test_parse_permissions_invalid_property(self):
58+
os.environ["P4A_BUILD_IS_RUNNING_UNITTESTS"] = "1"
59+
60+
ap = argparse.ArgumentParser()
61+
ap.add_argument(
62+
"--permission",
63+
dest="permissions",
64+
action="append",
65+
default=[],
66+
help="The permissions to give this app.",
67+
nargs="+",
68+
)
69+
70+
args = [
71+
"--permission",
72+
"(name=android.permission.BLUETOOTH_SCAN;propertyThatFails=neverForLocation)",
73+
]
74+
75+
args = ap.parse_args(args)
76+
77+
build_src = os.path.join(
78+
os.path.dirname(os.path.abspath(__file__)),
79+
"../pythonforandroid/bootstraps/common/build/build.py",
80+
)
81+
82+
buildpy = load_source("buildpy", build_src)
83+
84+
with pytest.raises(
85+
ValueError, match="Property 'propertyThatFails' is not supported."
86+
):
87+
buildpy.parse_permissions(args.permissions)

tests/test_build.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,10 @@ def test_android_manifest_xml(self):
6464
args.min_sdk_version = 12
6565
args.build_mode = 'debug'
6666
args.native_services = ['abcd', ]
67-
args.permissions = []
67+
args.permissions = [
68+
dict(name="android.permission.INTERNET"),
69+
dict(name="android.permission.WRITE_EXTERNAL_STORAGE", maxSdkVersion=18),
70+
dict(name="android.permission.BLUETOOTH_SCAN", usesPermissionFlags="neverForLocation")]
6871
args.add_activity = []
6972
args.android_used_libs = []
7073
args.meta_data = []
@@ -91,6 +94,9 @@ def test_android_manifest_xml(self):
9194
assert xml.count('targetSdkVersion="1234"') == 1
9295
assert xml.count('android:debuggable="true"') == 1
9396
assert xml.count('<service android:name="abcd" />') == 1
97+
assert xml.count('<uses-permission android:name="android.permission.INTERNET" />') == 1
98+
assert xml.count('<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="18" />') == 1
99+
assert xml.count('<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />') == 1
94100
# TODO: potentially some other checks to be added here to cover other "logic" (flags and loops) in the template
95101

96102

0 commit comments

Comments
 (0)