Android has two kinds of permissions. The first kind shows a dialog: camera, contacts, location. The second kind has no dialog at all. To read app usage statistics, draw over other apps, schedule exact alarms or get access to all files, the user must open a specific screen in Settings and flip a toggle. Android calls these "special access" permissions.
For a Flutter developer this means three separate jobs for every permission: find out whether the access is granted, find the right Settings screen for this Android version, and find out what happened when the user comes back. Each permission has its own check, its own intent and its own API level. android_special_permissions puts all of that behind one small API.
What it does
Status for 13 special permissions. status() returns granted, denied or notApplicable. The last value means that the device's API level has no such toggle. The list covers usageAccess, overlay, accessibilityService, notificationListener, ignoreBatteryOptimizations, exactAlarm, installUnknownApps, allFilesAccess, writeSettings, doNotDisturb, fullScreenIntent, pictureInPicture and backgroundLocation.
The most specific Settings screen. openSettings() tries the screen scoped to your package where Android supports it, then the general list, then App info, then Settings home. Every candidate intent is checked with resolveActivity before it is started, so the call does not crash on a device where one screen is missing.
Open, wait, re-check. request() opens the screen, waits until your app resumes, checks the status again and returns it. It uses an ActivityAware binding and a lifecycle observer, survives configuration changes, and fails with activityNotAttached if the activity goes away. When the status is already granted or notApplicable, it returns at once and opens nothing.
Default-app roles. roleStatus() and requestRole() cover the dialer, SMS, home, browser and assistant roles through RoleManager. requestRole() waits for the activity result.
Accessibility-service detection. Android stores enabled accessibility services in ENABLED_ACCESSIBILITY_SERVICES, and the component can be written in a short form (pkg/.Svc) or a long form (pkg/pkg.Svc). The plugin reads both.
No permissions of its own. The plugin declares nothing in its manifest. Your app declares only the permissions behind the accesses it uses.
How to use it
Add it from GitHub:
dependencies: android_special_permissions: git: https://github.com/AbdeldjalilChougui/android_special_permissions The plugin's minSdk is 21. In your app's AndroidManifest.xml, declare the permissions behind the accesses you use, for example PACKAGE_USAGE_STATS for usage access or SYSTEM_ALERT_WINDOW for overlays. Without the declaration, most Settings screens will not list your app. The README has a table with the manifest entry for each permission.
Then, in Dart:
import 'package:android_special_permissions/android_special_permissions.dart'; final permissions = AndroidSpecialPermissions(); Future<void> ensureUsageAccess() async { final status = await permissions.status(SpecialPermission.usageAccess); if (status == SpecialPermissionStatus.denied) { // Opens "Usage access", waits for the user to return, re-checks. final after = await permissions.request(SpecialPermission.usageAccess); print('usage access: ${after.name}'); } } Future<void> batteryAndRoles() async { // Direct "allow" dialog if REQUEST_IGNORE_BATTERY_OPTIMIZATIONS is declared, // otherwise the list screen. await permissions.request(SpecialPermission.ignoreBatteryOptimizations); final sms = await permissions.roleStatus(AndroidRole.sms); if (sms == SpecialPermissionStatus.denied) { await permissions.requestRole(AndroidRole.sms); } } For an accessibility service or a notification listener, pass your service class in serviceClassName, either fully qualified or relative with a leading dot ('.MyAccessibilityService').
Errors are typed:
try { await permissions.request(SpecialPermission.exactAlarm); } on SpecialPermissionException catch (e) { if (e.code == SpecialPermissionErrorCode.activityNotAttached) { // No foreground activity, or it was destroyed while waiting. } } Only one request() or requestRole() can wait at a time. A second one fails with requestInProgress.
What each permission looks like
The README has a full table: the manifest entry, the Android version where the toggle exists, what the user sees, and a note about Google Play. A few rows are worth knowing before you start:
-
overlayexists from API 23. Android 11 and later ignore the package URI and show the full list of apps. -
exactAlarmexists from API 31, and is denied by default on Android 14 and later for new installs. -
allFilesAccessexists from API 30 and needs a declaration on Play; it is meant for file managers, backup, antivirus and similar core uses. -
ignoreBatteryOptimizationsbehaves in two ways.request()shows the direct system dialog, which needsREQUEST_IGNORE_BATTERY_OPTIMIZATIONSand is restricted on Play.openSettings()shows the "Battery optimization" list, which needs no permission. -
fullScreenIntentexists from API 34. It is auto-granted only to calling and alarm apps; other apps must send the user to Settings. - The roles need API 29 or later. Android refuses
home,browserandassistantif the app does not declare the matching intent filters.
The Play notes in that table are the author's summary at the time of writing. The Play policy centre is the authority, so check it before you ship.
Background location
ACCESS_BACKGROUND_LOCATION is a runtime permission, so the plugin only reports its status. request() and openSettings() throw notSupported for it. The README describes the two-step flow with permission_handler: ask for foreground location first, then explain why you need it all the time and request Permission.locationAlways. On Android 11 and later, that second step sends the user to the app's location page. On API 21 to 28 there is no separate background permission, so the status is notApplicable.
Things to know
- Android only. On other platforms every status is
notApplicableand nothing is opened. -
notApplicablemeans "no toggle exists". On API 21 and 22, the permissions behindoverlay,writeSettingsand similar are granted at install time when declared. The plugin does not report that. - Undeclared permissions read as
denied, and their Settings screen may not list your app. -
request()completes on the first resume after the app was paused. If the user comes back by another route, you still get the status at that moment. - Android can kill your process. When some accesses are revoked, Android kills the app (seen for "Install unknown apps" on the emulator; Android documents the same for "Alarms & reminders"). A pending
request()then never completes, so check the status again on the next launch. - OEM Settings apps vary. The fallback chain always ends at App info and then Settings home, but the most specific screen may be missing on some devices.
- Two screens are special. The accessibility details page needs a system permission ordinary apps do not have, so they get the general Accessibility screen. The picture-in-picture screen is not a public SDK action; the plugin tries it only when it resolves, and falls back otherwise.
- Picture-in-picture status is the AppOps state. It does not check that one of your activities declares
supportsPictureInPicture. - Older default-app APIs are not wrapped. Roles work from API 29.
- What was tested. The Dart API has tests with a mocked
MethodChannel, and the pure Kotlin logic (component parsing, API-level rules, intent fallback order, resume tracking) has JUnit tests. The example app has an emulator integration test that flips each access with adb and checks every status, every Settings screen and therequest()round trip. It was tested on the Android emulator (Pixel 10 system image, API 37). It has not yet been tested on a physical phone or on OEM Settings apps.
Why I built it
The runtime permission dialog is a solved problem in Flutter. The special accesses are not covered by that dialog, and each one is a small research project: which AppOps or Settings value to read, which intent to send, whether the package URI is honoured on this Android version, and what to do when the screen does not exist. Then there is the part most code skips: knowing when the user is back and what they chose.
I wanted one place for that knowledge. One enum for the permissions, one status type with an honest notApplicable, a fallback chain that always ends somewhere, and a request() that returns the new status so the app can move on. The table of Play policy notes is in the README for the same reason: several of these accesses need a declaration, and it is better to know that before writing the feature.
Get the code
The source, the full permission table, the example app and the tests are on GitHub: https://github.com/AbdeldjalilChougui/android_special_permissions. It's MIT licensed. Issues and pull requests are welcome.
Originally published at abdeldjalilchougui.biz.
