Skip to content

Commit

Permalink
Create android native alarm service
Browse files Browse the repository at this point in the history
  • Loading branch information
gdelataillade committed Oct 24, 2023
1 parent 278b89d commit f439fa0
Show file tree
Hide file tree
Showing 13 changed files with 264 additions and 71 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

# Alarm plugin for iOS and Android

This Flutter plugin provides a simple and easy-to-use interface for setting and canceling alarms on iOS and Android devices. It utilizes the `android_alarm_manager_plus` plugin for Android and the native iOS `AVAudioPlayer` class.
This plugin offers a straightforward interface to set and cancel alarms on both iOS and Android devices. Using native code, it handles audio playback, vibrations, system volume, and notifications seamlessly.

## 🔧 Installation steps

Expand Down Expand Up @@ -156,4 +156,8 @@ These are some features that I have in mind that could be useful:

Thank you for considering contributing to this plugin. Your help is greatly appreciated!

❤️ Let me know if you like the plugin by liking it on [pub.dev](https://pub.dev/packages/alarm) and starring the repo on [Github](https://github.com/gdelataillade/alarm) 🙂
🙏 Special thanks to the main contributors 🇫🇷
- [evolum](https://evolum.co)
- [WayUp](https://wayuphealth.fr)

❤️ Let me know if you like the plugin by liking it on [pub.dev](https://pub.dev/packages/alarm) and starring the repo on [Github](https://github.com/gdelataillade/alarm) 🙂
4 changes: 4 additions & 0 deletions android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
<application>
<receiver android:name=".AlarmReceiver" />
<service android:name=".AlarmService" />
<service
android:name=".NotificationOnKillService"
android:enabled="true"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,52 +1,76 @@
package com.gdelataillade.alarm.alarm

import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import androidx.annotation.NonNull
import io.flutter.Log
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.Log

/// Communication between Flutter Alarm service and native Android.
class AlarmPlugin: FlutterPlugin, MethodCallHandler {
private lateinit var context: Context
private lateinit var channel : MethodChannel
private lateinit var context: Context
private lateinit var channel : MethodChannel

override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.gdelataillade.alarm/notifOnAppKill")
channel.setMethodCallHandler(this)
}
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.gdelataillade.alarm/notifOnAppKill")
channel.setMethodCallHandler(this)
}

override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"setNotificationOnKillService" -> {
val title = call.argument<String>("title")
val description = call.argument<String>("description")
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"setAlarm" -> {
val alarmIntent = Intent(context, AlarmReceiver::class.java)

val serviceIntent = Intent(context, NotificationOnKillService::class.java)
val delayInSeconds = call.argument<Int>("delayInSeconds")

serviceIntent.putExtra("title", title)
serviceIntent.putExtra("description", description)
alarmIntent.putExtra("id", call.argument<Int>("id"))
alarmIntent.putExtra("assetAudioPath", call.argument<String>("assetAudioPath"))
alarmIntent.putExtra("loopAudio", call.argument<Boolean>("loopAudio"))
alarmIntent.putExtra("vibrate", call.argument<Boolean>("vibrate"))
alarmIntent.putExtra("volume", call.argument<Boolean>("volume"))
alarmIntent.putExtra("fadeDuration", call.argument<Double>("fadeDuration"))
alarmIntent.putExtra("notificationTitle", call.argument<String>("notificationTitle"))
alarmIntent.putExtra("notificationBody", call.argument<String>("notificationBody"))

context.startService(serviceIntent)
result.success(true)
}
"stopNotificationOnKillService" -> {
val serviceIntent = Intent(context, NotificationOnKillService::class.java)
context.stopService(serviceIntent)
result.success(true)
}
else -> {
result.notImplemented()
val triggerAtMillis = System.currentTimeMillis() + delayInSeconds!! * 1000
val pendingIntent = PendingIntent.getBroadcast(context, 0, alarmIntent, 0)
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager

Log.d("AlarmService", "triggerAtMillis: $triggerAtMillis")
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent)

result.success(true)
}
"setNotificationOnKillService" -> {
val title = call.argument<String>("title")
val description = call.argument<String>("description")
val body = call.argument<String>("body")

val serviceIntent = Intent(context, NotificationOnKillService::class.java)
serviceIntent.putExtra("title", title)
serviceIntent.putExtra("description", description)

context.startService(serviceIntent)
result.success(true)
}
"stopNotificationOnKillService" -> {
val serviceIntent = Intent(context, NotificationOnKillService::class.java)
context.stopService(serviceIntent)
result.success(true)
}
else -> {
result.notImplemented()
}
}
}
}

