Skip to content

Commit

Permalink
Merge branch 'feat/notification-action-buttons'
Browse files Browse the repository at this point in the history
  • Loading branch information
gdelataillade committed Aug 30, 2024
2 parents 785dc47 + 4441153 commit 000c605
Show file tree
Hide file tree
Showing 33 changed files with 872 additions and 324 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 4.0.0-dev.2
* [Android] Fix native GSON parsing error.
* Update `flutter_fgbg` dependency.

## 4.0.0-dev.1
* Add notification action stop button.

## 3.1.7
* Update kotlin version to `1.7.10`.

Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ final alarmSettings = AlarmSettings(
notificationTitle: 'This is the title',
notificationBody: 'This is the body',
enableNotificationOnKill: Platform.isIOS,
notificationActionSettings: const NotificationActionSettings(
hasStopButton: true,
),
);
```

Expand All @@ -66,6 +69,7 @@ notificationTitle | `String` | The title of the notification triggered whe
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.
androidFullScreenIntent | `bool` | Whether to turn screen on when android alarm notification is triggered. Enabled by default.
notificationActionSettings | `NotificationActionSettings` | Settings for notification action buttons (only stop at the moment). Won't work on iOS if app was killed. Disabled by default.

Note that if `notificationTitle` and `notificationBody` are both empty, iOS will not show the notification and Android will show an empty notification.

Expand Down Expand Up @@ -181,7 +185,6 @@ We welcome contributions to this plugin! If you would like to make a change or a

These are some features that I have in mind that could be useful:
- [Android] Reschedule alarms after device reboot.
- Notification actions: stop and snooze.
- Use `ffigen` and `jnigen` binding generators to call native code more efficiently instead of using method channels.
- Stop alarm sound when notification is dismissed.

Expand Down
10 changes: 10 additions & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,14 @@ android {
defaultConfig {
minSdkVersion 19
}

buildTypes {
release {
consumerProguardFiles 'proguard-rules.pro'
}
}
}

dependencies {
implementation 'com.google.code.gson:gson:2.11.0'
}
24 changes: 24 additions & 0 deletions android/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Keep all classes in your plugin's package
-keep class com.gdelataillade.alarm.** { *; }

# Keep all classes related to Gson and prevent them from being obfuscated
-keep class com.google.gson.** { *; }
-keep class sun.misc.Unsafe { *; }
-keepattributes Signature
-keepattributes *Annotation*

# Prevent stripping of methods/fields annotated with specific annotations, if needed.
-keepclassmembers class * {
@com.google.gson.annotations.SerializedName <fields>;
}

# Preserve classes that might be used in reflection or through indirect means
-keepclassmembers class **.R$* {
<fields>;
}

# Avoid stripping enums, if your plugin uses them
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
2 changes: 1 addition & 1 deletion android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<uses-permission android:name="android.permission.USE_EXACT_ALARM"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<application>
<receiver android:name=".AlarmReceiver" />
<receiver android:name=".AlarmReceiver" android:exported="true" />
<service
android:name=".AlarmService"
android:exported="false"
Expand Down
147 changes: 99 additions & 48 deletions android/src/main/kotlin/com/gdelataillade/alarm/alarm/AlarmPlugin.kt
Original file line number Diff line number Diff line change
@@ -1,28 +1,39 @@
package com.gdelataillade.alarm.alarm

import com.gdelataillade.alarm.services.NotificationOnKillService
import com.gdelataillade.alarm.services.AlarmStorage
import com.gdelataillade.alarm.models.AlarmSettings

import android.os.Build
import android.os.Handler
import android.os.Looper
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import androidx.annotation.NonNull
import java.util.Date
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.Log
import org.json.JSONObject

class AlarmPlugin: FlutterPlugin, MethodCallHandler {
private lateinit var context: Context
private lateinit var methodChannel : MethodChannel
private lateinit var methodChannel: MethodChannel
private lateinit var eventChannel: EventChannel

private val alarmIds: MutableList<Int> = mutableListOf()
private var notifOnKillEnabled: Boolean = false
private var notificationOnKillTitle: String = "Your alarms may not ring"
private var notificationOnKillBody: String = "You killed the app. Please reopen so your alarms can be rescheduled."

companion object {
@JvmStatic
var eventSink: EventChannel.EventSink? = null
Expand All @@ -49,18 +60,7 @@ class AlarmPlugin: FlutterPlugin, MethodCallHandler {
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"setAlarm" -> {
val id = call.argument<Int>("id")!!
val delayInSeconds = call.argument<Int>("delayInSeconds")!!

val alarmIntent = createAlarmIntent(context, call, id)

if (delayInSeconds <= 5) {
handleImmediateAlarm(context, alarmIntent, delayInSeconds)
} else {
handleDelayedAlarm(context, alarmIntent, delayInSeconds, id)
}

result.success(true)
setAlarm(call, result)
}
"stopAlarm" -> {
val id = call.argument<Int>("id")
Expand All @@ -69,29 +69,7 @@ class AlarmPlugin: FlutterPlugin, MethodCallHandler {
return
}

// Check if the alarm is currently ringing
if (AlarmService.ringingAlarmIds.contains(id)) {
// If the alarm is ringing, stop the alarm service for this ID
val stopIntent = Intent(context, AlarmService::class.java)
stopIntent.action = "STOP_ALARM"
stopIntent.putExtra("id", id)
context.stopService(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 or PendingIntent.FLAG_IMMUTABLE
)

// Cancel the future alarm using AlarmManager
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmManager.cancel(pendingIntent)

result.success(true)
stopAlarm(id, result)
}
"isRinging" -> {
val id = call.argument<Int>("id")
Expand All @@ -100,20 +78,14 @@ class AlarmPlugin: FlutterPlugin, MethodCallHandler {
result.success(isRinging)
}
"setNotificationOnKillService" -> {
val title = call.argument<String>("title")
val body = call.argument<String>("body")

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

context.startService(serviceIntent)

if (call.argument<String>("title") != null && call.argument<String>("body") != null) {
notificationOnKillTitle = call.argument<String>("title")!!
notificationOnKillBody = call.argument<String>("body")!!
}
result.success(true)
}
"stopNotificationOnKillService" -> {
val serviceIntent = Intent(context, NotificationOnKillService::class.java)
context.stopService(serviceIntent)
stopNotificationOnKillService(context)
result.success(true)
}
else -> {
Expand All @@ -122,6 +94,62 @@ class AlarmPlugin: FlutterPlugin, MethodCallHandler {
}
}

fun setAlarm(call: MethodCall, result: Result) {
val alarmJsonMap = call.arguments as? Map<String, Any>
if (alarmJsonMap != null) {
val alarm = AlarmSettings.fromJson(alarmJsonMap)
if (alarm != null) {
val alarmIntent = createAlarmIntent(context, call, alarm.id)
val delayInSeconds = (alarm.dateTime.time - System.currentTimeMillis()) / 1000

if (delayInSeconds <= 5) {
handleImmediateAlarm(context, alarmIntent, delayInSeconds.toInt())
} else {
handleDelayedAlarm(context, alarmIntent, delayInSeconds.toInt(), alarm.id, alarm.enableNotificationOnKill)
}
alarmIds.add(alarm.id)
result.success(true)
} else {
result.error("INVALID_ALARM", "Failed to parse alarm JSON", null)
}
} else {
result.error("INVALID_ARGUMENTS", "Invalid arguments provided for setAlarm", null)
}
}

fun stopAlarm(id: Int, result: Result? = null) {
// Check if the alarm is currently ringing
if (AlarmService.ringingAlarmIds.contains(id)) {
// If the alarm is ringing, stop the alarm service for this ID
val stopIntent = Intent(context, AlarmService::class.java)
stopIntent.action = "STOP_ALARM"
stopIntent.putExtra("id", id)
context.stopService(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 or PendingIntent.FLAG_IMMUTABLE
)

// Cancel the future alarm using AlarmManager
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmManager.cancel(pendingIntent)

alarmIds.remove(id)
if (alarmIds.isEmpty() && notifOnKillEnabled) {
stopNotificationOnKillService(context)
}

if (result != null) {
result.success(true)
}
}

fun createAlarmIntent(context: Context, call: MethodCall, id: Int?): Intent {
val alarmIntent = Intent(context, AlarmReceiver::class.java)
setIntentExtras(alarmIntent, call, id)
Expand All @@ -138,6 +166,10 @@ class AlarmPlugin: FlutterPlugin, MethodCallHandler {
intent.putExtra("notificationTitle", call.argument<String>("notificationTitle"))
intent.putExtra("notificationBody", call.argument<String>("notificationBody"))
intent.putExtra("fullScreenIntent", call.argument<Boolean>("fullScreenIntent"))

val notificationActionSettingsMap = call.argument<Map<String, Any>>("notificationActionSettings")
val notificationActionSettingsJson = JSONObject(notificationActionSettingsMap ?: emptyMap<String, Any>()).toString()
intent.putExtra("notificationActionSettings", notificationActionSettingsJson)
}

fun handleImmediateAlarm(context: Context, intent: Intent, delayInSeconds: Int) {
Expand All @@ -147,7 +179,7 @@ class AlarmPlugin: FlutterPlugin, MethodCallHandler {
}, delayInSeconds * 1000L)
}

fun handleDelayedAlarm(context: Context, intent: Intent, delayInSeconds: Int, id: Int) {
fun handleDelayedAlarm(context: Context, intent: Intent, delayInSeconds: Int, id: Int, enableNotificationOnKill: Boolean) {
try {
val triggerTime = System.currentTimeMillis() + delayInSeconds * 1000L
val pendingIntent = PendingIntent.getBroadcast(
Expand All @@ -167,6 +199,10 @@ class AlarmPlugin: FlutterPlugin, MethodCallHandler {
} else {
alarmManager.set(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent)
}

if (enableNotificationOnKill && !notifOnKillEnabled) {
setNotificationOnKillService(context)
}
} catch (e: ClassCastException) {
Log.e("AlarmPlugin", "AlarmManager service type casting failed", e)
} catch (e: IllegalStateException) {
Expand All @@ -176,6 +212,21 @@ class AlarmPlugin: FlutterPlugin, MethodCallHandler {
}
}

fun setNotificationOnKillService(context: Context) {
val serviceIntent = Intent(context, NotificationOnKillService::class.java)
serviceIntent.putExtra("title", notificationOnKillTitle)
serviceIntent.putExtra("body", notificationOnKillBody)

context.startService(serviceIntent)
notifOnKillEnabled = true
}

fun stopNotificationOnKillService(context: Context) {
val serviceIntent = Intent(context, NotificationOnKillService::class.java)
context.stopService(serviceIntent)
notifOnKillEnabled = false
}

override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
methodChannel.setMethodCallHandler(null)
eventChannel.setStreamHandler(null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,18 @@ import android.os.Build
import io.flutter.Log

class AlarmReceiver : BroadcastReceiver() {
companion object {
const val ACTION_ALARM_STOP = "com.gdelataillade.alarm.ACTION_STOP"
const val EXTRA_ALARM_ACTION = "EXTRA_ALARM_ACTION"
}

override fun onReceive(context: Context, intent: Intent) {
val action = intent.action
if (action == ACTION_ALARM_STOP) {
intent.putExtra(EXTRA_ALARM_ACTION, "STOP_ALARM")
}

// Start Alarm Service
val serviceIntent = Intent(context, AlarmService::class.java)
serviceIntent.putExtras(intent)

Expand All @@ -21,4 +32,4 @@ class AlarmReceiver : BroadcastReceiver() {
context.startService(serviceIntent)
}
}
}
}
Loading

0 comments on commit 000c605

Please sign in to comment.