From b698c10a8f76684ce7aff36015b2f510d6f1452d Mon Sep 17 00:00:00 2001 From: Orkun Duman Date: Tue, 10 Dec 2024 00:03:30 -0500 Subject: [PATCH] [Android] Store a copy of alarms natively --- android/build.gradle | 9 ++- .../gdelataillade/alarm/alarm/AlarmService.kt | 7 +- .../gdelataillade/alarm/api/AlarmApiImpl.kt | 7 +- .../alarm/models/AlarmSettings.kt | 55 ++++----------- .../alarm/models/NotificationSettings.kt | 2 + .../alarm/models/VolumeSettings.kt | 3 + .../alarm/services/AlarmStorage.kt | 70 ++++++++++++------- 7 files changed, 79 insertions(+), 74 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index b17c01df..ae5a3974 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,7 +2,7 @@ group 'com.gdelataillade.alarm.alarm' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.8.22' + ext.kotlin_version = '1.9.25' repositories { google() mavenCentral() @@ -11,6 +11,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:8.2.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" } } @@ -23,6 +24,7 @@ allprojects { apply plugin: 'com.android.library' apply plugin: 'kotlin-android' +apply plugin: 'kotlinx-serialization' android { namespace 'com.gdelataillade.alarm.alarm' @@ -54,5 +56,8 @@ android { } dependencies { - implementation 'com.google.code.gson:gson:2.11.0' + implementation 'androidx.datastore:datastore:1.1.1' + implementation 'androidx.datastore:datastore-preferences:1.1.1' + //noinspection GradleDependency: 1.7+ requires Kotlin 2 + implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3' } \ No newline at end of file diff --git a/android/src/main/kotlin/com/gdelataillade/alarm/alarm/AlarmService.kt b/android/src/main/kotlin/com/gdelataillade/alarm/alarm/AlarmService.kt index 8a6a1e52..4efc5f2c 100644 --- a/android/src/main/kotlin/com/gdelataillade/alarm/alarm/AlarmService.kt +++ b/android/src/main/kotlin/com/gdelataillade/alarm/alarm/AlarmService.kt @@ -19,6 +19,7 @@ import com.gdelataillade.alarm.models.AlarmSettings import com.gdelataillade.alarm.services.AlarmRingingLiveData import com.gdelataillade.alarm.services.NotificationHandler import io.flutter.Log +import kotlinx.serialization.json.Json class AlarmService : Service() { private var audioService: AudioService? = null @@ -70,8 +71,10 @@ class AlarmService : Service() { return START_NOT_STICKY } - val alarmSettings = AlarmSettings.fromJson(alarmSettingsJson) - if (alarmSettings == null) { + val alarmSettings: AlarmSettings + try { + alarmSettings = Json.decodeFromString(alarmSettingsJson) + } catch (e: Exception) { Log.e("AlarmService", "Cannot parse AlarmSettings from Intent.") return START_NOT_STICKY } diff --git a/android/src/main/kotlin/com/gdelataillade/alarm/api/AlarmApiImpl.kt b/android/src/main/kotlin/com/gdelataillade/alarm/api/AlarmApiImpl.kt index fc32b0e0..43d0a330 100644 --- a/android/src/main/kotlin/com/gdelataillade/alarm/api/AlarmApiImpl.kt +++ b/android/src/main/kotlin/com/gdelataillade/alarm/api/AlarmApiImpl.kt @@ -12,8 +12,11 @@ import android.os.Looper import com.gdelataillade.alarm.alarm.AlarmReceiver import com.gdelataillade.alarm.alarm.AlarmService import com.gdelataillade.alarm.models.AlarmSettings +import com.gdelataillade.alarm.services.AlarmStorage import com.gdelataillade.alarm.services.NotificationOnKillService import io.flutter.Log +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json class AlarmApiImpl(private val context: Context) : AlarmApi { private val alarmIds: MutableList = mutableListOf() @@ -49,6 +52,7 @@ class AlarmApiImpl(private val context: Context) : AlarmApi { alarmManager.cancel(pendingIntent) alarmIds.remove(id) + AlarmStorage(context).unsaveAlarm(id) if (alarmIds.isEmpty() && notifyOnKillEnabled) { disableWarningNotificationOnKill(context) } @@ -86,6 +90,7 @@ class AlarmApiImpl(private val context: Context) : AlarmApi { ) } alarmIds.add(alarm.id) + AlarmStorage(context).saveAlarm(alarm) } private fun createAlarmIntent(alarm: AlarmSettings): Intent { @@ -96,7 +101,7 @@ class AlarmApiImpl(private val context: Context) : AlarmApi { private fun setIntentExtras(intent: Intent, alarm: AlarmSettings) { intent.putExtra("id", alarm.id) - intent.putExtra("alarmSettings", alarm.toJson()) + intent.putExtra("alarmSettings", Json.encodeToString(alarm)) } private fun handleImmediateAlarm(intent: Intent, delayInSeconds: Int) { diff --git a/android/src/main/kotlin/com/gdelataillade/alarm/models/AlarmSettings.kt b/android/src/main/kotlin/com/gdelataillade/alarm/models/AlarmSettings.kt index d7062771..9e72e9b1 100644 --- a/android/src/main/kotlin/com/gdelataillade/alarm/models/AlarmSettings.kt +++ b/android/src/main/kotlin/com/gdelataillade/alarm/models/AlarmSettings.kt @@ -1,13 +1,18 @@ package com.gdelataillade.alarm.models import AlarmSettingsWire -import com.google.gson.* +import kotlinx.serialization.KSerializer import java.util.Date -import io.flutter.Log -import java.lang.reflect.Type +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +@Serializable data class AlarmSettings( val id: Int, + @Serializable(with = DateSerializer::class) val dateTime: Date, val assetAudioPath: String, val volumeSettings: VolumeSettings, @@ -31,47 +36,11 @@ data class AlarmSettings( e.androidFullScreenIntent, ) } - - fun fromJson(jsonString: String): AlarmSettings? { - return try { - val gson = GsonBuilder() - .registerTypeAdapter(Date::class.java, DateDeserializer()) - .create() - gson.fromJson(jsonString, AlarmSettings::class.java) - } catch (e: Exception) { - Log.e("AlarmSettings", "Error parsing JSON to AlarmSettings: ${e.message}", e) - null - } - } - } - - fun toJson(): String { - val gson = GsonBuilder() - .registerTypeAdapter(Date::class.java, DateSerializer()) - .create() - return gson.toJson(this) } } -class DateDeserializer : JsonDeserializer { - override fun deserialize( - json: JsonElement?, - typeOfT: Type?, - context: JsonDeserializationContext? - ): Date { - val dateTimeMicroseconds = json?.asLong ?: 0L - val dateTimeMilliseconds = dateTimeMicroseconds / 1000 - return Date(dateTimeMilliseconds) - } +object DateSerializer : KSerializer { + override val descriptor = PrimitiveSerialDescriptor("Date", PrimitiveKind.LONG) + override fun serialize(encoder: Encoder, value: Date) = encoder.encodeLong(value.time) + override fun deserialize(decoder: Decoder): Date = Date(decoder.decodeLong()) } - -class DateSerializer : JsonSerializer { - override fun serialize( - src: Date?, - typeOfSrc: Type?, - context: JsonSerializationContext? - ): JsonElement { - val dateTimeMicroseconds = src?.time?.times(1000) ?: 0L - return JsonPrimitive(dateTimeMicroseconds) - } -} \ No newline at end of file diff --git a/android/src/main/kotlin/com/gdelataillade/alarm/models/NotificationSettings.kt b/android/src/main/kotlin/com/gdelataillade/alarm/models/NotificationSettings.kt index 0d9fe22b..bd479b8e 100644 --- a/android/src/main/kotlin/com/gdelataillade/alarm/models/NotificationSettings.kt +++ b/android/src/main/kotlin/com/gdelataillade/alarm/models/NotificationSettings.kt @@ -1,7 +1,9 @@ package com.gdelataillade.alarm.models import NotificationSettingsWire +import kotlinx.serialization.Serializable +@Serializable data class NotificationSettings( val title: String, val body: String, diff --git a/android/src/main/kotlin/com/gdelataillade/alarm/models/VolumeSettings.kt b/android/src/main/kotlin/com/gdelataillade/alarm/models/VolumeSettings.kt index 97fba188..f977bc97 100644 --- a/android/src/main/kotlin/com/gdelataillade/alarm/models/VolumeSettings.kt +++ b/android/src/main/kotlin/com/gdelataillade/alarm/models/VolumeSettings.kt @@ -2,9 +2,11 @@ package com.gdelataillade.alarm.models import VolumeFadeStepWire import VolumeSettingsWire +import kotlinx.serialization.Serializable import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds +@Serializable data class VolumeSettings( val volume: Double?, val fadeDuration: Duration?, @@ -23,6 +25,7 @@ data class VolumeSettings( } } +@Serializable data class VolumeFadeStep( val time: Duration, val volume: Double diff --git a/android/src/main/kotlin/com/gdelataillade/alarm/services/AlarmStorage.kt b/android/src/main/kotlin/com/gdelataillade/alarm/services/AlarmStorage.kt index 0198dc96..4989e455 100644 --- a/android/src/main/kotlin/com/gdelataillade/alarm/services/AlarmStorage.kt +++ b/android/src/main/kotlin/com/gdelataillade/alarm/services/AlarmStorage.kt @@ -3,50 +3,68 @@ package com.gdelataillade.alarm.services import com.gdelataillade.alarm.models.AlarmSettings import android.content.Context -import android.content.SharedPreferences import io.flutter.Log +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.serialization.json.Json +import kotlinx.serialization.encodeToString + +const val SHARED_PREFERENCES_NAME = "AlarmSharedPreferences" + +private val Context.dataStore: DataStore by +preferencesDataStore(SHARED_PREFERENCES_NAME) class AlarmStorage(context: Context) { companion object { - private const val PREFIX = "flutter.__alarm_id__" + private const val PREFIX = "__alarm_id__" } - private val prefs: SharedPreferences = - context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE) + private val dataStore = context.dataStore - // TODO(gdelataillade): Ensure this function is called or remove it. fun saveAlarm(alarmSettings: AlarmSettings) { - val key = "$PREFIX${alarmSettings.id}" - val editor = prefs.edit() - editor.putString(key, alarmSettings.toJson()) - editor.apply() + return runBlocking { + val key = stringPreferencesKey("$PREFIX${alarmSettings.id}") + val value = Json.encodeToString(alarmSettings) + dataStore.edit { preferences -> preferences[key] = value } + } } fun unsaveAlarm(id: Int) { - val key = "$PREFIX$id" - val editor = prefs.edit() - editor.remove(key) - editor.apply() + return runBlocking { + val key = stringPreferencesKey("$PREFIX$id") + dataStore.edit { preferences -> preferences.remove(key) } + } } fun getSavedAlarms(): List { - val alarms = mutableListOf() - prefs.all.forEach { (key, value) -> - if (key.startsWith(PREFIX) && value is String) { - try { - val alarm = AlarmSettings.fromJson(value) - if (alarm != null) { + return runBlocking { + val preferences = dataStore.data.map { prefs -> + prefs.asMap().filterKeys { it.name.startsWith(PREFIX) } + }.first() + + val alarms = mutableListOf() + preferences.forEach { (key, value) -> + if (value is String) { + try { + val alarm = Json.decodeFromString(value) alarms.add(alarm) - } else { - Log.e("AlarmStorage", "Alarm for key $key could not be deserialized") + } catch (e: Exception) { + Log.e( + "AlarmStorage", + "Error parsing alarm settings for key ${key.name}: ${e.message}" + ) } - } catch (e: Exception) { - Log.e("AlarmStorage", "Error parsing alarm settings for key $key: ${e.message}") + } else { + Log.w("AlarmStorage", "Skipping non-alarm preference with key: ${key.name}") } - } else { - Log.w("AlarmStorage", "Skipping non-alarm preference with key: $key") } + alarms } - return alarms } } \ No newline at end of file