Skip to content

Commit

Permalink
fix: can't set custom timer sound on recent Android versions
Browse files Browse the repository at this point in the history
  • Loading branch information
Bnyro committed Feb 7, 2025
1 parent 47e2735 commit f4c6949
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 39 deletions.
6 changes: 6 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@
android:value="alarm" />
</service>

<receiver android:name=".util.receivers.DeleteNotificationChannelReceiver"
android:enabled="true"
android:exported="false">

</receiver>

<receiver
android:name=".presentation.widgets.DigitalClockWidget"
android:enabled="true"
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/com/bnyro/clock/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class App : Application() {
super.onCreate()

Preferences.init(this)
NotificationHelper().createNotificationChannels(this)
NotificationHelper.createStaticNotificationChannels(this)

container = AppContainer(database)
}
Expand Down
69 changes: 48 additions & 21 deletions app/src/main/java/com/bnyro/clock/util/NotificationHelper.kt
Original file line number Diff line number Diff line change
@@ -1,30 +1,64 @@
package com.bnyro.clock.util

import android.content.Context
import android.content.Intent
import android.media.AudioAttributes
import android.net.Uri
import androidx.annotation.StringRes
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationManagerCompat
import com.bnyro.clock.R
import com.bnyro.clock.util.receivers.DeleteNotificationChannelReceiver

class NotificationHelper {
companion object {
const val STOPWATCH_CHANNEL = "stopwatch"
const val TIMER_CHANNEL = "timer"
const val TIMER_SERVICE_CHANNEL = "timer_service"
const val TIMER_FINISHED_CHANNEL = "timer_finished"
const val ALARM_CHANNEL = "alarm"
object NotificationHelper {
const val STOPWATCH_CHANNEL = "stopwatch"
const val TIMER_CHANNEL = "timer"
const val TIMER_SERVICE_CHANNEL = "timer_service"
const val TIMER_FINISHED_CHANNEL = "timer_finished"
const val ALARM_CHANNEL = "alarm"

val vibrationPattern = longArrayOf(1000, 1000, 1000, 1000, 1000)
val vibrationPattern = longArrayOf(1000, 1000, 1000, 1000, 1000)

val audioAttributes: AudioAttributes? = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ALARM)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build()
val audioAttributes: AudioAttributes? = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ALARM)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build()

/**
* Create a temporary dynamic notification channel.
*
* The returned intent must be called when the notification is dismissed to delete the channel!
*/
fun createDynamicChannel(
context: Context,
@StringRes nameRes: Int,
channelId: String,
ringtoneUri: Uri?,
vibrationPattern: LongArray?
): Intent {
val nManager = NotificationManagerCompat.from(context)

val channel = NotificationChannelCompat.Builder(
channelId,
NotificationManagerCompat.IMPORTANCE_MAX
)
.setName(context.getString(nameRes))

if (ringtoneUri != null) channel.setSound(ringtoneUri, audioAttributes)
if (vibrationPattern != null) channel.setVibrationPattern(vibrationPattern)
channel.setVibrationEnabled(vibrationPattern != null)

nManager.createNotificationChannel(channel.build())

return Intent(context, DeleteNotificationChannelReceiver::class.java)
.putExtra(
DeleteNotificationChannelReceiver.NOTIFICATION_CHANNEL_ID_EXTRA,
channelId
)
}

fun createNotificationChannels(context: Context) {
fun createStaticNotificationChannels(context: Context) {
val nManager = NotificationManagerCompat.from(context)
val ringtoneHelper = RingtoneHelper()

val channels = listOf(
NotificationChannelCompat.Builder(
Expand All @@ -45,13 +79,6 @@ class NotificationHelper {
)
.setName(context.getString(R.string.timer_service))
.build(),
NotificationChannelCompat.Builder(
TIMER_FINISHED_CHANNEL,
NotificationManagerCompat.IMPORTANCE_HIGH
)
.setName(context.getString(R.string.timer_finished))
.setSound(ringtoneHelper.getDefault(context), audioAttributes)
.build(),
NotificationChannelCompat.Builder(
ALARM_CHANNEL,
NotificationManagerCompat.IMPORTANCE_MAX
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import com.bnyro.clock.util.services.AlarmService
import kotlinx.coroutines.runBlocking

class AlarmReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
override fun onReceive(context: Context, intent: Intent?) {
if (intent == null) return

Log.e("receiver", "received")
val id = intent.getLongExtra(AlarmHelper.EXTRA_ID, -1).takeIf { it != -1L } ?: return
val alarmRepository = (context.applicationContext as App).container.alarmRepository
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.bnyro.clock.util.receivers

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.core.app.NotificationManagerCompat

class DeleteNotificationChannelReceiver: BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val channelId = intent?.getStringExtra(NOTIFICATION_CHANNEL_ID_EXTRA) ?: return
val nManager = NotificationManagerCompat.from(context ?: return)

nManager.deleteNotificationChannel(channelId)
}

companion object {
const val NOTIFICATION_CHANNEL_ID_EXTRA = "notification_channel_id"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import android.content.IntentFilter
import android.media.MediaPlayer
import android.media.RingtoneManager
import android.net.Uri
import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.os.Looper
Expand Down
51 changes: 36 additions & 15 deletions app/src/main/java/com/bnyro/clock/util/services/TimerService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import com.bnyro.clock.domain.model.TimerObject
import com.bnyro.clock.domain.model.WatchState
import com.bnyro.clock.util.NotificationHelper
import com.bnyro.clock.util.RingtoneHelper
import com.bnyro.clock.util.receivers.DeleteNotificationChannelReceiver
import java.util.Timer
import java.util.TimerTask

Expand Down Expand Up @@ -183,22 +184,42 @@ class TimerService : Service() {
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
) {
val notification = NotificationCompat.Builder(
this,
NotificationHelper.TIMER_FINISHED_CHANNEL
)
.setSmallIcon(R.drawable.ic_notification)
.setSound(timerObject.ringtone ?: RingtoneHelper().getDefault(this))
.setVibrate(NotificationHelper.vibrationPattern.takeIf { timerObject.vibrate })
.setContentTitle(getString(R.string.timer_finished))
.setContentText(timerObject.label.value)
.build()
) != PackageManager.PERMISSION_GRANTED
) return

NotificationManagerCompat.from(this)
.notify(Integer.MAX_VALUE - timerObject.id, notification)
}
val ringtoneUri = timerObject.ringtone ?: RingtoneHelper().getDefault(this)
val vibrationPattern = NotificationHelper.vibrationPattern.takeIf { timerObject.vibrate }
val notificationChannelId =
NotificationHelper.TIMER_FINISHED_CHANNEL + "-" + System.currentTimeMillis()
val notificationId = (Integer.MAX_VALUE / 3) + timerObject.id * 10
// create a new temporary notification channel in order to work around the restriction
// that apps can only set the ringtone uri and vibration pattern upon notification channel
// creation, but can't update it
val deleteNotificationChannelIntent = NotificationHelper.createDynamicChannel(
this,
R.string.timer_finished,
notificationChannelId,
ringtoneUri = ringtoneUri,
vibrationPattern = vibrationPattern
)
val deleteNotificationChannelPendingIntent = PendingIntent.getBroadcast(
this,
notificationId + 1,
deleteNotificationChannelIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)

val notification = NotificationCompat.Builder(this, notificationChannelId)
.setSmallIcon(R.drawable.ic_notification)
.setSound(ringtoneUri)
.setVibrate(vibrationPattern)
.setContentTitle(getString(R.string.timer_finished))
.setContentText(timerObject.label.value)
.setDeleteIntent(deleteNotificationChannelPendingIntent)
.build()

NotificationManagerCompat.from(this)
.notify(notificationId, notification)
}

private fun pauseResumeAction(timerObject: TimerObject): NotificationCompat.Action {
Expand Down

0 comments on commit f4c6949

Please sign in to comment.