From 5e01be274c2c40d9a046e52062c5f75868854c9a Mon Sep 17 00:00:00 2001
From: Gautier de Lataillade <32983806+gdelataillade@users.noreply.github.com>
Date: Sat, 18 Nov 2023 16:41:23 +0100
Subject: [PATCH] Refactor -> Android native alarm service (#105)
* Create android native alarm service
* Add multiple alarms management
* Add stop method & previous volume & fadeDuration
* Increment plugin version to 2.2.0
* Implement alarm isRinging method
* Add minor fixes
* Remove native notification
* Split alarm services
* Add onRing callback
* Fix NotificationOnKillService
* Optimize alarm services instances
* Make alarm foreground service
* Add full screen intent notification
* Add minor adjustments
* Handle immediate alarms
* Update android installation steps
* Remove stopOnNotificationOpen
* Update changelog & readme + add minor improvements
* Update pubspec
* Update version to 3.0.0-dev.1
---
CHANGELOG.md | 20 +-
README.md | 24 +-
android/src/main/AndroidManifest.xml | 24 +-
.../gdelataillade/alarm/alarm/AlarmPlugin.kt | 150 +++++++---
.../alarm/alarm/AlarmReceiver.kt | 15 +
.../gdelataillade/alarm/alarm/AlarmService.kt | 145 ++++++++++
.../alarm/services/AudioService.kt | 86 ++++++
.../NotificationOnKillService.kt | 21 +-
.../alarm/services/NotificationService.kt | 66 +++++
.../alarm/services/VibrationService.kt | 19 ++
.../alarm/services/VolumeService.kt | 53 ++++
android/src/main/res/raw/blank.mp3 | Bin 0 -> 1574 bytes
.../android/app/src/main/AndroidManifest.xml | 22 +-
example/ios/Podfile.lock | 39 +--
example/lib/screens/edit_alarm.dart | 23 +-
example/lib/screens/home.dart | 2 +-
example/lib/screens/shortcut_button.dart | 7 +-
example/pubspec.lock | 208 +++-----------
help/INSTALL-ANDROID.md | 68 +++--
ios/Classes/SwiftAlarmPlugin.swift | 6 +-
lib/alarm.dart | 35 +--
lib/model/alarm_settings.dart | 36 +--
lib/service/notification.dart | 73 ++---
lib/src/android_alarm.dart | 270 +++---------------
lib/src/ios_alarm.dart | 2 +-
pubspec.yaml | 14 +-
26 files changed, 742 insertions(+), 686 deletions(-)
create mode 100644 android/src/main/kotlin/com/gdelataillade/alarm/alarm/AlarmReceiver.kt
create mode 100644 android/src/main/kotlin/com/gdelataillade/alarm/alarm/AlarmService.kt
create mode 100644 android/src/main/kotlin/com/gdelataillade/alarm/services/AudioService.kt
rename android/src/main/kotlin/com/gdelataillade/alarm/{alarm => services}/NotificationOnKillService.kt (81%)
create mode 100644 android/src/main/kotlin/com/gdelataillade/alarm/services/NotificationService.kt
create mode 100644 android/src/main/kotlin/com/gdelataillade/alarm/services/VibrationService.kt
create mode 100644 android/src/main/kotlin/com/gdelataillade/alarm/services/VolumeService.kt
create mode 100644 android/src/main/res/raw/blank.mp3
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e13676cc..c11f2816 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,18 @@
+## 3.0.0-dev.1
+**💥 Breaking Changes**
+**🔧 Android installation steps were updated [here](https://github.com/gdelataillade/alarm/blob/main/help/INSTALL-ANDROID.md).**
+* Remove [stopOnNotificationOpen] property.
+* Make notification mandatory so android foreground services can be used.
+* [Android] Refactor alarm to native android services.
+
+## 2.2.0
+* [Android] Move alarm service to native code.
+
## 2.1.1
* Fix AlarmSettings.fromJson method with missing [androidFullScreenIntent].
## 2.1.0
-**Android installation steps were updated.**
+**🔧 Android installation steps were updated [here](https://github.com/gdelataillade/alarm/blob/main/help/INSTALL-ANDROID.md).**
* [Android] Add parameter [androidFullScreenIntent] that turns screen on when alarm notification is triggered.
* [Android] Fix 'ring now' alarm delay.
* [Android] Fix fadeDuration cast error.
@@ -14,7 +24,7 @@
* Refactor set alarm methods.
## 2.0.0
-**Breaking Changes**
+**💥 Breaking Changes**
* Installation steps were updated in the README. Please make sure to follow them.
* [iOS] Add Background Fetch to periodically make sure alarms are still active in the background.
@@ -131,7 +141,7 @@
* Add optional [vibrate] parameter, to toggle vibrations when alarm rings.
## 0.2.0
-* **Breaking changes**: Add multiple alarm management. Now, you have to provide a unique [id] to [AlarmSettings].
+* **💥 Breaking changes**: Add multiple alarm management. Now, you have to provide a unique [id] to [AlarmSettings].
* Update example application.
* [Android] Fix potential delay between notification and alarm sound.
@@ -155,8 +165,8 @@
* Export [AlarmSettings] model in [Alarm] service so it's not necessary to import it separately anymore.
## 0.1.0
-* **Breaking changes**: [Alarm.set] method now takes a [AlarmSettings] as only parameter.
-* **Breaking changes**: You will have to create a `StreamSubscription` attached to [Alarm.ringStream.stream] in order to listen to the alarm ringing state now. This way, even if your app was previously killed, your custom callback can still be triggered.
+* **💥 Breaking changes**: [Alarm.set] method now takes a [AlarmSettings] as only parameter.
+* **💥 Breaking changes**: You will have to create a `StreamSubscription` attached to [Alarm.ringStream.stream] in order to listen to the alarm ringing state now. This way, even if your app was previously killed, your custom callback can still be triggered.
* By default, if an alarm was set and the app is killed, a notification will be shown to warn
the user that the alarm may not ring, with the possibility to reopen the app and automatically reschedule the alarm.
To disable this feature, you can call the method [Alarm.toggleNotificationOnAppKill(false)].
diff --git a/README.md b/README.md
index 242d7e58..819965b6 100644
--- a/README.md
+++ b/README.md
@@ -9,11 +9,11 @@
# 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
-Please carefully follow these installation steps. They have been updated for plugin version `2.0.0`.
+Please carefully follow these installation steps. They have been updated for plugin version `3.0.0`.
### [iOS Setup](https://github.com/gdelataillade/alarm/blob/feat/ios-background-fetch/help/INSTALL-IOS.md)
### [Android Setup](https://github.com/gdelataillade/alarm/blob/feat/ios-background-fetch/help/INSTALL-ANDROID.md)
@@ -63,11 +63,8 @@ fadeDuration | `double` | Duration, in seconds, over which to fade the ala
notificationTitle | `String` | The title of the notification triggered when alarm rings if app is on background.
notificationBody | `String` | The body of the notification.
enableNotificationOnKill | `bool` | Whether to show a notification when application is killed to warn the user that the alarm he set may not ring. Enabled by default.
-stopOnNotificationOpen | `bool` | Whether to stop the alarm when opening the received notification. Disabled by default.
androidFullScreenIntent | `bool` | Whether to turn screen on when android alarm notification is triggered. Enabled by default.
-The notification shown on alarm ring can be disabled simply by ignoring the parameters `notificationTitle` and `notificationBody`. However, if you want a notification to be triggered, you will have to provide **both of them**.
-
If you enabled `enableNotificationOnKill`, you can chose your own notification title and body by using this method before setting your alarms:
```Dart
await Alarm.setNotificationOnAppKillContent(title, body)
@@ -101,9 +98,11 @@ Don't hesitate to check out the example's code, and take a look at the app:
| Do not disturb | ✅ | ✅ | ✅ | Silenced
| Sleep mode | ✅ | ✅ | ✅ | Silenced
| While playing other media| ✅ | ✅ | ✅ | ✅
-| App killed | ❌ | ❌ | ❌ | ✅
+| App killed | 🤖 | 🤖 | 🤖 | ✅
-*Silenced: Means that the notification is not shown directly on the top of the screen. You have to go in your notification center to see it.*
+✅ : iOS and Android
+🤖 : Android only.
+Silenced: Means that the notification is not shown directly on the top of the screen. You have to go in your notification center to see it.
## ❓ FAQ
@@ -123,14 +122,15 @@ Most common solution is to educate users to disable **battery optimization** set
The more time the app spends in the background, the higher the chance the OS might stop it from running due to memory or battery optimizations. Here's how you can optimize:
+- **Battery Optimization**: Educate users to disable battery optimization on Android.
- **Regular App Usage**: Encourage users to open the app at least once a day.
- **Leverage Background Modes**: Engage in activities like weather API calls that keep the app active in the background.
-- **User Settings**: Educate users to refrain from using 'Do Not Disturb' (DnD) and 'Low Power Mode' when they're expecting the alarm to ring.
+- **User Settings**: Educate users to refrain from using 'Do Not Disturb' and 'Low Power Mode' when they're expecting the alarm to ring.
## ⚙️ Under the hood
### Android
-Uses `oneShotAt` from the `android_alarm_manager_plus` plugin with a two-way communication isolated callback to start/stop the alarm.
+Leverages a foreground service with AlarmManager scheduling to ensure alarm reliability, even if the app is terminated. Utilizes AudioManager for robust alarm sound management.
### iOS
Keeps the app awake using a silent `AVAudioPlayer` until alarm rings. When in the background, it also uses `Background App Refresh` to periodically ensure the app is still active.
@@ -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) 🙂
\ No newline at end of file
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index 517a3eaf..b74d0315 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -5,25 +5,11 @@
+
+
+
-
-
-
-
-
-
-
-
+
+
diff --git a/android/src/main/kotlin/com/gdelataillade/alarm/alarm/AlarmPlugin.kt b/android/src/main/kotlin/com/gdelataillade/alarm/alarm/AlarmPlugin.kt
index de3545cd..bb683d69 100644
--- a/android/src/main/kotlin/com/gdelataillade/alarm/alarm/AlarmPlugin.kt
+++ b/android/src/main/kotlin/com/gdelataillade/alarm/alarm/AlarmPlugin.kt
@@ -1,52 +1,138 @@
package com.gdelataillade.alarm.alarm
+import com.gdelataillade.alarm.services.NotificationOnKillService
+
+import android.os.Build
+import android.os.Handler
+import android.os.Looper
+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.plugin.common.BinaryMessenger
+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)
- }
+ companion object {
+ @JvmStatic
+ lateinit var binaryMessenger: BinaryMessenger
+ }
- override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
- when (call.method) {
- "setNotificationOnKillService" -> {
- val title = call.argument("title")
- val description = call.argument("description")
+ override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
+ context = flutterPluginBinding.applicationContext
+ channel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.gdelataillade.alarm/alarm")
+ channel.setMethodCallHandler(this)
+ binaryMessenger = flutterPluginBinding.binaryMessenger
+ }
- val serviceIntent = Intent(context, NotificationOnKillService::class.java)
+ override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
+ when (call.method) {
+ "setAlarm" -> {
+ val id = call.argument("id")!!
+ val delayInSeconds = call.argument("delayInSeconds")!!
- serviceIntent.putExtra("title", title)
- serviceIntent.putExtra("description", description)
+ val alarmIntent = createAlarmIntent(context, call, id)
- context.startService(serviceIntent)
- result.success(true)
- }
- "stopNotificationOnKillService" -> {
- val serviceIntent = Intent(context, NotificationOnKillService::class.java)
- context.stopService(serviceIntent)
- result.success(true)
- }
- else -> {
- result.notImplemented()
+ if (delayInSeconds <= 5) {
+ handleImmediateAlarm(context, alarmIntent, delayInSeconds)
+ } else {
+ handleDelayedAlarm(context, alarmIntent, delayInSeconds, id)
+ }
+
+ result.success(true)
+ }
+ "stopAlarm" -> {
+ val id = call.argument("id")
+
+ // Intent to stop the alarm
+ val stopIntent = Intent(context, AlarmService::class.java)
+ stopIntent.action = "STOP_ALARM"
+ stopIntent.putExtra("id", id)
+ context.startService(stopIntent)
+
+ // Intent to cancel the future alarm if it's set
+ val alarmIntent = Intent(context, AlarmReceiver::class.java)
+ val pendingIntent = PendingIntent.getBroadcast(context, id!!, alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT)
+
+ // Cancel the future alarm using AlarmManager
+ val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
+ alarmManager.cancel(pendingIntent)
+
+ result.success(true)
+ }
+ "isRinging" -> {
+ val id = call.argument("id")
+ val ringingAlarmIds = AlarmService.ringingAlarmIds
+ val isRinging = ringingAlarmIds.contains(id)
+ result.success(isRinging)
+ }
+ "setNotificationOnKillService" -> {
+ val title = call.argument("title")
+ val description = call.argument("description")
+ val body = call.argument("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)
- }
-}
+ fun createAlarmIntent(context: Context, call: MethodCall, id: Int?): Intent {
+ val alarmIntent = Intent(context, AlarmReceiver::class.java)
+ setIntentExtras(alarmIntent, call, id)
+ return alarmIntent
+ }
+
+ fun setIntentExtras(intent: Intent, call: MethodCall, id: Int?) {
+ intent.putExtra("id", id)
+ intent.putExtra("assetAudioPath", call.argument("assetAudioPath"))
+ intent.putExtra("loopAudio", call.argument("loopAudio"))
+ intent.putExtra("vibrate", call.argument("vibrate"))
+ intent.putExtra("volume", call.argument("volume"))
+ intent.putExtra("fadeDuration", call.argument("fadeDuration"))
+ intent.putExtra("notificationTitle", call.argument("notificationTitle"))
+ intent.putExtra("notificationBody", call.argument("notificationBody"))
+ intent.putExtra("fullScreenIntent", call.argument("fullScreenIntent"))
+ }
+
+ fun handleImmediateAlarm(context: Context, intent: Intent, delayInSeconds: Int) {
+ val handler = Handler(Looper.getMainLooper())
+ handler.postDelayed({
+ context.sendBroadcast(intent)
+ }, delayInSeconds * 1000L)
+ }
+
+ fun handleDelayedAlarm(context: Context, intent: Intent, delayInSeconds: Int, id: Int) {
+ val triggerTime = System.currentTimeMillis() + delayInSeconds * 1000
+ val pendingIntent = PendingIntent.getBroadcast(context, id, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
+
+ alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent)
+ }
+
+ override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
+ channel.setMethodCallHandler(null)
+ }
+}
\ No newline at end of file
diff --git a/android/src/main/kotlin/com/gdelataillade/alarm/alarm/AlarmReceiver.kt b/android/src/main/kotlin/com/gdelataillade/alarm/alarm/AlarmReceiver.kt
new file mode 100644
index 00000000..ee190e1d
--- /dev/null
+++ b/android/src/main/kotlin/com/gdelataillade/alarm/alarm/AlarmReceiver.kt
@@ -0,0 +1,15 @@
+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) {
+ val serviceIntent = Intent(context, AlarmService::class.java)
+ serviceIntent.putExtras(intent)
+
+ context.startService(serviceIntent)
+ }
+}
diff --git a/android/src/main/kotlin/com/gdelataillade/alarm/alarm/AlarmService.kt b/android/src/main/kotlin/com/gdelataillade/alarm/alarm/AlarmService.kt
new file mode 100644
index 00000000..9d4439a1
--- /dev/null
+++ b/android/src/main/kotlin/com/gdelataillade/alarm/alarm/AlarmService.kt
@@ -0,0 +1,145 @@
+package com.gdelataillade.alarm.alarm
+
+import com.gdelataillade.alarm.services.AudioService
+import com.gdelataillade.alarm.services.VibrationService
+import com.gdelataillade.alarm.services.VolumeService
+
+import android.app.Service
+import android.app.PendingIntent
+import android.content.Intent
+import android.content.Context
+import android.os.IBinder
+import android.os.PowerManager
+import io.flutter.Log
+import io.flutter.plugin.common.MethodChannel
+import io.flutter.embedding.engine.dart.DartExecutor
+import io.flutter.embedding.engine.FlutterEngine
+
+class AlarmService : Service() {
+ private var channel: MethodChannel? = null
+ private var audioService: AudioService? = null
+ private var vibrationService: VibrationService? = null
+ private var volumeService: VolumeService? = null
+ private var showSystemUI: Boolean = true
+
+ companion object {
+ @JvmStatic
+ var ringingAlarmIds: List = listOf()
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+
+ try {
+ val messenger = AlarmPlugin.binaryMessenger
+ if (messenger != null) {
+ channel = MethodChannel(messenger, "com.gdelataillade.alarm/alarm")
+ }
+ } catch (e: Exception) {
+ Log.d("AlarmService", "Error while creating method channel: $e")
+ }
+
+ audioService = AudioService(this)
+ vibrationService = VibrationService(this)
+ volumeService = VolumeService(this)
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ val action = intent?.action
+ val id = intent?.getIntExtra("id", 0) ?: 0
+
+ if (action == "STOP_ALARM" && id != -1) {
+ stopAlarm(id)
+ return START_NOT_STICKY
+ }
+
+ val assetAudioPath = intent?.getStringExtra("assetAudioPath")
+ val loopAudio = intent?.getBooleanExtra("loopAudio", true)
+ val vibrate = intent?.getBooleanExtra("vibrate", true)
+ val volume = intent?.getDoubleExtra("volume", -1.0) ?: -1.0
+ val fadeDuration = intent?.getDoubleExtra("fadeDuration", 0.0)
+ val notificationTitle = intent?.getStringExtra("notificationTitle")
+ val notificationBody = intent?.getStringExtra("notificationBody")
+ val fullScreenIntent = intent?.getBooleanExtra("fullScreenIntent", true)
+ showSystemUI = intent?.getBooleanExtra("showSystemUI", true) ?: 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")
+ // Log.d("AlarmService", "fullScreenIntent: $fullScreenIntent")
+
+ val notificationHandler = NotificationHandler(this)
+
+ val intent = applicationContext.packageManager.getLaunchIntentForPackage(applicationContext.packageName)
+ val pendingIntent = PendingIntent.getActivity(this, id!!, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+
+ val notification = notificationHandler.buildNotification(notificationTitle!!, notificationBody!!, fullScreenIntent!!, pendingIntent)
+ startForeground(id, notification)
+
+ try {
+ if (channel != null) {
+ channel?.invokeMethod("alarmRinging", mapOf("id" to id))
+ }
+ } catch (e: Exception) {
+ Log.d("AlarmService", "Error while invoking alarmRinging channel: $e")
+ }
+
+ if (volume != -1.0) {
+ volumeService?.setVolume(volume, showSystemUI)
+ }
+
+ volumeService?.requestAudioFocus()
+
+ audioService?.playAudio(id, assetAudioPath!!, loopAudio!!, fadeDuration!!)
+
+ ringingAlarmIds = audioService?.getPlayingMediaPlayersIds()!!
+
+ if (vibrate!!) {
+ vibrationService?.startVibrating(longArrayOf(0, 500, 500), 1)
+ }
+
+ // Wake up the device
+ val wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager)
+ .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "app:AlarmWakelockTag")
+ wakeLock.acquire(5 * 60 * 1000L) // 5 minutes
+
+ return START_STICKY
+ }
+
+ fun stopAlarm(id: Int) {
+ ringingAlarmIds = audioService?.getPlayingMediaPlayersIds()!!
+
+ volumeService?.restorePreviousVolume(showSystemUI)
+ volumeService?.abandonAudioFocus()
+
+ audioService?.stopAudio(id)
+ if (audioService?.isMediaPlayerEmpty()!!) {
+ vibrationService?.stopVibrating()
+ stopSelf()
+ }
+
+ stopForeground(true)
+ }
+
+ override fun onDestroy() {
+ ringingAlarmIds = listOf()
+
+ audioService?.cleanUp()
+ vibrationService?.stopVibrating()
+ volumeService?.restorePreviousVolume(showSystemUI)
+
+ stopForeground(true)
+
+ // Call the superclass method
+ super.onDestroy()
+ }
+
+ override fun onBind(intent: Intent?): IBinder? {
+ return null
+ }
+}
diff --git a/android/src/main/kotlin/com/gdelataillade/alarm/services/AudioService.kt b/android/src/main/kotlin/com/gdelataillade/alarm/services/AudioService.kt
new file mode 100644
index 00000000..23087d9a
--- /dev/null
+++ b/android/src/main/kotlin/com/gdelataillade/alarm/services/AudioService.kt
@@ -0,0 +1,86 @@
+package com.gdelataillade.alarm.services
+
+import android.content.Context
+import android.media.MediaPlayer
+import java.util.Timer
+import java.util.TimerTask
+import kotlin.math.round
+
+class AudioService(private val context: Context) {
+ private val mediaPlayers = mutableMapOf()
+
+ fun isMediaPlayerEmpty(): Boolean {
+ return mediaPlayers.isEmpty()
+ }
+
+ fun getPlayingMediaPlayersIds(): List {
+ return mediaPlayers.filter { (_, mediaPlayer) -> mediaPlayer.isPlaying }.keys.toList()
+ }
+
+ fun playAudio(id: Int, assetAudioPath: String, loopAudio: Boolean, fadeDuration: Double?) {
+ try {
+ mediaPlayers.forEach { (_, mediaPlayer) ->
+ if (mediaPlayer.isPlaying) {
+ mediaPlayer.stop()
+ mediaPlayer.release()
+ }
+ }
+
+ val assetManager = context.assets
+ val descriptor = assetManager.openFd("flutter_assets/$assetAudioPath")
+ val mediaPlayer = MediaPlayer().apply {
+ setDataSource(descriptor.fileDescriptor, descriptor.startOffset, descriptor.length)
+ prepare()
+ isLooping = loopAudio
+ }
+ mediaPlayer.start()
+
+ mediaPlayers[id] = mediaPlayer
+
+ if (fadeDuration != null && fadeDuration > 0) {
+ startFadeIn(mediaPlayer, fadeDuration)
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ fun stopAudio(id: Int) {
+ mediaPlayers[id]?.stop()
+ mediaPlayers[id]?.release()
+ mediaPlayers.remove(id)
+ }
+
+ private fun startFadeIn(mediaPlayer: MediaPlayer, duration: Double) {
+ val maxVolume = 1.0f // Use 1.0f for MediaPlayer's max volume
+ val fadeDuration = (duration * 1000).toLong() // Convert seconds to milliseconds
+ val fadeInterval = 100L // Interval for volume increment
+ val numberOfSteps = fadeDuration / fadeInterval // Number of volume increments
+ val deltaVolume = maxVolume / numberOfSteps // Volume increment per step
+
+ val timer = Timer(true) // Use a daemon thread
+ var volume = 0.0f
+
+ val timerTask = object : TimerTask() {
+ override fun run() {
+ mediaPlayer.setVolume(volume, volume) // Set volume for both channels
+ volume += deltaVolume
+
+ if (volume >= maxVolume) {
+ mediaPlayer.setVolume(maxVolume, maxVolume) // Ensure max volume is set
+ this.cancel() // Cancel the timer
+ }
+ }
+ }
+
+ timer.schedule(timerTask, 0, fadeInterval)
+ }
+
+ fun cleanUp() {
+ mediaPlayers.forEach { (_, mediaPlayer) ->
+ mediaPlayer.stop()
+ mediaPlayer.release()
+ }
+ mediaPlayers.clear()
+ }
+}
diff --git a/android/src/main/kotlin/com/gdelataillade/alarm/alarm/NotificationOnKillService.kt b/android/src/main/kotlin/com/gdelataillade/alarm/services/NotificationOnKillService.kt
similarity index 81%
rename from android/src/main/kotlin/com/gdelataillade/alarm/alarm/NotificationOnKillService.kt
rename to android/src/main/kotlin/com/gdelataillade/alarm/services/NotificationOnKillService.kt
index c31f405c..52e1a576 100644
--- a/android/src/main/kotlin/com/gdelataillade/alarm/alarm/NotificationOnKillService.kt
+++ b/android/src/main/kotlin/com/gdelataillade/alarm/services/NotificationOnKillService.kt
@@ -1,4 +1,4 @@
-package com.gdelataillade.alarm.alarm
+package com.gdelataillade.alarm.services
import android.annotation.SuppressLint
import android.app.NotificationChannel
@@ -17,9 +17,11 @@ import io.flutter.Log
class NotificationOnKillService: Service() {
private lateinit var title: String
private lateinit var description: String
+ private val NOTIFICATION_ID = 88888
+ private val CHANNEL_ID = "com.gdelataillade.alarm.alarm_channel"
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- title = intent?.getStringExtra("title") ?: "Your alarms may not ring"
+ title = intent?.getStringExtra("title") ?: "Your alarms could not ring"
description = intent?.getStringExtra("description") ?: "You killed the app. Please reopen so your alarms can be rescheduled."
return START_STICKY
@@ -28,23 +30,22 @@ class NotificationOnKillService: Service() {
@RequiresApi(Build.VERSION_CODES.O)
override fun onTaskRemoved(rootIntent: Intent?) {
try {
-
val notificationIntent = packageManager.getLaunchIntentForPackage(packageName)
val pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE)
- val notificationBuilder = NotificationCompat.Builder(this, "com.gdelataillade.alarm.alarm")
+ val notificationBuilder = NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(android.R.drawable.ic_notification_overlay)
.setContentTitle(title)
.setContentText(description)
.setAutoCancel(false)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setContentIntent(pendingIntent)
- .setSound(Settings.System.DEFAULT_NOTIFICATION_URI)
+ .setSound(Settings.System.DEFAULT_ALARM_ALERT_URI)
val name = "Alarm notification service on application kill"
- val descriptionText = "If an alarm was set and the app is killed, a notification will show to warn the user the alarm will not ring as long as the app is killed"
- val importance = NotificationManager.IMPORTANCE_DEFAULT
- val channel = NotificationChannel("com.gdelataillade.alarm.alarm", name, importance).apply {
+ val descriptionText = "If an alarm was set and the app is killed, a notification will show to warn the user the alarm could not ring as long as the app is killed"
+ val importance = NotificationManager.IMPORTANCE_MAX
+ val channel = NotificationChannel(CHANNEL_ID, name, importance).apply {
description = descriptionText
}
@@ -52,7 +53,7 @@ class NotificationOnKillService: Service() {
val notificationManager: NotificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
- notificationManager.notify(123, notificationBuilder.build())
+ notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build())
} catch (e: Exception) {
Log.d("NotificationOnKillService", "Error showing notification", e)
}
@@ -62,4 +63,4 @@ class NotificationOnKillService: Service() {
override fun onBind(intent: Intent?): IBinder? {
return null
}
-}
+}
\ No newline at end of file
diff --git a/android/src/main/kotlin/com/gdelataillade/alarm/services/NotificationService.kt b/android/src/main/kotlin/com/gdelataillade/alarm/services/NotificationService.kt
new file mode 100644
index 00000000..65ee0c2f
--- /dev/null
+++ b/android/src/main/kotlin/com/gdelataillade/alarm/services/NotificationService.kt
@@ -0,0 +1,66 @@
+package com.gdelataillade.alarm.alarm
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.net.Uri
+import android.media.AudioAttributes
+import androidx.core.app.NotificationCompat
+
+class NotificationHandler(private val context: Context) {
+ companion object {
+ private const val CHANNEL_ID = "alarm_service_channel"
+ private const val CHANNEL_NAME = "Alarm Notification"
+ }
+
+ init {
+ createNotificationChannel()
+ }
+
+ private fun createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val soundUri = Uri.parse("android.resource://${context.packageName}/${R.raw.blank}")
+ val channel = NotificationChannel(
+ CHANNEL_ID,
+ CHANNEL_NAME,
+ NotificationManager.IMPORTANCE_MAX
+ ).apply {
+ setSound(soundUri, AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION).setUsage(AudioAttributes.USAGE_NOTIFICATION).build())
+ }
+
+ val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.createNotificationChannel(channel)
+ }
+ }
+
+
+ fun buildNotification(title: String, body: String, fullScreen: Boolean, pendingIntent: PendingIntent): Notification {
+ val iconResId = context.resources.getIdentifier("ic_launcher", "mipmap", context.packageName)
+ val soundUri = Uri.parse("android.resource://${context.packageName}/${R.raw.blank}")
+
+ val intent = context.packageManager.getLaunchIntentForPackage(context.packageName)
+ val pendingIntent = PendingIntent.getActivity(context, 0, intent, 0)
+
+ val notificationBuilder = Notification.Builder(context, CHANNEL_ID)
+ .setSmallIcon(iconResId)
+ .setContentTitle(title)
+ .setContentText(body)
+ .setPriority(NotificationCompat.PRIORITY_MAX)
+ .setCategory(NotificationCompat.CATEGORY_ALARM)
+ .setAutoCancel(false)
+ .setOngoing(true)
+ .setContentIntent(pendingIntent)
+ .setSound(soundUri)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+
+ if (fullScreen) {
+ notificationBuilder.setFullScreenIntent(pendingIntent, true)
+ }
+
+ return notificationBuilder.build()
+ }
+}
\ No newline at end of file
diff --git a/android/src/main/kotlin/com/gdelataillade/alarm/services/VibrationService.kt b/android/src/main/kotlin/com/gdelataillade/alarm/services/VibrationService.kt
new file mode 100644
index 00000000..e3bf6c53
--- /dev/null
+++ b/android/src/main/kotlin/com/gdelataillade/alarm/services/VibrationService.kt
@@ -0,0 +1,19 @@
+package com.gdelataillade.alarm.services
+
+import android.content.Context
+import android.os.Build
+import android.os.VibrationEffect
+import android.os.Vibrator
+
+class VibrationService(private val context: Context) {
+ private val vibrator: Vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
+
+ fun startVibrating(pattern: LongArray, repeat: Int) {
+ val vibrationEffect = VibrationEffect.createWaveform(pattern, repeat)
+ vibrator.vibrate(vibrationEffect)
+ }
+
+ fun stopVibrating() {
+ vibrator.cancel()
+ }
+}
diff --git a/android/src/main/kotlin/com/gdelataillade/alarm/services/VolumeService.kt b/android/src/main/kotlin/com/gdelataillade/alarm/services/VolumeService.kt
new file mode 100644
index 00000000..4f93f376
--- /dev/null
+++ b/android/src/main/kotlin/com/gdelataillade/alarm/services/VolumeService.kt
@@ -0,0 +1,53 @@
+package com.gdelataillade.alarm.services
+
+import android.content.Context
+import android.media.AudioManager
+import android.media.AudioAttributes
+import android.media.AudioFocusRequest
+import android.os.Build
+import kotlin.math.round
+
+class VolumeService(private val context: Context) {
+ private var previousVolume: Int? = null
+ private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
+
+ fun setVolume(volume: Double, showSystemUI: Boolean) {
+ val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
+ previousVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
+ val _volume = (round(volume * maxVolume)).toInt()
+ audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, _volume, if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0)
+ }
+
+ fun restorePreviousVolume(showSystemUI: Boolean) {
+ previousVolume?.let { prevVolume ->
+ audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, prevVolume, if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0)
+ previousVolume = null
+ }
+ }
+
+ fun requestAudioFocus() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val audioAttributes = AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_ALARM)
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ .build()
+
+ val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)
+ .setAudioAttributes(audioAttributes)
+ .build()
+
+ audioManager.requestAudioFocus(focusRequest)
+ } else {
+ @Suppress("DEPRECATION")
+ audioManager.requestAudioFocus(
+ null,
+ AudioManager.STREAM_ALARM,
+ AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
+ )
+ }
+ }
+
+ fun abandonAudioFocus() {
+ audioManager.abandonAudioFocus(null)
+ }
+}
diff --git a/android/src/main/res/raw/blank.mp3 b/android/src/main/res/raw/blank.mp3
new file mode 100644
index 0000000000000000000000000000000000000000..06d5152a7ea6d6e35449f2a5c8f7a20577ae7f64
GIT binary patch
literal 1574
zcmeZtF=k-^ftrwrhzKAp#K6Fuo0yef6rWU-n3uv(SfZf70F@I2$z|pN<;qfviZk=`
z7z|)i5@XlFX!>REXYUsOI2cSD?ZCKovfTWof1sdgf+&h6V;8TcP0pV~YbY
zHiKtgT0W2iRL=mkkaYorF$g$-fe#o&f6oWwn81#U_Ob}QG2Ajd)02rJBgPUOR
z3=BSl!G90{+Qk5J9EipaSQm%{#D2yuhg0nmUjX?o~P!BSv
lj7sKgq>?#DM$Lgl5RUj74IgOw9St8uM#jb(Y~h2&jR0)o;0XW#
literal 0
HcmV?d00001
diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml
index 058e495a..87881aaa 100644
--- a/example/android/app/src/main/AndroidManifest.xml
+++ b/example/android/app/src/main/AndroidManifest.xml
@@ -5,25 +5,23 @@
+
+
+
-
-
-
+
+
-
+
+
+
+
+
{
late bool loopAudio;
late bool vibrate;
late bool volumeMax;
- late bool showNotification;
late String assetAudio;
@override
@@ -33,17 +32,12 @@ class _ExampleAlarmEditScreenState extends State {
loopAudio = true;
vibrate = true;
volumeMax = false;
- showNotification = true;
assetAudio = 'assets/marimba.mp3';
} else {
selectedDateTime = widget.alarmSettings!.dateTime;
loopAudio = widget.alarmSettings!.loopAudio;
vibrate = widget.alarmSettings!.vibrate;
volumeMax = widget.alarmSettings!.volumeMax;
- showNotification = widget.alarmSettings!.notificationTitle != null &&
- widget.alarmSettings!.notificationTitle!.isNotEmpty &&
- widget.alarmSettings!.notificationBody != null &&
- widget.alarmSettings!.notificationBody!.isNotEmpty;
assetAudio = widget.alarmSettings!.assetAudioPath;
}
}
@@ -94,9 +88,9 @@ class _ExampleAlarmEditScreenState extends State {
loopAudio: loopAudio,
vibrate: vibrate,
volumeMax: volumeMax,
- notificationTitle: showNotification ? 'Alarm example' : null,
- notificationBody: showNotification ? 'Your alarm ($id) is ringing' : null,
assetAudioPath: assetAudio,
+ notificationTitle: 'Alarm example',
+ notificationBody: 'Your alarm ($id) is ringing',
);
return alarmSettings;
}
@@ -209,19 +203,6 @@ class _ExampleAlarmEditScreenState extends State {
),
],
),
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(
- 'Show notification',
- style: Theme.of(context).textTheme.titleMedium,
- ),
- Switch(
- value: showNotification,
- onChanged: (value) => setState(() => showNotification = value),
- ),
- ],
- ),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
diff --git a/example/lib/screens/home.dart b/example/lib/screens/home.dart
index a933985e..6185af16 100644
--- a/example/lib/screens/home.dart
+++ b/example/lib/screens/home.dart
@@ -71,7 +71,7 @@ class _ExampleAlarmHomeScreenState extends State {
@override
Widget build(BuildContext context) {
return Scaffold(
- appBar: AppBar(title: const Text('alarm 2.1.1')),
+ appBar: AppBar(title: const Text('alarm 3.0.0-dev.1')),
body: SafeArea(
child: alarms.isNotEmpty
? ListView.separated(
diff --git a/example/lib/screens/shortcut_button.dart b/example/lib/screens/shortcut_button.dart
index adc5f4f7..7bcfccc9 100644
--- a/example/lib/screens/shortcut_button.dart
+++ b/example/lib/screens/shortcut_button.dart
@@ -25,13 +25,14 @@ class _ExampleAlarmHomeShortcutButtonState
setState(() => showMenu = false);
- alarmPrint(dateTime.toString());
-
final alarmSettings = AlarmSettings(
id: DateTime.now().millisecondsSinceEpoch % 10000,
dateTime: dateTime,
assetAudioPath: 'assets/marimba.mp3',
- volumeMax: true,
+ volumeMax: false,
+ notificationTitle: 'Alarm example',
+ notificationBody:
+ 'Shortcut button alarm with delay of $delayInHours hours',
);
await Alarm.set(alarmSettings: alarmSettings);
diff --git a/example/pubspec.lock b/example/pubspec.lock
index cbf4d886..365475d2 100644
--- a/example/pubspec.lock
+++ b/example/pubspec.lock
@@ -7,15 +7,7 @@ packages:
path: ".."
relative: true
source: path
- version: "2.1.1"
- android_alarm_manager_plus:
- dependency: transitive
- description:
- name: android_alarm_manager_plus
- sha256: c20d91a9096596f66274bf8172321c278f9cba8091638f80205fe66d31587fa5
- url: "https://pub.dev"
- source: hosted
- version: "3.0.2"
+ version: "2.2.0"
args:
dependency: transitive
description:
@@ -32,14 +24,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.11.0"
- audio_session:
- dependency: transitive
- description:
- name: audio_session
- sha256: "8a2bc5e30520e18f3fb0e366793d78057fb64cd5287862c76af0c8771f2a52ad"
- url: "https://pub.dev"
- source: hosted
- version: "0.1.16"
boolean_selector:
dependency: transitive
description:
@@ -68,18 +52,10 @@ packages:
dependency: transitive
description:
name: collection
- sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
- url: "https://pub.dev"
- source: hosted
- version: "1.17.2"
- crypto:
- dependency: transitive
- description:
- name: crypto
- sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
+ sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
url: "https://pub.dev"
source: hosted
- version: "3.0.3"
+ version: "1.18.0"
cupertino_icons:
dependency: "direct main"
description:
@@ -96,22 +72,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.8"
- device_info_plus:
- dependency: transitive
- description:
- name: device_info_plus
- sha256: "86add5ef97215562d2e090535b0a16f197902b10c369c558a100e74ea06e8659"
- url: "https://pub.dev"
- source: hosted
- version: "9.0.3"
- device_info_plus_platform_interface:
- dependency: transitive
- description:
- name: device_info_plus_platform_interface
- sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64
- url: "https://pub.dev"
- source: hosted
- version: "7.0.0"
fake_async:
dependency: transitive
description:
@@ -132,10 +92,10 @@ packages:
dependency: transitive
description:
name: file
- sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d"
+ sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
url: "https://pub.dev"
source: hosted
- version: "6.1.4"
+ version: "7.0.0"
flutter:
dependency: "direct main"
description: flutter
@@ -161,10 +121,10 @@ packages:
dependency: transitive
description:
name: flutter_local_notifications
- sha256: "3002092e5b8ce2f86c3361422e52e6db6776c23ee21e0b2f71b892bf4259ef04"
+ sha256: "6d11ea777496061e583623aaf31923f93a9409ef8fcaeeefdd6cd78bf4fe5bb3"
url: "https://pub.dev"
source: hosted
- version: "15.1.1"
+ version: "16.1.0"
flutter_local_notifications_linux:
dependency: transitive
description:
@@ -191,30 +151,6 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
- just_audio:
- dependency: transitive
- description:
- name: just_audio
- sha256: "5ed0cd723e17dfd8cd4b0253726221e67f6546841ea4553635cf895061fc335b"
- url: "https://pub.dev"
- source: hosted
- version: "0.9.35"
- just_audio_platform_interface:
- dependency: transitive
- description:
- name: just_audio_platform_interface
- sha256: d8409da198bbc59426cd45d4c92fca522a2ec269b576ce29459d6d6fcaeb44df
- url: "https://pub.dev"
- source: hosted
- version: "4.2.1"
- just_audio_web:
- dependency: transitive
- description:
- name: just_audio_web
- sha256: ff62f733f437b25a0ff590f0e295fa5441dcb465f1edbdb33b3dea264705bc13
- url: "https://pub.dev"
- source: hosted
- version: "0.4.8"
lints:
dependency: transitive
description:
@@ -243,10 +179,10 @@ packages:
dependency: transitive
description:
name: meta
- sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
+ sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
url: "https://pub.dev"
source: hosted
- version: "1.9.1"
+ version: "1.10.0"
path:
dependency: transitive
description:
@@ -255,30 +191,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.8.3"
- path_provider:
- dependency: transitive
- description:
- name: path_provider
- sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa
- url: "https://pub.dev"
- source: hosted
- version: "2.1.1"
- path_provider_android:
- dependency: transitive
- description:
- name: path_provider_android
- sha256: "6b8b19bd80da4f11ce91b2d1fb931f3006911477cec227cce23d3253d80df3f1"
- url: "https://pub.dev"
- source: hosted
- version: "2.2.0"
- path_provider_foundation:
- dependency: transitive
- description:
- name: path_provider_foundation
- sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d"
- url: "https://pub.dev"
- source: hosted
- version: "2.3.1"
path_provider_linux:
dependency: transitive
description:
@@ -307,18 +219,18 @@ packages:
dependency: transitive
description:
name: petitparser
- sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750
+ sha256: eeb2d1428ee7f4170e2bd498827296a18d4e7fc462b71727d111c0ac7707cfa6
url: "https://pub.dev"
source: hosted
- version: "5.4.0"
+ version: "6.0.1"
platform:
dependency: transitive
description:
name: platform
- sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102
+ sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59"
url: "https://pub.dev"
source: hosted
- version: "3.1.2"
+ version: "3.1.3"
plugin_platform_interface:
dependency: transitive
description:
@@ -327,22 +239,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.6"
- rxdart:
- dependency: transitive
- description:
- name: rxdart
- sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb"
- url: "https://pub.dev"
- source: hosted
- version: "0.27.7"
shared_preferences:
dependency: transitive
description:
name: shared_preferences
- sha256: b7f41bad7e521d205998772545de63ff4e6c97714775902c199353f8bf1511ac
+ sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02"
url: "https://pub.dev"
source: hosted
- version: "2.2.1"
+ version: "2.2.2"
shared_preferences_android:
dependency: transitive
description:
@@ -363,10 +267,10 @@ packages:
dependency: transitive
description:
name: shared_preferences_linux
- sha256: c2eb5bf57a2fe9ad6988121609e47d3e07bb3bdca5b6f8444e4cf302428a128a
+ sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa"
url: "https://pub.dev"
source: hosted
- version: "2.3.1"
+ version: "2.3.2"
shared_preferences_platform_interface:
dependency: transitive
description:
@@ -387,10 +291,10 @@ packages:
dependency: transitive
description:
name: shared_preferences_windows
- sha256: f763a101313bd3be87edffe0560037500967de9c394a714cd598d945517f694f
+ sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59"
url: "https://pub.dev"
source: hosted
- version: "2.3.1"
+ version: "2.3.2"
sky_engine:
dependency: transitive
description: flutter
@@ -404,30 +308,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.10.0"
- sprintf:
- dependency: transitive
- description:
- name: sprintf
- sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
- url: "https://pub.dev"
- source: hosted
- version: "7.0.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
- sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5
+ sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
url: "https://pub.dev"
source: hosted
- version: "1.11.0"
+ version: "1.11.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
- sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8"
+ sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.dev"
source: hosted
- version: "2.1.1"
+ version: "2.1.2"
string_scanner:
dependency: transitive
description:
@@ -448,10 +344,10 @@ packages:
dependency: transitive
description:
name: test_api
- sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8"
+ sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
url: "https://pub.dev"
source: hosted
- version: "0.6.0"
+ version: "0.6.1"
timezone:
dependency: transitive
description:
@@ -460,22 +356,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.9.2"
- typed_data:
- dependency: transitive
- description:
- name: typed_data
- sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
- url: "https://pub.dev"
- source: hosted
- version: "1.3.2"
- uuid:
- dependency: transitive
- description:
- name: uuid
- sha256: b715b8d3858b6fa9f68f87d20d98830283628014750c2b09b6f516c1da4af2a7
- url: "https://pub.dev"
- source: hosted
- version: "4.1.0"
vector_math:
dependency: transitive
description:
@@ -484,46 +364,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
- vibration:
- dependency: transitive
- description:
- name: vibration
- sha256: ab6d26f6694ae0cf702b6d3d1b399570f2911eddb1132c8f82eeacb71a08ece2
- url: "https://pub.dev"
- source: hosted
- version: "1.8.2"
- volume_controller:
- dependency: transitive
- description:
- name: volume_controller
- sha256: "189bdc7a554f476b412e4c8b2f474562b09d74bc458c23667356bce3ca1d48c9"
- url: "https://pub.dev"
- source: hosted
- version: "2.0.7"
web:
dependency: transitive
description:
name: web
- sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
+ sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
url: "https://pub.dev"
source: hosted
- version: "0.1.4-beta"
+ version: "0.3.0"
win32:
dependency: transitive
description:
name: win32
- sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3"
- url: "https://pub.dev"
- source: hosted
- version: "5.0.9"
- win32_registry:
- dependency: transitive
- description:
- name: win32_registry
- sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a"
+ sha256: "7c99c0e1e2fa190b48d25c81ca5e42036d5cac81430ef249027d97b0935c553f"
url: "https://pub.dev"
source: hosted
- version: "1.1.2"
+ version: "5.1.0"
xdg_directories:
dependency: transitive
description:
@@ -536,10 +392,10 @@ packages:
dependency: transitive
description:
name: xml
- sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84"
+ sha256: af5e77e9b83f2f4adc5d3f0a4ece1c7f45a2467b695c2540381bac793e34e556
url: "https://pub.dev"
source: hosted
- version: "6.3.0"
+ version: "6.4.2"
sdks:
- dart: ">=3.1.0 <4.0.0"
+ dart: ">=3.2.0-194.0.dev <4.0.0"
flutter: ">=3.7.0"
diff --git a/help/INSTALL-ANDROID.md b/help/INSTALL-ANDROID.md
index f7f1abcb..eb4b66cb 100644
--- a/help/INSTALL-ANDROID.md
+++ b/help/INSTALL-ANDROID.md
@@ -14,41 +14,53 @@ android {
```
## Step 2
-Then, add the following to your `AndroidManifest.xml` within the `` tags:
+Then, add the following permissions to your `AndroidManifest.xml` within the `` tags:
```xml
-
-
-
-
-
+
+
+
+
+
+
+
+
```
-## Step 3
-Now, within the `` tags, add:
-
-```xml
-
-
-
-
-
-
-
-```
+See more details on Android permissions [here](https://developer.android.com/reference/android/Manifest.permission).
-Finally, if you want your notifications to show in full screen even when the device is locked, add these attributes in ``:
+## Step 3
+Finally, if you want your notifications to show in full screen even when the device is locked (`androidFullScreenIntent` parameter), add these attributes in ``:
```xml
-```
\ No newline at end of file
+```
+
+## Step 4
+Inside the tag of your AndroidManifest.xml, add the following declarations:
+```xml
+
+ [...]
+
+
+
+
+
+
+
+
+
+
+ [...]
+
+```
+
+This setup is essential for managing notifications, especially when the app is terminated or the device is rebooted.
+
+## Additional Resource
+
+For a practical implementation example, you can refer to the example's Android manifest in the plugin repository. This might help you better understand the setup and integration:
+
+[Example AndroidManifest.xml](https://github.com/gdelataillade/alarm/blob/main/example/android/app/src/main/AndroidManifest.xml)
\ No newline at end of file
diff --git a/ios/Classes/SwiftAlarmPlugin.swift b/ios/Classes/SwiftAlarmPlugin.swift
index 985e5a4d..e561385b 100644
--- a/ios/Classes/SwiftAlarmPlugin.swift
+++ b/ios/Classes/SwiftAlarmPlugin.swift
@@ -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
diff --git a/lib/alarm.dart b/lib/alarm.dart
index 7806041a..fcb39d46 100644
--- a/lib/alarm.dart
+++ b/lib/alarm.dart
@@ -54,18 +54,16 @@ class Alarm {
if (alarm.dateTime.isAfter(now)) {
await set(alarmSettings: alarm);
} else {
- await AlarmStorage.unsaveAlarm(alarm.id);
+ final isRinging = await Alarm.isRinging(alarm.id);
+ isRinging ? ringStream.add(alarm) : stop(alarm.id);
}
}
}
- /// Schedules an alarm with given [alarmSettings].
+ /// Schedules an alarm with given [alarmSettings] with its notification.
///
/// If you set an alarm for the same [dateTime] as an existing one,
/// the new alarm will replace the existing one.
- ///
- /// Also, schedules notification if [notificationTitle] and [notificationBody]
- /// are not null nor empty.
static Future set({required AlarmSettings alarmSettings}) async {
if (!alarmSettings.assetAudioPath.contains('.')) {
throw AlarmException(
@@ -84,22 +82,18 @@ 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 (iOS) {
+ 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();
+ await AlarmNotification.instance.requestNotificationPermission();
}
if (iOS) {
@@ -150,8 +144,9 @@ class Alarm {
}
/// Whether the alarm is ringing.
- static Future isRinging(int id) async =>
- iOS ? await IOSAlarm.checkIfRinging(id) : AndroidAlarm.isRinging;
+ static Future isRinging(int id) async => iOS
+ ? await IOSAlarm.checkIfRinging(id)
+ : await AndroidAlarm.isRinging(id);
/// Whether an alarm is set.
static bool hasAlarm() => AlarmStorage.hasAlarm();
diff --git a/lib/model/alarm_settings.dart b/lib/model/alarm_settings.dart
index d20612af..7d621a2e 100644
--- a/lib/model/alarm_settings.dart
+++ b/lib/model/alarm_settings.dart
@@ -30,23 +30,16 @@ class AlarmSettings {
final double fadeDuration;
/// Title of the notification to be shown when alarm is triggered.
- /// Must not be null nor empty to show a notification.
- final String? notificationTitle;
+ final String notificationTitle;
/// Body of the notification to be shown when alarm is triggered.
- /// Must not be null nor empty to show a notification.
- final String? notificationBody;
+ final String notificationBody;
/// Whether to show a notification when application is killed to warn
/// the user that the alarms won't ring anymore. Enabled by default.
final bool enableNotificationOnKill;
- /// Stops the alarm on opened notification.
- final bool stopOnNotificationOpen;
-
/// Whether to turn screen on when android alarm notification is triggered. Enabled by default.
- ///
- /// [notificationTitle] and [notificationBody] must not be null nor empty.
final bool androidFullScreenIntent;
/// Returns a hash code for this `AlarmSettings` instance using Jenkins hash function.
@@ -61,10 +54,9 @@ class AlarmSettings {
hash = hash ^ vibrate.hashCode;
hash = hash ^ volumeMax.hashCode;
hash = hash ^ fadeDuration.hashCode;
- hash = hash ^ (notificationTitle?.hashCode ?? 0);
- hash = hash ^ (notificationBody?.hashCode ?? 0);
+ hash = hash ^ (notificationTitle.hashCode);
+ hash = hash ^ (notificationBody.hashCode);
hash = hash ^ enableNotificationOnKill.hashCode;
- hash = hash ^ stopOnNotificationOpen.hashCode;
hash = hash & 0x3fffffff;
return hash;
@@ -83,10 +75,9 @@ class AlarmSettings {
this.vibrate = true,
this.volumeMax = true,
this.fadeDuration = 0.0,
- this.notificationTitle,
- this.notificationBody,
+ required this.notificationTitle,
+ required this.notificationBody,
this.enableNotificationOnKill = true,
- this.stopOnNotificationOpen = false,
this.androidFullScreenIntent = true,
});
@@ -99,12 +90,12 @@ class AlarmSettings {
vibrate: json['vibrate'] as bool,
volumeMax: json['volumeMax'] as bool,
fadeDuration: json['fadeDuration'] as double,
- notificationTitle: json['notificationTitle'] as String?,
- notificationBody: json['notificationBody'] as String?,
- enableNotificationOnKill: json['enableNotificationOnKill'] as bool,
- stopOnNotificationOpen: json['stopOnNotificationOpen'] as bool,
+ notificationTitle: json['notificationTitle'] as String,
+ notificationBody: json['notificationBody'] as String,
+ enableNotificationOnKill:
+ json['enableNotificationOnKill'] as bool? ?? true,
androidFullScreenIntent:
- json['androidFullScreenIntent'] as bool? ?? false,
+ json['androidFullScreenIntent'] as bool? ?? true,
);
/// Creates a copy of `AlarmSettings` but with the given fields replaced with
@@ -120,7 +111,6 @@ class AlarmSettings {
String? notificationTitle,
String? notificationBody,
bool? enableNotificationOnKill,
- bool? stopOnNotificationOpen,
bool? androidFullScreenIntent,
}) {
return AlarmSettings(
@@ -135,8 +125,6 @@ class AlarmSettings {
notificationBody: notificationBody ?? this.notificationBody,
enableNotificationOnKill:
enableNotificationOnKill ?? this.enableNotificationOnKill,
- stopOnNotificationOpen:
- stopOnNotificationOpen ?? this.stopOnNotificationOpen,
androidFullScreenIntent:
androidFullScreenIntent ?? this.androidFullScreenIntent,
);
@@ -154,7 +142,6 @@ class AlarmSettings {
'notificationTitle': notificationTitle,
'notificationBody': notificationBody,
'enableNotificationOnKill': enableNotificationOnKill,
- 'stopOnNotificationOpen': stopOnNotificationOpen,
'androidFullScreenIntent': androidFullScreenIntent,
};
@@ -183,6 +170,5 @@ class AlarmSettings {
notificationTitle == other.notificationTitle &&
notificationBody == other.notificationBody &&
enableNotificationOnKill == other.enableNotificationOnKill &&
- stopOnNotificationOpen == other.stopOnNotificationOpen &&
androidFullScreenIntent == other.androidFullScreenIntent;
}
diff --git a/lib/service/notification.dart b/lib/service/notification.dart
index f2b15215..5852caa2 100644
--- a/lib/service/notification.dart
+++ b/lib/service/notification.dart
@@ -25,60 +25,34 @@ class AlarmNotification {
requestAlertPermission: false,
requestSoundPermission: false,
requestBadgePermission: false,
- onDidReceiveLocalNotification: onSelectNotificationOldIOS,
);
const initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsIOS,
);
- await localNotif.initialize(
- initializationSettings,
- onDidReceiveBackgroundNotificationResponse: onSelectNotification,
- onDidReceiveNotificationResponse: onSelectNotification,
- );
+ await localNotif.initialize(initializationSettings);
tz.initializeTimeZones();
}
- // Callback to stop the alarm when the notification is opened.
- static onSelectNotification(NotificationResponse notificationResponse) async {
- if (notificationResponse.id == null) return;
- await stopAlarm(notificationResponse.id!);
- }
-
- // Callback to stop the alarm when the notification is opened for iOS versions older than 10.
- static onSelectNotificationOldIOS(
- int? id,
- String? _,
- String? __,
- String? ___,
- ) async {
- if (id != null) await stopAlarm(id);
- }
-
- /// Stops the alarm.
- static Future stopAlarm(int id) async {
- if (Alarm.getAlarm(id)?.stopOnNotificationOpen != null &&
- Alarm.getAlarm(id)!.stopOnNotificationOpen) {
- await Alarm.stop(id);
+ /// Shows notification permission request.
+ Future requestNotificationPermission() async {
+ if (defaultTargetPlatform == TargetPlatform.iOS) {
+ final res = await localNotif
+ .resolvePlatformSpecificImplementation<
+ IOSFlutterLocalNotificationsPlugin>()
+ ?.requestPermissions(alert: true, badge: true, sound: true);
+ return res ?? false;
}
- }
+ if (defaultTargetPlatform == TargetPlatform.android) {
+ final res = await localNotif
+ .resolvePlatformSpecificImplementation<
+ AndroidFlutterLocalNotificationsPlugin>()
+ ?.requestNotificationsPermission();
- /// Shows notification permission request.
- Future requestPermission() async {
- bool? result;
-
- result = defaultTargetPlatform == TargetPlatform.android
- ? await localNotif
- .resolvePlatformSpecificImplementation<
- AndroidFlutterLocalNotificationsPlugin>()
- ?.requestPermission()
- : await localNotif
- .resolvePlatformSpecificImplementation<
- IOSFlutterLocalNotificationsPlugin>()
- ?.requestPermissions(alert: true, badge: true, sound: true);
-
- return result ?? false;
+ return res ?? false;
+ }
+ return false;
}
tz.TZDateTime nextInstanceOfTime(DateTime dateTime) {
@@ -114,6 +88,7 @@ class AlarmNotification {
playSound: false,
enableLights: true,
fullScreenIntent: fullScreenIntent,
+ visibility: NotificationVisibility.public,
);
final platformChannelSpecifics = NotificationDetails(
@@ -123,10 +98,9 @@ class AlarmNotification {
final zdt = nextInstanceOfTime(dateTime);
- final hasPermission = await requestPermission();
- if (!hasPermission) {
+ final hasNotificationPermission = await requestNotificationPermission();
+ if (!hasNotificationPermission) {
alarmPrint('Notification permission not granted');
- return;
}
try {
@@ -136,7 +110,7 @@ class AlarmNotification {
body,
tz.TZDateTime.from(zdt.toUtc(), tz.UTC),
platformChannelSpecifics,
- androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
+ androidScheduleMode: AndroidScheduleMode.alarmClock,
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
);
@@ -150,8 +124,5 @@ class AlarmNotification {
/// Cancels notification. Called when the alarm is cancelled or
/// when an alarm is overriden.
- Future cancel(int id) async {
- await localNotif.cancel(id);
- alarmPrint('Notification with id $id canceled');
- }
+ Future cancel(int id) => localNotif.cancel(id);
}
diff --git a/lib/src/android_alarm.dart b/lib/src/android_alarm.dart
index 94a3ac06..ae2bf225 100644
--- a/lib/src/android_alarm.dart
+++ b/lib/src/android_alarm.dart
@@ -1,104 +1,57 @@
import 'dart:async';
-import 'dart:isolate';
-import 'dart:ui';
-
import 'package:alarm/alarm.dart';
import 'package:alarm/service/storage.dart';
-import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart';
import 'package:flutter/services.dart';
-import 'package:just_audio/just_audio.dart';
-import 'package:vibration/vibration.dart';
-import 'package:volume_controller/volume_controller.dart';
-/// For Android support, [AndroidAlarmManager] is used to trigger a callback
-/// when the given time is reached. The callback will run in an isolate if app
-/// is in background.
+/// Uses method channel to interact with the native platform.
class AndroidAlarm {
- static const ringPort = 'alarm-ring';
- static const stopPort = 'alarm-stop';
-
- static const platform =
- MethodChannel('com.gdelataillade.alarm/notifOnAppKill');
-
- static bool ringing = false;
- static bool vibrationsActive = false;
- static double? previousVolume;
+ static const platform = MethodChannel('com.gdelataillade.alarm/alarm');
- static bool get isRinging => ringing;
static bool get hasOtherAlarms => AlarmStorage.getSavedAlarms().length > 1;
- /// Initializes AndroidAlarmManager dependency.
- static Future init() => AndroidAlarmManager.initialize();
+ static Future init() async {
+ platform.setMethodCallHandler(handleMethodCall);
+ }
- /// Creates isolate communication channel and set alarm at given [dateTime].
+ static Future handleMethodCall(MethodCall call) async {
+ try {
+ if (call.method == 'alarmRinging') {
+ int id = call.arguments['id'];
+ final settings = Alarm.getAlarm(id);
+ if (settings != null) Alarm.ringStream.add(settings);
+ }
+ } catch (e) {
+ alarmPrint('[DEV] Handle method call "${call.method}" error: $e');
+ }
+ }
+
+ /// Schedules a native alarm with given [alarmSettings] with its notification.
static Future set(
AlarmSettings settings,
void Function()? onRing,
) async {
- final id = settings.id;
try {
- final port = ReceivePort();
- final success = IsolateNameServer.registerPortWithName(
- port.sendPort,
- "$ringPort-$id",
+ final delay = settings.dateTime.difference(DateTime.now());
+
+ await platform.invokeMethod(
+ 'setAlarm',
+ {
+ 'id': settings.id,
+ 'delayInSeconds': delay.inSeconds,
+ 'assetAudioPath': settings.assetAudioPath,
+ 'loopAudio': settings.loopAudio,
+ 'vibrate': settings.vibrate,
+ 'volume': settings.volumeMax ? 1.0 : -1.0,
+ 'fadeDuration': settings.fadeDuration,
+ 'notificationTitle': settings.notificationTitle,
+ 'notificationBody': settings.notificationBody,
+ 'fullScreenIntent': settings.androidFullScreenIntent,
+ },
);
-
- if (!success) {
- IsolateNameServer.removePortNameMapping("$ringPort-$id");
- IsolateNameServer.registerPortWithName(port.sendPort, "$ringPort-$id");
- }
- port.listen((message) {
- alarmPrint('$message');
- if (message == 'ring') {
- ringing = true;
- if (settings.volumeMax) setMaximumVolume();
- onRing?.call();
- } else {
- if (settings.vibrate &&
- message is String &&
- message.startsWith('vibrate')) {
- final audioDuration = message.split('-').last;
-
- if (int.tryParse(audioDuration) != null) {
- final duration = Duration(seconds: int.parse(audioDuration));
- triggerVibrations(duration: settings.loopAudio ? null : duration);
- }
- }
- }
- });
} catch (e) {
- throw AlarmException('Isolate error: $e');
+ throw AlarmException('nativeAndroidAlarm error: $e');
}
- if (settings.dateTime.difference(DateTime.now()).inSeconds <= 1) {
- await playAlarm(id, {
- "assetAudioPath": settings.assetAudioPath,
- "loopAudio": settings.loopAudio,
- "fadeDuration": settings.fadeDuration,
- });
- return true;
- }
-
- final res = await AndroidAlarmManager.oneShotAt(
- settings.dateTime,
- id,
- AndroidAlarm.playAlarm,
- alarmClock: true,
- allowWhileIdle: true,
- exact: true,
- rescheduleOnReboot: true,
- wakeup: true,
- params: {
- 'assetAudioPath': settings.assetAudioPath,
- 'loopAudio': settings.loopAudio,
- 'fadeDuration': settings.fadeDuration,
- },
- );
-
- alarmPrint(
- 'Alarm with id $id scheduled ${res ? 'successfully' : 'failed'} at ${settings.dateTime}',
- );
-
if (settings.enableNotificationOnKill && !hasOtherAlarms) {
try {
await platform.invokeMethod(
@@ -108,166 +61,35 @@ class AndroidAlarm {
'description': AlarmStorage.getNotificationOnAppKillBody(),
},
);
- alarmPrint('NotificationOnKillService set with success');
} catch (e) {
throw AlarmException('NotificationOnKillService error: $e');
}
}
- return res;
- }
-
- /// Callback triggered when alarmDateTime is reached.
- /// The message `ring` is sent to the main thread in order to
- /// tell the device that the alarm is starting to ring.
- /// Alarm is played with AudioPlayer and stopped when the message `stop`
- /// is received from the main thread.
- @pragma('vm:entry-point')
- static Future playAlarm(int id, Map data) async {
- final audioPlayer = AudioPlayer();
-
- final res = IsolateNameServer.lookupPortByName("$ringPort-$id");
- if (res == null) throw const AlarmException('Isolate port not found');
-
- final send = res;
- send.send('ring');
-
- try {
- final assetAudioPath = data['assetAudioPath'] as String;
- Duration? audioDuration;
-
- if (assetAudioPath.startsWith('http')) {
- send.send('Network URL not supported. Please provide local asset.');
- return;
- }
-
- audioDuration = assetAudioPath.startsWith('assets/')
- ? await audioPlayer.setAsset(assetAudioPath)
- : await audioPlayer.setFilePath(assetAudioPath);
-
- send.send('vibrate-${audioDuration?.inSeconds}');
-
- final loopAudio = data['loopAudio'] as bool;
- if (loopAudio) audioPlayer.setLoopMode(LoopMode.all);
-
- send.send('Alarm data received in isolate: $data');
-
- final fadeDuration = data['fadeDuration'];
-
- send.send('Alarm fadeDuration: $fadeDuration seconds');
-
- if (fadeDuration > 0.0) {
- int counter = 0;
-
- audioPlayer.setVolume(0.1);
- audioPlayer.play();
-
- send.send('Alarm playing with fadeDuration ${fadeDuration}s');
-
- Timer.periodic(
- Duration(milliseconds: fadeDuration * 1000 ~/ 10),
- (timer) {
- counter++;
- audioPlayer.setVolume(counter / 10);
- if (counter >= 10) timer.cancel();
- },
- );
- } else {
- audioPlayer.play();
- send.send('Alarm with id $id starts playing.');
- }
- } catch (e) {
- await AudioPlayer.clearAssetCache();
- send.send('Asset cache reset. Please try again.');
- throw AlarmException(
- "Alarm with id $id and asset path '${data['assetAudioPath']}' error: $e",
- );
- }
-
- try {
- final port = ReceivePort();
- final success =
- IsolateNameServer.registerPortWithName(port.sendPort, stopPort);
-
- if (!success) {
- IsolateNameServer.removePortNameMapping(stopPort);
- IsolateNameServer.registerPortWithName(port.sendPort, stopPort);
- }
-
- port.listen((message) async {
- send.send('(isolate) received: $message');
- if (message == 'stop') {
- await audioPlayer.stop();
- await audioPlayer.dispose();
- port.close();
- }
- });
- } catch (e) {
- throw AlarmException('Isolate error: $e');
- }
- }
-
- /// Triggers vibrations when alarm is ringing if [vibrationsActive] is true.
- ///
- /// If [loopAudio] is false, vibrations are triggered repeatedly during
- /// [duration] which is the duration of the audio.
- static Future triggerVibrations({Duration? duration}) async {
- final hasVibrator = await Vibration.hasVibrator() ?? false;
-
- if (!hasVibrator) {
- alarmPrint('Vibrations are not available on this device.');
- return;
- }
-
- vibrationsActive = true;
+ alarmPrint('[DEV] Alarm with id ${settings.id} scheduled');
- if (duration == null) {
- while (vibrationsActive) {
- Vibration.vibrate();
- await Future.delayed(const Duration(milliseconds: 1000));
- }
- } else {
- final endTime = DateTime.now().add(duration);
- while (vibrationsActive && DateTime.now().isBefore(endTime)) {
- Vibration.vibrate();
- await Future.delayed(const Duration(milliseconds: 1000));
- }
- }
- }
-
- /// Sets the device volume to the maximum.
- static Future setMaximumVolume() async {
- previousVolume = await VolumeController().getVolume();
- VolumeController().setVolume(1.0, showSystemUI: true);
+ return true;
}
/// Sends the message `stop` to the isolate so the audio player
/// can stop playing and dispose.
static Future stop(int id) async {
- ringing = false;
- vibrationsActive = false;
-
- final send = IsolateNameServer.lookupPortByName(stopPort);
-
- if (send != null) {
- send.send('stop');
- alarmPrint('Alarm with id $id stopped');
- }
-
- if (previousVolume != null) {
- VolumeController().setVolume(previousVolume!, showSystemUI: true);
- previousVolume = null;
- }
-
+ final res = await platform.invokeMethod('stopAlarm', {'id': id});
+ if (res) alarmPrint('[DEV] Alarm with id $id stopped');
if (!hasOtherAlarms) stopNotificationOnKillService();
+ return res;
+ }
- return await AndroidAlarmManager.cancel(id);
+ /// Checks if the alarm with given [id] is ringing.
+ static Future isRinging(int id) async {
+ final res = await platform.invokeMethod('isRinging', {'id': id});
+ return res;
}
+ /// Disable the notification on kill service.
static Future stopNotificationOnKillService() async {
try {
await platform.invokeMethod('stopNotificationOnKillService');
- alarmPrint('NotificationOnKillService stopped with success');
} catch (e) {
throw AlarmException('NotificationOnKillService error: $e');
}
diff --git a/lib/src/ios_alarm.dart b/lib/src/ios_alarm.dart
index fba07a90..c8757c80 100644
--- a/lib/src/ios_alarm.dart
+++ b/lib/src/ios_alarm.dart
@@ -89,7 +89,7 @@ class IOSAlarm {
) ??
false;
- if (res) alarmPrint('Alarm with id $id stopped with success');
+ if (res) alarmPrint('Alarm with id $id stopped');
return res;
}
diff --git a/pubspec.yaml b/pubspec.yaml
index c1cfbe21..c16c3c16 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,6 +1,6 @@
name: alarm
description: A simple Flutter alarm manager plugin for both iOS and Android.
-version: 2.1.1
+version: 3.0.0-dev.1
homepage: https://github.com/gdelataillade/alarm
environment:
@@ -8,26 +8,22 @@ environment:
flutter: ">=2.5.0"
dependencies:
- android_alarm_manager_plus: ^3.0.1
flutter:
sdk: flutter
flutter_fgbg: ^0.3.0
- flutter_local_notifications: ^15.1.0
- just_audio: ^0.9.34
- plugin_platform_interface: ^2.1.4
- shared_preferences: ^2.1.2
+ flutter_local_notifications: ^16.1.0
+ shared_preferences: ^2.2.2
timezone: ^0.9.2
- vibration: ^1.8.1
- volume_controller: ^2.0.7
dev_dependencies:
flutter_test:
sdk: flutter
- flutter_lints: ^2.0.0
+ flutter_lints: ^3.0.1
flutter:
assets:
- assets/long_blank.mp3
+ - assets/not_blank.mp3
plugin:
platforms: