From 62bd1d54db1b19a8e609f8d335e8fd830e34751f Mon Sep 17 00:00:00 2001 From: HasanAlqaisi Date: Wed, 19 Nov 2025 18:19:14 +0300 Subject: [PATCH 01/26] Fix: await Permission.location.request() returns PermissionStatus.denied when user selects approximate --- .../permissionhandler/PermissionManager.java | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/permission_handler_android/android/src/main/java/com/baseflow/permissionhandler/PermissionManager.java b/permission_handler_android/android/src/main/java/com/baseflow/permissionhandler/PermissionManager.java index 8da9ee2ef..cee61a42b 100644 --- a/permission_handler_android/android/src/main/java/com/baseflow/permissionhandler/PermissionManager.java +++ b/permission_handler_android/android/src/main/java/com/baseflow/permissionhandler/PermissionManager.java @@ -247,20 +247,25 @@ public boolean onRequestPermissionsResult( PermissionUtils.toPermissionStatus(this.activity, permissionName, result); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - if (!requestResults.containsKey(PermissionConstants.PERMISSION_GROUP_LOCATION_ALWAYS)) { + if (!requestResults.containsKey(PermissionConstants.PERMISSION_GROUP_LOCATION_ALWAYS) || + requestResults.get(PermissionConstants.PERMISSION_GROUP_LOCATION_ALWAYS) != PermissionConstants.PERMISSION_STATUS_GRANTED) { requestResults.put( PermissionConstants.PERMISSION_GROUP_LOCATION_ALWAYS, permissionStatus); } } - if (!requestResults.containsKey(PermissionConstants.PERMISSION_GROUP_LOCATION_WHEN_IN_USE)) { + if (!requestResults.containsKey(PermissionConstants.PERMISSION_GROUP_LOCATION_WHEN_IN_USE) || + requestResults.get(PermissionConstants.PERMISSION_GROUP_LOCATION_WHEN_IN_USE) != PermissionConstants.PERMISSION_STATUS_GRANTED) { requestResults.put( PermissionConstants.PERMISSION_GROUP_LOCATION_WHEN_IN_USE, permissionStatus); } - requestResults.put(permission, permissionStatus); + if (!requestResults.containsKey(permission) || + requestResults.get(permission) != PermissionConstants.PERMISSION_STATUS_GRANTED) { + requestResults.put(permission, permissionStatus); + } // [grantResults] can only contain PermissionConstants.PERMISSION_STATUS_GRANTED or PermissionConstants.PERMISSION_STATUS_DENIED status. // But these permissions can have status PermissionConstants.PERMISSION_STATUS_LIMITED, so we need to recheck status } else if (permission == PermissionConstants.PERMISSION_GROUP_PHOTOS || permission == PermissionConstants.PERMISSION_GROUP_VIDEOS) { @@ -502,6 +507,19 @@ private int determinePermissionStatus(final @PermissionConstants.PermissionGroup : PermissionConstants.PERMISSION_STATUS_DENIED; } + if (context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.M) { + if (permission == PermissionConstants.PERMISSION_GROUP_LOCATION || + permission == PermissionConstants.PERMISSION_GROUP_LOCATION_ALWAYS || + permission == PermissionConstants.PERMISSION_GROUP_LOCATION_WHEN_IN_USE) { + boolean isCoarseGranted = ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED; + boolean isFineGranted = ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED; + + if (isCoarseGranted || isFineGranted) { + return PermissionConstants.PERMISSION_STATUS_GRANTED; + } + } + } + final boolean requiresExplicitPermission = context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.M; if (requiresExplicitPermission) { From b91577879e2a20601c6af97421687cbcf1f925a4 Mon Sep 17 00:00:00 2001 From: TheoGermain <40172735+TheoGermain@users.noreply.github.com> Date: Fri, 29 May 2026 16:14:24 +0200 Subject: [PATCH 02/26] feat(apple): add Swift Package Manager support (#1523) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(apple): create SPM source directory structure * feat(apple): move ObjC sources to SPM-compatible structure * feat(apple): add public header for SPM plugin registration * feat(apple): move PrivacyInfo.xcprivacy to SPM sources * feat(apple): add Package.swift for Swift Package Manager support * feat(apple): update podspec to reference SPM source structure * chore(apple): ignore SPM build artifacts * docs: add SPM setup instructions to README * fix(apple): move Package.swift to correct SPM location (ios/permission_handler_apple/) * fix(apple): add missing PERMISSION_PHOTOS_ADD_ONLY and PERMISSION_LOCATION_ALWAYS defines * fix(apple): use forward declaration in public header to avoid missing internal import * fix(example): bump compileSdk to 36, AGP to 8.9.1, Gradle to 8.11.1 * fix(apple): add explicit UIKit import to strategies that use UIApplication under SPM * feat(apple): auto-detect permissions from Info.plist in Package.swift Package.swift now walks up the directory tree from its own location to find the app's Info.plist and enables each permission define when the corresponding usage description key is present. This mirrors the CocoaPods workflow: adding NSCameraUsageDescription to Info.plist is all that is needed to activate PERMISSION_CAMERA, with no extra configuration files or terminal commands. Environment variables remain supported as an explicit override (priority over Info.plist), which covers PermissionGroup.notification and criticalAlerts that have no required Info.plist key. Users must clear DerivedData once after changing Info.plist so Xcode re-evaluates the manifest: rm -rf ~/Library/Developer/Xcode/DerivedData * docs: update SPM setup instructions to use Info.plist auto-detection Replace the launchctl setenv / pre-action script approach with the new Info.plist-based mechanism: permissions are now enabled automatically when the corresponding usage description key is present in Info.plist, which is already required for any permission to work at runtime. Document the two permissions without an Info.plist key (notification, criticalAlerts) as the only case still requiring an env var. * chore(example): update iOS example app and remove plan artifact - Add all permission usage description keys to Info.plist so the SPM Info.plist auto-detection covers all permissions out of the box - Comment out the Siri entitlement (requires a paid Apple Developer account; uncomment to test PERMISSION_ASSISTANT) - Update AppDelegate to modern FlutterImplicitEngineDelegate pattern - Bump Podfile iOS platform to 13.0 - Remove docs/superpowers/plans/2026-05-05-spm-support.md (internal planning artifact not intended for the public repo) * chore(apple): bump version to 9.4.8 and update CHANGELOG * fix(apple): correct SPM permission flag mapping for photos and calendarWriteOnly - PERMISSION_PHOTOS now triggers on NSPhotoLibraryAddUsageDescription alone, since PhotoPermissionStrategy (which handles photosAddOnly) compiles under PERMISSION_PHOTOS — without this, photosAddOnly silently fell back to UnknownPermissionStrategy when NSPhotoLibraryUsageDescription was absent - PERMISSION_EVENTS_FULL_ACCESS now also triggers on NSCalendarsWriteOnlyAccessUsageDescription (iOS 17+), enabling calendarWriteOnly which requires PERMISSION_EVENTS || PERMISSION_EVENTS_FULL_ACCESS in native code - Sync podspec version to 9.4.8 - Restore NSCameraUsageDescription in example Info.plist (lost during rewrite) - Add NSCalendarsWriteOnlyAccessUsageDescription to example Info.plist * docs: add calendarWriteOnly to SPM permission table in README * fix(example): remove NSSiriUsageDescription and document permission constraints Siri requires the com.apple.developer.siri entitlement; including NSSiriUsageDescription without it crashes the app on launch under SPM. Added a README section listing permissions that cannot be tested on simulator or without special entitlements. * fix(apple): enable notifications and criticalAlerts by default under SPM These permissions have no required Info.plist key so the previous logic always compiled them out (defaultValue "0"), causing permanentlyDenied to be returned without ever showing a system dialog. They are now enabled by default and can be opted out via env var set to "0". * fix(apple): revert criticalAlerts to opt-in under SPM criticalAlerts requires a special Apple entitlement; compiling it into every app by default would add dead code for apps that don't use it. Only PERMISSION_NOTIFICATIONS defaults to enabled (no entitlement needed). * docs: clarify SPM special cases for notification and criticalAlerts - Add both permissions to the Info.plist table with notes - Distinguish export (terminal) vs launchctl setenv (Xcode GUI) - Explain why criticalAlerts is opt-in (Apple entitlement required) * chore(example): enable SPM in Xcode project for iOS example app Flutter auto-generated FlutterGeneratedPluginSwiftPackage reference when running with --enable-swift-package-manager. * fix(example): align Java source/target compatibility to VERSION_17 AGP 8.x + Kotlin 1.9+ enforce JVM-target consistency; compileJava was still on 1.8 while compileKotlin used 17, causing the build to fail. --- permission_handler/README.md | 63 +++++++ .../example/android/app/build.gradle | 6 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../example/android/settings.gradle | 2 +- permission_handler_apple/CHANGELOG.md | 10 ++ permission_handler_apple/example/README.md | 11 ++ .../ios/Flutter/AppFrameworkInfo.plist | 2 - permission_handler_apple/example/ios/Podfile | 2 +- .../ios/Runner.xcodeproj/project.pbxproj | 66 +++----- .../xcshareddata/xcschemes/Runner.xcscheme | 31 +++- .../example/ios/Runner/AppDelegate.swift | 11 +- .../example/ios/Runner/Info.plist | 134 ++++++++------- .../ios/Runner/RunnerDebug.entitlements | 3 + permission_handler_apple/ios/.gitignore | 4 + .../ios/permission_handler_apple.podspec | 10 +- .../permission_handler_apple/Package.swift | 156 ++++++++++++++++++ .../PermissionHandlerEnums.h | 0 .../PermissionHandlerPlugin.h | 0 .../PermissionHandlerPlugin.m | 0 .../PermissionManager.h | 0 .../PermissionManager.m | 0 .../PrivacyInfo.xcprivacy | 0 .../PermissionHandlerPlugin.h | 7 + ...ppTrackingTransparencyPermissionStrategy.h | 0 ...ppTrackingTransparencyPermissionStrategy.m | 0 .../strategies/AssistantPermissionStrategy.h | 0 .../strategies/AssistantPermissionStrategy.m | 0 .../strategies/AudioVideoPermissionStrategy.h | 0 .../strategies/AudioVideoPermissionStrategy.m | 0 .../strategies/BackgroundRefreshStrategy.h | 1 + .../strategies/BackgroundRefreshStrategy.m | 0 .../strategies/BluetoothPermissionStrategy.h | 0 .../strategies/BluetoothPermissionStrategy.m | 0 .../strategies/ContactPermissionStrategy.h | 0 .../strategies/ContactPermissionStrategy.m | 0 .../CriticalAlertsPermissionStrategy.h | 1 + .../CriticalAlertsPermissionStrategy.m | 0 .../strategies/EventPermissionStrategy.h | 0 .../strategies/EventPermissionStrategy.m | 0 .../strategies/LocationPermissionStrategy.h | 1 + .../strategies/LocationPermissionStrategy.m | 0 .../MediaLibraryPermissionStrategy.h | 0 .../MediaLibraryPermissionStrategy.m | 0 .../NotificationPermissionStrategy.h | 1 + .../NotificationPermissionStrategy.m | 0 .../strategies/PermissionStrategy.h | 0 .../strategies/PhonePermissionStrategy.h | 1 + .../strategies/PhonePermissionStrategy.m | 0 .../strategies/PhotoPermissionStrategy.h | 0 .../strategies/PhotoPermissionStrategy.m | 0 .../strategies/SensorPermissionStrategy.h | 0 .../strategies/SensorPermissionStrategy.m | 0 .../strategies/SpeechPermissionStrategy.h | 0 .../strategies/SpeechPermissionStrategy.m | 0 .../strategies/StoragePermissionStrategy.h | 0 .../strategies/StoragePermissionStrategy.m | 0 .../strategies/UnknownPermissionStrategy.h | 0 .../strategies/UnknownPermissionStrategy.m | 0 .../permission_handler_apple}/util/Codec.h | 0 .../permission_handler_apple}/util/Codec.m | 0 permission_handler_apple/pubspec.yaml | 2 +- 61 files changed, 394 insertions(+), 133 deletions(-) create mode 100644 permission_handler_apple/ios/permission_handler_apple/Package.swift rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/PermissionHandlerEnums.h (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/PermissionHandlerPlugin.h (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/PermissionHandlerPlugin.m (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/PermissionManager.h (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/PermissionManager.m (100%) rename permission_handler_apple/ios/{Resources => permission_handler_apple/Sources/permission_handler_apple}/PrivacyInfo.xcprivacy (100%) create mode 100644 permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/include/permission_handler_apple/PermissionHandlerPlugin.h rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/AppTrackingTransparencyPermissionStrategy.h (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/AppTrackingTransparencyPermissionStrategy.m (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/AssistantPermissionStrategy.h (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/AssistantPermissionStrategy.m (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/AudioVideoPermissionStrategy.h (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/AudioVideoPermissionStrategy.m (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/BackgroundRefreshStrategy.h (92%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/BackgroundRefreshStrategy.m (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/BluetoothPermissionStrategy.h (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/BluetoothPermissionStrategy.m (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/ContactPermissionStrategy.h (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/ContactPermissionStrategy.m (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/CriticalAlertsPermissionStrategy.h (95%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/CriticalAlertsPermissionStrategy.m (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/EventPermissionStrategy.h (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/EventPermissionStrategy.m (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/LocationPermissionStrategy.h (96%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/LocationPermissionStrategy.m (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/MediaLibraryPermissionStrategy.h (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/MediaLibraryPermissionStrategy.m (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/NotificationPermissionStrategy.h (95%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/NotificationPermissionStrategy.m (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/PermissionStrategy.h (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/PhonePermissionStrategy.h (92%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/PhonePermissionStrategy.m (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/PhotoPermissionStrategy.h (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/PhotoPermissionStrategy.m (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/SensorPermissionStrategy.h (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/SensorPermissionStrategy.m (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/SpeechPermissionStrategy.h (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/SpeechPermissionStrategy.m (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/StoragePermissionStrategy.h (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/StoragePermissionStrategy.m (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/UnknownPermissionStrategy.h (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/strategies/UnknownPermissionStrategy.m (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/util/Codec.h (100%) rename permission_handler_apple/ios/{Classes => permission_handler_apple/Sources/permission_handler_apple}/util/Codec.m (100%) diff --git a/permission_handler/README.md b/permission_handler/README.md index 836bd1339..3e17adf3e 100644 --- a/permission_handler/README.md +++ b/permission_handler/README.md @@ -174,6 +174,69 @@ You must list the permission you want to use in your application: +
+Swift Package Manager (SPM) + +> Requires Flutter 3.24.0 or higher and Xcode 15.0 or higher. + +With SPM, `Package.swift` automatically detects which permissions to enable by reading your app's `Info.plist`. A permission is compiled in when its corresponding usage description key is present: + +| Permission group | Info.plist key | +|---|---| +| `PermissionGroup.calendar` (< iOS 17) | `NSCalendarsUsageDescription` | +| `PermissionGroup.calendarWriteOnly` (iOS 17+) | `NSCalendarsWriteOnlyAccessUsageDescription` | +| `PermissionGroup.calendarFullAccess` (iOS 17+) | `NSCalendarsFullAccessUsageDescription` | +| `PermissionGroup.reminders` | `NSRemindersUsageDescription` | +| `PermissionGroup.contacts` | `NSContactsUsageDescription` | +| `PermissionGroup.camera` | `NSCameraUsageDescription` | +| `PermissionGroup.microphone` | `NSMicrophoneUsageDescription` | +| `PermissionGroup.speech` | `NSSpeechRecognitionUsageDescription` | +| `PermissionGroup.photos` | `NSPhotoLibraryUsageDescription` | +| `PermissionGroup.photosAddOnly` | `NSPhotoLibraryAddUsageDescription` | +| `PermissionGroup.location` / `locationWhenInUse` | `NSLocationWhenInUseUsageDescription` | +| `PermissionGroup.locationAlways` | `NSLocationAlwaysAndWhenInUseUsageDescription` | +| `PermissionGroup.mediaLibrary` | `NSAppleMusicUsageDescription` | +| `PermissionGroup.sensors` | `NSMotionUsageDescription` | +| `PermissionGroup.bluetooth` | `NSBluetoothAlwaysUsageDescription` | +| `PermissionGroup.appTrackingTransparency` | `NSUserTrackingUsageDescription` | +| `PermissionGroup.assistant` | `NSSiriUsageDescription` | +| `PermissionGroup.notification` | *(enabled by default — see below)* | +| `PermissionGroup.criticalAlerts` | *(disabled by default — see below)* | + +Because you must already add these keys to `Info.plist` for any permission to work, no additional configuration file is needed. + +#### Special cases: permissions without an Info.plist key + +**`PermissionGroup.notification`** has no required `Info.plist` key and is **enabled by default**. To opt out, disable it via environment variable before building: + +```bash +# When building from terminal (flutter run / flutter build) +export PERMISSION_NOTIFICATIONS=0 + +# When building from Xcode GUI (set once per Mac session, then restart Xcode) +launchctl setenv PERMISSION_NOTIFICATIONS 0 +``` + +**`PermissionGroup.criticalAlerts`** requires a [special entitlement](https://developer.apple.com/documentation/usernotifications/asking-permission-to-use-notifications) granted by Apple and is **disabled by default** to avoid compiling unused code into apps that don't need it. Enable it explicitly: + +```bash +# When building from terminal +export PERMISSION_CRITICAL_ALERTS=1 + +# When building from Xcode GUI +launchctl setenv PERMISSION_CRITICAL_ALERTS 1 +``` + +**After changing any env var or Info.plist key**, clear Xcode's package cache once so `Package.swift` is re-evaluated: + +```bash +rm -rf ~/Library/Developer/Xcode/DerivedData +``` + +Then run `flutter build ios` or rebuild in Xcode as usual. + +
+ ## How to use There are a number of [`Permission`](https://pub.dev/documentation/permission_handler_platform_interface/latest/permission_handler_platform_interface/Permission-class.html#constants)s. diff --git a/permission_handler/example/android/app/build.gradle b/permission_handler/example/android/app/build.gradle index 537b42534..047e56a5c 100644 --- a/permission_handler/example/android/app/build.gradle +++ b/permission_handler/example/android/app/build.gradle @@ -25,11 +25,11 @@ android { if (project.android.hasProperty("namespace")) { namespace 'com.baseflow.permissionhandlerexample' } - compileSdkVersion 35 + compileSdkVersion 36 compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } defaultConfig { diff --git a/permission_handler/example/android/gradle/wrapper/gradle-wrapper.properties b/permission_handler/example/android/gradle/wrapper/gradle-wrapper.properties index db18181ac..9162f1008 100644 --- a/permission_handler/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/permission_handler/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip diff --git a/permission_handler/example/android/settings.gradle b/permission_handler/example/android/settings.gradle index 56eb85cb1..8cbe490f3 100644 --- a/permission_handler/example/android/settings.gradle +++ b/permission_handler/example/android/settings.gradle @@ -18,7 +18,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.7.0" apply false + id "com.android.application" version "8.9.1" apply false } include ":app" \ No newline at end of file diff --git a/permission_handler_apple/CHANGELOG.md b/permission_handler_apple/CHANGELOG.md index fbd51929f..05a0f3485 100644 --- a/permission_handler_apple/CHANGELOG.md +++ b/permission_handler_apple/CHANGELOG.md @@ -1,3 +1,13 @@ +## 9.4.8 + +* Adds Swift Package Manager (SPM) support for Flutter 3.24+. Permissions are + enabled automatically based on usage description keys present in `Info.plist` + — no additional configuration required beyond clearing DerivedData once after + changes: `rm -rf ~/Library/Developer/Xcode/DerivedData`. +* Moves ObjC sources to SPM-compatible layout (`Sources/permission_handler_apple/`). + CocoaPods continues to work unchanged. +* Bumps minimum iOS deployment target to 12.0. + ## 9.4.7 * Increases minimum supported Flutter version to 3.3.0, and removes code only diff --git a/permission_handler_apple/example/README.md b/permission_handler_apple/example/README.md index cca8fca64..acc6dde68 100644 --- a/permission_handler_apple/example/README.md +++ b/permission_handler_apple/example/README.md @@ -14,3 +14,14 @@ A few resources to get you started if this is your first Flutter project: For help getting started with Flutter, view our [online documentation](https://flutter.io/docs), which offers tutorials, samples, guidance on mobile development, and a full API reference. + +## Testing constraints + +Some permissions cannot be tested in all environments: + +| Permission | Constraint | +|---|---| +| `assistant` (Siri) | Requires the `com.apple.developer.siri` entitlement in a provisioned app — **not testable on simulator or without a paid developer account**. Adding `NSSiriUsageDescription` to `Info.plist` without this entitlement will crash the app on launch. | +| `bluetooth` | Requires a **physical device** — Bluetooth is not available on the iOS simulator. | +| `locationAlways` | Requires a physical device and the `NSLocationAlwaysAndWhenInUseUsageDescription` key. | +| `appTrackingTransparency` | Dialog is only shown on iOS 14+ physical devices; always returns `authorized` on the simulator. | diff --git a/permission_handler_apple/example/ios/Flutter/AppFrameworkInfo.plist b/permission_handler_apple/example/ios/Flutter/AppFrameworkInfo.plist index 7c5696400..391a902b2 100644 --- a/permission_handler_apple/example/ios/Flutter/AppFrameworkInfo.plist +++ b/permission_handler_apple/example/ios/Flutter/AppFrameworkInfo.plist @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 12.0 diff --git a/permission_handler_apple/example/ios/Podfile b/permission_handler_apple/example/ios/Podfile index bdbed18e3..4bcee9ab6 100644 --- a/permission_handler_apple/example/ios/Podfile +++ b/permission_handler_apple/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '12.0' +platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/permission_handler_apple/example/ios/Runner.xcodeproj/project.pbxproj b/permission_handler_apple/example/ios/Runner.xcodeproj/project.pbxproj index 705be7680..b19cad307 100644 --- a/permission_handler_apple/example/ios/Runner.xcodeproj/project.pbxproj +++ b/permission_handler_apple/example/ios/Runner.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; @@ -35,6 +36,7 @@ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 862A53EA392D32566500E869 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; @@ -55,6 +57,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, B501E7F22BA22C455255CE2E /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -83,6 +86,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, @@ -140,14 +144,15 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - D38B08CB85942E5D11545EE3 /* [CP] Embed Pods Frameworks */, - A7B07F67421488A414C73AAD /* [CP] Copy Pods Resources */, ); buildRules = ( ); dependencies = ( ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; @@ -176,6 +181,9 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; @@ -253,40 +261,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - A7B07F67421488A414C73AAD /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; - D38B08CB85942E5D11545EE3 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -362,7 +336,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -448,7 +422,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -497,7 +471,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -594,6 +568,20 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } diff --git a/permission_handler_apple/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/permission_handler_apple/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index e67b2808a..16b76ec3c 100644 --- a/permission_handler_apple/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/permission_handler_apple/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,10 +1,28 @@ + version = "1.7"> + + + + + + + + + + - - - - + + @@ -61,8 +80,6 @@ ReferencedContainer = "container:Runner.xcodeproj"> - - Bool { - GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } } diff --git a/permission_handler_apple/example/ios/Runner/Info.plist b/permission_handler_apple/example/ios/Runner/Info.plist index e31eecab1..56e40159b 100644 --- a/permission_handler_apple/example/ios/Runner/Info.plist +++ b/permission_handler_apple/example/ios/Runner/Info.plist @@ -2,6 +2,8 @@ + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable @@ -22,6 +24,67 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + NSAppleMusicUsageDescription + Music! + NSBluetoothAlwaysUsageDescription + bluetooth + NSBluetoothPeripheralUsageDescription + bluetooth + NSCalendarsFullAccessUsageDescription + Calendar full access + NSCalendarsUsageDescription + Calendars + NSCalendarsWriteOnlyAccessUsageDescription + Calendar write only + NSCameraUsageDescription + camera + NSContactsUsageDescription + contacts + NSLocationAlwaysAndWhenInUseUsageDescription + Always and when in use! + NSLocationAlwaysUsageDescription + Can I have location always? + NSLocationUsageDescription + Older devices need location. + NSLocationWhenInUseUsageDescription + Need location when in use + NSMicrophoneUsageDescription + microphone + NSMotionUsageDescription + motion + NSPhotoLibraryAddUsageDescription + photos add only + NSPhotoLibraryUsageDescription + photos + NSRemindersUsageDescription + reminders +NSSpeechRecognitionUsageDescription + speech + NSUserTrackingUsageDescription + appTrackingTransparency + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + FlutterSceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -41,74 +104,7 @@ UIViewControllerBasedStatusBarAppearance - - - NSLocationWhenInUseUsageDescription - Need location when in use - NSLocationAlwaysAndWhenInUseUsageDescription - Always and when in use! - NSLocationUsageDescription - Older devices need location. - NSLocationAlwaysUsageDescription - Can I have location always? - - - NSAppleMusicUsageDescription - Music! - kTCCServiceMediaLibrary - media - - - NSCalendarsUsageDescription - Calendars - NSCalendarsFullAccessUsageDescription - Calendar full access - - - NSCameraUsageDescription - camera - - - NSContactsUsageDescription - contacts - - - NSMicrophoneUsageDescription - microphone - - - NSSpeechRecognitionUsageDescription - speech - - - NSMotionUsageDescription - motion - - - NSPhotoLibraryUsageDescription - photos - - - NSRemindersUsageDescription - reminders - - - NSBluetoothAlwaysUsageDescription - bluetooth - NSBluetoothPeripheralUsageDescription - bluetooth - - - NSUserTrackingUsageDescription - appTrackingTransparency - - - NSSiriUsageDescription - The example app would like access to Siri Kit to demonstrate requesting authorization. - - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - + kTCCServiceMediaLibrary + media diff --git a/permission_handler_apple/example/ios/Runner/RunnerDebug.entitlements b/permission_handler_apple/example/ios/Runner/RunnerDebug.entitlements index 21d95c45f..6503a8865 100644 --- a/permission_handler_apple/example/ios/Runner/RunnerDebug.entitlements +++ b/permission_handler_apple/example/ios/Runner/RunnerDebug.entitlements @@ -2,7 +2,10 @@ + diff --git a/permission_handler_apple/ios/.gitignore b/permission_handler_apple/ios/.gitignore index 710ec6cf1..3ed647344 100644 --- a/permission_handler_apple/ios/.gitignore +++ b/permission_handler_apple/ios/.gitignore @@ -34,3 +34,7 @@ Icon? .tags* /Flutter/Generated.xcconfig + +# Swift Package Manager +.build/ +*.resolved diff --git a/permission_handler_apple/ios/permission_handler_apple.podspec b/permission_handler_apple/ios/permission_handler_apple.podspec index 81210cb8a..ee0359656 100644 --- a/permission_handler_apple/ios/permission_handler_apple.podspec +++ b/permission_handler_apple/ios/permission_handler_apple.podspec @@ -3,7 +3,7 @@ # Pod::Spec.new do |s| s.name = 'permission_handler_apple' - s.version = '9.3.0' + s.version = '9.4.8' s.summary = 'Permission plugin for Flutter.' s.description = <<-DESC Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions. @@ -12,12 +12,12 @@ Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Andro s.license = { :file => '../LICENSE' } s.author = { 'Baseflow' => 'hello@baseflow.com' } s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' + s.source_files = 'permission_handler_apple/Sources/permission_handler_apple/**/*.{h,m}' + s.public_header_files = 'permission_handler_apple/Sources/permission_handler_apple/include/**/*.h' s.dependency 'Flutter' - s.ios.deployment_target = '8.0' + s.ios.deployment_target = '12.0' s.static_framework = true - s.resource_bundles = {'permission_handler_apple_privacy' => ['Resources/PrivacyInfo.xcprivacy']} + s.resource_bundles = {'permission_handler_apple_privacy' => ['permission_handler_apple/Sources/permission_handler_apple/PrivacyInfo.xcprivacy']} end diff --git a/permission_handler_apple/ios/permission_handler_apple/Package.swift b/permission_handler_apple/ios/permission_handler_apple/Package.swift new file mode 100644 index 000000000..5a9e95281 --- /dev/null +++ b/permission_handler_apple/ios/permission_handler_apple/Package.swift @@ -0,0 +1,156 @@ +// swift-tools-version: 5.9 + +import PackageDescription +import Foundation + +// --------------------------------------------------------------------------- +// Permission configuration +// +// Permissions are resolved in priority order: +// 1. Environment variable (e.g. `launchctl setenv PERMISSION_CAMERA 1` +// or `launchctl setenv PERMISSION_NOTIFICATIONS 0` to explicitly disable) +// 2. Matching key present in the app's Info.plist +// 3. Default: enabled for permissions with no required plist key +// (PERMISSION_NOTIFICATIONS, PERMISSION_CRITICAL_ALERTS), +// disabled for all others. +// +// After changing Info.plist or env vars, clear DerivedData once so Xcode +// re-evaluates this manifest: +// rm -rf ~/Library/Developer/Xcode/DerivedData +// --------------------------------------------------------------------------- + +let env = ProcessInfo.processInfo.environment + +/// Walk up from Package.swift looking for Runner/Info.plist. +/// Works when the package is resolved via Flutter's .symlinks/ directory. +func findInfoPlist() -> [String: Any] { + var dir = URL(fileURLWithPath: #file).deletingLastPathComponent() + for _ in 0..<8 { + let candidate = dir.appendingPathComponent("Runner/Info.plist") + if let plist = NSDictionary(contentsOf: candidate) as? [String: Any] { + return plist + } + dir = dir.deletingLastPathComponent() + } + return [:] +} + +let infoPlist = findInfoPlist() + +/// Return "1" if the env var is set (non-zero), "0" if explicitly set to "0", +/// else "1" if any Info.plist key is present, else `defaultValue`. +func enabled(_ envKey: String, plistKeys: String..., defaultValue: String = "0") -> String { + if let val = env[envKey] { return val == "0" ? "0" : "1" } + for key in plistKeys where infoPlist[key] != nil { return "1" } + return defaultValue +} + +let permissionDefines: [CSetting] = [ + // dart: PermissionGroup.calendar (< iOS 17) + .define("PERMISSION_EVENTS", + to: enabled("PERMISSION_EVENTS", + plistKeys: "NSCalendarsUsageDescription")), + // dart: PermissionGroup.calendarFullAccess (iOS 17+) / PermissionGroup.calendarWriteOnly (iOS 17+) + .define("PERMISSION_EVENTS_FULL_ACCESS", + to: enabled("PERMISSION_EVENTS_FULL_ACCESS", + plistKeys: "NSCalendarsFullAccessUsageDescription", + "NSCalendarsWriteOnlyAccessUsageDescription")), + // dart: PermissionGroup.reminders + .define("PERMISSION_REMINDERS", + to: enabled("PERMISSION_REMINDERS", + plistKeys: "NSRemindersUsageDescription")), + // dart: PermissionGroup.contacts + .define("PERMISSION_CONTACTS", + to: enabled("PERMISSION_CONTACTS", + plistKeys: "NSContactsUsageDescription")), + // dart: PermissionGroup.camera + .define("PERMISSION_CAMERA", + to: enabled("PERMISSION_CAMERA", + plistKeys: "NSCameraUsageDescription")), + // dart: PermissionGroup.microphone + .define("PERMISSION_MICROPHONE", + to: enabled("PERMISSION_MICROPHONE", + plistKeys: "NSMicrophoneUsageDescription")), + // dart: PermissionGroup.speech + .define("PERMISSION_SPEECH_RECOGNIZER", + to: enabled("PERMISSION_SPEECH_RECOGNIZER", + plistKeys: "NSSpeechRecognitionUsageDescription")), + // dart: PermissionGroup.photos / PermissionGroup.photosAddOnly + // NSPhotoLibraryAddUsageDescription alone also enables PhotoPermissionStrategy because the + // native code compiles photosAddOnly support under PERMISSION_PHOTOS. + .define("PERMISSION_PHOTOS", + to: enabled("PERMISSION_PHOTOS", + plistKeys: "NSPhotoLibraryUsageDescription", + "NSPhotoLibraryAddUsageDescription")), + // dart: PermissionGroup.photosAddOnly + .define("PERMISSION_PHOTOS_ADD_ONLY", + to: enabled("PERMISSION_PHOTOS_ADD_ONLY", + plistKeys: "NSPhotoLibraryAddUsageDescription")), + // dart: PermissionGroup.location / locationAlways / locationWhenInUse + .define("PERMISSION_LOCATION", + to: enabled("PERMISSION_LOCATION", + plistKeys: "NSLocationWhenInUseUsageDescription", + "NSLocationAlwaysAndWhenInUseUsageDescription")), + // dart: PermissionGroup.locationWhenInUse (only when locationAlways is NOT needed) + .define("PERMISSION_LOCATION_WHENINUSE", + to: enabled("PERMISSION_LOCATION_WHENINUSE", + plistKeys: "NSLocationWhenInUseUsageDescription")), + // dart: PermissionGroup.locationAlways + .define("PERMISSION_LOCATION_ALWAYS", + to: enabled("PERMISSION_LOCATION_ALWAYS", + plistKeys: "NSLocationAlwaysAndWhenInUseUsageDescription")), + // dart: PermissionGroup.notification (no required Info.plist key — enabled by default) + .define("PERMISSION_NOTIFICATIONS", + to: enabled("PERMISSION_NOTIFICATIONS", defaultValue: "1")), + // dart: PermissionGroup.mediaLibrary + .define("PERMISSION_MEDIA_LIBRARY", + to: enabled("PERMISSION_MEDIA_LIBRARY", + plistKeys: "NSAppleMusicUsageDescription")), + // dart: PermissionGroup.sensors + .define("PERMISSION_SENSORS", + to: enabled("PERMISSION_SENSORS", + plistKeys: "NSMotionUsageDescription")), + // dart: PermissionGroup.bluetooth + .define("PERMISSION_BLUETOOTH", + to: enabled("PERMISSION_BLUETOOTH", + plistKeys: "NSBluetoothAlwaysUsageDescription", + "NSBluetoothPeripheralUsageDescription")), + // dart: PermissionGroup.appTrackingTransparency + .define("PERMISSION_APP_TRACKING_TRANSPARENCY", + to: enabled("PERMISSION_APP_TRACKING_TRANSPARENCY", + plistKeys: "NSUserTrackingUsageDescription")), + // dart: PermissionGroup.criticalAlerts (no required Info.plist key — requires Apple entitlement, + // opt-in via env var: launchctl setenv PERMISSION_CRITICAL_ALERTS 1) + .define("PERMISSION_CRITICAL_ALERTS", + to: enabled("PERMISSION_CRITICAL_ALERTS")), + // dart: PermissionGroup.assistant + .define("PERMISSION_ASSISTANT", + to: enabled("PERMISSION_ASSISTANT", + plistKeys: "NSSiriUsageDescription")), +] + +let package = Package( + name: "permission_handler_apple", + platforms: [ + .iOS("12.0"), + ], + products: [ + .library(name: "permission-handler-apple", targets: ["permission_handler_apple"]), + ], + targets: [ + .target( + name: "permission_handler_apple", + path: "Sources/permission_handler_apple", + resources: [ + .process("PrivacyInfo.xcprivacy"), + ], + publicHeadersPath: "include", + cSettings: [ + .headerSearchPath("."), + .headerSearchPath("strategies"), + .headerSearchPath("util"), + .headerSearchPath("include/permission_handler_apple"), + ] + permissionDefines + ), + ] +) diff --git a/permission_handler_apple/ios/Classes/PermissionHandlerEnums.h b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/PermissionHandlerEnums.h similarity index 100% rename from permission_handler_apple/ios/Classes/PermissionHandlerEnums.h rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/PermissionHandlerEnums.h diff --git a/permission_handler_apple/ios/Classes/PermissionHandlerPlugin.h b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/PermissionHandlerPlugin.h similarity index 100% rename from permission_handler_apple/ios/Classes/PermissionHandlerPlugin.h rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/PermissionHandlerPlugin.h diff --git a/permission_handler_apple/ios/Classes/PermissionHandlerPlugin.m b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/PermissionHandlerPlugin.m similarity index 100% rename from permission_handler_apple/ios/Classes/PermissionHandlerPlugin.m rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/PermissionHandlerPlugin.m diff --git a/permission_handler_apple/ios/Classes/PermissionManager.h b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/PermissionManager.h similarity index 100% rename from permission_handler_apple/ios/Classes/PermissionManager.h rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/PermissionManager.h diff --git a/permission_handler_apple/ios/Classes/PermissionManager.m b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/PermissionManager.m similarity index 100% rename from permission_handler_apple/ios/Classes/PermissionManager.m rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/PermissionManager.m diff --git a/permission_handler_apple/ios/Resources/PrivacyInfo.xcprivacy b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/PrivacyInfo.xcprivacy similarity index 100% rename from permission_handler_apple/ios/Resources/PrivacyInfo.xcprivacy rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/PrivacyInfo.xcprivacy diff --git a/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/include/permission_handler_apple/PermissionHandlerPlugin.h b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/include/permission_handler_apple/PermissionHandlerPlugin.h new file mode 100644 index 000000000..2d3a848d8 --- /dev/null +++ b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/include/permission_handler_apple/PermissionHandlerPlugin.h @@ -0,0 +1,7 @@ +#import + +@class PermissionManager; + +@interface PermissionHandlerPlugin : NSObject +- (instancetype)initWithPermissionManager:(PermissionManager *)permissionManager; +@end diff --git a/permission_handler_apple/ios/Classes/strategies/AppTrackingTransparencyPermissionStrategy.h b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/AppTrackingTransparencyPermissionStrategy.h similarity index 100% rename from permission_handler_apple/ios/Classes/strategies/AppTrackingTransparencyPermissionStrategy.h rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/AppTrackingTransparencyPermissionStrategy.h diff --git a/permission_handler_apple/ios/Classes/strategies/AppTrackingTransparencyPermissionStrategy.m b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/AppTrackingTransparencyPermissionStrategy.m similarity index 100% rename from permission_handler_apple/ios/Classes/strategies/AppTrackingTransparencyPermissionStrategy.m rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/AppTrackingTransparencyPermissionStrategy.m diff --git a/permission_handler_apple/ios/Classes/strategies/AssistantPermissionStrategy.h b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/AssistantPermissionStrategy.h similarity index 100% rename from permission_handler_apple/ios/Classes/strategies/AssistantPermissionStrategy.h rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/AssistantPermissionStrategy.h diff --git a/permission_handler_apple/ios/Classes/strategies/AssistantPermissionStrategy.m b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/AssistantPermissionStrategy.m similarity index 100% rename from permission_handler_apple/ios/Classes/strategies/AssistantPermissionStrategy.m rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/AssistantPermissionStrategy.m diff --git a/permission_handler_apple/ios/Classes/strategies/AudioVideoPermissionStrategy.h b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/AudioVideoPermissionStrategy.h similarity index 100% rename from permission_handler_apple/ios/Classes/strategies/AudioVideoPermissionStrategy.h rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/AudioVideoPermissionStrategy.h diff --git a/permission_handler_apple/ios/Classes/strategies/AudioVideoPermissionStrategy.m b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/AudioVideoPermissionStrategy.m similarity index 100% rename from permission_handler_apple/ios/Classes/strategies/AudioVideoPermissionStrategy.m rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/AudioVideoPermissionStrategy.m diff --git a/permission_handler_apple/ios/Classes/strategies/BackgroundRefreshStrategy.h b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/BackgroundRefreshStrategy.h similarity index 92% rename from permission_handler_apple/ios/Classes/strategies/BackgroundRefreshStrategy.h rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/BackgroundRefreshStrategy.h index 2dfe0032c..2f6797453 100644 --- a/permission_handler_apple/ios/Classes/strategies/BackgroundRefreshStrategy.h +++ b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/BackgroundRefreshStrategy.h @@ -6,6 +6,7 @@ // #import +#import #import "PermissionStrategy.h" NS_ASSUME_NONNULL_BEGIN diff --git a/permission_handler_apple/ios/Classes/strategies/BackgroundRefreshStrategy.m b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/BackgroundRefreshStrategy.m similarity index 100% rename from permission_handler_apple/ios/Classes/strategies/BackgroundRefreshStrategy.m rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/BackgroundRefreshStrategy.m diff --git a/permission_handler_apple/ios/Classes/strategies/BluetoothPermissionStrategy.h b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/BluetoothPermissionStrategy.h similarity index 100% rename from permission_handler_apple/ios/Classes/strategies/BluetoothPermissionStrategy.h rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/BluetoothPermissionStrategy.h diff --git a/permission_handler_apple/ios/Classes/strategies/BluetoothPermissionStrategy.m b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/BluetoothPermissionStrategy.m similarity index 100% rename from permission_handler_apple/ios/Classes/strategies/BluetoothPermissionStrategy.m rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/BluetoothPermissionStrategy.m diff --git a/permission_handler_apple/ios/Classes/strategies/ContactPermissionStrategy.h b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/ContactPermissionStrategy.h similarity index 100% rename from permission_handler_apple/ios/Classes/strategies/ContactPermissionStrategy.h rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/ContactPermissionStrategy.h diff --git a/permission_handler_apple/ios/Classes/strategies/ContactPermissionStrategy.m b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/ContactPermissionStrategy.m similarity index 100% rename from permission_handler_apple/ios/Classes/strategies/ContactPermissionStrategy.m rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/ContactPermissionStrategy.m diff --git a/permission_handler_apple/ios/Classes/strategies/CriticalAlertsPermissionStrategy.h b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/CriticalAlertsPermissionStrategy.h similarity index 95% rename from permission_handler_apple/ios/Classes/strategies/CriticalAlertsPermissionStrategy.h rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/CriticalAlertsPermissionStrategy.h index 44b857f48..fbc332ccd 100644 --- a/permission_handler_apple/ios/Classes/strategies/CriticalAlertsPermissionStrategy.h +++ b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/CriticalAlertsPermissionStrategy.h @@ -10,6 +10,7 @@ #if PERMISSION_CRITICAL_ALERTS +#import #import @interface CriticalAlertsPermissionStrategy : NSObject diff --git a/permission_handler_apple/ios/Classes/strategies/CriticalAlertsPermissionStrategy.m b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/CriticalAlertsPermissionStrategy.m similarity index 100% rename from permission_handler_apple/ios/Classes/strategies/CriticalAlertsPermissionStrategy.m rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/CriticalAlertsPermissionStrategy.m diff --git a/permission_handler_apple/ios/Classes/strategies/EventPermissionStrategy.h b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/EventPermissionStrategy.h similarity index 100% rename from permission_handler_apple/ios/Classes/strategies/EventPermissionStrategy.h rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/EventPermissionStrategy.h diff --git a/permission_handler_apple/ios/Classes/strategies/EventPermissionStrategy.m b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/EventPermissionStrategy.m similarity index 100% rename from permission_handler_apple/ios/Classes/strategies/EventPermissionStrategy.m rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/EventPermissionStrategy.m diff --git a/permission_handler_apple/ios/Classes/strategies/LocationPermissionStrategy.h b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/LocationPermissionStrategy.h similarity index 96% rename from permission_handler_apple/ios/Classes/strategies/LocationPermissionStrategy.h rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/LocationPermissionStrategy.h index 52bcc89dc..082e9d5b4 100644 --- a/permission_handler_apple/ios/Classes/strategies/LocationPermissionStrategy.h +++ b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/LocationPermissionStrategy.h @@ -8,6 +8,7 @@ #if PERMISSION_LOCATION || PERMISSION_LOCATION_WHENINUSE || PERMISSION_LOCATION_ALWAYS +#import #import @interface LocationPermissionStrategy : NSObject diff --git a/permission_handler_apple/ios/Classes/strategies/LocationPermissionStrategy.m b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/LocationPermissionStrategy.m similarity index 100% rename from permission_handler_apple/ios/Classes/strategies/LocationPermissionStrategy.m rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/LocationPermissionStrategy.m diff --git a/permission_handler_apple/ios/Classes/strategies/MediaLibraryPermissionStrategy.h b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/MediaLibraryPermissionStrategy.h similarity index 100% rename from permission_handler_apple/ios/Classes/strategies/MediaLibraryPermissionStrategy.h rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/MediaLibraryPermissionStrategy.h diff --git a/permission_handler_apple/ios/Classes/strategies/MediaLibraryPermissionStrategy.m b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/MediaLibraryPermissionStrategy.m similarity index 100% rename from permission_handler_apple/ios/Classes/strategies/MediaLibraryPermissionStrategy.m rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/MediaLibraryPermissionStrategy.m diff --git a/permission_handler_apple/ios/Classes/strategies/NotificationPermissionStrategy.h b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/NotificationPermissionStrategy.h similarity index 95% rename from permission_handler_apple/ios/Classes/strategies/NotificationPermissionStrategy.h rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/NotificationPermissionStrategy.h index df5a795d6..73059a4b5 100644 --- a/permission_handler_apple/ios/Classes/strategies/NotificationPermissionStrategy.h +++ b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/NotificationPermissionStrategy.h @@ -10,6 +10,7 @@ #if PERMISSION_NOTIFICATIONS +#import #import @interface NotificationPermissionStrategy : NSObject diff --git a/permission_handler_apple/ios/Classes/strategies/NotificationPermissionStrategy.m b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/NotificationPermissionStrategy.m similarity index 100% rename from permission_handler_apple/ios/Classes/strategies/NotificationPermissionStrategy.m rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/NotificationPermissionStrategy.m diff --git a/permission_handler_apple/ios/Classes/strategies/PermissionStrategy.h b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/PermissionStrategy.h similarity index 100% rename from permission_handler_apple/ios/Classes/strategies/PermissionStrategy.h rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/PermissionStrategy.h diff --git a/permission_handler_apple/ios/Classes/strategies/PhonePermissionStrategy.h b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/PhonePermissionStrategy.h similarity index 92% rename from permission_handler_apple/ios/Classes/strategies/PhonePermissionStrategy.h rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/PhonePermissionStrategy.h index a90cfc81f..c36d999b1 100644 --- a/permission_handler_apple/ios/Classes/strategies/PhonePermissionStrategy.h +++ b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/PhonePermissionStrategy.h @@ -6,6 +6,7 @@ // #import +#import #import "PermissionStrategy.h" NS_ASSUME_NONNULL_BEGIN diff --git a/permission_handler_apple/ios/Classes/strategies/PhonePermissionStrategy.m b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/PhonePermissionStrategy.m similarity index 100% rename from permission_handler_apple/ios/Classes/strategies/PhonePermissionStrategy.m rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/PhonePermissionStrategy.m diff --git a/permission_handler_apple/ios/Classes/strategies/PhotoPermissionStrategy.h b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/PhotoPermissionStrategy.h similarity index 100% rename from permission_handler_apple/ios/Classes/strategies/PhotoPermissionStrategy.h rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/PhotoPermissionStrategy.h diff --git a/permission_handler_apple/ios/Classes/strategies/PhotoPermissionStrategy.m b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/PhotoPermissionStrategy.m similarity index 100% rename from permission_handler_apple/ios/Classes/strategies/PhotoPermissionStrategy.m rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/PhotoPermissionStrategy.m diff --git a/permission_handler_apple/ios/Classes/strategies/SensorPermissionStrategy.h b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/SensorPermissionStrategy.h similarity index 100% rename from permission_handler_apple/ios/Classes/strategies/SensorPermissionStrategy.h rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/SensorPermissionStrategy.h diff --git a/permission_handler_apple/ios/Classes/strategies/SensorPermissionStrategy.m b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/SensorPermissionStrategy.m similarity index 100% rename from permission_handler_apple/ios/Classes/strategies/SensorPermissionStrategy.m rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/SensorPermissionStrategy.m diff --git a/permission_handler_apple/ios/Classes/strategies/SpeechPermissionStrategy.h b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/SpeechPermissionStrategy.h similarity index 100% rename from permission_handler_apple/ios/Classes/strategies/SpeechPermissionStrategy.h rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/SpeechPermissionStrategy.h diff --git a/permission_handler_apple/ios/Classes/strategies/SpeechPermissionStrategy.m b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/SpeechPermissionStrategy.m similarity index 100% rename from permission_handler_apple/ios/Classes/strategies/SpeechPermissionStrategy.m rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/SpeechPermissionStrategy.m diff --git a/permission_handler_apple/ios/Classes/strategies/StoragePermissionStrategy.h b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/StoragePermissionStrategy.h similarity index 100% rename from permission_handler_apple/ios/Classes/strategies/StoragePermissionStrategy.h rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/StoragePermissionStrategy.h diff --git a/permission_handler_apple/ios/Classes/strategies/StoragePermissionStrategy.m b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/StoragePermissionStrategy.m similarity index 100% rename from permission_handler_apple/ios/Classes/strategies/StoragePermissionStrategy.m rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/StoragePermissionStrategy.m diff --git a/permission_handler_apple/ios/Classes/strategies/UnknownPermissionStrategy.h b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/UnknownPermissionStrategy.h similarity index 100% rename from permission_handler_apple/ios/Classes/strategies/UnknownPermissionStrategy.h rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/UnknownPermissionStrategy.h diff --git a/permission_handler_apple/ios/Classes/strategies/UnknownPermissionStrategy.m b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/UnknownPermissionStrategy.m similarity index 100% rename from permission_handler_apple/ios/Classes/strategies/UnknownPermissionStrategy.m rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/UnknownPermissionStrategy.m diff --git a/permission_handler_apple/ios/Classes/util/Codec.h b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/util/Codec.h similarity index 100% rename from permission_handler_apple/ios/Classes/util/Codec.h rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/util/Codec.h diff --git a/permission_handler_apple/ios/Classes/util/Codec.m b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/util/Codec.m similarity index 100% rename from permission_handler_apple/ios/Classes/util/Codec.m rename to permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/util/Codec.m diff --git a/permission_handler_apple/pubspec.yaml b/permission_handler_apple/pubspec.yaml index 4abd13fb3..496e1aca6 100644 --- a/permission_handler_apple/pubspec.yaml +++ b/permission_handler_apple/pubspec.yaml @@ -2,7 +2,7 @@ name: permission_handler_apple description: Permission plugin for Flutter. This plugin provides the iOS API to request and check permissions. repository: https://github.com/baseflow/flutter-permission-handler issue_tracker: https://github.com/Baseflow/flutter-permission-handler/issues -version: 9.4.7 +version: 9.4.8 environment: sdk: ">=2.18.0 <4.0.0" From 7c3add968cced02c37eb982d6b443a92e0fadd81 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Fri, 29 May 2026 16:29:07 +0200 Subject: [PATCH 03/26] Update version to 12.0.2 --- permission_handler/CHANGELOG.md | 4 + permission_handler/README.md | 128 ++++++++++++++++---------------- permission_handler/pubspec.yaml | 2 +- 3 files changed, 69 insertions(+), 65 deletions(-) diff --git a/permission_handler/CHANGELOG.md b/permission_handler/CHANGELOG.md index 576f07f1b..13eea61da 100644 --- a/permission_handler/CHANGELOG.md +++ b/permission_handler/CHANGELOG.md @@ -1,3 +1,7 @@ +## 12.0.2 + +- Updates the documentation to include instructions on Swift Package Manager (SPM) support. + ## 12.0.1 - Updates the correspondence between permission groups and the key values of Info.plist in the README.md. diff --git a/permission_handler/README.md b/permission_handler/README.md index 3e17adf3e..15a80e1e4 100644 --- a/permission_handler/README.md +++ b/permission_handler/README.md @@ -52,7 +52,70 @@ In general, it's sufficient to add permission only to the `main` version.
-iOS (click to expand) +iOS - Swift Package Manager (click to expand) + +> Requires Flutter 3.24.0 or higher and Xcode 15.0 or higher. + +With SPM, `Package.swift` automatically detects which permissions to enable by reading your app's `Info.plist`. A permission is compiled in when its corresponding usage description key is present: + +| Permission group | Info.plist key | +|---|---| +| `PermissionGroup.calendar` (< iOS 17) | `NSCalendarsUsageDescription` | +| `PermissionGroup.calendarWriteOnly` (iOS 17+) | `NSCalendarsWriteOnlyAccessUsageDescription` | +| `PermissionGroup.calendarFullAccess` (iOS 17+) | `NSCalendarsFullAccessUsageDescription` | +| `PermissionGroup.reminders` | `NSRemindersUsageDescription` | +| `PermissionGroup.contacts` | `NSContactsUsageDescription` | +| `PermissionGroup.camera` | `NSCameraUsageDescription` | +| `PermissionGroup.microphone` | `NSMicrophoneUsageDescription` | +| `PermissionGroup.speech` | `NSSpeechRecognitionUsageDescription` | +| `PermissionGroup.photos` | `NSPhotoLibraryUsageDescription` | +| `PermissionGroup.photosAddOnly` | `NSPhotoLibraryAddUsageDescription` | +| `PermissionGroup.location` / `locationWhenInUse` | `NSLocationWhenInUseUsageDescription` | +| `PermissionGroup.locationAlways` | `NSLocationAlwaysAndWhenInUseUsageDescription` | +| `PermissionGroup.mediaLibrary` | `NSAppleMusicUsageDescription` | +| `PermissionGroup.sensors` | `NSMotionUsageDescription` | +| `PermissionGroup.bluetooth` | `NSBluetoothAlwaysUsageDescription` | +| `PermissionGroup.appTrackingTransparency` | `NSUserTrackingUsageDescription` | +| `PermissionGroup.assistant` | `NSSiriUsageDescription` | +| `PermissionGroup.notification` | *(enabled by default — see below)* | +| `PermissionGroup.criticalAlerts` | *(disabled by default — see below)* | + +Because you must already add these keys to `Info.plist` for any permission to work, no additional configuration file is needed. + +#### Special cases: permissions without an Info.plist key + +**`PermissionGroup.notification`** has no required `Info.plist` key and is **enabled by default**. To opt out, disable it via environment variable before building: + +```bash +# When building from terminal (flutter run / flutter build) +export PERMISSION_NOTIFICATIONS=0 + +# When building from Xcode GUI (set once per Mac session, then restart Xcode) +launchctl setenv PERMISSION_NOTIFICATIONS 0 +``` + +**`PermissionGroup.criticalAlerts`** requires a [special entitlement](https://developer.apple.com/documentation/usernotifications/asking-permission-to-use-notifications) granted by Apple and is **disabled by default** to avoid compiling unused code into apps that don't need it. Enable it explicitly: + +```bash +# When building from terminal +export PERMISSION_CRITICAL_ALERTS=1 + +# When building from Xcode GUI +launchctl setenv PERMISSION_CRITICAL_ALERTS 1 +``` + +**After changing any env var or Info.plist key**, clear Xcode's package cache once so `Package.swift` is re-evaluated: + +```bash +rm -rf ~/Library/Developer/Xcode/DerivedData +``` + +Then run `flutter build ios` or rebuild in Xcode as usual. + +
+ +
+iOS - CocoaPods (click to expand) Add permission to your `Info.plist` file. [Here](https://github.com/Baseflow/flutter-permission-handler/blob/master/permission_handler/example/ios/Runner/Info.plist)'s an example `Info.plist` with a complete list of all possible permissions. @@ -174,69 +237,6 @@ You must list the permission you want to use in your application:
-
-Swift Package Manager (SPM) - -> Requires Flutter 3.24.0 or higher and Xcode 15.0 or higher. - -With SPM, `Package.swift` automatically detects which permissions to enable by reading your app's `Info.plist`. A permission is compiled in when its corresponding usage description key is present: - -| Permission group | Info.plist key | -|---|---| -| `PermissionGroup.calendar` (< iOS 17) | `NSCalendarsUsageDescription` | -| `PermissionGroup.calendarWriteOnly` (iOS 17+) | `NSCalendarsWriteOnlyAccessUsageDescription` | -| `PermissionGroup.calendarFullAccess` (iOS 17+) | `NSCalendarsFullAccessUsageDescription` | -| `PermissionGroup.reminders` | `NSRemindersUsageDescription` | -| `PermissionGroup.contacts` | `NSContactsUsageDescription` | -| `PermissionGroup.camera` | `NSCameraUsageDescription` | -| `PermissionGroup.microphone` | `NSMicrophoneUsageDescription` | -| `PermissionGroup.speech` | `NSSpeechRecognitionUsageDescription` | -| `PermissionGroup.photos` | `NSPhotoLibraryUsageDescription` | -| `PermissionGroup.photosAddOnly` | `NSPhotoLibraryAddUsageDescription` | -| `PermissionGroup.location` / `locationWhenInUse` | `NSLocationWhenInUseUsageDescription` | -| `PermissionGroup.locationAlways` | `NSLocationAlwaysAndWhenInUseUsageDescription` | -| `PermissionGroup.mediaLibrary` | `NSAppleMusicUsageDescription` | -| `PermissionGroup.sensors` | `NSMotionUsageDescription` | -| `PermissionGroup.bluetooth` | `NSBluetoothAlwaysUsageDescription` | -| `PermissionGroup.appTrackingTransparency` | `NSUserTrackingUsageDescription` | -| `PermissionGroup.assistant` | `NSSiriUsageDescription` | -| `PermissionGroup.notification` | *(enabled by default — see below)* | -| `PermissionGroup.criticalAlerts` | *(disabled by default — see below)* | - -Because you must already add these keys to `Info.plist` for any permission to work, no additional configuration file is needed. - -#### Special cases: permissions without an Info.plist key - -**`PermissionGroup.notification`** has no required `Info.plist` key and is **enabled by default**. To opt out, disable it via environment variable before building: - -```bash -# When building from terminal (flutter run / flutter build) -export PERMISSION_NOTIFICATIONS=0 - -# When building from Xcode GUI (set once per Mac session, then restart Xcode) -launchctl setenv PERMISSION_NOTIFICATIONS 0 -``` - -**`PermissionGroup.criticalAlerts`** requires a [special entitlement](https://developer.apple.com/documentation/usernotifications/asking-permission-to-use-notifications) granted by Apple and is **disabled by default** to avoid compiling unused code into apps that don't need it. Enable it explicitly: - -```bash -# When building from terminal -export PERMISSION_CRITICAL_ALERTS=1 - -# When building from Xcode GUI -launchctl setenv PERMISSION_CRITICAL_ALERTS 1 -``` - -**After changing any env var or Info.plist key**, clear Xcode's package cache once so `Package.swift` is re-evaluated: - -```bash -rm -rf ~/Library/Developer/Xcode/DerivedData -``` - -Then run `flutter build ios` or rebuild in Xcode as usual. - -
- ## How to use There are a number of [`Permission`](https://pub.dev/documentation/permission_handler_platform_interface/latest/permission_handler_platform_interface/Permission-class.html#constants)s. diff --git a/permission_handler/pubspec.yaml b/permission_handler/pubspec.yaml index 3e0861cd9..1c94a6d59 100644 --- a/permission_handler/pubspec.yaml +++ b/permission_handler/pubspec.yaml @@ -2,7 +2,7 @@ name: permission_handler description: Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions. repository: https://github.com/baseflow/flutter-permission-handler issue_tracker: https://github.com/Baseflow/flutter-permission-handler/issues -version: 12.0.1 +version: 12.0.2 environment: sdk: ^3.5.0 From 6be38d46a94ca6e74e7aa7a21c913959dd4c2050 Mon Sep 17 00:00:00 2001 From: Gauhar Date: Mon, 1 Jun 2026 13:29:05 +0500 Subject: [PATCH 04/26] fix typo in README about the compileSdkVersion (#1472) --- permission_handler/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/permission_handler/README.md b/permission_handler/README.md index 15a80e1e4..496043367 100644 --- a/permission_handler/README.md +++ b/permission_handler/README.md @@ -33,7 +33,7 @@ android.useAndroidX=true android.enableJetifier=true ``` -2. Make sure you set the `compileSdkVersion` in your "android/app/build.gradle" file to 33: +2. Make sure you set the `compileSdkVersion` in your "android/app/build.gradle" file to 35: ```gradle android { From db4e61d00733739868bab27fa7bd6e2dea8ed0be Mon Sep 17 00:00:00 2001 From: harshit saini Date: Mon, 1 Jun 2026 14:00:05 +0530 Subject: [PATCH 05/26] docs: fix completeSdkVersion typo to compileSdkVersion (#1494) Fix incorrect Android SDK parameter name in documentation. "completeSdkVersion" is not a valid Android parameter. The correct name is "compileSdkVersion". This prevents developer confusion when following the troubleshooting guide. --- permission_handler/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/permission_handler/README.md b/permission_handler/README.md index 496043367..2cf16fe33 100644 --- a/permission_handler/README.md +++ b/permission_handler/README.md @@ -357,7 +357,7 @@ Starting with Android 10, apps are required to first obtain permission to read t ### onRequestPermissionsResult is called without results. What can I do? -It is probably caused by a difference between completeSdkVersion and targetSdkVersion. It can be depending on the flutter version that you use. `targetSdkVersion = flutter.targetSdkVersion` in the app/build.gradle indicates that the targetSdkVersion is flutter version dependant. For more information: [issue 1222](https://github.com/Baseflow/flutter-permission-handler/issues/1222) +It is probably caused by a difference between compileSdkVersion and targetSdkVersion. It can be depending on the flutter version that you use. `targetSdkVersion = flutter.targetSdkVersion` in the app/build.gradle indicates that the targetSdkVersion is flutter version dependant. For more information: [issue 1222](https://github.com/Baseflow/flutter-permission-handler/issues/1222) ### Checking or requesting a permission terminates the application on iOS. What can I do? From 83b6f3419f0210ff316185203e05b2f0f1eb44b2 Mon Sep 17 00:00:00 2001 From: AlHomam Sultan Date: Mon, 1 Jun 2026 11:31:07 +0300 Subject: [PATCH 06/26] Improved the iOS Setup section in permission_handler/README.md (#1488) After version 8.0.0, permission handler no longer uses all permissions on iOS, but this was not explained clearly in the README. This is my attempt to fix that small mistake. --- permission_handler/README.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/permission_handler/README.md b/permission_handler/README.md index 2cf16fe33..dc411f4da 100644 --- a/permission_handler/README.md +++ b/permission_handler/README.md @@ -120,13 +120,14 @@ Then run `flutter build ios` or rebuild in Xcode as usual. Add permission to your `Info.plist` file. [Here](https://github.com/Baseflow/flutter-permission-handler/blob/master/permission_handler/example/ios/Runner/Info.plist)'s an example `Info.plist` with a complete list of all possible permissions. -> IMPORTANT: ~~You will have to include all permission options when you want to submit your App.~~ This is because the `permission_handler` plugin touches all different SDKs and because the static code analyzer (run by Apple upon App submission) detects this and will assert if it cannot find a matching permission option in the `Info.plist`. More information about this can be found [here](https://github.com/Baseflow/flutter-permission-handler/issues/26). +> IMPORTANT: ~~You will have to include all permission options when you want to submit your App. This is because the `permission_handler` plugin touches all different SDKs and because the static code analyzer (run by Apple upon App submission) detects this and will assert if it cannot find a matching permission option in the `Info.plist`. More information about this can be found [here](https://github.com/Baseflow/flutter-permission-handler/issues/26).~~ + This has been fixed since version 8.0.0, now permission_handler by default excludes all permissions and developers only have to enable those that the app really needs. The permission_handler plugin use [macros](https://github.com/Baseflow/flutter-permission-handler/blob/master/permission_handler_apple/ios/Classes/PermissionHandlerEnums.h) to control whether a permission is enabled. You must list the permission you want to use in your application: -1. Add the following to your `Podfile` file: +1. Add the following to your `Podfile`'s `post_install` block: ```ruby post_install do |installer| @@ -136,7 +137,8 @@ You must list the permission you want to use in your application: target.build_configurations.each do |config| # You can remove unused permissions here # for more information: https://github.com/Baseflow/flutter-permission-handler/blob/main/permission_handler_apple/ios/Classes/PermissionHandlerEnums.h - # e.g. when you don't need camera permission, just add 'PERMISSION_CAMERA=0' + # When you don't need a permission, just change its value to 0 + # e.g. 'PERMISSION_CAMERA=0' instead of 'PERMISSION_CAMERA=1' config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ '$(inherited)', @@ -199,16 +201,18 @@ You must list the permission you want to use in your application: end ``` -2. Remove the `#` character in front of the permission you want to use. For example, if you need access to the calendar make sure the code looks like this: +2. For the permissions you *want* to use, keep them as is. For example, if you need access to the calendar make sure the code looks like this: ```ruby ## dart: PermissionGroup.calendar 'PERMISSION_EVENTS=1', ``` +3. When you **DON'T** need a permission, change its value to `0` e.g. `'PERMISSION_CAMERA=0'` instead of `'PERMISSION_CAMERA=1'` -3. Delete the corresponding permission description in `Info.plist` - e.g. when you don't need camera permission, just delete 'NSCameraUsageDescription' - The following lists the relationship between `Permission` and `The key of Info.plist`: +3. And delete the corresponding permission description in `Info.plist` + e.g. when you don't need camera permission, just delete `'NSCameraUsageDescription'` + +The following lists the relationship between `Permission` and `The key of Info.plist`: | Permission | Info.plist | Macro | |---------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|----------------------------------------| From 977f72cd2cbd9ba7a74e3c4aafbf3864af9929fe Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Mon, 1 Jun 2026 11:49:04 +0200 Subject: [PATCH 07/26] Rewrite copyleft code from stackoverflow > > Co-authored-by: Novarest --- permission_handler_apple/example/.gitignore | 1 + .../ios/Runner.xcodeproj/project.pbxproj | 4 ++++ permission_handler_apple/example/pubspec.yaml | 2 +- .../strategies/PhonePermissionStrategy.m | 18 +++++++++--------- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/permission_handler_apple/example/.gitignore b/permission_handler_apple/example/.gitignore index 2156e772e..260fe82ac 100644 --- a/permission_handler_apple/example/.gitignore +++ b/permission_handler_apple/example/.gitignore @@ -60,6 +60,7 @@ build/ **/ios/Flutter/app.flx **/ios/Flutter/app.zip **/ios/Flutter/flutter_assets/ +**/ios/Flutter/ephemeral/ **/ios/ServiceDefinitions.json **/ios/Runner/GeneratedPluginRegistrant.* diff --git a/permission_handler_apple/example/ios/Runner.xcodeproj/project.pbxproj b/permission_handler_apple/example/ios/Runner.xcodeproj/project.pbxproj index b19cad307..ee00f0144 100644 --- a/permission_handler_apple/example/ios/Runner.xcodeproj/project.pbxproj +++ b/permission_handler_apple/example/ios/Runner.xcodeproj/project.pbxproj @@ -36,6 +36,8 @@ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 784666492D4C4C64000A1A5F /* FlutterFramework */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterFramework; path = Flutter/ephemeral/Packages/.packages/FlutterFramework; sourceTree = ""; }; + 78DABEA22ED26510000E7860 /* permission_handler_apple */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = permission_handler_apple; path = ../../ios/permission_handler_apple; sourceTree = ""; }; 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 862A53EA392D32566500E869 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; @@ -86,6 +88,8 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( + 78DABEA22ED26510000E7860 /* permission_handler_apple */, + 784666492D4C4C64000A1A5F /* FlutterFramework */, 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, diff --git a/permission_handler_apple/example/pubspec.yaml b/permission_handler_apple/example/pubspec.yaml index 81b6f2dc4..71aaacd79 100644 --- a/permission_handler_apple/example/pubspec.yaml +++ b/permission_handler_apple/example/pubspec.yaml @@ -21,7 +21,7 @@ dev_dependencies: # the parent directory to use the current plugin's version. path: ../ - url_launcher: ^6.0.12 + url_launcher: ^6.3.2 flutter: uses-material-design: true diff --git a/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/PhonePermissionStrategy.m b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/PhonePermissionStrategy.m index 37222849d..6944a9835 100644 --- a/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/PhonePermissionStrategy.m +++ b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/strategies/PhonePermissionStrategy.m @@ -17,8 +17,9 @@ - (PermissionStatus)checkPermissionStatus:(PermissionGroup)permission { } - (void)checkServiceStatus:(PermissionGroup)permission completionHandler:(ServiceStatusHandler)completionHandler { - // https://stackoverflow.com/a/5095058 - if (![[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"tel://"]]) { + UIApplication *app = [UIApplication sharedApplication]; + NSURL *telURL = [NSURL URLWithString:@"tel://"]; + if (![app canOpenURL:telURL]) { completionHandler(ServiceStatusNotApplicable); } completionHandler([self canDevicePlaceAPhoneCall] ? ServiceStatusEnabled : ServiceStatusDisabled); @@ -52,15 +53,14 @@ -(bool) canDevicePlaceAPhoneCall { } -(bool)canPlacePhoneCallWithCarrier:(CTCarrier *)carrier { - // https://stackoverflow.com/a/11595365 - NSString *mnc = [carrier mobileNetworkCode]; - if (([mnc length] == 0) || ([mnc isEqualToString:@"65535"])) { - // Device cannot place a call at this time. SIM might be removed. + NSString *networkCode = [carrier mobileNetworkCode]; + if (networkCode.length == 0 || [networkCode isEqualToString:@"65535"]) { + // Device is unable to initiate a call at this time. SIM might be missing. return NO; - } else { - // Device can place a phone call - return YES; } + + // Mobile Network Code is valid and device can initiate a call + return YES; } @end From 2e46a3f31827d3f978e02d761e5601c3e3a831c1 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Mon, 1 Jun 2026 11:57:24 +0200 Subject: [PATCH 08/26] Bump permission_handler_apple to 9.4.9 --- permission_handler_apple/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/permission_handler_apple/CHANGELOG.md b/permission_handler_apple/CHANGELOG.md index 05a0f3485..f60ecef2a 100644 --- a/permission_handler_apple/CHANGELOG.md +++ b/permission_handler_apple/CHANGELOG.md @@ -1,3 +1,7 @@ +## 9.4.9 + +* Rewrites copyleft code from stackoverflow to fix compliance issue. + ## 9.4.8 * Adds Swift Package Manager (SPM) support for Flutter 3.24+. Permissions are From 7ab3962ca31033873f6bcb8396fbec2d333fed84 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Mon, 1 Jun 2026 12:04:02 +0200 Subject: [PATCH 09/26] Bump permission_handler to 12.0.3 --- permission_handler/CHANGELOG.md | 5 +++++ permission_handler/pubspec.yaml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/permission_handler/CHANGELOG.md b/permission_handler/CHANGELOG.md index 13eea61da..e44cb8f14 100644 --- a/permission_handler/CHANGELOG.md +++ b/permission_handler/CHANGELOG.md @@ -1,3 +1,8 @@ +## 12.0.3 + +- Rewrites the "iOS - CocaoPods" section in the README.md to match version 8.0.0 of the permission_handler. +- Fixes several smaller typo's in the README.md. + ## 12.0.2 - Updates the documentation to include instructions on Swift Package Manager (SPM) support. diff --git a/permission_handler/pubspec.yaml b/permission_handler/pubspec.yaml index 1c94a6d59..60ee8f94e 100644 --- a/permission_handler/pubspec.yaml +++ b/permission_handler/pubspec.yaml @@ -2,7 +2,7 @@ name: permission_handler description: Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions. repository: https://github.com/baseflow/flutter-permission-handler issue_tracker: https://github.com/Baseflow/flutter-permission-handler/issues -version: 12.0.2 +version: 12.0.3 environment: sdk: ^3.5.0 From bfa56cfcb1d8e7e622dd270257cc3e0218bec925 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Mon, 1 Jun 2026 12:05:04 +0200 Subject: [PATCH 10/26] Bump permission_handler_apple to 9.4.9 --- permission_handler_apple/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/permission_handler_apple/pubspec.yaml b/permission_handler_apple/pubspec.yaml index 496e1aca6..a03947478 100644 --- a/permission_handler_apple/pubspec.yaml +++ b/permission_handler_apple/pubspec.yaml @@ -2,7 +2,7 @@ name: permission_handler_apple description: Permission plugin for Flutter. This plugin provides the iOS API to request and check permissions. repository: https://github.com/baseflow/flutter-permission-handler issue_tracker: https://github.com/Baseflow/flutter-permission-handler/issues -version: 9.4.8 +version: 9.4.9 environment: sdk: ">=2.18.0 <4.0.0" From 6bf322b16b4f03e94226cd39e50d204ba0a5dd37 Mon Sep 17 00:00:00 2001 From: oleh <113197666+olekeke999@users.noreply.github.com> Date: Fri, 12 Jun 2026 13:09:01 +0300 Subject: [PATCH 11/26] Changed Info.plist lookup (#1542) * Changed Info.plist lookup * Updated pubspec and changelog * Refactored look up of infoplist in the Package.swift * Reverting SPM changes in the example app. * fixed changelog. --- permission_handler_apple/CHANGELOG.md | 5 ++ .../permission_handler_apple/Package.swift | 50 ++++++++++++++++--- permission_handler_apple/pubspec.yaml | 2 +- 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/permission_handler_apple/CHANGELOG.md b/permission_handler_apple/CHANGELOG.md index f60ecef2a..8065518a1 100644 --- a/permission_handler_apple/CHANGELOG.md +++ b/permission_handler_apple/CHANGELOG.md @@ -1,3 +1,8 @@ +## 9.4.10 + +* Fixed Info.plist lookup in Package.swift to auto-apply permissions. +* You may see build log "Plugin permission_handler_apple has a Package.swift for ios but is missing a dependency on FlutterFramework". FlutterFramework hasn't been added intentionally because it requires to bump flutter constraint to >=3.41.0. + ## 9.4.9 * Rewrites copyleft code from stackoverflow to fix compliance issue. diff --git a/permission_handler_apple/ios/permission_handler_apple/Package.swift b/permission_handler_apple/ios/permission_handler_apple/Package.swift index 5a9e95281..88170e032 100644 --- a/permission_handler_apple/ios/permission_handler_apple/Package.swift +++ b/permission_handler_apple/ios/permission_handler_apple/Package.swift @@ -21,17 +21,49 @@ import Foundation let env = ProcessInfo.processInfo.environment -/// Walk up from Package.swift looking for Runner/Info.plist. -/// Works when the package is resolved via Flutter's .symlinks/ directory. +func loadInfoPlist(at url: URL) -> [String: Any]? { + NSDictionary(contentsOf: url) as? [String: Any] +} + +/// Find the host app's Runner/Info.plist. +/// +/// Flutter can resolve this package through a local plugin path, a generated +/// SPM package, or an Xcode package cache. Look for a Flutter app root by +/// walking up from the package and current working directory, using pubspec.yaml +/// next to ios/Runner/Info.plist as the app-root anchor. func findInfoPlist() -> [String: Any] { - var dir = URL(fileURLWithPath: #file).deletingLastPathComponent() - for _ in 0..<8 { - let candidate = dir.appendingPathComponent("Runner/Info.plist") - if let plist = NSDictionary(contentsOf: candidate) as? [String: Any] { - return plist + let fileManager = FileManager.default + + let packageDir = URL(fileURLWithPath: #file).deletingLastPathComponent() + let currentDir = URL(fileURLWithPath: fileManager.currentDirectoryPath) + + var visited = Set() + + for root in [packageDir, currentDir] { + var dir = root + + for _ in 0..<10 { + let key = dir.resolvingSymlinksInPath().path + guard visited.insert(key).inserted else { + break + } + + let pubspecURL = dir.appendingPathComponent("pubspec.yaml") + let plistURL = dir.appendingPathComponent("ios/Runner/Info.plist") + + if fileManager.fileExists(atPath: pubspecURL.path), + let plist = loadInfoPlist(at: plistURL) { + return plist + } + + let parent = dir.deletingLastPathComponent() + if parent.path == dir.path { + break + } + dir = parent } - dir = dir.deletingLastPathComponent() } + return [:] } @@ -137,9 +169,11 @@ let package = Package( products: [ .library(name: "permission-handler-apple", targets: ["permission_handler_apple"]), ], + dependencies: [], targets: [ .target( name: "permission_handler_apple", + dependencies: [], path: "Sources/permission_handler_apple", resources: [ .process("PrivacyInfo.xcprivacy"), diff --git a/permission_handler_apple/pubspec.yaml b/permission_handler_apple/pubspec.yaml index a03947478..97963c0ea 100644 --- a/permission_handler_apple/pubspec.yaml +++ b/permission_handler_apple/pubspec.yaml @@ -2,7 +2,7 @@ name: permission_handler_apple description: Permission plugin for Flutter. This plugin provides the iOS API to request and check permissions. repository: https://github.com/baseflow/flutter-permission-handler issue_tracker: https://github.com/Baseflow/flutter-permission-handler/issues -version: 9.4.9 +version: 9.4.10 environment: sdk: ">=2.18.0 <4.0.0" From cf9d9fa88f25f627b7333ce6a2d6805c7cf304c1 Mon Sep 17 00:00:00 2001 From: zeyus Date: Fri, 31 Jul 2026 16:16:53 +0200 Subject: [PATCH 12/26] Android `ACCESS_LOCAL_NETWORK`, package monorepo `pubspec.yaml` (#1541) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added ACCESS_LOCAL_NETWORK permission for android 17 * bump main package version * bump compilesdk version for android * Updated android pakcage version to 14 (breaking) * Just a typo * missing defs * ... * ... * Added ACCESS_LOCAL_NETWORK permission for android 17 * bump main package version * bump compilesdk version for android * Updated android pakcage version to 14 (breaking) * Just a typo * missing defs * ... * ... * dart formatting * Update to use monorepo for managing development * Linting, formatting, fix analysis issues * Fix test for number of permissions * Build after test (build is more time-costly) * Fix permission_handler android build compileSdk * Forgot to bump compilesdk version for android example app * update code coverage for permission_handler_platform_interface, add android 37 sdk * ...it is android 37.0, not android 37 * try to symlink the android-37.0 directory to android-37? * ohhh...i think it is flutter. * yes, flutter, but also outdated kotlin / gradle, etc * feat(apple): add Swift Package Manager support (#1523) * feat(apple): create SPM source directory structure * feat(apple): move ObjC sources to SPM-compatible structure * feat(apple): add public header for SPM plugin registration * feat(apple): move PrivacyInfo.xcprivacy to SPM sources * feat(apple): add Package.swift for Swift Package Manager support * feat(apple): update podspec to reference SPM source structure * chore(apple): ignore SPM build artifacts * docs: add SPM setup instructions to README * fix(apple): move Package.swift to correct SPM location (ios/permission_handler_apple/) * fix(apple): add missing PERMISSION_PHOTOS_ADD_ONLY and PERMISSION_LOCATION_ALWAYS defines * fix(apple): use forward declaration in public header to avoid missing internal import * fix(example): bump compileSdk to 36, AGP to 8.9.1, Gradle to 8.11.1 * fix(apple): add explicit UIKit import to strategies that use UIApplication under SPM * feat(apple): auto-detect permissions from Info.plist in Package.swift Package.swift now walks up the directory tree from its own location to find the app's Info.plist and enables each permission define when the corresponding usage description key is present. This mirrors the CocoaPods workflow: adding NSCameraUsageDescription to Info.plist is all that is needed to activate PERMISSION_CAMERA, with no extra configuration files or terminal commands. Environment variables remain supported as an explicit override (priority over Info.plist), which covers PermissionGroup.notification and criticalAlerts that have no required Info.plist key. Users must clear DerivedData once after changing Info.plist so Xcode re-evaluates the manifest: rm -rf ~/Library/Developer/Xcode/DerivedData * docs: update SPM setup instructions to use Info.plist auto-detection Replace the launchctl setenv / pre-action script approach with the new Info.plist-based mechanism: permissions are now enabled automatically when the corresponding usage description key is present in Info.plist, which is already required for any permission to work at runtime. Document the two permissions without an Info.plist key (notification, criticalAlerts) as the only case still requiring an env var. * chore(example): update iOS example app and remove plan artifact - Add all permission usage description keys to Info.plist so the SPM Info.plist auto-detection covers all permissions out of the box - Comment out the Siri entitlement (requires a paid Apple Developer account; uncomment to test PERMISSION_ASSISTANT) - Update AppDelegate to modern FlutterImplicitEngineDelegate pattern - Bump Podfile iOS platform to 13.0 - Remove docs/superpowers/plans/2026-05-05-spm-support.md (internal planning artifact not intended for the public repo) * chore(apple): bump version to 9.4.8 and update CHANGELOG * fix(apple): correct SPM permission flag mapping for photos and calendarWriteOnly - PERMISSION_PHOTOS now triggers on NSPhotoLibraryAddUsageDescription alone, since PhotoPermissionStrategy (which handles photosAddOnly) compiles under PERMISSION_PHOTOS — without this, photosAddOnly silently fell back to UnknownPermissionStrategy when NSPhotoLibraryUsageDescription was absent - PERMISSION_EVENTS_FULL_ACCESS now also triggers on NSCalendarsWriteOnlyAccessUsageDescription (iOS 17+), enabling calendarWriteOnly which requires PERMISSION_EVENTS || PERMISSION_EVENTS_FULL_ACCESS in native code - Sync podspec version to 9.4.8 - Restore NSCameraUsageDescription in example Info.plist (lost during rewrite) - Add NSCalendarsWriteOnlyAccessUsageDescription to example Info.plist * docs: add calendarWriteOnly to SPM permission table in README * fix(example): remove NSSiriUsageDescription and document permission constraints Siri requires the com.apple.developer.siri entitlement; including NSSiriUsageDescription without it crashes the app on launch under SPM. Added a README section listing permissions that cannot be tested on simulator or without special entitlements. * fix(apple): enable notifications and criticalAlerts by default under SPM These permissions have no required Info.plist key so the previous logic always compiled them out (defaultValue "0"), causing permanentlyDenied to be returned without ever showing a system dialog. They are now enabled by default and can be opted out via env var set to "0". * fix(apple): revert criticalAlerts to opt-in under SPM criticalAlerts requires a special Apple entitlement; compiling it into every app by default would add dead code for apps that don't use it. Only PERMISSION_NOTIFICATIONS defaults to enabled (no entitlement needed). * docs: clarify SPM special cases for notification and criticalAlerts - Add both permissions to the Info.plist table with notes - Distinguish export (terminal) vs launchctl setenv (Xcode GUI) - Explain why criticalAlerts is opt-in (Apple entitlement required) * chore(example): enable SPM in Xcode project for iOS example app Flutter auto-generated FlutterGeneratedPluginSwiftPackage reference when running with --enable-swift-package-manager. * fix(example): align Java source/target compatibility to VERSION_17 AGP 8.x + Kotlin 1.9+ enforce JVM-target consistency; compileJava was still on 1.8 while compileKotlin used 17, causing the build to fail. * Update version to 12.0.2 * fix typo in README about the compileSdkVersion (#1472) * docs: fix completeSdkVersion typo to compileSdkVersion (#1494) Fix incorrect Android SDK parameter name in documentation. "completeSdkVersion" is not a valid Android parameter. The correct name is "compileSdkVersion". This prevents developer confusion when following the troubleshooting guide. * Improved the iOS Setup section in permission_handler/README.md (#1488) After version 8.0.0, permission handler no longer uses all permissions on iOS, but this was not explained clearly in the README. This is my attempt to fix that small mistake. * Rewrite copyleft code from stackoverflow > > Co-authored-by: Novarest * Bump permission_handler_apple to 9.4.9 * Bump permission_handler to 12.0.3 * Bump permission_handler_apple to 9.4.9 * Added ACCESS_LOCAL_NETWORK permission for android 17 * bump main package version * missing defs * dart formatting * Update to use monorepo for managing development * Linting, formatting, fix analysis issues * Fix test for number of permissions * Build after test (build is more time-costly) * Fix permission_handler android build compileSdk * Forgot to bump compilesdk version for android example app * update code coverage for permission_handler_platform_interface, add android 37 sdk * ...it is android 37.0, not android 37 * try to symlink the android-37.0 directory to android-37? * ohhh...i think it is flutter. * yes, flutter, but also outdated kotlin / gradle, etc * accidental extra case during merge * updated gitignore for ios * Updated android plugin gradle / config * remove ios ephemeral * remove main permission_handler example ios ephemeral --------- Co-authored-by: Gauhar Co-authored-by: harshit saini Co-authored-by: AlHomam Sultan Co-authored-by: Maurits van Beusekom --- .github/workflows/permission_handler.yaml | 26 +- .../workflows/permission_handler_android.yaml | 22 +- ...permission_handler_platform_interface.yaml | 27 +- permission_handler/CHANGELOG.md | 9 + .../example/android/app/build.gradle | 55 -- .../example/android/app/build.gradle.kts | 45 ++ .../example/android/build.gradle | 18 - .../example/android/build.gradle.kts | 24 + .../example/android/gradle.properties | 7 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../example/android/settings.gradle | 24 - .../example/android/settings.gradle.kts | 26 + permission_handler/example/ios/.gitignore | 10 + permission_handler/example/lib/main.dart | 96 ++- permission_handler/example/pubspec.yaml | 5 +- permission_handler/pubspec.yaml | 13 +- .../test/permission_handler_test.dart | 90 +-- permission_handler_android/CHANGELOG.md | 15 +- .../android/build.gradle | 39 - .../android/build.gradle.kts | 77 ++ .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../{settings.gradle => settings.gradle.kts} | 0 .../PermissionConstants.java | 105 ++- .../permissionhandler/PermissionUtils.java | 671 +++++++++++++----- .../example/android/app/build.gradle | 53 -- .../example/android/app/build.gradle.kts | 45 ++ .../example/android/build.gradle | 30 - .../example/android/build.gradle.kts | 24 + .../example/android/gradle.properties | 7 +- .../gradle/wrapper/gradle-wrapper.properties | 5 +- .../example/android/settings.gradle | 24 - .../example/android/settings.gradle.kts | 26 + .../example/pubspec.yaml | 11 +- permission_handler_android/pubspec.yaml | 5 +- permission_handler_apple/CHANGELOG.md | 1 + .../example/lib/main.dart | 6 +- permission_handler_apple/example/pubspec.yaml | 14 +- permission_handler_apple/ios/.gitignore | 54 +- .../PermissionHandlerEnums.h | 3 +- permission_handler_apple/pubspec.yaml | 7 +- permission_handler_html/CHANGELOG.md | 4 + permission_handler_html/example/lib/main.dart | 89 +-- permission_handler_html/example/pubspec.yaml | 5 +- .../lib/permission_handler_html.dart | 17 +- permission_handler_html/lib/web_delegate.dart | 36 +- permission_handler_html/pubspec.yaml | 9 +- .../CHANGELOG.md | 8 + .../method_channel_permission_handler.dart | 29 +- .../lib/src/method_channel/utils/codec.dart | 11 +- ...permission_handler_platform_interface.dart | 9 +- .../lib/src/permissions.dart | 7 + .../pubspec.yaml | 5 +- .../method_channel/method_channel_mock.dart | 6 +- ...ethod_channel_permission_handler_test.dart | 268 +++---- .../src/method_channel/utils/coded_test.dart | 4 +- ...ssion_handler_platform_interface_test.dart | 110 +-- .../test/src/permission_status_test.dart | 55 +- .../test/src/permissions_test.dart | 72 +- .../test/src/service_status_test.dart | 18 +- permission_handler_windows/CHANGELOG.md | 4 + .../example/lib/main.dart | 104 +-- .../example/pubspec.yaml | 62 +- permission_handler_windows/pubspec.yaml | 7 +- .../windows/permission_constants.h | 3 +- pubspec.yaml | 21 + 65 files changed, 1625 insertions(+), 1061 deletions(-) delete mode 100644 permission_handler/example/android/app/build.gradle create mode 100644 permission_handler/example/android/app/build.gradle.kts delete mode 100644 permission_handler/example/android/build.gradle create mode 100644 permission_handler/example/android/build.gradle.kts delete mode 100644 permission_handler/example/android/settings.gradle create mode 100644 permission_handler/example/android/settings.gradle.kts delete mode 100644 permission_handler_android/android/build.gradle create mode 100644 permission_handler_android/android/build.gradle.kts rename permission_handler_android/android/{settings.gradle => settings.gradle.kts} (100%) delete mode 100644 permission_handler_android/example/android/app/build.gradle create mode 100644 permission_handler_android/example/android/app/build.gradle.kts delete mode 100644 permission_handler_android/example/android/build.gradle create mode 100644 permission_handler_android/example/android/build.gradle.kts delete mode 100644 permission_handler_android/example/android/settings.gradle create mode 100644 permission_handler_android/example/android/settings.gradle.kts create mode 100644 pubspec.yaml diff --git a/.github/workflows/permission_handler.yaml b/.github/workflows/permission_handler.yaml index 340849441..99f138b23 100644 --- a/.github/workflows/permission_handler.yaml +++ b/.github/workflows/permission_handler.yaml @@ -26,7 +26,7 @@ jobs: # TODO(mvanbeusekom): Manually set to macOS 15 to support Xcode 16 and iOS 18 SDKs. # Currently `macos-latest` is based on macOS 14 and doesn't support iOS 18 SDK. This # should be moved back to `macos-latest` when GitHub Actions images are updated. - runs-on: macos-15 + runs-on: macos-15 env: source-directory: ./permission_handler @@ -42,7 +42,6 @@ jobs: with: distribution: "temurin" # See 'Supported distributions' for available options java-version: "17" - # Make sure the stable version of Flutter is available - uses: subosito/flutter-action@v2 with: @@ -62,18 +61,7 @@ jobs: - name: Run Flutter Analyzer run: flutter analyze working-directory: ${{env.source-directory}} - - # Build Android version of the example App - - name: Run Android build - run: flutter build apk --release - working-directory: ${{env.example-directory}} - - # Build iOS version of the example App - - name: Run iOS build - run: flutter build ios --release --no-codesign - working-directory: ${{env.example-directory}} - - # Run all unit-tests with code coverage + # Run all unit-tests with code coverage - name: Run unit tests run: flutter test --coverage working-directory: ${{env.source-directory}} @@ -85,3 +73,13 @@ jobs: file: ${{env.source-directory}}/coverage/lcov.info # optional flags: unittests # optional name: permission_handler # optional + + # Build Android version of the example App + - name: Run Android build + run: flutter build apk --release + working-directory: ${{env.example-directory}} + + # Build iOS version of the example App + - name: Run iOS build + run: flutter build ios --release --no-codesign + working-directory: ${{env.example-directory}} diff --git a/.github/workflows/permission_handler_android.yaml b/.github/workflows/permission_handler_android.yaml index 0bbe16316..547ff265f 100644 --- a/.github/workflows/permission_handler_android.yaml +++ b/.github/workflows/permission_handler_android.yaml @@ -6,15 +6,15 @@ name: permission_handler_android # events but only for the main branch on: push: - branches: [ main ] + branches: [main] paths: - - 'permission_handler_android/**' - - '.github/workflows/permission_handler_android.yaml' + - "permission_handler_android/**" + - ".github/workflows/permission_handler_android.yaml" pull_request: - branches: [ main ] + branches: [main] paths: - - 'permission_handler_android/**' - - '.github/workflows/permission_handler_android.yaml' + - "permission_handler_android/**" + - ".github/workflows/permission_handler_android.yaml" # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: @@ -26,7 +26,7 @@ jobs: env: source-directory: ./permission_handler_android - example-directory: ./permission_handler_android/example + example-directory: ./permission_handler_android/example # Steps represent a sequence of tasks that will be executed as part of the job steps: @@ -36,13 +36,12 @@ jobs: # Make sure JAVA version 17 is installed on build agent. - uses: actions/setup-java@v3 with: - distribution: 'temurin' # See 'Supported distributions' for available options - java-version: '17' - + distribution: "temurin" # See 'Supported distributions' for available options + java-version: "17" # Make sure the stable version of Flutter is available - uses: subosito/flutter-action@v2 with: - channel: 'stable' + channel: "stable" # Download all Flutter packages - name: Download dependencies @@ -63,4 +62,3 @@ jobs: - name: Run Android build run: flutter build apk --release working-directory: ${{env.example-directory}} - \ No newline at end of file diff --git a/.github/workflows/permission_handler_platform_interface.yaml b/.github/workflows/permission_handler_platform_interface.yaml index 4f6408b78..46344a92b 100644 --- a/.github/workflows/permission_handler_platform_interface.yaml +++ b/.github/workflows/permission_handler_platform_interface.yaml @@ -6,15 +6,15 @@ name: permission_handler_platform_interface # events but only for the main branch on: push: - branches: [ main ] + branches: [main] paths: - - 'permission_handler_platform_interface/**' - - '.github/workflows/permission_handler_platform_interface.yaml' + - "permission_handler_platform_interface/**" + - ".github/workflows/permission_handler_platform_interface.yaml" pull_request: - branches: [ main ] + branches: [main] paths: - - 'permission_handler_platform_interface/**' - - '.github/workflows/permission_handler_platform_interface.yaml' + - "permission_handler_platform_interface/**" + - ".github/workflows/permission_handler_platform_interface.yaml" # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: @@ -25,17 +25,17 @@ jobs: runs-on: ubuntu-latest env: - source-directory: ./permission_handler_platform_interface + source-directory: ./permission_handler_platform_interface # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v3 - + # Make sure the stable version of Flutter is available - uses: subosito/flutter-action@v2 with: - channel: 'stable' + channel: "stable" # Download all Flutter packages - name: Download dependencies @@ -46,20 +46,21 @@ jobs: - name: Run Dart Format run: dart format --set-exit-if-changed . working-directory: ${{env.source-directory}} - + # Run Flutter Analyzer - name: Run Flutter Analyzer run: flutter analyze working-directory: ${{env.source-directory}} - + # Run all unit-tests with code coverage - name: Run unit tests run: flutter test --coverage working-directory: ${{env.source-directory}} # Upload code coverage information - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v3 with: + token: ${{ secrets.CODECOV_TOKEN }} file: ${{env.source-directory}}/coverage/lcov.info # optional name: permission_handler_platform_interface (Platform Interface Package) # optional - fail_ci_if_error: true \ No newline at end of file + flags: unittests diff --git a/permission_handler/CHANGELOG.md b/permission_handler/CHANGELOG.md index e44cb8f14..df454a31e 100644 --- a/permission_handler/CHANGELOG.md +++ b/permission_handler/CHANGELOG.md @@ -1,3 +1,12 @@ +## 13.0.1 + +- version bump + +## 13.0.0 + +- **BREAKING CHANGE:** , android compilesdk now set to version `compileSdkVersion 37` +- Added support for the new Android 17 permission `ACCESS_LOCAL_NETWORK` + ## 12.0.3 - Rewrites the "iOS - CocaoPods" section in the README.md to match version 8.0.0 of the permission_handler. diff --git a/permission_handler/example/android/app/build.gradle b/permission_handler/example/android/app/build.gradle deleted file mode 100644 index 047e56a5c..000000000 --- a/permission_handler/example/android/app/build.gradle +++ /dev/null @@ -1,55 +0,0 @@ -plugins { - id "com.android.application" - id "dev.flutter.flutter-gradle-plugin" -} - -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - -android { - if (project.android.hasProperty("namespace")) { - namespace 'com.baseflow.permissionhandlerexample' - } - compileSdkVersion 36 - - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - - defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "com.baseflow.permissionhandler.example" - minSdkVersion flutter.minSdkVersion - targetSdkVersion 34 - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - } - - buildTypes { - release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug - } - } -} - -flutter { - source '../..' -} diff --git a/permission_handler/example/android/app/build.gradle.kts b/permission_handler/example/android/app/build.gradle.kts new file mode 100644 index 000000000..602003d43 --- /dev/null +++ b/permission_handler/example/android/app/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + id("com.android.application") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.baseflow.permissionhandler.example" + compileSdk = 37 + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.baseflow.permissionhandler.example" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = 35 + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + +flutter { + source = "../.." +} diff --git a/permission_handler/example/android/build.gradle b/permission_handler/example/android/build.gradle deleted file mode 100644 index 8f31e8caf..000000000 --- a/permission_handler/example/android/build.gradle +++ /dev/null @@ -1,18 +0,0 @@ -allprojects { - repositories { - google() - mavenCentral() - } -} - -rootProject.buildDir = '../build' -subprojects { - project.buildDir = "${rootProject.buildDir}/${project.name}" -} -subprojects { - project.evaluationDependsOn(':app') -} - -tasks.register("clean", Delete) { - delete rootProject.buildDir -} \ No newline at end of file diff --git a/permission_handler/example/android/build.gradle.kts b/permission_handler/example/android/build.gradle.kts new file mode 100644 index 000000000..dbee657bb --- /dev/null +++ b/permission_handler/example/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/permission_handler/example/android/gradle.properties b/permission_handler/example/android/gradle.properties index 94adc3a3f..d5da7278a 100644 --- a/permission_handler/example/android/gradle.properties +++ b/permission_handler/example/android/gradle.properties @@ -1,3 +1,6 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true -android.enableJetifier=true +# This builtInKotlin flag was added automatically by Flutter migrator +android.builtInKotlin=false +# This newDsl flag was added automatically by Flutter migrator +android.newDsl=false diff --git a/permission_handler/example/android/gradle/wrapper/gradle-wrapper.properties b/permission_handler/example/android/gradle/wrapper/gradle-wrapper.properties index 9162f1008..a97e89ca1 100644 --- a/permission_handler/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/permission_handler/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip diff --git a/permission_handler/example/android/settings.gradle b/permission_handler/example/android/settings.gradle deleted file mode 100644 index 8cbe490f3..000000000 --- a/permission_handler/example/android/settings.gradle +++ /dev/null @@ -1,24 +0,0 @@ -pluginManagement { - def flutterSdkPath = { - def properties = new Properties() - file("local.properties").withInputStream { properties.load(it) } - def flutterSdkPath = properties.getProperty("flutter.sdk") - assert flutterSdkPath != null, "flutter.sdk not set in local.properties" - return flutterSdkPath - }() - - includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") - - repositories { - google() - mavenCentral() - gradlePluginPortal() - } -} - -plugins { - id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.9.1" apply false -} - -include ":app" \ No newline at end of file diff --git a/permission_handler/example/android/settings.gradle.kts b/permission_handler/example/android/settings.gradle.kts new file mode 100644 index 000000000..c21f0c5b4 --- /dev/null +++ b/permission_handler/example/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "9.0.1" apply false + id("org.jetbrains.kotlin.android") version "2.3.20" apply false +} + +include(":app") diff --git a/permission_handler/example/ios/.gitignore b/permission_handler/example/ios/.gitignore index e96ef602b..8a62ea716 100644 --- a/permission_handler/example/ios/.gitignore +++ b/permission_handler/example/ios/.gitignore @@ -1,3 +1,9 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +**/dgph *.mode1v3 *.mode2v3 *.moved-aside @@ -18,6 +24,7 @@ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig +Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ @@ -30,3 +37,6 @@ Runner/GeneratedPluginRegistrant.* !default.mode2v3 !default.pbxuser !default.perspectivev3 +# Swift Package Manager +.build/ +*.resolved diff --git a/permission_handler/example/lib/main.dart b/permission_handler/example/lib/main.dart index e37e51302..9dbe35a1d 100644 --- a/permission_handler/example/lib/main.dart +++ b/permission_handler/example/lib/main.dart @@ -18,8 +18,8 @@ void main() { ///Defines the main theme color final MaterialColor themeMaterialColor = BaseflowPluginExample.createMaterialColor( - const Color.fromRGBO(48, 49, 60, 1), - ); + const Color.fromRGBO(48, 49, 60, 1), +); /// A Flutter application demonstrating the functionality of this plugin class PermissionHandlerWidget extends StatefulWidget { @@ -43,41 +43,40 @@ class _PermissionHandlerWidgetState extends State { Widget build(BuildContext context) { return Center( child: ListView( - children: - Permission.values - .where((permission) { - if (Platform.isIOS) { - return permission != Permission.unknown && - permission != Permission.phone && - permission != Permission.sms && - permission != Permission.ignoreBatteryOptimizations && - permission != Permission.accessMediaLocation && - permission != Permission.activityRecognition && - permission != Permission.manageExternalStorage && - permission != Permission.systemAlertWindow && - permission != Permission.requestInstallPackages && - permission != Permission.accessNotificationPolicy && - permission != Permission.bluetoothScan && - permission != Permission.bluetoothAdvertise && - permission != Permission.bluetoothConnect && - permission != Permission.nearbyWifiDevices && - permission != Permission.videos && - permission != Permission.audio && - permission != Permission.scheduleExactAlarm && - permission != Permission.sensorsAlways; - } else { - return permission != Permission.unknown && - permission != Permission.mediaLibrary && - permission != Permission.photosAddOnly && - permission != Permission.reminders && - permission != Permission.bluetooth && - permission != Permission.appTrackingTransparency && - permission != Permission.criticalAlerts && - permission != Permission.assistant; - } - }) - .map((permission) => PermissionWidget(permission)) - .toList(), + children: Permission.values + .where((permission) { + if (Platform.isIOS) { + return permission != Permission.unknown && + permission != Permission.phone && + permission != Permission.sms && + permission != Permission.ignoreBatteryOptimizations && + permission != Permission.accessMediaLocation && + permission != Permission.activityRecognition && + permission != Permission.manageExternalStorage && + permission != Permission.systemAlertWindow && + permission != Permission.requestInstallPackages && + permission != Permission.accessNotificationPolicy && + permission != Permission.bluetoothScan && + permission != Permission.bluetoothAdvertise && + permission != Permission.bluetoothConnect && + permission != Permission.nearbyWifiDevices && + permission != Permission.videos && + permission != Permission.audio && + permission != Permission.scheduleExactAlarm && + permission != Permission.sensorsAlways; + } else { + return permission != Permission.unknown && + permission != Permission.mediaLibrary && + permission != Permission.photosAddOnly && + permission != Permission.reminders && + permission != Permission.bluetooth && + permission != Permission.appTrackingTransparency && + permission != Permission.criticalAlerts && + permission != Permission.assistant; + } + }) + .map((permission) => PermissionWidget(permission)) + .toList(), ), ); } @@ -136,18 +135,17 @@ class _PermissionState extends State { _permissionStatus.toString(), style: TextStyle(color: getPermissionColor()), ), - trailing: - (widget.permission is PermissionWithService) - ? IconButton( - icon: const Icon(Icons.info, color: Colors.white), - onPressed: () { - checkServiceStatus( - context, - widget.permission as PermissionWithService, - ); - }, - ) - : null, + trailing: (widget.permission is PermissionWithService) + ? IconButton( + icon: const Icon(Icons.info, color: Colors.white), + onPressed: () { + checkServiceStatus( + context, + widget.permission as PermissionWithService, + ); + }, + ) + : null, onTap: () { requestPermission(widget.permission); }, diff --git a/permission_handler/example/pubspec.yaml b/permission_handler/example/pubspec.yaml index 8ea895146..4b6545123 100644 --- a/permission_handler/example/pubspec.yaml +++ b/permission_handler/example/pubspec.yaml @@ -2,7 +2,9 @@ name: permission_handler_example description: Demonstrates how to use the permission_handler plugin. environment: - sdk: ^3.7.0 + sdk: ^3.6.0 + flutter: ">=3.24.0" +resolution: workspace dependencies: baseflow_plugin_template: ^2.1.1 @@ -20,6 +22,7 @@ dev_dependencies: flutter_test: sdk: flutter url_launcher: ^6.3.1 + flutter_lints: ^5.0.0 flutter: uses-material-design: true diff --git a/permission_handler/pubspec.yaml b/permission_handler/pubspec.yaml index 60ee8f94e..d720f281c 100644 --- a/permission_handler/pubspec.yaml +++ b/permission_handler/pubspec.yaml @@ -2,11 +2,12 @@ name: permission_handler description: Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions. repository: https://github.com/baseflow/flutter-permission-handler issue_tracker: https://github.com/Baseflow/flutter-permission-handler/issues -version: 12.0.3 +version: 13.0.1 environment: - sdk: ^3.5.0 + sdk: ^3.6.0 flutter: ">=3.24.0" +resolution: workspace flutter: plugin: @@ -24,11 +25,11 @@ dependencies: flutter: sdk: flutter meta: ^1.7.0 - permission_handler_android: ^13.0.0 - permission_handler_apple: ^9.4.6 + permission_handler_android: ^14.0.1 + permission_handler_apple: ^9.4.8 permission_handler_html: ^0.1.1 - permission_handler_windows: ^0.2.1 - permission_handler_platform_interface: ^4.3.0 + permission_handler_windows: ^0.2.2 + permission_handler_platform_interface: ^4.3.2 dev_dependencies: flutter_lints: ^5.0.0 diff --git a/permission_handler/test/permission_handler_test.dart b/permission_handler/test/permission_handler_test.dart index ae666e9a2..66ddfae12 100644 --- a/permission_handler/test/permission_handler_test.dart +++ b/permission_handler/test/permission_handler_test.dart @@ -23,21 +23,27 @@ void main() { }); test( - // ignore: lines_longer_than_80_chars - 'PermissionActions on Permission: get shouldShowRequestRationale should return true when on android', - () async { - final mockPermissionHandlerPlatform = PermissionHandlerPlatform.instance; - - when(mockPermissionHandlerPlatform - .shouldShowRequestPermissionRationale(Permission.contacts)) - .thenAnswer((_) => Future.value(true)); - - await Permission.contacts.shouldShowRequestRationale; - - verify(mockPermissionHandlerPlatform - .shouldShowRequestPermissionRationale(Permission.contacts)) - .called(1); - }); + // ignore: lines_longer_than_80_chars + 'PermissionActions on Permission: get shouldShowRequestRationale should return true when on android', + () async { + final mockPermissionHandlerPlatform = + PermissionHandlerPlatform.instance; + + when( + mockPermissionHandlerPlatform.shouldShowRequestPermissionRationale( + Permission.contacts, + ), + ).thenAnswer((_) => Future.value(true)); + + await Permission.contacts.shouldShowRequestRationale; + + verify( + mockPermissionHandlerPlatform.shouldShowRequestPermissionRationale( + Permission.contacts, + ), + ).called(1); + }, + ); test('PermissionActions on Permission: request()', () async { final permissionRequest = Permission.contacts.request(); @@ -65,11 +71,14 @@ void main() { expect(isLimited, false); }); - test('PermissionCheckShortcuts on Permission: get isPermanentlyDenied', - () async { - final isPermanentlyDenied = await Permission.contacts.isPermanentlyDenied; - expect(isPermanentlyDenied, false); - }); + test( + 'PermissionCheckShortcuts on Permission: get isPermanentlyDenied', + () async { + final isPermanentlyDenied = + await Permission.contacts.isPermanentlyDenied; + expect(isPermanentlyDenied, false); + }, + ); test('PermissionCheckShortcuts on Permission: get isProvisional', () async { final isProvisional = await Permission.contacts.isProvisional; @@ -77,23 +86,25 @@ void main() { }); test( - // ignore: lines_longer_than_80_chars - 'ServicePermissionActions on PermissionWithService: get ServiceStatus returns the right service status', - () async { - var serviceStatus = await Permission.phone.serviceStatus; + // ignore: lines_longer_than_80_chars + 'ServicePermissionActions on PermissionWithService: get ServiceStatus returns the right service status', + () async { + var serviceStatus = await Permission.phone.serviceStatus; - expect(serviceStatus, ServiceStatus.enabled); - }); + expect(serviceStatus, ServiceStatus.enabled); + }, + ); test( - // ignore: lines_longer_than_80_chars - 'PermissionListActions on List: request() on a list returns a Map', - () async { - var permissionList = []; - final permissionMap = await permissionList.request(); - - expect(permissionMap, isA>()); - }); + // ignore: lines_longer_than_80_chars + 'PermissionListActions on List: request() on a list returns a Map', + () async { + var permissionList = []; + final permissionMap = await permissionList.request(); + + expect(permissionMap, isA>()); + }, + ); test('onDeniedCallback sets onDenied', () async { bool callbackCalled = false; @@ -151,7 +162,8 @@ void main() { .onDeniedCallback(() => callbackCalled.add('Denied')) .onGrantedCallback(() => callbackCalled.add('Granted')) .onPermanentlyDeniedCallback( - () => callbackCalled.add('PermanentlyDenied')) + () => callbackCalled.add('PermanentlyDenied'), + ) .onRestrictedCallback(() => callbackCalled.add('Restricted')) .onLimitedCallback(() => callbackCalled.add('Limited')) .onProvisionalCallback(() => callbackCalled.add('Provisional')) @@ -181,7 +193,8 @@ class MockPermissionHandlerPlatform extends Mock @override Future> requestPermissions( - List permissions) { + List permissions, + ) { var permissionsMap = {}; return Future.value(permissionsMap); } @@ -189,10 +202,7 @@ class MockPermissionHandlerPlatform extends Mock @override Future shouldShowRequestPermissionRationale(Permission? permission) { return super.noSuchMethod( - Invocation.method( - #shouldShowPermissionRationale, - [permission], - ), + Invocation.method(#shouldShowPermissionRationale, [permission]), returnValue: Future.value(true), ); } diff --git a/permission_handler_android/CHANGELOG.md b/permission_handler_android/CHANGELOG.md index dfec82c83..7180a5985 100644 --- a/permission_handler_android/CHANGELOG.md +++ b/permission_handler_android/CHANGELOG.md @@ -1,7 +1,20 @@ +## 14.0.1 + +- Version bump + +## 14.0.0 + +- **BREAKING CHANGES:** When updating to version 14.0.0 make sure to also set the `compileSdkVersion` in the `app/build.gradle` file to `37`. +- Updates Android `compileSdkVersion: 35` to `37` +- Bump compileSDK version to 37 + +## 13.0.2 + +- Added support for the new Android 17 permission `ACCESS_LOCAL_NETWORK` ## 13.0.1 -* fix: Resolve `PermissionRequestInProgressException` when app is relaunched with non-standard launchMode. +- fix: Resolve `PermissionRequestInProgressException` when app is relaunched with non-standard launchMode. ## 13.0.0 diff --git a/permission_handler_android/android/build.gradle b/permission_handler_android/android/build.gradle deleted file mode 100644 index fff4a308b..000000000 --- a/permission_handler_android/android/build.gradle +++ /dev/null @@ -1,39 +0,0 @@ -group 'com.baseflow.permissionhandler' -version '1.0' - -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:8.0.2' - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -apply plugin: 'com.android.library' - -android { - // Conditional for compatibility with AGP <4.2. - if (project.android.hasProperty("namespace")) { - namespace 'com.baseflow.permissionhandler' - } - compileSdkVersion 35 - - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - - defaultConfig { - minSdkVersion 19 - } -} diff --git a/permission_handler_android/android/build.gradle.kts b/permission_handler_android/android/build.gradle.kts new file mode 100644 index 000000000..da4d8033d --- /dev/null +++ b/permission_handler_android/android/build.gradle.kts @@ -0,0 +1,77 @@ +group = "com.baseflow.permissionhandler" +version = "1.0" + +buildscript { + val kotlinVersion = "2.3.20" + repositories { + google() + mavenCentral() + } + + dependencies { + classpath("com.android.tools.build:gradle:9.0.1") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +plugins { + id("com.android.library") +} + +android { + namespace = "com.baseflow.permissionhandler" + + compileSdk = 37 + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + sourceSets { + getByName("main") { + java.srcDirs("src/main/kotlin") + } + getByName("test") { + java.srcDirs("src/test/kotlin") + } + } + + defaultConfig { + minSdk = 24 + } + + testOptions { + unitTests { + isIncludeAndroidResources = true + all { + it.useJUnitPlatform() + + it.outputs.upToDateWhen { false } + + it.testLogging { + events("passed", "skipped", "failed", "standardOut", "standardError") + showStandardStreams = true + } + } + } + } +} + +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + +dependencies { + testImplementation("org.jetbrains.kotlin:kotlin-test") + testImplementation("org.mockito:mockito-core:5.0.0") +} diff --git a/permission_handler_android/android/gradle/wrapper/gradle-wrapper.properties b/permission_handler_android/android/gradle/wrapper/gradle-wrapper.properties index da9702f9e..d706aba60 100644 --- a/permission_handler_android/android/gradle/wrapper/gradle-wrapper.properties +++ b/permission_handler_android/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/permission_handler_android/android/settings.gradle b/permission_handler_android/android/settings.gradle.kts similarity index 100% rename from permission_handler_android/android/settings.gradle rename to permission_handler_android/android/settings.gradle.kts diff --git a/permission_handler_android/android/src/main/java/com/baseflow/permissionhandler/PermissionConstants.java b/permission_handler_android/android/src/main/java/com/baseflow/permissionhandler/PermissionConstants.java index 0262db176..734aa019f 100644 --- a/permission_handler_android/android/src/main/java/com/baseflow/permissionhandler/PermissionConstants.java +++ b/permission_handler_android/android/src/main/java/com/baseflow/permissionhandler/PermissionConstants.java @@ -1,13 +1,13 @@ package com.baseflow.permissionhandler; import androidx.annotation.IntDef; - import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; final class PermissionConstants { + static final String LOG_TAG = "permissions_handler"; static final int PERMISSION_CODE = 24; static final int PERMISSION_CODE_IGNORE_BATTERY_OPTIMIZATIONS = 209; @@ -17,7 +17,6 @@ final class PermissionConstants { static final int PERMISSION_CODE_ACCESS_NOTIFICATION_POLICY = 213; static final int PERMISSION_CODE_SCHEDULE_EXACT_ALARM = 214; - // PERMISSION_GROUP // Deprecated in favor of PERMISSION_GROUP_CALENDAR_WRITE_ONLY and @@ -62,48 +61,50 @@ final class PermissionConstants { static final int PERMISSION_GROUP_CALENDAR_FULL_ACCESS = 37; static final int PERMISSION_GROUP_ASSISTANT = 38; static final int PERMISSION_GROUP_BACKGROUND_REFRESH = 39; + static final int PERMISSION_GROUP_ACCESS_LOCAL_NETWORK = 40; @Retention(RetentionPolicy.SOURCE) @IntDef({ - PERMISSION_GROUP_CALENDAR, - PERMISSION_GROUP_CAMERA, - PERMISSION_GROUP_CONTACTS, - PERMISSION_GROUP_LOCATION, - PERMISSION_GROUP_LOCATION_ALWAYS, - PERMISSION_GROUP_LOCATION_WHEN_IN_USE, - PERMISSION_GROUP_MEDIA_LIBRARY, - PERMISSION_GROUP_MICROPHONE, - PERMISSION_GROUP_PHONE, - PERMISSION_GROUP_PHOTOS, - PERMISSION_GROUP_REMINDERS, - PERMISSION_GROUP_SENSORS, - PERMISSION_GROUP_SENSORS_ALWAYS, - PERMISSION_GROUP_SMS, - PERMISSION_GROUP_SPEECH, - PERMISSION_GROUP_STORAGE, - PERMISSION_GROUP_IGNORE_BATTERY_OPTIMIZATIONS, - PERMISSION_GROUP_NOTIFICATION, - PERMISSION_GROUP_ACCESS_MEDIA_LOCATION, - PERMISSION_GROUP_ACTIVITY_RECOGNITION, - PERMISSION_GROUP_UNKNOWN, - PERMISSION_GROUP_BLUETOOTH, - PERMISSION_GROUP_MANAGE_EXTERNAL_STORAGE, - PERMISSION_GROUP_SYSTEM_ALERT_WINDOW, - PERMISSION_GROUP_REQUEST_INSTALL_PACKAGES, - PERMISSION_GROUP_ACCESS_NOTIFICATION_POLICY, - PERMISSION_GROUP_BLUETOOTH_SCAN, - PERMISSION_GROUP_BLUETOOTH_ADVERTISE, - PERMISSION_GROUP_BLUETOOTH_CONNECT, - PERMISSION_GROUP_NEARBY_WIFI_DEVICES, - PERMISSION_GROUP_VIDEOS, - PERMISSION_GROUP_AUDIO, - PERMISSION_GROUP_SCHEDULE_EXACT_ALARM, - PERMISSION_GROUP_CALENDAR_WRITE_ONLY, - PERMISSION_GROUP_CALENDAR_FULL_ACCESS, - PERMISSION_GROUP_ASSISTANT, + PERMISSION_GROUP_CALENDAR, + PERMISSION_GROUP_CAMERA, + PERMISSION_GROUP_CONTACTS, + PERMISSION_GROUP_LOCATION, + PERMISSION_GROUP_LOCATION_ALWAYS, + PERMISSION_GROUP_LOCATION_WHEN_IN_USE, + PERMISSION_GROUP_MEDIA_LIBRARY, + PERMISSION_GROUP_MICROPHONE, + PERMISSION_GROUP_PHONE, + PERMISSION_GROUP_PHOTOS, + PERMISSION_GROUP_REMINDERS, + PERMISSION_GROUP_SENSORS, + PERMISSION_GROUP_SENSORS_ALWAYS, + PERMISSION_GROUP_SMS, + PERMISSION_GROUP_SPEECH, + PERMISSION_GROUP_STORAGE, + PERMISSION_GROUP_IGNORE_BATTERY_OPTIMIZATIONS, + PERMISSION_GROUP_NOTIFICATION, + PERMISSION_GROUP_ACCESS_MEDIA_LOCATION, + PERMISSION_GROUP_ACTIVITY_RECOGNITION, + PERMISSION_GROUP_UNKNOWN, + PERMISSION_GROUP_BLUETOOTH, + PERMISSION_GROUP_MANAGE_EXTERNAL_STORAGE, + PERMISSION_GROUP_SYSTEM_ALERT_WINDOW, + PERMISSION_GROUP_REQUEST_INSTALL_PACKAGES, + PERMISSION_GROUP_ACCESS_NOTIFICATION_POLICY, + PERMISSION_GROUP_BLUETOOTH_SCAN, + PERMISSION_GROUP_BLUETOOTH_ADVERTISE, + PERMISSION_GROUP_BLUETOOTH_CONNECT, + PERMISSION_GROUP_NEARBY_WIFI_DEVICES, + PERMISSION_GROUP_VIDEOS, + PERMISSION_GROUP_AUDIO, + PERMISSION_GROUP_SCHEDULE_EXACT_ALARM, + PERMISSION_GROUP_CALENDAR_WRITE_ONLY, + PERMISSION_GROUP_CALENDAR_FULL_ACCESS, + PERMISSION_GROUP_ASSISTANT, + PERMISSION_GROUP_BACKGROUND_REFRESH, + PERMISSION_GROUP_ACCESS_LOCAL_NETWORK, }) - @interface PermissionGroup { - } + @interface PermissionGroup {} //PERMISSION_STATUS static final int PERMISSION_STATUS_DENIED = 0; @@ -115,14 +116,13 @@ final class PermissionConstants { @Target(ElementType.TYPE_USE) @Retention(RetentionPolicy.SOURCE) @IntDef({ - PERMISSION_STATUS_DENIED, - PERMISSION_STATUS_GRANTED, - PERMISSION_STATUS_RESTRICTED, - PERMISSION_STATUS_LIMITED, - PERMISSION_STATUS_NEVER_ASK_AGAIN + PERMISSION_STATUS_DENIED, + PERMISSION_STATUS_GRANTED, + PERMISSION_STATUS_RESTRICTED, + PERMISSION_STATUS_LIMITED, + PERMISSION_STATUS_NEVER_ASK_AGAIN, }) - @interface PermissionStatus { - } + @interface PermissionStatus {} //SERVICE_STATUS static final int SERVICE_STATUS_DISABLED = 0; @@ -131,10 +131,9 @@ final class PermissionConstants { @Retention(RetentionPolicy.SOURCE) @IntDef({ - SERVICE_STATUS_DISABLED, - SERVICE_STATUS_ENABLED, - SERVICE_STATUS_NOT_APPLICABLE + SERVICE_STATUS_DISABLED, + SERVICE_STATUS_ENABLED, + SERVICE_STATUS_NOT_APPLICABLE, }) - @interface ServiceStatus { - } -} \ No newline at end of file + @interface ServiceStatus {} +} diff --git a/permission_handler_android/android/src/main/java/com/baseflow/permissionhandler/PermissionUtils.java b/permission_handler_android/android/src/main/java/com/baseflow/permissionhandler/PermissionUtils.java index c56bcfdfb..35cc35e2d 100644 --- a/permission_handler_android/android/src/main/java/com/baseflow/permissionhandler/PermissionUtils.java +++ b/permission_handler_android/android/src/main/java/com/baseflow/permissionhandler/PermissionUtils.java @@ -10,12 +10,10 @@ import android.os.Build; import android.os.Environment; import android.util.Log; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.core.app.ActivityCompat; - import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -23,7 +21,9 @@ import java.util.List; public class PermissionUtils { - final static String SHARED_PREFERENCES_PERMISSION_WAS_DENIED_BEFORE_KEY = "sp_permission_handler_permission_was_denied_before"; + + static final String SHARED_PREFERENCES_PERMISSION_WAS_DENIED_BEFORE_KEY = + "sp_permission_handler_permission_was_denied_before"; @PermissionConstants.PermissionGroup static int parseManifestName(String permission) { @@ -95,45 +95,81 @@ static int parseManifestName(String permission) { return PermissionConstants.PERMISSION_GROUP_AUDIO; case Manifest.permission.SCHEDULE_EXACT_ALARM: return PermissionConstants.PERMISSION_GROUP_SCHEDULE_EXACT_ALARM; + case Manifest.permission.ACCESS_LOCAL_NETWORK: + return PermissionConstants.PERMISSION_GROUP_ACCESS_LOCAL_NETWORK; default: return PermissionConstants.PERMISSION_GROUP_UNKNOWN; } } @TargetApi(22) - static List getManifestNames(Context context, @PermissionConstants.PermissionGroup int permission) { + static List getManifestNames( + Context context, + @PermissionConstants.PermissionGroup int permission + ) { final ArrayList permissionNames = new ArrayList<>(); switch (permission) { case PermissionConstants.PERMISSION_GROUP_CALENDAR_WRITE_ONLY: - if (hasPermissionInManifest(context, permissionNames, Manifest.permission.WRITE_CALENDAR)) - permissionNames.add(Manifest.permission.WRITE_CALENDAR); + if ( + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.WRITE_CALENDAR + ) + ) permissionNames.add(Manifest.permission.WRITE_CALENDAR); break; - case PermissionConstants.PERMISSION_GROUP_CALENDAR_FULL_ACCESS: case PermissionConstants.PERMISSION_GROUP_CALENDAR: - if (hasPermissionInManifest(context, permissionNames, Manifest.permission.WRITE_CALENDAR)) - permissionNames.add(Manifest.permission.WRITE_CALENDAR); - if (hasPermissionInManifest(context, permissionNames, Manifest.permission.READ_CALENDAR)) - permissionNames.add(Manifest.permission.READ_CALENDAR); + if ( + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.WRITE_CALENDAR + ) + ) permissionNames.add(Manifest.permission.WRITE_CALENDAR); + if ( + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.READ_CALENDAR + ) + ) permissionNames.add(Manifest.permission.READ_CALENDAR); break; - case PermissionConstants.PERMISSION_GROUP_CAMERA: - if (hasPermissionInManifest(context, permissionNames, Manifest.permission.CAMERA)) - permissionNames.add(Manifest.permission.CAMERA); + if ( + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.CAMERA + ) + ) permissionNames.add(Manifest.permission.CAMERA); break; - case PermissionConstants.PERMISSION_GROUP_CONTACTS: - if (hasPermissionInManifest(context, permissionNames, Manifest.permission.READ_CONTACTS)) - permissionNames.add(Manifest.permission.READ_CONTACTS); - - if (hasPermissionInManifest(context, permissionNames, Manifest.permission.WRITE_CONTACTS)) - permissionNames.add(Manifest.permission.WRITE_CONTACTS); - - if (hasPermissionInManifest(context, permissionNames, Manifest.permission.GET_ACCOUNTS)) - permissionNames.add(Manifest.permission.GET_ACCOUNTS); + if ( + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.READ_CONTACTS + ) + ) permissionNames.add(Manifest.permission.READ_CONTACTS); + + if ( + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.WRITE_CONTACTS + ) + ) permissionNames.add(Manifest.permission.WRITE_CONTACTS); + + if ( + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.GET_ACCOUNTS + ) + ) permissionNames.add(Manifest.permission.GET_ACCOUNTS); break; - case PermissionConstants.PERMISSION_GROUP_LOCATION_ALWAYS: case PermissionConstants.PERMISSION_GROUP_LOCATION_WHEN_IN_USE: case PermissionConstants.PERMISSION_GROUP_LOCATION: @@ -141,150 +177,324 @@ static List getManifestNames(Context context, @PermissionConstants.Permi // case on pre Android Q devices. The ACCESS_BACKGROUND_LOCATION permission was only // introduced in Android Q, before it should be treated as the ACCESS_COARSE_LOCATION or // ACCESS_FINE_LOCATION. - if (permission == PermissionConstants.PERMISSION_GROUP_LOCATION_ALWAYS && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - if (hasPermissionInManifest(context, permissionNames, Manifest.permission.ACCESS_BACKGROUND_LOCATION)) - permissionNames.add(Manifest.permission.ACCESS_BACKGROUND_LOCATION); + if ( + permission == + PermissionConstants.PERMISSION_GROUP_LOCATION_ALWAYS && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + ) { + if ( + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.ACCESS_BACKGROUND_LOCATION + ) + ) permissionNames.add( + Manifest.permission.ACCESS_BACKGROUND_LOCATION + ); break; } - if (hasPermissionInManifest(context, permissionNames, Manifest.permission.ACCESS_COARSE_LOCATION)) - permissionNames.add(Manifest.permission.ACCESS_COARSE_LOCATION); - - if (hasPermissionInManifest(context, permissionNames, Manifest.permission.ACCESS_FINE_LOCATION)) - permissionNames.add(Manifest.permission.ACCESS_FINE_LOCATION); + if ( + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.ACCESS_COARSE_LOCATION + ) + ) permissionNames.add( + Manifest.permission.ACCESS_COARSE_LOCATION + ); + + if ( + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.ACCESS_FINE_LOCATION + ) + ) permissionNames.add(Manifest.permission.ACCESS_FINE_LOCATION); break; case PermissionConstants.PERMISSION_GROUP_SPEECH: case PermissionConstants.PERMISSION_GROUP_MICROPHONE: - if (hasPermissionInManifest(context, permissionNames, Manifest.permission.RECORD_AUDIO)) - permissionNames.add(Manifest.permission.RECORD_AUDIO); + if ( + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.RECORD_AUDIO + ) + ) permissionNames.add(Manifest.permission.RECORD_AUDIO); break; - case PermissionConstants.PERMISSION_GROUP_PHONE: - if (hasPermissionInManifest(context, permissionNames, Manifest.permission.READ_PHONE_STATE)) - permissionNames.add(Manifest.permission.READ_PHONE_STATE); - - if (android.os.Build.VERSION.SDK_INT > Build.VERSION_CODES.Q && hasPermissionInManifest(context, permissionNames, Manifest.permission.READ_PHONE_NUMBERS)) - permissionNames.add(Manifest.permission.READ_PHONE_NUMBERS); - - if (hasPermissionInManifest(context, permissionNames, Manifest.permission.CALL_PHONE)) - permissionNames.add(Manifest.permission.CALL_PHONE); - - if (hasPermissionInManifest(context, permissionNames, Manifest.permission.READ_CALL_LOG)) - permissionNames.add(Manifest.permission.READ_CALL_LOG); - - if (hasPermissionInManifest(context, permissionNames, Manifest.permission.WRITE_CALL_LOG)) - permissionNames.add(Manifest.permission.WRITE_CALL_LOG); - - if (hasPermissionInManifest(context, permissionNames, Manifest.permission.ADD_VOICEMAIL)) - permissionNames.add(Manifest.permission.ADD_VOICEMAIL); - - if (hasPermissionInManifest(context, permissionNames, Manifest.permission.USE_SIP)) - permissionNames.add(Manifest.permission.USE_SIP); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && hasPermissionInManifest(context, permissionNames, Manifest.permission.ANSWER_PHONE_CALLS)) - permissionNames.add(Manifest.permission.ANSWER_PHONE_CALLS); + if ( + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.READ_PHONE_STATE + ) + ) permissionNames.add(Manifest.permission.READ_PHONE_STATE); + + if ( + android.os.Build.VERSION.SDK_INT > Build.VERSION_CODES.Q && + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.READ_PHONE_NUMBERS + ) + ) permissionNames.add(Manifest.permission.READ_PHONE_NUMBERS); + + if ( + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.CALL_PHONE + ) + ) permissionNames.add(Manifest.permission.CALL_PHONE); + + if ( + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.READ_CALL_LOG + ) + ) permissionNames.add(Manifest.permission.READ_CALL_LOG); + + if ( + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.WRITE_CALL_LOG + ) + ) permissionNames.add(Manifest.permission.WRITE_CALL_LOG); + + if ( + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.ADD_VOICEMAIL + ) + ) permissionNames.add(Manifest.permission.ADD_VOICEMAIL); + + if ( + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.USE_SIP + ) + ) permissionNames.add(Manifest.permission.USE_SIP); + + if ( + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.ANSWER_PHONE_CALLS + ) + ) permissionNames.add(Manifest.permission.ANSWER_PHONE_CALLS); break; - case PermissionConstants.PERMISSION_GROUP_SENSORS: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { - if (hasPermissionInManifest(context, permissionNames, Manifest.permission.BODY_SENSORS)) { + if ( + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.BODY_SENSORS + ) + ) { permissionNames.add(Manifest.permission.BODY_SENSORS); } } break; case PermissionConstants.PERMISSION_GROUP_SENSORS_ALWAYS: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - if (hasPermissionInManifest(context, permissionNames, Manifest.permission.BODY_SENSORS_BACKGROUND)) { - permissionNames.add(Manifest.permission.BODY_SENSORS_BACKGROUND); + if ( + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.BODY_SENSORS_BACKGROUND + ) + ) { + permissionNames.add( + Manifest.permission.BODY_SENSORS_BACKGROUND + ); } } break; case PermissionConstants.PERMISSION_GROUP_SMS: - if (hasPermissionInManifest(context, permissionNames, Manifest.permission.SEND_SMS)) - permissionNames.add(Manifest.permission.SEND_SMS); - - if (hasPermissionInManifest(context, permissionNames, Manifest.permission.RECEIVE_SMS)) - permissionNames.add(Manifest.permission.RECEIVE_SMS); - - if (hasPermissionInManifest(context, permissionNames, Manifest.permission.READ_SMS)) - permissionNames.add(Manifest.permission.READ_SMS); - - if (hasPermissionInManifest(context, permissionNames, Manifest.permission.RECEIVE_WAP_PUSH)) - permissionNames.add(Manifest.permission.RECEIVE_WAP_PUSH); - - if (hasPermissionInManifest(context, permissionNames, Manifest.permission.RECEIVE_MMS)) - permissionNames.add(Manifest.permission.RECEIVE_MMS); + if ( + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.SEND_SMS + ) + ) permissionNames.add(Manifest.permission.SEND_SMS); + + if ( + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.RECEIVE_SMS + ) + ) permissionNames.add(Manifest.permission.RECEIVE_SMS); + + if ( + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.READ_SMS + ) + ) permissionNames.add(Manifest.permission.READ_SMS); + + if ( + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.RECEIVE_WAP_PUSH + ) + ) permissionNames.add(Manifest.permission.RECEIVE_WAP_PUSH); + + if ( + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.RECEIVE_MMS + ) + ) permissionNames.add(Manifest.permission.RECEIVE_MMS); break; - case PermissionConstants.PERMISSION_GROUP_STORAGE: - if (hasPermissionInManifest(context, permissionNames, Manifest.permission.READ_EXTERNAL_STORAGE)) - permissionNames.add(Manifest.permission.READ_EXTERNAL_STORAGE); - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q && Environment.isExternalStorageLegacy())) { - if (hasPermissionInManifest(context, permissionNames, Manifest.permission.WRITE_EXTERNAL_STORAGE)) - permissionNames.add(Manifest.permission.WRITE_EXTERNAL_STORAGE); + if ( + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.READ_EXTERNAL_STORAGE + ) + ) permissionNames.add( + Manifest.permission.READ_EXTERNAL_STORAGE + ); + + if ( + Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || + (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q && + Environment.isExternalStorageLegacy()) + ) { + if ( + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) + ) permissionNames.add( + Manifest.permission.WRITE_EXTERNAL_STORAGE + ); break; } break; - case PermissionConstants.PERMISSION_GROUP_IGNORE_BATTERY_OPTIMIZATIONS: - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && hasPermissionInManifest(context, permissionNames, Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)) - permissionNames.add(Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); + if ( + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS + ) + ) permissionNames.add( + Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS + ); break; - case PermissionConstants.PERMISSION_GROUP_ACCESS_MEDIA_LOCATION: // The ACCESS_MEDIA_LOCATION permission is introduced in Android Q, meaning we should // not handle permissions on pre Android Q devices. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return null; - if (hasPermissionInManifest(context, permissionNames, Manifest.permission.ACCESS_MEDIA_LOCATION)) - permissionNames.add(Manifest.permission.ACCESS_MEDIA_LOCATION); + if ( + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.ACCESS_MEDIA_LOCATION + ) + ) permissionNames.add( + Manifest.permission.ACCESS_MEDIA_LOCATION + ); break; - case PermissionConstants.PERMISSION_GROUP_ACTIVITY_RECOGNITION: // The ACTIVITY_RECOGNITION permission is introduced in Android Q, meaning we should // not handle permissions on pre Android Q devices. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return null; - if (hasPermissionInManifest(context, permissionNames, Manifest.permission.ACTIVITY_RECOGNITION)) - permissionNames.add(Manifest.permission.ACTIVITY_RECOGNITION); + if ( + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.ACTIVITY_RECOGNITION + ) + ) permissionNames.add(Manifest.permission.ACTIVITY_RECOGNITION); break; - case PermissionConstants.PERMISSION_GROUP_BLUETOOTH: - if (hasPermissionInManifest(context, permissionNames, Manifest.permission.BLUETOOTH)) - permissionNames.add(Manifest.permission.BLUETOOTH); + if ( + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.BLUETOOTH + ) + ) permissionNames.add(Manifest.permission.BLUETOOTH); break; - case PermissionConstants.PERMISSION_GROUP_MANAGE_EXTERNAL_STORAGE: // The MANAGE_EXTERNAL_STORAGE permission is introduced in Android R, meaning we should // not handle permissions on pre Android R devices. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && hasPermissionInManifest(context, permissionNames, Manifest.permission.MANAGE_EXTERNAL_STORAGE)) - permissionNames.add(Manifest.permission.MANAGE_EXTERNAL_STORAGE); + if ( + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.MANAGE_EXTERNAL_STORAGE + ) + ) permissionNames.add( + Manifest.permission.MANAGE_EXTERNAL_STORAGE + ); break; - case PermissionConstants.PERMISSION_GROUP_SYSTEM_ALERT_WINDOW: - if (hasPermissionInManifest(context, permissionNames, Manifest.permission.SYSTEM_ALERT_WINDOW)) - permissionNames.add(Manifest.permission.SYSTEM_ALERT_WINDOW); + if ( + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.SYSTEM_ALERT_WINDOW + ) + ) permissionNames.add(Manifest.permission.SYSTEM_ALERT_WINDOW); break; - case PermissionConstants.PERMISSION_GROUP_REQUEST_INSTALL_PACKAGES: // The REQUEST_INSTALL_PACKAGES permission is introduced in Android M, meaning we should // not handle permissions on pre Android M devices. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && hasPermissionInManifest(context, permissionNames, Manifest.permission.REQUEST_INSTALL_PACKAGES)) - permissionNames.add(Manifest.permission.REQUEST_INSTALL_PACKAGES); + if ( + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.REQUEST_INSTALL_PACKAGES + ) + ) permissionNames.add( + Manifest.permission.REQUEST_INSTALL_PACKAGES + ); break; case PermissionConstants.PERMISSION_GROUP_ACCESS_NOTIFICATION_POLICY: // The REQUEST_NOTIFICATION_POLICY permission is introduced in Android M, meaning we should // not handle permissions on pre Android M devices. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && hasPermissionInManifest(context, permissionNames, Manifest.permission.ACCESS_NOTIFICATION_POLICY)) - permissionNames.add(Manifest.permission.ACCESS_NOTIFICATION_POLICY); + if ( + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.ACCESS_NOTIFICATION_POLICY + ) + ) permissionNames.add( + Manifest.permission.ACCESS_NOTIFICATION_POLICY + ); break; case PermissionConstants.PERMISSION_GROUP_BLUETOOTH_SCAN: { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // The BLUETOOTH_SCAN permission is introduced in Android S, meaning we should // not handle permissions on pre Android S devices. - String result = determineBluetoothPermission(context, Manifest.permission.BLUETOOTH_SCAN); + String result = determineBluetoothPermission( + context, + Manifest.permission.BLUETOOTH_SCAN + ); if (result != null) { permissionNames.add(result); @@ -297,7 +507,10 @@ static List getManifestNames(Context context, @PermissionConstants.Permi if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // The BLUETOOTH_ADVERTISE permission is introduced in Android S, meaning we should // not handle permissions on pre Android S devices. - String result = determineBluetoothPermission(context, Manifest.permission.BLUETOOTH_ADVERTISE); + String result = determineBluetoothPermission( + context, + Manifest.permission.BLUETOOTH_ADVERTISE + ); if (result != null) { permissionNames.add(result); @@ -310,7 +523,10 @@ static List getManifestNames(Context context, @PermissionConstants.Permi if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // The BLUETOOTH_CONNECT permission is introduced in Android S, meaning we should // not handle permissions on pre Android S devices. - String result = determineBluetoothPermission(context, Manifest.permission.BLUETOOTH_CONNECT); + String result = determineBluetoothPermission( + context, + Manifest.permission.BLUETOOTH_CONNECT + ); if (result != null) { permissionNames.add(result); @@ -322,37 +538,85 @@ static List getManifestNames(Context context, @PermissionConstants.Permi case PermissionConstants.PERMISSION_GROUP_NOTIFICATION: // The POST_NOTIFICATIONS permission is introduced in Android TIRAMISU, meaning we should // not handle permissions on pre Android TIRAMISU devices. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && hasPermissionInManifest(context, permissionNames, Manifest.permission.POST_NOTIFICATIONS)) - permissionNames.add(Manifest.permission.POST_NOTIFICATIONS); + if ( + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.POST_NOTIFICATIONS + ) + ) permissionNames.add(Manifest.permission.POST_NOTIFICATIONS); break; case PermissionConstants.PERMISSION_GROUP_NEARBY_WIFI_DEVICES: // The NEARBY_WIFI_DEVICES permission is introduced in Android TIRAMISU, meaning we should // not handle permissions on pre Android TIRAMISU devices. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && hasPermissionInManifest(context, permissionNames, Manifest.permission.NEARBY_WIFI_DEVICES)) - permissionNames.add(Manifest.permission.NEARBY_WIFI_DEVICES); + if ( + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.NEARBY_WIFI_DEVICES + ) + ) permissionNames.add(Manifest.permission.NEARBY_WIFI_DEVICES); break; case PermissionConstants.PERMISSION_GROUP_PHOTOS: // The READ_MEDIA_IMAGES permission is introduced in Android TIRAMISU, meaning we should // not handle permissions on pre Android TIRAMISU devices. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && hasPermissionInManifest(context, permissionNames, Manifest.permission.READ_MEDIA_IMAGES)) - permissionNames.add(Manifest.permission.READ_MEDIA_IMAGES); + if ( + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.READ_MEDIA_IMAGES + ) + ) permissionNames.add(Manifest.permission.READ_MEDIA_IMAGES); break; case PermissionConstants.PERMISSION_GROUP_VIDEOS: // The READ_MEDIA_VIDEOS permission is introduced in Android TIRAMISU, meaning we should // not handle permissions on pre Android TIRAMISU devices. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && hasPermissionInManifest(context, permissionNames, Manifest.permission.READ_MEDIA_VIDEO)) - permissionNames.add(Manifest.permission.READ_MEDIA_VIDEO); + if ( + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.READ_MEDIA_VIDEO + ) + ) permissionNames.add(Manifest.permission.READ_MEDIA_VIDEO); break; case PermissionConstants.PERMISSION_GROUP_AUDIO: // The READ_MEDIA_AUDIO permission is introduced in Android TIRAMISU, meaning we should // not handle permissions on pre Android TIRAMISU devices. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && hasPermissionInManifest(context, permissionNames, Manifest.permission.READ_MEDIA_AUDIO)) - permissionNames.add(Manifest.permission.READ_MEDIA_AUDIO); + if ( + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.READ_MEDIA_AUDIO + ) + ) permissionNames.add(Manifest.permission.READ_MEDIA_AUDIO); break; case PermissionConstants.PERMISSION_GROUP_SCHEDULE_EXACT_ALARM: // The SCHEDULE_EXACT_ALARM permission is introduced in Android S, before Android 31 it should alway return Granted - if (hasPermissionInManifest(context, permissionNames, Manifest.permission.SCHEDULE_EXACT_ALARM)) - permissionNames.add(Manifest.permission.SCHEDULE_EXACT_ALARM); + if ( + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.SCHEDULE_EXACT_ALARM + ) + ) permissionNames.add(Manifest.permission.SCHEDULE_EXACT_ALARM); + break; + case PermissionConstants.PERMISSION_GROUP_ACCESS_LOCAL_NETWORK: + // The ACCESS_LOCAL_NETWORK permission is introduced in API level 37 (Cinnamon Bun) + // API 36 was allowed but only with e.g. NEARBY_WIFI_DEVICES, so we should not handle permissions on pre Android 37 devices. + // Prior to 36, this was always allowed. + if ( + Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN && + hasPermissionInManifest( + context, + permissionNames, + Manifest.permission.ACCESS_LOCAL_NETWORK + ) + ) permissionNames.add(Manifest.permission.ACCESS_LOCAL_NETWORK); break; case PermissionConstants.PERMISSION_GROUP_MEDIA_LIBRARY: case PermissionConstants.PERMISSION_GROUP_REMINDERS: @@ -363,7 +627,11 @@ static List getManifestNames(Context context, @PermissionConstants.Permi return permissionNames; } - private static boolean hasPermissionInManifest(Context context, ArrayList confirmedPermissions, String permission) { + private static boolean hasPermissionInManifest( + Context context, + ArrayList confirmedPermissions, + String permission + ) { try { if (confirmedPermissions != null) { for (String r : confirmedPermissions) { @@ -374,25 +642,37 @@ private static boolean hasPermissionInManifest(Context context, ArrayList(Arrays.asList(info.requestedPermissions)); + confirmedPermissions = new ArrayList<>( + Arrays.asList(info.requestedPermissions) + ); for (String r : confirmedPermissions) { if (r.equals(permission)) { return true; } } } catch (Exception ex) { - Log.d(PermissionConstants.LOG_TAG, "Unable to check manifest for permission: ", ex); + Log.d( + PermissionConstants.LOG_TAG, + "Unable to check manifest for permission: ", + ex + ); } return false; } @@ -459,8 +739,8 @@ private static boolean hasPermissionInManifest(Context context, ArrayList statuses) { - if (statuses.contains(PermissionConstants.PERMISSION_STATUS_NEVER_ASK_AGAIN)) - return PermissionConstants.PERMISSION_STATUS_NEVER_ASK_AGAIN; - if (statuses.contains(PermissionConstants.PERMISSION_STATUS_RESTRICTED)) - return PermissionConstants.PERMISSION_STATUS_RESTRICTED; - if (statuses.contains(PermissionConstants.PERMISSION_STATUS_DENIED)) - return PermissionConstants.PERMISSION_STATUS_DENIED; - if (statuses.contains(PermissionConstants.PERMISSION_STATUS_LIMITED)) - return PermissionConstants.PERMISSION_STATUS_LIMITED; + static Integer strictestStatus( + final @NonNull Collection< + @PermissionConstants.PermissionStatus Integer + > statuses + ) { + if ( + statuses.contains( + PermissionConstants.PERMISSION_STATUS_NEVER_ASK_AGAIN + ) + ) return PermissionConstants.PERMISSION_STATUS_NEVER_ASK_AGAIN; + if ( + statuses.contains(PermissionConstants.PERMISSION_STATUS_RESTRICTED) + ) return PermissionConstants.PERMISSION_STATUS_RESTRICTED; + if ( + statuses.contains(PermissionConstants.PERMISSION_STATUS_DENIED) + ) return PermissionConstants.PERMISSION_STATUS_DENIED; + if ( + statuses.contains(PermissionConstants.PERMISSION_STATUS_LIMITED) + ) return PermissionConstants.PERMISSION_STATUS_LIMITED; return PermissionConstants.PERMISSION_STATUS_GRANTED; } @@ -486,9 +776,11 @@ static Integer strictestStatus(final @NonNull Collection<@PermissionConstants.Pe @PermissionConstants.PermissionStatus static Integer strictestStatus( final @Nullable @PermissionConstants.PermissionStatus Integer statusA, - final @Nullable @PermissionConstants.PermissionStatus Integer statusB) { - - final Collection<@PermissionConstants.PermissionStatus Integer> statuses = new HashSet<>(); + final @Nullable @PermissionConstants.PermissionStatus Integer statusB + ) { + final Collection< + @PermissionConstants.PermissionStatus Integer + > statuses = new HashSet<>(); statuses.add(statusA); statuses.add(statusB); return strictestStatus(statuses); @@ -508,8 +800,8 @@ static Integer strictestStatus( @PermissionConstants.PermissionStatus static int determineDeniedVariant( final @Nullable Activity activity, - final String permissionName) { - + final String permissionName + ) { if (activity == null) { return PermissionConstants.PERMISSION_STATUS_DENIED; } @@ -518,11 +810,15 @@ static int determineDeniedVariant( return PermissionConstants.PERMISSION_STATUS_DENIED; } - final boolean wasDeniedBefore = PermissionUtils.wasPermissionDeniedBefore(activity, permissionName); - final boolean shouldShowRational = !PermissionUtils.isNeverAskAgainSelected(activity, permissionName); + final boolean wasDeniedBefore = + PermissionUtils.wasPermissionDeniedBefore(activity, permissionName); + final boolean shouldShowRational = + !PermissionUtils.isNeverAskAgainSelected(activity, permissionName); //noinspection SimplifiableConditionalExpression - final boolean isDenied = wasDeniedBefore ? !shouldShowRational : shouldShowRational; + final boolean isDenied = wasDeniedBefore + ? !shouldShowRational + : shouldShowRational; if (!wasDeniedBefore && isDenied) { setPermissionDenied(activity, permissionName); @@ -538,24 +834,49 @@ static int determineDeniedVariant( @RequiresApi(api = Build.VERSION_CODES.M) static boolean isNeverAskAgainSelected( @NonNull final Activity activity, - final String name) { - - final boolean shouldShowRequestPermissionRationale = ActivityCompat.shouldShowRequestPermissionRationale(activity, name); + final String name + ) { + final boolean shouldShowRequestPermissionRationale = + ActivityCompat.shouldShowRequestPermissionRationale(activity, name); return !shouldShowRequestPermissionRationale; } - private static String determineBluetoothPermission(Context context, String permission) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && hasPermissionInManifest(context, null, permission)) { + private static String determineBluetoothPermission( + Context context, + String permission + ) { + if ( + Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && + hasPermissionInManifest(context, null, permission) + ) { return permission; } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - if (hasPermissionInManifest(context, null, Manifest.permission.ACCESS_FINE_LOCATION)) { + if ( + hasPermissionInManifest( + context, + null, + Manifest.permission.ACCESS_FINE_LOCATION + ) + ) { return Manifest.permission.ACCESS_FINE_LOCATION; - } else if (hasPermissionInManifest(context, null, Manifest.permission.ACCESS_COARSE_LOCATION)) { + } else if ( + hasPermissionInManifest( + context, + null, + Manifest.permission.ACCESS_COARSE_LOCATION + ) + ) { return Manifest.permission.ACCESS_COARSE_LOCATION; } return null; - } else if (hasPermissionInManifest(context, null, Manifest.permission.ACCESS_FINE_LOCATION)) { + } else if ( + hasPermissionInManifest( + context, + null, + Manifest.permission.ACCESS_FINE_LOCATION + ) + ) { return Manifest.permission.ACCESS_FINE_LOCATION; } @@ -565,13 +886,22 @@ private static String determineBluetoothPermission(Context context, String permi // Suppress deprecation warnings since its purpose is to support to be backwards compatible with // pre TIRAMISU versions of Android @SuppressWarnings("deprecation") - private static PackageInfo getPackageInfo(Context context) throws PackageManager.NameNotFoundException { + private static PackageInfo getPackageInfo(Context context) + throws PackageManager.NameNotFoundException { final PackageManager pm = context.getPackageManager(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - return pm.getPackageInfo(context.getPackageName(), PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS)); + return pm.getPackageInfo( + context.getPackageName(), + PackageManager.PackageInfoFlags.of( + PackageManager.GET_PERMISSIONS + ) + ); } else { - return pm.getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS); + return pm.getPackageInfo( + context.getPackageName(), + PackageManager.GET_PERMISSIONS + ); } } @@ -587,9 +917,16 @@ private static PackageInfo getPackageInfo(Context context) throws PackageManager * @param permissionName the name of the permission * @return whether the permission was denied in the past */ - private static boolean wasPermissionDeniedBefore(final Context context, final String permissionName) { - final SharedPreferences sharedPreferences = context.getSharedPreferences(permissionName, Context.MODE_PRIVATE); - return sharedPreferences.getBoolean(SHARED_PREFERENCES_PERMISSION_WAS_DENIED_BEFORE_KEY, false); + private static boolean wasPermissionDeniedBefore( + final Context context, + final String permissionName + ) { + final SharedPreferences sharedPreferences = + context.getSharedPreferences(permissionName, Context.MODE_PRIVATE); + return sharedPreferences.getBoolean( + SHARED_PREFERENCES_PERMISSION_WAS_DENIED_BEFORE_KEY, + false + ); } /** @@ -602,8 +939,18 @@ private static boolean wasPermissionDeniedBefore(final Context context, final St * @param context context needed for accessing shared preferences. * @param permissionName the name of the permission */ - private static void setPermissionDenied(final Context context, final String permissionName) { - final SharedPreferences sharedPreferences = context.getSharedPreferences(permissionName, Context.MODE_PRIVATE); - sharedPreferences.edit().putBoolean(SHARED_PREFERENCES_PERMISSION_WAS_DENIED_BEFORE_KEY, true).apply(); + private static void setPermissionDenied( + final Context context, + final String permissionName + ) { + final SharedPreferences sharedPreferences = + context.getSharedPreferences(permissionName, Context.MODE_PRIVATE); + sharedPreferences + .edit() + .putBoolean( + SHARED_PREFERENCES_PERMISSION_WAS_DENIED_BEFORE_KEY, + true + ) + .apply(); } } diff --git a/permission_handler_android/example/android/app/build.gradle b/permission_handler_android/example/android/app/build.gradle deleted file mode 100644 index 04ce10422..000000000 --- a/permission_handler_android/example/android/app/build.gradle +++ /dev/null @@ -1,53 +0,0 @@ -plugins { - id "com.android.application" - id "dev.flutter.flutter-gradle-plugin" -} - -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - -android { - namespace 'com.baseflow.permissionhandler.example' - compileSdkVersion 35 - - defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "com.baseflow.permissionhandler.example" - minSdkVersion flutter.minSdkVersion - targetSdkVersion 35 - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - } - - buildTypes { - release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug - } - } -} - -flutter { - source '../..' -} diff --git a/permission_handler_android/example/android/app/build.gradle.kts b/permission_handler_android/example/android/app/build.gradle.kts new file mode 100644 index 000000000..602003d43 --- /dev/null +++ b/permission_handler_android/example/android/app/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + id("com.android.application") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.baseflow.permissionhandler.example" + compileSdk = 37 + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.baseflow.permissionhandler.example" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = 35 + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + +flutter { + source = "../.." +} diff --git a/permission_handler_android/example/android/build.gradle b/permission_handler_android/example/android/build.gradle deleted file mode 100644 index 0ed2bee85..000000000 --- a/permission_handler_android/example/android/build.gradle +++ /dev/null @@ -1,30 +0,0 @@ -allprojects { - repositories { - google() - mavenCentral() - } -} - -rootProject.buildDir = '../build' -subprojects { - project.buildDir = "${rootProject.buildDir}/${project.name}" -} -subprojects { - project.evaluationDependsOn(':app') -} - -tasks.register("clean", Delete) { - delete rootProject.buildDir -} - -// Build the plugin project with warnings enabled. This is here rather than -// in the plugin itself to avoid breaking clients that have different -// warnings (e.g., deprecation warnings from a newer SDK than this project -// builds with). -gradle.projectsEvaluated { - project(":permission_handler_android") { - tasks.withType(JavaCompile) { - options.compilerArgs << "-Xlint:all" << "-Werror" - } - } -} diff --git a/permission_handler_android/example/android/build.gradle.kts b/permission_handler_android/example/android/build.gradle.kts new file mode 100644 index 000000000..dbee657bb --- /dev/null +++ b/permission_handler_android/example/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/permission_handler_android/example/android/gradle.properties b/permission_handler_android/example/android/gradle.properties index 598d13fee..d5da7278a 100644 --- a/permission_handler_android/example/android/gradle.properties +++ b/permission_handler_android/example/android/gradle.properties @@ -1,3 +1,6 @@ -org.gradle.jvmargs=-Xmx4G +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true -android.enableJetifier=true +# This builtInKotlin flag was added automatically by Flutter migrator +android.builtInKotlin=false +# This newDsl flag was added automatically by Flutter migrator +android.newDsl=false diff --git a/permission_handler_android/example/android/gradle/wrapper/gradle-wrapper.properties b/permission_handler_android/example/android/gradle/wrapper/gradle-wrapper.properties index db18181ac..2e1113280 100644 --- a/permission_handler_android/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/permission_handler_android/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ -#Fri Jun 23 08:50:38 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip diff --git a/permission_handler_android/example/android/settings.gradle b/permission_handler_android/example/android/settings.gradle deleted file mode 100644 index 56eb85cb1..000000000 --- a/permission_handler_android/example/android/settings.gradle +++ /dev/null @@ -1,24 +0,0 @@ -pluginManagement { - def flutterSdkPath = { - def properties = new Properties() - file("local.properties").withInputStream { properties.load(it) } - def flutterSdkPath = properties.getProperty("flutter.sdk") - assert flutterSdkPath != null, "flutter.sdk not set in local.properties" - return flutterSdkPath - }() - - includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") - - repositories { - google() - mavenCentral() - gradlePluginPortal() - } -} - -plugins { - id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.7.0" apply false -} - -include ":app" \ No newline at end of file diff --git a/permission_handler_android/example/android/settings.gradle.kts b/permission_handler_android/example/android/settings.gradle.kts new file mode 100644 index 000000000..c21f0c5b4 --- /dev/null +++ b/permission_handler_android/example/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "9.0.1" apply false + id("org.jetbrains.kotlin.android") version "2.3.20" apply false +} + +include(":app") diff --git a/permission_handler_android/example/pubspec.yaml b/permission_handler_android/example/pubspec.yaml index e76011501..22839e1ae 100644 --- a/permission_handler_android/example/pubspec.yaml +++ b/permission_handler_android/example/pubspec.yaml @@ -3,6 +3,7 @@ description: Demonstrates how to use the permission_handler_android plugin. environment: sdk: ^3.7.0 +resolution: workspace dependencies: baseflow_plugin_template: ^2.1.2 @@ -13,14 +14,8 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - - permission_handler_android: - # When depending on this package from a real application you should use: - # permission_handler_android: ^x.y.z - # See https://dart.dev/tools/pub/dependencies#version-constraints - # The example app is bundled with the plugin so we use a path dependency on - # the parent directory to use the current plugin's version. - path: ../ + flutter_lints: ^5.0.0 + permission_handler_android: ^14.0.1 url_launcher: ^6.0.12 diff --git a/permission_handler_android/pubspec.yaml b/permission_handler_android/pubspec.yaml index b16ec5278..a60c3ed14 100644 --- a/permission_handler_android/pubspec.yaml +++ b/permission_handler_android/pubspec.yaml @@ -1,11 +1,12 @@ name: permission_handler_android description: Permission plugin for Flutter. This plugin provides the Android API to request and check permissions. homepage: https://github.com/baseflow/flutter-permission-handler -version: 13.0.1 +version: 14.0.1 environment: - sdk: ^3.5.0 + sdk: ^3.6.0 flutter: ">=3.24.0" +resolution: workspace flutter: plugin: diff --git a/permission_handler_apple/CHANGELOG.md b/permission_handler_apple/CHANGELOG.md index 8065518a1..b0ab7d688 100644 --- a/permission_handler_apple/CHANGELOG.md +++ b/permission_handler_apple/CHANGELOG.md @@ -6,6 +6,7 @@ ## 9.4.9 * Rewrites copyleft code from stackoverflow to fix compliance issue. +* Added support for the new Android 17 permission `ACCESS_LOCAL_NETWORK` ## 9.4.8 diff --git a/permission_handler_apple/example/lib/main.dart b/permission_handler_apple/example/lib/main.dart index 4750f1ac5..ae9563d62 100644 --- a/permission_handler_apple/example/lib/main.dart +++ b/permission_handler_apple/example/lib/main.dart @@ -32,11 +32,11 @@ class PermissionHandlerWidget extends StatefulWidget { } @override - _PermissionHandlerWidgetState createState() => - _PermissionHandlerWidgetState(); + PermissionHandlerWidgetState createState() => PermissionHandlerWidgetState(); } -class _PermissionHandlerWidgetState extends State { +/// The state of the [PermissionHandlerWidget] that listens for permission status changes. +class PermissionHandlerWidgetState extends State { @override Widget build(BuildContext context) { return Center( diff --git a/permission_handler_apple/example/pubspec.yaml b/permission_handler_apple/example/pubspec.yaml index 71aaacd79..042d6ae27 100644 --- a/permission_handler_apple/example/pubspec.yaml +++ b/permission_handler_apple/example/pubspec.yaml @@ -3,23 +3,21 @@ description: Demonstrates how to use the permission_handler_apple plugin. environment: sdk: ^3.7.0 + flutter: ">=3.24.0" dependencies: baseflow_plugin_template: ^2.1.1 + permission_handler_apple: ^9.4.9 + permission_handler_platform_interface: ^4.3.2 flutter: sdk: flutter +resolution: workspace + dev_dependencies: flutter_test: sdk: flutter - - permission_handler_apple: - # When depending on this package from a real application you should use: - # permission_handler: ^x.y.z - # See https://dart.dev/tools/pub/dependencies#version-constraints - # The example app is bundled with the plugin so we use a path dependency on - # the parent directory to use the current plugin's version. - path: ../ + flutter_lints: ^5.0.0 url_launcher: ^6.3.2 diff --git a/permission_handler_apple/ios/.gitignore b/permission_handler_apple/ios/.gitignore index 3ed647344..8a62ea716 100644 --- a/permission_handler_apple/ios/.gitignore +++ b/permission_handler_apple/ios/.gitignore @@ -3,38 +3,40 @@ .sconsign.dblite .svn/ -.DS_Store -*.swp -profile - -DerivedData/ -build/ -GeneratedPluginRegistrant.h -GeneratedPluginRegistrant.m - -.generated/ - -*.pbxuser +**/dgph *.mode1v3 *.mode2v3 +*.moved-aside +*.pbxuser *.perspectivev3 - -!default.pbxuser +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. !default.mode1v3 !default.mode2v3 +!default.pbxuser !default.perspectivev3 - -xcuserdata - -*.moved-aside - -*.pyc -*sync/ -Icon? -.tags* - -/Flutter/Generated.xcconfig - # Swift Package Manager .build/ *.resolved diff --git a/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/PermissionHandlerEnums.h b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/PermissionHandlerEnums.h index c153dc500..69cf6620c 100644 --- a/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/PermissionHandlerEnums.h +++ b/permission_handler_apple/ios/permission_handler_apple/Sources/permission_handler_apple/PermissionHandlerEnums.h @@ -163,7 +163,8 @@ typedef NS_ENUM(int, PermissionGroup) { PermissionGroupCalendarWriteOnly, PermissionGroupCalendarFullAccess, PermissionGroupAssistant, - PermissionGroupBackgroundRefresh + PermissionGroupBackgroundRefresh, + PermissionGroupAccessLocalNetwork }; typedef NS_ENUM(int, PermissionStatus) { diff --git a/permission_handler_apple/pubspec.yaml b/permission_handler_apple/pubspec.yaml index 97963c0ea..5ed4a98e4 100644 --- a/permission_handler_apple/pubspec.yaml +++ b/permission_handler_apple/pubspec.yaml @@ -5,8 +5,9 @@ issue_tracker: https://github.com/Baseflow/flutter-permission-handler/issues version: 9.4.10 environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ^3.6.0 + flutter: ">=3.24.0" +resolution: workspace flutter: plugin: @@ -21,5 +22,5 @@ dependencies: permission_handler_platform_interface: ^4.2.0 dev_dependencies: - flutter_lints: ^1.0.4 + flutter_lints: ^5.0.0 plugin_platform_interface: ^2.0.0 diff --git a/permission_handler_html/CHANGELOG.md b/permission_handler_html/CHANGELOG.md index bfe470242..4349fbf58 100644 --- a/permission_handler_html/CHANGELOG.md +++ b/permission_handler_html/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.4+0 + +* Added support for the new Android 17 permission `ACCESS_LOCAL_NETWORK` + ## 0.1.3+5 - Updates the way how `window.navigator.mediaDevices` is accessed to keep supporting WASM. diff --git a/permission_handler_html/example/lib/main.dart b/permission_handler_html/example/lib/main.dart index e3650eba8..3cc3d0b5b 100644 --- a/permission_handler_html/example/lib/main.dart +++ b/permission_handler_html/example/lib/main.dart @@ -3,29 +3,33 @@ import 'package:flutter/material.dart'; import 'package:permission_handler_platform_interface/permission_handler_platform_interface.dart'; void main() { - runApp(BaseflowPluginExample( + runApp( + BaseflowPluginExample( pluginName: 'Permission Handler', githubURL: 'https://github.com/Baseflow/flutter-permission-handler', pubDevURL: 'https://pub.dev/packages/permission_handler', - pages: [PermissionHandlerWidget.createPage()])); + pages: [PermissionHandlerWidget.createPage()], + ), + ); } ///Defines the main theme color final MaterialColor themeMaterialColor = BaseflowPluginExample.createMaterialColor( - const Color.fromRGBO(48, 49, 60, 1)); + const Color.fromRGBO(48, 49, 60, 1), +); /// A Flutter application demonstrating the functionality of this plugin class PermissionHandlerWidget extends StatefulWidget { /// Creates a [PermissionHandlerWidget]. - const PermissionHandlerWidget({ - super.key, - }); + const PermissionHandlerWidget({super.key}); /// Create a page containing the functionality of this plugin static ExamplePage createPage() { return ExamplePage( - Icons.location_on, (context) => const PermissionHandlerWidget()); + Icons.location_on, + (context) => const PermissionHandlerWidget(), + ); } @override @@ -38,18 +42,19 @@ class _PermissionHandlerWidgetState extends State { Widget build(BuildContext context) { return Center( child: ListView( - children: Permission.values - .where((permission) { - return permission != Permission.unknown && - permission != Permission.mediaLibrary && - permission != Permission.photos && - permission != Permission.photosAddOnly && - permission != Permission.reminders && - permission != Permission.appTrackingTransparency && - permission != Permission.criticalAlerts; - }) - .map((permission) => PermissionWidget(permission)) - .toList()), + children: Permission.values + .where((permission) { + return permission != Permission.unknown && + permission != Permission.mediaLibrary && + permission != Permission.photos && + permission != Permission.photosAddOnly && + permission != Permission.reminders && + permission != Permission.appTrackingTransparency && + permission != Permission.criticalAlerts; + }) + .map((permission) => PermissionWidget(permission)) + .toList(), + ), ); } } @@ -57,10 +62,7 @@ class _PermissionHandlerWidgetState extends State { /// Permission widget containing information about the passed [Permission] class PermissionWidget extends StatefulWidget { /// Constructs a [PermissionWidget] for the supplied [Permission] - const PermissionWidget( - this._permission, { - super.key, - }); + const PermissionWidget(this._permission, {super.key}); final Permission _permission; @@ -84,8 +86,9 @@ class _PermissionState extends State { void _listenForPermissionStatus() async { await _permissionHandler.checkPermissionStatus(widget._permission).then( - (status) => setState(() => _permissionStatus = status), - onError: (error, st) => debugPrint('$error')); + (status) => setState(() => _permissionStatus = status), + onError: (error, st) => debugPrint('$error'), + ); } Color getPermissionColor() { @@ -114,14 +117,14 @@ class _PermissionState extends State { ), trailing: (widget._permission is PermissionWithService) ? IconButton( - icon: const Icon( - Icons.info, - color: Colors.white, - ), + icon: const Icon(Icons.info, color: Colors.white), onPressed: () { checkServiceStatus( - context, widget._permission as PermissionWithService); - }) + context, + widget._permission as PermissionWithService, + ); + }, + ) : null, onTap: () { requestPermission(widget._permission); @@ -130,18 +133,24 @@ class _PermissionState extends State { } void checkServiceStatus( - BuildContext context, PermissionWithService permission) async { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text( - (await _permissionHandler.checkServiceStatus(permission)).toString()), - )); + BuildContext context, + PermissionWithService permission, + ) async { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + (await _permissionHandler.checkServiceStatus(permission)).toString(), + ), + ), + ); } Future requestPermission(Permission permission) async { await _permissionHandler.requestPermissions([permission]).then( - (status) => setState(() { - _permissionStatus = status[permission] ?? PermissionStatus.denied; - }), - onError: (error, st) => debugPrint('$error')); + (status) => setState(() { + _permissionStatus = status[permission] ?? PermissionStatus.denied; + }), + onError: (error, st) => debugPrint('$error'), + ); } } diff --git a/permission_handler_html/example/pubspec.yaml b/permission_handler_html/example/pubspec.yaml index f380c161a..122092b5b 100644 --- a/permission_handler_html/example/pubspec.yaml +++ b/permission_handler_html/example/pubspec.yaml @@ -2,8 +2,10 @@ name: permission_handler_html_example description: Demonstrates how to use the permission_handler_html plugin. environment: - sdk: ">=3.0.5 <4.0.0" + sdk: ^3.6.0 + flutter: ">=3.24.0" +resolution: workspace dependencies: permission_handler_platform_interface: ^4.2.0 baseflow_plugin_template: ^2.1.2 @@ -15,6 +17,7 @@ dev_dependencies: sdk: flutter flutter_test: sdk: flutter + flutter_lints: ^5.0.0 permission_handler_html: # When depending on this package from a real application you should use: diff --git a/permission_handler_html/lib/permission_handler_html.dart b/permission_handler_html/lib/permission_handler_html.dart index 8be2fa7c4..05b8afa0a 100644 --- a/permission_handler_html/lib/permission_handler_html.dart +++ b/permission_handler_html/lib/permission_handler_html.dart @@ -33,22 +33,18 @@ class WebPermissionHandler extends PermissionHandlerPlatform { /// Registers the web plugin implementation. static void registerWith(Registrar registrar) { PermissionHandlerPlatform.instance = WebPermissionHandler( - webDelegate: WebDelegate( - _devices, - _geolocation, - _htmlPermissions, - ), + webDelegate: WebDelegate(_devices, _geolocation, _htmlPermissions), ); } /// Constructs a WebPermissionHandler. - WebPermissionHandler({ - required WebDelegate webDelegate, - }) : _webDelegate = webDelegate; + WebPermissionHandler({required WebDelegate webDelegate}) + : _webDelegate = webDelegate; @override Future> requestPermissions( - List permissions) async { + List permissions, + ) async { return _webDelegate.requestPermissions(permissions); } @@ -64,7 +60,8 @@ class WebPermissionHandler extends PermissionHandlerPlatform { @override Future shouldShowRequestPermissionRationale( - Permission permission) async { + Permission permission, + ) async { return SynchronousFuture(false); } diff --git a/permission_handler_html/lib/web_delegate.dart b/permission_handler_html/lib/web_delegate.dart index 1db991418..0736ab310 100644 --- a/permission_handler_html/lib/web_delegate.dart +++ b/permission_handler_html/lib/web_delegate.dart @@ -61,7 +61,9 @@ class WebDelegate { } Future _permissionStatusState( - String webPermissionName, web.Permissions? permissions) async { + String webPermissionName, + web.Permissions? permissions, + ) async { final webPermissionStatus = await permissions ?.query(_PermissionDescriptor(name: webPermissionName)) .toDart; @@ -91,8 +93,10 @@ class WebDelegate { audioTracks[0].stop(); } } - } on web.DOMException { - return false; + } catch (e) { + if (e.isA()) { + return false; + } } return true; @@ -121,17 +125,19 @@ class WebDelegate { videoTracks[0].stop(); } } - } on web.DOMException { - return false; + } catch (e) { + if (e.isA()) { + return false; + } } return true; } Future _requestNotificationPermission() async { - return web.Notification.requestPermission() - .toDart - .then((permission) => (permission == "granted".toJS)); + return web.Notification.requestPermission().toDart.then( + (permission) => (permission == "granted".toJS), + ); } Future _requestLocationPermission() async { @@ -152,14 +158,16 @@ class WebDelegate { } Future _requestSingularPermission( - Permission permission) async { + Permission permission, + ) async { bool permissionGranted = switch (permission) { Permission.microphone => await _requestMicrophonePermission(), Permission.camera => await _requestCameraPermission(), Permission.notification => await _requestNotificationPermission(), Permission.location => await _requestLocationPermission(), _ => throw UnsupportedError( - 'The ${permission.toString()} permission is currently not supported on web.') + 'The ${permission.toString()} permission is currently not supported on web.', + ), }; if (!permissionGranted) { @@ -173,13 +181,15 @@ class WebDelegate { /// /// Returns a [Map] containing the status per requested [Permission]. Future> requestPermissions( - List permissions) async { + List permissions, + ) async { final Map permissionStatusMap = {}; for (final permission in permissions) { try { - permissionStatusMap[permission] = - await _requestSingularPermission(permission); + permissionStatusMap[permission] = await _requestSingularPermission( + permission, + ); } on UnimplementedError { rethrow; } diff --git a/permission_handler_html/pubspec.yaml b/permission_handler_html/pubspec.yaml index 2b3a022e5..7af165520 100644 --- a/permission_handler_html/pubspec.yaml +++ b/permission_handler_html/pubspec.yaml @@ -1,12 +1,13 @@ name: permission_handler_html description: Permission plugin for Flutter. This plugin provides the web API to request and check permissions. -version: 0.1.3+5 +version: 0.1.4+0 homepage: https://github.com/baseflow/flutter-permission-handler environment: - sdk: ">=3.3.0 <4.0.0" - flutter: ">=3.16.0" + sdk: ^3.5.0 + flutter: ">=3.24.0" +resolution: workspace dependencies: flutter: @@ -19,7 +20,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^2.0.0 + flutter_lints: ^5.0.0 mockito: ^5.4.2 build_runner: ^2.1.2 test: ^1.24.4 diff --git a/permission_handler_platform_interface/CHANGELOG.md b/permission_handler_platform_interface/CHANGELOG.md index 825a49ee8..a75b9f029 100644 --- a/permission_handler_platform_interface/CHANGELOG.md +++ b/permission_handler_platform_interface/CHANGELOG.md @@ -1,3 +1,11 @@ +## 4.3.2 + +* Fix support for the new Android 17 permission `ACCESS_LOCAL_NETWORK` + +## 4.3.1 + +* Added support for the new Android 17 permission `ACCESS_LOCAL_NETWORK` + ## 4.3.0 - Updates project dependencies. diff --git a/permission_handler_platform_interface/lib/src/method_channel/method_channel_permission_handler.dart b/permission_handler_platform_interface/lib/src/method_channel/method_channel_permission_handler.dart index 5f03e3e7a..ca11e55d0 100644 --- a/permission_handler_platform_interface/lib/src/method_channel/method_channel_permission_handler.dart +++ b/permission_handler_platform_interface/lib/src/method_channel/method_channel_permission_handler.dart @@ -6,8 +6,9 @@ import 'package:flutter/services.dart'; import '../../permission_handler_platform_interface.dart'; import 'utils/codec.dart'; -const MethodChannel _methodChannel = - MethodChannel('flutter.baseflow.com/permissions/methods'); +const MethodChannel _methodChannel = MethodChannel( + 'flutter.baseflow.com/permissions/methods', +); /// An implementation of [PermissionHandlerPlatform] that uses [MethodChannel]s. class MethodChannelPermissionHandler extends PermissionHandlerPlatform { @@ -15,7 +16,9 @@ class MethodChannelPermissionHandler extends PermissionHandlerPlatform { @override Future checkPermissionStatus(Permission permission) async { final status = await _methodChannel.invokeMethod( - 'checkPermissionStatus', permission.value); + 'checkPermissionStatus', + permission.value, + ); return decodePermissionStatus(status); } @@ -52,7 +55,9 @@ class MethodChannelPermissionHandler extends PermissionHandlerPlatform { @override Future checkServiceStatus(Permission permission) async { final status = await _methodChannel.invokeMethod( - 'checkServiceStatus', permission.value); + 'checkServiceStatus', + permission.value, + ); return decodeServiceStatus(status); } @@ -74,10 +79,13 @@ class MethodChannelPermissionHandler extends PermissionHandlerPlatform { /// Returns a [Map] containing the status per requested [Permission]. @override Future> requestPermissions( - List permissions) async { + List permissions, + ) async { final data = encodePermissions(permissions); - final status = - await _methodChannel.invokeMethod('requestPermissions', data); + final status = await _methodChannel.invokeMethod( + 'requestPermissions', + data, + ); return decodePermissionRequestResult(Map.from(status)); } @@ -88,13 +96,16 @@ class MethodChannelPermissionHandler extends PermissionHandlerPlatform { /// returns [false]. @override Future shouldShowRequestPermissionRationale( - Permission permission) async { + Permission permission, + ) async { if (defaultTargetPlatform != TargetPlatform.android) { return false; } final shouldShowRationale = await _methodChannel.invokeMethod( - 'shouldShowRequestPermissionRationale', permission.value); + 'shouldShowRequestPermissionRationale', + permission.value, + ); return shouldShowRationale ?? false; } diff --git a/permission_handler_platform_interface/lib/src/method_channel/utils/codec.dart b/permission_handler_platform_interface/lib/src/method_channel/utils/codec.dart index dc5db075c..99bd0768a 100644 --- a/permission_handler_platform_interface/lib/src/method_channel/utils/codec.dart +++ b/permission_handler_platform_interface/lib/src/method_channel/utils/codec.dart @@ -13,9 +13,14 @@ ServiceStatus decodeServiceStatus(int value) { /// Converts the given [Map] of [int]s into a [Map] with [Permission]s as /// keys and their respective [PermissionStatus] as value. Map decodePermissionRequestResult( - Map value) { - return value.map((key, value) => MapEntry( - Permission.byValue(key), PermissionStatusValue.statusByValue(value))); + Map value, +) { + return value.map( + (key, value) => MapEntry( + Permission.byValue(key), + PermissionStatusValue.statusByValue(value), + ), + ); } /// Converts the given [List] of [Permission]s into a [List] of [int]s which diff --git a/permission_handler_platform_interface/lib/src/permission_handler_platform_interface.dart b/permission_handler_platform_interface/lib/src/permission_handler_platform_interface.dart index 32cb2b722..24685265e 100644 --- a/permission_handler_platform_interface/lib/src/permission_handler_platform_interface.dart +++ b/permission_handler_platform_interface/lib/src/permission_handler_platform_interface.dart @@ -32,7 +32,8 @@ abstract class PermissionHandlerPlatform extends PlatformInterface { /// Checks the current status of the given [Permission]. Future checkPermissionStatus(Permission permission) { throw UnimplementedError( - 'checkPermissionStatus() has not been implemented.'); + 'checkPermissionStatus() has not been implemented.', + ); } /// Checks the current status of the service associated with the given @@ -73,7 +74,8 @@ abstract class PermissionHandlerPlatform extends PlatformInterface { /// /// Returns a [Map] containing the status per requested [Permission]. Future> requestPermissions( - List permissions) { + List permissions, + ) { throw UnimplementedError('requestPermissions() has not been implemented.'); } @@ -83,6 +85,7 @@ abstract class PermissionHandlerPlatform extends PlatformInterface { /// returns [false]. Future shouldShowRequestPermissionRationale(Permission permission) { throw UnimplementedError( - 'shouldShowRequestPermissionRationale() has not been implemented.'); + 'shouldShowRequestPermissionRationale() has not been implemented.', + ); } } diff --git a/permission_handler_platform_interface/lib/src/permissions.dart b/permission_handler_platform_interface/lib/src/permissions.dart index b81a99f28..8eb1f743f 100644 --- a/permission_handler_platform_interface/lib/src/permissions.dart +++ b/permission_handler_platform_interface/lib/src/permissions.dart @@ -326,6 +326,11 @@ class Permission { /// Permission for reading the current background refresh status. (iOS only) static const backgroundRefresh = Permission._(39); + /// Permission for using local network protocols (broad access) + /// + /// Android 17+ (API 37+) + static const accessLocalNetwork = Permission._(40); + /// Returns a list of all possible [PermissionGroup] values. static const List values = [ // ignore: deprecated_member_use_from_same_package @@ -369,6 +374,7 @@ class Permission { calendarFullAccess, assistant, backgroundRefresh, + accessLocalNetwork, ]; static const List _names = [ @@ -412,6 +418,7 @@ class Permission { 'calendarFullAccess', 'assistant', 'backgroundRefresh', + 'accessLocalNetwork', ]; @override diff --git a/permission_handler_platform_interface/pubspec.yaml b/permission_handler_platform_interface/pubspec.yaml index 586671633..157c686d5 100644 --- a/permission_handler_platform_interface/pubspec.yaml +++ b/permission_handler_platform_interface/pubspec.yaml @@ -3,11 +3,12 @@ description: A common platform interface for the permission_handler plugin. homepage: https://github.com/baseflow/flutter-permission-handler/tree/master/permission_handler_platform_interface # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 4.3.0 +version: 4.3.2 environment: - sdk: ^3.5.0 + sdk: ^3.6.0 flutter: ">=3.24.0" +resolution: workspace dependencies: flutter: diff --git a/permission_handler_platform_interface/test/src/method_channel/method_channel_mock.dart b/permission_handler_platform_interface/test/src/method_channel/method_channel_mock.dart index 2b5da964a..77e4671f4 100644 --- a/permission_handler_platform_interface/test/src/method_channel/method_channel_mock.dart +++ b/permission_handler_platform_interface/test/src/method_channel/method_channel_mock.dart @@ -19,8 +19,10 @@ class MethodChannelMock { Future _handler(MethodCall methodCall) async { if (methodCall.method != method) { - throw MissingPluginException('No implementation found for method ' - '$method on channel ${methodChannel.name}'); + throw MissingPluginException( + 'No implementation found for method ' + '$method on channel ${methodChannel.name}', + ); } return Future.delayed(delay, () { diff --git a/permission_handler_platform_interface/test/src/method_channel/method_channel_permission_handler_test.dart b/permission_handler_platform_interface/test/src/method_channel/method_channel_permission_handler_test.dart index c03c279a1..b39410a65 100644 --- a/permission_handler_platform_interface/test/src/method_channel/method_channel_permission_handler_test.dart +++ b/permission_handler_platform_interface/test/src/method_channel/method_channel_permission_handler_test.dart @@ -15,129 +15,140 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('checkPermissionStatus: When checking for permission', () { - test('Should receive granted if user wants access to the requested feature', - () async { - MethodChannelMock( - channelName: 'flutter.baseflow.com/permissions/methods', - method: 'checkPermissionStatus', - result: PermissionStatus.denied.value, - ); - - final permissionStatus = await MethodChannelPermissionHandler() - .checkPermissionStatus(Permission.contacts); - - expect(permissionStatus, PermissionStatus.denied); - }); - - test('Should receive denied if user denied access to the requested feature', - () async { - MethodChannelMock( - channelName: 'flutter.baseflow.com/permissions/methods', - method: 'checkPermissionStatus', - result: PermissionStatus.denied.value, - ); - - final permissionStatus = await MethodChannelPermissionHandler() - .checkPermissionStatus(Permission.contacts); - - expect(permissionStatus, PermissionStatus.denied); - }); - test( - // ignore: lines_longer_than_80_chars - 'Should receive restricted if OS denied rights for to the requested feature', - () async { - MethodChannelMock( - channelName: 'flutter.baseflow.com/permissions/methods', - method: 'checkPermissionStatus', - result: PermissionStatus.restricted.value, - ); + 'Should receive granted if user wants access to the requested feature', + () async { + MethodChannelMock( + channelName: 'flutter.baseflow.com/permissions/methods', + method: 'checkPermissionStatus', + result: PermissionStatus.denied.value, + ); - final permissionStatus = await MethodChannelPermissionHandler() - .checkPermissionStatus(Permission.contacts); + final permissionStatus = await MethodChannelPermissionHandler() + .checkPermissionStatus(Permission.contacts); - expect(permissionStatus, PermissionStatus.restricted); - }); + expect(permissionStatus, PermissionStatus.denied); + }, + ); test( - // ignore: lines_longer_than_80_chars - 'Should receive limited if user has authorized this application for limited access', - () async { - MethodChannelMock( - channelName: 'flutter.baseflow.com/permissions/methods', - method: 'checkPermissionStatus', - result: PermissionStatus.limited.value, - ); + 'Should receive denied if user denied access to the requested feature', + () async { + MethodChannelMock( + channelName: 'flutter.baseflow.com/permissions/methods', + method: 'checkPermissionStatus', + result: PermissionStatus.denied.value, + ); - final permissionStatus = await MethodChannelPermissionHandler() - .checkPermissionStatus(Permission.contacts); + final permissionStatus = await MethodChannelPermissionHandler() + .checkPermissionStatus(Permission.contacts); - expect(permissionStatus, PermissionStatus.limited); - }); + expect(permissionStatus, PermissionStatus.denied); + }, + ); test( - // ignore: lines_longer_than_80_chars - 'Should receive permanentlyDenied if user denied access and selected to never show a request for this permission again', - () async { - MethodChannelMock( - channelName: 'flutter.baseflow.com/permissions/methods', - method: 'checkPermissionStatus', - result: PermissionStatus.permanentlyDenied.value, - ); + // ignore: lines_longer_than_80_chars + 'Should receive restricted if OS denied rights for to the requested feature', + () async { + MethodChannelMock( + channelName: 'flutter.baseflow.com/permissions/methods', + method: 'checkPermissionStatus', + result: PermissionStatus.restricted.value, + ); + + final permissionStatus = await MethodChannelPermissionHandler() + .checkPermissionStatus(Permission.contacts); + + expect(permissionStatus, PermissionStatus.restricted); + }, + ); - final permissionStatus = await MethodChannelPermissionHandler() - .checkPermissionStatus(Permission.contacts); + test( + // ignore: lines_longer_than_80_chars + 'Should receive limited if user has authorized this application for limited access', + () async { + MethodChannelMock( + channelName: 'flutter.baseflow.com/permissions/methods', + method: 'checkPermissionStatus', + result: PermissionStatus.limited.value, + ); + + final permissionStatus = await MethodChannelPermissionHandler() + .checkPermissionStatus(Permission.contacts); + + expect(permissionStatus, PermissionStatus.limited); + }, + ); - expect(permissionStatus, PermissionStatus.permanentlyDenied); - }); + test( + // ignore: lines_longer_than_80_chars + 'Should receive permanentlyDenied if user denied access and selected to never show a request for this permission again', + () async { + MethodChannelMock( + channelName: 'flutter.baseflow.com/permissions/methods', + method: 'checkPermissionStatus', + result: PermissionStatus.permanentlyDenied.value, + ); + + final permissionStatus = await MethodChannelPermissionHandler() + .checkPermissionStatus(Permission.contacts); + + expect(permissionStatus, PermissionStatus.permanentlyDenied); + }, + ); }); group('checkServiceStatus: When checking for service', () { // ignore: lines_longer_than_80_chars test( - 'Should receive disabled if the service for the permission is disabled', - () async { - MethodChannelMock( - channelName: 'flutter.baseflow.com/permissions/methods', - method: 'checkServiceStatus', - result: ServiceStatus.disabled.value, - ); + 'Should receive disabled if the service for the permission is disabled', + () async { + MethodChannelMock( + channelName: 'flutter.baseflow.com/permissions/methods', + method: 'checkServiceStatus', + result: ServiceStatus.disabled.value, + ); - final serviceStatus = await MethodChannelPermissionHandler() - .checkServiceStatus(Permission.contacts); + final serviceStatus = await MethodChannelPermissionHandler() + .checkServiceStatus(Permission.contacts); - expect(serviceStatus, ServiceStatus.disabled); - }); + expect(serviceStatus, ServiceStatus.disabled); + }, + ); - test('Should receive enabled if the service for the permission is enabled', - () async { - MethodChannelMock( - channelName: 'flutter.baseflow.com/permissions/methods', - method: 'checkServiceStatus', - result: ServiceStatus.enabled.value, - ); + test( + 'Should receive enabled if the service for the permission is enabled', + () async { + MethodChannelMock( + channelName: 'flutter.baseflow.com/permissions/methods', + method: 'checkServiceStatus', + result: ServiceStatus.enabled.value, + ); - final serviceStatus = await MethodChannelPermissionHandler() - .checkServiceStatus(Permission.contacts); + final serviceStatus = await MethodChannelPermissionHandler() + .checkServiceStatus(Permission.contacts); - expect(serviceStatus, ServiceStatus.enabled); - }); + expect(serviceStatus, ServiceStatus.enabled); + }, + ); test( - // ignore: lines_longer_than_80_chars - 'Should receive notApplicable if the permission does not have an associated service on the current platform', - () async { - MethodChannelMock( - channelName: 'flutter.baseflow.com/permissions/methods', - method: 'checkServiceStatus', - result: ServiceStatus.notApplicable.value, - ); - - final serviceStatus = await MethodChannelPermissionHandler() - .checkServiceStatus(Permission.contacts); - - expect(serviceStatus, ServiceStatus.notApplicable); - }); + // ignore: lines_longer_than_80_chars + 'Should receive notApplicable if the permission does not have an associated service on the current platform', + () async { + MethodChannelMock( + channelName: 'flutter.baseflow.com/permissions/methods', + method: 'checkServiceStatus', + result: ServiceStatus.notApplicable.value, + ); + + final serviceStatus = await MethodChannelPermissionHandler() + .checkServiceStatus(Permission.contacts); + + expect(serviceStatus, ServiceStatus.notApplicable); + }, + ); }); group('openAppSettings: When opening the App settings', () { @@ -170,36 +181,39 @@ void main() { group('requestPermissions: When requesting for permission', () { // ignore: lines_longer_than_80_chars - test('returns a Map with all the PermissionStatus of the given permissions', - () async { - MethodChannelMock( - channelName: 'flutter.baseflow.com/permissions/methods', - method: 'requestPermissions', - result: mockPermissionMap, - ); - - final result = await MethodChannelPermissionHandler() - .requestPermissions(mockPermissions); - - expect(result, isA>()); - }); + test( + 'returns a Map with all the PermissionStatus of the given permissions', + () async { + MethodChannelMock( + channelName: 'flutter.baseflow.com/permissions/methods', + method: 'requestPermissions', + result: mockPermissionMap, + ); + + final result = await MethodChannelPermissionHandler() + .requestPermissions(mockPermissions); + + expect(result, isA>()); + }, + ); }); group('shouldShowRequestPermissionRationale:', () { test( - // ignore: lines_longer_than_80_chars - 'should return true when you should show a rationale for requesting permission.', - () async { - MethodChannelMock( - channelName: 'flutter.baseflow.com/permissions/methods', - method: 'shouldShowRequestPermissionRationale', - result: true, - ); - - final shouldShowRationale = await MethodChannelPermissionHandler() - .shouldShowRequestPermissionRationale(mockPermissions.first); - - expect(shouldShowRationale, true); - }); + // ignore: lines_longer_than_80_chars + 'should return true when you should show a rationale for requesting permission.', + () async { + MethodChannelMock( + channelName: 'flutter.baseflow.com/permissions/methods', + method: 'shouldShowRequestPermissionRationale', + result: true, + ); + + final shouldShowRationale = await MethodChannelPermissionHandler() + .shouldShowRequestPermissionRationale(mockPermissions.first); + + expect(shouldShowRationale, true); + }, + ); }); } diff --git a/permission_handler_platform_interface/test/src/method_channel/utils/coded_test.dart b/permission_handler_platform_interface/test/src/method_channel/utils/coded_test.dart index 1742b9fc6..2932a6406 100644 --- a/permission_handler_platform_interface/test/src/method_channel/utils/coded_test.dart +++ b/permission_handler_platform_interface/test/src/method_channel/utils/coded_test.dart @@ -15,9 +15,7 @@ void main() { test( 'decodePermissionRequestResult should convert a map' 'to map', () { - var value = { - 1: 1, - }; + var value = {1: 1}; var permissionMap = decodePermissionRequestResult(value); diff --git a/permission_handler_platform_interface/test/src/permission_handler_platform_interface_test.dart b/permission_handler_platform_interface/test/src/permission_handler_platform_interface_test.dart index c965e72ed..157602eef 100644 --- a/permission_handler_platform_interface/test/src/permission_handler_platform_interface_test.dart +++ b/permission_handler_platform_interface/test/src/permission_handler_platform_interface_test.dart @@ -9,8 +9,10 @@ void main() { group('$PermissionHandlerPlatform', () { test('$MethodChannelPermissionHandler is the default instance', () { - expect(PermissionHandlerPlatform.instance, - isA()); + expect( + PermissionHandlerPlatform.instance, + isA(), + ); }); test('Cannot be implemented with `implements`', () { @@ -30,61 +32,71 @@ void main() { }); test( - // ignore: lines_longer_than_80_chars - 'Default implementation of checkPermissionStatus should throw unimplemented error', - () { - final permissionHandlerPlatform = ExtendsPermissionHandlerPlatform(); - - expect(() { - permissionHandlerPlatform - .checkPermissionStatus(Permission.accessMediaLocation); - }, throwsUnimplementedError); - }); + // ignore: lines_longer_than_80_chars + 'Default implementation of checkPermissionStatus should throw unimplemented error', + () { + final permissionHandlerPlatform = ExtendsPermissionHandlerPlatform(); + + expect(() { + permissionHandlerPlatform.checkPermissionStatus( + Permission.accessMediaLocation, + ); + }, throwsUnimplementedError); + }, + ); test( - // ignore: lines_longer_than_80_chars - 'Default implementation of checkServiceStatus should throw unimplemented error', - () { - final permissionHandlerPlatform = ExtendsPermissionHandlerPlatform(); - - expect(() { - permissionHandlerPlatform - .checkServiceStatus(Permission.accessMediaLocation); - }, throwsUnimplementedError); - }); + // ignore: lines_longer_than_80_chars + 'Default implementation of checkServiceStatus should throw unimplemented error', + () { + final permissionHandlerPlatform = ExtendsPermissionHandlerPlatform(); + + expect(() { + permissionHandlerPlatform.checkServiceStatus( + Permission.accessMediaLocation, + ); + }, throwsUnimplementedError); + }, + ); test( - // ignore: lines_longer_than_80_chars - 'Default implementation of openAppSettings should throw unimplemented error', - () { - final permissionHandlerPlatform = ExtendsPermissionHandlerPlatform(); - - expect( - permissionHandlerPlatform.openAppSettings, throwsUnimplementedError); - }); + // ignore: lines_longer_than_80_chars + 'Default implementation of openAppSettings should throw unimplemented error', + () { + final permissionHandlerPlatform = ExtendsPermissionHandlerPlatform(); + + expect( + permissionHandlerPlatform.openAppSettings, + throwsUnimplementedError, + ); + }, + ); test( - // ignore: lines_longer_than_80_chars - 'Default implementation of requestPermissions should throw unimplemented error', - () { - final permissionHandlerPlatform = ExtendsPermissionHandlerPlatform(); - var permission = [Permission.accessMediaLocation]; - - expect(() { - permissionHandlerPlatform.requestPermissions(permission); - }, throwsUnimplementedError); - }); + // ignore: lines_longer_than_80_chars + 'Default implementation of requestPermissions should throw unimplemented error', + () { + final permissionHandlerPlatform = ExtendsPermissionHandlerPlatform(); + var permission = [Permission.accessMediaLocation]; + + expect(() { + permissionHandlerPlatform.requestPermissions(permission); + }, throwsUnimplementedError); + }, + ); test( - // ignore: lines_longer_than_80_chars - 'Default implementation of shouldShowRequestPermissionRationale should throw unimplemented error', - () { - final permissionHandlerPlatform = ExtendsPermissionHandlerPlatform(); - expect(() { - permissionHandlerPlatform.shouldShowRequestPermissionRationale( - Permission.accessMediaLocation); - }, throwsUnimplementedError); - }); + // ignore: lines_longer_than_80_chars + 'Default implementation of shouldShowRequestPermissionRationale should throw unimplemented error', + () { + final permissionHandlerPlatform = ExtendsPermissionHandlerPlatform(); + expect(() { + permissionHandlerPlatform.shouldShowRequestPermissionRationale( + Permission.accessMediaLocation, + ); + }, throwsUnimplementedError); + }, + ); }); } diff --git a/permission_handler_platform_interface/test/src/permission_status_test.dart b/permission_handler_platform_interface/test/src/permission_status_test.dart index d26ee6b32..3e127754d 100644 --- a/permission_handler_platform_interface/test/src/permission_status_test.dart +++ b/permission_handler_platform_interface/test/src/permission_status_test.dart @@ -32,19 +32,32 @@ void main() { }); test( - // ignore: lines_longer_than_80_chars - 'statusByValue should return right index int that corresponds with the right PermissionStatus', - () { - expect(PermissionStatusValue.statusByValue(0), PermissionStatus.denied); - expect(PermissionStatusValue.statusByValue(1), PermissionStatus.granted); - expect( - PermissionStatusValue.statusByValue(2), PermissionStatus.restricted); - expect(PermissionStatusValue.statusByValue(3), PermissionStatus.limited); - expect(PermissionStatusValue.statusByValue(4), - PermissionStatus.permanentlyDenied); - expect( - PermissionStatusValue.statusByValue(5), PermissionStatus.provisional); - }); + // ignore: lines_longer_than_80_chars + 'statusByValue should return right index int that corresponds with the right PermissionStatus', + () { + expect(PermissionStatusValue.statusByValue(0), PermissionStatus.denied); + expect( + PermissionStatusValue.statusByValue(1), + PermissionStatus.granted, + ); + expect( + PermissionStatusValue.statusByValue(2), + PermissionStatus.restricted, + ); + expect( + PermissionStatusValue.statusByValue(3), + PermissionStatus.limited, + ); + expect( + PermissionStatusValue.statusByValue(4), + PermissionStatus.permanentlyDenied, + ); + expect( + PermissionStatusValue.statusByValue(5), + PermissionStatus.provisional, + ); + }, + ); }); group('PermissionStatusGetters', () { @@ -76,11 +89,15 @@ void main() { expect(await mockFuture(PermissionStatus.restricted).isRestricted, true); expect(await mockFuture(PermissionStatus.limited).isLimited, true); expect( - await mockFuture(PermissionStatus.permanentlyDenied) - .isPermanentlyDenied, - true); + await mockFuture( + PermissionStatus.permanentlyDenied, + ).isPermanentlyDenied, + true, + ); expect( - await mockFuture(PermissionStatus.provisional).isProvisional, true); + await mockFuture(PermissionStatus.provisional).isProvisional, + true, + ); }); test('Getters should return false if statement is not met', () async { @@ -89,7 +106,9 @@ void main() { expect(await mockFuture(PermissionStatus.restricted).isDenied, false); expect(await mockFuture(PermissionStatus.limited).isDenied, false); expect( - await mockFuture(PermissionStatus.permanentlyDenied).isDenied, false); + await mockFuture(PermissionStatus.permanentlyDenied).isDenied, + false, + ); expect(await mockFuture(PermissionStatus.provisional).isDenied, false); }); }); diff --git a/permission_handler_platform_interface/test/src/permissions_test.dart b/permission_handler_platform_interface/test/src/permissions_test.dart index 0f8beb470..78160ddae 100644 --- a/permission_handler_platform_interface/test/src/permissions_test.dart +++ b/permission_handler_platform_interface/test/src/permissions_test.dart @@ -5,7 +5,7 @@ void main() { test('Permission has the right amount of possible Permission values', () { const values = Permission.values; - expect(values.length, 40); + expect(values.length, 41); }); test('check if byValue returns corresponding Permission value', () { @@ -32,47 +32,42 @@ void main() { }); test( - // ignore: lines_longer_than_80_chars - 'equality operator should return true for two instances with the same values', - () { - // Arrange - final firstPermission = Permission.byValue(1); - final secondPermission = Permission.byValue(1); + // ignore: lines_longer_than_80_chars + 'equality operator should return true for two instances with the same values', + () { + // Arrange + final firstPermission = Permission.byValue(1); + final secondPermission = Permission.byValue(1); - // Act & Assert - expect( - firstPermission == secondPermission, - true, - ); - }); + // Act & Assert + expect(firstPermission == secondPermission, true); + }, + ); test( - // ignore: lines_longer_than_80_chars - 'equality operator should return false for two instances with different values', - () { - // Arrange - final firstPermission = Permission.byValue(1); - final secondPermission = Permission.byValue(2); + // ignore: lines_longer_than_80_chars + 'equality operator should return false for two instances with different values', + () { + // Arrange + final firstPermission = Permission.byValue(1); + final secondPermission = Permission.byValue(2); - // Act & Assert - expect( - firstPermission == secondPermission, - false, - ); - }); + // Act & Assert + expect(firstPermission == secondPermission, false); + }, + ); - test('hashCode should be the same for two instances with the same values', - () { - // Arrange - final firstPermission = Permission.byValue(1); - final secondPermission = Permission.byValue(1); + test( + 'hashCode should be the same for two instances with the same values', + () { + // Arrange + final firstPermission = Permission.byValue(1); + final secondPermission = Permission.byValue(1); - // Act & Assert - expect( - firstPermission.hashCode, - secondPermission.hashCode, - ); - }); + // Act & Assert + expect(firstPermission.hashCode, secondPermission.hashCode); + }, + ); test('hashCode should not match for two instances with different values', () { // Arrange @@ -80,9 +75,6 @@ void main() { final secondPermission = Permission.byValue(2); // Act & Assert - expect( - firstPermission.hashCode == secondPermission.hashCode, - false, - ); + expect(firstPermission.hashCode == secondPermission.hashCode, false); }); } diff --git a/permission_handler_platform_interface/test/src/service_status_test.dart b/permission_handler_platform_interface/test/src/service_status_test.dart index b5db02424..6dad5489c 100644 --- a/permission_handler_platform_interface/test/src/service_status_test.dart +++ b/permission_handler_platform_interface/test/src/service_status_test.dart @@ -26,13 +26,17 @@ void main() { }); test( - // ignore: lines_longer_than_80_chars - 'statusByValue should return right index int that corresponds with the right PermissionStatus', - () { - expect(ServiceStatusValue.statusByValue(0), ServiceStatus.disabled); - expect(ServiceStatusValue.statusByValue(1), ServiceStatus.enabled); - expect(ServiceStatusValue.statusByValue(2), ServiceStatus.notApplicable); - }); + // ignore: lines_longer_than_80_chars + 'statusByValue should return right index int that corresponds with the right PermissionStatus', + () { + expect(ServiceStatusValue.statusByValue(0), ServiceStatus.disabled); + expect(ServiceStatusValue.statusByValue(1), ServiceStatus.enabled); + expect( + ServiceStatusValue.statusByValue(2), + ServiceStatus.notApplicable, + ); + }, + ); }); group('ServiceStatusGetters', () { diff --git a/permission_handler_windows/CHANGELOG.md b/permission_handler_windows/CHANGELOG.md index 91c2f5049..f951b347d 100644 --- a/permission_handler_windows/CHANGELOG.md +++ b/permission_handler_windows/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.2.2 + +* Added support for the new Android 17 permission `ACCESS_LOCAL_NETWORK` + ## 0.2.1 * Updates the dependency on `permission_handler_platform_interface` to version 4.1.0 (SiriKit support is only available for iOS and macOS). diff --git a/permission_handler_windows/example/lib/main.dart b/permission_handler_windows/example/lib/main.dart index 58a4dc053..4e5f30ca1 100644 --- a/permission_handler_windows/example/lib/main.dart +++ b/permission_handler_windows/example/lib/main.dart @@ -1,50 +1,62 @@ +// ignore_for_file: avoid_print + import 'package:baseflow_plugin_template/baseflow_plugin_template.dart'; import 'package:flutter/material.dart'; import 'package:permission_handler_platform_interface/permission_handler_platform_interface.dart'; void main() { - runApp(BaseflowPluginExample( + runApp( + BaseflowPluginExample( pluginName: 'Permission Handler', githubURL: 'https://github.com/Baseflow/flutter-permission-handler', pubDevURL: 'https://pub.dev/packages/permission_handler', - pages: [PermissionHandlerWidget.createPage()])); + pages: [PermissionHandlerWidget.createPage()], + ), + ); } ///Defines the main theme color final MaterialColor themeMaterialColor = BaseflowPluginExample.createMaterialColor( - const Color.fromRGBO(48, 49, 60, 1)); + const Color.fromRGBO(48, 49, 60, 1), +); /// A Flutter application demonstrating the functionality of this plugin class PermissionHandlerWidget extends StatefulWidget { + /// Creates a [PermissionHandlerWidget] that listens for permission status changes. + const PermissionHandlerWidget({super.key}); + /// Create a page containing the functionality of this plugin static ExamplePage createPage() { return ExamplePage( - Icons.location_on, (context) => PermissionHandlerWidget()); + Icons.location_on, + (context) => PermissionHandlerWidget(), + ); } @override - _PermissionHandlerWidgetState createState() => - _PermissionHandlerWidgetState(); + PermissionHandlerWidgetState createState() => PermissionHandlerWidgetState(); } -class _PermissionHandlerWidgetState extends State { +/// State for the [PermissionHandlerWidget] that listens for permission status changes. +class PermissionHandlerWidgetState extends State { @override Widget build(BuildContext context) { return Center( child: ListView( - children: Permission.values - .where((permission) { - return permission != Permission.unknown && - permission != Permission.mediaLibrary && - permission != Permission.photos && - permission != Permission.photosAddOnly && - permission != Permission.reminders && - permission != Permission.appTrackingTransparency && - permission != Permission.criticalAlerts; - }) - .map((permission) => PermissionWidget(permission)) - .toList()), + children: Permission.values + .where((permission) { + return permission != Permission.unknown && + permission != Permission.mediaLibrary && + permission != Permission.photos && + permission != Permission.photosAddOnly && + permission != Permission.reminders && + permission != Permission.appTrackingTransparency && + permission != Permission.criticalAlerts; + }) + .map((permission) => PermissionWidget(permission)) + .toList(), + ), ); } } @@ -52,18 +64,21 @@ class _PermissionHandlerWidgetState extends State { /// Permission widget containing information about the passed [Permission] class PermissionWidget extends StatefulWidget { /// Constructs a [PermissionWidget] for the supplied [Permission] - const PermissionWidget(this._permission); + const PermissionWidget(this._permission, {super.key}); final Permission _permission; + /// Returns the [Permission] associated with this widget. + Permission get permission => _permission; + @override - _PermissionState createState() => _PermissionState(_permission); + PermissionWidgetState createState() => PermissionWidgetState(); } -class _PermissionState extends State { - _PermissionState(this._permission); - - final Permission _permission; +/// State for the [PermissionWidget] that listens for permission status changes. +class PermissionWidgetState extends State { + /// Constructs a [PermissionWidgetState] for the supplied [PermissionWidget]. + PermissionWidgetState(); final PermissionHandlerPlatform _permissionHandler = PermissionHandlerPlatform.instance; PermissionStatus _permissionStatus = PermissionStatus.denied; @@ -76,10 +91,12 @@ class _PermissionState extends State { } void _listenForPermissionStatus() async { - final status = await _permissionHandler.checkPermissionStatus(_permission); + final status = + await _permissionHandler.checkPermissionStatus(widget.permission); setState(() => _permissionStatus = status); } + /// Returns the color to use for the permission status. Color getPermissionColor() { switch (_permissionStatus) { case PermissionStatus.denied: @@ -97,38 +114,45 @@ class _PermissionState extends State { Widget build(BuildContext context) { return ListTile( title: Text( - _permission.toString(), + widget.permission.toString(), style: Theme.of(context).textTheme.bodyLarge, ), subtitle: Text( _permissionStatus.toString(), style: TextStyle(color: getPermissionColor()), ), - trailing: (_permission is PermissionWithService) + trailing: (widget.permission is PermissionWithService) ? IconButton( - icon: const Icon( - Icons.info, - color: Colors.white, - ), + icon: const Icon(Icons.info, color: Colors.white), onPressed: () { checkServiceStatus( - context, _permission as PermissionWithService); - }) + context, + widget.permission as PermissionWithService, + ); + }, + ) : null, onTap: () { - requestPermission(_permission); + requestPermission(widget.permission); }, ); } + /// Requests permission for the given [Permission]. void checkServiceStatus( - BuildContext context, PermissionWithService permission) async { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text( - (await _permissionHandler.checkServiceStatus(permission)).toString()), - )); + BuildContext context, + PermissionWithService permission, + ) async { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + (await _permissionHandler.checkServiceStatus(permission)).toString(), + ), + ), + ); } + /// Requests permission for the given [Permission]. Future requestPermission(Permission permission) async { final status = await _permissionHandler.requestPermissions([permission]); diff --git a/permission_handler_windows/example/pubspec.yaml b/permission_handler_windows/example/pubspec.yaml index 2deb8e72c..0c9e1d4f5 100644 --- a/permission_handler_windows/example/pubspec.yaml +++ b/permission_handler_windows/example/pubspec.yaml @@ -1,33 +1,29 @@ -name: permission_handler_windows_example -description: Demonstrates how to use the permission_handler_windows plugin. - -environment: - sdk: ">=2.15.0 <3.0.0" - -dependencies: - baseflow_plugin_template: ^2.1.1 - flutter: - sdk: flutter - -dev_dependencies: - flutter_test: - sdk: flutter - - permission_handler_windows: - # When depending on this package from a real application you should use: - # permission_handler_windows: ^x.y.z - # See https://dart.dev/tools/pub/dependencies#version-constraints - # The example app is bundled with the plugin so we use a path dependency on - # the parent directory to use the current plugin's version. - path: ../ - - url_launcher: ^6.0.12 - -flutter: - uses-material-design: true - - assets: - - res/images/baseflow_logo_def_light-02.png - - res/images/poweredByBaseflowLogoLight@3x.png - - packages/baseflow_plugin_template/logo.png - - packages/baseflow_plugin_template/poweredByBaseflow.png +name: permission_handler_windows_example +description: Demonstrates how to use the permission_handler_windows plugin. + +environment: + sdk: ^3.6.0 + flutter: ">=3.24.0" +resolution: workspace + +dependencies: + baseflow_plugin_template: ^2.1.1 + flutter: + sdk: flutter + permission_handler_windows: ^0.2.2 + permission_handler_platform_interface: ^4.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + url_launcher: ^6.0.12 + +flutter: + uses-material-design: true + + assets: + - res/images/baseflow_logo_def_light-02.png + - res/images/poweredByBaseflowLogoLight@3x.png + - packages/baseflow_plugin_template/logo.png + - packages/baseflow_plugin_template/poweredByBaseflow.png diff --git a/permission_handler_windows/pubspec.yaml b/permission_handler_windows/pubspec.yaml index f5553e0d6..b01412999 100644 --- a/permission_handler_windows/pubspec.yaml +++ b/permission_handler_windows/pubspec.yaml @@ -1,6 +1,6 @@ name: permission_handler_windows description: Permission plugin for Flutter. This plugin provides the Windows API to request and check permissions. -version: 0.2.1 +version: 0.2.2 homepage: https://github.com/baseflow/flutter-permission-handler flutter: @@ -21,5 +21,6 @@ dev_dependencies: plugin_platform_interface: ^2.0.0 environment: - sdk: ">=2.12.0 <4.0.0" - flutter: ">=2.0.0" + sdk: ^3.6.0 + flutter: ">=3.24.0" +resolution: workspace diff --git a/permission_handler_windows/windows/permission_constants.h b/permission_handler_windows/windows/permission_constants.h index 1bc10737c..d0c580d68 100644 --- a/permission_handler_windows/windows/permission_constants.h +++ b/permission_handler_windows/windows/permission_constants.h @@ -50,7 +50,8 @@ class PermissionConstants { CALENDAR_WRITE_ONLY = 36, CALENDAR_FULL_ACCESS = 37, ASSISTANT = 38, - BACKGROUND_REFRESH = 39 + BACKGROUND_REFRESH = 39, + ACCESS_LOCAL_NETWORK = 40, }; //PERMISSION_STATUS diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 000000000..56e2c0439 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,21 @@ +name: permission_handler_monorepo +publish_to: none +environment: + sdk: ^3.6.0 + flutter: ">=3.24.0" + +workspace: + - permission_handler + - permission_handler/example + - permission_handler_android + - permission_handler_android/example + - permission_handler_apple + - permission_handler_apple/example + - permission_handler_html + - permission_handler_html/example + - permission_handler_platform_interface + - permission_handler_windows + - permission_handler_windows/example + +dev_dependencies: + flutter_lints: ^5.0.0 From d347e5d221716fc6203b7072e8b21e6b46fa38e7 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Fri, 31 Jul 2026 16:19:53 +0200 Subject: [PATCH 13/26] Removed obsolete and conflicting packageName attribute --- .../example/android/app/src/debug/AndroidManifest.xml | 3 +-- .../example/android/app/src/profile/AndroidManifest.xml | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/permission_handler_android/example/android/app/src/debug/AndroidManifest.xml b/permission_handler_android/example/android/app/src/debug/AndroidManifest.xml index d692acb24..f880684a6 100644 --- a/permission_handler_android/example/android/app/src/debug/AndroidManifest.xml +++ b/permission_handler_android/example/android/app/src/debug/AndroidManifest.xml @@ -1,5 +1,4 @@ - + diff --git a/permission_handler_android/example/android/app/src/profile/AndroidManifest.xml b/permission_handler_android/example/android/app/src/profile/AndroidManifest.xml index acdbca678..9b6b110c8 100644 --- a/permission_handler_android/example/android/app/src/profile/AndroidManifest.xml +++ b/permission_handler_android/example/android/app/src/profile/AndroidManifest.xml @@ -1,5 +1,4 @@ - + From e503c1ceca8fc366ad65c03f290d6bbc5533c4fd Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Fri, 31 Jul 2026 16:22:52 +0200 Subject: [PATCH 14/26] Update version to reflect new feature --- permission_handler_platform_interface/CHANGELOG.md | 8 ++------ permission_handler_platform_interface/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/permission_handler_platform_interface/CHANGELOG.md b/permission_handler_platform_interface/CHANGELOG.md index a75b9f029..c94bf582a 100644 --- a/permission_handler_platform_interface/CHANGELOG.md +++ b/permission_handler_platform_interface/CHANGELOG.md @@ -1,10 +1,6 @@ -## 4.3.2 +## 4.4.0 -* Fix support for the new Android 17 permission `ACCESS_LOCAL_NETWORK` - -## 4.3.1 - -* Added support for the new Android 17 permission `ACCESS_LOCAL_NETWORK` +- Adds support for the new Android 17 permission `ACCESS_LOCAL_NETWORK` ## 4.3.0 diff --git a/permission_handler_platform_interface/pubspec.yaml b/permission_handler_platform_interface/pubspec.yaml index 157c686d5..cbc9afddd 100644 --- a/permission_handler_platform_interface/pubspec.yaml +++ b/permission_handler_platform_interface/pubspec.yaml @@ -3,7 +3,7 @@ description: A common platform interface for the permission_handler plugin. homepage: https://github.com/baseflow/flutter-permission-handler/tree/master/permission_handler_platform_interface # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 4.3.2 +version: 4.4.0 environment: sdk: ^3.6.0 From 96bf5cc4e4f8e15b95e82261c654ab0d705b938b Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Fri, 31 Jul 2026 16:29:32 +0200 Subject: [PATCH 15/26] Update version to reflect new feature --- permission_handler_apple/CHANGELOG.md | 5 ++++- permission_handler_apple/pubspec.yaml | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/permission_handler_apple/CHANGELOG.md b/permission_handler_apple/CHANGELOG.md index b0ab7d688..c6fb55a35 100644 --- a/permission_handler_apple/CHANGELOG.md +++ b/permission_handler_apple/CHANGELOG.md @@ -1,3 +1,7 @@ +## 9.5.0 + +* Adds support for the new Android 17 permission `ACCESS_LOCAL_NETWORK`. + ## 9.4.10 * Fixed Info.plist lookup in Package.swift to auto-apply permissions. @@ -6,7 +10,6 @@ ## 9.4.9 * Rewrites copyleft code from stackoverflow to fix compliance issue. -* Added support for the new Android 17 permission `ACCESS_LOCAL_NETWORK` ## 9.4.8 diff --git a/permission_handler_apple/pubspec.yaml b/permission_handler_apple/pubspec.yaml index 5ed4a98e4..60321eb4d 100644 --- a/permission_handler_apple/pubspec.yaml +++ b/permission_handler_apple/pubspec.yaml @@ -2,7 +2,7 @@ name: permission_handler_apple description: Permission plugin for Flutter. This plugin provides the iOS API to request and check permissions. repository: https://github.com/baseflow/flutter-permission-handler issue_tracker: https://github.com/Baseflow/flutter-permission-handler/issues -version: 9.4.10 +version: 9.5.0 environment: sdk: ^3.6.0 @@ -19,7 +19,7 @@ flutter: dependencies: flutter: sdk: flutter - permission_handler_platform_interface: ^4.2.0 + permission_handler_platform_interface: ^4.4.0 dev_dependencies: flutter_lints: ^5.0.0 From 7b36a22a7e49f1438550e327b698c984ad619c81 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Fri, 31 Jul 2026 16:31:55 +0200 Subject: [PATCH 16/26] Fix version to align with current production version --- permission_handler_android/CHANGELOG.md | 4 ---- permission_handler_android/pubspec.yaml | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/permission_handler_android/CHANGELOG.md b/permission_handler_android/CHANGELOG.md index 7180a5985..b895ea7ed 100644 --- a/permission_handler_android/CHANGELOG.md +++ b/permission_handler_android/CHANGELOG.md @@ -1,7 +1,3 @@ -## 14.0.1 - -- Version bump - ## 14.0.0 - **BREAKING CHANGES:** When updating to version 14.0.0 make sure to also set the `compileSdkVersion` in the `app/build.gradle` file to `37`. diff --git a/permission_handler_android/pubspec.yaml b/permission_handler_android/pubspec.yaml index a60c3ed14..725632701 100644 --- a/permission_handler_android/pubspec.yaml +++ b/permission_handler_android/pubspec.yaml @@ -1,7 +1,7 @@ name: permission_handler_android description: Permission plugin for Flutter. This plugin provides the Android API to request and check permissions. homepage: https://github.com/baseflow/flutter-permission-handler -version: 14.0.1 +version: 14.0.0 environment: sdk: ^3.6.0 @@ -19,7 +19,7 @@ flutter: dependencies: flutter: sdk: flutter - permission_handler_platform_interface: ^4.2.0 + permission_handler_platform_interface: ^4.4.0 dev_dependencies: flutter_lints: ^5.0.0 From 1655dee2afe35c845c4bd7d2c5b9fb89f6bcd9a9 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Fri, 31 Jul 2026 16:35:12 +0200 Subject: [PATCH 17/26] Fix dependency in android example app --- permission_handler/pubspec.yaml | 8 ++++---- permission_handler_android/example/pubspec.yaml | 11 ++++++++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/permission_handler/pubspec.yaml b/permission_handler/pubspec.yaml index d720f281c..26c35ae46 100644 --- a/permission_handler/pubspec.yaml +++ b/permission_handler/pubspec.yaml @@ -25,11 +25,11 @@ dependencies: flutter: sdk: flutter meta: ^1.7.0 - permission_handler_android: ^14.0.1 - permission_handler_apple: ^9.4.8 - permission_handler_html: ^0.1.1 + permission_handler_android: ^14.0.0 + permission_handler_apple: ^9.5.0 + permission_handler_html: ^0.1.4+0 permission_handler_windows: ^0.2.2 - permission_handler_platform_interface: ^4.3.2 + permission_handler_platform_interface: ^4.4.0 dev_dependencies: flutter_lints: ^5.0.0 diff --git a/permission_handler_android/example/pubspec.yaml b/permission_handler_android/example/pubspec.yaml index 22839e1ae..7d2894a03 100644 --- a/permission_handler_android/example/pubspec.yaml +++ b/permission_handler_android/example/pubspec.yaml @@ -9,14 +9,19 @@ dependencies: baseflow_plugin_template: ^2.1.2 flutter: sdk: flutter - permission_handler_platform_interface: ^4.2.0 + permission_handler_platform_interface: ^4.4.0 + permission_handler_android: + # When depending on this package from a real application you should use: + # permission_handler_android: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^5.0.0 - permission_handler_android: ^14.0.1 - url_launcher: ^6.0.12 flutter: From 363bb4010b2ec9ac7bb406f32d946704da1f9a59 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Fri, 31 Jul 2026 16:36:44 +0200 Subject: [PATCH 18/26] Update version to reflect breaking change in Android --- permission_handler/CHANGELOG.md | 4 ---- permission_handler/pubspec.yaml | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/permission_handler/CHANGELOG.md b/permission_handler/CHANGELOG.md index df454a31e..4303f3752 100644 --- a/permission_handler/CHANGELOG.md +++ b/permission_handler/CHANGELOG.md @@ -1,7 +1,3 @@ -## 13.0.1 - -- version bump - ## 13.0.0 - **BREAKING CHANGE:** , android compilesdk now set to version `compileSdkVersion 37` diff --git a/permission_handler/pubspec.yaml b/permission_handler/pubspec.yaml index 26c35ae46..6a50c7ade 100644 --- a/permission_handler/pubspec.yaml +++ b/permission_handler/pubspec.yaml @@ -2,7 +2,7 @@ name: permission_handler description: Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions. repository: https://github.com/baseflow/flutter-permission-handler issue_tracker: https://github.com/Baseflow/flutter-permission-handler/issues -version: 13.0.1 +version: 13.0.0 environment: sdk: ^3.6.0 From 0d9e6ea78c21d127617d1a97ae7240617b57ee38 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Sat, 1 Aug 2026 09:34:14 +0200 Subject: [PATCH 19/26] Fixes error handling in web.delegate --- permission_handler_html/CHANGELOG.md | 6 +++++- permission_handler_html/pubspec.yaml | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/permission_handler_html/CHANGELOG.md b/permission_handler_html/CHANGELOG.md index 4349fbf58..145f6018d 100644 --- a/permission_handler_html/CHANGELOG.md +++ b/permission_handler_html/CHANGELOG.md @@ -1,6 +1,10 @@ +## 0.1.4+1 + +* Replaces runtime-unsafe .isA() calls with Dart is web.DOMException checks to fix web release build failures. + ## 0.1.4+0 -* Added support for the new Android 17 permission `ACCESS_LOCAL_NETWORK` +* Adds support for the new Android 17 permission `ACCESS_LOCAL_NETWORK` ## 0.1.3+5 diff --git a/permission_handler_html/pubspec.yaml b/permission_handler_html/pubspec.yaml index 7af165520..ccb28ca7a 100644 --- a/permission_handler_html/pubspec.yaml +++ b/permission_handler_html/pubspec.yaml @@ -1,6 +1,6 @@ name: permission_handler_html description: Permission plugin for Flutter. This plugin provides the web API to request and check permissions. -version: 0.1.4+0 +version: 0.1.4+1 homepage: https://github.com/baseflow/flutter-permission-handler From ac58849ae39a5b109b2fe2565b13c21ee479d339 Mon Sep 17 00:00:00 2001 From: prkay <57384721+prkay@users.noreply.github.com> Date: Sat, 1 Aug 2026 13:01:51 +0530 Subject: [PATCH 20/26] Fix web release crash: use Dart is checks for DOMException in web_delegate (#1551) --- permission_handler_html/lib/web_delegate.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/permission_handler_html/lib/web_delegate.dart b/permission_handler_html/lib/web_delegate.dart index 0736ab310..32652c1f2 100644 --- a/permission_handler_html/lib/web_delegate.dart +++ b/permission_handler_html/lib/web_delegate.dart @@ -94,7 +94,7 @@ class WebDelegate { } } } catch (e) { - if (e.isA()) { + if (e is web.DOMException) { return false; } } @@ -126,7 +126,7 @@ class WebDelegate { } } } catch (e) { - if (e.isA()) { + if (e is web.DOMException) { return false; } } From b7d387a9ae114180dc9d15274f2cea15366db805 Mon Sep 17 00:00:00 2001 From: Pachebel <89677437+Pachebel@users.noreply.github.com> Date: Mon, 10 Aug 2026 05:00:52 -0300 Subject: [PATCH 21/26] Fix SPM Info.plist discovery for flavored and multi-configuration apps (#1553) The Package.swift manifest derives the PERMISSION_* macros from the host app's Info.plist. Discovery failed in two independent ways: - It only ever looked at `ios/Runner/Info.plist`, so apps using build configuration or flavor specific plists (`Info-Debug.plist`, `Runner/Info-$(CONFIGURATION).plist`, ...) were never matched. - The app-root lookup required a pubspec.yaml and a loadable plist in the same condition, so a miss did not stop the walk-up at the app and could adopt an unrelated app's plist in a monorepo. Info.plist locations are now resolved from INFOPLIST_FILE in the Xcode project and any .xcconfig files, with a scan of `ios/` as a fallback, and the usage description keys found across them are merged. The app root is anchored on a pubspec.yaml next to an `ios/*.xcodeproj`, which is what distinguishes a host app from a plugin package. The INFOPLIST_FILE pattern is anchored so it cannot match inside GENERATE_INFOPLIST_FILE, which Xcode writes into every target it creates from a template, and candidates that are not files on disk no longer count towards "the build settings produced something". Either one alone was enough to suppress the fallback scan and compile out every permission, for an app whose own target names its plist through a build variable this manifest does not expand. Adds PERMISSION_HANDLER_INFO_PLIST to point the manifest at specific plists, which is the only mechanism available to builds started from Xcode.app: those run with `/` as their working directory, and Xcode passes none of its build settings to manifest evaluation. Adds PERMISSION_HANDLER_VERBOSE to log the app root, the plists used and the resolved macros, because Xcode discards manifest output entirely. The example app's Debug configuration now uses a separate `Info-Debug.plist` to cover the reported layout. It is identical to Info.plist except that it declares no NSContactsUsageDescription, so PERMISSION_CONTACTS can only resolve to 1 by merging the two, which is what lets CI assert on the discovery rather than on the example's default plist. A second CI step builds a project that forces the scan fallback. Fixes #1548 --- .../workflows/permission_handler_apple.yaml | 81 ++++ permission_handler_apple/CHANGELOG.md | 18 + permission_handler_apple/README.md | 61 +++ .../ios/Flutter/permission_handler.selected | 1 + .../ios/Runner.xcodeproj/project.pbxproj | 2 +- .../example/ios/Runner/Info-Debug.plist | 118 ++++++ .../permission_handler_apple/Package.swift | 389 ++++++++++++++++-- permission_handler_apple/pubspec.yaml | 2 +- 8 files changed, 634 insertions(+), 38 deletions(-) create mode 100644 permission_handler_apple/example/ios/Flutter/permission_handler.selected create mode 100644 permission_handler_apple/example/ios/Runner/Info-Debug.plist diff --git a/.github/workflows/permission_handler_apple.yaml b/.github/workflows/permission_handler_apple.yaml index 5540a042c..e4ca000b4 100644 --- a/.github/workflows/permission_handler_apple.yaml +++ b/.github/workflows/permission_handler_apple.yaml @@ -61,3 +61,84 @@ jobs: - name: Run iOS build run: flutter build ios --no-codesign --release working-directory: ${{env.example-directory}} + + # Guard the Swift Package Manager permission auto-detection. + # + # Package.swift turns the usage description keys of the host app's + # Info.plist into PERMISSION_* macros. When that discovery breaks every + # macro silently falls back to 0 and all permissions are compiled out, so + # assert on the resolved manifest rather than on the build succeeding. + # + # The example's Debug configuration deliberately uses a separate + # `Info-Debug.plist` — the build-configuration-specific layout reported in + # issue #1548 — which is identical to `Info.plist` except that it declares + # no NSContactsUsageDescription. PERMISSION_CONTACTS can therefore only + # resolve to 1 if *both* plists were discovered and merged, which is what + # makes these assertions test the discovery rather than the example's + # default plist. + - name: Verify SPM permission detection + working-directory: ${{env.example-directory}} + run: | + set -euo pipefail + flutter config --enable-swift-package-manager + flutter build ios --config-only --debug --no-codesign + + rm -rf ~/Library/Caches/org.swift.swiftpm/manifests + PERMISSION_HANDLER_VERBOSE=1 swift package --manifest-cache none \ + --package-path ios/Flutter/ephemeral/Packages/.packages/permission_handler_apple \ + dump-package > "${RUNNER_TEMP}/manifest.json" 2> "${RUNNER_TEMP}/manifest.log" + cat "${RUNNER_TEMP}/manifest.log" + + fail() { # + echo "::error::$1" + grep -o 'PERMISSION_[A-Z_]*=[01]' "${RUNNER_TEMP}/manifest.json" | sort -u + exit 1 + } + + # Both the default and the Debug-only plist must be picked up. + grep -qF 'Runner/Info.plist' "${RUNNER_TEMP}/manifest.log" \ + || fail 'ios/Runner/Info.plist was not discovered' + grep -qF 'Runner/Info-Debug.plist' "${RUNNER_TEMP}/manifest.log" \ + || fail 'ios/Runner/Info-Debug.plist was not discovered — INFOPLIST_FILE parsing is broken' + + # Reached only when two plists were loaded and their keys differ, so + # this is the assertion that proves the merge across configurations. + grep -qF 'NSContactsUsageDescription' "${RUNNER_TEMP}/manifest.log" \ + || fail 'the divergence warning did not name NSContactsUsageDescription — the plists were not merged' + + # Present in Info.plist only; the rest are in both. + for macro in PERMISSION_CONTACTS \ + PERMISSION_CAMERA PERMISSION_MICROPHONE \ + PERMISSION_PHOTOS PERMISSION_LOCATION; do + grep -qF "${macro}=1" "${RUNNER_TEMP}/manifest.json" \ + || fail "${macro} resolved to 0 — Info.plist discovery is broken" + done + + # An app target whose INFOPLIST_FILE is built from a variable this manifest + # does not expand, next to an extension carrying Xcode's stock + # `GENERATE_INFOPLIST_FILE = YES`. Neither yields a usable path, so the + # scan of `ios/` has to take over; a settings hit that resolves to nothing + # must not suppress it. Built here rather than committed, because it is a + # project layout the example cannot also be. + - name: Verify SPM detection falls back to scanning + run: | + set -euo pipefail + app="${RUNNER_TEMP}/fallback-app" + rm -rf "${app}" + mkdir -p "${app}/ios/Extension.xcodeproj" "${app}/ios/Runner" + touch "${app}/pubspec.yaml" + cp "${{env.example-directory}}/ios/Runner/Info.plist" "${app}/ios/Runner/Info.plist" + printf 'GENERATE_INFOPLIST_FILE = YES;\nINFOPLIST_FILE = "$(TARGET_NAME)/Info.plist";\n' \ + > "${app}/ios/Extension.xcodeproj/project.pbxproj" + + cd "${app}" + rm -rf ~/Library/Caches/org.swift.swiftpm/manifests + PERMISSION_HANDLER_VERBOSE=1 swift package --manifest-cache none \ + --package-path "${GITHUB_WORKSPACE}/permission_handler_apple/ios/permission_handler_apple" \ + dump-package > "${RUNNER_TEMP}/fallback.json" 2> "${RUNNER_TEMP}/fallback.log" + cat "${RUNNER_TEMP}/fallback.log" + + grep -qF 'PERMISSION_CAMERA=1' "${RUNNER_TEMP}/fallback.json" || { + echo "::error::the scan fallback did not run — an unresolvable INFOPLIST_FILE suppressed it" + exit 1 + } diff --git a/permission_handler_apple/CHANGELOG.md b/permission_handler_apple/CHANGELOG.md index c6fb55a35..8d20d6a57 100644 --- a/permission_handler_apple/CHANGELOG.md +++ b/permission_handler_apple/CHANGELOG.md @@ -1,3 +1,21 @@ +## 9.5.1 + +* Fixes the Swift Package Manager permission auto-detection, which failed to find the host app's + `Info.plist` and silently compiled out every permission. Apps hit this in two ways: the manifest + only ever looked at `ios/Runner/Info.plist`, so build-configuration or flavor specific plists such + as `Info-Debug.plist` were never seen ([#1548](https://github.com/Baseflow/flutter-permission-handler/issues/1548)), + and the app-root lookup could walk past the app entirely. `Info.plist` locations are now resolved + from `INFOPLIST_FILE` in the Xcode project and any `.xcconfig` files, with a scan of `ios/` as a + fallback, and the usage description keys found across them are merged. +* Adds the `PERMISSION_HANDLER_INFO_PLIST` environment variable, which points the manifest at one or + more `Info.plist` files and replaces automatic discovery. This is required for builds started from + Xcode.app, which run with `/` as their working directory and cannot be detected automatically. +* Adds the `PERMISSION_HANDLER_VERBOSE` environment variable, which logs the app root, the + `Info.plist` files used, and the resolved `PERMISSION_*` macros. +* Emits a warning when no `Info.plist` can be located, instead of silently disabling every + permission. Note that Xcode discards Swift package manifest output, so this warning is only + visible through the `swift package` command line. + ## 9.5.0 * Adds support for the new Android 17 permission `ACCESS_LOCAL_NETWORK`. diff --git a/permission_handler_apple/README.md b/permission_handler_apple/README.md index 22d8adfd2..93ec82b1f 100644 --- a/permission_handler_apple/README.md +++ b/permission_handler_apple/README.md @@ -10,6 +10,67 @@ Since version 9.1.0 of the [permission_handler](https://pub.dev/packages/permiss More detailed instructions on using the API can be found in the [README.md](../permission_handler/README.md) of the [permission_handler](https://pub.dev/packages/permission_handler) package. +## Swift Package Manager + +Only the permissions your app actually uses are compiled into the binary. Referencing an iOS +permission API you have no usage description for is grounds for App Store rejection +(`ITMS-90683`), so each permission is guarded by a `PERMISSION_*` macro. + +Under CocoaPods you set those macros yourself, in the `GCC_PREPROCESSOR_DEFINITIONS` block of your +`Podfile`. Under Swift Package Manager the package manifest derives them instead: it locates your +app's `Info.plist` files and enables a permission when the matching `NS*UsageDescription` key is +present. `INFOPLIST_FILE` is read from your Xcode project and `.xcconfig` files, so +build-configuration and flavor specific plists (`Info-Debug.plist`, `Info-dev.plist`, +`Runner/Info-$(CONFIGURATION).plist`, …) are all picked up. + +**Keys are merged across every configuration.** A package manifest is evaluated once and cannot +vary its settings per build configuration, so a permission declared only in `Info-dev.plist` is +compiled into your release binary too. Use the per-permission variables below where that matters. + +**Changes are cached.** The manifest is not re-evaluated when an `Info.plist` or an environment +variable changes. Clear DerivedData once afterwards: + +```bash +rm -rf ~/Library/Developer/Xcode/DerivedData +``` + +### Environment variables + +Xcode.app does not inherit your shell's environment, so set these with `launchctl setenv` rather +than exporting them, then restart Xcode. + +| Variable | Effect | +| --- | --- | +| `PERMISSION_` | Forces a single permission on (`1`) or off (`0`), overriding everything else. For example `launchctl setenv PERMISSION_CAMERA 0`. | +| `PERMISSION_HANDLER_INFO_PLIST` | A `:`-separated list of `Info.plist` paths. When set, replaces automatic discovery entirely. | +| `PERMISSION_HANDLER_VERBOSE` | Set to `1` to log the detected app root, the `Info.plist` files used, and the resolved macros. | + +### Builds started from Xcode.app + +Automatic discovery finds your app through the build's working directory, which points at the +Flutter project for `flutter run`, `flutter build ios` and a direct `xcodebuild` invocation. Builds +started from Xcode.app run with `/` as their working directory, and the manifest is given none of +Xcode's build settings, so there is nothing to find the app by. Point it at the plist explicitly: + +```bash +launchctl setenv PERMISSION_HANDLER_INFO_PLIST /absolute/path/to/ios/Runner/Info.plist +rm -rf ~/Library/Developer/Xcode/DerivedData +``` + +If no `Info.plist` is found, every permission is compiled out and permission checks report +`denied`. The manifest warns about this, but Xcode discards Swift package manifest output, so the +warning only reaches you through the command line: + +```bash +cd your_app +PERMISSION_HANDLER_VERBOSE=1 swift package --manifest-cache none \ + --package-path ios/Flutter/ephemeral/Packages/.packages/permission_handler_apple \ + dump-package > /dev/null +``` + +That prints the app root, the `Info.plist` files used and the resolved macros, and is the quickest +way to check what your app will actually be built with. + ## Issues Please file any issues, bugs, or feature requests as an issue on our [GitHub](https://github.com/Baseflow/flutter-permission-handler/issues) page. Commercial support is available, you can contact us at . diff --git a/permission_handler_apple/example/ios/Flutter/permission_handler.selected b/permission_handler_apple/example/ios/Flutter/permission_handler.selected new file mode 100644 index 000000000..d287cd3ed --- /dev/null +++ b/permission_handler_apple/example/ios/Flutter/permission_handler.selected @@ -0,0 +1 @@ +debug diff --git a/permission_handler_apple/example/ios/Runner.xcodeproj/project.pbxproj b/permission_handler_apple/example/ios/Runner.xcodeproj/project.pbxproj index ee00f0144..51d5f1ace 100644 --- a/permission_handler_apple/example/ios/Runner.xcodeproj/project.pbxproj +++ b/permission_handler_apple/example/ios/Runner.xcodeproj/project.pbxproj @@ -500,7 +500,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_FILE = "Runner/Info-Debug.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/permission_handler_apple/example/ios/Runner/Info-Debug.plist b/permission_handler_apple/example/ios/Runner/Info-Debug.plist new file mode 100644 index 000000000..d6b681e5b --- /dev/null +++ b/permission_handler_apple/example/ios/Runner/Info-Debug.plist @@ -0,0 +1,118 @@ + + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + NSAppleMusicUsageDescription + Music! + NSBluetoothAlwaysUsageDescription + bluetooth + NSBluetoothPeripheralUsageDescription + bluetooth + NSCalendarsFullAccessUsageDescription + Calendar full access + NSCalendarsUsageDescription + Calendars + NSCalendarsWriteOnlyAccessUsageDescription + Calendar write only + NSCameraUsageDescription + camera + NSLocationAlwaysAndWhenInUseUsageDescription + Always and when in use! + NSLocationAlwaysUsageDescription + Can I have location always? + NSLocationUsageDescription + Older devices need location. + NSLocationWhenInUseUsageDescription + Need location when in use + NSMicrophoneUsageDescription + microphone + NSMotionUsageDescription + motion + NSPhotoLibraryAddUsageDescription + photos add only + NSPhotoLibraryUsageDescription + photos + NSRemindersUsageDescription + reminders +NSSpeechRecognitionUsageDescription + speech + NSUserTrackingUsageDescription + appTrackingTransparency + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + FlutterSceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + kTCCServiceMediaLibrary + media + + diff --git a/permission_handler_apple/ios/permission_handler_apple/Package.swift b/permission_handler_apple/ios/permission_handler_apple/Package.swift index 88170e032..c4720c6f3 100644 --- a/permission_handler_apple/ios/permission_handler_apple/Package.swift +++ b/permission_handler_apple/ios/permission_handler_apple/Package.swift @@ -14,67 +14,377 @@ import Foundation // (PERMISSION_NOTIFICATIONS, PERMISSION_CRITICAL_ALERTS), // disabled for all others. // +// Additional environment variables: +// PERMISSION_HANDLER_INFO_PLIST ':'-separated Info.plist paths. When set, +// replaces automatic discovery entirely. This +// is the only mechanism that works for builds +// started from Xcode.app (see findAppRoot()). +// PERMISSION_HANDLER_VERBOSE Set to 1 to log what was discovered and +// which permissions ended up enabled. +// // After changing Info.plist or env vars, clear DerivedData once so Xcode // re-evaluates this manifest: // rm -rf ~/Library/Developer/Xcode/DerivedData // --------------------------------------------------------------------------- let env = ProcessInfo.processInfo.environment +let fileManager = FileManager.default +let verbose = (env["PERMISSION_HANDLER_VERBOSE"] ?? "0") != "0" + +/// Write to stderr. +/// +/// The `swift package` CLI reports this; Xcode discards manifest output +/// entirely, during package resolution and during a build alike. Anything +/// written here is therefore invisible to exactly the users who most need it, +/// which is why a failed lookup also has to degrade safely: every permission +/// reports `denied` rather than crashing (see the disabled strategy +/// implementations in Sources/.../strategies). +/// +/// Never call `fatalError` here: it aborts evaluation of the whole package +/// graph with a message the user cannot act on. +func diagnostic(_ level: String, _ message: String) { + FileHandle.standardError.write("\(level): [permission_handler_apple] \(message)\n".data(using: .utf8)!) +} func loadInfoPlist(at url: URL) -> [String: Any]? { NSDictionary(contentsOf: url) as? [String: Any] } -/// Find the host app's Runner/Info.plist. +// MARK: - Locating the host app + +/// Directories that never contain the host app's Info.plist, and that are +/// expensive to walk. +let skippedDirectoryNames: Set = [ + "Pods", "build", "DerivedData", "ephemeral", ".dart_tool", ".symlinks", ".git", +] + +/// Depth-first walk of `root`, collecting files whose name satisfies `isMatch`. +/// Package directories such as `Runner.xcodeproj` are descended into, because +/// `project.pbxproj` lives inside one. +func enumerateFiles(under root: URL, matching isMatch: (String) -> Bool) -> [URL] { + guard let enumerator = fileManager.enumerator( + at: root, + includingPropertiesForKeys: [.isDirectoryKey] + ) else { return [] } + + var matches: [URL] = [] + for case let url as URL in enumerator { + let name = url.lastPathComponent + let isDirectory = (try? url.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory ?? false + + if isDirectory { + if skippedDirectoryNames.contains(name) || name.hasSuffix(".xcworkspace") { + enumerator.skipDescendants() + } + continue + } + + if isMatch(name) { matches.append(url) } + } + return matches +} + +/// True when `dir` is a Flutter *application* root. /// -/// Flutter can resolve this package through a local plugin path, a generated -/// SPM package, or an Xcode package cache. Look for a Flutter app root by -/// walking up from the package and current working directory, using pubspec.yaml -/// next to ios/Runner/Info.plist as the app-root anchor. -func findInfoPlist() -> [String: Any] { - let fileManager = FileManager.default - - let packageDir = URL(fileURLWithPath: #file).deletingLastPathComponent() - let currentDir = URL(fileURLWithPath: fileManager.currentDirectoryPath) - +/// The `ios/*.xcodeproj` requirement is what separates an app from a plugin +/// package: plugins also have a pubspec.yaml next to an `ios/` directory, but +/// never an Xcode project inside it. Without this check the walk-up below +/// stops on this very package when it is consumed as a path dependency. +func isAppRoot(_ dir: URL) -> Bool { + guard fileManager.fileExists(atPath: dir.appendingPathComponent("pubspec.yaml").path) else { + return false + } + let iosDir = dir.appendingPathComponent("ios") + guard let entries = try? fileManager.contentsOfDirectory(atPath: iosDir.path) else { + return false + } + return entries.contains { $0.hasSuffix(".xcodeproj") } +} + +func walkUpToAppRoot(from start: URL, maxDepth: Int = 12) -> URL? { + var dir = start.standardizedFileURL + for _ in 0.. URL? { + var starts = [URL(fileURLWithPath: fileManager.currentDirectoryPath)] + if let pwd = env["PWD"], !pwd.isEmpty { + starts.append(URL(fileURLWithPath: pwd)) + } + starts.append(URL(fileURLWithPath: #file).deletingLastPathComponent()) + var visited = Set() - - for root in [packageDir, currentDir] { - var dir = root - - for _ in 0..<10 { - let key = dir.resolvingSymlinksInPath().path - guard visited.insert(key).inserted else { - break - } - - let pubspecURL = dir.appendingPathComponent("pubspec.yaml") - let plistURL = dir.appendingPathComponent("ios/Runner/Info.plist") + for start in starts { + guard visited.insert(start.resolvingSymlinksInPath().path).inserted else { continue } + if let appRoot = walkUpToAppRoot(from: start) { return appRoot } + } + return nil +} - if fileManager.fileExists(atPath: pubspecURL.path), - let plist = loadInfoPlist(at: plistURL) { - return plist - } +// MARK: - Locating Info.plist files - let parent = dir.deletingLastPathComponent() - if parent.path == dir.path { - break - } - dir = parent +/// The lookbehind keeps `GENERATE_INFOPLIST_FILE = YES;` from matching. Xcode +/// writes that setting into every target it creates from a template — app +/// extensions especially — and a match there yields a candidate named `YES`, +/// which is enough to suppress the fallback scan below. +let infoPlistSettingRegex = try? NSRegularExpression( + pattern: #"(? = [ + "AppFrameworkInfo.plist", "GoogleService-Info.plist", +] + +func expandBuildSettings(_ value: String, projectDir: URL) -> String { + var expanded = value + for name in ["SRCROOT", "SOURCE_ROOT", "PROJECT_DIR"] { + expanded = expanded + .replacingOccurrences(of: "$(\(name))", with: projectDir.path) + .replacingOccurrences(of: "${\(name)}", with: projectDir.path) + } + return expanded +} + +/// Turn one `INFOPLIST_FILE` value into concrete plist URLs. +/// +/// A value like `Runner/Info-$(CONFIGURATION).plist` cannot be resolved here — +/// the manifest has no idea which configuration is building, and is evaluated +/// once for all of them. Rather than drop it, expand it to every plist in that +/// directory sharing the literal prefix, and let the caller merge them. +func resolveInfoPlistSetting(_ value: String, projectDir: URL) -> [URL] { + let expanded = expandBuildSettings(value, projectDir: projectDir) + let url = expanded.hasPrefix("/") + ? URL(fileURLWithPath: expanded) + : projectDir.appendingPathComponent(expanded) + + guard expanded.contains("$(") || expanded.contains("${") else { return [url] } + + // Only a variable in the file name can be globbed; one in a parent + // directory would need the directory listing of an unknown path. + let directory = url.deletingLastPathComponent() + if directory.path.contains("$(") || directory.path.contains("${") { return [] } + + let name = url.lastPathComponent + guard let variableStart = name.range(of: "$(") ?? name.range(of: "${") else { return [] } + let prefix = String(name[name.startIndex.. [URL] { + guard let regex = infoPlistSettingRegex else { return [] } + let iosDir = appRoot.appendingPathComponent("ios") + + let settingsFiles = enumerateFiles(under: iosDir) { name in + name == "project.pbxproj" || name.hasSuffix(".xcconfig") + } + + var results: [URL] = [] + for file in settingsFiles { + guard let contents = try? String(contentsOf: file, encoding: .utf8) else { continue } + + // SRCROOT is the directory holding the .xcodeproj — `ios/` in a stock + // Flutter app. For a bare xcconfig, `ios/` is the best assumption. + let projectDir = file.pathComponents.contains { $0.hasSuffix(".xcodeproj") } + ? file.deletingLastPathComponent().deletingLastPathComponent() + : iosDir + + let range = NSRange(contents.startIndex..., in: contents) + for match in regex.matches(in: contents, range: range) { + guard match.numberOfRanges > 1, + let valueRange = Range(match.range(at: 1), in: contents) else { continue } + let value = contents[valueRange] + .trimmingCharacters(in: CharacterSet(charactersIn: " \t\"'")) + guard !value.isEmpty else { continue } + results.append(contentsOf: resolveInfoPlistSetting(value, projectDir: projectDir)) } } + return results +} + +/// Discard candidates that are not files on disk. +/// +/// `INFOPLIST_FILE` is read from build settings that may name a plist this +/// manifest cannot resolve — a path built from a build variable it does not +/// expand, or a target whose plist Xcode generates at build time. Such a +/// candidate must not count towards "settings produced something", or it +/// suppresses the fallback scan and every permission is compiled out. +func existingFiles(_ urls: [URL]) -> [URL] { + urls.filter { fileManager.fileExists(atPath: $0.path) } +} + +/// Last resort: any plausibly-named plist under `ios/`. Covers projects whose +/// `INFOPLIST_FILE` lives somewhere this manifest does not parse. +func infoPlistsFromScan(appRoot: URL) -> [URL] { + enumerateFiles(under: appRoot.appendingPathComponent("ios")) { name in + name.hasSuffix(".plist") + && name.contains("Info") + && !name.contains("Test") + && !ignoredPlistNames.contains(name) + } +} + +func infoPlistsFromEnvironment() -> [URL]? { + guard let raw = env["PERMISSION_HANDLER_INFO_PLIST"], !raw.isEmpty else { return nil } + return raw + .split(separator: ":") + .map { URL(fileURLWithPath: String($0).trimmingCharacters(in: .whitespaces)) } +} + +/// Collect the usage description keys of every Info.plist belonging to the +/// host app. +/// +/// The keys are *merged* across build configurations and flavors. The manifest +/// is evaluated once per package resolution and cannot know which configuration +/// is building, so per-flavor macros are not expressible here. Merging errs +/// towards enabling a permission: an app whose `Info-dev.plist` declares camera +/// access compiles the camera code into its release binary too. Use the +/// per-permission `PERMISSION_*` environment variables where that matters. +func findInfoPlist() -> [String: Any] { + var candidates: [URL] + + if let explicit = infoPlistsFromEnvironment() { + candidates = explicit + } else if let appRoot = findAppRoot() { + if verbose { diagnostic("note", "app root: \(appRoot.path)") } + let fromSettings = existingFiles(infoPlistsFromBuildSettings(appRoot: appRoot)) + candidates = fromSettings.isEmpty ? infoPlistsFromScan(appRoot: appRoot) : fromSettings + } else { + diagnostic("warning", """ + Could not locate the host app, so every iOS permission has been compiled out and \ + permission checks will report `denied`. Automatic detection needs the build to be \ + started from the Flutter project directory, which is not the case for builds run \ + directly from Xcode.app. Point this manifest at the app's Info.plist with \ + `launchctl setenv PERMISSION_HANDLER_INFO_PLIST /path/to/ios/Runner/Info.plist`, or \ + enable permissions individually with `launchctl setenv PERMISSION_CAMERA 1`, then \ + run `rm -rf ~/Library/Developer/Xcode/DerivedData` so this manifest is re-evaluated. + """) + return [:] + } - return [:] + var seen = Set() + candidates = candidates.filter { seen.insert($0.standardizedFileURL.path).inserted } + + var merged: [String: Any] = [:] + var loaded: [URL] = [] + var keysPerPlist: [Set] = [] + + for url in candidates { + guard let plist = loadInfoPlist(at: url) else { continue } + loaded.append(url) + keysPerPlist.append(Set(plist.keys.filter { $0.hasSuffix("UsageDescription") })) + for (key, value) in plist where merged[key] == nil { merged[key] = value } + } + + if loaded.isEmpty { + diagnostic("warning", """ + No readable Info.plist was found for the host app, so every iOS permission has been \ + compiled out and permission checks will report `denied`. Set \ + PERMISSION_HANDLER_INFO_PLIST to the path of your Info.plist, then run \ + `rm -rf ~/Library/Developer/Xcode/DerivedData`. + """) + return [:] + } + + if verbose { + diagnostic("note", "Info.plist files used:\n " + loaded.map(\.path).joined(separator: "\n ")) + } + + // Merging across configurations enables the union of their permissions. Say + // so when they actually disagree, because the extra permissions end up in + // the release binary and can trigger App Store rejection (ITMS-90683). + if let first = keysPerPlist.first, keysPerPlist.contains(where: { $0 != first }) { + let divergent = keysPerPlist.reduce(into: Set()) { $0.formUnion($1) } + .subtracting(keysPerPlist.reduce(into: keysPerPlist[0]) { $0.formIntersection($1) }) + diagnostic("warning", """ + The discovered Info.plist files declare different usage descriptions \ + (\(divergent.sorted().joined(separator: ", "))). Every permission found in any of \ + them is enabled for all build configurations, because a Swift package manifest \ + cannot vary its settings per configuration. Set PERMISSION_HANDLER_INFO_PLIST or the \ + per-permission PERMISSION_* variables to control this explicitly. + """) + } + + return merged } let infoPlist = findInfoPlist() +/// Every macro this manifest resolved, for PERMISSION_HANDLER_VERBOSE. +var resolvedMacros: [String: String] = [:] + /// Return "1" if the env var is set (non-zero), "0" if explicitly set to "0", /// else "1" if any Info.plist key is present, else `defaultValue`. +/// +/// An empty value counts as unset: `launchctl setenv PERMISSION_CAMERA ""` is +/// how a variable gets cleared, and reading that as "enabled" would be the +/// opposite of what was asked. func enabled(_ envKey: String, plistKeys: String..., defaultValue: String = "0") -> String { - if let val = env[envKey] { return val == "0" ? "0" : "1" } - for key in plistKeys where infoPlist[key] != nil { return "1" } - return defaultValue + let value: String + if let val = env[envKey]?.trimmingCharacters(in: .whitespaces), !val.isEmpty { + value = val == "0" ? "0" : "1" + } else if plistKeys.contains(where: { infoPlist[$0] != nil }) { + value = "1" + } else { + value = defaultValue + } + resolvedMacros[envKey] = value + return value } let permissionDefines: [CSetting] = [ @@ -161,6 +471,13 @@ let permissionDefines: [CSetting] = [ plistKeys: "NSSiriUsageDescription")), ] +if verbose { + diagnostic("note", "resolved permission macros:\n " + + resolvedMacros.sorted { $0.key < $1.key } + .map { "\($0.key)=\($0.value)" } + .joined(separator: "\n ")) +} + let package = Package( name: "permission_handler_apple", platforms: [ diff --git a/permission_handler_apple/pubspec.yaml b/permission_handler_apple/pubspec.yaml index 60321eb4d..dd6e6758d 100644 --- a/permission_handler_apple/pubspec.yaml +++ b/permission_handler_apple/pubspec.yaml @@ -2,7 +2,7 @@ name: permission_handler_apple description: Permission plugin for Flutter. This plugin provides the iOS API to request and check permissions. repository: https://github.com/baseflow/flutter-permission-handler issue_tracker: https://github.com/Baseflow/flutter-permission-handler/issues -version: 9.5.0 +version: 9.5.1 environment: sdk: ^3.6.0 From c5634444f2208b0ac3a1eec9b584c0a66f62b500 Mon Sep 17 00:00:00 2001 From: Pachebel <89677437+Pachebel@users.noreply.github.com> Date: Mon, 10 Aug 2026 15:03:45 -0300 Subject: [PATCH 22/26] Add opt-in per-flavor permissions for Swift Package Manager builds (2/2) (#1554) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add opt-in per-flavor permissions for Swift Package Manager builds Permissions were merged across every Info.plist the manifest could find, so an app whose dev flavor declares camera access compiled the camera code into its prod binary too — the ITMS-90683 rejection the macros exist to avoid. An app can now declare a permission_handler.json next to its pubspec.yaml, mapping each flavor to the single Info.plist that defines it. Only the selected flavor's plist is read and nothing is merged, so a flavor cannot inherit another flavor's permissions. Apps without the file keep the previous behaviour. This is a Swift Package Manager feature. CocoaPods builds take the PERMISSION_* macros from the Podfile, where they can already be set per configuration, so permission_handler.json does not apply to them and the verification build phase exits early when the SPM package is not part of the build. Without that the example, which builds under CocoaPods, would require a flavor selection that changes nothing. Selecting a flavor is a separate step rather than something the manifest derives, because of three properties of Swift package manifests that were measured rather than assumed: - Xcode passes none of its build settings to manifest evaluation, so CONFIGURATION is not available and the manifest cannot know which flavor is building. - Xcode does not re-evaluate a cached manifest when an environment variable or the selection changes. It keeps serving the previous answer until SourcePackages, XCBuildData and the shared SwiftPM manifest cache are gone. - Scheme pre-actions run after package resolution, so they cannot prepare the selection for the build that follows. `dart run permission_handler_apple:select ` therefore records the choice and clears exactly those caches. Since a cached manifest is never executed, nothing inside it can detect that its own result went stale. tool/verify_flavor_selection.sh runs as a build phase on the app target, where CONFIGURATION is available, and fails the build when the selected flavor does not match the configuration being built. It runs first in the phase list, so a mismatch costs a second rather than a full compile, and it resolves the plugin through either the SPM package or .symlinks so the same snippet works whichever way a project consumes it. A flavor whose infoPlist does not exist is an error rather than a fallback to discovery: falling back would hand that flavor the union of every other flavor's permissions, which is the leak this exists to stop. The example declares two flavors over the two Info.plist files that differ by a single key. CI asserts that neither flavor sees the other's permission, that a missing selection compiles nothing in under "strict": true, that a mismatched selection fails the build, and — with the config switched off — that the merging path this replaces still resolves both plists. The repository .gitignore excluded every bin/ directory, which would have dropped the new executable from the package; pub requires it to live there. * Use YAML for the per-flavor permission configuration Flutter developers configure everything else in YAML, so the per-flavor config moves from permission_handler.json to permission_handler.yaml, as requested in review. None of the build-time consumers can parse YAML natively: a Swift package manifest cannot import libraries for its own evaluation and Foundation has no YAML support, and the verification build phase runs on python3 whose standard library has none either. Rather than hand-rolling a YAML parser in each of them, the `select` command becomes the single YAML reader — it parses the config with package:yaml, the same parser pub itself uses, and translates it into a generated ios/Flutter/permission_handler.resolved.json that the manifest and the build phase keep reading with the native JSON parsers they already had. The generated file is an internal artifact: gitignored, never edited, regenerated on every `select`. Two guards keep it from going stale. The manifest and the build phase both refuse to build when the YAML is newer than its translation or when the translation is missing, compiling no permissions in and saying to re-run `select` — falling back to discovery instead would hand the build the union of every flavor's permissions, the exact leak the config exists to prevent. `select` was already mandatory before any flavored build because Xcode never re-evaluates a cached manifest, so the translation step adds no new workflow. CI gains a staleness assertion: touching permission_handler.yaml without re-running `select` must compile everything out and name the guard in the diagnostics. * Reject ambiguous flavor configurations instead of guessing Two flavors could list the same build configuration. Nothing rejected that, and the verification build phase resolved it by taking whichever came first, so selecting the flavor that configuration actually belongs to failed the build with a message naming the other one — talking the user into shipping the wrong flavor's permissions, which is the leak the config exists to prevent. A `configurations` written as a scalar rather than a list became an empty list, silently removing every configuration of that flavor from the verification phase's view. The build then reported that it could not verify the permissions and continued. `select` is the only reader of the YAML, so it is the only place where either can be reported at all: everything downstream sees the generated JSON, where a typo and a deliberate value are indistinguishable. It now refuses a configuration claimed by more than one flavor, a `configurations` that is not a list, a non-string flavor name, and a `strict` that is not a boolean. Three guards that failed open now fail closed. The manifest treated unreadable modification times as "not stale" and built anyway; the verification phase read a failing `find` as "not stale" for the same reason; and PERMISSION_HANDLER_CONFIG was honoured only for locating the YAML, while its generated translation and Info.plists were still resolved against the automatically detected app — so the file that gated the staleness check and the file that supplied the permissions could come from two different apps. --- .../workflows/permission_handler_apple.yaml | 147 +++++--- .gitignore | 2 + permission_handler_apple/CHANGELOG.md | 27 ++ permission_handler_apple/README.md | 89 ++++- permission_handler_apple/bin/select.dart | 345 ++++++++++++++++++ .../example/ios/.gitignore | 2 + .../ios/Flutter/permission_handler.selected | 1 - .../ios/Runner.xcodeproj/project.pbxproj | 16 + .../example/permission_handler.yaml | 17 + .../permission_handler_apple/Package.swift | 278 +++++++++++++- permission_handler_apple/pubspec.yaml | 5 +- .../tool/verify_flavor_selection.sh | 102 ++++++ 12 files changed, 968 insertions(+), 63 deletions(-) create mode 100644 permission_handler_apple/bin/select.dart delete mode 100644 permission_handler_apple/example/ios/Flutter/permission_handler.selected create mode 100644 permission_handler_apple/example/permission_handler.yaml create mode 100755 permission_handler_apple/tool/verify_flavor_selection.sh diff --git a/.github/workflows/permission_handler_apple.yaml b/.github/workflows/permission_handler_apple.yaml index e4ca000b4..5be3cb6e0 100644 --- a/.github/workflows/permission_handler_apple.yaml +++ b/.github/workflows/permission_handler_apple.yaml @@ -57,62 +57,108 @@ jobs: run: flutter analyze working-directory: ${{env.source-directory}} - # Build iOS version of the example App + # Build iOS version of the example App. + # + # The example resolves this plugin as a Swift package (the Podfile only + # covers its remaining CocoaPods dependencies), so the "Verify + # permission_handler flavor" build phase is active and `strict: true` + # demands a selection before a Release build can run. `select` also + # generates ios/Flutter/permission_handler.resolved.json from + # permission_handler.yaml — neither it nor the selection file is + # committed, so every CI run has to make its own. - name: Run iOS build - run: flutter build ios --no-codesign --release working-directory: ${{env.example-directory}} + run: | + set -euo pipefail + dart run permission_handler_apple:select release + flutter build ios --no-codesign --release - # Guard the Swift Package Manager permission auto-detection. + # Guard the Swift Package Manager permission resolution. # # Package.swift turns the usage description keys of the host app's - # Info.plist into PERMISSION_* macros. When that discovery breaks every - # macro silently falls back to 0 and all permissions are compiled out, so - # assert on the resolved manifest rather than on the build succeeding. + # Info.plist into PERMISSION_* macros. When that breaks, every macro + # silently falls back to 0 and all permissions are compiled out, so assert + # on the resolved manifest rather than on the build succeeding. # - # The example's Debug configuration deliberately uses a separate - # `Info-Debug.plist` — the build-configuration-specific layout reported in - # issue #1548 — which is identical to `Info.plist` except that it declares - # no NSContactsUsageDescription. PERMISSION_CONTACTS can therefore only - # resolve to 1 if *both* plists were discovered and merged, which is what - # makes these assertions test the discovery rather than the example's - # default plist. - - name: Verify SPM permission detection + # The example declares two flavors over two Info.plist files that differ by + # exactly one key: `debug` (Info-Debug.plist, the build-configuration + # specific layout reported in issue #1548) has no contacts entry, + # `release` (Info.plist) does. That single difference is what proves both + # that a flavor cannot inherit another flavor's permissions and, on the + # legacy path, that both plists were discovered and merged. + - name: Verify SPM permission resolution working-directory: ${{env.example-directory}} run: | set -euo pipefail flutter config --enable-swift-package-manager flutter build ios --config-only --debug --no-codesign - rm -rf ~/Library/Caches/org.swift.swiftpm/manifests - PERMISSION_HANDLER_VERBOSE=1 swift package --manifest-cache none \ - --package-path ios/Flutter/ephemeral/Packages/.packages/permission_handler_apple \ - dump-package > "${RUNNER_TEMP}/manifest.json" 2> "${RUNNER_TEMP}/manifest.log" - cat "${RUNNER_TEMP}/manifest.log" + # Resolve the manifest under a given environment, capturing the macros + # (stdout) and the PERMISSION_HANDLER_VERBOSE diagnostics (stderr). + resolve() { # ... + local name="$1"; shift + rm -rf ~/Library/Caches/org.swift.swiftpm/manifests + env PERMISSION_HANDLER_VERBOSE=1 "$@" swift package --manifest-cache none \ + --package-path ios/Flutter/ephemeral/Packages/.packages/permission_handler_apple \ + dump-package > "${RUNNER_TEMP}/${name}.json" 2> "${RUNNER_TEMP}/${name}.log" + echo "--- ${name} ---" + cat "${RUNNER_TEMP}/${name}.log" + } - fail() { # - echo "::error::$1" - grep -o 'PERMISSION_[A-Z_]*=[01]' "${RUNNER_TEMP}/manifest.json" | sort -u - exit 1 + expect() { # ... + local name="$1"; shift + for macro in "$@"; do + grep -qF "${macro}" "${RUNNER_TEMP}/${name}.json" || { + echo "::error::[${name}] expected ${macro} in the resolved manifest" + grep -o 'PERMISSION_[A-Z_]*=[01]' "${RUNNER_TEMP}/${name}.json" | sort -u + exit 1 + } + done + } + + expect_log() { # + grep -qF "$2" "${RUNNER_TEMP}/$1.log" || { echo "::error::[$1] $3"; exit 1; } } - # Both the default and the Debug-only plist must be picked up. - grep -qF 'Runner/Info.plist' "${RUNNER_TEMP}/manifest.log" \ - || fail 'ios/Runner/Info.plist was not discovered' - grep -qF 'Runner/Info-Debug.plist' "${RUNNER_TEMP}/manifest.log" \ - || fail 'ios/Runner/Info-Debug.plist was not discovered — INFOPLIST_FILE parsing is broken' - - # Reached only when two plists were loaded and their keys differ, so - # this is the assertion that proves the merge across configurations. - grep -qF 'NSContactsUsageDescription' "${RUNNER_TEMP}/manifest.log" \ - || fail 'the divergence warning did not name NSContactsUsageDescription — the plists were not merged' - - # Present in Info.plist only; the rest are in both. - for macro in PERMISSION_CONTACTS \ - PERMISSION_CAMERA PERMISSION_MICROPHONE \ - PERMISSION_PHOTOS PERMISSION_LOCATION; do - grep -qF "${macro}=1" "${RUNNER_TEMP}/manifest.json" \ - || fail "${macro} resolved to 0 — Info.plist discovery is broken" - done + # Without a permission_handler.yaml the pre-flavor behaviour has to be + # unchanged: discover every Info.plist and merge them. PERMISSION_CONTACTS + # can only be 1 if both plists were found, which is what makes this + # assert on the discovery rather than on the example's default plist. + resolve legacy PERMISSION_HANDLER_CONFIG=/nonexistent/permission_handler.yaml + expect_log legacy 'Runner/Info.plist' \ + 'ios/Runner/Info.plist was not discovered' + expect_log legacy 'Runner/Info-Debug.plist' \ + 'ios/Runner/Info-Debug.plist was not discovered — INFOPLIST_FILE parsing is broken' + expect_log legacy 'NSContactsUsageDescription' \ + 'the divergence warning did not fire — the plists were not merged' + expect legacy PERMISSION_CONTACTS=1 PERMISSION_CAMERA=1 PERMISSION_MICROPHONE=1 \ + PERMISSION_PHOTOS=1 PERMISSION_LOCATION=1 + + # The debug flavor must not see the release flavor's contacts entry. + resolve debug PERMISSION_HANDLER_FLAVOR=debug + expect debug PERMISSION_CONTACTS=0 PERMISSION_CAMERA=1 PERMISSION_MICROPHONE=1 + + # ...and the release flavor must see it. + resolve release PERMISSION_HANDLER_FLAVOR=release + expect release PERMISSION_CONTACTS=1 PERMISSION_CAMERA=1 PERMISSION_MICROPHONE=1 + + # With `strict: true` an unresolved flavor compiles nothing in, + # rather than falling back to the union of every flavor. The earlier + # build step recorded a selection, so drop it first — otherwise the + # manifest resolves that flavor and this asserts nothing. The + # generated resolved.json stays; only the selection is missing. + rm -f ios/Flutter/permission_handler.selected + resolve strict + expect strict PERMISSION_CONTACTS=0 PERMISSION_CAMERA=0 PERMISSION_MICROPHONE=0 + + # Editing permission_handler.yaml without re-running `select` must not + # build with the stale translation: everything compiles out and the + # manifest says to re-run select. + touch permission_handler.yaml + resolve stale PERMISSION_HANDLER_FLAVOR=release + expect stale PERMISSION_CONTACTS=0 PERMISSION_CAMERA=0 PERMISSION_MICROPHONE=0 + expect_log stale 'modified after its generated translation' \ + 'the staleness guard did not fire' # An app target whose INFOPLIST_FILE is built from a variable this manifest # does not expand, next to an extension carrying Xcode's stock @@ -142,3 +188,22 @@ jobs: echo "::error::the scan fallback did not run — an unresolvable INFOPLIST_FILE suppressed it" exit 1 } + + # The build phase that catches a stale selection is the only check that + # runs on every build, so make sure it actually fails. + - name: Verify flavor mismatch fails the build + working-directory: ${{env.example-directory}} + run: | + set -euo pipefail + dart run permission_handler_apple:select release + if xcodebuild -workspace ios/Runner.xcworkspace -scheme Runner \ + -configuration Debug -sdk iphonesimulator \ + -derivedDataPath "${RUNNER_TEMP}/dd" build > "${RUNNER_TEMP}/mismatch.log" 2>&1; then + echo "::error::a Debug build with the release flavor selected should have failed" + exit 1 + fi + grep -q 'needs the "debug" permission flavor' "${RUNNER_TEMP}/mismatch.log" || { + echo "::error::build failed, but not with the flavor mismatch error" + tail -40 "${RUNNER_TEMP}/mismatch.log" + exit 1 + } diff --git a/.gitignore b/.gitignore index 161ad2bb3..534dbdf8b 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,8 @@ .project .svn/ bin/ +# ...except packages that ship an executable, which pub requires to live in bin/. +!permission_handler_apple/bin/ migrate_working_dir/ # IntelliJ related diff --git a/permission_handler_apple/CHANGELOG.md b/permission_handler_apple/CHANGELOG.md index 8d20d6a57..578ef76c6 100644 --- a/permission_handler_apple/CHANGELOG.md +++ b/permission_handler_apple/CHANGELOG.md @@ -1,3 +1,30 @@ +## 9.6.0 + +* Adds opt-in per-flavor permissions for Swift Package Manager builds. An app can declare a + `permission_handler.yaml` next to its `pubspec.yaml` mapping each flavor to the `Info.plist` that + defines it, and only the selected flavor's permissions are compiled in — a permission declared by + `dev` can no longer reach a `prod` binary. Without this file the previous behaviour is unchanged. + This is a Swift Package Manager feature: CocoaPods builds set the `PERMISSION_*` macros from the + `Podfile` and are unaffected, including by the build phase below. +* Adds `dart run permission_handler_apple:select `, which records the active flavor and + clears the caches that would otherwise keep serving the previously resolved permissions. Xcode + does not re-evaluate a package manifest when an environment variable or the selection changes, so + this step is required when switching flavors. `select` is also the only YAML reader: a Swift + package manifest cannot parse YAML, so the command translates the config into a generated + `ios/Flutter/permission_handler.resolved.json` (gitignore it) that the manifest and the build + phase read with their native JSON parsers. A translation older than the YAML fails the build + instead of shipping stale permissions. +* Adds `tool/verify_flavor_selection.sh`, a build phase for the app target that fails the build when + the selected flavor does not match the configuration being built. A package manifest is evaluated + once and cannot detect that its own result went stale, so this is what catches a forgotten + `select`. It is a no-op without a `permission_handler.yaml` and on CocoaPods builds. +* Adds the `PERMISSION_HANDLER_FLAVOR` and `PERMISSION_HANDLER_CONFIG` environment variables to set + the active flavor and the configuration file location explicitly. +* `select` validates the configuration up front and refuses anything ambiguous: a build + configuration claimed by more than one flavor, a `configurations` that is not a list, a + non-string flavor name, or a `strict` that is not a boolean. Being the only reader of the YAML, + it is the only place where these can be reported at all. + ## 9.5.1 * Fixes the Swift Package Manager permission auto-detection, which failed to find the host app's diff --git a/permission_handler_apple/README.md b/permission_handler_apple/README.md index 93ec82b1f..c5d1f6a23 100644 --- a/permission_handler_apple/README.md +++ b/permission_handler_apple/README.md @@ -25,7 +25,7 @@ build-configuration and flavor specific plists (`Info-Debug.plist`, `Info-dev.pl **Keys are merged across every configuration.** A package manifest is evaluated once and cannot vary its settings per build configuration, so a permission declared only in `Info-dev.plist` is -compiled into your release binary too. Use the per-permission variables below where that matters. +compiled into your release binary too. Declare flavors (below) when that matters. **Changes are cached.** The manifest is not re-evaluated when an `Info.plist` or an environment variable changes. Clear DerivedData once afterwards: @@ -34,6 +34,91 @@ variable changes. Clear DerivedData once afterwards: rm -rf ~/Library/Developer/Xcode/DerivedData ``` +### Per-flavor permissions + +> Swift Package Manager only. Under CocoaPods the macros come from your `Podfile`, where you can +> already set them per configuration; `permission_handler.yaml` is ignored and the build phase +> below is a no-op. + +If your flavors need different permissions — a `dev` build that scans QR codes, a `prod` build that +does not — merging is wrong: it compiles the camera code into `prod` too, which is what +`ITMS-90683` rejects. Declare a `permission_handler.yaml` next to your `pubspec.yaml`: + +```yaml +strict: true +flavors: + dev: + info-plist: ios/Runner/Info-dev.plist + configurations: + - Debug-dev + - Profile-dev + - Release-dev + prod: + info-plist: ios/Runner/Info-prod.plist + configurations: + - Debug-prod + - Profile-prod + - Release-prod +``` + +Each flavor names the one `Info.plist` that defines it, and nothing is merged: a flavor can never +inherit another flavor's permissions. `configurations` lists the Xcode build configurations that +belong to the flavor, which is how the build phase below knows what to expect. Each configuration +must belong to exactly one flavor — `select` rejects a config where two flavors claim the same one, +since the build would have no way to tell which permissions it should ship. + +Select a flavor before building: + +```bash +dart run permission_handler_apple:select prod # --list shows what is declared +flutter run --flavor prod +``` + +Selecting is a separate step because the package manifest is evaluated once, is cached, and is +given none of Xcode's build settings — it cannot tell which configuration is running, and Xcode +will not re-evaluate it just because an environment variable changed. `select` records the choice +*and* clears the caches that would otherwise keep serving the previous flavor's permissions. + +`select` is also the only YAML reader in the pipeline. A Swift package manifest cannot parse YAML — +Foundation has no support for it and a manifest cannot import libraries — so `select` translates +your config into a generated `ios/Flutter/permission_handler.resolved.json` that the manifest and +the build phase read with their native JSON parsers. Never edit that file; if you change +`permission_handler.yaml`, re-run `select`. Building with a translation older than the YAML fails +rather than using stale permissions. + +With `strict: true` (the default) a build whose flavor cannot be determined compiles no +permissions at all, rather than falling back to the union of every flavor. + +#### Fail the build on a stale selection + +Nothing inside the manifest can detect a stale selection, because a cached manifest is not +executed. Add a **Run Script** build phase to your Runner target, and drag it to the *top* of the +phase list so a mismatch fails before anything is compiled: + +```sh +# Resolve the plugin, wherever this project gets it from. +PLUGIN="$SRCROOT/Flutter/ephemeral/Packages/.packages/permission_handler_apple/../.." +[ -d "$PLUGIN/tool" ] || PLUGIN="$SRCROOT/.symlinks/plugins/permission_handler_apple" +[ -f "$PLUGIN/tool/verify_flavor_selection.sh" ] || exit 0 +/bin/sh "$PLUGIN/tool/verify_flavor_selection.sh" +``` + +It runs on every build, where `CONFIGURATION` is available, and fails with an actionable message +when the selected flavor does not match what is being built: + +``` +error: [permission_handler_apple] building "Release-prod" needs the "prod" permission flavor, +but "dev" is selected, so this build would ship dev's permissions. +Run: dart run permission_handler_apple:select prod +``` + +Add these to your `.gitignore` — one records a local choice, the other is generated: + +``` +ios/Flutter/permission_handler.selected +ios/Flutter/permission_handler.resolved.json +``` + ### Environment variables Xcode.app does not inherit your shell's environment, so set these with `launchctl setenv` rather @@ -43,6 +128,8 @@ than exporting them, then restart Xcode. | --- | --- | | `PERMISSION_` | Forces a single permission on (`1`) or off (`0`), overriding everything else. For example `launchctl setenv PERMISSION_CAMERA 0`. | | `PERMISSION_HANDLER_INFO_PLIST` | A `:`-separated list of `Info.plist` paths. When set, replaces automatic discovery entirely. | +| `PERMISSION_HANDLER_FLAVOR` | The active flavor, overriding the one recorded by `select`. Changing it still needs the caches cleared. | +| `PERMISSION_HANDLER_CONFIG` | Path to `permission_handler.yaml`, for builds that cannot locate the app automatically. | | `PERMISSION_HANDLER_VERBOSE` | Set to `1` to log the detected app root, the `Info.plist` files used, and the resolved macros. | ### Builds started from Xcode.app diff --git a/permission_handler_apple/bin/select.dart b/permission_handler_apple/bin/select.dart new file mode 100644 index 000000000..133899a23 --- /dev/null +++ b/permission_handler_apple/bin/select.dart @@ -0,0 +1,345 @@ +// Selects which flavor's permissions the Swift Package Manager build compiles. +// +// Usage: +// dart run permission_handler_apple:select +// dart run permission_handler_apple:select --list +// +// A Swift package manifest is evaluated once per package resolution and is given +// none of Xcode's build settings, so it cannot know which build configuration is +// running. The flavor therefore has to be chosen before the build, which is what +// this command does: it records the choice and clears the caches that would +// otherwise keep serving the previous flavor's macros. +// +// The configuration lives in permission_handler.yaml, but a Swift package +// manifest cannot parse YAML — Foundation has no YAML support and a manifest +// cannot import libraries for its own evaluation. This command is therefore the +// only YAML reader: it translates the config into a generated +// permission_handler.resolved.json that the manifest and the verification build +// phase consume with their native JSON parsers. The generated file is an +// internal artifact — gitignore it, never edit it. + +import 'dart:convert'; +import 'dart:io'; + +import 'package:yaml/yaml.dart'; + +const _configName = 'permission_handler.yaml'; +const _selectionPath = 'ios/Flutter/permission_handler.selected'; +const _resolvedPath = 'ios/Flutter/permission_handler.resolved.json'; + +/// Caches that keep a previously evaluated manifest alive. +/// +/// Xcode does not re-evaluate a package manifest when an environment variable or +/// the selection file changes — only when these are gone. Clearing +/// `SourcePackages` on its own is not enough; the resolved build description in +/// `XCBuildData` pins the old settings too. +const _derivedDataSubpaths = [ + 'SourcePackages', + 'Build/Intermediates.noindex/XCBuildData', +]; + +void main(List args) { + final flags = {}; + final positional = []; + for (var i = 0; i < args.length; i++) { + final arg = args[i]; + if (arg == '--list') { + flags['list'] = 'true'; + } else if (arg.startsWith('--app=')) { + flags['app'] = arg.substring(6); + } else if (arg.startsWith('--derived-data=')) { + flags['derived-data'] = arg.substring(15); + } else if (arg == '--help' || arg == '-h') { + stdout.writeln(_usage); + return; + } else if (arg.startsWith('-')) { + _fail('Unknown option "$arg".\n\n$_usage'); + } else { + positional.add(arg); + } + } + + final appRoot = _findAppRoot(flags['app']); + final configFile = File('${appRoot.path}/$_configName'); + if (!configFile.existsSync()) { + _fail('No $_configName found in ${appRoot.path}.\n\n' + 'Create one to describe your flavors:\n$_exampleConfig'); + } + + final config = _readConfig(configFile); + + if (flags.containsKey('list')) { + stdout.writeln('Flavors declared in ${configFile.path}:'); + for (final entry in config.flavors.entries) { + final exists = + File('${appRoot.path}/${entry.value.infoPlist}').existsSync(); + stdout.writeln(' ${entry.key.padRight(12)} ${entry.value.infoPlist}' + '${exists ? '' : ' (missing!)'}'); + } + return; + } + + if (positional.length != 1) { + _fail('Expected exactly one flavor name.\n\n$_usage'); + } + final flavor = positional.single; + + final entry = config.flavors[flavor]; + if (entry == null) { + _fail('Flavor "$flavor" is not declared in ${configFile.path}.\n' + 'Known flavors: ${config.flavors.keys.join(', ')}'); + } + + final plist = File('${appRoot.path}/${entry.infoPlist}'); + if (!plist.existsSync()) { + _fail( + 'Flavor "$flavor" points at ${entry.infoPlist}, which does not exist.'); + } + + _writeResolved(appRoot, config); + + final selection = File('${appRoot.path}/$_selectionPath'); + selection.parent.createSync(recursive: true); + selection.writeAsStringSync('$flavor\n'); + + final cleared = _clearCaches(appRoot, flags['derived-data']); + + stdout.writeln('Selected flavor "$flavor" (${entry.infoPlist}).'); + stdout.writeln(''); + stdout.writeln('Permissions that will be compiled in:'); + final descriptions = _usageDescriptions(plist); + if (descriptions.isEmpty) { + stdout.writeln( + ' (none — ${entry.infoPlist} declares no usage descriptions)'); + } else { + for (final key in descriptions) { + stdout.writeln(' $key'); + } + } + stdout.writeln(''); + stdout.writeln(cleared.isEmpty + ? 'No package caches needed clearing.' + : 'Cleared ${cleared.length} cache location(s) so the manifest is ' + 're-evaluated on the next build.'); +} + +class _Flavor { + const _Flavor(this.infoPlist, this.configurations); + + final String infoPlist; + final List configurations; +} + +class _Config { + const _Config(this.strict, this.flavors); + + final bool strict; + final Map flavors; +} + +/// Walk up looking for a Flutter app: a pubspec.yaml next to an Xcode project. +Directory _findAppRoot(String? override) { + var dir = Directory(override ?? Directory.current.path).absolute; + for (var i = 0; i < 12; i++) { + final hasPubspec = File('${dir.path}/pubspec.yaml').existsSync(); + final iosDir = Directory('${dir.path}/ios'); + final hasProject = iosDir.existsSync() && + iosDir.listSync().any((e) => e.path.endsWith('.xcodeproj')); + if (hasPubspec && hasProject) return dir; + final parent = dir.parent; + if (parent.path == dir.path) break; + dir = parent; + } + _fail('Could not find a Flutter app (a pubspec.yaml next to ios/*.xcodeproj) ' + 'from ${override ?? Directory.current.path}. Pass --app=.'); +} + +_Config _readConfig(File configFile) { + final Object? decoded; + try { + decoded = loadYaml(configFile.readAsStringSync()); + } on YamlException catch (e) { + _fail('${configFile.path} is not valid YAML: ${e.message}'); + } + + if (decoded is! YamlMap) { + _fail('${configFile.path} must contain a YAML mapping.\n\n$_exampleConfig'); + } + final flavors = decoded['flavors']; + if (flavors is! YamlMap || flavors.isEmpty) { + _fail('${configFile.path} declares no "flavors".\n\n$_exampleConfig'); + } + + // This command is the only thing that reads the YAML, so it is the only place + // that can reject a malformed config. Everything downstream sees the + // generated JSON and has no way to tell a deliberate value from a typo, so + // validation that is skipped here is validation that never happens. + final result = {}; + final claimedBy = {}; // build configuration -> flavor + + for (final entry in flavors.entries) { + final key = entry.key; + if (key is! String) { + _fail('Flavor name ${jsonEncode(key)} in ${configFile.path} is not a ' + 'string. Quote it if you meant a literal name: "$key".'); + } + final name = key; + + final value = entry.value; + if (value is! YamlMap || value['info-plist'] is! String) { + _fail('Flavor "$name" in ${configFile.path} has no "info-plist" string.'); + } + + // A scalar here used to become an empty list, which silently disables the + // build phase's mismatch check for every configuration of this flavor. + final configurations = value['configurations']; + if (configurations != null && configurations is! YamlList) { + _fail('Flavor "$name" in ${configFile.path} has a "configurations" that ' + 'is not a list. Write it as:\n' + ' configurations:\n' + ' - Debug-$name\n' + ' - Release-$name'); + } + + final names = + (configurations as YamlList?)?.map((c) => c.toString()).toList() ?? + const []; + + // Two flavors claiming one configuration makes the build phase pick + // whichever comes first and demand that flavor, which would talk the user + // into shipping the other flavor's permissions. + for (final configuration in names) { + final owner = claimedBy[configuration]; + if (owner != null) { + _fail('Build configuration "$configuration" in ${configFile.path} is ' + 'claimed by both "$owner" and "$name". Each configuration must ' + 'belong to exactly one flavor, otherwise the build cannot tell ' + 'which permissions it should ship.'); + } + claimedBy[configuration] = name; + } + + result[name] = _Flavor(value['info-plist'] as String, names); + } + + final strict = decoded['strict']; + if (strict != null && strict is! bool) { + _fail('"strict" in ${configFile.path} must be true or false, not ' + '${jsonEncode(strict.toString())}.'); + } + + return _Config(strict as bool? ?? true, result); +} + +/// Write the generated JSON translation the manifest and build phase read. +/// +/// The camelCase `infoPlist` key is deliberate: it matches what Package.swift +/// and verify_flavor_selection.sh already parse, and this file is not +/// user-facing. +void _writeResolved(Directory appRoot, _Config config) { + final resolved = File('${appRoot.path}/$_resolvedPath'); + resolved.parent.createSync(recursive: true); + resolved.writeAsStringSync( + const JsonEncoder.withIndent(' ').convert({ + 'note': 'Generated by permission_handler_apple:select from ' + '$_configName. Do not edit or commit.', + 'strict': config.strict, + 'flavors': { + for (final entry in config.flavors.entries) + entry.key: { + 'infoPlist': entry.value.infoPlist, + 'configurations': entry.value.configurations, + }, + }, + }), + ); +} + +List _usageDescriptions(File plist) { + final matches = RegExp(r'(NS\w*UsageDescription)') + .allMatches(plist.readAsStringSync()) + .map((m) => m.group(1)!) + .toSet() + .toList() + ..sort(); + return matches; +} + +/// Remove the caches pinning the previously resolved manifest. +List _clearCaches(Directory appRoot, String? derivedDataOverride) { + final cleared = []; + + void remove(String path) { + final dir = Directory(path); + if (!dir.existsSync()) return; + dir.deleteSync(recursive: true); + cleared.add(path); + } + + final home = Platform.environment['HOME']; + if (home != null) { + remove('$home/Library/Caches/org.swift.swiftpm/manifests'); + } + + for (final derivedData in _derivedDataDirs(appRoot, derivedDataOverride)) { + for (final sub in _derivedDataSubpaths) { + remove('${derivedData.path}/$sub'); + } + } + + return cleared; +} + +/// Locate the DerivedData directories belonging to this app. +/// +/// Every Flutter app's Xcode project is called `Runner`, so matching on the +/// directory name would clear unrelated apps' caches. Each DerivedData +/// directory records the workspace it belongs to in its `info.plist`; match on +/// that instead. +List _derivedDataDirs(Directory appRoot, String? override) { + if (override != null) return [Directory(override)]; + + final home = Platform.environment['HOME']; + if (home == null) return const []; + final root = Directory('$home/Library/Developer/Xcode/DerivedData'); + if (!root.existsSync()) return const []; + + final iosDir = '${appRoot.resolveSymbolicLinksSync()}/ios'; + return root.listSync().whereType().where((dir) { + final info = File('${dir.path}/info.plist'); + if (!info.existsSync()) return false; + final match = RegExp(r'WorkspacePath\s*([^<]*)') + .firstMatch(info.readAsStringSync()); + final workspace = match?.group(1); + return workspace != null && workspace.startsWith(iosDir); + }).toList(); +} + +Never _fail(String message) { + stderr.writeln('permission_handler_apple:select: $message'); + exit(1); +} + +const _usage = ''' +Usage: dart run permission_handler_apple:select + + --list Show the flavors declared in $_configName. + --app= App directory (defaults to the current directory). + --derived-data= Custom DerivedData location, matching xcodebuild's + -derivedDataPath. +'''; + +const _exampleConfig = ''' +strict: true +flavors: + dev: + info-plist: ios/Runner/Info-dev.plist + configurations: + - Debug-dev + - Release-dev + prod: + info-plist: ios/Runner/Info-prod.plist + configurations: + - Debug-prod + - Release-prod +'''; diff --git a/permission_handler_apple/example/ios/.gitignore b/permission_handler_apple/example/ios/.gitignore index e96ef602b..da20d3007 100644 --- a/permission_handler_apple/example/ios/.gitignore +++ b/permission_handler_apple/example/ios/.gitignore @@ -30,3 +30,5 @@ Runner/GeneratedPluginRegistrant.* !default.mode2v3 !default.pbxuser !default.perspectivev3 +Flutter/permission_handler.selected +Flutter/permission_handler.resolved.json diff --git a/permission_handler_apple/example/ios/Flutter/permission_handler.selected b/permission_handler_apple/example/ios/Flutter/permission_handler.selected deleted file mode 100644 index d287cd3ed..000000000 --- a/permission_handler_apple/example/ios/Flutter/permission_handler.selected +++ /dev/null @@ -1 +0,0 @@ -debug diff --git a/permission_handler_apple/example/ios/Runner.xcodeproj/project.pbxproj b/permission_handler_apple/example/ios/Runner.xcodeproj/project.pbxproj index 51d5f1ace..3e63a5493 100644 --- a/permission_handler_apple/example/ios/Runner.xcodeproj/project.pbxproj +++ b/permission_handler_apple/example/ios/Runner.xcodeproj/project.pbxproj @@ -141,6 +141,7 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + PH0FLAV0R0VERIFY0PHASE01 /* Verify permission_handler flavor */, 5CF31A8C72B66ABB365E813D /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, @@ -212,6 +213,21 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + PH0FLAV0R0VERIFY0PHASE01 /* Verify permission_handler flavor */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Verify permission_handler flavor"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Resolve the plugin, wherever this project gets it from.\nPLUGIN=\"$SRCROOT/Flutter/ephemeral/Packages/.packages/permission_handler_apple/../..\"\n[ -d \"$PLUGIN/tool\" ] || PLUGIN=\"$SRCROOT/.symlinks/plugins/permission_handler_apple\"\n[ -f \"$PLUGIN/tool/verify_flavor_selection.sh\" ] || exit 0\n/bin/sh \"$PLUGIN/tool/verify_flavor_selection.sh\"\n"; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; diff --git a/permission_handler_apple/example/permission_handler.yaml b/permission_handler_apple/example/permission_handler.yaml new file mode 100644 index 000000000..cf1532e9e --- /dev/null +++ b/permission_handler_apple/example/permission_handler.yaml @@ -0,0 +1,17 @@ +# Per-flavor permissions for Swift Package Manager builds. +# +# The two flavors deliberately differ by exactly one key: `debug` +# (Info-Debug.plist) declares no NSContactsUsageDescription, `release` +# (Info.plist) does. CI asserts on that difference to prove a flavor cannot +# inherit another flavor's permissions. +strict: true +flavors: + debug: + info-plist: ios/Runner/Info-Debug.plist + configurations: + - Debug + release: + info-plist: ios/Runner/Info.plist + configurations: + - Release + - Profile diff --git a/permission_handler_apple/ios/permission_handler_apple/Package.swift b/permission_handler_apple/ios/permission_handler_apple/Package.swift index c4720c6f3..6ca1b7527 100644 --- a/permission_handler_apple/ios/permission_handler_apple/Package.swift +++ b/permission_handler_apple/ios/permission_handler_apple/Package.swift @@ -19,6 +19,10 @@ import Foundation // replaces automatic discovery entirely. This // is the only mechanism that works for builds // started from Xcode.app (see findAppRoot()). +// PERMISSION_HANDLER_FLAVOR The active flavor, overriding the one +// recorded by the `select` command. +// PERMISSION_HANDLER_CONFIG Path to permission_handler.yaml, for builds +// that cannot locate the app automatically. // PERMISSION_HANDLER_VERBOSE Set to 1 to log what was discovered and // which permissions ended up enabled. // @@ -286,24 +290,246 @@ func infoPlistsFromEnvironment() -> [URL]? { .map { URL(fileURLWithPath: String($0).trimmingCharacters(in: .whitespaces)) } } -/// Collect the usage description keys of every Info.plist belonging to the -/// host app. +/// Every Info.plist that appears to belong to `appRoot`: what the build +/// settings name, falling back to a scan of `ios/` when they name nothing that +/// exists. +func discoverInfoPlists(appRoot: URL) -> [URL] { + let fromSettings = existingFiles(infoPlistsFromBuildSettings(appRoot: appRoot)) + return fromSettings.isEmpty ? infoPlistsFromScan(appRoot: appRoot) : fromSettings +} + +// MARK: - Per-flavor configuration + +/// The user declares flavors in a `permission_handler.yaml` next to the app's +/// pubspec.yaml: +/// +/// ```yaml +/// strict: true +/// flavors: +/// dev: +/// info-plist: ios/Runner/Info-dev.plist +/// configurations: +/// - Debug-dev +/// - Release-dev +/// prod: +/// info-plist: ios/Runner/Info-prod.plist +/// configurations: +/// - Debug-prod +/// - Release-prod +/// ``` +/// +/// This manifest never parses that file. Foundation has no YAML support and a +/// package manifest cannot import a library for its own evaluation, so +/// `dart run permission_handler_apple:select` — the one place with a real YAML +/// parser — translates it into a generated +/// `ios/Flutter/permission_handler.resolved.json`, which is what is read here +/// with JSONSerialization. The YAML file's existence and modification time are +/// the only things consulted directly, to catch a translation that is missing +/// or stale. +/// +/// A flavor names the single Info.plist that defines it. Listing usage +/// description keys directly was considered and rejected: it would duplicate the +/// permission vocabulary across the config, this manifest and the verification +/// build phase, and a drift between those copies fails silently. +/// +/// `configurations` is deliberately not read here. A manifest is given no build +/// settings, so it cannot know which configuration is building and could not act +/// on the mapping; only `tool/verify_flavor_selection.sh`, which runs as a build +/// phase where CONFIGURATION exists, consumes that key. +struct FlavorConfig { + let strict: Bool + let infoPlists: [String: String] // flavor -> path, relative to the app root + let url: URL // the user-facing permission_handler.yaml +} + +/// The user-facing config file and the directory its relative paths resolve +/// against, when this app has one. /// -/// The keys are *merged* across build configurations and flavors. The manifest -/// is evaluated once per package resolution and cannot know which configuration -/// is building, so per-flavor macros are not expressible here. Merging errs -/// towards enabling a permission: an app whose `Info-dev.plist` declares camera -/// access compiles the camera code into its release binary too. Use the -/// per-permission `PERMISSION_*` environment variables where that matters. +/// PERMISSION_HANDLER_CONFIG names the config *and* the root: the file sits +/// next to the app's pubspec.yaml by definition, so its directory is the app. +/// Deriving the root from `appRoot` instead would let the config come from one +/// app while its generated translation and Info.plists come from another. +func locateConfigYaml(appRoot: URL?) -> (yaml: URL, root: URL)? { + let configURL: URL + let root: URL + if let explicit = env["PERMISSION_HANDLER_CONFIG"], !explicit.isEmpty { + configURL = URL(fileURLWithPath: explicit).standardizedFileURL + root = configURL.deletingLastPathComponent() + } else if let appRoot { + configURL = appRoot.appendingPathComponent("permission_handler.yaml") + root = appRoot + } else { + return nil + } + return fileManager.fileExists(atPath: configURL.path) ? (configURL, root) : nil +} + +func modificationDate(of url: URL) -> Date? { + (try? fileManager.attributesOfItem(atPath: url.path))?[.modificationDate] as? Date +} + +/// Load the generated translation of `yaml`, refusing anything missing, stale +/// or malformed. +/// +/// Every failure returns nil after a diagnostic, and the caller compiles no +/// permissions in. Falling back to merged discovery instead would hand the +/// build the union of every flavor's permissions — the exact leak a config +/// file exists to prevent — so a broken translation must never be "ignored". +func loadFlavorConfig(yaml: URL, configRoot: URL) -> FlavorConfig? { + let resolved = configRoot.appendingPathComponent("ios/Flutter/permission_handler.resolved.json") + let rerun = """ + Run `dart run permission_handler_apple:select ` to regenerate it, then build again. + """ + + guard fileManager.fileExists(atPath: resolved.path) else { + diagnostic("error", """ + \(yaml.lastPathComponent) is present but its generated translation \ + (\(resolved.path)) is not, so every iOS permission has been compiled out. \(rerun) + """) + return nil + } + + // Both files exist, so unreadable timestamps mean something is wrong with + // the filesystem rather than with the config. Refuse either way: skipping + // the check would let a stale translation through silently, which is the + // one outcome this guard exists to prevent. + guard let yamlDate = modificationDate(of: yaml), + let resolvedDate = modificationDate(of: resolved) else { + diagnostic("error", """ + The modification time of \(yaml.lastPathComponent) or \ + \(resolved.lastPathComponent) could not be read, so it is not possible to tell \ + whether the generated translation is current. Every iOS permission has been \ + compiled out. \(rerun) + """) + return nil + } + + if yamlDate > resolvedDate { + diagnostic("error", """ + \(yaml.lastPathComponent) was modified after its generated translation \ + (\(resolved.lastPathComponent)), so every iOS permission has been compiled out \ + rather than building with a stale configuration. \(rerun) + """) + return nil + } + + guard let data = try? Data(contentsOf: resolved), + let root = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any], + let flavors = root["flavors"] as? [String: Any], !flavors.isEmpty else { + diagnostic("error", """ + \(resolved.path) is not a valid generated configuration, so every iOS permission \ + has been compiled out. \(rerun) + """) + return nil + } + + var infoPlists: [String: String] = [:] + for (flavor, raw) in flavors { + guard let entry = raw as? [String: Any], + let plist = entry["infoPlist"] as? String, !plist.isEmpty else { continue } + infoPlists[flavor] = plist + } + + guard !infoPlists.isEmpty else { + diagnostic("error", """ + \(resolved.path) declares no usable flavors, so every iOS permission has been \ + compiled out. \(rerun) + """) + return nil + } + + return FlavorConfig( + strict: root["strict"] as? Bool ?? true, + infoPlists: infoPlists, + url: yaml + ) +} + +/// The flavor this build should compile permissions for. +/// +/// Xcode exposes no build settings to manifest evaluation, so `CONFIGURATION` +/// cannot be read here and the selection has to be made out of band — by +/// `dart run permission_handler_apple:select `, which records it and +/// clears the caches that would otherwise keep serving the previous answer. +func resolveFlavor(appRoot: URL?) -> String? { + if let fromEnv = env["PERMISSION_HANDLER_FLAVOR"], !fromEnv.isEmpty { return fromEnv } + guard let appRoot else { return nil } + let selection = appRoot.appendingPathComponent("ios/Flutter/permission_handler.selected") + guard let raw = try? String(contentsOf: selection, encoding: .utf8) else { return nil } + let flavor = raw.trimmingCharacters(in: .whitespacesAndNewlines) + return flavor.isEmpty ? nil : flavor +} + +/// Collect the usage description keys that apply to this build. +/// +/// With a `permission_handler.yaml` the active flavor selects exactly one +/// Info.plist and nothing is merged, so a permission declared only by `dev` +/// never reaches a `prod` binary. Without one the keys of every discovered +/// Info.plist are merged, which errs towards enabling a permission. func findInfoPlist() -> [String: Any] { var candidates: [URL] + let appRoot = findAppRoot() if let explicit = infoPlistsFromEnvironment() { candidates = explicit - } else if let appRoot = findAppRoot() { + } else if let located = locateConfigYaml(appRoot: appRoot) { + let configYaml = located.yaml + let configRoot = located.root + + guard let config = loadFlavorConfig(yaml: configYaml, configRoot: configRoot) else { + // Diagnostics already emitted; a present-but-unusable config + // compiles nothing in rather than falling back to the merge. + return [:] + } + + guard let flavor = resolveFlavor(appRoot: configRoot) else { + guard !config.strict else { + diagnostic("error", """ + \(config.url.lastPathComponent) declares the flavors [\(known(config))] with \ + "strict": true, but the active flavor could not be determined, so every iOS \ + permission has been compiled out. Run \ + `dart run permission_handler_apple:select ` before building, or set \ + PERMISSION_HANDLER_FLAVOR. + """) + return [:] + } + diagnostic("warning", """ + \(config.url.lastPathComponent) declares the flavors [\(known(config))] but the \ + active flavor could not be determined. Falling back to merging every Info.plist \ + found, which enables the union of all flavors' permissions. Set "strict": true to \ + turn this into an error instead. + """) + return mergeInfoPlists(discoverInfoPlists(appRoot: configRoot), warnOnDivergence: true) + } + + guard let relative = config.infoPlists[flavor] else { + diagnostic("error", """ + Flavor "\(flavor)" is not declared in \(config.url.lastPathComponent) \ + (known flavors: \(known(config))). Every iOS permission has been compiled out. + """) + return [:] + } + + let plist = configRoot.appendingPathComponent(relative) + guard fileManager.fileExists(atPath: plist.path) else { + // Falling back to discovery here would hand this flavor the union of + // every other flavor's permissions, which is the leak the config + // exists to prevent. Compile nothing in and say why. + diagnostic("error", """ + Flavor "\(flavor)" points at \(relative), which does not exist \ + (\(plist.path)). Every iOS permission has been compiled out. Fix the "info-plist" \ + path in \(config.url.lastPathComponent) and re-run \ + `dart run permission_handler_apple:select \(flavor)`. + """) + return [:] + } + + if verbose { diagnostic("note", "active flavor: \(flavor) (\(relative))") } + // One flavor, one plist: never merge, so nothing can leak between flavors. + return mergeInfoPlists([plist], warnOnDivergence: false) + } else if let appRoot { if verbose { diagnostic("note", "app root: \(appRoot.path)") } - let fromSettings = existingFiles(infoPlistsFromBuildSettings(appRoot: appRoot)) - candidates = fromSettings.isEmpty ? infoPlistsFromScan(appRoot: appRoot) : fromSettings + candidates = discoverInfoPlists(appRoot: appRoot) } else { diagnostic("warning", """ Could not locate the host app, so every iOS permission has been compiled out and \ @@ -317,14 +543,27 @@ func findInfoPlist() -> [String: Any] { return [:] } + return mergeInfoPlists(candidates, warnOnDivergence: true) +} + +func known(_ config: FlavorConfig) -> String { + config.infoPlists.keys.sorted().joined(separator: ", ") +} + +/// Read every candidate plist and union their keys. +/// +/// Only key *presence* matters to `enabled()`, so the first value for a key +/// wins. `warnOnDivergence` is off when a flavor picked a single plist, where +/// there is nothing to diverge. +func mergeInfoPlists(_ candidates: [URL], warnOnDivergence: Bool) -> [String: Any] { var seen = Set() - candidates = candidates.filter { seen.insert($0.standardizedFileURL.path).inserted } + let unique = candidates.filter { seen.insert($0.standardizedFileURL.path).inserted } var merged: [String: Any] = [:] var loaded: [URL] = [] var keysPerPlist: [Set] = [] - for url in candidates { + for url in unique { guard let plist = loadInfoPlist(at: url) else { continue } loaded.append(url) keysPerPlist.append(Set(plist.keys.filter { $0.hasSuffix("UsageDescription") })) @@ -334,9 +573,9 @@ func findInfoPlist() -> [String: Any] { if loaded.isEmpty { diagnostic("warning", """ No readable Info.plist was found for the host app, so every iOS permission has been \ - compiled out and permission checks will report `denied`. Set \ - PERMISSION_HANDLER_INFO_PLIST to the path of your Info.plist, then run \ - `rm -rf ~/Library/Developer/Xcode/DerivedData`. + compiled out and permission checks will report `denied`. Looked at: \ + \(unique.map(\.path).joined(separator: ", ")). Set PERMISSION_HANDLER_INFO_PLIST to \ + the path of your Info.plist, then run `rm -rf ~/Library/Developer/Xcode/DerivedData`. """) return [:] } @@ -348,15 +587,16 @@ func findInfoPlist() -> [String: Any] { // Merging across configurations enables the union of their permissions. Say // so when they actually disagree, because the extra permissions end up in // the release binary and can trigger App Store rejection (ITMS-90683). - if let first = keysPerPlist.first, keysPerPlist.contains(where: { $0 != first }) { + if warnOnDivergence, let first = keysPerPlist.first, + keysPerPlist.contains(where: { $0 != first }) { let divergent = keysPerPlist.reduce(into: Set()) { $0.formUnion($1) } .subtracting(keysPerPlist.reduce(into: keysPerPlist[0]) { $0.formIntersection($1) }) diagnostic("warning", """ The discovered Info.plist files declare different usage descriptions \ (\(divergent.sorted().joined(separator: ", "))). Every permission found in any of \ them is enabled for all build configurations, because a Swift package manifest \ - cannot vary its settings per configuration. Set PERMISSION_HANDLER_INFO_PLIST or the \ - per-permission PERMISSION_* variables to control this explicitly. + cannot vary its settings per configuration. Declare a permission_handler.json with \ + one flavor per configuration to compile each of them separately. """) } diff --git a/permission_handler_apple/pubspec.yaml b/permission_handler_apple/pubspec.yaml index dd6e6758d..395984963 100644 --- a/permission_handler_apple/pubspec.yaml +++ b/permission_handler_apple/pubspec.yaml @@ -2,7 +2,7 @@ name: permission_handler_apple description: Permission plugin for Flutter. This plugin provides the iOS API to request and check permissions. repository: https://github.com/baseflow/flutter-permission-handler issue_tracker: https://github.com/Baseflow/flutter-permission-handler/issues -version: 9.5.1 +version: 9.6.0 environment: sdk: ^3.6.0 @@ -20,6 +20,9 @@ dependencies: flutter: sdk: flutter permission_handler_platform_interface: ^4.4.0 + # Used only by `bin/select.dart` to read permission_handler.yaml; pure Dart, + # tree-shaken out of app binaries. + yaml: ^3.1.0 dev_dependencies: flutter_lints: ^5.0.0 diff --git a/permission_handler_apple/tool/verify_flavor_selection.sh b/permission_handler_apple/tool/verify_flavor_selection.sh new file mode 100755 index 000000000..55df4c4d0 --- /dev/null +++ b/permission_handler_apple/tool/verify_flavor_selection.sh @@ -0,0 +1,102 @@ +#!/bin/sh +# +# Fails the build when the permissions compiled into the Swift package do not +# belong to the configuration being built. +# +# A Swift package manifest is evaluated once, is cached, and is given none of +# Xcode's build settings, so it cannot tell which configuration is running and +# cannot notice that its answer went stale. This script runs as a build phase of +# the app target, where CONFIGURATION *is* available, and compares it against the +# flavor recorded by `dart run permission_handler_apple:select`. +# +# The user-facing config is permission_handler.yaml, but this script reads the +# generated permission_handler.resolved.json that `select` derives from it — +# `yaml` is not in the Python standard library, and JSON is. The YAML file +# itself is consulted only for existence and modification time, to catch a +# translation that is missing or stale. +# +# Add it as a "Run Script" build phase on the Runner target, as early in the +# phase list as possible so a mismatch fails before the app is compiled. It is a +# no-op for projects without a permission_handler.yaml and for CocoaPods builds. + +set -eu + +APP_ROOT="${SRCROOT}/.." +CONFIG="${APP_ROOT}/permission_handler.yaml" +RESOLVED="${APP_ROOT}/ios/Flutter/permission_handler.resolved.json" +SELECTION="${APP_ROOT}/ios/Flutter/permission_handler.selected" +SPM_PACKAGE="${SRCROOT}/Flutter/ephemeral/Packages/.packages/permission_handler_apple" + +# Per-flavor permissions are opt-in; nothing to check without a config. +[ -f "${CONFIG}" ] || exit 0 + +# Only Swift Package Manager builds resolve permissions from Package.swift. +# Under CocoaPods the PERMISSION_* macros come from the Podfile's +# GCC_PREPROCESSOR_DEFINITIONS, which this script has no say over, so a flavor +# selection means nothing and must not fail the build. +[ -d "${SPM_PACKAGE}" ] || exit 0 + +if [ ! -f "${RESOLVED}" ]; then + echo "error: [permission_handler_apple] ${CONFIG} is present but its generated translation (${RESOLVED}) is not, so no permissions were compiled in. Run: dart run permission_handler_apple:select " + exit 1 +fi + +# `find -newer` instead of `[ -nt ]`: -nt is a bash extension and this script +# declares /bin/sh. Errors are not swallowed — a comparison that could not run +# is reported rather than read as "not stale", which would let a stale +# translation build silently. +if ! STALE=$(find "${CONFIG}" -newer "${RESOLVED}" 2>&1); then + echo "error: [permission_handler_apple] could not compare ${CONFIG} against its generated translation: ${STALE}" + exit 1 +fi + +if [ -n "${STALE}" ]; then + echo "error: [permission_handler_apple] ${CONFIG} was modified after its generated translation, so this build would use a stale permission configuration. Run: dart run permission_handler_apple:select " + exit 1 +fi + +if [ ! -x /usr/bin/python3 ]; then + echo "warning: [permission_handler_apple] /usr/bin/python3 not found, skipping flavor verification." + exit 0 +fi + +EXPECTED=$(/usr/bin/python3 - "${RESOLVED}" "${CONFIGURATION}" <<'PY' +import json, sys + +resolved_path, configuration = sys.argv[1], sys.argv[2] +try: + with open(resolved_path) as handle: + flavors = json.load(handle).get("flavors", {}) +except (OSError, ValueError) as error: + print(f"!invalid:{error}") + sys.exit(0) + +for name, entry in flavors.items(): + if configuration in (entry or {}).get("configurations", []): + print(name) + break +PY +) + +case "${EXPECTED}" in + '!invalid:'*) + echo "error: [permission_handler_apple] ${RESOLVED} could not be read: ${EXPECTED#!invalid:}. Run: dart run permission_handler_apple:select " + exit 1 + ;; + '') + echo "warning: [permission_handler_apple] no flavor in ${CONFIG} lists the \"${CONFIGURATION}\" configuration, so the compiled permissions cannot be verified. Add it to the flavor's \"configurations\" list." + exit 0 + ;; +esac + +if [ ! -f "${SELECTION}" ]; then + echo "error: [permission_handler_apple] building \"${CONFIGURATION}\" needs the \"${EXPECTED}\" permission flavor, but no flavor has been selected. Run: dart run permission_handler_apple:select ${EXPECTED}" + exit 1 +fi + +SELECTED=$(tr -d '[:space:]' < "${SELECTION}") + +if [ "${SELECTED}" != "${EXPECTED}" ]; then + echo "error: [permission_handler_apple] building \"${CONFIGURATION}\" needs the \"${EXPECTED}\" permission flavor, but \"${SELECTED}\" is selected, so this build would ship ${SELECTED}'s permissions. Run: dart run permission_handler_apple:select ${EXPECTED}" + exit 1 +fi From 0501b21f4e989fa9a9942ce1b77742123b9408c7 Mon Sep 17 00:00:00 2001 From: Pachebel <89677437+Pachebel@users.noreply.github.com> Date: Tue, 11 Aug 2026 03:54:20 -0300 Subject: [PATCH 23/26] Fix and document the Swift Package Manager section of the main README (#1555) The "iOS - Swift Package Manager" section of permission_handler/README.md is where most users will look, but it no longer covers what the package does. This brings it up to date and points to the permission_handler_apple README for the full detail, rather than duplicating documentation that would drift. Follow-up to #1554, as discussed there. The Info.plist key table was wrong in four places, checked against the plistKeys of each macro in Package.swift: calendarWriteOnly and calendarFullAccess were listed as two separate rows when both map to the same macro (PERMISSION_EVENTS_FULL_ACCESS, either key enables both); photos, location and bluetooth were each missing a second key they also accept; and locationWhenInUse shared a row with location despite being a narrower, separate macro. The section also claimed "no additional configuration file is needed", which stopped being true with #1554. Separately, PERMISSION_HANDLER_VERBOSE was documented in both this README and permission_handler_apple/README.md as always logging "the app root". That only holds without a permission_handler.yaml; with one, it logs the active flavor instead. Fixed in both places. permission_handler_apple/README.md is the README this section now points users to, so a few gaps found in the same pass are closed there too: strict: false was never explained (only strict: true was), select's validation added in #1554 had one line covering one of its four rules, and select --app / --derived-data were undocumented despite being what makes the command usable in CI. Adds to permission_handler/README.md: how Info.plist files are located and merged across build configurations, a short flavors example pointing to the full per-flavor documentation, the Xcode.app case, and a troubleshooting block for the silent-denied failure mode behind #1548. No code changes, so no version bump and no CHANGELOG.md entry. --- permission_handler/README.md | 62 +++++++++++++++++++++++++++--- permission_handler_apple/README.md | 51 +++++++++++++++++++----- 2 files changed, 97 insertions(+), 16 deletions(-) diff --git a/permission_handler/README.md b/permission_handler/README.md index dc411f4da..2159ae49b 100644 --- a/permission_handler/README.md +++ b/permission_handler/README.md @@ -61,26 +61,28 @@ With SPM, `Package.swift` automatically detects which permissions to enable by r | Permission group | Info.plist key | |---|---| | `PermissionGroup.calendar` (< iOS 17) | `NSCalendarsUsageDescription` | -| `PermissionGroup.calendarWriteOnly` (iOS 17+) | `NSCalendarsWriteOnlyAccessUsageDescription` | -| `PermissionGroup.calendarFullAccess` (iOS 17+) | `NSCalendarsFullAccessUsageDescription` | +| `PermissionGroup.calendarWriteOnly` / `calendarFullAccess` (iOS 17+) | `NSCalendarsFullAccessUsageDescription` or `NSCalendarsWriteOnlyAccessUsageDescription` | | `PermissionGroup.reminders` | `NSRemindersUsageDescription` | | `PermissionGroup.contacts` | `NSContactsUsageDescription` | | `PermissionGroup.camera` | `NSCameraUsageDescription` | | `PermissionGroup.microphone` | `NSMicrophoneUsageDescription` | | `PermissionGroup.speech` | `NSSpeechRecognitionUsageDescription` | -| `PermissionGroup.photos` | `NSPhotoLibraryUsageDescription` | +| `PermissionGroup.photos` | `NSPhotoLibraryUsageDescription` or `NSPhotoLibraryAddUsageDescription` | | `PermissionGroup.photosAddOnly` | `NSPhotoLibraryAddUsageDescription` | -| `PermissionGroup.location` / `locationWhenInUse` | `NSLocationWhenInUseUsageDescription` | +| `PermissionGroup.location` | `NSLocationWhenInUseUsageDescription` or `NSLocationAlwaysAndWhenInUseUsageDescription` | +| `PermissionGroup.locationWhenInUse` | `NSLocationWhenInUseUsageDescription` | | `PermissionGroup.locationAlways` | `NSLocationAlwaysAndWhenInUseUsageDescription` | | `PermissionGroup.mediaLibrary` | `NSAppleMusicUsageDescription` | | `PermissionGroup.sensors` | `NSMotionUsageDescription` | -| `PermissionGroup.bluetooth` | `NSBluetoothAlwaysUsageDescription` | +| `PermissionGroup.bluetooth` | `NSBluetoothAlwaysUsageDescription` or `NSBluetoothPeripheralUsageDescription` | | `PermissionGroup.appTrackingTransparency` | `NSUserTrackingUsageDescription` | | `PermissionGroup.assistant` | `NSSiriUsageDescription` | | `PermissionGroup.notification` | *(enabled by default — see below)* | | `PermissionGroup.criticalAlerts` | *(disabled by default — see below)* | -Because you must already add these keys to `Info.plist` for any permission to work, no additional configuration file is needed. +Because you must already add these keys to `Info.plist` for any permission to work, a single-flavor app needs no additional configuration. + +`Info.plist` files are located through the `INFOPLIST_FILE` setting of your Xcode project and `.xcconfig` files, so build-configuration specific plists (`Info-Debug.plist`, `Runner/Info-$(CONFIGURATION).plist`, …) are picked up as well. When an app has several of them, the keys are **merged**: a permission declared in any one of them is compiled into every build configuration. #### Special cases: permissions without an Info.plist key @@ -112,6 +114,54 @@ rm -rf ~/Library/Developer/Xcode/DerivedData Then run `flutter build ios` or rebuild in Xcode as usual. +#### Apps with flavors + +Merging is wrong when your flavors need *different* permissions: a permission declared only in `Info-dev.plist` is compiled into your production binary too, which is grounds for App Store rejection (`ITMS-90683`). Declare a `permission_handler.yaml` next to your `pubspec.yaml` to give each flavor its own `Info.plist`, and select the flavor before building: + +```yaml +strict: true +flavors: + dev: + info-plist: ios/Runner/Info-dev.plist + configurations: [Debug-dev, Profile-dev, Release-dev] + prod: + info-plist: ios/Runner/Info-prod.plist + configurations: [Debug-prod, Profile-prod, Release-prod] +``` + +```bash +dart run permission_handler_apple:select prod +flutter run --flavor prod +``` + +Only the selected flavor's `Info.plist` is read and nothing is merged, so a flavor can never inherit another flavor's permissions. See [Per-flavor permissions](https://github.com/Baseflow/flutter-permission-handler/blob/main/permission_handler_apple/README.md#per-flavor-permissions) for the build phase that catches a stale selection. + +#### Builds started from Xcode.app + +Automatic detection finds your app through the build's working directory, which points at the Flutter project for `flutter run`, `flutter build ios` and `xcodebuild`. Builds started from Xcode.app run with `/` as their working directory and cannot be detected, so point the manifest at your `Info.plist` explicitly: + +```bash +launchctl setenv PERMISSION_HANDLER_INFO_PLIST /absolute/path/to/ios/Runner/Info.plist +rm -rf ~/Library/Developer/Xcode/DerivedData +``` + +#### Troubleshooting + +If no `Info.plist` is found, **every permission is compiled out and all permission checks report `denied`**. Xcode discards Swift package manifest output, so the warning about this only reaches you from the command line: + +```bash +cd your_app +PERMISSION_HANDLER_VERBOSE=1 swift package --manifest-cache none \ + --package-path ios/Flutter/ephemeral/Packages/.packages/permission_handler_apple \ + dump-package > /dev/null +``` + +That prints the `Info.plist` files that were used, the active flavor if you declared one, and the value every `PERMISSION_*` macro resolved to. + +#### More detail + +The [`permission_handler_apple` README](https://github.com/Baseflow/flutter-permission-handler/blob/main/permission_handler_apple/README.md#swift-package-manager) documents this in full, including every `PERMISSION_HANDLER_*` environment variable and the per-flavor workflow. +
diff --git a/permission_handler_apple/README.md b/permission_handler_apple/README.md index c5d1f6a23..23c47cd4c 100644 --- a/permission_handler_apple/README.md +++ b/permission_handler_apple/README.md @@ -63,14 +63,39 @@ flavors: Each flavor names the one `Info.plist` that defines it, and nothing is merged: a flavor can never inherit another flavor's permissions. `configurations` lists the Xcode build configurations that -belong to the flavor, which is how the build phase below knows what to expect. Each configuration -must belong to exactly one flavor — `select` rejects a config where two flavors claim the same one, -since the build would have no way to tell which permissions it should ship. +belong to the flavor, which is how the build phase below knows what to expect. + +`select` validates the file before doing anything else, since it is the only thing that reads the +YAML — a mistake it doesn't catch here has to fail silently later instead: + +- Each build configuration must belong to exactly one flavor. Two flavors listing the same one + leaves the build phase unable to tell which permissions it should ship. +- `configurations` must be a list, even for a single value — `configurations: Debug` (without the + `-`) is rejected rather than silently treated as "no configurations", which would otherwise + disable the build phase's verification for that flavor without any warning. +- Flavor names must be strings. A bare `123:` is read as a YAML integer key and rejected — quote + it (`"123":`) if you actually want that literal name. +- `strict`, if present, must be `true` or `false`. Select a flavor before building: ```bash -dart run permission_handler_apple:select prod # --list shows what is declared +dart run permission_handler_apple:select prod +``` + +``` +Usage: dart run permission_handler_apple:select + + --list Show the flavors declared in permission_handler.yaml. + --app= App directory (defaults to the current directory). + --derived-data= Custom DerivedData location, matching xcodebuild's + -derivedDataPath. +``` + +`--app` and `--derived-data` matter mainly in CI, where the command may not run from inside the app +directory and DerivedData may live somewhere other than `~/Library/Developer/Xcode/DerivedData`. + +```bash flutter run --flavor prod ``` @@ -84,10 +109,15 @@ Foundation has no support for it and a manifest cannot import libraries — so ` your config into a generated `ios/Flutter/permission_handler.resolved.json` that the manifest and the build phase read with their native JSON parsers. Never edit that file; if you change `permission_handler.yaml`, re-run `select`. Building with a translation older than the YAML fails -rather than using stale permissions. +rather than using stale permissions. The file is plain JSON, so it also doubles as a quick way to +see exactly what `select` resolved, without waiting for a build. -With `strict: true` (the default) a build whose flavor cannot be determined compiles no -permissions at all, rather than falling back to the union of every flavor. +With `strict: true` (the default) a build whose flavor cannot be determined compiles no permissions +at all, rather than falling back to the union of every flavor. Set `strict: false` to opt into that +fallback instead: the manifest merges every `Info.plist` it can discover, with a warning, the same +as an app with no `permission_handler.yaml` at all. Treat it as a landing pad while adopting +flavors gradually, not a long-term setting — it is the exact leak per-flavor configuration exists +to close. #### Fail the build on a stale selection @@ -130,7 +160,7 @@ than exporting them, then restart Xcode. | `PERMISSION_HANDLER_INFO_PLIST` | A `:`-separated list of `Info.plist` paths. When set, replaces automatic discovery entirely. | | `PERMISSION_HANDLER_FLAVOR` | The active flavor, overriding the one recorded by `select`. Changing it still needs the caches cleared. | | `PERMISSION_HANDLER_CONFIG` | Path to `permission_handler.yaml`, for builds that cannot locate the app automatically. | -| `PERMISSION_HANDLER_VERBOSE` | Set to `1` to log the detected app root, the `Info.plist` files used, and the resolved macros. | +| `PERMISSION_HANDLER_VERBOSE` | Set to `1` to log the `Info.plist` files used, the app root or active flavor, and the resolved macros. | ### Builds started from Xcode.app @@ -155,8 +185,9 @@ PERMISSION_HANDLER_VERBOSE=1 swift package --manifest-cache none \ dump-package > /dev/null ``` -That prints the app root, the `Info.plist` files used and the resolved macros, and is the quickest -way to check what your app will actually be built with. +That prints the `Info.plist` files that were used, the app root or the active flavor depending on +whether you declared one, and the value every `PERMISSION_*` macro resolved to — the quickest way +to check what your app will actually be built with. ## Issues From 6605dbb30ada13486664bb4b7709d9c6bf7774ba Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Tue, 11 Aug 2026 08:58:23 +0200 Subject: [PATCH 24/26] Updates version number --- permission_handler/CHANGELOG.md | 4 ++ permission_handler/pubspec.yaml | 2 +- permission_handler_apple/CHANGELOG.md | 88 ++++++++++++++------------- permission_handler_apple/pubspec.yaml | 2 +- 4 files changed, 52 insertions(+), 44 deletions(-) diff --git a/permission_handler/CHANGELOG.md b/permission_handler/CHANGELOG.md index 4303f3752..534d08f57 100644 --- a/permission_handler/CHANGELOG.md +++ b/permission_handler/CHANGELOG.md @@ -1,3 +1,7 @@ +## 13.0.1 + +- Updates documentation on how to configure the permission handler on macOS / iOS. + ## 13.0.0 - **BREAKING CHANGE:** , android compilesdk now set to version `compileSdkVersion 37` diff --git a/permission_handler/pubspec.yaml b/permission_handler/pubspec.yaml index 6a50c7ade..26c35ae46 100644 --- a/permission_handler/pubspec.yaml +++ b/permission_handler/pubspec.yaml @@ -2,7 +2,7 @@ name: permission_handler description: Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions. repository: https://github.com/baseflow/flutter-permission-handler issue_tracker: https://github.com/Baseflow/flutter-permission-handler/issues -version: 13.0.0 +version: 13.0.1 environment: sdk: ^3.6.0 diff --git a/permission_handler_apple/CHANGELOG.md b/permission_handler_apple/CHANGELOG.md index 578ef76c6..aab215b32 100644 --- a/permission_handler_apple/CHANGELOG.md +++ b/permission_handler_apple/CHANGELOG.md @@ -1,12 +1,16 @@ +## 9.6.1 + +- Fixes small mistakes in the README.md documentation. + ## 9.6.0 -* Adds opt-in per-flavor permissions for Swift Package Manager builds. An app can declare a +- Adds opt-in per-flavor permissions for Swift Package Manager builds. An app can declare a `permission_handler.yaml` next to its `pubspec.yaml` mapping each flavor to the `Info.plist` that defines it, and only the selected flavor's permissions are compiled in — a permission declared by `dev` can no longer reach a `prod` binary. Without this file the previous behaviour is unchanged. This is a Swift Package Manager feature: CocoaPods builds set the `PERMISSION_*` macros from the `Podfile` and are unaffected, including by the build phase below. -* Adds `dart run permission_handler_apple:select `, which records the active flavor and +- Adds `dart run permission_handler_apple:select `, which records the active flavor and clears the caches that would otherwise keep serving the previously resolved permissions. Xcode does not re-evaluate a package manifest when an environment variable or the selection changes, so this step is required when switching flavors. `select` is also the only YAML reader: a Swift @@ -14,155 +18,155 @@ `ios/Flutter/permission_handler.resolved.json` (gitignore it) that the manifest and the build phase read with their native JSON parsers. A translation older than the YAML fails the build instead of shipping stale permissions. -* Adds `tool/verify_flavor_selection.sh`, a build phase for the app target that fails the build when +- Adds `tool/verify_flavor_selection.sh`, a build phase for the app target that fails the build when the selected flavor does not match the configuration being built. A package manifest is evaluated once and cannot detect that its own result went stale, so this is what catches a forgotten `select`. It is a no-op without a `permission_handler.yaml` and on CocoaPods builds. -* Adds the `PERMISSION_HANDLER_FLAVOR` and `PERMISSION_HANDLER_CONFIG` environment variables to set +- Adds the `PERMISSION_HANDLER_FLAVOR` and `PERMISSION_HANDLER_CONFIG` environment variables to set the active flavor and the configuration file location explicitly. -* `select` validates the configuration up front and refuses anything ambiguous: a build +- `select` validates the configuration up front and refuses anything ambiguous: a build configuration claimed by more than one flavor, a `configurations` that is not a list, a non-string flavor name, or a `strict` that is not a boolean. Being the only reader of the YAML, it is the only place where these can be reported at all. ## 9.5.1 -* Fixes the Swift Package Manager permission auto-detection, which failed to find the host app's +- Fixes the Swift Package Manager permission auto-detection, which failed to find the host app's `Info.plist` and silently compiled out every permission. Apps hit this in two ways: the manifest only ever looked at `ios/Runner/Info.plist`, so build-configuration or flavor specific plists such as `Info-Debug.plist` were never seen ([#1548](https://github.com/Baseflow/flutter-permission-handler/issues/1548)), and the app-root lookup could walk past the app entirely. `Info.plist` locations are now resolved from `INFOPLIST_FILE` in the Xcode project and any `.xcconfig` files, with a scan of `ios/` as a fallback, and the usage description keys found across them are merged. -* Adds the `PERMISSION_HANDLER_INFO_PLIST` environment variable, which points the manifest at one or +- Adds the `PERMISSION_HANDLER_INFO_PLIST` environment variable, which points the manifest at one or more `Info.plist` files and replaces automatic discovery. This is required for builds started from Xcode.app, which run with `/` as their working directory and cannot be detected automatically. -* Adds the `PERMISSION_HANDLER_VERBOSE` environment variable, which logs the app root, the +- Adds the `PERMISSION_HANDLER_VERBOSE` environment variable, which logs the app root, the `Info.plist` files used, and the resolved `PERMISSION_*` macros. -* Emits a warning when no `Info.plist` can be located, instead of silently disabling every +- Emits a warning when no `Info.plist` can be located, instead of silently disabling every permission. Note that Xcode discards Swift package manifest output, so this warning is only visible through the `swift package` command line. ## 9.5.0 -* Adds support for the new Android 17 permission `ACCESS_LOCAL_NETWORK`. +- Adds support for the new Android 17 permission `ACCESS_LOCAL_NETWORK`. ## 9.4.10 -* Fixed Info.plist lookup in Package.swift to auto-apply permissions. -* You may see build log "Plugin permission_handler_apple has a Package.swift for ios but is missing a dependency on FlutterFramework". FlutterFramework hasn't been added intentionally because it requires to bump flutter constraint to >=3.41.0. +- Fixed Info.plist lookup in Package.swift to auto-apply permissions. +- You may see build log "Plugin permission_handler_apple has a Package.swift for ios but is missing a dependency on FlutterFramework". FlutterFramework hasn't been added intentionally because it requires to bump flutter constraint to >=3.41.0. ## 9.4.9 -* Rewrites copyleft code from stackoverflow to fix compliance issue. +- Rewrites copyleft code from stackoverflow to fix compliance issue. ## 9.4.8 -* Adds Swift Package Manager (SPM) support for Flutter 3.24+. Permissions are +- Adds Swift Package Manager (SPM) support for Flutter 3.24+. Permissions are enabled automatically based on usage description keys present in `Info.plist` — no additional configuration required beyond clearing DerivedData once after changes: `rm -rf ~/Library/Developer/Xcode/DerivedData`. -* Moves ObjC sources to SPM-compatible layout (`Sources/permission_handler_apple/`). +- Moves ObjC sources to SPM-compatible layout (`Sources/permission_handler_apple/`). CocoaPods continues to work unchanged. -* Bumps minimum iOS deployment target to 12.0. +- Bumps minimum iOS deployment target to 12.0. ## 9.4.7 -* Increases minimum supported Flutter version to 3.3.0, and removes code only +- Increases minimum supported Flutter version to 3.3.0, and removes code only required for iOS versions prior to iOS 11. ## 9.4.6 -* Adds the ability to handle `CNAuthorizationStatusLimited` introduced in ios18 +- Adds the ability to handle `CNAuthorizationStatusLimited` introduced in ios18 ## 9.4.5 -* Fixes issue #1002, Xcode warning of the unresponsive of main thread when checking isLocationEnabled. +- Fixes issue #1002, Xcode warning of the unresponsive of main thread when checking isLocationEnabled. ## 9.4.4 -* Fixes potentially-nil return type of EventPermissionStrategy#getEntityType. -* * Fixes typo in comment for full calendar access. +- Fixes potentially-nil return type of EventPermissionStrategy#getEntityType. +- - Fixes typo in comment for full calendar access. ## 9.4.3 -* Adds the `PERMISSION_LOCATION_WHENINUSE` macro, which can be used instead of +- Adds the `PERMISSION_LOCATION_WHENINUSE` macro, which can be used instead of the `PERMISSION_LOCATION` macro, and exclusively enables the `requestWhenInUseAuthorization` and remove the `requestAlwaysAuthorization` when requesting location permission. -* Improves error handling when `Info.plist` doesn't contain the correct declarations. -* Adds support for the `NSLocationAlwaysAndWhenInUseUsageDescription` property list +- Improves error handling when `Info.plist` doesn't contain the correct declarations. +- Adds support for the `NSLocationAlwaysAndWhenInUseUsageDescription` property list key. ## 9.4.2 -* Updates the privacy manifest to include the use of the `NSUserDefaults` API. +- Updates the privacy manifest to include the use of the `NSUserDefaults` API. The permission_handler stores a boolean value to track if permission to always access the device location has been requested. ## 9.4.1 -* Adds empty privacy manifest. +- Adds empty privacy manifest. ## 9.4.0 -* Adds a new permission `Permission.backgroundRefresh` to check the background refresh permission status. +- Adds a new permission `Permission.backgroundRefresh` to check the background refresh permission status. ## 9.3.1 -* Updates plist key from `NSPhotoLibraryUsageDescription` to `NSPhotoLibraryAddUsageDescription`. +- Updates plist key from `NSPhotoLibraryUsageDescription` to `NSPhotoLibraryAddUsageDescription`. ## 9.3.0 -* Adds support to request authorization to access SiriKit via the `Permission.assistant` permission. +- Adds support to request authorization to access SiriKit via the `Permission.assistant` permission. ## 9.2.0 -* Adds the support for `Permission.calendarWriteOnly` and `Permission.calendarFullAccess` permissions which are introduced in iOS 17+. +- Adds the support for `Permission.calendarWriteOnly` and `Permission.calendarFullAccess` permissions which are introduced in iOS 17+. ## 9.1.4 -* Adds checking whether Bluetooth service is enabled through `Permission.bluetooth.serviceStatus`. +- Adds checking whether Bluetooth service is enabled through `Permission.bluetooth.serviceStatus`. ## 9.1.3 -* Fixes an issue where the `Permission.location.request()`, `Permission.locationWhenInUse.request()` and `Permission.locationAlways.request()` calls returned `PermissionStatus.denied` regardless of the actual permission status. +- Fixes an issue where the `Permission.location.request()`, `Permission.locationWhenInUse.request()` and `Permission.locationAlways.request()` calls returned `PermissionStatus.denied` regardless of the actual permission status. ## 9.1.2 -* Fixes an issue where the `Permission.locationAlways.request()` call hangs when the application was granted "Allow once" permissions for fetching location coordinates. +- Fixes an issue where the `Permission.locationAlways.request()` call hangs when the application was granted "Allow once" permissions for fetching location coordinates. ## 9.1.1 -* Adds the new Android 13 permission "BODY_SENSORS_BACKGROUND" to PermissionHandlerEnums.h. +- Adds the new Android 13 permission "BODY_SENSORS_BACKGROUND" to PermissionHandlerEnums.h. ## 9.1.0 -* Adds the "Provisional" permission status which is introduced in iOS 12+. +- Adds the "Provisional" permission status which is introduced in iOS 12+. ## 9.0.8 -* Adds missing return statement causing the permission_handler to freeze when already requesting permissions. +- Adds missing return statement causing the permission_handler to freeze when already requesting permissions. ## 9.0.7 -* Adds new Android 13 permissions "SCHEDULE_EXACT_ALARM, READ_MEDIA_IMAGES, READ_MEDIA_VIDEO and READ_MEDIA_AUDIO" to PermissionHandlerEnums.h +- Adds new Android 13 permissions "SCHEDULE_EXACT_ALARM, READ_MEDIA_IMAGES, READ_MEDIA_VIDEO and READ_MEDIA_AUDIO" to PermissionHandlerEnums.h ## 9.0.6 -* Prevents appearing popup that asks to turn on Bluetooth on iOS +- Prevents appearing popup that asks to turn on Bluetooth on iOS ## 9.0.5 -* Adds new Android 13 NEARBY_WIFI_DEVICES permission to PermissionHandlerEnums.h +- Adds new Android 13 NEARBY_WIFI_DEVICES permission to PermissionHandlerEnums.h ## 9.0.4 -* Adds flag inside `UserDefaults` to save whether `locationAlways` has already been requested and prevent further requests, which would be left unanswered by the system. +- Adds flag inside `UserDefaults` to save whether `locationAlways` has already been requested and prevent further requests, which would be left unanswered by the system. ## 9.0.3 -* Ensures a request for `locationAlways` permission returns a result unblocking the permission request and preventing the `ERROR_ALREADY_REQUESTING_PERMISSIONS` error for subsequent permission requests. +- Ensures a request for `locationAlways` permission returns a result unblocking the permission request and preventing the `ERROR_ALREADY_REQUESTING_PERMISSIONS` error for subsequent permission requests. ## 9.0.2 -* Moves Apple implementation into its own package. +- Moves Apple implementation into its own package. diff --git a/permission_handler_apple/pubspec.yaml b/permission_handler_apple/pubspec.yaml index 395984963..6bacce0b4 100644 --- a/permission_handler_apple/pubspec.yaml +++ b/permission_handler_apple/pubspec.yaml @@ -2,7 +2,7 @@ name: permission_handler_apple description: Permission plugin for Flutter. This plugin provides the iOS API to request and check permissions. repository: https://github.com/baseflow/flutter-permission-handler issue_tracker: https://github.com/Baseflow/flutter-permission-handler/issues -version: 9.6.0 +version: 9.6.1 environment: sdk: ^3.6.0 From 8c64b009bb32dfc3c73b98bf3bd4ae891dfeba4f Mon Sep 17 00:00:00 2001 From: TetrixGauss Date: Fri, 4 Sep 2026 12:56:57 +0300 Subject: [PATCH 25/26] Fix/android ask every time permanently denied (#1559) * Fix Android status reporting permanentlyDenied after "Ask every time" On Android 11+, selecting "Ask every time" in the app settings revokes the permission as a one-time permission, which clears FLAG_PERMISSION_USER_SET. shouldShowRequestPermissionRationale() then returns false, exactly as for a permission that was never requested or one that is permanently denied. The plugin combined that with a "was denied before" flag in SharedPreferences that was never cleared, so `status` kept reporting permanentlyDenied even though the OS would show the request dialog again (Baseflow#1206). - A status check now always reports `denied` for a denied runtime permission; Android offers no API to tell the three states apart. - `request()` resolves permanentlyDenied from the change of the rationale flag across the request (true -> false means the second denial), falling back to the stored flag when the OS resolved the request without a dialog. - The stored flag is cleared whenever the permission is observed granted. - Docs, README and changelogs updated; versions bumped. Verified on an Android 16 emulator with the example app: the old build reports permanentlyDenied after "Ask every time", the fixed build reports denied and shows the dialog on the next request. Co-Authored-By: Claude Fable 5.1 * Add app adoption guide for the Android permanently-denied fix Co-Authored-By: Claude Fable 5.1 * Add app adoption guide for the Android permanently-denied fix Co-Authored-By: Claude Fable 5.1 --------- Co-authored-by: Claude Fable 5.1 --- ANDROID_PERMANENTLY_DENIED_FIX_GUIDE.md | 216 ++++++++++++++++++ permission_handler/CHANGELOG.md | 6 + permission_handler/README.md | 17 +- permission_handler/pubspec.yaml | 4 +- permission_handler_android/CHANGELOG.md | 7 + .../permissionhandler/PermissionManager.java | 95 ++++++-- .../permissionhandler/PermissionUtils.java | 213 +++++++++-------- permission_handler_android/pubspec.yaml | 2 +- .../CHANGELOG.md | 4 + .../lib/src/permission_status.dart | 40 ++-- .../pubspec.yaml | 2 +- 11 files changed, 476 insertions(+), 130 deletions(-) create mode 100644 ANDROID_PERMANENTLY_DENIED_FIX_GUIDE.md diff --git a/ANDROID_PERMANENTLY_DENIED_FIX_GUIDE.md b/ANDROID_PERMANENTLY_DENIED_FIX_GUIDE.md new file mode 100644 index 000000000..9d4f48f71 --- /dev/null +++ b/ANDROID_PERMANENTLY_DENIED_FIX_GUIDE.md @@ -0,0 +1,216 @@ +# Adopting the Android "permanently denied" fix in an app + +`permission_handler_android` 14.1.0 changes what a status check reports for a +denied permission on Android. This guide explains the change, tells you how to +write permission code that stays correct with it, and how to audit an existing +app for the pattern that the old behavior encouraged. + +## 1. What changed, in one paragraph + +On Android, `Permission.x.status` can no longer return `permanentlyDenied`. +It returns `denied` for every denied runtime permission. Only the result of +`Permission.x.request()` can be `permanentlyDenied`. Android gives apps no way +to tell a permanently denied permission apart from one that was never requested +or one that the user reset to **Ask every time** in the app settings, and the +old heuristic guessed wrong in the last case: an app could get stuck sending +the user to the app settings even though the OS was ready to show the request +dialog again ([#1206](https://github.com/Baseflow/flutter-permission-handler/issues/1206)). +Requesting a permanently denied permission is cheap: the OS resolves it +immediately, without showing a dialog. + +iOS is unchanged: `status` still returns `permanentlyDenied` there. The +request-driven pattern below works on both platforms. + +## 2. Behavior on Android after the fix + +| Situation on the device | `status` returns | `request()` returns | +|---|---|---| +| Never asked | `denied` | dialog shown. `granted`, or `denied` after "Don't allow" or a dismissal | +| Denied once | `denied` | dialog shown. `granted`, or `permanentlyDenied` after a second "Don't allow" | +| Denied twice (permanently denied) | `denied` | `permanentlyDenied`, immediately, no dialog | +| Reset to "Ask every time" in settings | `denied` | dialog shown. `granted`, or `denied` after "Don't allow" | +| Granted | `granted` | `granted`, immediately, no dialog | + +The row that used to be wrong is the fourth one: `status` reported +`permanentlyDenied` and `request()` showed the dialog anyway. + +## 3. Upgrade + +Take `permission_handler` 13.0.2 or later, which depends on +`permission_handler_android` 14.1.0: + +```yaml +dependencies: + permission_handler: ^13.0.2 +``` + +`permission_handler_android` 14.x compiles against API 37, a requirement +introduced in 14.0.0. If you are coming from 13.x, set `compileSdk` in +`android/app/build.gradle(.kts)`: + +```kotlin +android { + compileSdk = 37 +``` + +Then run `flutter pub get` and confirm the resolved version: + +```bash +flutter pub get +grep -A 3 "^ permission_handler_android:" pubspec.lock +``` + +## 4. Rules for the app's permission code + +1. **Never derive "permanently denied" from `status`.** Use `status` only to + skip the request when the permission is already `granted` or `limited`, and + to decide whether to show your own explainer before asking. +2. **Always call `request()` when you need a permission that is not granted.** + This is safe when it is permanently denied: no dialog appears and the result + comes back immediately. +3. **Branch on the `request()` result.** + - `granted` or `limited`: proceed. + - `denied`: the user declined just now. Do not re-prompt immediately, offer + the feature again on the next user action. + - `permanentlyDenied`: offer an "Open settings" action via + `openAppSettings()`. +4. **Never persist a `permanentlyDenied` verdict.** Not to disk, and not as a + long-lived flag that blocks future requests. After the app resumes from + Settings, read `status` again; if it is not `granted`, the next user action + calls `request()` again. +5. **Do not use `shouldShowRequestRationale` to detect permanent denial.** It + is `false` for "never asked", "Ask every time" and "permanently denied" + alike. +6. **Do not gate `request()` on your own `canRequest` style flag that excludes + `permanentlyDenied`.** That is exactly the pattern that keeps users stuck in + Settings. + +Reference pattern: + +```dart +Future ensureLocation() async { + var status = await Permission.locationWhenInUse.status; + if (status.isGranted || status.isLimited) return true; + + // Optionally show your own explainer here when status.isDenied. + + status = await Permission.locationWhenInUse.request(); + if (status.isGranted || status.isLimited) return true; + + if (status.isPermanentlyDenied) { + // Offer "Open settings". Do not remember this decision. + await showOpenSettingsPrompt(); + return false; + } + // Denied just now: stay quiet, let the user try again later. + return false; +} +``` + +## 5. Auditing an existing app + +Search the app for `isPermanentlyDenied` and `permanentlyDenied`. Keep every +usage that is fed by the result of a `request()` call, and rewrite the ones fed +by a `status` read. These are the three shapes that show up most: + +**A "can request" helper that excludes permanently denied.** This is the one +that keeps a feature from ever asking again: + +```dart +// Before: a permanently denied permission — and, before the fix, one reset to +// "Ask every time" — is never requested again. +bool get canRequest => + this == PermissionStatus.denied || this == PermissionStatus.restricted; + +// After: requesting is always safe when the permission is not granted. +bool get canRequest => + this != PermissionStatus.granted && this != PermissionStatus.limited; +``` + +**An early return on a status read.** Code that refreshes statuses and bails +out to a "go to settings" screen when one of them is `permanentlyDenied` is +dead code on Android now. Drop the early return and let the following +`request()` decide; it resolves immediately for a permanently denied permission +and yields the same outcome. + +**A "already asked" guard that survives a trip to Settings.** A flag like +`_permissionsRequested`, set once per screen visit, stops the second request. +When the user returns from Settings after choosing **Ask every time**, the +screen re-initializes, sees `denied`, skips the request and shows the settings +prompt again instead of the system dialog. Reset the flag before calling +`openAppSettings()`, or when the app resumes: + +```dart +/// Allow the next request to show the system dialog again, for example after +/// the user returns from the app settings. +void allowPermissionRePrompt() => _permissionsRequested = false; +``` + +Cached status values are fine as long as they are refreshed. On Android a +refresh never yields `permanentlyDenied`, so a stored `permanentlyDenied` is +only true right after a request stored its result and drops back to `denied` on +the next refresh. That is intended: the user may have changed the permission in +Settings in the meantime. + +## 6. Verify on a device or emulator + +Use a debug build of the app. `` is the application id. + +1. Fresh install. Trigger the feature, tap **Don't allow**. The app must + behave as "denied" (no settings prompt). +2. Trigger it again, tap **Don't allow**. The request result is + `permanentlyDenied`; the app shows its "Open settings" prompt. + Check the flags: + + ```bash + adb shell dumpsys package | grep -A1 "ACCESS_FINE_LOCATION: granted" + # expect: granted=false, flags=[ USER_SET|USER_FIXED|... ] + ``` + +3. Open Settings > Apps > the app > Permissions > Location, choose + **Ask every time**, return to the app. + + ```bash + adb shell dumpsys package | grep -A1 "ACCESS_FINE_LOCATION: granted" + # expect: granted=false, flags=[ ...|ONE_TIME ] (no USER_SET, no USER_FIXED) + ``` + + Trigger the feature: the system dialog must appear. Before the fix the app + went straight to its "Open settings" prompt. +4. Tap **While using the app**: the feature works and `status` is `granted`. + +To reset the permission between runs without reinstalling: + +```bash +adb shell pm revoke android.permission.ACCESS_FINE_LOCATION +adb shell pm revoke android.permission.ACCESS_COARSE_LOCATION +adb shell pm clear-permission-flags android.permission.ACCESS_FINE_LOCATION user-set user-fixed +adb shell pm clear-permission-flags android.permission.ACCESS_COARSE_LOCATION user-set user-fixed +``` + +The `ONE_TIME` flag cannot be cleared from the shell; choose **Don't allow** in +Settings first, then run the commands above. + +## 7. If an app cannot upgrade yet + +The unfixed plugin is wrong only about `status`. Apps on an older version can +avoid the stuck state by following the rules in section 4 today: ignore +`status.isPermanentlyDenied` on Android, always call `request()` and act on its +result. With the old plugin a request after **Ask every time** shows the +dialog and reports `granted` or `denied` correctly; only the pre-check was +lying. + +## 8. Background + +- Root cause: choosing **Ask every time** revokes the permission as a one-time + permission, which clears `FLAG_PERMISSION_USER_SET`. + `shouldShowRequestPermissionRationale()` returns exactly that flag, so it is + `false`, and the old plugin combined that with a never-cleared + "was denied before" flag in `SharedPreferences`. +- A second denial is now detected from the change of + `shouldShowRequestPermissionRationale()` across the request (`true` -> + `false`), so it is reported as `permanentlyDenied` even when the first denial + happened in the app settings. +- Upstream issue: [#1206](https://github.com/Baseflow/flutter-permission-handler/issues/1206). +- See the `permission_handler_android` 14.1.0 entry in its + [CHANGELOG](https://github.com/Baseflow/flutter-permission-handler/blob/main/permission_handler_android/CHANGELOG.md). diff --git a/permission_handler/CHANGELOG.md b/permission_handler/CHANGELOG.md index 534d08f57..46b35adb6 100644 --- a/permission_handler/CHANGELOG.md +++ b/permission_handler/CHANGELOG.md @@ -1,3 +1,9 @@ +## 13.0.2 + +- Updates the README to detect a permanent denial on Android from the result of `request()`, as `status` cannot detect it. +- Updates `permission_handler_android` to version 14.1.0, which fixes `status` reporting `permanentlyDenied` after the user selected "Ask every time" in the Android app settings (see issue [#1206](https://github.com/Baseflow/flutter-permission-handler/issues/1206)). +- Adds the [Android "permanently denied" guide](https://github.com/Baseflow/flutter-permission-handler/blob/main/ANDROID_PERMANENTLY_DENIED_FIX_GUIDE.md), explaining the changed Android behavior, the request-driven pattern to use and how to audit an existing app for code that relies on the old behavior. + ## 13.0.1 - Updates documentation on how to configure the permission handler on macOS / iOS. diff --git a/permission_handler/README.md b/permission_handler/README.md index 2159ae49b..88dca2854 100644 --- a/permission_handler/README.md +++ b/permission_handler/README.md @@ -299,7 +299,9 @@ You can get a `Permission`'s `status`, which is either `granted`, `denied`, `res ```dart var status = await Permission.camera.status; if (status.isDenied) { - // We haven't asked for permission yet or the permission has been denied before, but not permanently. + // We haven't asked for permission yet or the permission has been denied before. + // On Android this also covers a permanently denied permission: the OS does not + // expose the difference without requesting, so call `request()` to find out. } // You can also directly ask permission about its status. @@ -361,14 +363,19 @@ if (await Permission.locationWhenInUse.serviceStatus.isEnabled) { You can also open the app settings: ```dart -if (await Permission.speech.isPermanentlyDenied) { +if (await Permission.speech.request().isPermanentlyDenied) { // The user opted to never again see the permission request dialog for this // app. The only way to change the permission's status now is to let the - // user manually enables it in the system settings. + // user manually enable it in the system settings. openAppSettings(); } ``` +On Android, only the result of `request()` can be `permanentlyDenied`; `status` reports `denied` instead. +Android does not expose whether a permission is permanently denied: a permission that was never requested, one that the user reset to "Ask every time" in the app settings and a permanently denied one all look the same to the app. +Requesting a permanently denied permission is cheap, the OS resolves it immediately without showing a dialog. +The [Android "permanently denied" guide](https://github.com/Baseflow/flutter-permission-handler/blob/main/ANDROID_PERMANENTLY_DENIED_FIX_GUIDE.md) covers this in full: what each status means on Android, the request-driven pattern to use instead, and how to audit an existing app for code that relies on the old behavior. + On Android, you can show a rationale for using permission: ```dart @@ -397,6 +404,10 @@ This will then bring up another permission popup asking you to `Keep Only While ## FAQ +### `Permission.status` never returns `permanentlyDenied` on Android. What can I do? + +That is intentional as of `permission_handler_android` 14.1.0. Android does not expose whether a permission is permanently denied, so a status check reports `denied` for every denied runtime permission. Call `request()` and branch on its result: it returns `permanentlyDenied` without showing a dialog when the permission really is permanently denied. See the [Android "permanently denied" guide](https://github.com/Baseflow/flutter-permission-handler/blob/main/ANDROID_PERMANENTLY_DENIED_FIX_GUIDE.md) for the full behavior table, the pattern to use and an audit checklist for existing apps. + ### Requesting "storage" permissions always returns "denied" on Android 13+. What can I do? On Android, the `Permission.storage` permission is linked to the Android `READ_EXTERNAL_STORAGE` and `WRITE_EXTERNAL_STORAGE` permissions. Starting from Android 10 (API 29) the `READ_EXTERNAL_STORAGE` and `WRITE_EXTERNAL_STORAGE` permissions have been marked deprecated and have been fully removed/disabled since Android 13 (API 33). diff --git a/permission_handler/pubspec.yaml b/permission_handler/pubspec.yaml index 26c35ae46..4811c78e7 100644 --- a/permission_handler/pubspec.yaml +++ b/permission_handler/pubspec.yaml @@ -2,7 +2,7 @@ name: permission_handler description: Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions. repository: https://github.com/baseflow/flutter-permission-handler issue_tracker: https://github.com/Baseflow/flutter-permission-handler/issues -version: 13.0.1 +version: 13.0.2 environment: sdk: ^3.6.0 @@ -25,7 +25,7 @@ dependencies: flutter: sdk: flutter meta: ^1.7.0 - permission_handler_android: ^14.0.0 + permission_handler_android: ^14.1.0 permission_handler_apple: ^9.5.0 permission_handler_html: ^0.1.4+0 permission_handler_windows: ^0.2.2 diff --git a/permission_handler_android/CHANGELOG.md b/permission_handler_android/CHANGELOG.md index b895ea7ed..a017182ba 100644 --- a/permission_handler_android/CHANGELOG.md +++ b/permission_handler_android/CHANGELOG.md @@ -1,3 +1,10 @@ +## 14.1.0 + +- Fixes `Permission.status` reporting `permanentlyDenied` after the user reset a permanently denied permission to "Ask every time" in the Android app settings (Android 11+). See [#1206](https://github.com/Baseflow/flutter-permission-handler/issues/1206). +- **Behavior change:** on Android, `Permission.status` no longer resolves to `permanentlyDenied`. Android does not expose the difference between a permission that was never requested, one reset to "Ask every time" and one that is permanently denied. Only the result of `Permission.request()` can be `permanentlyDenied`; requesting a permanently denied permission resolves immediately without showing a dialog. +- A second denial is now detected from the change of `shouldShowRequestPermissionRationale` across the request, so it is reported as `permanentlyDenied` even when the first denial happened in the app settings. +- Dismissing the very first request dialog is reported as `denied` again instead of `permanentlyDenied`. + ## 14.0.0 - **BREAKING CHANGES:** When updating to version 14.0.0 make sure to also set the `compileSdkVersion` in the `app/build.gradle` file to `37`. diff --git a/permission_handler_android/android/src/main/java/com/baseflow/permissionhandler/PermissionManager.java b/permission_handler_android/android/src/main/java/com/baseflow/permissionhandler/PermissionManager.java index cee61a42b..462d4aaf2 100644 --- a/permission_handler_android/android/src/main/java/com/baseflow/permissionhandler/PermissionManager.java +++ b/permission_handler_android/android/src/main/java/com/baseflow/permissionhandler/PermissionManager.java @@ -59,6 +59,16 @@ final class PermissionManager implements PluginRegistry.ActivityResultListener, * {@link this#requestPermissions(List, RequestPermissionsSuccessCallback, ErrorCallback)}. */ private Map requestResults; + /** + * The output of {@link ActivityCompat#shouldShowRequestPermissionRationale(Activity, String)} + * for every runtime permission requested through + * {@link this#requestPermissions(List, RequestPermissionsSuccessCallback, ErrorCallback)}, + * captured right before the request was made. + *

+ * {@link PermissionUtils#toPermissionStatus(Activity, String, int, Boolean)} compares it with + * the value after the request to detect a permission that just became permanently denied. + */ + private Map shouldShowRationaleBeforeRequest; public PermissionManager(@NonNull Context context) { this.context = context; @@ -187,7 +197,7 @@ public boolean onRequestPermissionsResult( if (calendarWriteIndex >= 0) { final int writeGrantResult = grantResults[calendarWriteIndex]; final @PermissionConstants.PermissionStatus int writeStatus = - PermissionUtils.toPermissionStatus(this.activity, Manifest.permission.WRITE_CALENDAR, writeGrantResult); + toRequestedPermissionStatus(Manifest.permission.WRITE_CALENDAR, writeGrantResult); requestResults.put(PermissionConstants.PERMISSION_GROUP_CALENDAR_WRITE_ONLY, writeStatus); // WRITE + READ -> FULL ACCESS. @@ -195,7 +205,7 @@ public boolean onRequestPermissionsResult( if (calendarReadIndex >= 0) { final int readGrantResult = grantResults[calendarReadIndex]; final @PermissionConstants.PermissionStatus int readStatus = - PermissionUtils.toPermissionStatus(this.activity, Manifest.permission.READ_CALENDAR, readGrantResult); + toRequestedPermissionStatus(Manifest.permission.READ_CALENDAR, readGrantResult); final @PermissionConstants.PermissionStatus int fullAccessStatus = strictestStatus(writeStatus, readStatus); requestResults.put(PermissionConstants.PERMISSION_GROUP_CALENDAR_FULL_ACCESS, fullAccessStatus); // Support deprecated CALENDAR permission. @@ -221,30 +231,30 @@ public boolean onRequestPermissionsResult( if (permission == PermissionConstants.PERMISSION_GROUP_PHONE) { @Nullable @PermissionConstants.PermissionStatus Integer previousResult = requestResults.get(PermissionConstants.PERMISSION_GROUP_PHONE); - @PermissionConstants.PermissionStatus int newResult = PermissionUtils.toPermissionStatus(this.activity, permissionName, result); + @PermissionConstants.PermissionStatus int newResult = toRequestedPermissionStatus(permissionName, result); @Nullable @PermissionConstants.PermissionStatus Integer strictestStatus = strictestStatus(previousResult, newResult); requestResults.put(PermissionConstants.PERMISSION_GROUP_PHONE, strictestStatus); } else if (permission == PermissionConstants.PERMISSION_GROUP_MICROPHONE) { if (!requestResults.containsKey(PermissionConstants.PERMISSION_GROUP_MICROPHONE)) { requestResults.put( PermissionConstants.PERMISSION_GROUP_MICROPHONE, - PermissionUtils.toPermissionStatus(this.activity, permissionName, result)); + toRequestedPermissionStatus(permissionName, result)); } if (!requestResults.containsKey(PermissionConstants.PERMISSION_GROUP_SPEECH)) { requestResults.put( PermissionConstants.PERMISSION_GROUP_SPEECH, - PermissionUtils.toPermissionStatus(this.activity, permissionName, result)); + toRequestedPermissionStatus(permissionName, result)); } } else if (permission == PermissionConstants.PERMISSION_GROUP_LOCATION_ALWAYS) { @PermissionConstants.PermissionStatus int permissionStatus = - PermissionUtils.toPermissionStatus(this.activity, permissionName, result); + toRequestedPermissionStatus(permissionName, result); if (!requestResults.containsKey(PermissionConstants.PERMISSION_GROUP_LOCATION_ALWAYS)) { requestResults.put(PermissionConstants.PERMISSION_GROUP_LOCATION_ALWAYS, permissionStatus); } } else if (permission == PermissionConstants.PERMISSION_GROUP_LOCATION) { @PermissionConstants.PermissionStatus int permissionStatus = - PermissionUtils.toPermissionStatus(this.activity, permissionName, result); + toRequestedPermissionStatus(permissionName, result); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (!requestResults.containsKey(PermissionConstants.PERMISSION_GROUP_LOCATION_ALWAYS) || @@ -269,13 +279,17 @@ public boolean onRequestPermissionsResult( // [grantResults] can only contain PermissionConstants.PERMISSION_STATUS_GRANTED or PermissionConstants.PERMISSION_STATUS_DENIED status. // But these permissions can have status PermissionConstants.PERMISSION_STATUS_LIMITED, so we need to recheck status } else if (permission == PermissionConstants.PERMISSION_GROUP_PHOTOS || permission == PermissionConstants.PERMISSION_GROUP_VIDEOS) { - requestResults.put( - permission, - determinePermissionStatus(permission)); + // A status check cannot tell 'denied' from 'permanently denied', so only reuse it + // for the 'granted' and 'limited' statuses and resolve a denial from the request. + @PermissionConstants.PermissionStatus int permissionStatus = determinePermissionStatus(permission); + if (permissionStatus == PermissionConstants.PERMISSION_STATUS_DENIED) { + permissionStatus = toRequestedPermissionStatus(permissionName, result); + } + requestResults.put(permission, permissionStatus); } else if (!requestResults.containsKey(permission)) { requestResults.put( permission, - PermissionUtils.toPermissionStatus(this.activity, permissionName, result)); + toRequestedPermissionStatus(permissionName, result)); } } @@ -291,10 +305,14 @@ public boolean onRequestPermissionsResult( /** * Determines the permission status of the provided permission. *

- * To distinguish between a status of 'denied' and a status of 'permanently denied', the plugin - * needs access to an activity. If `this.activity` is null, for example when running the - * application in the background, the resolved status will be 'denied' for both 'denied' and - * 'permanently denied'. + * On Android a status check can never resolve to 'permanently denied': the OS reports a + * permanently denied runtime permission exactly like one that was never requested, or one that + * the user reset to 'Ask every time' in the app settings (Android 11+). A denied runtime + * permission is therefore always reported as 'denied'. Only + * {@link this#requestPermissions(List, RequestPermissionsSuccessCallback, ErrorCallback)} can + * resolve to 'permanently denied', see + * {@link PermissionUtils#toPermissionStatus(Activity, String, int, Boolean)}. Requesting a + * permanently denied permission is cheap as the OS resolves it immediately without a dialog. * * @param permission the permission for which to determine the status. * @param successCallback the callback to which the resolved status must be supplied. @@ -355,6 +373,7 @@ void requestPermissions( this.successCallback = successCallback; this.requestResults = new HashMap<>(); + this.shouldShowRationaleBeforeRequest = new HashMap<>(); this.pendingRequestCount = 0; // sanity check ArrayList permissionsToRequest = new ArrayList<>(); @@ -436,6 +455,13 @@ void requestPermissions( // Request runtime permissions. if (permissionsToRequest.size() > 0) { final String[] requestPermissions = permissionsToRequest.toArray(new String[0]); + // Capture whether the rationale should be shown before requesting. See + // PermissionUtils.toPermissionStatus for how this is used to resolve the request result. + for (String permissionName : requestPermissions) { + shouldShowRationaleBeforeRequest.put( + permissionName, + ActivityCompat.shouldShowRequestPermissionRationale(activity, permissionName)); + } ActivityCompat.requestPermissions( activity, requestPermissions, @@ -448,6 +474,26 @@ void requestPermissions( } } + /** + * Resolves the status of a runtime permission from the result of a permission request. + * + * @param permissionName the name of the requested permission. + * @param grantResult the grant result reported by the OS for this permission. + * @return the resolved permission status, see + * {@link PermissionUtils#toPermissionStatus(Activity, String, int, Boolean)}. + */ + @PermissionConstants.PermissionStatus + private int toRequestedPermissionStatus(final String permissionName, final int grantResult) { + final Boolean shouldShowRationaleBefore = shouldShowRationaleBeforeRequest == null + ? null + : shouldShowRationaleBeforeRequest.get(permissionName); + return PermissionUtils.toPermissionStatus( + activity, + permissionName, + grantResult, + shouldShowRationaleBefore); + } + @PermissionConstants.PermissionStatus private int determinePermissionStatus(final @PermissionConstants.PermissionGroup int permission) { @@ -580,14 +626,19 @@ private int determinePermissionStatus(final @PermissionConstants.PermissionGroup if (permissionStatusLimited == PackageManager.PERMISSION_GRANTED && permissionStatus == PackageManager.PERMISSION_DENIED) { permissionStatuses.add(PermissionConstants.PERMISSION_STATUS_LIMITED); } else if (permissionStatus == PackageManager.PERMISSION_GRANTED) { + PermissionUtils.clearPermissionDenied(context, name); permissionStatuses.add(PermissionConstants.PERMISSION_STATUS_GRANTED); - }else { - permissionStatuses.add(PermissionUtils.determineDeniedVariant(activity, name)); + } else { + // See checkPermissionStatus: a status check cannot detect 'permanently denied'. + permissionStatuses.add(PermissionConstants.PERMISSION_STATUS_DENIED); } - }else { + } else { final int permissionStatus = ContextCompat.checkSelfPermission(context, name); - if (permissionStatus != PackageManager.PERMISSION_GRANTED) { - permissionStatuses.add(PermissionUtils.determineDeniedVariant(activity, name)); + if (permissionStatus == PackageManager.PERMISSION_GRANTED) { + PermissionUtils.clearPermissionDenied(context, name); + } else { + // See checkPermissionStatus: a status check cannot detect 'permanently denied'. + permissionStatuses.add(PermissionConstants.PERMISSION_STATUS_DENIED); } } } @@ -667,9 +718,11 @@ private int checkNotificationPermissionStatus() { final int status = context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS); if (status == PackageManager.PERMISSION_GRANTED) { + PermissionUtils.clearPermissionDenied(context, Manifest.permission.POST_NOTIFICATIONS); return PermissionConstants.PERMISSION_STATUS_GRANTED; } - return PermissionUtils.determineDeniedVariant(activity, Manifest.permission.POST_NOTIFICATIONS); + // See checkPermissionStatus: a status check cannot detect 'permanently denied'. + return PermissionConstants.PERMISSION_STATUS_DENIED; } @PermissionConstants.PermissionStatus diff --git a/permission_handler_android/android/src/main/java/com/baseflow/permissionhandler/PermissionUtils.java b/permission_handler_android/android/src/main/java/com/baseflow/permissionhandler/PermissionUtils.java index 35cc35e2d..14ab4e9fc 100644 --- a/permission_handler_android/android/src/main/java/com/baseflow/permissionhandler/PermissionUtils.java +++ b/permission_handler_android/android/src/main/java/com/baseflow/permissionhandler/PermissionUtils.java @@ -12,7 +12,6 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; import androidx.core.app.ActivityCompat; import java.util.ArrayList; import java.util.Arrays; @@ -678,12 +677,14 @@ private static boolean hasPermissionInManifest( } /** - * Returns a {@link PermissionConstants} for a given permission. + * Converts the result of a runtime permission request into a + * {@link PermissionConstants.PermissionStatus}. *

- * When {@link PackageManager#PERMISSION_DENIED} is received, we do not know if the permission was - * denied permanently. The OS does not tell us whether the user dismissed the dialog or pressed - * 'deny'. Therefore, we need a more sophisticated (read: hacky) approach to determine whether the - * permission status is {@link PermissionConstants#PERMISSION_STATUS_DENIED} or + * When {@link PackageManager#PERMISSION_DENIED} is received, the OS does not tell us whether the + * user denied the request, dismissed the dialog, or whether the dialog was never shown because + * the permission is permanently denied. Therefore, we need a more sophisticated (read: hacky) + * approach to determine whether the permission status is + * {@link PermissionConstants#PERMISSION_STATUS_DENIED} or * {@link PermissionConstants#PERMISSION_STATUS_NEVER_ASK_AGAIN}. *

* The OS behavior has been researched experimentally and is displayed in the following diagrams: @@ -700,37 +701,63 @@ private static boolean hasPermissionInManifest( * │ │ * ┌─▼────────┴┐ ┌────────────────────────────────┐ * │Denied once├────────►Denied twice(permanently denied)│ - * └──▲┌───────┘ Denied └────────────────────────────────┘ - * └┘ - * Dismissed + * └──▲┌───────┘ Denied └──┬─────────────────────────────┘ + * └┘ │ 'Ask every time' in the app settings (Android 11+) + * Dismissed ▼ + * ┌──────────────┐ + * │Ask every time│ (looks exactly like 'Not asked' to the app) + * └──────────────┘ *

* Scenario table listing output of * {@link ActivityCompat#shouldShowRequestPermissionRationale(Activity, String)}: - * ┌────────────┬────────────────┬─────────┬───────────────────────────────────┬─────────────────────────┐ - * │ Scenario # │ Previous state │ Action │ New state │ 'Show rationale' output │ - * ├────────────┼────────────────┼─────────┼───────────────────────────────────┼─────────────────────────┤ - * │ 1. │ Not asked │ Dismiss │ Not asked │ false │ - * │ 2. │ Not asked │ Deny │ Denied once │ true │ - * │ 3. │ Denied once │ Dismiss │ Denied once │ true │ - * │ 4. │ Denied once │ Deny │ Denied twice (permanently denied) │ false │ - * └────────────┴────────────────┴─────────┴───────────────────────────────────┴─────────────────────────┘ + * ┌────────────┬─────────────────────────────┬────────────────────────────┬───────────────────────────────────┬─────────────────────────┐ + * │ Scenario # │ Previous state │ Action │ New state │ 'Show rationale' output │ + * ├────────────┼─────────────────────────────┼────────────────────────────┼───────────────────────────────────┼─────────────────────────┤ + * │ 1. │ Not asked │ Dismiss │ Not asked │ false │ + * │ 2. │ Not asked │ Deny │ Denied once │ true │ + * │ 3. │ Denied once │ Dismiss │ Denied once │ true │ + * │ 4. │ Denied once │ Deny │ Denied twice (permanently denied) │ false │ + * │ 5. │ Denied twice │ Request (no dialog shown) │ Denied twice │ false │ + * │ 6. │ Granted / Ask every time │ 'Don't allow' in settings │ Denied once │ true │ + * │ 7. │ Any │ 'Ask every time' in │ Ask every time │ false │ + * │ │ │ settings (Android 11+) │ │ │ + * │ 8. │ Ask every time │ Dismiss │ Ask every time │ false │ + * │ 9. │ Ask every time │ Deny │ Denied once │ true │ + * └────────────┴─────────────────────────────┴────────────────────────────┴───────────────────────────────────┴─────────────────────────┘ + *

+ * Selecting 'Ask every time' makes the OS revoke the permission as a one-time permission, which + * clears the {@code FLAG_PERMISSION_USER_SET} flag. As 'show rationale' is exactly that flag + * (as long as the permission is not user fixed), scenarios 1, 4, 5, 7 and 8 all produce + * {@code false} and no public API exposes the difference between them. *

- * To distinguish between scenarios, we can use - * {@link ActivityCompat#shouldShowRequestPermissionRationale(Activity, String)}. If it returns - * true, we can safely return {@link PermissionConstants#PERMISSION_STATUS_DENIED}. To distinguish - * between scenarios 1 and 4, however, we need an extra mechanism. We opt to store a boolean - * stating whether permission has been requested before. Using a combination of checking for - * showing the permission rationale and the boolean, we can distinguish all scenarios and return - * the appropriate permission status. + * A denied request result is therefore resolved from three signals: + *

    + *
  • 'Show rationale' after the request is {@code true} (scenarios 2, 3 and 9): the user can + * be asked again, resolve to 'denied'.
  • + *
  • 'Show rationale' was {@code true} before the request and is {@code false} after it + * (scenario 4): the user denied for the second time, resolve to 'permanently denied'.
  • + *
  • 'Show rationale' was {@code false} before and after the request and the permission was + * denied before (stored in {@link SharedPreferences}, see + * {@link #wasPermissionDeniedBefore(Context, String)}): the OS resolved the request without + * showing a dialog (scenario 5), resolve to 'permanently denied'. A dismissed dialog after the + * user selected 'Ask every time' (scenario 8) is indistinguishable and resolves the same way; + * the next request will show the dialog again.
  • + *
  • Otherwise the user dismissed the very first request (scenario 1), resolve to 'denied'.
  • + *
*

- * Changing permissions via the app info screen, so outside of the application, changes the - * permission state to 'Granted' if the permission is allowed, or 'Denied once' if denied. This - * behavior should not require any additional logic. + * Note that a permission status check without a request can never resolve to 'permanently + * denied' on Android for the same reason, see + * {@link PermissionManager#checkPermissionStatus(int, PermissionManager.CheckPermissionsSuccessCallback)}. * - * @param activity the activity for context - * @param permissionName the name of the permission - * @param grantResult the result of the permission intent. Either - * {@link PackageManager#PERMISSION_DENIED} or {@link PackageManager#PERMISSION_GRANTED}. + * @param activity the activity for context + * @param permissionName the name of the permission + * @param grantResult the result of the permission request. Either + * {@link PackageManager#PERMISSION_DENIED} or + * {@link PackageManager#PERMISSION_GRANTED}. + * @param shouldShowRationaleBeforeRequest the output of + * {@link ActivityCompat#shouldShowRequestPermissionRationale(Activity, String)} + * captured right before the request was made, or + * {@code null} when unknown. * @return {@link PermissionConstants#PERMISSION_STATUS_GRANTED}, * {@link PermissionConstants#PERMISSION_STATUS_DENIED}, or * {@link PermissionConstants#PERMISSION_STATUS_NEVER_ASK_AGAIN}. @@ -739,13 +766,44 @@ private static boolean hasPermissionInManifest( static int toPermissionStatus( final @Nullable Activity activity, final String permissionName, - int grantResult + final int grantResult, + final @Nullable Boolean shouldShowRationaleBeforeRequest ) { - if (grantResult == PackageManager.PERMISSION_DENIED) { - return determineDeniedVariant(activity, permissionName); + if (grantResult != PackageManager.PERMISSION_DENIED) { + if (activity != null) { + clearPermissionDenied(activity, permissionName); + } + return PermissionConstants.PERMISSION_STATUS_GRANTED; } - return PermissionConstants.PERMISSION_STATUS_GRANTED; + if (activity == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return PermissionConstants.PERMISSION_STATUS_DENIED; + } + + final boolean shouldShowRationaleAfterRequest = + ActivityCompat.shouldShowRequestPermissionRationale(activity, permissionName); + + if (shouldShowRationaleAfterRequest) { + // Scenarios 2, 3 and 9: the OS will show the dialog again on the next request. + setPermissionDenied(activity, permissionName); + return PermissionConstants.PERMISSION_STATUS_DENIED; + } + + if (Boolean.TRUE.equals(shouldShowRationaleBeforeRequest)) { + // Scenario 4: the user denied for the second time (or ticked 'Don't ask again' on + // Android 10 and below). + setPermissionDenied(activity, permissionName); + return PermissionConstants.PERMISSION_STATUS_NEVER_ASK_AGAIN; + } + + if (wasPermissionDeniedBefore(activity, permissionName)) { + // Scenario 5 (and 8): the OS did not show a dialog because the permission is + // permanently denied. + return PermissionConstants.PERMISSION_STATUS_NEVER_ASK_AGAIN; + } + + // Scenario 1: the user dismissed the very first request without making a choice. + return PermissionConstants.PERMISSION_STATUS_DENIED; } @NonNull @@ -786,60 +844,6 @@ static Integer strictestStatus( return strictestStatus(statuses); } - /** - * Determines whether a permission was either 'denied' or 'permanently denied'. - *

- * To distinguish between these two variants, the method needs access to an {@link Activity}. - * If the provided activity is null, the result will always be resolved to 'denied'. - * - * @param activity the activity needed to resolve the permission status. - * @param permissionName the name of the permission. - * @return either {@link PermissionConstants#PERMISSION_STATUS_DENIED} or - * {@link PermissionConstants#PERMISSION_STATUS_NEVER_ASK_AGAIN}. - */ - @PermissionConstants.PermissionStatus - static int determineDeniedVariant( - final @Nullable Activity activity, - final String permissionName - ) { - if (activity == null) { - return PermissionConstants.PERMISSION_STATUS_DENIED; - } - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - return PermissionConstants.PERMISSION_STATUS_DENIED; - } - - final boolean wasDeniedBefore = - PermissionUtils.wasPermissionDeniedBefore(activity, permissionName); - final boolean shouldShowRational = - !PermissionUtils.isNeverAskAgainSelected(activity, permissionName); - - //noinspection SimplifiableConditionalExpression - final boolean isDenied = wasDeniedBefore - ? !shouldShowRational - : shouldShowRational; - - if (!wasDeniedBefore && isDenied) { - setPermissionDenied(activity, permissionName); - } - - if (wasDeniedBefore && isDenied) { - return PermissionConstants.PERMISSION_STATUS_NEVER_ASK_AGAIN; - } - - return PermissionConstants.PERMISSION_STATUS_DENIED; - } - - @RequiresApi(api = Build.VERSION_CODES.M) - static boolean isNeverAskAgainSelected( - @NonNull final Activity activity, - final String name - ) { - final boolean shouldShowRequestPermissionRationale = - ActivityCompat.shouldShowRequestPermissionRationale(activity, name); - return !shouldShowRequestPermissionRationale; - } private static String determineBluetoothPermission( Context context, @@ -953,4 +957,35 @@ private static void setPermissionDenied( ) .apply(); } + + /** + * Removes the {@link SharedPreferences} flag that marks the provided permission as denied + * before, if present. + *

+ * Called whenever the permission is observed to be granted, so that a later denial is tracked + * from a clean slate. Without this, a permission that was denied once, granted in the app + * settings and then reset to 'Ask every time' would resolve to 'permanently denied' when the + * user dismisses the next request dialog. + * + * @param context context needed for accessing shared preferences. + * @param permissionName the name of the permission + */ + static void clearPermissionDenied( + final Context context, + final String permissionName + ) { + final SharedPreferences sharedPreferences = + context.getSharedPreferences(permissionName, Context.MODE_PRIVATE); + if ( + !sharedPreferences.contains( + SHARED_PREFERENCES_PERMISSION_WAS_DENIED_BEFORE_KEY + ) + ) { + return; + } + sharedPreferences + .edit() + .remove(SHARED_PREFERENCES_PERMISSION_WAS_DENIED_BEFORE_KEY) + .apply(); + } } diff --git a/permission_handler_android/pubspec.yaml b/permission_handler_android/pubspec.yaml index 725632701..dcae7813d 100644 --- a/permission_handler_android/pubspec.yaml +++ b/permission_handler_android/pubspec.yaml @@ -1,7 +1,7 @@ name: permission_handler_android description: Permission plugin for Flutter. This plugin provides the Android API to request and check permissions. homepage: https://github.com/baseflow/flutter-permission-handler -version: 14.0.0 +version: 14.1.0 environment: sdk: ^3.6.0 diff --git a/permission_handler_platform_interface/CHANGELOG.md b/permission_handler_platform_interface/CHANGELOG.md index c94bf582a..2d1d1a0b3 100644 --- a/permission_handler_platform_interface/CHANGELOG.md +++ b/permission_handler_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.4.1 + +- Documents that on Android `PermissionStatus.permanentlyDenied` is only returned as the result of a request, never by a status check. + ## 4.4.0 - Adds support for the new Android 17 permission `ACCESS_LOCAL_NETWORK` diff --git a/permission_handler_platform_interface/lib/src/permission_status.dart b/permission_handler_platform_interface/lib/src/permission_status.dart index 29e9c94f9..038fd944f 100644 --- a/permission_handler_platform_interface/lib/src/permission_status.dart +++ b/permission_handler_platform_interface/lib/src/permission_status.dart @@ -27,10 +27,15 @@ enum PermissionStatus { /// still change the permission status in the settings. /// /// *On Android:* - /// Android 11+ (API 30+): whether the user denied the permission for a second - /// time. - /// Below Android 11 (API 30): whether the user denied access to the requested - /// feature and selected to never again show a request. + /// Only returned as the result of a request, never by a status check. Android + /// offers no API to tell a permanently denied permission apart from one that + /// was never requested or that the user reset to 'Ask every time' in the app + /// settings, so `status` reports `denied` for all of them. Requesting a + /// permanently denied permission resolves immediately, without showing a + /// dialog, to this status. + /// Android 11+ (API 30+): the user denied the permission for a second time. + /// Below Android 11 (API 30): the user denied access to the requested feature + /// and selected to never again show a request. /// /// *On iOS:* /// If the user has denied access to the requested feature. @@ -96,11 +101,15 @@ extension PermissionStatusGetters on PermissionStatus { /// user may still change the permission status in the settings. /// /// *On Android:* - /// Android 11+ (API 30+): whether the user denied the permission for a second - /// time. - /// Below Android 11 (API 30): whether the user denied access to the requested - /// feature and selected to never again show a request. - /// The user may still change the permission status in the settings. + /// Only returned as the result of a request, never by a status check. Android + /// offers no API to tell a permanently denied permission apart from one that + /// was never requested or that the user reset to 'Ask every time' in the app + /// settings, so `status` reports `denied` for all of them. Requesting a + /// permanently denied permission resolves immediately, without showing a + /// dialog, to this status. + /// Android 11+ (API 30+): the user denied the permission for a second time. + /// Below Android 11 (API 30): the user denied access to the requested feature + /// and selected to never again show a request. /// /// *On iOS:* /// If the user has denied access to the requested feature. @@ -139,10 +148,15 @@ extension FuturePermissionStatusGetters on Future { /// user may still change the permission status in the settings. /// /// *On Android:* - /// Android 11+ (API 30+): whether the user denied the permission for a second - /// time. - /// Below Android 11 (API 30): whether the user denied access to the requested - /// feature and selected to never again show a request. + /// Only returned as the result of a request, never by a status check. Android + /// offers no API to tell a permanently denied permission apart from one that + /// was never requested or that the user reset to 'Ask every time' in the app + /// settings, so `status` reports `denied` for all of them. Requesting a + /// permanently denied permission resolves immediately, without showing a + /// dialog, to this status. + /// Android 11+ (API 30+): the user denied the permission for a second time. + /// Below Android 11 (API 30): the user denied access to the requested feature + /// and selected to never again show a request. /// /// *On iOS:* /// If the user has denied access to the requested feature. diff --git a/permission_handler_platform_interface/pubspec.yaml b/permission_handler_platform_interface/pubspec.yaml index cbc9afddd..88a95a917 100644 --- a/permission_handler_platform_interface/pubspec.yaml +++ b/permission_handler_platform_interface/pubspec.yaml @@ -3,7 +3,7 @@ description: A common platform interface for the permission_handler plugin. homepage: https://github.com/baseflow/flutter-permission-handler/tree/master/permission_handler_platform_interface # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 4.4.0 +version: 4.4.1 environment: sdk: ^3.6.0 From 4d6eda7b3a100816be6b82e664826b5e357e6343 Mon Sep 17 00:00:00 2001 From: Abdullah <89297042+AzazelSensei@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:33:10 +0300 Subject: [PATCH 26/26] docs: clarify which Info.plist key to delete (#1013) (#1560) The iOS CocoaPods setup mixed the calendar Podfile example with a camera plist key, and "corresponding" was easy to read as pointing at step 2. --- permission_handler/README.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/permission_handler/README.md b/permission_handler/README.md index 88dca2854..b47df7b6a 100644 --- a/permission_handler/README.md +++ b/permission_handler/README.md @@ -257,10 +257,19 @@ You must list the permission you want to use in your application: ## dart: PermissionGroup.calendar 'PERMISSION_EVENTS=1', ``` -3. When you **DON'T** need a permission, change its value to `0` e.g. `'PERMISSION_CAMERA=0'` instead of `'PERMISSION_CAMERA=1'` -3. And delete the corresponding permission description in `Info.plist` - e.g. when you don't need camera permission, just delete `'NSCameraUsageDescription'` + Also keep that permission's usage description in `Info.plist` (for calendar: `NSCalendarsUsageDescription`). The example plist above is a complete list so you can copy the keys you actually use. + +3. When you **don't** need a permission, set its macro to `0` **and** delete that same permission's usage description from `Info.plist`. The key to delete is the one for the permission you just disabled, not the calendar example in step 2. + + For example, if you don't need calendar access: + + ```ruby + ## dart: PermissionGroup.calendar + 'PERMISSION_EVENTS=0', + ``` + + Then delete `NSCalendarsUsageDescription` from `Info.plist`. The following lists the relationship between `Permission` and `The key of Info.plist`: