Skip to content

Commit

Permalink
[Feature] Allow specifying the alarm volume using steps/stops (#282)
Browse files Browse the repository at this point in the history
* Add tests for fadeDuration and isRinging to example app

* Use alarmPrint instead of debugPrint

* Add staircase fade feature

* Implement staircase fade for Android

* Migrate to pigeon and revamp public API

* Update deps

* Migrate Kotlin and Swift code to Pigeon framework

* Allow system volume customization when using audio fade

* Update to v5

* Revert new alarm delay change

* [Android] Store a copy of alarms natively
  • Loading branch information
orkun1675 authored Dec 10, 2024
1 parent c62bcfd commit 5f6e53d
Show file tree
Hide file tree
Showing 40 changed files with 1,050 additions and 1,048 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 5.0.0
* **BREAKING**: Old alarms (alarms created pre v5) will be deleted.
* BREAKING: Some API parameters have been renamed, this update requires a small amount of refactoring for users. No features have been removed.
* Add support for fading the alarm volume using a staircase function.

## 4.1.1
* [Android] Show app on lock screen when alarm rings.

Expand Down
2 changes: 1 addition & 1 deletion analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
include: package:very_good_analysis/analysis_options.yaml

analyzer:
exclude: [lib/src/generated/**]
exclude: [lib/src/generated/**, lib/**/*.g.dart]
errors:
todo: ignore
9 changes: 7 additions & 2 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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"
}
}

