From 364d033acd0471901e91af3c6b1d76c9abd1524a Mon Sep 17 00:00:00 2001 From: Isaac Serrano Date: Fri, 23 Oct 2026 17:12:07 -0700 Subject: [PATCH 1/3] feat(island): toggle system icon for bubble queue and hub --- .../data/repository/SettingsRepository.kt | 4 ++ .../domain/model/ActiveNotificationAlert.kt | 2 + .../notifications/NotificationsPlugin.kt | 45 ++++++++++++++----- .../island/service/IslandCoordinator.kt | 2 +- .../island/ui/components/IslandComponents.kt | 36 +++++++++++++++ .../services/NotificationListener.kt | 6 +++ .../features/display/IslandSubSettingsUI.kt | 12 +++++ .../essentials/viewmodels/MainViewModel.kt | 7 +++ app/src/main/res/values/strings.xml | 2 + 9 files changed, 104 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt b/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt index dc499c7f5..13c3c7733 100644 --- a/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt +++ b/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt @@ -551,6 +551,7 @@ class SettingsRepository( const val KEY_ISLAND_BATTERY_HIDE_LOCKED = "island_battery_hide_locked" const val KEY_ISLAND_TIMEOUT_MS = "island_timeout_ms" const val KEY_ISLAND_SHOW_GLOW = "island_show_glow" + const val KEY_ISLAND_MONOCHROME = "island_monochrome" const val KEY_ISLAND_EXPANDED_WIDTH = "island_expanded_width" const val KEY_ISLAND_EXPANDED_ROUNDNESS = "island_expanded_roundness" const val KEY_ISLAND_EXPANDED_PADDING = "island_expanded_padding" @@ -4106,6 +4107,9 @@ class SettingsRepository( fun isIslandShowGlowEnabled(): Boolean = getBoolean(KEY_ISLAND_SHOW_GLOW, true) fun setIslandShowGlowEnabled(enabled: Boolean) = putBoolean(KEY_ISLAND_SHOW_GLOW, enabled) + fun isIslandMonochromeEnabled(): Boolean = getBoolean(KEY_ISLAND_MONOCHROME, false) + fun setIslandMonochromeEnabled(enabled: Boolean) = putBoolean(KEY_ISLAND_MONOCHROME, enabled) + fun getIslandExpandedWidth(): Float = getFloat(KEY_ISLAND_EXPANDED_WIDTH, 360f) fun setIslandExpandedWidth(value: Float) = putFloat(KEY_ISLAND_EXPANDED_WIDTH, value) diff --git a/app/src/main/java/com/sameerasw/essentials/domain/model/ActiveNotificationAlert.kt b/app/src/main/java/com/sameerasw/essentials/domain/model/ActiveNotificationAlert.kt index 1ec301ff1..4038351d9 100644 --- a/app/src/main/java/com/sameerasw/essentials/domain/model/ActiveNotificationAlert.kt +++ b/app/src/main/java/com/sameerasw/essentials/domain/model/ActiveNotificationAlert.kt @@ -32,5 +32,7 @@ data class ActiveNotificationAlert( val ongoing: Boolean = false, val isConversation: Boolean = false, val hubOnly: Boolean = false, + // The status bar glyph (Notification.smallIcon), used by the monochrome island + val smallIcon: Bitmap? = null, ) diff --git a/app/src/main/java/com/sameerasw/essentials/island/plugins/notifications/NotificationsPlugin.kt b/app/src/main/java/com/sameerasw/essentials/island/plugins/notifications/NotificationsPlugin.kt index 12fc9de6e..787c35ffa 100644 --- a/app/src/main/java/com/sameerasw/essentials/island/plugins/notifications/NotificationsPlugin.kt +++ b/app/src/main/java/com/sameerasw/essentials/island/plugins/notifications/NotificationsPlugin.kt @@ -11,6 +11,9 @@ import android.app.KeyguardManager import android.content.Context import android.os.PowerManager import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.runtime.Composable +import android.graphics.Bitmap import androidx.compose.ui.unit.dp import com.sameerasw.essentials.R import com.sameerasw.essentials.data.repository.SettingsRepository @@ -34,6 +37,7 @@ import com.sameerasw.essentials.island.plugins.soften import com.sameerasw.essentials.island.ui.IslandStackBubble import com.sameerasw.essentials.island.ui.IslandStackOverlap import com.sameerasw.essentials.island.ui.components.IslandBitmap +import com.sameerasw.essentials.island.ui.components.NotificationGlyph import com.sameerasw.essentials.island.ui.components.IslandIcon import com.sameerasw.essentials.services.NotificationListener import com.sameerasw.essentials.utils.HapticUtil @@ -48,6 +52,7 @@ class NotificationsPlugin : BaseIslandPlugin() { SettingsRepository.KEY_ISLAND_NOTIF_QUEUE_INLINE, SettingsRepository.KEY_ISLAND_LINE_STAGE_ENABLED, SettingsRepository.KEY_ISLAND_SHOW_GLOW, + SettingsRepository.KEY_ISLAND_MONOCHROME, SettingsRepository.KEY_ISLAND_NOTIF_QUEUE, SettingsRepository.KEY_ISLAND_NOTIF_SKIP_SILENT, SettingsRepository.KEY_ISLAND_NOTIF_FOLLOW_DND, @@ -565,7 +570,7 @@ class NotificationsPlugin : BaseIslandPlugin() { if (!entry.grouped) return stackIconFor(entry.lead, current = false, highlight = highlight) val icon = entry.lead.appIcon ?: entry.lead.icon return StackIcon(entry.key, onSelect = { select(entry.lead.key) }, highlight = highlight) { size -> - IslandBitmap(icon, size, circle = true, fallbackRes = R.drawable.rounded_notifications_unread_24) + AlertIcon(entry.lead, icon, size, circle = true) } } @@ -745,16 +750,16 @@ class NotificationsPlugin : BaseIslandPlugin() { compact = buildList { add( CompactCell("notif.icon") { - IslandBitmap(icon, 22.dp, fallbackRes = R.drawable.rounded_notifications_unread_24) + AlertIcon(alert, icon, 22.dp) }, ) val appIcon = alert.appIcon - if (!minimal() && alert.chatIcon != null && appIcon != null) { + if (!minimal() && !monochrome() && alert.chatIcon != null && appIcon != null) { add(CompactCell("notif.app", soloOnly = true) { IslandBitmap(appIcon, 22.dp, circle = true) }) } }, line = LineContent( - icon = { IslandBitmap(icon, 24.dp, fallbackRes = R.drawable.rounded_notifications_unread_24) }, + icon = { AlertIcon(alert, icon, 24.dp) }, start = sender, end = message, condensed = condensedLine(), @@ -802,24 +807,24 @@ class NotificationsPlugin : BaseIslandPlugin() { compact = buildList { add( CompactCell("notif.icon") { - IslandBitmap(icon, 22.dp, fallbackRes = R.drawable.rounded_notifications_unread_24) + AlertIcon(alert, icon, 22.dp) }, ) val appIcon = alert.appIcon - if (!minimal() && !hideChat && alert.chatIcon != null && appIcon != null) { + if (!minimal() && !monochrome() && !hideChat && alert.chatIcon != null && appIcon != null) { add(CompactCell("notif.app", soloOnly = true) { IslandBitmap(appIcon, 22.dp, circle = true) }) } }, line = if (isOtp(alert)) { LineContent( - icon = { IslandBitmap(alert.appIcon ?: alert.icon, 24.dp, fallbackRes = R.drawable.rounded_lock_24) }, + icon = { AlertIcon(alert, alert.appIcon ?: alert.icon, 24.dp, fallbackRes = R.drawable.rounded_lock_24) }, start = "${appNameFor(context, alert)} ยท ${context.getString(R.string.island_otp_label)}", end = alert.otpCode?.let(::maskOtp).orEmpty(), endSlot = { IslandIcon(R.drawable.rounded_lock_24, size = 18.dp) }, ) } else { LineContent( - icon = { IslandBitmap(icon, 24.dp, fallbackRes = R.drawable.rounded_notifications_unread_24) }, + icon = { AlertIcon(alert, icon, 24.dp) }, start = appNameFor(context, alert), end = "", condensed = condensedLine(), @@ -853,11 +858,11 @@ class NotificationsPlugin : BaseIslandPlugin() { placement = CompactPlacement.Dynamic, compact = listOf( CompactCell("notif.icon") { - IslandBitmap(icon, 22.dp, fallbackRes = R.drawable.rounded_notifications_unread_24) + AlertIcon(alert, icon, 22.dp) }, ), line = LineContent( - icon = { IslandBitmap(icon, 24.dp, fallbackRes = R.drawable.rounded_notifications_unread_24) }, + icon = { AlertIcon(alert, icon, 24.dp) }, start = sender, end = formatOtp(code), endSlot = { IslandIcon(R.drawable.rounded_content_copy_24, size = 18.dp) }, @@ -943,7 +948,25 @@ class NotificationsPlugin : BaseIslandPlugin() { private fun stackIconFor(alert: ActiveNotificationAlert, current: Boolean, highlight: Boolean = false): StackIcon { val icon = if ((concealed() && hidesChatPictures()) || (hub() && !settings.isIslandNotifHubPreferChatPictures())) alert.appIcon ?: alert.icon else alert.chatIcon ?: alert.appIcon ?: alert.icon return StackIcon(alert.key, current = current, onSelect = { select(alert.key) }, highlight = highlight) { size -> - IslandBitmap(icon, size, circle = true, fallbackRes = R.drawable.rounded_notifications_unread_24) + AlertIcon(alert, icon, size, circle = true) + } + } + + private fun monochrome() = settings.isIslandMonochromeEnabled() + + // Monochrome swaps the picture for the app's notification glyph on the island's softened accent + @Composable + private fun AlertIcon( + alert: ActiveNotificationAlert, + bitmap: Bitmap?, + size: Dp, + circle: Boolean = false, + fallbackRes: Int = R.drawable.rounded_notifications_unread_24, + ) { + if (monochrome()) { + NotificationGlyph(alert.smallIcon, alert.appColor?.let { Color(soften(it)) }, size, fallbackRes = fallbackRes) + } else { + IslandBitmap(bitmap, size, circle = circle, fallbackRes = fallbackRes) } } diff --git a/app/src/main/java/com/sameerasw/essentials/island/service/IslandCoordinator.kt b/app/src/main/java/com/sameerasw/essentials/island/service/IslandCoordinator.kt index b550b1e41..963cb7580 100644 --- a/app/src/main/java/com/sameerasw/essentials/island/service/IslandCoordinator.kt +++ b/app/src/main/java/com/sameerasw/essentials/island/service/IslandCoordinator.kt @@ -731,7 +731,6 @@ class IslandCoordinator( controller.balanceCompact = settings.isIslandBalanceCompactEnabled() controller.chipsEnabled = settings.getBoolean(SettingsRepository.KEY_ISLAND_CHIPS, false) controller.secondaryBubbleEnabled = settings.isIslandSecondaryBubbleEnabled() - controller.secondaryBubbleOverlap = settings.getIslandNotifQueueStyle() == SettingsRepository.ISLAND_NOTIF_QUEUE_STYLE_OVERLAP controller.relayout() controller.expandedTimeoutMs = settings.getIslandExpandedTimeoutMs() applyPreviewRing() @@ -825,6 +824,7 @@ class IslandCoordinator( "alarm" to SettingsRepository.KEY_ISLAND_ALARM_LAUNCHER_ONLY, ) val CONFIG_KEYS = setOf( + SettingsRepository.KEY_ISLAND_MONOCHROME, SettingsRepository.KEY_ISLAND_NOTIF_QUEUE_STYLE, SettingsRepository.KEY_ISLAND_NOTIF_QUEUE_INLINE, SettingsRepository.KEY_ISLAND_USE_AUTO_DETECT, diff --git a/app/src/main/java/com/sameerasw/essentials/island/ui/components/IslandComponents.kt b/app/src/main/java/com/sameerasw/essentials/island/ui/components/IslandComponents.kt index c9df3f1a6..5c60c3702 100644 --- a/app/src/main/java/com/sameerasw/essentials/island/ui/components/IslandComponents.kt +++ b/app/src/main/java/com/sameerasw/essentials/island/ui/components/IslandComponents.kt @@ -59,9 +59,45 @@ import androidx.compose.ui.draw.drawBehind import androidx.compose.material3.Text import androidx.compose.foundation.layout.Row import com.sameerasw.essentials.island.ui.IslandMotion +import androidx.compose.foundation.background +import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.graphics.ColorFilter import kotlinx.coroutines.delay import kotlin.random.Random +// Monochrome stand-in for a notification picture: the app's status bar glyph on its accent color +@Composable +fun NotificationGlyph( + smallIcon: Bitmap?, + color: Color?, + size: Dp, + modifier: Modifier = Modifier, + fallbackRes: Int, +) { + val background = color ?: MaterialTheme.colorScheme.primary + val glyph = if (background.luminance() > 0.6f) Color.Black else Color.White + Box( + modifier + .sizeIn(maxWidth = size, maxHeight = size) + .aspectRatio(1f, matchHeightConstraintsFirst = true) + .clip(CircleShape) + .background(background), + contentAlignment = Alignment.Center, + ) { + if (smallIcon != null) { + Image( + bitmap = remember(smallIcon) { smallIcon.asImageBitmap() }, + contentDescription = null, + contentScale = ContentScale.Fit, + colorFilter = ColorFilter.tint(glyph), + modifier = Modifier.fillMaxSize(0.6f), + ) + } else { + Icon(painterResource(fallbackRes), null, tint = glyph, modifier = Modifier.fillMaxSize(0.6f)) + } + } +} + @Composable fun IslandBitmap( bitmap: Bitmap?, diff --git a/app/src/main/java/com/sameerasw/essentials/services/NotificationListener.kt b/app/src/main/java/com/sameerasw/essentials/services/NotificationListener.kt index 929b0a581..df99aaa4a 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/NotificationListener.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/NotificationListener.kt @@ -2353,6 +2353,11 @@ class NotificationListener : NotificationListenerService() { bitmap = appIcon } + var smallIconBitmap: Bitmap? = null + try { + notif.smallIcon?.loadDrawable(this)?.let { smallIconBitmap = AppUtil.drawableToBitmap(it) } + } catch (_: Exception) {} + var appColor: Int? = null if (notif.color != 0 && notif.color != android.graphics.Color.TRANSPARENT) { appColor = notif.color @@ -2403,6 +2408,7 @@ class NotificationListener : NotificationListenerService() { appIcon = appIcon, actions = actionList, chatIcon = chatIcon, + smallIcon = smallIconBitmap, ) } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSubSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSubSettingsUI.kt index b24c91ca6..94901ebbb 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSubSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSubSettingsUI.kt @@ -1299,6 +1299,18 @@ fun IslandNotificationSettingsUI( modifier = Modifier.highlight(highlightSetting == "island_catch_up_timeout"), ) } + + IconToggleItem( + iconRes = R.drawable.rounded_invert_colors_24, + title = stringResource(R.string.island_monochrome_title), + description = stringResource(R.string.island_monochrome_desc), + isChecked = viewModel.isIslandMonochrome.value, + onCheckedChange = { checked -> + HapticUtil.performVirtualKeyHaptic(view) + viewModel.setIslandMonochrome(checked) + }, + modifier = Modifier.highlight(highlightSetting == "island_monochrome"), + ) } if (showOtpPermissions) { diff --git a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt index 41e0e7596..5ae5c8d43 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt @@ -303,6 +303,7 @@ class MainViewModel : ViewModel() { val isIslandOtpEnabled = mutableStateOf(false) val islandOtpTimeoutMs = mutableLongStateOf(60_000L) val isIslandShowGlow = mutableStateOf(true) + val isIslandMonochrome = mutableStateOf(false) val isIslandBorderOutline = mutableStateOf(false) val islandBorderOutlineColor = mutableStateOf(SettingsRepository.ISLAND_BORDER_OUTLINE_DEFAULT_COLOR) val isIslandBorderOutlineDynamic = mutableStateOf(false) @@ -2596,6 +2597,7 @@ class MainViewModel : ViewModel() { isIslandOtpEnabled.value = settingsRepository.isIslandOtpEnabled() islandOtpTimeoutMs.longValue = settingsRepository.getIslandOtpTimeoutMs() isIslandShowGlow.value = settingsRepository.isIslandShowGlowEnabled() + isIslandMonochrome.value = settingsRepository.isIslandMonochromeEnabled() isIslandBorderOutline.value = settingsRepository.isIslandBorderOutlineEnabled() islandBorderOutlineColor.value = settingsRepository.getIslandBorderOutlineColor() isIslandBorderOutlineDynamic.value = settingsRepository.isIslandBorderOutlineDynamicEnabled() @@ -6199,6 +6201,11 @@ class MainViewModel : ViewModel() { settingsRepository.setIslandShowGlowEnabled(enabled) } + fun setIslandMonochrome(enabled: Boolean) { + isIslandMonochrome.value = enabled + settingsRepository.setIslandMonochromeEnabled(enabled) + } + fun setIslandBorderOutline(enabled: Boolean) { isIslandBorderOutline.value = enabled settingsRepository.setIslandBorderOutlineEnabled(enabled) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e08d93c08..7b1cef806 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3231,6 +3231,8 @@ Glow spread Pulse duration App based color glow + App icon bubble + Show the app\'s notification icon on its app color instead of the picture or content Expanded width Camera gap Width when long pressed to expand From f845ea6a2d11ac05ee358950d87be44d9110c4f6 Mon Sep 17 00:00:00 2001 From: Isaac Serrano Date: Fri, 23 Oct 2026 20:27:26 -0700 Subject: [PATCH 2/3] feat(island): handle contrast difference on icons --- .../essentials/island/ui/components/IslandComponents.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/sameerasw/essentials/island/ui/components/IslandComponents.kt b/app/src/main/java/com/sameerasw/essentials/island/ui/components/IslandComponents.kt index 5c60c3702..4b376937a 100644 --- a/app/src/main/java/com/sameerasw/essentials/island/ui/components/IslandComponents.kt +++ b/app/src/main/java/com/sameerasw/essentials/island/ui/components/IslandComponents.kt @@ -53,6 +53,7 @@ import com.sameerasw.essentials.island.ui.IslandTextStyles import androidx.compose.ui.unit.sp import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.graphics.luminance +import androidx.compose.ui.graphics.lerp import androidx.compose.ui.graphics.drawscope.clipRect import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.draw.drawBehind @@ -75,7 +76,7 @@ fun NotificationGlyph( fallbackRes: Int, ) { val background = color ?: MaterialTheme.colorScheme.primary - val glyph = if (background.luminance() > 0.6f) Color.Black else Color.White + val glyph = lerp(background, if (background.luminance() > 0.179f) Color.Black else Color.White, 0.8f) Box( modifier .sizeIn(maxWidth = size, maxHeight = size) From af867c73336db942175ec1b9b6268c033f67c9aa Mon Sep 17 00:00:00 2001 From: Isaac Serrano Date: Sun, 25 Oct 2026 14:42:03 -0700 Subject: [PATCH 3/3] feat(island): enable per app color on hub --- .../data/repository/SettingsRepository.kt | 9 ++++++--- .../domain/model/ActiveNotificationAlert.kt | 2 +- .../domain/registry/FeatureRegistry.kt | 5 +++++ .../plugins/notifications/NotificationsPlugin.kt | 14 +++++++------- .../island/service/IslandCoordinator.kt | 2 +- .../island/ui/components/IslandComponents.kt | 11 +++++------ .../essentials/services/NotificationListener.kt | 11 +++++++++-- .../ui/features/display/IslandSubSettingsUI.kt | 12 ------------ .../IslandNotificationHubOptionsBottomSheet.kt | 16 ++++++++++++++++ .../sameerasw/essentials/utils/AppIconUtil.kt | 16 ++++++++++++++-- .../essentials/viewmodels/MainViewModel.kt | 10 +++++----- app/src/main/res/values/strings.xml | 4 ++-- 12 files changed, 71 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt b/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt index 13c3c7733..8132b30ec 100644 --- a/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt +++ b/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt @@ -551,7 +551,7 @@ class SettingsRepository( const val KEY_ISLAND_BATTERY_HIDE_LOCKED = "island_battery_hide_locked" const val KEY_ISLAND_TIMEOUT_MS = "island_timeout_ms" const val KEY_ISLAND_SHOW_GLOW = "island_show_glow" - const val KEY_ISLAND_MONOCHROME = "island_monochrome" + const val KEY_ISLAND_NOTIF_THEMED_BUBBLE = "island_notif_themed_bubble" const val KEY_ISLAND_EXPANDED_WIDTH = "island_expanded_width" const val KEY_ISLAND_EXPANDED_ROUNDNESS = "island_expanded_roundness" const val KEY_ISLAND_EXPANDED_PADDING = "island_expanded_padding" @@ -4107,8 +4107,11 @@ class SettingsRepository( fun isIslandShowGlowEnabled(): Boolean = getBoolean(KEY_ISLAND_SHOW_GLOW, true) fun setIslandShowGlowEnabled(enabled: Boolean) = putBoolean(KEY_ISLAND_SHOW_GLOW, enabled) - fun isIslandMonochromeEnabled(): Boolean = getBoolean(KEY_ISLAND_MONOCHROME, false) - fun setIslandMonochromeEnabled(enabled: Boolean) = putBoolean(KEY_ISLAND_MONOCHROME, enabled) + fun isIslandNotifThemedBubbleEnabled(): Boolean = getBoolean(KEY_ISLAND_NOTIF_THEMED_BUBBLE, false) + fun setIslandNotifThemedBubbleEnabled(enabled: Boolean) = putBoolean(KEY_ISLAND_NOTIF_THEMED_BUBBLE, enabled) + + fun isIslandNotifThemedBubbleActive(): Boolean = + isIslandNotifThemedBubbleEnabled() && getBoolean(KEY_ISLAND_THEMED_APP_ICONS, false) fun getIslandExpandedWidth(): Float = getFloat(KEY_ISLAND_EXPANDED_WIDTH, 360f) fun setIslandExpandedWidth(value: Float) = putFloat(KEY_ISLAND_EXPANDED_WIDTH, value) diff --git a/app/src/main/java/com/sameerasw/essentials/domain/model/ActiveNotificationAlert.kt b/app/src/main/java/com/sameerasw/essentials/domain/model/ActiveNotificationAlert.kt index 4038351d9..d16a336aa 100644 --- a/app/src/main/java/com/sameerasw/essentials/domain/model/ActiveNotificationAlert.kt +++ b/app/src/main/java/com/sameerasw/essentials/domain/model/ActiveNotificationAlert.kt @@ -32,7 +32,7 @@ data class ActiveNotificationAlert( val ongoing: Boolean = false, val isConversation: Boolean = false, val hubOnly: Boolean = false, - // The status bar glyph (Notification.smallIcon), used by the monochrome island + val themedIcon: Bitmap? = null, val smallIcon: Bitmap? = null, ) diff --git a/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt b/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt index 49fcfa500..afe29dc2f 100644 --- a/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt +++ b/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt @@ -887,6 +887,11 @@ object FeatureRegistry { R.string.island_notification_options_title, "island_notif_hub", ), + SearchSetting( + R.string.island_notif_themed_bubble_title, + R.string.island_notif_hub_title, + "island_notif_hub", + ), SearchSetting( R.string.island_show_calls_title, R.string.island_call_incoming, diff --git a/app/src/main/java/com/sameerasw/essentials/island/plugins/notifications/NotificationsPlugin.kt b/app/src/main/java/com/sameerasw/essentials/island/plugins/notifications/NotificationsPlugin.kt index 787c35ffa..ffb0ef1f0 100644 --- a/app/src/main/java/com/sameerasw/essentials/island/plugins/notifications/NotificationsPlugin.kt +++ b/app/src/main/java/com/sameerasw/essentials/island/plugins/notifications/NotificationsPlugin.kt @@ -52,7 +52,8 @@ class NotificationsPlugin : BaseIslandPlugin() { SettingsRepository.KEY_ISLAND_NOTIF_QUEUE_INLINE, SettingsRepository.KEY_ISLAND_LINE_STAGE_ENABLED, SettingsRepository.KEY_ISLAND_SHOW_GLOW, - SettingsRepository.KEY_ISLAND_MONOCHROME, + SettingsRepository.KEY_ISLAND_NOTIF_THEMED_BUBBLE, + SettingsRepository.KEY_ISLAND_THEMED_APP_ICONS, SettingsRepository.KEY_ISLAND_NOTIF_QUEUE, SettingsRepository.KEY_ISLAND_NOTIF_SKIP_SILENT, SettingsRepository.KEY_ISLAND_NOTIF_FOLLOW_DND, @@ -754,7 +755,7 @@ class NotificationsPlugin : BaseIslandPlugin() { }, ) val appIcon = alert.appIcon - if (!minimal() && !monochrome() && alert.chatIcon != null && appIcon != null) { + if (!minimal() && !themedBubble() && alert.chatIcon != null && appIcon != null) { add(CompactCell("notif.app", soloOnly = true) { IslandBitmap(appIcon, 22.dp, circle = true) }) } }, @@ -811,7 +812,7 @@ class NotificationsPlugin : BaseIslandPlugin() { }, ) val appIcon = alert.appIcon - if (!minimal() && !monochrome() && !hideChat && alert.chatIcon != null && appIcon != null) { + if (!minimal() && !themedBubble() && !hideChat && alert.chatIcon != null && appIcon != null) { add(CompactCell("notif.app", soloOnly = true) { IslandBitmap(appIcon, 22.dp, circle = true) }) } }, @@ -952,9 +953,8 @@ class NotificationsPlugin : BaseIslandPlugin() { } } - private fun monochrome() = settings.isIslandMonochromeEnabled() + private fun themedBubble() = settings.isIslandNotifThemedBubbleActive() - // Monochrome swaps the picture for the app's notification glyph on the island's softened accent @Composable private fun AlertIcon( alert: ActiveNotificationAlert, @@ -963,8 +963,8 @@ class NotificationsPlugin : BaseIslandPlugin() { circle: Boolean = false, fallbackRes: Int = R.drawable.rounded_notifications_unread_24, ) { - if (monochrome()) { - NotificationGlyph(alert.smallIcon, alert.appColor?.let { Color(soften(it)) }, size, fallbackRes = fallbackRes) + if (themedBubble()) { + NotificationGlyph(alert.themedIcon, alert.smallIcon, alert.appColor?.let { Color(soften(it)) }, size, fallbackRes = fallbackRes) } else { IslandBitmap(bitmap, size, circle = circle, fallbackRes = fallbackRes) } diff --git a/app/src/main/java/com/sameerasw/essentials/island/service/IslandCoordinator.kt b/app/src/main/java/com/sameerasw/essentials/island/service/IslandCoordinator.kt index 963cb7580..028e1db92 100644 --- a/app/src/main/java/com/sameerasw/essentials/island/service/IslandCoordinator.kt +++ b/app/src/main/java/com/sameerasw/essentials/island/service/IslandCoordinator.kt @@ -824,7 +824,7 @@ class IslandCoordinator( "alarm" to SettingsRepository.KEY_ISLAND_ALARM_LAUNCHER_ONLY, ) val CONFIG_KEYS = setOf( - SettingsRepository.KEY_ISLAND_MONOCHROME, + SettingsRepository.KEY_ISLAND_NOTIF_THEMED_BUBBLE, SettingsRepository.KEY_ISLAND_NOTIF_QUEUE_STYLE, SettingsRepository.KEY_ISLAND_NOTIF_QUEUE_INLINE, SettingsRepository.KEY_ISLAND_USE_AUTO_DETECT, diff --git a/app/src/main/java/com/sameerasw/essentials/island/ui/components/IslandComponents.kt b/app/src/main/java/com/sameerasw/essentials/island/ui/components/IslandComponents.kt index 4b376937a..2b1df6627 100644 --- a/app/src/main/java/com/sameerasw/essentials/island/ui/components/IslandComponents.kt +++ b/app/src/main/java/com/sameerasw/essentials/island/ui/components/IslandComponents.kt @@ -61,14 +61,12 @@ import androidx.compose.material3.Text import androidx.compose.foundation.layout.Row import com.sameerasw.essentials.island.ui.IslandMotion import androidx.compose.foundation.background -import androidx.compose.material3.MaterialTheme -import androidx.compose.ui.graphics.ColorFilter import kotlinx.coroutines.delay import kotlin.random.Random -// Monochrome stand-in for a notification picture: the app's status bar glyph on its accent color @Composable fun NotificationGlyph( + themedIcon: Bitmap?, smallIcon: Bitmap?, color: Color?, size: Dp, @@ -85,13 +83,14 @@ fun NotificationGlyph( .background(background), contentAlignment = Alignment.Center, ) { - if (smallIcon != null) { + val icon = themedIcon ?: smallIcon + if (icon != null) { Image( - bitmap = remember(smallIcon) { smallIcon.asImageBitmap() }, + bitmap = remember(icon) { icon.asImageBitmap() }, contentDescription = null, contentScale = ContentScale.Fit, colorFilter = ColorFilter.tint(glyph), - modifier = Modifier.fillMaxSize(0.6f), + modifier = Modifier.fillMaxSize(if (themedIcon != null) 1f else 0.6f), ) } else { Icon(painterResource(fallbackRes), null, tint = glyph, modifier = Modifier.fillMaxSize(0.6f)) diff --git a/app/src/main/java/com/sameerasw/essentials/services/NotificationListener.kt b/app/src/main/java/com/sameerasw/essentials/services/NotificationListener.kt index df99aaa4a..2fb45a60b 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/NotificationListener.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/NotificationListener.kt @@ -2358,12 +2358,18 @@ class NotificationListener : NotificationListenerService() { notif.smallIcon?.loadDrawable(this)?.let { smallIconBitmap = AppUtil.drawableToBitmap(it) } } catch (_: Exception) {} + val themedBubble = SettingsRepository(this).isIslandNotifThemedBubbleActive() + val paletteSource = if (themedBubble) { + runCatching { AppUtil.drawableToBitmap(packageManager.getApplicationIcon(sbn.packageName)) }.getOrNull() ?: bitmap + } else { + bitmap + } var appColor: Int? = null if (notif.color != 0 && notif.color != android.graphics.Color.TRANSPARENT) { appColor = notif.color - } else if (bitmap != null) { + } else if (paletteSource != null) { try { - val palette = androidx.palette.graphics.Palette.from(bitmap).generate() + val palette = androidx.palette.graphics.Palette.from(paletteSource).generate() val vibrant = palette.getVibrantColor(0) val dominant = palette.getDominantColor(0) if (vibrant != 0) { @@ -2408,6 +2414,7 @@ class NotificationListener : NotificationListenerService() { appIcon = appIcon, actions = actionList, chatIcon = chatIcon, + themedIcon = if (themedBubble) AppIconUtil.monochromeAppIcon(this, sbn.packageName) else null, smallIcon = smallIconBitmap, ) } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSubSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSubSettingsUI.kt index 94901ebbb..b24c91ca6 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSubSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSubSettingsUI.kt @@ -1299,18 +1299,6 @@ fun IslandNotificationSettingsUI( modifier = Modifier.highlight(highlightSetting == "island_catch_up_timeout"), ) } - - IconToggleItem( - iconRes = R.drawable.rounded_invert_colors_24, - title = stringResource(R.string.island_monochrome_title), - description = stringResource(R.string.island_monochrome_desc), - isChecked = viewModel.isIslandMonochrome.value, - onCheckedChange = { checked -> - HapticUtil.performVirtualKeyHaptic(view) - viewModel.setIslandMonochrome(checked) - }, - modifier = Modifier.highlight(highlightSetting == "island_monochrome"), - ) } if (showOtpPermissions) { diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandNotificationHubOptionsBottomSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandNotificationHubOptionsBottomSheet.kt index 46ae00f8d..a7e2c63ca 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandNotificationHubOptionsBottomSheet.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandNotificationHubOptionsBottomSheet.kt @@ -8,6 +8,8 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource @@ -29,6 +31,8 @@ fun IslandNotificationHubOptionsBottomSheet( onDismissRequest: () -> Unit, ) { val view = LocalView.current + val context = LocalContext.current + val themedAppIcons = remember { SettingsRepository(context).getBoolean(SettingsRepository.KEY_ISLAND_THEMED_APP_ICONS, false) } EssentialsBottomSheet(onDismissRequest = onDismissRequest) { Column( @@ -90,11 +94,23 @@ fun IslandNotificationHubOptionsBottomSheet( iconRes = R.drawable.rounded_person_24, title = stringResource(R.string.island_notif_hub_chat_pictures), isChecked = viewModel.isIslandNotifHubPreferChatPictures.value, + enabled = !(viewModel.isIslandNotifThemedBubble.value && themedAppIcons), onCheckedChange = { HapticUtil.performVirtualKeyHaptic(view) viewModel.setIslandNotifHubPreferChatPictures(it) }, ) + IconToggleItem( + iconRes = R.drawable.rounded_invert_colors_24, + title = stringResource(R.string.island_notif_themed_bubble_title), + description = if (themedAppIcons) null else stringResource(R.string.island_notif_themed_bubble_needs_themed_icons), + isChecked = viewModel.isIslandNotifThemedBubble.value && themedAppIcons, + enabled = themedAppIcons, + onCheckedChange = { + HapticUtil.performVirtualKeyHaptic(view) + viewModel.setIslandNotifThemedBubble(it) + }, + ) ConfigSliderItem( title = stringResource(R.string.island_hide_icon_notifications), value = viewModel.islandNotifHubIcons.intValue.toFloat(), diff --git a/app/src/main/java/com/sameerasw/essentials/utils/AppIconUtil.kt b/app/src/main/java/com/sameerasw/essentials/utils/AppIconUtil.kt index d79ac292d..c62bb035d 100644 --- a/app/src/main/java/com/sameerasw/essentials/utils/AppIconUtil.kt +++ b/app/src/main/java/com/sameerasw/essentials/utils/AppIconUtil.kt @@ -55,6 +55,19 @@ object AppIconUtil { val icon = pm.getApplicationIcon(packageName) if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return icon if (!SettingsRepository(context).getBoolean(SettingsRepository.KEY_ISLAND_THEMED_APP_ICONS, false)) return icon + return themed(context, monochromeCandidate(pm, packageName, icon) ?: icon) ?: icon + } + + fun monochromeAppIcon(context: Context, packageName: String): Bitmap? { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return null + val pm = context.packageManager + val icon = runCatching { pm.getApplicationIcon(packageName) }.getOrNull() ?: return null + val adaptive = monochromeCandidate(pm, packageName, icon) ?: return null + return (themed(context, adaptive) as? BitmapDrawable)?.bitmap + } + + @androidx.annotation.RequiresApi(Build.VERSION_CODES.TIRAMISU) + private fun monochromeCandidate(pm: PackageManager, packageName: String, icon: Drawable): Drawable? { val launcher = android.content.Intent(android.content.Intent.ACTION_MAIN).addCategory(android.content.Intent.CATEGORY_LAUNCHER).setPackage(packageName) val candidates = listOf(icon) + ( try { @@ -63,8 +76,7 @@ object AppIconUtil { emptyList() } ) - val withMonochrome = candidates.firstOrNull { (it as? AdaptiveIconDrawable)?.monochrome != null } - return themed(context, withMonochrome ?: icon) ?: icon + return candidates.firstOrNull { (it as? AdaptiveIconDrawable)?.monochrome != null } } enum class ThemedMode { MONO, DUO } diff --git a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt index 5ae5c8d43..ff8a101bc 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt @@ -303,7 +303,7 @@ class MainViewModel : ViewModel() { val isIslandOtpEnabled = mutableStateOf(false) val islandOtpTimeoutMs = mutableLongStateOf(60_000L) val isIslandShowGlow = mutableStateOf(true) - val isIslandMonochrome = mutableStateOf(false) + val isIslandNotifThemedBubble = mutableStateOf(false) val isIslandBorderOutline = mutableStateOf(false) val islandBorderOutlineColor = mutableStateOf(SettingsRepository.ISLAND_BORDER_OUTLINE_DEFAULT_COLOR) val isIslandBorderOutlineDynamic = mutableStateOf(false) @@ -2597,7 +2597,7 @@ class MainViewModel : ViewModel() { isIslandOtpEnabled.value = settingsRepository.isIslandOtpEnabled() islandOtpTimeoutMs.longValue = settingsRepository.getIslandOtpTimeoutMs() isIslandShowGlow.value = settingsRepository.isIslandShowGlowEnabled() - isIslandMonochrome.value = settingsRepository.isIslandMonochromeEnabled() + isIslandNotifThemedBubble.value = settingsRepository.isIslandNotifThemedBubbleEnabled() isIslandBorderOutline.value = settingsRepository.isIslandBorderOutlineEnabled() islandBorderOutlineColor.value = settingsRepository.getIslandBorderOutlineColor() isIslandBorderOutlineDynamic.value = settingsRepository.isIslandBorderOutlineDynamicEnabled() @@ -6201,9 +6201,9 @@ class MainViewModel : ViewModel() { settingsRepository.setIslandShowGlowEnabled(enabled) } - fun setIslandMonochrome(enabled: Boolean) { - isIslandMonochrome.value = enabled - settingsRepository.setIslandMonochromeEnabled(enabled) + fun setIslandNotifThemedBubble(enabled: Boolean) { + isIslandNotifThemedBubble.value = enabled + settingsRepository.setIslandNotifThemedBubbleEnabled(enabled) } fun setIslandBorderOutline(enabled: Boolean) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7b1cef806..2c89a5122 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3231,8 +3231,8 @@ Glow spread Pulse duration App based color glow - App icon bubble - Show the app\'s notification icon on its app color instead of the picture or content + App icon color bubbles + Turn on Use themed app icons in Visuals Expanded width Camera gap Width when long pressed to expand