diff --git a/changes/2522.bugfix.md b/changes/2522.bugfix.md new file mode 100644 index 000000000..98631ada4 --- /dev/null +++ b/changes/2522.bugfix.md @@ -0,0 +1 @@ +Permission disabling is now handled correctly diff --git a/changes/2522.feature.md b/changes/2522.feature.md new file mode 100644 index 000000000..dfd6c9ed2 --- /dev/null +++ b/changes/2522.feature.md @@ -0,0 +1 @@ +Add the ability to specify bluetooth permission. diff --git a/docs/en/reference/configuration.md b/docs/en/reference/configuration.md index 491295667..df70ff6ef 100644 --- a/docs/en/reference/configuration.md +++ b/docs/en/reference/configuration.md @@ -379,6 +379,10 @@ Applications may also need to declare the permissions they require. Permissions Briefcase maintains a set of cross-platform permissions: +#### `permission.bluetooth` + +Permission to connect to an external device via Bluetooth. + #### `permission.camera` Permission to access the camera to take photos or video. diff --git a/docs/en/reference/platforms/android/gradle.md b/docs/en/reference/platforms/android/gradle.md index 32c59bdc2..f36a575ad 100644 --- a/docs/en/reference/platforms/android/gradle.md +++ b/docs/en/reference/platforms/android/gradle.md @@ -239,8 +239,8 @@ A property whose sub-properties define the platform-specific permissions that wi For example, specifying: -```python -permission."android.permission.HIGH_SAMPLING_RATE_SENSORS" = true +```toml +permission."android.permission.HIGH_SAMPLING_RATE_SENSORS" = {} ``` will result in an `AndroidManifest.xml` declaration of: @@ -249,6 +249,22 @@ will result in an `AndroidManifest.xml` declaration of: ``` +Using a dictionary as a value allows you to specify additional attributes that detail, for example, the API version or combined constraints. + +For example, specifying: + +```toml +permission."android.permission.BLUETOOTH_ADMIN" = {"android:maxSdkVersion"= "30"} +permission."android.permission.BLUETOOTH_SCAN" = {"android:usesPermissionFlags"= "neverForLocation"} +``` + +will result in an `AndroidManifest.xml` declaration of: + +```xml + + +``` + ### `target_os_version` The API level that the app will target. This controls the version of the Android SDK that is used to build your app (by setting the `compileSdkVersion` for your app), and the forwards compatibility behavioral changes your app will enable (by setting the `targetSdkVersion` setting). This is *not* the Android version; it is the underlying API level. For example, Android 15 uses an API level of 35; if you wanted to specify Android 15 as your target API level, you would define `target_os_version = "35"`. @@ -270,6 +286,7 @@ If you want to manually specify a version code by defining `version_code` in you Briefcase cross platform permissions map to `` declarations in the app's `AppManifest.xml`: +* [`permission.bluetooth`][permissionbluetooth]: `android.permission.BLUETOOTH` and other (see below for details) * [`permission.camera`][permissioncamera]: `android.permission.CAMERA` * [`permission.microphone`][permissioncamera]: `android.permission.RECORD_AUDIO` * [`permission.coarse_location`][permissioncoarse_location]: `android.permission.ACCESS_COARSE_LOCATION` @@ -279,6 +296,15 @@ Briefcase cross platform permissions map to `` declarations in Every application will be automatically granted the `android.permission.INTERNET` and `android.permission.NETWORK_STATE` permissions. +Specifying a [`permission.bluetooth`][permissionbluetooth] permission will result in the following `` declarations in the app's `AppManifest.xml`: + +* `android.permission.ACCESS_COARSE_LOCATION`, with an attribute declaration of `android:maxSdkVersion="30"`. If `permission.coarse_location` is defined, the attribute declaration will be omitted +* `android.permission.ACCESS_FINE_LOCATION`, with an attribute declaration of `android:maxSdkVersion="30"`. If `permission.coarse_location` is defined, the attribute declaration will be omitted +* `android.permission.BLUETOOTH`, with an attribute declaration of `android:maxSdkVersion="30"` +* `android.permission.BLUETOOTH_ADMIN"`, with an attribute declaration of `android:maxSdkVersion="30"` +* `android.permission.BLUETOOTH_CONNECT` +* `android.permission.BLUETOOTH_SCAN`, with an attribute declaration of `android:usesPermissionFlags="neverForLocation"`. If `permission.fine_location` or `permission.coarse_location` is defined, the attribute declaration will be omitted. + Specifying a [`permission.camera`][permissioncamera] permission will result in the following non-required [`feature`][] definitions being implicitly added to your app: * `android.hardware.camera`, diff --git a/docs/en/reference/platforms/iOS/xcode.md b/docs/en/reference/platforms/iOS/xcode.md index d181bed60..b6eceddca 100644 --- a/docs/en/reference/platforms/iOS/xcode.md +++ b/docs/en/reference/platforms/iOS/xcode.md @@ -121,6 +121,7 @@ The minimum iOS version that the app will support. This controls the value of `I Briefcase cross-platform permissions map to the following [`info`][] keys: +* [`permission.bluetooth`][permissionbluetooth]: `NSBluetoothAlwaysUsageDescription` * [`permission.camera`][permissioncamera]: `NSCameraUsageDescription` * [`permission.microphone`][permissionmicrophone]: `NSMicrophoneUsageDescription` * [`permission.coarse_location`][permissioncoarse_location]: diff --git a/docs/en/reference/platforms/macOS/index.md b/docs/en/reference/platforms/macOS/index.md index 1245e395f..36dd5cb5e 100644 --- a/docs/en/reference/platforms/macOS/index.md +++ b/docs/en/reference/platforms/macOS/index.md @@ -218,12 +218,11 @@ For more details on macOS document type declarations, see the following web page ## Permissions -### `macOS` - Briefcase cross platform permissions map to a combination of [`info`][] and [`entitlement`][] keys: -- [`permission.microphone`][permissionmicrophone]: an [`info`][] entry for `NSMicrophoneUsageDescription`; and an [`entitlement`][] of `com.apple.security.device.audio-input` +- [`permission.bluetooth`][permissionbluetooth]: an [`info`][] entry for `NSBluetoothAlwaysUsageDescription`; and an [`entitlement`][] of `com.apple.security.device.bluetooth` - [`permission.camera`][permissioncamera]: an [`info`][] entry for `NSCameraUsageDescription`; and an [`entitlement`][] of `com.apple.security.device.camera` +- [`permission.microphone`][permissionmicrophone]: an [`info`][] entry for `NSMicrophoneUsageDescription`; and an [`entitlement`][] of `com.apple.security.device.audio-input` - [`permission.coarse_location`][permissioncoarse_location]: an [`info`][] entry for `NSLocationUsageDescription` (ignored if [`permission.background_location`][permissionbackground_location] or [`permission.fine_location`][permissionfine_location] is defined); plus an entitlement of `com.apple.security.personal-information.location` - [`permission.fine_location`][permissionfine_location]: an [`info`][] entry for `NSLocationUsageDescription`(ignored if [`permission.background_location`][permissionbackground_location] is defined); plus an [`entitlement`][] of `com.apple.security.personal-information.location` - [`permission.background_location`][permissionbackground_location]: an [`info`][] entry for `NSLocationUsageDescription`; plus an [`entitlement`][] of `com.apple.security.personal-information.location` diff --git a/src/briefcase/commands/create.py b/src/briefcase/commands/create.py index 6f86c0986..637f632b5 100644 --- a/src/briefcase/commands/create.py +++ b/src/briefcase/commands/create.py @@ -192,6 +192,7 @@ def _x_permissions(self, app: AppConfig): return { key: app.permission.pop(key, None) for key in [ + "bluetooth", "camera", "microphone", "coarse_location", diff --git a/src/briefcase/platforms/android/gradle.py b/src/briefcase/platforms/android/gradle.py index b32382ea7..06b3c7829 100644 --- a/src/briefcase/platforms/android/gradle.py +++ b/src/briefcase/platforms/android/gradle.py @@ -239,15 +239,33 @@ def permissions_context(self, app: AppConfig, x_permissions: dict[str, str]): """ # Default permissions for all Android apps permissions = { - "android.permission.INTERNET": True, - "android.permission.ACCESS_NETWORK_STATE": True, + "android.permission.INTERNET": {}, + "android.permission.ACCESS_NETWORK_STATE": {}, } # Default feature usage for all Android apps features = {} + if x_permissions["bluetooth"]: + permissions["android.permission.ACCESS_COARSE_LOCATION"] = { + "android:maxSdkVersion": "30" + } + permissions["android.permission.ACCESS_FINE_LOCATION"] = { + "android:maxSdkVersion": "30" + } + permissions["android.permission.BLUETOOTH"] = { + "android:maxSdkVersion": "30" + } + permissions["android.permission.BLUETOOTH_ADMIN"] = { + "android:maxSdkVersion": "30" + } + permissions["android.permission.BLUETOOTH_CONNECT"] = {} + permissions["android.permission.BLUETOOTH_SCAN"] = { + "android:usesPermissionFlags": "neverForLocation" + } + if x_permissions["camera"]: - permissions["android.permission.CAMERA"] = True + permissions["android.permission.CAMERA"] = {} features["android.hardware.camera"] = False features["android.hardware.camera.any"] = False features["android.hardware.camera.front"] = False @@ -255,25 +273,33 @@ def permissions_context(self, app: AppConfig, x_permissions: dict[str, str]): features["android.hardware.camera.autofocus"] = False if x_permissions["microphone"]: - permissions["android.permission.RECORD_AUDIO"] = True + permissions["android.permission.RECORD_AUDIO"] = {} if x_permissions["fine_location"]: - permissions["android.permission.ACCESS_FINE_LOCATION"] = True + permissions["android.permission.ACCESS_FINE_LOCATION"] = {} features["android.hardware.location.network"] = False features["android.hardware.location.gps"] = False + # We're good with the location. So we can also use BLUETOOTH_SCAN. + bt_scan_perm = permissions.get("android.permission.BLUETOOTH_SCAN") + if bt_scan_perm: + bt_scan_perm.pop("android:usesPermissionFlags", None) if x_permissions["coarse_location"]: - permissions["android.permission.ACCESS_COARSE_LOCATION"] = True + permissions["android.permission.ACCESS_COARSE_LOCATION"] = {} features["android.hardware.location.network"] = False features["android.hardware.location.gps"] = False + # We're good with the location. So we can also use BLUETOOTH_SCAN. + bt_scan_perm = permissions.get("android.permission.BLUETOOTH_SCAN") + if bt_scan_perm: + bt_scan_perm.pop("android:usesPermissionFlags", None) if x_permissions["background_location"]: - permissions["android.permission.ACCESS_BACKGROUND_LOCATION"] = True + permissions["android.permission.ACCESS_BACKGROUND_LOCATION"] = {} features["android.hardware.location.network"] = False features["android.hardware.location.gps"] = False if x_permissions["photo_library"]: - permissions["android.permission.READ_MEDIA_VISUAL_USER_SELECTED"] = True + permissions["android.permission.READ_MEDIA_VISUAL_USER_SELECTED"] = {} # Override any permission and entitlement definitions # with the platform-specific definitions diff --git a/src/briefcase/platforms/iOS/xcode.py b/src/briefcase/platforms/iOS/xcode.py index da2c5d8ce..6a183a57f 100644 --- a/src/briefcase/platforms/iOS/xcode.py +++ b/src/briefcase/platforms/iOS/xcode.py @@ -270,6 +270,8 @@ def permissions_context(self, app: AppConfig, x_permissions: dict[str, str]): # The collection of info.plist entries info = {} + if x_permissions["bluetooth"]: + info["NSBluetoothAlwaysUsageDescription"] = x_permissions["bluetooth"] if x_permissions["camera"]: info["NSCameraUsageDescription"] = x_permissions["camera"] if x_permissions["microphone"]: diff --git a/src/briefcase/platforms/macOS/__init__.py b/src/briefcase/platforms/macOS/__init__.py index 17838bd6c..ef980c69d 100644 --- a/src/briefcase/platforms/macOS/__init__.py +++ b/src/briefcase/platforms/macOS/__init__.py @@ -377,6 +377,9 @@ def permissions_context(self, app: AppConfig, cross_platform: dict[str, str]): "com.apple.security.cs.disable-library-validation": True, } + if cross_platform["bluetooth"]: + entitlements["com.apple.security.device.bluetooth"] = True + info["NSBluetoothAlwaysUsageDescription"] = cross_platform["bluetooth"] if cross_platform["camera"]: entitlements["com.apple.security.device.camera"] = True info["NSCameraUsageDescription"] = cross_platform["camera"] diff --git a/tests/platforms/android/gradle/test_create.py b/tests/platforms/android/gradle/test_create.py index a5cbcd8d1..59fdfe5ca 100644 --- a/tests/platforms/android/gradle/test_create.py +++ b/tests/platforms/android/gradle/test_create.py @@ -209,8 +209,8 @@ def test_extract_packages(create_command, first_app_config, test_sources, expect {}, { "permissions": { - "android.permission.ACCESS_NETWORK_STATE": True, - "android.permission.INTERNET": True, + "android.permission.ACCESS_NETWORK_STATE": {}, + "android.permission.INTERNET": {}, }, "features": {}, }, @@ -218,16 +218,16 @@ def test_extract_packages(create_command, first_app_config, test_sources, expect # Only custom permissions ( { - "android.permission.READ_CONTACTS": True, + "android.permission.READ_CONTACTS": {}, }, { "android.hardware.bluetooth": True, }, { "permissions": { - "android.permission.ACCESS_NETWORK_STATE": True, - "android.permission.INTERNET": True, - "android.permission.READ_CONTACTS": True, + "android.permission.ACCESS_NETWORK_STATE": {}, + "android.permission.INTERNET": {}, + "android.permission.READ_CONTACTS": {}, }, "features": { "android.hardware.bluetooth": True, @@ -242,9 +242,9 @@ def test_extract_packages(create_command, first_app_config, test_sources, expect {}, { "permissions": { - "android.permission.ACCESS_NETWORK_STATE": True, - "android.permission.INTERNET": True, - "android.permission.CAMERA": True, + "android.permission.ACCESS_NETWORK_STATE": {}, + "android.permission.INTERNET": {}, + "android.permission.CAMERA": {}, }, "features": { "android.hardware.camera": False, @@ -263,9 +263,9 @@ def test_extract_packages(create_command, first_app_config, test_sources, expect {}, { "permissions": { - "android.permission.ACCESS_NETWORK_STATE": True, - "android.permission.INTERNET": True, - "android.permission.RECORD_AUDIO": True, + "android.permission.ACCESS_NETWORK_STATE": {}, + "android.permission.INTERNET": {}, + "android.permission.RECORD_AUDIO": {}, }, "features": {}, }, @@ -278,9 +278,9 @@ def test_extract_packages(create_command, first_app_config, test_sources, expect {}, { "permissions": { - "android.permission.ACCESS_NETWORK_STATE": True, - "android.permission.INTERNET": True, - "android.permission.ACCESS_COARSE_LOCATION": True, + "android.permission.ACCESS_NETWORK_STATE": {}, + "android.permission.INTERNET": {}, + "android.permission.ACCESS_COARSE_LOCATION": {}, }, "features": { "android.hardware.location.gps": False, @@ -296,9 +296,9 @@ def test_extract_packages(create_command, first_app_config, test_sources, expect {}, { "permissions": { - "android.permission.ACCESS_NETWORK_STATE": True, - "android.permission.INTERNET": True, - "android.permission.ACCESS_FINE_LOCATION": True, + "android.permission.ACCESS_NETWORK_STATE": {}, + "android.permission.INTERNET": {}, + "android.permission.ACCESS_FINE_LOCATION": {}, }, "features": { "android.hardware.location.gps": False, @@ -314,9 +314,9 @@ def test_extract_packages(create_command, first_app_config, test_sources, expect {}, { "permissions": { - "android.permission.ACCESS_NETWORK_STATE": True, - "android.permission.INTERNET": True, - "android.permission.ACCESS_BACKGROUND_LOCATION": True, + "android.permission.ACCESS_NETWORK_STATE": {}, + "android.permission.INTERNET": {}, + "android.permission.ACCESS_BACKGROUND_LOCATION": {}, }, "features": { "android.hardware.location.gps": False, @@ -333,10 +333,10 @@ def test_extract_packages(create_command, first_app_config, test_sources, expect {}, { "permissions": { - "android.permission.ACCESS_NETWORK_STATE": True, - "android.permission.INTERNET": True, - "android.permission.ACCESS_COARSE_LOCATION": True, - "android.permission.ACCESS_BACKGROUND_LOCATION": True, + "android.permission.ACCESS_NETWORK_STATE": {}, + "android.permission.INTERNET": {}, + "android.permission.ACCESS_COARSE_LOCATION": {}, + "android.permission.ACCESS_BACKGROUND_LOCATION": {}, }, "features": { "android.hardware.location.gps": False, @@ -353,10 +353,10 @@ def test_extract_packages(create_command, first_app_config, test_sources, expect {}, { "permissions": { - "android.permission.ACCESS_NETWORK_STATE": True, - "android.permission.INTERNET": True, - "android.permission.ACCESS_FINE_LOCATION": True, - "android.permission.ACCESS_BACKGROUND_LOCATION": True, + "android.permission.ACCESS_NETWORK_STATE": {}, + "android.permission.INTERNET": {}, + "android.permission.ACCESS_FINE_LOCATION": {}, + "android.permission.ACCESS_BACKGROUND_LOCATION": {}, }, "features": { "android.hardware.location.gps": False, @@ -373,10 +373,10 @@ def test_extract_packages(create_command, first_app_config, test_sources, expect {}, { "permissions": { - "android.permission.ACCESS_NETWORK_STATE": True, - "android.permission.INTERNET": True, - "android.permission.ACCESS_COARSE_LOCATION": True, - "android.permission.ACCESS_FINE_LOCATION": True, + "android.permission.ACCESS_NETWORK_STATE": {}, + "android.permission.INTERNET": {}, + "android.permission.ACCESS_COARSE_LOCATION": {}, + "android.permission.ACCESS_FINE_LOCATION": {}, }, "features": { "android.hardware.location.gps": False, @@ -394,11 +394,11 @@ def test_extract_packages(create_command, first_app_config, test_sources, expect {}, { "permissions": { - "android.permission.ACCESS_NETWORK_STATE": True, - "android.permission.INTERNET": True, - "android.permission.ACCESS_COARSE_LOCATION": True, - "android.permission.ACCESS_FINE_LOCATION": True, - "android.permission.ACCESS_BACKGROUND_LOCATION": True, + "android.permission.ACCESS_NETWORK_STATE": {}, + "android.permission.INTERNET": {}, + "android.permission.ACCESS_COARSE_LOCATION": {}, + "android.permission.ACCESS_FINE_LOCATION": {}, + "android.permission.ACCESS_BACKGROUND_LOCATION": {}, }, "features": { "android.hardware.location.gps": False, @@ -414,19 +414,103 @@ def test_extract_packages(create_command, first_app_config, test_sources, expect {}, { "permissions": { - "android.permission.ACCESS_NETWORK_STATE": True, - "android.permission.INTERNET": True, - "android.permission.READ_MEDIA_VISUAL_USER_SELECTED": True, + "android.permission.ACCESS_NETWORK_STATE": {}, + "android.permission.INTERNET": {}, + "android.permission.READ_MEDIA_VISUAL_USER_SELECTED": {}, }, "features": {}, }, ), + # Bluetooth permissions + ( + { + "bluetooth": "I need to connect to bluetooth device", + }, + {}, + { + "permissions": { + "android.permission.ACCESS_COARSE_LOCATION": { + "android:maxSdkVersion": "30" + }, + "android.permission.ACCESS_FINE_LOCATION": { + "android:maxSdkVersion": "30" + }, + "android.permission.ACCESS_NETWORK_STATE": {}, + "android.permission.BLUETOOTH": {"android:maxSdkVersion": "30"}, + "android.permission.BLUETOOTH_ADMIN": { + "android:maxSdkVersion": "30" + }, + "android.permission.BLUETOOTH_CONNECT": {}, + "android.permission.BLUETOOTH_SCAN": { + "android:usesPermissionFlags": "neverForLocation" + }, + "android.permission.INTERNET": {}, + }, + "features": {}, + }, + ), + # Bluetooth permissions WITH coarse location permissions + ( + { + "bluetooth": "I need to connect to bluetooth device", + "coarse_location": "I need to know roughly where you are", + }, + {}, + { + "permissions": { + "android.permission.ACCESS_COARSE_LOCATION": {}, + "android.permission.ACCESS_FINE_LOCATION": { + "android:maxSdkVersion": "30" + }, + "android.permission.ACCESS_NETWORK_STATE": {}, + "android.permission.BLUETOOTH": {"android:maxSdkVersion": "30"}, + "android.permission.BLUETOOTH_ADMIN": { + "android:maxSdkVersion": "30" + }, + "android.permission.BLUETOOTH_CONNECT": {}, + "android.permission.BLUETOOTH_SCAN": {}, + "android.permission.INTERNET": {}, + }, + "features": { + "android.hardware.location.gps": False, + "android.hardware.location.network": False, + }, + }, + ), + # Bluetooth permissions WITH fine location permissions + ( + { + "bluetooth": "I need to connect to bluetooth device", + "fine_location": "I need to know exactly where you are", + }, + {}, + { + "permissions": { + "android.permission.ACCESS_COARSE_LOCATION": { + "android:maxSdkVersion": "30" + }, + "android.permission.ACCESS_FINE_LOCATION": {}, + "android.permission.ACCESS_NETWORK_STATE": {}, + "android.permission.BLUETOOTH": {"android:maxSdkVersion": "30"}, + "android.permission.BLUETOOTH_ADMIN": { + "android:maxSdkVersion": "30" + }, + "android.permission.BLUETOOTH_CONNECT": {}, + "android.permission.BLUETOOTH_SCAN": {}, + "android.permission.INTERNET": {}, + }, + "features": { + "android.hardware.location.gps": False, + "android.hardware.location.network": False, + }, + }, + ), # Override and augment by cross-platform definitions ( { "camera": "I need to see you", "android.permission.CAMERA": False, - "android.permission.READ_CONTACTS": True, + "android.permission.READ_CONTACTS": {}, }, { "android.hardware.camera.external": True, @@ -434,10 +518,10 @@ def test_extract_packages(create_command, first_app_config, test_sources, expect }, { "permissions": { - "android.permission.ACCESS_NETWORK_STATE": True, - "android.permission.INTERNET": True, + "android.permission.ACCESS_NETWORK_STATE": {}, + "android.permission.INTERNET": {}, "android.permission.CAMERA": False, - "android.permission.READ_CONTACTS": True, + "android.permission.READ_CONTACTS": {}, }, "features": { "android.hardware.camera": False, diff --git a/tests/platforms/iOS/xcode/test_create.py b/tests/platforms/iOS/xcode/test_create.py index fc1a58c50..41d714445 100644 --- a/tests/platforms/iOS/xcode/test_create.py +++ b/tests/platforms/iOS/xcode/test_create.py @@ -408,6 +408,18 @@ def test_incompatible_min_os_version(create_command, first_app_generated, tmp_pa } }, ), + # Bluetooth permissions + ( + { + "bluetooth": "I need to connect to bluetooth device.", + }, + {}, + { + "info": { + "NSBluetoothAlwaysUsageDescription": "I need to connect to bluetooth device." + }, + }, + ), # Camera permissions ( { diff --git a/tests/platforms/macOS/app/test_create.py b/tests/platforms/macOS/app/test_create.py index 28183dd2d..989025bab 100644 --- a/tests/platforms/macOS/app/test_create.py +++ b/tests/platforms/macOS/app/test_create.py @@ -73,6 +73,24 @@ def create_command(dummy_console, tmp_path, first_app_templated): }, }, ), + # Bluetooth permissions + ( + { + "bluetooth": "I need to connect to bluetooth device.", + }, + {}, + {}, + { + "info": { + "NSBluetoothAlwaysUsageDescription": "I need to connect to bluetooth device." + }, + "entitlements": { + "com.apple.security.cs.allow-unsigned-executable-memory": True, + "com.apple.security.cs.disable-library-validation": True, + "com.apple.security.device.bluetooth": True, + }, + }, + ), # Camera permissions ( {