I am new to flutter and android developing. I want to create an app that works like this =
if some conditions fulfill then it must set an alarm in my default alarm clock app (time can be current datetime). this condition check, alarm setup, and frequently checking parts will all need to be done in 10-minute intervals. It must function, even though I am using another app or the app is dismissed from the recent tab or it is in the recent tab.
Solution I make thus far= using android_alarm_manager_plus for periodic condition check. In callback if satisfy the condition then setting alarm using android_intent_plus with android.intent.action.SET_ALARM action. also disabled Battery optimization and added a persistent notification so that system doesn't close my app when it is in background.
Currently in Android 9 (API level 28) and below = it is working as I expected even in the background.
But in Android 10 (API level 29) and above = only condition check part is working in background, android.intent.action.SET_ALARM action is executing but not setting any alarm (checked that with print statement, gives no error when executing that intent part, but alarm not setting)
main.dart code
import 'dart:io';
import 'package:disable_battery_optimization/disable_battery_optimization.dart';
import 'package:intl/intl.dart';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'dart:async';
import 'package:android_intent_plus/android_intent.dart';
import 'package:android_intent_plus/flag.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart';
import 'package:platform/platform.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
const latestTimestampKey = "latestTimestamp";
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
class PostHttpOverrides extends HttpOverrides{
@override
HttpClient createHttpClient(context){
return super.createHttpClient(context)
..badCertificateCallback = (X509Certificate cert, String host, int port)=> true;
}
}
// this is the callback function for android_alarm_manager_plus
@pragma('vm:entry-point') // Mandatory if the App is obfuscated or using Flutter 3.1+
Future<void> _createAlarm() async {
// Retrieve the latest timestamp from shared preferences
final SharedPreferences prefs = await SharedPreferences.getInstance();
String? latestTimestamp = prefs.getString(latestTimestampKey);
// Fetching data from API
final response = await http.get(Uri.parse("api_url"));
if (response.statusCode == 200) {
final Map<String, dynamic> data = json.decode(response.body);
final String timestamp = data['timestamp'];
// Checking if the timestamp has changed or not
if (timestamp != latestTimestamp) {
// saving the latest timestamp to shared preferences for next time
latestTimestamp = timestamp;
prefs.setString(latestTimestampKey, latestTimestamp);
// setting the alarm 1 minute after the fetching data
// means when the app fetches the data, it will create the alarm 1 minute after that
final DateTime scheduleddatetimeNow = DateTime.now();
final DateTime scheduleddatetimeNowNow = scheduleddatetimeNow.add(const Duration(minutes: 2));
int hour = scheduleddatetimeNowNow.hour;
int minute = scheduleddatetimeNowNow.minute;
int hh = hour;
int mm = minute;
try {
// this try-catch code block executig but not setting alarm
print("zzzzzzzzzzzzzzzzzzzzzzzzz = try catch begin ");
final intent = AndroidIntent(
action: 'android.intent.action.SET_ALARM',
arguments: <String, dynamic>{
'android.intent.extra.alarm.HOUR': hh,
'android.intent.extra.alarm.MINUTES': mm,
'android.intent.extra.alarm.SKIP_UI': true,
'android.intent.extra.alarm.MESSAGE': 'CR Posted',
},
);
await intent.launch();
print("zzzzzzzzzzzzzzzzzzzzzzzzz = try catch end ");
}
catch (e) {
throw Exception('zzzzzzzzzzzzzzzzzzzzzzzz Error creating alarm: $e');
}
}
}
else {
throw Exception('zzzzzzzzzzzzzzzzzzzzzzzzz Failed to load data from API');
}
}
Future<void> main() async {
HttpOverrides.global = new PostHttpOverrides();
WidgetsFlutterBinding.ensureInitialized();
await AndroidAlarmManager.initialize();
runApp(const MyApp());
}
void showPersistentNotification() async {
const AndroidNotificationDetails androidPlatformChannelSpecifics =
AndroidNotificationDetails(
'your_channel_id', // Change to your channel ID
'Your Notification Title',
importance: Importance.high,
priority: Priority.high,
ongoing: true, // This makes it persistent
autoCancel: false,
);
const NotificationDetails platformChannelSpecifics =
NotificationDetails(android: androidPlatformChannelSpecifics);
await flutterLocalNotificationsPlugin.show(
0, // Notification ID
'CR ALARM',
'Running CR Alarm ..... ',
platformChannelSpecifics,
);
}
Future<void> requestNotificationPermission(BuildContext context) async {
final status = await Permission.notification.request();
if (status.isGranted) {
// Permission granted, you can show notifications now
// You can call your notification creation method here
showPersistentNotification();
} else if (status.isDenied) {
// Permission denied, show a dialog or message to inform the user
// You can also provide a button to open app settings
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Notification Permission'),
content: const Text('Please allow notification access in app settings.'),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
openAppSettings(); // Open app settings on button press
},
child: const Text('Open Settings'),
),
],
),
);
} else if (status.isPermanentlyDenied) {
// Permission permanently denied, handle accordingly
// You can inform the user and provide a way to open app settings
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Notification Permission'),
content: const Text('Notification permission is permanently denied. Please open app settings to enable it.'),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
openAppSettings(); // Open app settings on button press
},
child: const Text('Open Settings'),
),
],
),
);
}
}
Future<void> requestScheduleExactAlarmPermission() async {
PermissionStatus status = await Permission.scheduleExactAlarm.request();
if (status.isGranted) {
print('SCHEDULE_EXACT_ALARM permission granted');
} else if (status.isDenied) {
print('SCHEDULE_EXACT_ALARM permission denied');
openAppSettings(); // Open app settings to allow the user to grant the permission
} else if (status.isPermanentlyDenied) {
print('SCHEDULE_EXACT_ALARM permission permanently denied');
openAppSettings(); // Open app settings to allow the user to grant the permission
}
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'CR Alarm',
home: const MyHomePage(title: 'CR Alarm Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
void initState() {
super.initState();
// Initialize notifications
const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('app');
final InitializationSettings initializationSettings = InitializationSettings(android: initializationSettingsAndroid);
flutterLocalNotificationsPlugin.initialize(initializationSettings);
flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()?.requestPermission();
}
Future<void> requestBatteryOptimizationsPermissions() async {
PermissionStatus status = await Permission.ignoreBatteryOptimizations.status;
if (!status.isGranted) {
status = await Permission.ignoreBatteryOptimizations.request();
}
}
Future<void> cancel() async {
await AndroidAlarmManager.cancel(4);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Center(
child: Text('CR Alarm Student App'),
),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
child: const Text("Show Persistent Notification"),
onPressed: () {
showPersistentNotification();
}
),
ElevatedButton(
child: const Text("Disable all Optimizations"),
onPressed: () {
DisableBatteryOptimization.showDisableAllOptimizationsSettings('App Process', 'Enable App Battery Usage', 'Battery Optimization', 'Enable process');
}
),
ElevatedButton(
child: const Text("request ScheduleExact Alarm Permission"),
onPressed: () {
requestScheduleExactAlarmPermission();
}
),
ElevatedButton(
child: const Text("Disable Battery Optimizations"),
onPressed: () {
requestBatteryOptimizationsPermissions();
}
),
ElevatedButton(
child: const Text('Start CR Alarm'),
onPressed: () async {
// Set a repeating alarm
await AndroidAlarmManager.periodic(
const Duration(minutes: 5), 4, _createAlarm,
exact: true,
wakeup: true,
allowWhileIdle: true,
rescheduleOnReboot: true).then((val) => print('set up:$val'));
},
},
),
const SizedBox(height: 20),
ElevatedButton(
child: const Text('Cancel Alarm'),
onPressed: () {
cancel();
},
},
),
],
),
),
);
;
}
}
AndroidManifest.xml code
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.spryshanto.cr_alarm">
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<!-- For apps with targetSDK=31 (Android 12) -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<!-- <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" android:maxSdkVersion="32" /> -->
<!-- USE_EXACT_ALARM on SDK 33 and above -->
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="com.android.alarm.permission.SET_ALARM" />
<uses-permission android:name="android.permission.VIBRATE" />
<!-- <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" /> -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
<application
android:label="CR Alarm"
android:name="${applicationName}"
android:icon="@mipmap/launcher_icon">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<service
android:name="dev.fluttercommunity.plus.androidalarmmanager.AlarmService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false"/>
<receiver
android:name="dev.fluttercommunity.plus.androidalarmmanager.AlarmBroadcastReceiver"
android:exported="false"/>
<receiver
android:name="dev.fluttercommunity.plus.androidalarmmanager.RebootBroadcastReceiver"
android:enabled="false"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>
</manifest>