Skip to content
Merged
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 @@ -680,6 +680,11 @@ class SettingsRepository(
const val KEY_ISLAND_NOTIF_FILTER_APPS_LIST = "island_notif_filter_apps_list"
const val KEY_ISLAND_SHOW_NOTIFICATIONS = "island_show_notifications"
const val KEY_ISLAND_NOTIF_TAP_TO_OPEN = "island_notif_tap_to_open"
const val KEY_ISLAND_OTP_ENABLED = "island_otp_enabled"
const val KEY_ISLAND_OTP_TAP_TO_COPY = "island_otp_tap_to_copy"
const val KEY_ISLAND_OTP_DISMISS_AFTER_COPY = "island_otp_dismiss_after_copy"
const val KEY_ISLAND_OTP_BYPASS_FILTER = "island_otp_bypass_filter"
const val KEY_ISLAND_OTP_TIMEOUT_MS = "island_otp_timeout_ms"
const val KEY_ISLAND_BORDER_OUTLINE_ENABLED = "island_border_outline_enabled"
const val KEY_ISLAND_BORDER_OUTLINE_COLOR = "island_border_outline_color"
const val KEY_ISLAND_BORDER_OUTLINE_DYNAMIC = "island_border_outline_dynamic"
Expand Down Expand Up @@ -4396,6 +4401,14 @@ class SettingsRepository(
fun isIslandNotifTapToOpenEnabled(): Boolean = getBoolean(KEY_ISLAND_NOTIF_TAP_TO_OPEN, false)
fun setIslandNotifTapToOpenEnabled(enabled: Boolean) = putBoolean(KEY_ISLAND_NOTIF_TAP_TO_OPEN, enabled)

fun isIslandOtpEnabled(): Boolean = getBoolean(KEY_ISLAND_OTP_ENABLED, false)
fun setIslandOtpEnabled(enabled: Boolean) = putBoolean(KEY_ISLAND_OTP_ENABLED, enabled)
fun isIslandOtpTapToCopyEnabled(): Boolean = getBoolean(KEY_ISLAND_OTP_TAP_TO_COPY, true)
fun isIslandOtpDismissAfterCopyEnabled(): Boolean = getBoolean(KEY_ISLAND_OTP_DISMISS_AFTER_COPY, false)
fun isIslandOtpBypassFilterEnabled(): Boolean = getBoolean(KEY_ISLAND_OTP_BYPASS_FILTER, true)
fun getIslandOtpTimeoutMs(): Long = getLong(KEY_ISLAND_OTP_TIMEOUT_MS, 60_000L)
fun setIslandOtpTimeoutMs(ms: Long) = putLong(KEY_ISLAND_OTP_TIMEOUT_MS, ms)

fun isIslandNotifCompactHeadsUpEnabled(): Boolean = getBoolean(KEY_ISLAND_NOTIF_COMPACT_HEADS_UP, true)
fun setIslandNotifCompactHeadsUpEnabled(enabled: Boolean) = putBoolean(KEY_ISLAND_NOTIF_COMPACT_HEADS_UP, enabled)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,6 @@ data class ActiveNotificationAlert(
val chatIcon: Bitmap? = null,
val isSilent: Boolean = false,
val blockedByDnd: Boolean = false,
val otpCode: String? = null,
)

Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import android.content.Context
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import com.sameerasw.essentials.R
import com.sameerasw.essentials.utils.PermissionGrantUtil
import com.sameerasw.essentials.utils.PermissionUtils
import com.sameerasw.essentials.utils.ShellUtils

Expand Down Expand Up @@ -131,6 +132,11 @@ enum class AppPermission(
iconRes = R.drawable.rounded_image_24,
aliases = listOf("READ_MEDIA_IMAGES", "READ_MEDIA_VIDEO", "READ_MEDIA_AUDIO", "READ_EXTERNAL_STORAGE", "MANAGE_EXTERNAL_STORAGE"),
),
SENSITIVE_NOTIFICATIONS(
key = "SENSITIVE_NOTIFICATIONS",
titleRes = R.string.perm_sensitive_notifications_title,
iconRes = R.drawable.rounded_vpn_key_24,
),
NOTIFICATION_BUBBLES(
key = "NOTIFICATION_BUBBLES",
titleRes = R.string.perm_bubbles_title,
Expand All @@ -145,6 +151,7 @@ enum class AppPermission(
DRAW_OVERLAYS -> PermissionUtils.canDrawOverlays(context)
WRITE_SETTINGS -> PermissionUtils.canWriteSystemSettings(context)
NOTIFICATION_POLICY -> PermissionUtils.hasNotificationPolicyAccess(context)
SENSITIVE_NOTIFICATIONS -> PermissionGrantUtil.hasSensitiveNotifications(context)
ROOT, SHIZUKU -> ShellUtils.isAvailable(context) && ShellUtils.hasPermission(context)
else -> true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1089,6 +1089,31 @@ object FeatureRegistry {
R.string.island_catch_up_desc,
"island_catch_up",
),
SearchSetting(
R.string.island_otp_enabled_title,
R.string.island_otp_enabled_desc,
"island_otp_enabled",
),
SearchSetting(
R.string.island_otp_tap_to_copy_title,
R.string.island_otp_tap_to_copy_desc,
"island_otp_tap_to_copy",
),
SearchSetting(
R.string.island_otp_dismiss_after_copy_title,
R.string.island_otp_section,
"island_otp_dismiss_after_copy",
),
SearchSetting(
R.string.island_otp_timeout_title,
R.string.island_otp_section,
"island_otp_timeout_ms",
),
SearchSetting(
R.string.island_otp_bypass_filter_title,
R.string.island_otp_bypass_filter_desc,
"island_otp_bypass_filter",
),
),
) {
override fun isEnabled(viewModel: MainViewModel) = viewModel.isIslandEnabled.value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ fun initPermissionRegistry() {

// Post notifications permission
PermissionRegistry.register("POST_NOTIFICATIONS", R.string.feat_caffeinate_title)
PermissionRegistry.register("SENSITIVE_NOTIFICATIONS", R.string.island_otp_section)
PermissionRegistry.register("POST_NOTIFICATIONS", R.string.feat_battery_notification_title)

// Read phone state permission
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ class InteractionOverrides(
val onTap: (() -> Boolean)? = null,
val onLongPress: (() -> Unit)? = null,
val onExpandedTap: (() -> Boolean)? = null,
val onLineTap: (() -> Boolean)? = null,
) {
companion object {
val Default = InteractionOverrides()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,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.IslandIcon
import com.sameerasw.essentials.services.NotificationListener
import com.sameerasw.essentials.utils.HapticUtil

Expand All @@ -53,6 +54,11 @@ class NotificationsPlugin : BaseIslandPlugin() {
SettingsRepository.KEY_ISLAND_SHOW_NOTIFICATIONS,
SettingsRepository.KEY_ISLAND_NOTIF_CONCEAL_LOCKED,
SettingsRepository.KEY_ISLAND_NOTIF_CONCEAL_CHAT_PICTURES,
SettingsRepository.KEY_ISLAND_OTP_ENABLED,
SettingsRepository.KEY_ISLAND_OTP_TAP_TO_COPY,
SettingsRepository.KEY_ISLAND_OTP_DISMISS_AFTER_COPY,
SettingsRepository.KEY_ISLAND_OTP_BYPASS_FILTER,
SettingsRepository.KEY_ISLAND_OTP_TIMEOUT_MS,
)

private val alerts = ArrayDeque<ActiveNotificationAlert>()
Expand Down Expand Up @@ -149,6 +155,10 @@ class NotificationsPlugin : BaseIslandPlugin() {
private val timeoutRunnable = Runnable { onTimeout() }
private val catchUpRunnable = Runnable { clearAll() }

private var copiedKey: String? = null
private var postedWhileCopied = false
private val copiedRunnable = Runnable { finishCopy() }

private val listener = object : NotificationListener.NotificationAlertListener {
override fun onNotificationAlertPosted(alert: ActiveNotificationAlert) {
ctx?.mainHandler?.post { onPosted(alert) }
Expand Down Expand Up @@ -195,10 +205,22 @@ class NotificationsPlugin : BaseIslandPlugin() {
private fun passesFilters(alert: ActiveNotificationAlert): Boolean {
if (alert.isSilent && settings.isIslandNotifSkipSilentEnabled()) return false
if (alert.blockedByDnd && settings.isIslandNotifFollowDndEnabled()) return false
if (isOtp(alert) && settings.isIslandOtpBypassFilterEnabled()) return true
if (!settings.isIslandNotifFilterAppsEnabled()) return true
return settings.loadIslandNotifFilterApps().any { it.isEnabled && it.packageName == alert.packageName }
}

private fun otpCodeOf(alert: ActiveNotificationAlert) = alert.otpCode?.takeIf { settings.isIslandOtpEnabled() }

private fun isOtp(alert: ActiveNotificationAlert) = otpCodeOf(alert) != null

private fun timeoutMs(): Long {
val base = settings.getIslandTimeoutMs()
return if (currentAlert()?.let(::otpCodeOf) != null) settings.getIslandOtpTimeoutMs() else base
}

private fun maskOtp(code: String) = formatOtp(code).map { if (it == ' ') ' ' else '•' }.joinToString("")

private fun concealed(): Boolean =
settings.getBoolean(SettingsRepository.KEY_ISLAND_NOTIF_CONCEAL_LOCKED, false) &&
(context.getSystemService(Context.KEYGUARD_SERVICE) as? KeyguardManager)?.isKeyguardLocked == true
Expand All @@ -212,6 +234,12 @@ class NotificationsPlugin : BaseIslandPlugin() {
render()
return
}
if (copiedKey != null) {
alerts.addLast(alert)
while (alerts.size > MAX_QUEUE) alerts.removeFirst()
postedWhileCopied = true
return
}
c.mainHandler.removeCallbacks(catchUpRunnable)
catchingUp = false
if (queueEnabled() && showingHere()) {
Expand Down Expand Up @@ -255,10 +283,11 @@ class NotificationsPlugin : BaseIslandPlugin() {
private fun popUp() {
val c = ctx ?: return
if (minimal()) return
val otpNeedsExpand = currentAlert()?.let(::isOtp) == true && !settings.isIslandOtpTapToCopyEnabled()
val lineAvailable = settings.isIslandLineStageEnabled() && settings.isIslandNotifCompactHeadsUpEnabled() && !otpNeedsExpand
markSeen()
val lineAvailable = settings.isIslandLineStageEnabled() && settings.isIslandNotifCompactHeadsUpEnabled()
c.request(
if (lineAvailable) PluginRequest.Peek(ITEM_KEY, settings.getIslandTimeoutMs(), sticky = true)
if (lineAvailable) PluginRequest.Peek(ITEM_KEY, timeoutMs(), sticky = true)
else PluginRequest.Expand(ITEM_KEY).also { autoExpanded = true },
)
}
Expand Down Expand Up @@ -319,16 +348,19 @@ class NotificationsPlugin : BaseIslandPlugin() {
private fun scheduleTimeout() {
val handler = ctx?.mainHandler ?: return
handler.removeCallbacks(timeoutRunnable)
handler.postDelayed(timeoutRunnable, settings.getIslandTimeoutMs())
handler.postDelayed(timeoutRunnable, timeoutMs())
}

private fun cancelTimers() {
ctx?.mainHandler?.removeCallbacks(timeoutRunnable)
ctx?.mainHandler?.removeCallbacks(catchUpRunnable)
ctx?.mainHandler?.removeCallbacks(copiedRunnable)
}

private fun clearAll() {
catchingUp = false
copiedKey = null
postedWhileCopied = false
cancelTimers()
alerts.clear()
seen.clear()
Expand Down Expand Up @@ -358,6 +390,10 @@ class NotificationsPlugin : BaseIslandPlugin() {
}

private fun render() {
if (copiedKey != null) {
publish(copiedItem())
return
}
val alert = currentAlert()
if (alert == null) {
publish(null)
Expand Down Expand Up @@ -435,6 +471,8 @@ class NotificationsPlugin : BaseIslandPlugin() {

private fun itemFor(alert: ActiveNotificationAlert): IslandItem {
if (concealed()) return concealedItemFor(alert)
val code = otpCodeOf(alert)
if (code != null) return otpItemFor(alert, code)
val (sender, message) = senderAndMessage(context, alert)
val showGlow = settings.isIslandShowGlowEnabled()
val tapToOpen = settings.isIslandNotifTapToOpenEnabled()
Expand Down Expand Up @@ -512,12 +550,21 @@ class NotificationsPlugin : BaseIslandPlugin() {
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) },
start = appNameFor(context, alert),
end = "",
condensed = condensedLine(),
),
line = if (isOtp(alert)) {
LineContent(
icon = { IslandBitmap(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) },
start = appNameFor(context, alert),
end = "",
condensed = condensedLine(),
)
},
accent = alert.appColor?.let { Color(soften(it)) },
dismissible = true,
onDismiss = {
Expand All @@ -531,6 +578,108 @@ class NotificationsPlugin : BaseIslandPlugin() {
)
}

private fun otpItemFor(alert: ActiveNotificationAlert, code: String): IslandItem {
val (sender, message) = senderAndMessage(context, alert)
val showGlow = settings.isIslandShowGlowEnabled()
val icon = alert.appIcon ?: alert.icon
val copy = { copyCode(alert, code) }
val dismiss = {
NotificationListener.dismissNotification(alert.key)
popCurrent(reExpand = false)
}
return IslandItem(
key = ITEM_KEY,
priority = IslandPriority.NOTIFICATION,
placement = CompactPlacement.Dynamic,
compact = listOf(
CompactCell("notif.icon") {
IslandBitmap(icon, 22.dp, fallbackRes = R.drawable.rounded_notifications_unread_24)
},
),
line = LineContent(
icon = { IslandBitmap(icon, 24.dp, fallbackRes = R.drawable.rounded_notifications_unread_24) },
start = sender,
end = formatOtp(code),
endSlot = { IslandIcon(R.drawable.rounded_content_copy_24, size = 18.dp) },
),
expanded = ExpandedContent { scope ->
OtpExpanded(
alert = alert,
code = code,
sender = sender,
message = message,
showGlow = showGlow,
onCopy = copy,
onDismiss = dismiss,
scope = scope,
)
},
accent = alert.appColor?.let { Color(soften(it)) },
dismissible = true,
onDismiss = dismiss,
onOpen = {
if (!sendPendingIntent(context, alert.contentIntent)) launchPackage(context, alert.packageName)
popCurrent(reExpand = false)
},
interactions = if (settings.isIslandOtpTapToCopyEnabled()) {
InteractionOverrides(onTap = { copy(); true }, onLineTap = { copy(); true })
} else {
InteractionOverrides.Default
},
sourcePackage = alert.packageName,
contentId = alert.key,
)
}

private fun copiedItem() = IslandItem(
key = ITEM_KEY,
priority = IslandPriority.NOTIFICATION,
placement = CompactPlacement.Dynamic,
compact = listOf(CompactCell("notif.copied") { IslandIcon(R.drawable.rounded_check_24, size = 20.dp) }),
line = LineContent(
icon = { IslandIcon(R.drawable.rounded_check_24, size = 22.dp) },
start = context.getString(R.string.island_otp_copied),
end = "",
),
contentId = "otp.copied",
)

private fun copyCode(alert: ActiveNotificationAlert, code: String) {
val c = ctx ?: return
if (!OtpClipboard.copy(context, code)) return
HapticUtil.performStrongTickHaptic(context)
cancelTimers()
copiedKey = alert.key
render()
c.request(PluginRequest.Collapse(ITEM_KEY))
c.request(PluginRequest.Peek(ITEM_KEY, COPIED_MS))
c.mainHandler.postDelayed(copiedRunnable, COPIED_MS)
}

private fun finishCopy() {
val key = copiedKey ?: return
copiedKey = null
if (settings.isIslandOtpDismissAfterCopyEnabled()) NotificationListener.dismissNotification(key)
val index = alerts.indexOfFirst { it.key == key }
if (index >= 0) currentIndex = alerts.removeKeepingCurrent(index, currentIndex)
if (alerts.isEmpty()) {
clearAll()
return
}
if (postedWhileCopied) {
postedWhileCopied = false
if (!queueEnabled()) while (alerts.size > 1) alerts.removeFirst()
currentIndex = alerts.lastIndex
manualPick = false
scheduleTimeout()
render()
popUp()
return
}
scheduleTimeout()
render()
}

private fun stackIconFor(alert: ActiveNotificationAlert, current: Boolean): StackIcon {
val icon = if (concealed() && hidesChatPictures()) alert.appIcon ?: alert.icon else alert.chatIcon ?: alert.appIcon ?: alert.icon
return StackIcon(alert.key, current = current, onSelect = { select(alert.key) }) { size ->
Expand Down Expand Up @@ -592,6 +741,7 @@ class NotificationsPlugin : BaseIslandPlugin() {
companion object {
const val ITEM_KEY = "notifications"
private const val MAX_QUEUE = 20
private const val COPIED_MS = 1_500L

private val GENERIC_TITLES = setOf(
"you", "whatsapp", "messages", "telegram", "gmail", "instagram", "slack", "discord", "essentials",
Expand Down
Loading
Loading