override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.gdelataillade.alarm.alarm

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import io.flutter.Log

class AlarmReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Log.d("AlarmReceiver", "AlarmReceiver triggered")

val serviceIntent = Intent(context, AlarmService::class.java)
serviceIntent.putExtras(intent)

context.startService(serviceIntent)
}
}
132 changes: 132 additions & 0 deletions android/src/main/kotlin/com/gdelataillade/alarm/alarm/AlarmService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package com.gdelataillade.alarm.alarm

import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.media.MediaPlayer
import android.media.AudioManager
import android.media.AudioManager.FLAG_SHOW_UI
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
import android.os.VibrationEffect
import android.os.Vibrator
import androidx.core.app.NotificationCompat
import io.flutter.Log
import kotlin.math.round

class AlarmService : Service() {
private var mediaPlayer: MediaPlayer? = null
private val CHANNEL_ID = "AlarmServiceChannel"

override fun onCreate() {
super.onCreate()
createNotificationChannel()
}

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d("AlarmService", "onStartCommand")

val id = intent?.getIntExtra("id", 0)
val assetAudioPath = intent?.getStringExtra("assetAudioPath")
val loopAudio = intent?.getBooleanExtra("loopAudio", true)
val vibrate = intent?.getBooleanExtra("vibrate", true)
val volume = intent?.getDoubleExtra("volume", -1.0)
val fadeDuration = intent?.getDoubleExtra("fadeDuration", 0.0)
val notificationTitle = intent?.getStringExtra("notificationTitle")
val notificationBody = intent?.getStringExtra("notificationBody")
val showSystemUI = true

Log.d("AlarmService", "id: $id")
Log.d("AlarmService", "assetAudioPath: $assetAudioPath")
Log.d("AlarmService", "loopAudio: $loopAudio")
Log.d("AlarmService", "vibrate: $vibrate")
Log.d("AlarmService", "volume: $volume")
Log.d("AlarmService", "fadeDuration: $fadeDuration")
Log.d("AlarmService", "notificationTitle: $notificationTitle")
Log.d("AlarmService", "notificationBody: $notificationBody")

if (notificationTitle != null && notificationBody != null) {
val iconResId = applicationContext.resources.getIdentifier("ic_launcher", "mipmap", applicationContext.packageName)
val intent = applicationContext.packageManager.getLaunchIntentForPackage(applicationContext.packageName)
val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)

val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(notificationTitle)
.setContentText(notificationBody)
.setSmallIcon(iconResId)
.setContentIntent(pendingIntent)
.setAutoCancel(true) // Automatically remove the notification when tapped
.build()

startForeground(id!!, notification)
}

if (volume != -1.0) {
val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
///
var maxVolume:Int = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
var _volume:Int = (round(volume!! * maxVolume)).toInt()

audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, _volume, if (showSystemUI) FLAG_SHOW_UI else 0)
}

try {
val assetManager = applicationContext.assets
val descriptor = assetManager.openFd("flutter_assets/" + assetAudioPath!!)

val mediaPlayer = MediaPlayer().apply {
setDataSource(descriptor.fileDescriptor, descriptor.startOffset, descriptor.length)
prepare()
isLooping = loopAudio!!
}
mediaPlayer.start()

} catch (e: Exception) {
// Handle exceptions related to asset loading or MediaPlayer
e.printStackTrace()
}

if (vibrate!!) {
// Vibrate the device in a loop: vibrate for 500ms, pause for 500ms
val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
val pattern = longArrayOf(0, 500, 500) // Start immediately, vibrate 500ms, pause 500ms
val repeat = 1 // Repeat from the second element (0-based) of the pattern, which is the pause
val vibrationEffect = VibrationEffect.createWaveform(pattern, repeat)
vibrator.vibrate(vibrationEffect)
}

// Wake up the device
val wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager)
.newWakeLock(PowerManager.FULL_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP, "app:AlarmWakelockTag")
wakeLock.acquire(5 * 60 * 1000L /*5 minutes*/)

return START_STICKY
}

private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel = NotificationChannel(
CHANNEL_ID,
"Alarm Service Channel",
NotificationManager.IMPORTANCE_DEFAULT
)

val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(serviceChannel)
}
}

override fun onDestroy() {
mediaPlayer?.stop()
mediaPlayer?.release()
super.onDestroy()
}

override fun onBind(intent: Intent?): IBinder? {
return null
}
}
2 changes: 2 additions & 0 deletions example/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
<application
android:label="alarm_example"
android:name="${applicationName}"
Expand Down
2 changes: 1 addition & 1 deletion example/lib/screens/shortcut_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class _ExampleAlarmHomeShortcutButtonState
id: DateTime.now().millisecondsSinceEpoch % 10000,
dateTime: dateTime,
assetAudioPath: 'assets/marimba.mp3',
volumeMax: true,
volumeMax: false,
);

await Alarm.set(alarmSettings: alarmSettings);
Expand Down
4 changes: 2 additions & 2 deletions example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,10 @@ packages:
dependency: transitive
description:
name: flutter_local_notifications
sha256: "3002092e5b8ce2f86c3361422e52e6db6776c23ee21e0b2f71b892bf4259ef04"
sha256: "501ed9d54f1c8c0535b7991bade36f9e7e3b45a2346401f03775c1ec7a3c06ae"
url: "https://pub.dev"
source: hosted
version: "15.1.1"
version: "15.1.2"
flutter_local_notifications_linux:
dependency: transitive
description:
Expand Down
20 changes: 0 additions & 20 deletions help/INSTALL-ANDROID.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,26 +25,6 @@ Then, add the following to your `AndroidManifest.xml` within the `<manifest></ma
```

## Step 3
Now, within the `<application></application>` tags, add:

```xml
<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>
```

Finally, if you want your notifications to show in full screen even when the device is locked, add these attributes in `<activity>`:

```xml
Expand Down
6 changes: 5 additions & 1 deletion ios/Classes/SwiftAlarmPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ public class SwiftAlarmPlugin: NSObject, FlutterPlugin {

if notifOnKillEnabled && !observerAdded {
observerAdded = true
NotificationCenter.default.addObserver(self, selector: #selector(applicationWillTerminate(_:)), name: UIApplication.willTerminateNotification, object: nil)
do {
NotificationCenter.default.addObserver(self, selector: #selector(applicationWillTerminate(_:)), name: UIApplication.willTerminateNotification, object: nil)
} catch {
NSLog("SwiftAlarmPlugin: Failed to register observer for UIApplication.willTerminateNotification: \(error)")
}
}

let id = args["id"] as! Int
Expand Down
26 changes: 13 additions & 13 deletions lib/alarm.dart
Original file line number Diff line number Diff line change
Expand Up @@ -84,19 +84,19 @@ class Alarm {

await AlarmStorage.saveAlarm(alarmSettings);

if (alarmSettings.notificationTitle != null &&
alarmSettings.notificationBody != null) {
if (alarmSettings.notificationTitle!.isNotEmpty &&
alarmSettings.notificationBody!.isNotEmpty) {
await AlarmNotification.instance.scheduleAlarmNotif(
id: alarmSettings.id,
dateTime: alarmSettings.dateTime,
title: alarmSettings.notificationTitle!,
body: alarmSettings.notificationBody!,
fullScreenIntent: alarmSettings.androidFullScreenIntent,
);
}
}
// if (alarmSettings.notificationTitle != null &&
// alarmSettings.notificationBody != null) {
// if (alarmSettings.notificationTitle!.isNotEmpty &&
// alarmSettings.notificationBody!.isNotEmpty) {
// await AlarmNotification.instance.scheduleAlarmNotif(
// id: alarmSettings.id,
// dateTime: alarmSettings.dateTime,
// title: alarmSettings.notificationTitle!,
// body: alarmSettings.notificationBody!,
// fullScreenIntent: alarmSettings.androidFullScreenIntent,
// );
// }
// }

if (alarmSettings.enableNotificationOnKill) {
await AlarmNotification.instance.requestPermission();
Expand Down
Loading

0 comments on commit f439fa0

Please sign in to comment.