Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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_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"
Expand Down Expand Up @@ -4106,6 +4107,12 @@ class SettingsRepository(
fun isIslandShowGlowEnabled(): Boolean = getBoolean(KEY_ISLAND_SHOW_GLOW, true)
fun setIslandShowGlowEnabled(enabled: Boolean) = putBoolean(KEY_ISLAND_SHOW_GLOW, 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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,7 @@ data class ActiveNotificationAlert(
val ongoing: Boolean = false,
val isConversation: Boolean = false,
val hubOnly: Boolean = false,
val themedIcon: Bitmap? = null,
val smallIcon: Bitmap? = null,
)

Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -48,6 +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_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,
Expand Down Expand Up @@ -565,7 +571,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)
}
}

Expand Down Expand Up @@ -745,16 +751,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() && !themedBubble() && 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(),
Expand Down Expand Up @@ -802,24 +808,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() && !themedBubble() && !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(),
Expand Down Expand Up @@ -853,11 +859,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) },
Expand Down Expand Up @@ -943,7 +949,24 @@ 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 themedBubble() = settings.isIslandNotifThemedBubbleActive()

@Composable
private fun AlertIcon(
alert: ActiveNotificationAlert,
bitmap: Bitmap?,
size: Dp,
circle: Boolean = false,
fallbackRes: Int = R.drawable.rounded_notifications_unread_24,
) {
if (themedBubble()) {
NotificationGlyph(alert.themedIcon, alert.smallIcon, alert.appColor?.let { Color(soften(it)) }, size, fallbackRes = fallbackRes)
} else {
IslandBitmap(bitmap, size, circle = circle, fallbackRes = fallbackRes)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -825,6 +824,7 @@ class IslandCoordinator(
"alarm" to SettingsRepository.KEY_ISLAND_ALARM_LAUNCHER_ONLY,
)
val CONFIG_KEYS = setOf(
SettingsRepository.KEY_ISLAND_NOTIF_THEMED_BUBBLE,
SettingsRepository.KEY_ISLAND_NOTIF_QUEUE_STYLE,
SettingsRepository.KEY_ISLAND_NOTIF_QUEUE_INLINE,
SettingsRepository.KEY_ISLAND_USE_AUTO_DETECT,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,51 @@ 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
import androidx.compose.material3.Text
import androidx.compose.foundation.layout.Row
import com.sameerasw.essentials.island.ui.IslandMotion
import androidx.compose.foundation.background
import kotlinx.coroutines.delay
import kotlin.random.Random

@Composable
fun NotificationGlyph(
themedIcon: Bitmap?,
smallIcon: Bitmap?,
color: Color?,
size: Dp,
modifier: Modifier = Modifier,
fallbackRes: Int,
) {
val background = color ?: MaterialTheme.colorScheme.primary
val glyph = lerp(background, if (background.luminance() > 0.179f) Color.Black else Color.White, 0.8f)
Box(
modifier
.sizeIn(maxWidth = size, maxHeight = size)
.aspectRatio(1f, matchHeightConstraintsFirst = true)
.clip(CircleShape)
.background(background),
contentAlignment = Alignment.Center,
) {
val icon = themedIcon ?: smallIcon
if (icon != null) {
Image(
bitmap = remember(icon) { icon.asImageBitmap() },
contentDescription = null,
contentScale = ContentScale.Fit,
colorFilter = ColorFilter.tint(glyph),
modifier = Modifier.fillMaxSize(if (themedIcon != null) 1f else 0.6f),
)
} else {
Icon(painterResource(fallbackRes), null, tint = glyph, modifier = Modifier.fillMaxSize(0.6f))
}
}
}

@Composable
fun IslandBitmap(
bitmap: Bitmap?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2353,12 +2353,23 @@ class NotificationListener : NotificationListenerService() {
bitmap = appIcon
}

var smallIconBitmap: Bitmap? = null
try {
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) {
Expand Down Expand Up @@ -2403,6 +2414,8 @@ class NotificationListener : NotificationListenerService() {
appIcon = appIcon,
actions = actionList,
chatIcon = chatIcon,
themedIcon = if (themedBubble) AppIconUtil.monochromeAppIcon(this, sbn.packageName) else null,
smallIcon = smallIconBitmap,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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(),
Expand Down
16 changes: 14 additions & 2 deletions app/src/main/java/com/sameerasw/essentials/utils/AppIconUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ class MainViewModel : ViewModel() {
val isIslandOtpEnabled = mutableStateOf(false)
val islandOtpTimeoutMs = mutableLongStateOf(60_000L)
val isIslandShowGlow = mutableStateOf(true)
val isIslandNotifThemedBubble = mutableStateOf(false)
val isIslandBorderOutline = mutableStateOf(false)
val islandBorderOutlineColor = mutableStateOf(SettingsRepository.ISLAND_BORDER_OUTLINE_DEFAULT_COLOR)
val isIslandBorderOutlineDynamic = mutableStateOf(false)
Expand Down Expand Up @@ -2596,6 +2597,7 @@ class MainViewModel : ViewModel() {
isIslandOtpEnabled.value = settingsRepository.isIslandOtpEnabled()
islandOtpTimeoutMs.longValue = settingsRepository.getIslandOtpTimeoutMs()
isIslandShowGlow.value = settingsRepository.isIslandShowGlowEnabled()
isIslandNotifThemedBubble.value = settingsRepository.isIslandNotifThemedBubbleEnabled()
isIslandBorderOutline.value = settingsRepository.isIslandBorderOutlineEnabled()
islandBorderOutlineColor.value = settingsRepository.getIslandBorderOutlineColor()
isIslandBorderOutlineDynamic.value = settingsRepository.isIslandBorderOutlineDynamicEnabled()
Expand Down Expand Up @@ -6199,6 +6201,11 @@ class MainViewModel : ViewModel() {
settingsRepository.setIslandShowGlowEnabled(enabled)
}

fun setIslandNotifThemedBubble(enabled: Boolean) {
isIslandNotifThemedBubble.value = enabled
settingsRepository.setIslandNotifThemedBubbleEnabled(enabled)
}

fun setIslandBorderOutline(enabled: Boolean) {
isIslandBorderOutline.value = enabled
settingsRepository.setIslandBorderOutlineEnabled(enabled)
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3231,6 +3231,8 @@
<string name="island_pulse_shadow_spread_title">Glow spread</string>
<string name="island_pulse_shadow_duration_title">Pulse duration</string>
<string name="island_show_glow_desc">App based color glow</string>
<string name="island_notif_themed_bubble_title">App icon color bubbles</string>
<string name="island_notif_themed_bubble_needs_themed_icons">Turn on Use themed app icons in Visuals</string>
<string name="island_expanded_width_title">Expanded width</string>
<string name="island_cutout_gap_title">Camera gap</string>
<string name="island_expanded_width_desc">Width when long pressed to expand</string>
Expand Down
Loading