@@ -13,6 +13,7 @@ import android.content.Context
1313import android.content.Intent
1414import android.database.ContentObserver
1515import android.graphics.Bitmap
16+ import android.graphics.Color
1617import android.media.MediaMetadata
1718import android.net.Uri
1819import android.os.Handler
@@ -28,7 +29,9 @@ import android.graphics.drawable.Drawable
2829import androidx.compose.runtime.Immutable
2930import androidx.compose.ui.graphics.ImageBitmap
3031import androidx.compose.ui.graphics.asImageBitmap
32+ import androidx.core.graphics.ColorUtils
3133import androidx.core.graphics.drawable.toBitmap
34+ import androidx.palette.graphics.Palette
3235import com.android.systemui.res.R
3336import com.android.systemui.statusbar.notification.headsup.HeadsUpManager
3437import com.android.systemui.statusbar.notification.headsup.OnHeadsUpChangedListener
@@ -76,6 +79,7 @@ class OnGoingActionProgressController(
7679 private var headsUpPinned = false
7780 private var isEnabled = false
7881 private var isCompactModeEnabled = false
82+ private var chipColorMode = CHIP_COLOR_MODE_DEFAULT
7983
8084 private var currentProgress = 0
8185 private var currentProgressMax = 0
@@ -85,6 +89,10 @@ class OnGoingActionProgressController(
8589 private var currentArtistName: String? = null
8690 private var currentAlbumArt: Bitmap ? = null
8791
92+ private var currentChipBgColor: Int? = null
93+ private var lastColorExtractedIcon: Drawable ? = null
94+ private var lastColorExtractedAlbumArt: Bitmap ? = null
95+
8896 private var lastObservedTitle: String? = null
8997
9098 private var isMenuVisible = false
@@ -119,7 +127,8 @@ class OnGoingActionProgressController(
119127 if (uri == null ) return
120128 if (uri == Settings .System .getUriFor(ONGOING_ACTION_CHIP_ENABLED ) ||
121129 uri == Settings .System .getUriFor(ONGOING_MEDIA_PROGRESS ) ||
122- uri == Settings .System .getUriFor(ONGOING_COMPACT_MODE_ENABLED )) {
130+ uri == Settings .System .getUriFor(Settings .System .ONGOING_COMPACT_MODE ) ||
131+ uri == Settings .System .getUriFor(Settings .System .ONGOING_CHIP_COLOR_MODE )) {
123132 updateSettings()
124133 }
125134 }
@@ -143,6 +152,12 @@ class OnGoingActionProgressController(
143152 this ,
144153 UserHandle .USER_ALL
145154 )
155+ contentResolver.registerContentObserver(
156+ Settings .System .getUriFor(Settings .System .ONGOING_CHIP_COLOR_MODE ),
157+ false ,
158+ this ,
159+ UserHandle .USER_ALL
160+ )
146161 updateSettings()
147162 }
148163
@@ -195,6 +210,7 @@ class OnGoingActionProgressController(
195210 private fun onTrackChanged () {
196211 needsFullUiUpdate = true
197212 currentAlbumArt = null
213+ invalidateChipBgColor()
198214 scheduleAlbumArtRetry()
199215 }
200216
@@ -210,6 +226,7 @@ class OnGoingActionProgressController(
210226
211227 if (art != null ) {
212228 currentAlbumArt = art
229+ if (chipColorMode == CHIP_COLOR_MODE_ALBUM_ART ) invalidateChipBgColor()
213230 requestUiUpdate()
214231 return @launch
215232 }
@@ -280,6 +297,98 @@ class OnGoingActionProgressController(
280297 }
281298 }
282299
300+ private fun invalidateChipBgColor () {
301+ currentChipBgColor = null
302+ lastColorExtractedIcon = null
303+ lastColorExtractedAlbumArt = null
304+ }
305+
306+ private suspend fun extractDominantColorFromBitmap (bitmap : Bitmap ): Int? =
307+ withContext(bgDispatcher) {
308+ try {
309+ if (bitmap.isRecycled || bitmap.width <= 0 || bitmap.height <= 0 ) return @withContext null
310+
311+ val palette = Palette .from(bitmap).generate()
312+
313+ val candidates = listOfNotNull(
314+ palette.vibrantSwatch,
315+ palette.mutedSwatch,
316+ palette.dominantSwatch,
317+ palette.darkVibrantSwatch,
318+ palette.darkMutedSwatch,
319+ )
320+
321+ candidates
322+ .map { it.rgb }
323+ .firstOrNull { color ->
324+ val alpha = Color .alpha(color)
325+ val luminance = ColorUtils .calculateLuminance(color)
326+ alpha > 200 && luminance > 0.05 && luminance < 0.95
327+ }
328+ } catch (e: Exception ) {
329+ Log .w(TAG , " Failed to extract dominant color from bitmap" , e)
330+ null
331+ }
332+ }
333+
334+ private fun extractAndApplyChipBgColorFromIcon (icon : Drawable ) {
335+ if (icon == = lastColorExtractedIcon) return
336+
337+ mainScope.launch {
338+ val size = (48f * context.resources.displayMetrics.density).toInt().coerceAtLeast(1 )
339+ val bitmap = try {
340+ withContext(bgDispatcher) {
341+ icon.toBitmap(width = size, height = size, config = Bitmap .Config .ARGB_8888 )
342+ }
343+ } catch (e: Exception ) {
344+ Log .w(TAG , " Failed to rasterize icon for chip color extraction" , e)
345+ null
346+ } ? : return @launch
347+
348+ lastColorExtractedIcon = icon
349+ currentChipBgColor = extractDominantColorFromBitmap(bitmap)
350+ updateProgressState()
351+ }
352+ }
353+
354+ private fun extractAndApplyChipBgColorFromAlbumArt (albumArt : Bitmap , iconFallback : Drawable ? ) {
355+ if (albumArt == = lastColorExtractedAlbumArt) return
356+
357+ mainScope.launch {
358+ val color = extractDominantColorFromBitmap(albumArt)
359+
360+ if (color != null ) {
361+ lastColorExtractedAlbumArt = albumArt
362+ currentChipBgColor = color
363+ updateProgressState()
364+ return @launch
365+ }
366+
367+ Log .d(TAG , " Album art color extraction failed, falling back to icon color" )
368+ if (iconFallback == null ) {
369+ lastColorExtractedAlbumArt = albumArt
370+ currentChipBgColor = null
371+ updateProgressState()
372+ return @launch
373+ }
374+
375+ val size = (48f * context.resources.displayMetrics.density).toInt().coerceAtLeast(1 )
376+ val iconBitmap = try {
377+ withContext(bgDispatcher) {
378+ iconFallback.toBitmap(width = size, height = size,
379+ config = Bitmap .Config .ARGB_8888 )
380+ }
381+ } catch (e: Exception ) {
382+ Log .w(TAG , " Failed to rasterize icon fallback for chip color extraction" , e)
383+ null
384+ }
385+
386+ lastColorExtractedAlbumArt = albumArt
387+ currentChipBgColor = iconBitmap?.let { extractDominantColorFromBitmap(it) }
388+ updateProgressState()
389+ }
390+ }
391+
283392 private fun updateProgressState () {
284393 var isVisible = ! isForceHidden && ! headsUpPinned && ! isSystemChipVisible
285394 val hasMediaSession = isMediaSessionActiveForChip()
@@ -302,6 +411,7 @@ class OnGoingActionProgressController(
302411 isMediaPlaying = false ,
303412 trackTitle = null ,
304413 artistName = null ,
414+ chipBgColor = null ,
305415 )
306416 )
307417 return
@@ -338,6 +448,25 @@ class OnGoingActionProgressController(
338448 val trackTitle = if (hasMediaSession) currentTrackTitle else null
339449 val artistName = if (hasMediaSession) currentArtistName else null
340450
451+ if (hasMediaSession) {
452+ when (chipColorMode) {
453+ CHIP_COLOR_MODE_ICON -> {
454+ currentIcon?.let { extractAndApplyChipBgColorFromIcon(it) }
455+ }
456+ CHIP_COLOR_MODE_ALBUM_ART -> {
457+ val art = currentAlbumArt
458+ if (art != null ) {
459+ extractAndApplyChipBgColorFromAlbumArt(art, currentIcon)
460+ } else {
461+ currentIcon?.let { extractAndApplyChipBgColorFromIcon(it) }
462+ }
463+ }
464+ }
465+ }
466+
467+ val resolvedChipBgColor = if (chipColorMode != CHIP_COLOR_MODE_DEFAULT ) currentChipBgColor
468+ else null
469+
341470 publish(
342471 ProgressState (
343472 isVisible = true ,
@@ -351,6 +480,7 @@ class OnGoingActionProgressController(
351480 isMediaPlaying = isMediaPlaying,
352481 trackTitle = trackTitle,
353482 artistName = artistName,
483+ chipBgColor = resolvedChipBgColor,
354484 )
355485 )
356486 }
@@ -822,6 +952,7 @@ class OnGoingActionProgressController(
822952 val wasEnabled = isEnabled
823953 val wasShowingMedia = showMediaProgress
824954 val wasCompactMode = isCompactModeEnabled
955+ val wasChipColorMode = chipColorMode
825956
826957 isEnabled = Settings .System .getIntForUser(
827958 contentResolver,
@@ -844,11 +975,23 @@ class OnGoingActionProgressController(
844975 UserHandle .USER_CURRENT
845976 ) == 1
846977
978+ chipColorMode = Settings .System .getIntForUser(
979+ contentResolver,
980+ Settings .System .ONGOING_CHIP_COLOR_MODE ,
981+ CHIP_COLOR_MODE_DEFAULT ,
982+ UserHandle .USER_CURRENT
983+ )
984+
847985 if (wasEnabled != isEnabled || wasShowingMedia != showMediaProgress || wasCompactMode != isCompactModeEnabled) {
848986 needsFullUiUpdate = true
849987 isExpanded = false
850988 }
851989
990+ if (wasChipColorMode != chipColorMode) {
991+ invalidateChipBgColor()
992+ needsFullUiUpdate = true
993+ }
994+
852995 requestUiUpdate()
853996 }
854997
@@ -897,6 +1040,10 @@ class OnGoingActionProgressController(
8971040 private const val ALBUM_ART_RETRY_INTERVAL_MS = 300L
8981041 private const val POSITION_RESET_THRESHOLD_MS = 1_500L
8991042
1043+ const val CHIP_COLOR_MODE_DEFAULT = 0
1044+ const val CHIP_COLOR_MODE_ICON = 1
1045+ const val CHIP_COLOR_MODE_ALBUM_ART = 2
1046+
9001047 private val HAPTIC_CLICK =
9011048 VibrationEffect .createPredefined(VibrationEffect .EFFECT_CLICK )
9021049 private val HAPTIC_DOUBLE =
@@ -919,4 +1066,5 @@ data class ProgressState(
9191066 val isMediaPlaying : Boolean = false ,
9201067 val trackTitle : String? = null ,
9211068 val artistName : String? = null ,
1069+ val chipBgColor : Int? = null ,
9221070)
0 commit comments