Skip to content

Commit

Permalink
Implement staircase fade for Android
Browse files Browse the repository at this point in the history
  • Loading branch information
orkun1675 committed Nov 1, 2024
1 parent 00243f8 commit 31417a9
Show file tree
Hide file tree
Showing 15 changed files with 102 additions and 93 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
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
Expand All @@ -11,8 +10,6 @@ import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
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
Expand All @@ -35,7 +32,7 @@ class AlarmPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
var eventSink: EventChannel.EventSink? = null
}

override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext

methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.gdelataillade.alarm/alarm")
Expand All @@ -53,7 +50,7 @@ class AlarmPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
})
}

override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: MethodChannel.Result) {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"setAlarm" -> {
setAlarm(call, result)
Expand Down Expand Up @@ -135,7 +132,7 @@ class AlarmPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
}
}

fun stopAlarm(id: Int, result: MethodChannel.Result? = null) {
private fun stopAlarm(id: Int, result: MethodChannel.Result? = null) {
if (AlarmService.ringingAlarmIds.contains(id)) {
val stopIntent = Intent(context, AlarmService::class.java)
stopIntent.action = "STOP_ALARM"
Expand Down Expand Up @@ -164,19 +161,21 @@ class AlarmPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
result?.success(true)
}

fun createAlarmIntent(context: Context, call: MethodCall, id: Int?): Intent {
private 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?) {
private fun setIntentExtras(intent: Intent, call: MethodCall, id: Int?) {
intent.putExtra("id", id)
intent.putExtra("assetAudioPath", call.argument<String>("assetAudioPath"))
intent.putExtra("loopAudio", call.argument<Boolean>("loopAudio") ?: true)
intent.putExtra("vibrate", call.argument<Boolean>("vibrate") ?: true)
intent.putExtra("volume", call.argument<Double>("volume"))
intent.putExtra("fadeDuration", call.argument<Double>("fadeDuration") ?: 0.0)
intent.putExtra("fadeStopTimes", call.argument<ArrayList<Double>>("fadeStopTimes") ?: arrayListOf<Double>())
intent.putExtra("fadeStopVolumes", call.argument<ArrayList<Double>>("fadeStopVolumes") ?: arrayListOf<Double>())
intent.putExtra("fullScreenIntent", call.argument<Boolean>("fullScreenIntent") ?: true)

val notificationSettingsMap = call.argument<Map<String, Any>>("notificationSettings")
Expand All @@ -185,14 +184,14 @@ class AlarmPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
intent.putExtra("notificationSettings", notificationSettingsJson)
}

fun handleImmediateAlarm(context: Context, intent: Intent, delayInSeconds: Int) {
private fun handleImmediateAlarm(context: Context, intent: Intent, delayInSeconds: Int) {
val handler = Handler(Looper.getMainLooper())
handler.postDelayed({
context.sendBroadcast(intent)
}, delayInSeconds * 1000L)
}

fun handleDelayedAlarm(
private fun handleDelayedAlarm(
context: Context,
intent: Intent,
delayInSeconds: Int,
Expand Down Expand Up @@ -231,7 +230,7 @@ class AlarmPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
}
}

fun setWarningNotificationOnKill(context: Context) {
private fun setWarningNotificationOnKill(context: Context) {
val serviceIntent = Intent(context, NotificationOnKillService::class.java)
serviceIntent.putExtra("title", notificationOnKillTitle)
serviceIntent.putExtra("body", notificationOnKillBody)
Expand All @@ -240,13 +239,13 @@ class AlarmPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
notifOnKillEnabled = true
}

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

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

class AlarmReceiver : BroadcastReceiver() {
companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,8 @@ import android.content.pm.ServiceInfo
import android.os.IBinder
import android.os.PowerManager
import android.os.Build
import com.gdelataillade.alarm.services.NotificationHandler
import io.flutter.Log
import io.flutter.embedding.engine.dart.DartExecutor
import io.flutter.embedding.engine.FlutterEngine
import org.json.JSONObject

class AlarmService : Service() {
private var audioService: AudioService? = null
Expand Down Expand Up @@ -89,12 +87,17 @@ class AlarmService : Service() {
} else {
startForeground(id, notification)
}
} catch (e: ForegroundServiceStartNotAllowedException) {
Log.e("AlarmService", "Foreground service start not allowed", e)
return START_NOT_STICKY
} 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 @@ -110,6 +113,8 @@ class AlarmService : Service() {
val vibrate = intent.getBooleanExtra("vibrate", true)
val volume = intent.getDoubleExtra("volume", -1.0)
val fadeDuration = intent.getDoubleExtra("fadeDuration", 0.0)
val fadeStopTimes = intent.getSerializableExtra("fadeStopTimes") as? ArrayList<Double> ?: arrayListOf()
val fadeStopVolumes = intent.getSerializableExtra("fadeStopVolumes") as? ArrayList<Double> ?: arrayListOf()

// Notify the plugin about the alarm ringing
AlarmPlugin.eventSink?.success(
Expand All @@ -120,7 +125,7 @@ class AlarmService : Service() {
)

// Set the volume if specified
if (volume >= 0.0 && volume <= 1.0) {
if (volume in 0.0..1.0) {
volumeService?.setVolume(volume, showSystemUI)
}

Expand All @@ -137,7 +142,7 @@ class AlarmService : Service() {
}

// Play the alarm audio
audioService?.playAudio(id, assetAudioPath, loopAudio, fadeDuration)
audioService?.playAudio(id, assetAudioPath, loopAudio, fadeDuration, fadeStopTimes, fadeStopVolumes)

// Update the list of ringing alarms
ringingAlarmIds = audioService?.getPlayingMediaPlayersIds() ?: listOf()
Expand All @@ -155,7 +160,7 @@ class AlarmService : Service() {
return START_STICKY
}

fun unsaveAlarm(id: Int) {
private fun unsaveAlarm(id: Int) {
AlarmStorage(this).unsaveAlarm(id)
AlarmPlugin.eventSink?.success(mapOf(
"id" to id,
Expand All @@ -164,7 +169,7 @@ class AlarmService : Service() {
stopAlarm(id)
}

fun stopAlarm(id: Int) {
private fun stopAlarm(id: Int) {
try {
val playingIds = audioService?.getPlayingMediaPlayersIds() ?: listOf()
ringingAlarmIds = playingIds
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,10 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
import com.gdelataillade.alarm.alarm.AlarmPlugin
import com.gdelataillade.alarm.services.AlarmStorage
import com.google.gson.Gson
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel

import android.app.NotificationChannel
import android.app.NotificationManager
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat

class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
Expand All @@ -32,22 +24,19 @@ class BootReceiver : BroadcastReceiver() {
Log.d("BootReceiver", "Rescheduling ${storedAlarms.size} alarms")

for (alarm in storedAlarms) {
if (alarm.notificationSettings == null) {
Log.d("BootReceiver", "Skipping alarm with ID: ${alarm.id} due to missing notificationSettings")
continue
}

var alarmArgs: Map<String, Any>? = null

try {
// Create the arguments for the MethodCall
alarmArgs = mapOf(
"id" to alarm.id,
"dateTime" to alarm.dateTime.time,
"assetAudioPath" to (alarm.assetAudioPath ?: ""),
"assetAudioPath" to alarm.assetAudioPath,
"loopAudio" to alarm.loopAudio,
"vibrate" to alarm.vibrate,
"fadeDuration" to alarm.fadeDuration,
"fadeStopTimes" to alarm.fadeStopTimes,
"fadeStopVolumes" to alarm.fadeStopVolumes,
"fullScreenIntent" to alarm.androidFullScreenIntent,
"notificationSettings" to mapOf(
"title" to alarm.notificationSettings.title,
Expand All @@ -58,7 +47,7 @@ class BootReceiver : BroadcastReceiver() {
).toMutableMap()

alarm.volume?.let {
(alarmArgs as MutableMap)[ "volume" ] = it
alarmArgs[ "volume" ] = it
}

Log.d("BootReceiver", "Rescheduling alarm with ID: ${alarm.id}")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.gdelataillade.alarm.models

import com.google.gson.*
import com.google.gson.annotations.SerializedName
import java.util.Date
import io.flutter.Log
import java.lang.reflect.Type
Expand All @@ -15,6 +14,8 @@ data class AlarmSettings(
val vibrate: Boolean,
val volume: Double?,
val fadeDuration: Double,
val fadeStopTimes: List<Double>,
val fadeStopVolumes: List<Double>,
val warningNotificationOnKill: Boolean,
val androidFullScreenIntent: Boolean
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import io.flutter.Log

class AlarmStorage(context: Context) {
companion object {
private const val PREFS_NAME = "alarm_prefs"
private const val PREFIX = "flutter.__alarm_id__"
}

Expand All @@ -33,7 +32,7 @@ class AlarmStorage(context: Context) {
}

fun getSavedAlarms(): List<AlarmSettings> {
val gsonBuilder = GsonBuilder().registerTypeAdapter(Date::class.java, JsonDeserializer<Date> { json, _, _ ->
val gsonBuilder = GsonBuilder().registerTypeAdapter(Date::class.java, JsonDeserializer { json, _, _ ->
Date(json.asJsonPrimitive.asLong)
})
val gson: Gson = gsonBuilder.create()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ class AudioService(private val context: Context) {
return mediaPlayers.filter { (_, mediaPlayer) -> mediaPlayer.isPlaying }.keys.toList()
}

fun playAudio(id: Int, filePath: String, loopAudio: Boolean, fadeDuration: Double?) {
fun playAudio(id: Int, filePath: String, loopAudio: Boolean, fadeDuration: Double?, fadeStopTimes: List<Double>, fadeStopVolumes: List<Double>) {
stopAudio(id) // Stop and release any existing MediaPlayer and Timer for this ID

val baseAppFlutterPath = context.filesDir.parent + "/app_flutter/"
val baseAppFlutterPath = context.filesDir.parent?.plus("/app_flutter/")
val adjustedFilePath = when {
filePath.startsWith("assets/") -> "flutter_assets/$filePath"
!filePath.startsWith("/") -> baseAppFlutterPath + filePath
Expand Down Expand Up @@ -62,7 +62,11 @@ class AudioService(private val context: Context) {

mediaPlayers[id] = this

if (fadeDuration != null && fadeDuration > 0) {
if (fadeStopTimes.isNotEmpty()) {
val timer = Timer(true)
timers[id] = timer
startStaircaseFadeIn(this, fadeStopTimes, fadeStopVolumes, timer)
} else if (fadeDuration != null && fadeDuration > 0) {
val timer = Timer(true)
timers[id] = timer
startFadeIn(this, fadeDuration, timer)
Expand Down Expand Up @@ -114,6 +118,48 @@ class AudioService(private val context: Context) {
}, 0, fadeInterval)
}

private fun startStaircaseFadeIn(mediaPlayer: MediaPlayer, stopTimes: List<Double>, stopVolumes: List<Double>, timer: Timer) {
if (stopTimes.size != stopVolumes.size) {
Log.e("AudioService", "Stop times and volumes don't have the same length.")
return
}

val fadeInterval = 100L
var currentStep = 0

timer.schedule(object : TimerTask() {
override fun run() {
if (!mediaPlayer.isPlaying) {
cancel()
return
}

val currentTime = (currentStep * fadeInterval) / 1000
val nextIndex = stopTimes.indexOfFirst { it >= currentTime }

if (nextIndex < 0) {
cancel()
return
}

val nextVolume = stopVolumes[nextIndex]
var currentVolume = nextVolume

if (nextIndex > 0) {
val prevTime = stopTimes[nextIndex - 1]
val nextTime = stopTimes[nextIndex]
val nextRatio = (currentTime - prevTime) / (nextTime - prevTime)

val prevVolume = stopVolumes[nextIndex - 1]
currentVolume = nextVolume * nextRatio + prevVolume * (1 - nextRatio)
}

mediaPlayer.setVolume(currentVolume.toFloat(), currentVolume.toFloat())
currentStep++
}
}, 0, fadeInterval)
}

fun cleanUp() {
timers.values.forEach(Timer::cancel)
timers.clear()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.provider.Settings
import android.os.Build
import android.os.IBinder
Expand All @@ -16,10 +14,13 @@ import androidx.core.app.NotificationCompat
import io.flutter.Log

class NotificationOnKillService : Service() {
companion object {
const val NOTIFICATION_ID = 88888
const val CHANNEL_ID = "com.gdelataillade.alarm.alarm_channel"
}

private lateinit var title: String
private lateinit var body: 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 could not ring"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.gdelataillade.alarm.alarm
package com.gdelataillade.alarm.services

import com.gdelataillade.alarm.models.NotificationSettings
import android.app.Notification
Expand All @@ -9,6 +9,7 @@ import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.core.app.NotificationCompat
import com.gdelataillade.alarm.alarm.AlarmReceiver

class NotificationHandler(private val context: Context) {
companion object {
Expand Down
Loading

0 comments on commit 31417a9

Please sign in to comment.