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: