Skip to content

Commit

Permalink
feat: Media background support!
Browse files Browse the repository at this point in the history
  • Loading branch information
agronick committed Dec 25, 2023
1 parent 02e07b2 commit c9dee3d
Show file tree
Hide file tree
Showing 9 changed files with 364 additions and 27 deletions.
14 changes: 13 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,22 @@
android:label="TorqueService"
tools:ignore="ExportedService" />

<service
android:name=".NotiService"
android:exported="true"
android:label="Enable media controls"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>

<meta-data
android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc" />
<meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" />
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
</application>

</manifest>
55 changes: 48 additions & 7 deletions app/src/main/java/com/aatorque/prefs/SettingsFragment.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.aatorque.prefs

import android.content.Intent
import android.os.Bundle
import android.text.InputType
import androidx.lifecycle.lifecycleScope
Expand All @@ -9,8 +10,11 @@ import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SeekBarPreference
import com.aatorque.datastore.UserPreference
import com.aatorque.stats.NotiService
import com.aatorque.stats.R
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.distinctUntilChangedBy
Expand All @@ -27,6 +31,10 @@ class SettingsFragment : PreferenceFragmentCompat() {
lateinit var centerGaugeLargePref: CheckBoxPreference
lateinit var rotaryInputPref: CheckBoxPreference
lateinit var minMaxBelowPref: CheckBoxPreference
lateinit var mediaBgPref: CheckBoxPreference
lateinit var opacityPref: SeekBarPreference
lateinit var darkenArtPref: SeekBarPreference
lateinit var blurArtPref: SeekBarPreference

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand All @@ -39,12 +47,15 @@ class SettingsFragment : PreferenceFragmentCompat() {
centerGaugeLargePref = findPreference("centerGaugeLarge")!!
rotaryInputPref = findPreference("rotaryInput")!!
minMaxBelowPref = findPreference("minMaxBelow")!!
mediaBgPref = findPreference("mediaBg")!!
opacityPref = findPreference("gaugeOpacity")!!
blurArtPref = findPreference("blurArtwork")!!
darkenArtPref = findPreference("darkenArtwork")!!
themePref.summaryProvider = ListPreference.SimpleSummaryProvider.getInstance()
fontPref.summaryProvider = ListPreference.SimpleSummaryProvider.getInstance()
backgroundPref.summaryProvider = ListPreference.SimpleSummaryProvider.getInstance()
numScreensPref.summaryProvider = EditTextPreference.SimpleSummaryProvider.getInstance()
numScreensPref.setOnPreferenceChangeListener {
_, newValue ->
numScreensPref.setOnPreferenceChangeListener { _, newValue ->
val intVal = (newValue as String).toInt()
if (intVal in 1..10) {
lifecycleScope.launch {
Expand Down Expand Up @@ -98,20 +109,45 @@ class SettingsFragment : PreferenceFragmentCompat() {
}
return@setOnPreferenceChangeListener true
}
rotaryInputPref.setOnPreferenceChangeListener {
preference, newValue ->
rotaryInputPref.setOnPreferenceChangeListener { preference, newValue ->
updateDatastorePref {
it.setRotaryInput(newValue as Boolean)
}
return@setOnPreferenceChangeListener true
}
minMaxBelowPref.setOnPreferenceChangeListener{
preference, newValue ->
minMaxBelowPref.setOnPreferenceChangeListener { preference, newValue ->
updateDatastorePref {
it.setMinMaxBelow(newValue as Boolean)
}
return@setOnPreferenceChangeListener true
}
mediaBgPref.setOnPreferenceChangeListener { preference, newValue ->
updateDatastorePref {
if (newValue as Boolean && !NotiService.isNotificationAccessEnabled) {
startActivity(Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"))
}
it.setAlbumArt(newValue)
}
return@setOnPreferenceChangeListener true
}
opacityPref.setOnPreferenceChangeListener { preference, newValue ->
updateDatastorePref {
it.setOpacity(newValue as Int)
}
return@setOnPreferenceChangeListener true
}
darkenArtPref.setOnPreferenceChangeListener { preference, newValue ->
updateDatastorePref {
it.setDarkenArt(newValue as Int)
}
return@setOnPreferenceChangeListener true
}
blurArtPref.setOnPreferenceChangeListener { preference, newValue ->
updateDatastorePref {
it.setBlurArt(newValue as Int)
}
return@setOnPreferenceChangeListener true
}

numScreensPref.setOnBindEditTextListener {
it.inputType = InputType.TYPE_CLASS_NUMBER
Expand All @@ -125,6 +161,10 @@ class SettingsFragment : PreferenceFragmentCompat() {
centerGaugeLargePref.isChecked = it.centerGaugeLarge
rotaryInputPref.isChecked = it.rotaryInput
minMaxBelowPref.isChecked = it.minMaxBelow
mediaBgPref.isChecked = it.albumArt
opacityPref.value = if (it.opacity == 0) 100 else it.opacity
blurArtPref.value = it.blurArt
darkenArtPref.value = it.darkenArt
}
}
lifecycleScope.launch {
Expand All @@ -149,7 +189,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
}
}

private fun updateDatastorePref(updateBuilder: (obj: UserPreference.Builder) -> UserPreference.Builder): Unit {
@OptIn(DelicateCoroutinesApi::class)
private fun updateDatastorePref(updateBuilder: (obj: UserPreference.Builder) -> UserPreference.Builder) {
GlobalScope.launch(Dispatchers.IO) {
requireContext().dataStore.updateData { currentSettings ->
updateBuilder(currentSettings.toBuilder()).build()
Expand Down
110 changes: 110 additions & 0 deletions app/src/main/java/com/aatorque/stats/AlbumArt.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package com.aatorque.stats

import android.content.ComponentName
import android.media.MediaMetadata
import android.media.session.MediaController
import android.media.session.MediaSessionManager
import android.media.session.PlaybackState
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.launch
import timber.log.Timber

abstract class AlbumArt : CarFragment() {

val registed = HashMap<Int, () -> Unit>()
private val updateChannel = MutableSharedFlow<MediaMetadata?>()

@OptIn(FlowPreview::class)
override fun onStart() {
super.onStart()
registerMedia()
lifecycleScope.launch {
updateChannel.asSharedFlow()
.debounce(300)
.collect(this@AlbumArt::onMediaChanged)
}
}

private fun registerMedia() {
val mediaMan = ContextCompat.getSystemService(
requireContext(),
MediaSessionManager::class.java
) ?: return
val component = ComponentName(requireContext(), NotiService::class.java)
try {
mediaMan.addOnActiveSessionsChangedListener({
updateSessions(it ?: emptyList())
}, component)
updateSessions(mediaMan.getActiveSessions(component))
} catch (e: SecurityException) {
Timber.e("No permission to read media", e)
}
}

override fun onStop() {
super.onStop()
updateSessions(emptyList())
}

private fun updateSessions(mediaControllers: List<MediaController>) {
val found = mediaControllers.map {
val token = it.sessionToken
val code = token.hashCode()
if (!registed.containsKey(code)) {
val callback = object : MediaController.Callback() {
override fun onPlaybackStateChanged(state: PlaybackState?) {
super.onPlaybackStateChanged(state)
Timber.i("Playback changed event ${state?.state}")
lifecycleScope.launch {
if (arrayOf(
PlaybackState.STATE_PAUSED,
PlaybackState.STATE_STOPPED
).contains(state?.state)
) {
updateChannel.emit(null)
} else if (isActive(it.playbackState)) {
updateChannel.emit(it.metadata)
}
}
}
}
it.registerCallback(callback)
registed[code] = {
it.unregisterCallback(callback)
registed.remove(code)
}
if (isActive(it.playbackState)) {
lifecycleScope.launch {
updateChannel.emit(it.metadata)
}
}
}
code
}.toSet()
registed.filterNot { found.contains(it.key) }.forEach {
it.value()
}
}

open fun isActive(state: PlaybackState?): Boolean {
return when (state?.state) {
PlaybackState.STATE_FAST_FORWARDING,
PlaybackState.STATE_REWINDING,
PlaybackState.STATE_SKIPPING_TO_PREVIOUS,
PlaybackState.STATE_SKIPPING_TO_NEXT,
PlaybackState.STATE_SKIPPING_TO_QUEUE_ITEM,
PlaybackState.STATE_BUFFERING,
PlaybackState.STATE_CONNECTING,
PlaybackState.STATE_PLAYING -> true

else -> false
}
}

abstract fun onMediaChanged(medadata: MediaMetadata?)
}
Loading

0 comments on commit c9dee3d

Please sign in to comment.