Expand All @@ -23,6 +24,7 @@ allprojects {

apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'

android {
namespace 'com.gdelataillade.alarm.alarm'
Expand Down Expand Up @@ -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'
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import com.gdelataillade.alarm.services.AudioService
import com.gdelataillade.alarm.services.AlarmStorage
import com.gdelataillade.alarm.services.VibrationService
import com.gdelataillade.alarm.services.VolumeService
import com.gdelataillade.alarm.models.NotificationSettings
import com.google.gson.Gson

import android.app.Service
import android.app.PendingIntent
Expand All @@ -17,9 +15,11 @@ import android.content.pm.ServiceInfo
import android.os.IBinder
import android.os.PowerManager
import android.os.Build
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
Expand Down Expand Up @@ -65,20 +65,23 @@ class AlarmService : Service() {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)

val notificationSettingsJson = intent.getStringExtra("notificationSettings")
val notificationSettings = if (notificationSettingsJson != null) {
val gson = Gson()
gson.fromJson(notificationSettingsJson, NotificationSettings::class.java)
} else {
val notificationTitle = intent.getStringExtra("notificationTitle") ?: "Title"
val notificationBody = intent.getStringExtra("notificationBody") ?: "Body"
NotificationSettings(notificationTitle, notificationBody)
val alarmSettingsJson = intent.getStringExtra("alarmSettings")
if (alarmSettingsJson == null) {
Log.e("AlarmService", "Intent is missing AlarmSettings.")
return START_NOT_STICKY
}

val alarmSettings: AlarmSettings
try {
alarmSettings = Json.decodeFromString<AlarmSettings>(alarmSettingsJson)
} catch (e: Exception) {
Log.e("AlarmService", "Cannot parse AlarmSettings from Intent.")
return START_NOT_STICKY
}

val fullScreenIntent = intent.getBooleanExtra("fullScreenIntent", true)
val notification = notificationHandler.buildNotification(
notificationSettings,
fullScreenIntent,
alarmSettings.notificationSettings,
alarmSettings.androidFullScreenIntent,
pendingIntent,
id
)
Expand All @@ -98,6 +101,14 @@ class AlarmService : Service() {
} catch (e: SecurityException) {
Log.e("AlarmService", "Security exception in starting foreground service", e)
return START_NOT_STICKY
} catch (e: Exception) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (e is ForegroundServiceStartNotAllowedException) {
Log.e("AlarmService", "Foreground service start not allowed", e)
return START_NOT_STICKY
}
}
throw e
}

// Check if an alarm is already ringing
Expand All @@ -107,48 +118,50 @@ class AlarmService : Service() {
return START_NOT_STICKY
}

if (fullScreenIntent) {
if (alarmSettings.androidFullScreenIntent) {
AlarmRingingLiveData.instance.update(true)
}

// Proceed with handling the new alarm
val assetAudioPath = intent.getStringExtra("assetAudioPath") ?: return START_NOT_STICKY
val loopAudio = intent.getBooleanExtra("loopAudio", true)
val vibrate = intent.getBooleanExtra("vibrate", true)
val volume = intent.getDoubleExtra("volume", -1.0)
val volumeEnforced = intent.getBooleanExtra("volumeEnforced", false)
val fadeDuration = intent.getDoubleExtra("fadeDuration", 0.0)

// Notify the plugin about the alarm ringing
AlarmPlugin.alarmTriggerApi?.alarmRang(id.toLong()) {
Log.d("AlarmService", "Flutter was notified that alarm $id is ringing.")
}

// Set the volume if specified
if (volume in 0.0..1.0) {
volumeService?.setVolume(volume, volumeEnforced, showSystemUI)
if (alarmSettings.volumeSettings.volume != null) {
volumeService?.setVolume(
alarmSettings.volumeSettings.volume,
alarmSettings.volumeSettings.volumeEnforced,
showSystemUI
)
}

// Request audio focus
volumeService?.requestAudioFocus()

// Set up audio completion listener
audioService?.setOnAudioCompleteListener {
if (!loopAudio) {
if (!alarmSettings.loopAudio) {
vibrationService?.stopVibrating()
volumeService?.restorePreviousVolume(showSystemUI)
volumeService?.abandonAudioFocus()
}
}

// Play the alarm audio
audioService?.playAudio(id, assetAudioPath, loopAudio, fadeDuration)
audioService?.playAudio(
id,
alarmSettings.assetAudioPath,
alarmSettings.loopAudio,
alarmSettings.volumeSettings.fadeDuration,
alarmSettings.volumeSettings.fadeSteps
)

// Update the list of ringing alarms
ringingAlarmIds = audioService?.getPlayingMediaPlayersIds() ?: listOf()

// Start vibration if enabled
if (vibrate) {
if (alarmSettings.vibrate) {
vibrationService?.startVibrating(longArrayOf(0, 500, 500), 1)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +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 com.google.gson.Gson
import io.flutter.Log
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

class AlarmApiImpl(private val context: Context) : AlarmApi {
private val alarmIds: MutableList<Int> = mutableListOf()
Expand Down Expand Up @@ -50,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)
}
Expand Down Expand Up @@ -87,6 +90,7 @@ class AlarmApiImpl(private val context: Context) : AlarmApi {
)
}
alarmIds.add(alarm.id)
AlarmStorage(context).saveAlarm(alarm)
}

private fun createAlarmIntent(alarm: AlarmSettings): Intent {
Expand All @@ -97,18 +101,7 @@ class AlarmApiImpl(private val context: Context) : AlarmApi {

private fun setIntentExtras(intent: Intent, alarm: AlarmSettings) {
intent.putExtra("id", alarm.id)
intent.putExtra("assetAudioPath", alarm.assetAudioPath)
intent.putExtra("loopAudio", alarm.loopAudio)
intent.putExtra("vibrate", alarm.vibrate)
intent.putExtra("volume", alarm.volume)
intent.putExtra("volumeEnforced", alarm.volumeEnforced)
intent.putExtra("fadeDuration", alarm.fadeDuration)
intent.putExtra("fullScreenIntent", alarm.androidFullScreenIntent)

val notificationSettingsMap = alarm.notificationSettings
val gson = Gson()
val notificationSettingsJson = gson.toJson(notificationSettingsMap)
intent.putExtra("notificationSettings", notificationSettingsJson)
intent.putExtra("alarmSettings", Json.encodeToString(alarm))
}

private fun handleImmediateAlarm(intent: Intent, delayInSeconds: Int) {
Expand Down
Loading

0 comments on commit 5f6e53d

Please sign in to comment.