Skip to content

Commit fa27ae2

Browse files
authored
[flutter_local_locations] added ability to request permission to schedule exact alarms and updated permissions and receivers in plugins' manifest file (#2052)
* added the ability to request the permission to schedule exact alarms * updated Android side of plugin to only request the minimum permissions via the AndroidManifest.xml file * Google Java Format * added details to AndroidManifest.xml section of readme that developers can refer to example app * updated details underneath Android setup section of readme to draw attention to Android 14 changes * added test to verify requestExactAlarmsPermission call * added note to changelog around Android 14 changes to do with exact alarms and link to issue raised on GitHub repo --------- Co-authored-by: github-actions <>
1 parent 44158f8 commit fa27ae2

File tree

10 files changed

+178
-25
lines changed

10 files changed

+178
-25
lines changed

flutter_local_notifications/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# [16.0.0-dev.1]
2+
3+
* [Android] **Breaking changes** the plugin now only declares the bare minimum in its `AndroidManifest.xml`. This means applications making use of either scheduled notifications, full-screen intent notifications or notification actions will now require changes in the application's own `AndroidManifest.xml` file. Please check the [AndroidManifest.xml setup](https://pub.dev/packages/flutter_local_notifications#androidmanifestxml-setup) section of the readme for more details. The reason this was done was because not all applications will leverage all of the plugin's features. Doing this will now allow applications to only request the appropriate permissions needed for their application. This addresses issue [1687](https://github.com/MaikuB/flutter_local_notifications/issues/1687)
4+
* [Android] added the ability to request permission to schedule exact alarms via the `requestExactAlarmsPermission()` method that has been added to the `AndroidFlutterLocalNotificationsPlugin` class that represents the Android implementation of the plugin. This has been done in response to behaviour changes introduced in Android 14 (API level 34) when comes to using exact alarms. See the official documentation about these changes [here](https://developer.android.com/about/versions/14/changes/schedule-exact-alarms). This change addresses issue [1906](https://github.com/MaikuB/flutter_local_notifications/issues/1906)
5+
6+
17
# [15.1.0+1]
28

39
* Fixed formatting of 15.1.0 changelog entry

flutter_local_notifications/README.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ A cross platform plugin for displaying local notifications.
2323
- **[👏 Acknowledgements](#-acknowledgements)**
2424
- **[🔧 Android Setup](#-android-setup)**
2525
- [Gradle setup](#gradle-setup)
26+
- [AndroidManifest.xml setup](#androidmanifestxml-setup)
2627
- [Requesting permissions on Android 13 or higher](#requesting-permissions-on-android-13-or-higher)
2728
- [Custom notification icons and sounds](#custom-notification-icons-and-sounds)
2829
- [Scheduled notifications](#scheduling-a-notification)
@@ -171,7 +172,7 @@ Due to some limitations on iOS with how it treats null values in dictionaries, a
171172

172173
## 🔧 Android Setup
173174

174-
Before proceeding, please make sure you are using the latest version of the plugin. The reason for this is that since version 3.0.1+4, the amount of setup needed has been reduced. Previously, applications needed changes done to the `AndroidManifest.xml` file and there was a bit more setup needed for release builds. If for some reason, your application still needs to use an older version of the plugin then make use of the release tags to refer back to older versions of readme.
175+
Before proceeding, please make sure you are using the latest version of the plugin. Note that there have been differences in the setup depending on the version of the plugin used. Please make use of the release tags to refer back to older versions of readme. Applications that schedule notifications should pay close attention to the [AndroidManifest.xml setup](#androidmanifestxml-setup) section of the readme since Android 14 has brought about some behavioural changes.
175176

176177
### Gradle setup
177178

@@ -230,6 +231,35 @@ android {
230231
}
231232
```
232233

234+
### AndroidManifest.xml setup
235+
236+
Previously the plugin would specify all the permissions required all of the features that the plugin support in its own `AndroidManifest.xml` file so that developers wouldn't need to do this in their own app's `AndroidManifest.xml` file. Since version 16 onwards, the plugin will now only specify the bare minimum and these [`POST_NOTIFICATIONS`] (https://developer.android.com/reference/android/Manifest.permission#POST_NOTIFICATIONS) and [`VIBRATE`](https://developer.android.com/reference/android/Manifest.permission#VIBRATE) permissions.
237+
238+
For apps that need the following functionality please the following in your app's `AndroidManifest.xml`
239+
240+
* To schedule notifications the following changes are needed
241+
* Specify the appropriate permissions between the `<manifest>` tags.
242+
* `<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>`: this is required so the plugin can known when the device is rebooted. This is required so that the plugin can reschedule notifications upon a reboot
243+
* If the app requires scheduling notifications with exact timings (aka exact alarms), there are two options since Android 14 brought about behavioural changes (see [here](https://developer.android.com/about/versions/14/changes/schedule-exact-alarms) for more details)
244+
* specify `<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />` and call the `requestExactAlarmsPermission()` exposed by the `AndroidFlutterNotificationsPlugin` class so that the user can grant the permission via the app or
245+
* specify `<uses-permission android:name="android.permission.USE_EXACT_ALARM" />`. Users will not be prompted to grant permission, however as per the official Android documentation on the `USE_EXACT_ALARM` permission (refer to [here](https://developer.android.com/about/versions/14/changes/schedule-exact-alarms#calendar-alarm-clock) and [here](https://developer.android.com/reference/android/Manifest.permission#USE_EXACT_ALARM)), this requires the app to target Android 13 (API level 33) or higher and could be subject to approval and auditing by the app store(s) used to publish theapp
246+
* Specify the following between the `<application>` tags so that the plugin can actually show the scheduled notification(s)
247+
```xml
248+
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
249+
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
250+
<intent-filter>
251+
<action android:name="android.intent.action.BOOT_COMPLETED"/>
252+
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
253+
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
254+
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
255+
</intent-filter>
256+
</receiver>
257+
```
258+
* To use full-screen intent notifications, specify the `<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />` permission between the `<manifest>` tags.
259+
* To use notification actions, specify `<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ActionBroadcastReceiver" />` between the `<application>` tags so that the plugin can process the actions and trigger the appropriate callback(s)
260+
261+
Developers can refer to the example app's `AndroidManifest.xml` to help see what the end result may look like. Do note that the example app covers all the plugin's supported functionality so will request more permissions than your own app may need
262+
233263
### Requesting permissions on Android 13 or higher
234264

235265
From Android 13 (API level 33) onwards, apps now have the ability to display a prompt where users can decide if they want to grant an app permission to show notifications. For further reading on this matter read https://developer.android.com/guide/topics/ui/notifiers/notification-permission. To support this applications need target their application to Android 13 or higher and the compile SDK version needs to be at least 33 (Android 13). For example, to target Android 13, update your app's `build.gradle` file to have a `targetSdkVersion` of `33`. Applications can then call the following code to request the permission where the `requestPermission` method is associated with the `AndroidFlutterLocalNotificationsPlugin` class (i.e. the Android implementation of the plugin)
Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,5 @@
11
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
22
package="com.dexterous.flutterlocalnotifications">
3-
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
43
<uses-permission android:name="android.permission.VIBRATE" />
5-
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
6-
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
74
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
8-
<application>
9-
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ActionBroadcastReceiver" />
10-
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
11-
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
12-
<intent-filter>
13-
<action android:name="android.intent.action.BOOT_COMPLETED"/>
14-
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
15-
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
16-
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
17-
</intent-filter>
18-
</receiver>
19-
</application>
205
</manifest>

flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.dexterous.flutterlocalnotifications;
22

3+
import static android.provider.Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM;
4+
35
import android.Manifest;
46
import android.annotation.SuppressLint;
57
import android.app.Activity;
@@ -34,6 +36,7 @@
3436

3537
import androidx.annotation.Keep;
3638
import androidx.annotation.NonNull;
39+
import androidx.annotation.Nullable;
3740
import androidx.core.app.ActivityCompat;
3841
import androidx.core.app.AlarmManagerCompat;
3942
import androidx.core.app.NotificationCompat;
@@ -111,6 +114,7 @@ public class FlutterLocalNotificationsPlugin
111114
implements MethodCallHandler,
112115
PluginRegistry.NewIntentListener,
113116
PluginRegistry.RequestPermissionsResultListener,
117+
PluginRegistry.ActivityResultListener,
114118
FlutterPlugin,
115119
ActivityAware {
116120

@@ -155,6 +159,8 @@ public class FlutterLocalNotificationsPlugin
155159
private static final String GET_NOTIFICATION_APP_LAUNCH_DETAILS_METHOD =
156160
"getNotificationAppLaunchDetails";
157161
private static final String REQUEST_PERMISSION_METHOD = "requestPermission";
162+
private static final String REQUEST_EXACT_ALARMS_PERMISSION_METHOD =
163+
"requestExactAlarmsPermission";
158164
private static final String METHOD_CHANNEL = "dexterous.com/flutter/local_notifications";
159165
private static final String INVALID_ICON_ERROR_CODE = "invalid_icon";
160166
private static final String INVALID_LARGE_ICON_ERROR_CODE = "invalid_large_icon";
@@ -194,8 +200,12 @@ public class FlutterLocalNotificationsPlugin
194200
private Context applicationContext;
195201
private Activity mainActivity;
196202
static final int NOTIFICATION_PERMISSION_REQUEST_CODE = 1;
203+
204+
static final int EXACT_ALARM_PERMISSION_REQUEST_CODE = 2;
205+
197206
private PermissionRequestListener callback;
198-
private boolean permissionRequestInProgress = false;
207+
208+
private PermissionRequestProgress permissionRequestProgress = PermissionRequestProgress.None;
199209

200210
static void rescheduleNotifications(Context context) {
201211
ArrayList<NotificationDetails> scheduledNotifications = loadScheduledNotifications(context);
@@ -1335,6 +1345,8 @@ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
13351345
public void onAttachedToActivity(ActivityPluginBinding binding) {
13361346
binding.addOnNewIntentListener(this);
13371347
binding.addRequestPermissionsResultListener(this);
1348+
binding.addActivityResultListener(this);
1349+
13381350
mainActivity = binding.getActivity();
13391351
Intent mainActivityIntent = mainActivity.getIntent();
13401352
if (!launchedActivityFromHistory(mainActivityIntent)) {
@@ -1355,6 +1367,7 @@ public void onDetachedFromActivityForConfigChanges() {
13551367
public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) {
13561368
binding.addOnNewIntentListener(this);
13571369
binding.addRequestPermissionsResultListener(this);
1370+
binding.addActivityResultListener(this);
13581371
mainActivity = binding.getActivity();
13591372
}
13601373

@@ -1395,6 +1408,20 @@ public void fail(String message) {
13951408
}
13961409
});
13971410
break;
1411+
case REQUEST_EXACT_ALARMS_PERMISSION_METHOD:
1412+
requestExactAlarmsPermission(
1413+
new PermissionRequestListener() {
1414+
@Override
1415+
public void complete(boolean granted) {
1416+
result.success(granted);
1417+
}
1418+
1419+
@Override
1420+
public void fail(String message) {
1421+
result.error(PERMISSION_REQUEST_IN_PROGRESS_ERROR_CODE, message, null);
1422+
}
1423+
});
1424+
break;
13981425
case PERIODICALLY_SHOW_METHOD:
13991426
repeat(call, result);
14001427
break;
@@ -1715,7 +1742,7 @@ private void cancelAllNotifications(Result result) {
17151742
}
17161743

17171744
public void requestPermission(@NonNull PermissionRequestListener callback) {
1718-
if (permissionRequestInProgress) {
1745+
if (permissionRequestProgress != PermissionRequestProgress.None) {
17191746
callback.fail(PERMISSION_REQUEST_IN_PROGRESS_ERROR_MESSAGE);
17201747
return;
17211748
}
@@ -1729,27 +1756,53 @@ public void requestPermission(@NonNull PermissionRequestListener callback) {
17291756
== PackageManager.PERMISSION_GRANTED;
17301757

17311758
if (!permissionGranted) {
1732-
permissionRequestInProgress = true;
1759+
permissionRequestProgress = PermissionRequestProgress.RequestingNotificationPermission;
17331760
ActivityCompat.requestPermissions(
17341761
mainActivity, new String[] {permission}, NOTIFICATION_PERMISSION_REQUEST_CODE);
17351762
} else {
17361763
this.callback.complete(true);
1737-
permissionRequestInProgress = false;
1764+
permissionRequestProgress = PermissionRequestProgress.None;
17381765
}
17391766
} else {
17401767
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mainActivity);
17411768
this.callback.complete(notificationManager.areNotificationsEnabled());
17421769
}
17431770
}
17441771

1772+
public void requestExactAlarmsPermission(@NonNull PermissionRequestListener callback) {
1773+
if (permissionRequestProgress != PermissionRequestProgress.None) {
1774+
callback.fail(PERMISSION_REQUEST_IN_PROGRESS_ERROR_MESSAGE);
1775+
return;
1776+
}
1777+
1778+
this.callback = callback;
1779+
1780+
if (Build.VERSION.SDK_INT >= VERSION_CODES.S) {
1781+
AlarmManager alarmManager = getAlarmManager(applicationContext);
1782+
boolean permissionGranted = alarmManager.canScheduleExactAlarms();
1783+
1784+
if (!permissionGranted) {
1785+
permissionRequestProgress = PermissionRequestProgress.RequestingExactAlarmsPermission;
1786+
mainActivity.startActivityForResult(
1787+
new Intent(ACTION_REQUEST_SCHEDULE_EXACT_ALARM), EXACT_ALARM_PERMISSION_REQUEST_CODE);
1788+
} else {
1789+
this.callback.complete(true);
1790+
permissionRequestProgress = PermissionRequestProgress.None;
1791+
}
1792+
} else {
1793+
this.callback.complete(true);
1794+
}
1795+
}
1796+
17451797
@Override
17461798
public boolean onRequestPermissionsResult(
17471799
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
1748-
if (permissionRequestInProgress && requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) {
1800+
if (permissionRequestProgress == PermissionRequestProgress.RequestingNotificationPermission
1801+
&& requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) {
17491802
boolean granted =
17501803
grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
17511804
callback.complete(granted);
1752-
permissionRequestInProgress = false;
1805+
permissionRequestProgress = PermissionRequestProgress.None;
17531806
return granted;
17541807
} else {
17551808
return false;
@@ -2059,6 +2112,21 @@ private void setCanScheduleExactNotifications(Result result) {
20592112
}
20602113
}
20612114

2115+
@Override
2116+
public boolean onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
2117+
if (requestCode != NOTIFICATION_PERMISSION_REQUEST_CODE
2118+
&& requestCode != EXACT_ALARM_PERMISSION_REQUEST_CODE) {
2119+
return false;
2120+
}
2121+
2122+
if (requestCode == EXACT_ALARM_PERMISSION_REQUEST_CODE && VERSION.SDK_INT >= VERSION_CODES.S) {
2123+
AlarmManager alarmManager = getAlarmManager(applicationContext);
2124+
this.callback.complete(alarmManager.canScheduleExactAlarms());
2125+
}
2126+
2127+
return true;
2128+
}
2129+
20622130
private static class PluginException extends RuntimeException {
20632131
public final String code;
20642132

@@ -2073,4 +2141,10 @@ public ExactAlarmPermissionException() {
20732141
super(EXACT_ALARMS_PERMISSION_ERROR_CODE, "Exact alarms are not permitted");
20742142
}
20752143
}
2144+
2145+
enum PermissionRequestProgress {
2146+
None,
2147+
RequestingNotificationPermission,
2148+
RequestingExactAlarmsPermission
2149+
}
20762150
}

flutter_local_notifications/example/android/app/src/main/AndroidManifest.xml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
22
package="com.dexterous.flutter_local_notifications_example">
3+
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
4+
<uses-permission android:name="android.permission.VIBRATE" />
5+
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
6+
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
7+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
38
<application
49
android:label="flutter_local_notifications_example"
510
android:icon="@mipmap/ic_launcher">
@@ -38,6 +43,16 @@
3843
android:name="com.dexterous.flutterlocalnotifications.ForegroundService"
3944
android:exported="false"
4045
android:stopWithTask="false"/>
46+
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ActionBroadcastReceiver" />
47+
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
48+
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
49+
<intent-filter>
50+
<action android:name="android.intent.action.BOOT_COMPLETED"/>
51+
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
52+
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
53+
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
54+
</intent-filter>
55+
</receiver>
4156
</application>
4257
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
4358
</manifest>

flutter_local_notifications/example/lib/main.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,9 +303,10 @@ class _HomePageState extends State<HomePage> {
303303
flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<
304304
AndroidFlutterLocalNotificationsPlugin>();
305305

306-
final bool? granted = await androidImplementation?.requestPermission();
306+
final bool? grantedNotificationPermission =
307+
await androidImplementation?.requestPermission();
307308
setState(() {
308-
_notificationsEnabled = granted ?? false;
309+
_notificationsEnabled = grantedNotificationPermission ?? false;
309310
});
310311
}
311312
}

flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,10 @@ class FlutterLocalNotificationsPlugin {
327327
/// 10:00 and the value of the [matchDateTimeComponents] is
328328
/// [DateTimeComponents.time], then the next time a notification will
329329
/// appear is 2020-10-20 10:00.
330+
///
331+
/// On Android, this will also require additional setup for the app,
332+
/// especially in the app's `AndroidManifest.xml` file. Please see check the
333+
/// readme for further details.
330334
Future<void> zonedSchedule(
331335
int id,
332336
String? title,
@@ -395,6 +399,10 @@ class FlutterLocalNotificationsPlugin {
395399
/// future majorrelease in favour of the [androidScheduledMode] parameter that
396400
/// provides the same functionality in addition to being able to schedule
397401
/// notifications with inexact timings.
402+
///
403+
/// On Android, this will also require additional setup for the app,
404+
/// especially in the app's `AndroidManifest.xml` file. Please see check the
405+
/// readme for further details.
398406
Future<void> periodicallyShow(
399407
int id,
400408
String? title,

0 commit comments

Comments
 (0)