android_special_permissions 0.1.0
android_special_permissions: ^0.1.0 copied to clipboard
One Android API for special-access permissions (usage access, overlay, accessibility, notification listener, exact alarms, roles) with status checks and Settings deep links.
android_special_permissions #
One API for the Android "special access" permissions that the runtime permission dialog does not cover: check the status, open the right Settings screen, or open it and wait for the user to come back with the new status.
Android only; iOS not supported. On other platforms every status is
notApplicable and nothing is opened.
Features #
status()for 13 special permissions, returninggranted,deniedornotApplicable(the device's API level has no such toggle).openSettings()opens the most specific screen available: package-scoped where Android supports it, then the general list, then App info, then Settings home. Every candidate intent is checked withresolveActivitybefore it is started.request()opens the screen, waits until your app resumes, checks again and returns the new status. It uses anActivityAwarebinding and a lifecycle observer, survives configuration changes and fails cleanly withactivityNotAttachedif the activity goes away.- Default-app roles (dialer, SMS, home, browser, assistant) through
RoleManager:roleStatus()andrequestRole(), which waits for the activity result. - Accessibility-service detection that reads the short (
pkg/.Svc) and long (pkg/pkg.Svc) component forms inENABLED_ACCESSIBILITY_SERVICES. - The plugin declares no permissions. Your app declares only what it uses.
Install #
dependencies:
android_special_permissions: ^0.1.0
The plugin's minSdk is 21. Declare in your app's AndroidManifest.xml the
permissions behind the accesses you use (see the table below). Without the
declaration, most Settings screens will not list your app.
Usage #
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> accessibility() async {
// Your AccessibilityService class: fully qualified, or relative with '.'.
const service = '.MyAccessibilityService';
final status = await permissions.status(
SpecialPermission.accessibilityService,
serviceClassName: service,
);
if (!status.isGranted) {
await permissions.request(
SpecialPermission.accessibilityService,
serviceClassName: service,
);
}
}
Future<void> batteryAndRoles() async {
// Direct "allow" dialog if REQUEST_IGNORE_BATTERY_OPTIMIZATIONS is declared,
// otherwise the list screen.
await permissions.request(SpecialPermission.ignoreBatteryOptimizations);
// The list of all apps, without the direct dialog.
await permissions.openSettings(SpecialPermission.ignoreBatteryOptimizations);
final sms = await permissions.roleStatus(AndroidRole.sms);
if (sms == SpecialPermissionStatus.denied) {
await permissions.requestRole(AndroidRole.sms);
}
}
Future<void> handleErrors() async {
try {
await permissions.request(SpecialPermission.exactAlarm);
} on SpecialPermissionException catch (e) {
if (e.code == SpecialPermissionErrorCode.activityNotAttached) {
// No foreground activity, or it was destroyed while waiting.
}
print('failed: $e');
}
}
request() returns at once, without opening anything, when the status is
already granted or notApplicable. Only one request() or requestRole()
can wait at a time; a second one fails with requestInProgress.
Permissions #
The API levels are those where the plugin reports something other than
notApplicable. The Google Play notes summarise the policies as the author
understands them at the time of writing. The
Play policy centre
is authoritative, so check it before you ship.
Permission (SpecialPermission) |
Manifest | Android | What the user sees | Google Play notes |
|---|---|---|---|---|
usageAccess |
PACKAGE_USAGE_STATS |
5.0+ (API 21) | "Usage access" list; on API 29+ the app's own toggle when the device honours the package URI | The author knows of no dedicated Console form, but usage data falls under the User Data policy: show a prominent disclosure and get consent before sending the user there. |
overlay |
SYSTEM_ALERT_WINDOW |
6.0+ (API 23) | "Display over other apps". Android 11+ ignores the package URI and shows the list | No declaration form. Overlays must not obscure system UI or be used deceptively. |
accessibilityService |
an AccessibilityService guarded by BIND_ACCESSIBILITY_SERVICE |
all | "Accessibility" settings. The service's own page (API 33+) is used only by apps holding the system OPEN_ACCESSIBILITY_DETAILS_SETTINGS permission. Enabling shows a "full control" warning. On Android 13+, sideloaded apps first need "Allow restricted settings" in App info. |
Declaration required (Accessibility API form in Play Console) plus prominent disclosure, unless the app is an accessibility tool (isAccessibilityTool). |
notificationListener |
a NotificationListenerService |
all | "Notification access" list; on API 30+ the listener's own page when you pass serviceClassName. The restricted-settings gate also applies to sideloaded apps on Android 13+. |
The author knows of no dedicated form; notification content is user data (User Data policy). |
ignoreBatteryOptimizations |
REQUEST_IGNORE_BATTERY_OPTIMIZATIONS (for the direct dialog) |
6.0+ (API 23) | request(): a system dialog asking to let the app always run in the background. openSettings(): the "Battery optimization" list, where the user must switch the filter to all apps. |
Restricted. Play allows the direct dialog only when Doze breaks the app's core function (see "acceptable use cases for exemptions" in Android's Doze documentation). Otherwise use openSettings(), which needs no permission. |
exactAlarm |
SCHEDULE_EXACT_ALARM or USE_EXACT_ALARM |
12+ (API 31) | "Alarms & reminders" toggle for the app. Denied by default on Android 14+ for new installs. | SCHEDULE_EXACT_ALARM is user-granted. USE_EXACT_ALARM is auto-granted but needs a declaration and is limited to alarm-clock and calendar apps. |
installUnknownApps |
REQUEST_INSTALL_PACKAGES |
8.0+ (API 26) | "Install unknown apps" toggle for the app | Declaration required; allowed only when installing packages is a core function. |
allFilesAccess |
MANAGE_EXTERNAL_STORAGE |
11+ (API 30) | "All files access" toggle for the app | Declaration required (All files access form); only for file managers, backup, antivirus and similar core uses. |
writeSettings |
WRITE_SETTINGS |
6.0+ (API 23) | "Modify system settings" toggle | No declaration form. |
doNotDisturb |
ACCESS_NOTIFICATION_POLICY |
6.0+ (API 23) | "Do Not Disturb access" list | No declaration form. |
fullScreenIntent |
USE_FULL_SCREEN_INTENT |
14+ (API 34) | "Full screen notifications" toggle for the app, otherwise the app's notification settings | Declaration required on Play. Auto-granted only to calling and alarm apps; other apps must send the user to Settings. |
pictureInPicture |
android:supportsPictureInPicture on an activity |
8.0+ (API 26), and the device must support PiP | "Picture-in-picture" special access. This is a non-public Settings action, so it often falls back to App info | No declaration form. |
backgroundLocation |
ACCESS_BACKGROUND_LOCATION |
10+ (API 29) | Status only, see below | Declaration required (location permissions form, with a video) plus prominent disclosure. |
Role (AndroidRole) |
Android | What the user sees | Google Play notes |
|---|---|---|---|
dialer, sms |
10+ (API 29) | A system dialog asking to make the app the default phone or SMS app | Needed before using the restricted SMS and Call Log permissions, which have their own declaration form. The app must implement the handler's required components. |
home, browser, assistant |
10+ (API 29) | A system dialog, or nothing if the app does not qualify for the role | No declaration form. The app must declare the matching intent filters, otherwise Android refuses the role. |
Background location: the two-step flow #
ACCESS_BACKGROUND_LOCATION is a runtime permission, so this plugin only
reports its status (SpecialPermission.backgroundLocation). request() and
openSettings() throw SpecialPermissionErrorCode.notSupported. Request it
with permission_handler:
- Ask for foreground location (
Permission.locationWhenInUse) first. - Once that is granted, explain why the app needs location all the time,
then request
Permission.locationAlways. On Android 11+ this shows no dialog: it sends the user to the app's location page, where they pick "Allow all the time".
On API 21-28 there is no separate background permission, so the status is
notApplicable; background access follows the foreground grant.
Limitations #
- Below each permission's API level,
notApplicablemeans "no toggle exists". On API 21-22 the permissions behindoverlay,writeSettingsand similar are granted at install time when declared; this plugin does not report that. - A permission that is not declared in the manifest usually reads as
denied, and its Settings screen may not list the 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.- The accessibility details page (
android.settings.ACCESSIBILITY_DETAILS_SETTINGS) requires the signature/installer permissionOPEN_ACCESSIBILITY_DETAILS_SETTINGS, so ordinary apps get the general Accessibility screen; the plugin only tries the details page when the app holds that permission. The picture-in-picture screen is not a public SDK action; it is tried only when it resolves, and if Settings refuses to start it the plugin falls back to the next screen. - Android kills the app process when some accesses are revoked (seen for
"Install unknown apps" on the emulator; Android documents the same for
"Alarms & reminders"). A pending
request()then never completes; 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.
- Roles need API 29+. The older dialer and SMS default-app APIs are not wrapped.
- The picture-in-picture status is the AppOps state. It does not check that
one of your activities declares
supportsPictureInPicture. - Tests cover the Dart API (mocked
MethodChannel) and the pure Kotlin logic (component parsing, API-level rules, intent fallback order, resume tracking, argument errors) with JUnit. The example app also has an emulator integration test (example/integration_test/, run withdart run tool/run_on_device.dart <applicationId>fromexample/) that flips each access with adb (appops,cmd notification,deviceidle,settings,pm grant) and checks every status, every Settings screen and therequest()round trip.
Tested on: Android Emulator, Pixel 10 system image (sdk_gphone16k_x86_64),
API 37. Not yet tested on a physical phone or OEM Settings apps.
License #
MIT. See LICENSE.
Made by Abdeldjalil Chougui.