From 0675978af021329c73f75a5d330d87e807471238 Mon Sep 17 00:00:00 2001 From: Isaac Serrano Date: Tue, 13 Oct 2026 11:45:46 -0700 Subject: [PATCH 1/4] feat(island): add navigation plugin and settings to island --- .../data/repository/SettingsRepository.kt | 22 +++ .../model/NavigationNotificationData.kt | 73 ++++++++ .../domain/registry/FeatureRegistry.kt | 5 + .../essentials/island/model/IslandModels.kt | 1 + .../island/model/IslandPriorityEntries.kt | 1 + .../plugins/navigation/NavigationExpanded.kt | 93 +++++++++ .../plugins/navigation/NavigationPlugin.kt | 177 ++++++++++++++++++ .../island/service/IslandCoordinator.kt | 2 + .../services/NotificationListener.kt | 86 ++++++++- .../ui/features/display/IslandSettingsUI.kt | 37 +++- .../IslandNavigationOptionsBottomSheet.kt | 132 +++++++++++++ .../essentials/viewmodels/MainViewModel.kt | 7 + app/src/main/res/values/strings.xml | 14 ++ .../essentials/domain/NavigationParserTest.kt | 67 +++++++ 14 files changed, 708 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/com/sameerasw/essentials/domain/model/NavigationNotificationData.kt create mode 100644 app/src/main/java/com/sameerasw/essentials/island/plugins/navigation/NavigationExpanded.kt create mode 100644 app/src/main/java/com/sameerasw/essentials/island/plugins/navigation/NavigationPlugin.kt create mode 100644 app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandNavigationOptionsBottomSheet.kt create mode 100644 app/src/test/java/com/sameerasw/essentials/domain/NavigationParserTest.kt 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 507ae9618..5898a80de 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 @@ -610,6 +610,13 @@ class SettingsRepository( const val KEY_ISLAND_SHOW_TRAVEL = "island_show_travel" const val KEY_ISLAND_SHOW_CAFFEINATE = "island_show_caffeinate" const val KEY_ISLAND_SHOW_DEVICES = "island_show_devices" + const val KEY_ISLAND_SHOW_NAVIGATION = "island_show_navigation" + const val KEY_ISLAND_NAVIGATION_ON_NEW = "island_navigation_on_new" + const val KEY_ISLAND_NAVIGATION_PEEK_AFTER_EXPAND = "island_navigation_peek_after_expand" + const val ISLAND_NAVIGATION_ON_NEW_EXPAND = "expand" + const val ISLAND_NAVIGATION_ON_NEW_PEEK = "peek" + const val ISLAND_NAVIGATION_ON_NEW_OFF = "off" + const val KEY_ISLAND_NAVIGATION_EXPAND_DISTANCE = "island_navigation_expand_distance" const val KEY_ISLAND_BRIEF_ENABLED = "island_brief_enabled" const val KEY_ISLAND_BRIEF_SHOW_ALARM = "island_brief_show_alarm" const val KEY_ISLAND_BRIEF_TWO_LINE_HEADER = "island_brief_two_line_header" @@ -4051,6 +4058,21 @@ class SettingsRepository( fun isIslandShowDevicesEnabled(): Boolean = getBoolean(KEY_ISLAND_SHOW_DEVICES, true) fun setIslandShowDevicesEnabled(enabled: Boolean) = putBoolean(KEY_ISLAND_SHOW_DEVICES, enabled) + fun isIslandShowNavigationEnabled(): Boolean = getBoolean(KEY_ISLAND_SHOW_NAVIGATION, true) + fun setIslandShowNavigationEnabled(enabled: Boolean) = putBoolean(KEY_ISLAND_SHOW_NAVIGATION, enabled) + + // What the island does when a new instruction arrives: expand, peek or off + fun getIslandNavigationOnNew(): String = + getString(KEY_ISLAND_NAVIGATION_ON_NEW, ISLAND_NAVIGATION_ON_NEW_PEEK) ?: ISLAND_NAVIGATION_ON_NEW_PEEK + fun setIslandNavigationOnNew(mode: String) = putString(KEY_ISLAND_NAVIGATION_ON_NEW, mode) + + fun isIslandNavigationPeekAfterExpandEnabled(): Boolean = getBoolean(KEY_ISLAND_NAVIGATION_PEEK_AFTER_EXPAND, true) + fun setIslandNavigationPeekAfterExpandEnabled(enabled: Boolean) = putBoolean(KEY_ISLAND_NAVIGATION_PEEK_AFTER_EXPAND, enabled) + + // Meters before a turn at which the island expands; 0 = off + fun getIslandNavigationExpandDistance(): Int = getInt(KEY_ISLAND_NAVIGATION_EXPAND_DISTANCE, 200) + fun setIslandNavigationExpandDistance(meters: Int) = putInt(KEY_ISLAND_NAVIGATION_EXPAND_DISTANCE, meters) + fun isIslandBriefEnabled(): Boolean = getBoolean(KEY_ISLAND_BRIEF_ENABLED, false) fun setIslandBriefEnabled(enabled: Boolean) = putBoolean(KEY_ISLAND_BRIEF_ENABLED, enabled) diff --git a/app/src/main/java/com/sameerasw/essentials/domain/model/NavigationNotificationData.kt b/app/src/main/java/com/sameerasw/essentials/domain/model/NavigationNotificationData.kt new file mode 100644 index 000000000..1916b2592 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/domain/model/NavigationNotificationData.kt @@ -0,0 +1,73 @@ +package com.sameerasw.essentials.domain.model + +import android.app.PendingIntent +import android.graphics.Bitmap + +data class NavigationNotificationData( + val key: String, + val packageName: String, + val instruction: String?, + val distance: String?, + val eta: String?, + // Turn arrow, usually the notification's large icon. + val maneuver: Bitmap?, + val contentIntent: PendingIntent?, + val exitIntent: PendingIntent?, + val postTime: Long, +) + +object NavigationParser { + data class Parsed(val instruction: String?, val distance: String?, val eta: String?) + + private val distanceRegex = Regex("""\d[\d.,]*\s?(km|m|mi|ft|yd)\b""", RegexOption.IGNORE_CASE) + private val exitRegex = Regex( + """\b(exit|stop|end|close|quit|salir|terminar|detener|finalizar|beenden|quitter|arrêter|sair|encerrar)\b""", + RegexOption.IGNORE_CASE, + ) + + fun isDistance(s: String?) = s != null && s.length <= 24 && distanceRegex.containsMatchIn(s) + + // Apps put the distance in either the title or the text, so take whichever looks like one. + // `chip` is the Live Update status-bar text, which navigation apps fill with the distance. + fun parse(title: String?, text: String?, subText: String?, chip: String?): Parsed { + val titleIsDistance = isDistance(title) + val textIsDistance = !titleIsDistance && isDistance(text) + val instruction = when { + titleIsDistance -> text + textIsDistance -> title + else -> title ?: text + } + val leftover = if (!titleIsDistance && !textIsDistance && title != null) text else null + val distance = chip?.takeIf { it.isNotBlank() } ?: when { + titleIsDistance -> title + textIsDistance -> text + else -> null + } + return Parsed( + instruction = instruction?.takeIf { it.isNotBlank() }, + distance = distance, + eta = (subText ?: leftover)?.takeIf { it.isNotBlank() }, + ) + } + + private val meterRegex = Regex("""(\d{1,3}(?:,\d{3})+|\d+(?:[.,]\d+)?)\s?(km|m|mi|ft|yd)\b""", RegexOption.IGNORE_CASE) + private val unitMeters = mapOf("km" to 1000.0, "m" to 1.0, "mi" to 1609.344, "ft" to 0.3048, "yd" to 0.9144) + + // "1,200 ft" uses a thousands separator; "1,2 km" uses a decimal comma. + fun meters(distance: String?): Double? { + val match = meterRegex.find(distance ?: return null) ?: return null + val (number, unit) = match.destructured + val value = if (Regex("""\d{1,3}(,\d{3})+""").matches(number)) number.replace(",", "") else number.replace(',', '.') + return value.toDoubleOrNull()?.times(unitMeters.getValue(unit.lowercase())) + } + + // ponytail: matches the button label; an app with several unlabeled-looking actions gets no Exit button + fun exitActionIndex(titles: List): Int { + val match = titles.indexOfFirst { it != null && exitRegex.containsMatchIn(it) } + return when { + match >= 0 -> match + titles.size == 1 -> 0 + else -> -1 + } + } +} 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 61443cc45..5432ac7c6 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 @@ -775,6 +775,11 @@ object FeatureRegistry { R.string.island_stopwatch_default, "island_show_timers", ), + SearchSetting( + R.string.island_show_navigation_title, + R.string.island_navigation_desc, + "island_show_navigation", + ), SearchSetting( R.string.feat_caffeinate_title, R.string.caffeinate_notification_desc, diff --git a/app/src/main/java/com/sameerasw/essentials/island/model/IslandModels.kt b/app/src/main/java/com/sameerasw/essentials/island/model/IslandModels.kt index ee2e056fb..b52a0310b 100644 --- a/app/src/main/java/com/sameerasw/essentials/island/model/IslandModels.kt +++ b/app/src/main/java/com/sameerasw/essentials/island/model/IslandModels.kt @@ -21,6 +21,7 @@ object IslandPriority { const val CALENDAR_OVERRIDE = 6 const val NOTIFICATION = 10 + const val NAVIGATION = 14 const val PROGRESS = 15 const val FLASHLIGHT = 20 const val TIMER = 25 diff --git a/app/src/main/java/com/sameerasw/essentials/island/model/IslandPriorityEntries.kt b/app/src/main/java/com/sameerasw/essentials/island/model/IslandPriorityEntries.kt index 59bd90675..07d1e54c1 100644 --- a/app/src/main/java/com/sameerasw/essentials/island/model/IslandPriorityEntries.kt +++ b/app/src/main/java/com/sameerasw/essentials/island/model/IslandPriorityEntries.kt @@ -6,6 +6,7 @@ object IslandPriorityEntries { val all = listOf( Entry("battery", setOf("battery_alerts_plugin"), IslandPriority.BATTERY_ALERT), Entry("notifications", setOf("notifications"), IslandPriority.NOTIFICATION), + Entry("navigation", setOf("navigation"), IslandPriority.NAVIGATION), Entry("flashlight", setOf("flashlight"), IslandPriority.FLASHLIGHT), Entry("timer", setOf("timer"), IslandPriority.TIMER), Entry("media", setOf("media"), IslandPriority.MEDIA), diff --git a/app/src/main/java/com/sameerasw/essentials/island/plugins/navigation/NavigationExpanded.kt b/app/src/main/java/com/sameerasw/essentials/island/plugins/navigation/NavigationExpanded.kt new file mode 100644 index 000000000..d502e6aea --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/island/plugins/navigation/NavigationExpanded.kt @@ -0,0 +1,93 @@ +package com.sameerasw.essentials.island.plugins.navigation + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.sameerasw.essentials.R +import com.sameerasw.essentials.domain.model.NavigationNotificationData +import com.sameerasw.essentials.island.model.IslandExpandedScope +import com.sameerasw.essentials.island.ui.IslandTextStyles +import com.sameerasw.essentials.island.ui.components.CameraRow +import com.sameerasw.essentials.island.ui.components.ConnectedButtonRow +import com.sameerasw.essentials.island.ui.components.ConnectedItem +import com.sameerasw.essentials.island.ui.components.ConnectedTextLabel +import com.sameerasw.essentials.island.ui.components.MarqueeText + +@Composable +fun NavigationExpanded( + data: NavigationNotificationData, + accent: Color, + onOpen: () -> Unit, + onExit: (() -> Unit)?, + scope: IslandExpandedScope, +) { + val spec = scope.spec + val sidePadding = spec.expandedPadding + spec.expandedCorner * 0.35f + Box(propagateMinConstraints = true) { + Column(Modifier.fillMaxWidth().padding(spec.expandedOutset)) { + Spacer(Modifier.height(spec.expandedTopPadding)) + scope.CameraRow( + horizontalPadding = spec.cameraGap + spec.expandedCorner * 0.35f, + start = { + Maneuver(data, spec.cellSize.value.toInt()) + MarqueeText( + text = data.distance ?: data.instruction.orEmpty(), + style = IslandTextStyles.title.copy(color = accent), + modifier = Modifier.weight(1f), + ) + }, + end = {}, + ) + val instruction = data.instruction.takeIf { data.distance != null } + if (!instruction.isNullOrBlank()) { + Spacer(Modifier.height(4.dp)) + Text( + text = instruction, + style = IslandTextStyles.body.copy(color = Color.White, fontSize = 17.sp, lineHeight = 22.sp), + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.fillMaxWidth().padding(horizontal = sidePadding), + ) + } + if (!data.eta.isNullOrBlank()) { + Spacer(Modifier.height(2.dp)) + Text( + text = data.eta, + style = IslandTextStyles.body.copy(color = Color.White.copy(alpha = 0.7f)), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.fillMaxWidth().padding(horizontal = sidePadding), + ) + } + Spacer(Modifier.height(16.dp)) + ConnectedButtonRow( + height = 44.dp, + modifier = Modifier.padding(horizontal = sidePadding).padding(bottom = spec.expandedBottomPadding), + container = Color.White.copy(alpha = 0.2f), + items = listOfNotNull( + ConnectedItem({ + onOpen() + scope.collapse() + }) { ConnectedTextLabel(stringResource(R.string.island_progress_open)) }, + onExit?.let { exit -> + ConnectedItem({ + exit() + scope.collapse() + }) { ConnectedTextLabel(stringResource(R.string.island_navigation_exit)) } + }, + ), + ) + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/island/plugins/navigation/NavigationPlugin.kt b/app/src/main/java/com/sameerasw/essentials/island/plugins/navigation/NavigationPlugin.kt new file mode 100644 index 000000000..b5400353d --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/island/plugins/navigation/NavigationPlugin.kt @@ -0,0 +1,177 @@ +package com.sameerasw.essentials.island.plugins.navigation + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import com.sameerasw.essentials.R +import com.sameerasw.essentials.data.repository.SettingsRepository +import com.sameerasw.essentials.domain.model.NavigationNotificationData +import com.sameerasw.essentials.domain.model.NavigationParser +import com.sameerasw.essentials.island.model.CompactCell +import com.sameerasw.essentials.island.model.CompactPlacement +import com.sameerasw.essentials.island.model.ExpandedContent +import com.sameerasw.essentials.island.model.IslandItem +import com.sameerasw.essentials.island.model.IslandPluginContext +import com.sameerasw.essentials.island.model.IslandPriority +import com.sameerasw.essentials.island.model.IslandStage +import com.sameerasw.essentials.island.model.LineContent +import com.sameerasw.essentials.island.model.PluginRequest +import com.sameerasw.essentials.island.plugins.BaseIslandPlugin +import com.sameerasw.essentials.island.plugins.launchPackage +import com.sameerasw.essentials.island.plugins.sendPendingIntent +import com.sameerasw.essentials.island.ui.components.IslandBitmap +import com.sameerasw.essentials.island.ui.components.IslandIcon +import com.sameerasw.essentials.island.ui.components.RollingText +import com.sameerasw.essentials.services.NotificationListener + +class NavigationPlugin : BaseIslandPlugin() { + override val id = "navigation" + + override val settingKeys = setOf( + SettingsRepository.KEY_ISLAND_SHOW_NAVIGATION, + SettingsRepository.KEY_ISLAND_NAVIGATION_ON_NEW, + SettingsRepository.KEY_ISLAND_NAVIGATION_PEEK_AFTER_EXPAND, + SettingsRepository.KEY_ISLAND_NAVIGATION_EXPAND_DISTANCE, + ) + + private var data: NavigationNotificationData? = null + + // Instruction the island already expanded for, so each turn expands once + private var expandedFor: String? = null + + private val endExpand = Runnable { + val c = ctx ?: return@Runnable + // The user closed it or switched to something else + if (c.focusedKey() != ITEM_KEY || c.currentStage() != IslandStage.Expanded) return@Runnable + c.request(PluginRequest.Collapse(ITEM_KEY)) + if (settings.isIslandNavigationPeekAfterExpandEnabled() && settings.isIslandLineStageEnabled()) { + c.mainHandler.postDelayed(peekAfterCollapse, COLLAPSE_ANIM_MS) + } + } + + private val peekAfterCollapse = Runnable { + ctx?.request?.invoke(PluginRequest.Peek(ITEM_KEY, settings.getIslandPeekDurationMs())) + } + + private val listener = object : NotificationListener.NavigationNotificationListener { + override fun onNavigationNotificationUpdated(data: NavigationNotificationData?) { + ctx?.mainHandler?.post { update(data) } + } + } + + override fun onStart() { + NotificationListener.addNavigationNotificationListener(listener) + } + + override fun onStop() { + NotificationListener.removeNavigationNotificationListener(listener) + ctx?.mainHandler?.removeCallbacks(endExpand) + ctx?.mainHandler?.removeCallbacks(peekAfterCollapse) + data = null + expandedFor = null + } + + override fun refresh() { + ctx ?: return + data = NotificationListener.getLatestNavigationNotification() + render() + } + + private fun update(next: NavigationNotificationData?) { + val c = ctx ?: return + val turned = next?.instruction != null && next.instruction != data?.instruction + data = next + render() + if (next == null) { + expandedFor = null + c.mainHandler.removeCallbacks(endExpand) + c.mainHandler.removeCallbacks(peekAfterCollapse) + return + } + if (!enabled()) return + val instruction = next.instruction.orEmpty() + val threshold = settings.getIslandNavigationExpandDistance() + val meters = NavigationParser.meters(next.distance) + // Distance jumped back up: a new maneuver with the same text ("Turn right" twice in a row) + if (meters != null && meters > threshold * REARM_FACTOR) expandedFor = null + val approaching = threshold > 0 && meters != null && meters <= threshold && instruction != expandedFor + when { + approaching && expand(c) -> expandedFor = instruction + !turned -> {} + settings.getIslandNavigationOnNew() == SettingsRepository.ISLAND_NAVIGATION_ON_NEW_EXPAND -> expand(c) + settings.getIslandNavigationOnNew() == SettingsRepository.ISLAND_NAVIGATION_ON_NEW_PEEK && settings.isIslandLineStageEnabled() -> + c.request(PluginRequest.Peek(ITEM_KEY, settings.getIslandPeekDurationMs())) + } + } + + // Expands for a while, then collapses to a line peek or compact + private fun expand(c: IslandPluginContext): Boolean { + // Don't take over something the user is looking at; retried on the next distance update + val focused = c.focusedKey() + if (focused != null && focused != ITEM_KEY) return false + c.mainHandler.removeCallbacks(peekAfterCollapse) + c.request(PluginRequest.Expand(ITEM_KEY)) + c.mainHandler.removeCallbacks(endExpand) + c.mainHandler.postDelayed(endExpand, EXPANDED_MS) + return true + } + + private fun enabled() = settings.isIslandShowNavigationEnabled() + + private fun render() { + val d = data + if (ctx == null || d == null || !enabled()) { + publish(null) + return + } + publish( + IslandItem( + key = ITEM_KEY, + priority = IslandPriority.NAVIGATION, + placement = CompactPlacement.Dynamic, + compact = listOfNotNull( + d.distance?.let { CompactCell("navigation.distance") { RollingText(it) } }, + CompactCell("navigation.maneuver") { Maneuver(d, 18) }, + ), + line = LineContent( + icon = { Maneuver(d, 20) }, + start = d.instruction.orEmpty(), + end = d.distance.orEmpty(), + ), + expanded = ExpandedContent { scope -> + NavigationExpanded( + data = d, + accent = MaterialTheme.colorScheme.primary, + onOpen = { open(d) }, + onExit = d.exitIntent?.let { intent -> { sendPendingIntent(context, intent) } }, + scope = scope, + ) + }, + onOpen = { open(d) }, + sourcePackage = d.packageName, + ), + ) + } + + private fun open(d: NavigationNotificationData) { + if (!sendPendingIntent(context, d.contentIntent)) launchPackage(context, d.packageName) + } + + companion object { + const val ITEM_KEY = "navigation" + + // ponytail: fixed timings; make them settings if people ask + private const val EXPANDED_MS = 6_000L + private const val COLLAPSE_ANIM_MS = 600L + private const val REARM_FACTOR = 1.5 + } +} + +@Composable +internal fun Maneuver(d: NavigationNotificationData, size: Int) { + if (d.maneuver != null) { + IslandBitmap(d.maneuver, size.dp, fallbackRes = R.drawable.round_navigation_24) + } else { + IslandIcon(R.drawable.round_navigation_24, size = size.dp, tint = MaterialTheme.colorScheme.primary) + } +} 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 6885c88ea..3f984312d 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 @@ -42,6 +42,7 @@ import com.sameerasw.essentials.island.model.IslandStage import com.sameerasw.essentials.island.plugins.calendar.CalendarPlugin import com.sameerasw.essentials.island.plugins.consciousgate.ConsciousGatePlugin import com.sameerasw.essentials.island.plugins.flashlight.FlashlightPlugin +import com.sameerasw.essentials.island.plugins.navigation.NavigationPlugin import com.sameerasw.essentials.island.plugins.network.NetworkPlugin import com.sameerasw.essentials.island.plugins.signal.SignalPlugin import com.sameerasw.essentials.island.plugins.devices.DevicesPlugin @@ -139,6 +140,7 @@ class IslandCoordinator( RemoteProvidersPlugin(), CaffeinatePlugin(), TravelPlugin(), + NavigationPlugin(), NetworkPlugin(), SignalPlugin(), DevicesPlugin(), 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 02528f2b1..b7ec5962c 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/NotificationListener.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/NotificationListener.kt @@ -47,6 +47,8 @@ import com.sameerasw.essentials.domain.model.DashConfig import com.sameerasw.essentials.domain.model.NotificationActionItem import com.sameerasw.essentials.domain.model.NotificationLightingColorMode import com.sameerasw.essentials.domain.model.NotificationLightingSide +import com.sameerasw.essentials.domain.model.NavigationNotificationData +import com.sameerasw.essentials.domain.model.NavigationParser import com.sameerasw.essentials.domain.model.ProgressNotificationData import com.sameerasw.essentials.domain.model.RippleConfig import com.sameerasw.essentials.services.receivers.FlashlightActionReceiver @@ -68,6 +70,10 @@ class NotificationListener : NotificationListenerService() { fun onProgressNotificationUpdated(data: ProgressNotificationData?) } + interface NavigationNotificationListener { + fun onNavigationNotificationUpdated(data: NavigationNotificationData?) + } + interface NotificationAlertListener { fun onNotificationAlertPosted(alert: ActiveNotificationAlert) fun onNotificationAlertRemoved(key: String) @@ -75,6 +81,7 @@ class NotificationListener : NotificationListenerService() { companion object { private const val PROGRESS_REFRESH_DEBOUNCE_MS = 250L + private const val EXTRA_SHORT_CRITICAL_TEXT = "android.shortCriticalText" const val ACTION_LIKE_CURRENT_SONG = "com.sameerasw.essentials.ACTION_LIKE_CURRENT_SONG" const val ACTION_REQUEST_AMBIENT_GLANCE = @@ -87,6 +94,25 @@ class NotificationListener : NotificationListenerService() { private val progressListeners = mutableListOf() private val alertListeners = mutableListOf() + private val navigationListeners = mutableListOf() + + fun addNavigationNotificationListener(listener: NavigationNotificationListener) { + synchronized(navigationListeners) { + if (!navigationListeners.contains(listener)) navigationListeners.add(listener) + } + } + + fun removeNavigationNotificationListener(listener: NavigationNotificationListener) { + synchronized(navigationListeners) { navigationListeners.remove(listener) } + } + + fun notifyNavigationListeners(data: NavigationNotificationData?) { + val listenersCopy = synchronized(navigationListeners) { navigationListeners.toList() } + listenersCopy.forEach { it.onNavigationNotificationUpdated(data) } + } + + fun getLatestNavigationNotification(): NavigationNotificationData? = + instance?.extractLatestNavigationNotification() fun addNotificationAlertListener(listener: NotificationAlertListener) { synchronized(alertListeners) { @@ -1021,6 +1047,9 @@ class NotificationListener : NotificationListenerService() { val extras = sbn.notification.extras if (extras != null && (extras.getInt(Notification.EXTRA_PROGRESS_MAX, 0) > 0 || extras.containsKey(Notification.EXTRA_PROGRESS_INDETERMINATE))) { scheduleProgressRefresh() + } else if (sbn.isOngoing && isNavigationNotification(sbn)) { + // Navigation shares the progress refresh pass + scheduleProgressRefresh() } else if (HilightProgressTracker.isTracking(sbn.key)) { // Its progress bar was replaced, e.g. by "Download complete" scheduleProgressRefresh() @@ -1876,7 +1905,17 @@ class NotificationListener : NotificationListenerService() { return@execute } HilightProgressTracker.onProgress(applicationContext, data) - progressHandler.post { notifyProgressListeners(data) } + val navigation = + try { + extractLatestNavigationNotification() + } catch (e: Exception) { + Log.e("NotificationListener", "Failed to extract navigation notification", e) + null + } + progressHandler.post { + notifyProgressListeners(data) + notifyNavigationListeners(navigation) + } } } @@ -1907,11 +1946,56 @@ class NotificationListener : NotificationListenerService() { val active = (safeActiveNotifications() ?: return null).toList() + LiveUpdateSnoozer.snoozedNotifications() val progressNotifs = active.mapNotNull { sbn -> if (sbn.packageName == packageName || isMediaNotification(sbn)) return@mapNotNull null + if (islandShowsNavigation && sbn.isOngoing && isNavigationNotification(sbn)) return@mapNotNull null extractProgressNotification(sbn) } return progressNotifs.maxByOrNull { it.postTime } } + // The island's navigation item replaces the progress ring for these + private val islandShowsNavigation: Boolean + get() = getSharedPreferences(SettingsRepository.PREFS_NAME, MODE_PRIVATE).let { + it.getBoolean(SettingsRepository.KEY_ISLAND_ENABLED, false) && + it.getBoolean(SettingsRepository.KEY_ISLAND_SHOW_NAVIGATION, true) + } + + fun extractLatestNavigationNotification(): NavigationNotificationData? { + val active = (safeActiveNotifications() ?: return null).toList() + LiveUpdateSnoozer.snoozedNotifications() + return active + .filter { it.packageName != packageName && it.isOngoing && isNavigationNotification(it) } + .mapNotNull { extractNavigationNotification(it) } + .maxByOrNull { it.postTime } + } + + private fun extractNavigationNotification(sbn: StatusBarNotification): NavigationNotificationData? { + val notif = sbn.notification ?: return null + val extras = notif.extras ?: return null + val parsed = NavigationParser.parse( + title = extras.getCharSequence(Notification.EXTRA_TITLE)?.toString(), + text = extras.getCharSequence(Notification.EXTRA_TEXT)?.toString(), + subText = extras.getCharSequence(Notification.EXTRA_SUB_TEXT)?.toString(), + chip = extras.getCharSequence(EXTRA_SHORT_CRITICAL_TEXT)?.toString(), + ) + val maneuver = try { + notif.getLargeIcon()?.loadDrawable(this)?.let { AppUtil.drawableToBitmap(it) } + } catch (_: Exception) { + null + } + val actions = notif.actions.orEmpty() + val exit = NavigationParser.exitActionIndex(actions.map { it.title?.toString() }) + return NavigationNotificationData( + key = sbn.key, + packageName = sbn.packageName, + instruction = parsed.instruction, + distance = parsed.distance, + eta = parsed.eta, + maneuver = maneuver, + contentIntent = notif.contentIntent, + exitIntent = actions.getOrNull(exit)?.actionIntent, + postTime = if (sbn.postTime > 0) sbn.postTime else notif.`when`, + ) + } + fun isHeadsUpNotification( sbn: StatusBarNotification, rankingMap: RankingMap? = null, diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSettingsUI.kt index 0444787cf..81ba98c9a 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSettingsUI.kt @@ -81,6 +81,8 @@ import com.sameerasw.essentials.ui.features.display.sheets.IslandBorderOutlineOp import com.sameerasw.essentials.ui.features.display.sheets.IslandBriefOptionsBottomSheet import com.sameerasw.essentials.ui.features.display.sheets.IslandCallOptionsBottomSheet import com.sameerasw.essentials.ui.features.display.sheets.IslandDevicesBatteryBottomSheet +import com.sameerasw.essentials.ui.features.display.sheets.IslandNavigationOptionsBottomSheet +import com.sameerasw.essentials.ui.features.display.sheets.IslandNotificationOptionsBottomSheet import com.sameerasw.essentials.ui.features.display.sheets.IslandPulseShadowOptionsBottomSheet import com.sameerasw.essentials.ui.features.display.sheets.IslandSoundModeOptionsBottomSheet import com.sameerasw.essentials.ui.features.display.sheets.IslandTimeOptionsBottomSheet @@ -123,6 +125,7 @@ fun IslandSettingsUI( var showCallOptionsSheet by remember { mutableStateOf(false) } var showSignalOptionsSheet by remember { mutableStateOf(false) } var showAlarmOptionsSheet by remember { mutableStateOf(false) } + var showNavigationOptionsSheet by remember { mutableStateOf(false) } var showBriefOptionsSheet by remember { mutableStateOf(highlightSetting == "island_brief_show_alarm") } var pickingGesture by remember { mutableStateOf(null) } var showSlideModeSheet by remember { mutableStateOf(false) } @@ -270,14 +273,7 @@ fun IslandSettingsUI( HapticUtil.performVirtualKeyHaptic(view) viewModel.setIslandShowNotifications(checked) }, - onSettingsClick = { - HapticUtil.performVirtualKeyHaptic(view) - context.startActivity( - Intent(context, FeatureSettingsActivity::class.java).apply { - putExtra("feature", ISLAND_NOTIFICATIONS_FEATURE_ID) - }, - ) - }, + onSettingsClick = { showNotificationOptionsSheet = true }, modifier = Modifier.highlight(highlightSetting == "island_show_notifications"), ) "time" -> IconToggleItem( @@ -430,6 +426,19 @@ fun IslandSettingsUI( onSettingsClick = { showAlarmOptionsSheet = true }, modifier = Modifier.highlight(highlightSetting == "island_show_alarm"), ) + "navigation" -> IconToggleItem( + dragHandle = handle, + iconRes = R.drawable.round_navigation_24, + title = stringResource(R.string.island_show_navigation_title), + description = stringResource(R.string.island_navigation_desc), + isChecked = viewModel.isIslandShowNavigation.value, + onCheckedChange = { checked -> + HapticUtil.performVirtualKeyHaptic(view) + viewModel.setIslandShowNavigation(checked) + }, + onSettingsClick = { showNavigationOptionsSheet = true }, + modifier = Modifier.highlight(highlightSetting == "island_show_navigation"), + ) "devices" -> IconToggleItem( dragHandle = handle, iconRes = R.drawable.rounded_bluetooth_24, @@ -760,6 +769,14 @@ fun IslandSettingsUI( ) } + if (showNotificationOptionsSheet) { + IslandNotificationOptionsBottomSheet( + viewModel = viewModel, + onDismissRequest = { showNotificationOptionsSheet = false }, + highlightSetting = highlightSetting, + ) + } + if (showBriefOptionsSheet) { IslandBriefOptionsBottomSheet( viewModel = viewModel, @@ -775,6 +792,10 @@ fun IslandSettingsUI( ) } + if (showNavigationOptionsSheet) { + IslandNavigationOptionsBottomSheet(onDismissRequest = { showNavigationOptionsSheet = false }) + } + if (showAlarmOptionsSheet) { IslandAlarmOptionsBottomSheet( viewModel = viewModel, diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandNavigationOptionsBottomSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandNavigationOptionsBottomSheet.kt new file mode 100644 index 000000000..432ed257d --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandNavigationOptionsBottomSheet.kt @@ -0,0 +1,132 @@ +package com.sameerasw.essentials.ui.features.display.sheets + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.sameerasw.essentials.R +import com.sameerasw.essentials.data.repository.SettingsRepository +import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenuItem +import com.sameerasw.essentials.ui.core.cards.ConfigPickerItem +import com.sameerasw.essentials.ui.core.cards.IconToggleItem +import com.sameerasw.essentials.ui.core.containers.RoundedCardContainer +import com.sameerasw.essentials.ui.core.pickers.SegmentedPicker +import com.sameerasw.essentials.ui.core.sheets.EssentialsBottomSheet +import com.sameerasw.essentials.utils.HapticUtil + +private val EXPAND_DISTANCE_OPTIONS = listOf(0, 50, 100, 200, 500) + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun IslandNavigationOptionsBottomSheet(onDismissRequest: () -> Unit) { + val context = LocalContext.current + val view = LocalView.current + val settings = remember { SettingsRepository(context) } + var onNew by remember { mutableStateOf(settings.getIslandNavigationOnNew()) } + var expandDistance by remember { mutableStateOf(settings.getIslandNavigationExpandDistance()) } + var peekAfterExpand by remember { mutableStateOf(settings.isIslandNavigationPeekAfterExpandEnabled()) } + + @Composable + fun distanceLabel(meters: Int) = + if (meters == 0) stringResource(R.string.island_navigation_expand_off) else stringResource(R.string.island_navigation_expand_meters, meters) + + EssentialsBottomSheet(onDismissRequest = onDismissRequest) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 24.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text( + text = stringResource(R.string.island_navigation_options_title), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(start = 8.dp, bottom = 4.dp), + ) + + RoundedCardContainer(spacing = 2.dp, cornerRadius = 24.dp) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surfaceBright, MaterialTheme.shapes.extraSmall) + .padding(top = 12.dp), + ) { + Text( + text = stringResource(R.string.island_navigation_on_new_title), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(horizontal = 16.dp), + ) + Text( + text = stringResource(R.string.island_navigation_on_new_desc), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 16.dp), + ) + val labels = mapOf( + SettingsRepository.ISLAND_NAVIGATION_ON_NEW_EXPAND to stringResource(R.string.island_navigation_on_new_expand), + SettingsRepository.ISLAND_NAVIGATION_ON_NEW_PEEK to stringResource(R.string.island_navigation_on_new_peek), + SettingsRepository.ISLAND_NAVIGATION_ON_NEW_OFF to stringResource(R.string.island_navigation_expand_off), + ) + SegmentedPicker( + items = labels.keys.toList(), + selectedItem = onNew, + onItemSelected = { + onNew = it + settings.setIslandNavigationOnNew(it) + }, + labelProvider = { labels[it].orEmpty() }, + title = R.string.island_navigation_on_new_title, + modifier = Modifier.fillMaxWidth(), + ) + } + + ConfigPickerItem( + title = stringResource(R.string.island_navigation_expand_title), + description = stringResource(R.string.island_navigation_expand_desc), + selectedValue = distanceLabel(expandDistance), + iconRes = R.drawable.rounded_magnify_fullscreen_24, + modifier = Modifier.fillMaxWidth(), + ) { + EXPAND_DISTANCE_OPTIONS.forEach { meters -> + SegmentedDropdownMenuItem( + text = { Text(distanceLabel(meters)) }, + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + expandDistance = meters + settings.setIslandNavigationExpandDistance(meters) + }, + ) + } + } + + IconToggleItem( + iconRes = R.drawable.round_navigation_24, + title = stringResource(R.string.island_navigation_peek_after_expand_title), + description = stringResource(R.string.island_navigation_peek_after_expand_desc), + isChecked = peekAfterExpand, + onCheckedChange = { + HapticUtil.performVirtualKeyHaptic(view) + peekAfterExpand = it + settings.setIslandNavigationPeekAfterExpandEnabled(it) + }, + ) + } + } + } +} 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 99f679edd..1f8ee9b1f 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt @@ -241,6 +241,7 @@ class MainViewModel : ViewModel() { val islandAlarmWindowHours = mutableIntStateOf(12) val isIslandShowTravel = mutableStateOf(true) val isIslandShowCaffeinate = mutableStateOf(true) + val isIslandShowNavigation = mutableStateOf(true) val isIslandShowDevices = mutableStateOf(true) val isIslandBriefEnabled = mutableStateOf(false) val isIslandBriefShowAlarm = mutableStateOf(true) @@ -2406,6 +2407,7 @@ class MainViewModel : ViewModel() { isDuoKeepInFullscreen.value = settingsRepository.isDuoKeepInFullscreenEnabled() isDuoDisableInteractionsInFullscreen.value = settingsRepository.isDuoDisableInteractionsInFullscreenEnabled() isIslandShowCaffeinate.value = settingsRepository.isIslandShowCaffeinateEnabled() + isIslandShowNavigation.value = settingsRepository.isIslandShowNavigationEnabled() isIslandShowDevices.value = settingsRepository.isIslandShowDevicesEnabled() isIslandBriefEnabled.value = settingsRepository.isIslandBriefEnabled() isIslandBriefShowAlarm.value = settingsRepository.isIslandBriefShowAlarmEnabled() @@ -5492,6 +5494,11 @@ class MainViewModel : ViewModel() { settingsRepository.setIslandShowDevicesEnabled(enabled) } + fun setIslandShowNavigation(enabled: Boolean) { + isIslandShowNavigation.value = enabled + settingsRepository.setIslandShowNavigationEnabled(enabled) + } + fun setIslandShowCaffeinate(enabled: Boolean) { isIslandShowCaffeinate.value = enabled settingsRepository.setIslandShowCaffeinateEnabled(enabled) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 42b5f4366..c278a4315 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3157,6 +3157,20 @@ Combine with Island Combine with Duo Devices + Navigation + Next turn and distance from Maps and other navigation apps + Navigation + Exit + New instruction + What the island does when the next direction appears + Expand + Line peek + Expand before a turn + Expands with the direction when you get this close to the turn + Off + %d m + Line peek after expanding + When off, the island goes straight back to compact Connected Disconnected Bluetooth device diff --git a/app/src/test/java/com/sameerasw/essentials/domain/NavigationParserTest.kt b/app/src/test/java/com/sameerasw/essentials/domain/NavigationParserTest.kt new file mode 100644 index 000000000..cfeb434da --- /dev/null +++ b/app/src/test/java/com/sameerasw/essentials/domain/NavigationParserTest.kt @@ -0,0 +1,67 @@ +package com.sameerasw.essentials.domain + +import com.sameerasw.essentials.domain.model.NavigationParser +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class NavigationParserTest { + @Test + fun distanceInTitle() { + val p = NavigationParser.parse("300 m", "Turn right onto Main St", "12 min · 4.5 km", null) + assertEquals("300 m", p.distance) + assertEquals("Turn right onto Main St", p.instruction) + assertEquals("12 min · 4.5 km", p.eta) + } + + @Test + fun distanceInText() { + val p = NavigationParser.parse("Gira a la derecha hacia Av. Juárez", "1,2 km", null, null) + assertEquals("1,2 km", p.distance) + assertEquals("Gira a la derecha hacia Av. Juárez", p.instruction) + assertNull(p.eta) + } + + @Test + fun chipWinsForDistance() { + val p = NavigationParser.parse("Turn left", "Toward downtown", "3:45 PM", "0.2 mi") + assertEquals("0.2 mi", p.distance) + assertEquals("Turn left", p.instruction) + assertEquals("3:45 PM", p.eta) + } + + @Test + fun noDistanceKeepsTextAsDetail() { + val p = NavigationParser.parse("Head north", "Toward Main St", null, null) + assertNull(p.distance) + assertEquals("Head north", p.instruction) + assertEquals("Toward Main St", p.eta) + } + + @Test + fun longTextIsNotADistance() { + val p = NavigationParser.parse("Continue on Highway 15D for 25 km", null, null, null) + assertNull(p.distance) + } + + @Test + fun exitAction() { + assertEquals(1, NavigationParser.exitActionIndex(listOf("Mute", "Exit navigation"))) + assertEquals(0, NavigationParser.exitActionIndex(listOf("Salir"))) + assertEquals(0, NavigationParser.exitActionIndex(listOf("Something"))) + assertEquals(-1, NavigationParser.exitActionIndex(listOf("Mute", "Send"))) + assertEquals(-1, NavigationParser.exitActionIndex(emptyList())) + } + + @Test + fun meters() { + assertEquals(240 * 0.9144, NavigationParser.meters("240 yd")!!, 0.01) + assertEquals(1200.0, NavigationParser.meters("1,2 km")!!, 0.01) + assertEquals(1200 * 0.3048, NavigationParser.meters("1,200 ft")!!, 0.01) + assertEquals(0.2 * 1609.344, NavigationParser.meters("0.2 mi")!!, 0.01) + assertEquals(300.0, NavigationParser.meters("300 m")!!, 0.01) + assertEquals(50.0, NavigationParser.meters("In 50m")!!, 0.01) + assertNull(NavigationParser.meters("Western Road")) + assertNull(NavigationParser.meters(null)) + } +} From 938dc1b95a40c1c922a75ec5a00d7e64977ffc74 Mon Sep 17 00:00:00 2001 From: Isaac Serrano Date: Tue, 13 Oct 2026 15:38:10 -0700 Subject: [PATCH 2/4] feat(island): add navigation plugin and settings to island --- .../plugins/navigation/NavigationExpanded.kt | 27 ++++--- .../plugins/navigation/NavigationPlugin.kt | 78 +++++++++++++++---- .../ui/features/display/IslandSettingsUI.kt | 20 ++--- 3 files changed, 83 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/com/sameerasw/essentials/island/plugins/navigation/NavigationExpanded.kt b/app/src/main/java/com/sameerasw/essentials/island/plugins/navigation/NavigationExpanded.kt index d502e6aea..1d9eefe7c 100644 --- a/app/src/main/java/com/sameerasw/essentials/island/plugins/navigation/NavigationExpanded.kt +++ b/app/src/main/java/com/sameerasw/essentials/island/plugins/navigation/NavigationExpanded.kt @@ -1,5 +1,6 @@ package com.sameerasw.essentials.island.plugins.navigation +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -12,17 +13,19 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.sameerasw.essentials.R import com.sameerasw.essentials.domain.model.NavigationNotificationData import com.sameerasw.essentials.island.model.IslandExpandedScope +import com.sameerasw.essentials.island.ui.IslandLayoutSpec import com.sameerasw.essentials.island.ui.IslandTextStyles import com.sameerasw.essentials.island.ui.components.CameraRow import com.sameerasw.essentials.island.ui.components.ConnectedButtonRow import com.sameerasw.essentials.island.ui.components.ConnectedItem import com.sameerasw.essentials.island.ui.components.ConnectedTextLabel -import com.sameerasw.essentials.island.ui.components.MarqueeText +import com.sameerasw.essentials.ui.theme.EssentialsTheme @Composable fun NavigationExpanded( @@ -39,22 +42,18 @@ fun NavigationExpanded( Spacer(Modifier.height(spec.expandedTopPadding)) scope.CameraRow( horizontalPadding = spec.cameraGap + spec.expandedCorner * 0.35f, - start = { - Maneuver(data, spec.cellSize.value.toInt()) - MarqueeText( - text = data.distance ?: data.instruction.orEmpty(), - style = IslandTextStyles.title.copy(color = accent), - modifier = Modifier.weight(1f), - ) + start = { Maneuver(data, spec.cellSize.value.toInt()) }, + end = { + data.distance?.let { + Text(it, style = IslandTextStyles.title.copy(color = accent), maxLines = 1) + } }, - end = {}, ) - val instruction = data.instruction.takeIf { data.distance != null } - if (!instruction.isNullOrBlank()) { - Spacer(Modifier.height(4.dp)) + if (!data.instruction.isNullOrBlank()) { + Spacer(Modifier.height(8.dp)) Text( - text = instruction, - style = IslandTextStyles.body.copy(color = Color.White, fontSize = 17.sp, lineHeight = 22.sp), + text = data.instruction, + style = IslandTextStyles.title.copy(color = Color.White, fontSize = 20.sp, lineHeight = 25.sp), maxLines = 2, overflow = TextOverflow.Ellipsis, modifier = Modifier.fillMaxWidth().padding(horizontal = sidePadding), diff --git a/app/src/main/java/com/sameerasw/essentials/island/plugins/navigation/NavigationPlugin.kt b/app/src/main/java/com/sameerasw/essentials/island/plugins/navigation/NavigationPlugin.kt index b5400353d..c577b7099 100644 --- a/app/src/main/java/com/sameerasw/essentials/island/plugins/navigation/NavigationPlugin.kt +++ b/app/src/main/java/com/sameerasw/essentials/island/plugins/navigation/NavigationPlugin.kt @@ -36,12 +36,19 @@ class NavigationPlugin : BaseIslandPlugin() { private var data: NavigationNotificationData? = null - // Instruction the island already expanded for, so each turn expands once - private var expandedFor: String? = null + private val expandedFor = HashSet() + + private val surfaced = LinkedHashSet() + private var lastSurfacedAt = 0L + + private val endRoute = Runnable { + expandedFor.clear() + surfaced.clear() + lastSurfacedAt = 0L + } private val endExpand = Runnable { val c = ctx ?: return@Runnable - // The user closed it or switched to something else if (c.focusedKey() != ITEM_KEY || c.currentStage() != IslandStage.Expanded) return@Runnable c.request(PluginRequest.Collapse(ITEM_KEY)) if (settings.isIslandNavigationPeekAfterExpandEnabled() && settings.isIslandLineStageEnabled()) { @@ -67,8 +74,9 @@ class NavigationPlugin : BaseIslandPlugin() { NotificationListener.removeNavigationNotificationListener(listener) ctx?.mainHandler?.removeCallbacks(endExpand) ctx?.mainHandler?.removeCallbacks(peekAfterCollapse) + ctx?.mainHandler?.removeCallbacks(endRoute) data = null - expandedFor = null + endRoute.run() } override fun refresh() { @@ -83,32 +91,57 @@ class NavigationPlugin : BaseIslandPlugin() { data = next render() if (next == null) { - expandedFor = null c.mainHandler.removeCallbacks(endExpand) c.mainHandler.removeCallbacks(peekAfterCollapse) + c.mainHandler.removeCallbacks(endRoute) + c.mainHandler.postDelayed(endRoute, ROUTE_GRACE_MS) return } + c.mainHandler.removeCallbacks(endRoute) if (!enabled()) return val instruction = next.instruction.orEmpty() val threshold = settings.getIslandNavigationExpandDistance() val meters = NavigationParser.meters(next.distance) // Distance jumped back up: a new maneuver with the same text ("Turn right" twice in a row) - if (meters != null && meters > threshold * REARM_FACTOR) expandedFor = null - val approaching = threshold > 0 && meters != null && meters <= threshold && instruction != expandedFor + if (meters != null && meters > threshold * REARM_FACTOR) { + expandedFor.remove(instruction) + surfaced.remove(instruction) + } + val approaching = + threshold > 0 && meters != null && meters <= threshold && instruction !in expandedFor + val expandSoon = threshold > 0 && meters != null && meters <= threshold * REARM_FACTOR + val now = System.currentTimeMillis() + val announce = + turned && instruction !in surfaced && !expandSoon && now - lastSurfacedAt >= COOLDOWN_MS + when { - approaching && expand(c) -> expandedFor = instruction - !turned -> {} - settings.getIslandNavigationOnNew() == SettingsRepository.ISLAND_NAVIGATION_ON_NEW_EXPAND -> expand(c) - settings.getIslandNavigationOnNew() == SettingsRepository.ISLAND_NAVIGATION_ON_NEW_PEEK && settings.isIslandLineStageEnabled() -> + approaching && expand(c) -> { + expandedFor += instruction + markSurfaced(instruction, now) + } + + !announce -> {} + settings.getIslandNavigationOnNew() == SettingsRepository.ISLAND_NAVIGATION_ON_NEW_EXPAND -> { + if (expand(c)) markSurfaced(instruction, now) + } + + settings.getIslandNavigationOnNew() == SettingsRepository.ISLAND_NAVIGATION_ON_NEW_PEEK && settings.isIslandLineStageEnabled() -> { c.request(PluginRequest.Peek(ITEM_KEY, settings.getIslandPeekDurationMs())) + markSurfaced(instruction, now) + } } } - // Expands for a while, then collapses to a line peek or compact + private fun markSurfaced(instruction: String, now: Long) { + surfaced += instruction + if (surfaced.size > MAX_SURFACED) surfaced.remove(surfaced.first()) + lastSurfacedAt = now + } + private fun expand(c: IslandPluginContext): Boolean { - // Don't take over something the user is looking at; retried on the next distance update val focused = c.focusedKey() if (focused != null && focused != ITEM_KEY) return false + if (focused == ITEM_KEY && c.currentStage() == IslandStage.Expanded) return true c.mainHandler.removeCallbacks(peekAfterCollapse) c.request(PluginRequest.Expand(ITEM_KEY)) c.mainHandler.removeCallbacks(endExpand) @@ -143,7 +176,14 @@ class NavigationPlugin : BaseIslandPlugin() { data = d, accent = MaterialTheme.colorScheme.primary, onOpen = { open(d) }, - onExit = d.exitIntent?.let { intent -> { sendPendingIntent(context, intent) } }, + onExit = d.exitIntent?.let { intent -> + { + sendPendingIntent( + context, + intent + ) + } + }, scope = scope, ) }, @@ -160,10 +200,12 @@ class NavigationPlugin : BaseIslandPlugin() { companion object { const val ITEM_KEY = "navigation" - // ponytail: fixed timings; make them settings if people ask private const val EXPANDED_MS = 6_000L private const val COLLAPSE_ANIM_MS = 600L private const val REARM_FACTOR = 1.5 + private const val COOLDOWN_MS = 30_000L + private const val ROUTE_GRACE_MS = 60_000L + private const val MAX_SURFACED = 50 } } @@ -172,6 +214,10 @@ internal fun Maneuver(d: NavigationNotificationData, size: Int) { if (d.maneuver != null) { IslandBitmap(d.maneuver, size.dp, fallbackRes = R.drawable.round_navigation_24) } else { - IslandIcon(R.drawable.round_navigation_24, size = size.dp, tint = MaterialTheme.colorScheme.primary) + IslandIcon( + R.drawable.round_navigation_24, + size = size.dp, + tint = MaterialTheme.colorScheme.primary + ) } } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSettingsUI.kt index b4acc256c..b4cf28bdb 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSettingsUI.kt @@ -74,14 +74,11 @@ import com.sameerasw.essentials.ui.features.display.actions.HorizontalSlideModeS import com.sameerasw.essentials.ui.features.display.actions.horizontalSlideDescription import com.sameerasw.essentials.ui.features.display.sheets.IslandAlarmOptionsBottomSheet import com.sameerasw.essentials.ui.features.display.sheets.IslandBatteryOptionsBottomSheet -import com.sameerasw.essentials.ui.features.display.sheets.IslandBorderOutlineOptionsBottomSheet import com.sameerasw.essentials.ui.features.display.sheets.IslandBriefOptionsBottomSheet import com.sameerasw.essentials.ui.features.display.sheets.IslandAppOptionsBottomSheet import com.sameerasw.essentials.ui.features.display.sheets.IslandCallOptionsBottomSheet import com.sameerasw.essentials.ui.features.display.sheets.IslandDevicesBatteryBottomSheet import com.sameerasw.essentials.ui.features.display.sheets.IslandNavigationOptionsBottomSheet -import com.sameerasw.essentials.ui.features.display.sheets.IslandNotificationOptionsBottomSheet -import com.sameerasw.essentials.ui.features.display.sheets.IslandPulseShadowOptionsBottomSheet import com.sameerasw.essentials.ui.features.display.sheets.IslandSoundModeOptionsBottomSheet import com.sameerasw.essentials.ui.features.display.sheets.IslandTimeOptionsBottomSheet import com.sameerasw.essentials.ui.features.display.sheets.IslandSignalOptionsBottomSheet @@ -271,7 +268,14 @@ fun IslandSettingsUI( HapticUtil.performVirtualKeyHaptic(view) viewModel.setIslandShowNotifications(checked) }, - onSettingsClick = { showNotificationOptionsSheet = true }, + onSettingsClick = { + HapticUtil.performVirtualKeyHaptic(view) + context.startActivity( + Intent(context, FeatureSettingsActivity::class.java).apply { + putExtra("feature", ISLAND_NOTIFICATIONS_FEATURE_ID) + }, + ) + }, modifier = Modifier.highlight(highlightSetting == "island_show_notifications"), ) "time" -> IconToggleItem( @@ -783,14 +787,6 @@ fun IslandSettingsUI( ) } - if (showNotificationOptionsSheet) { - IslandNotificationOptionsBottomSheet( - viewModel = viewModel, - onDismissRequest = { showNotificationOptionsSheet = false }, - highlightSetting = highlightSetting, - ) - } - if (showBriefOptionsSheet) { IslandBriefOptionsBottomSheet( viewModel = viewModel, From bd4e97d5b3a03f261ee582c8c228a18822450116 Mon Sep 17 00:00:00 2001 From: Isaac Serrano Date: Thu, 15 Oct 2026 11:48:36 -0700 Subject: [PATCH 3/4] feat(island): add navigation plugin and settings to island --- .../data/repository/SettingsRepository.kt | 6 +- .../model/NavigationNotificationData.kt | 26 ++-- .../plugins/navigation/NavigationPlugin.kt | 127 ++++++++++-------- .../services/NotificationListener.kt | 41 +++--- .../ui/features/display/IslandSettingsUI.kt | 7 +- .../IslandNavigationOptionsBottomSheet.kt | 33 ++--- .../essentials/viewmodels/MainViewModel.kt | 33 +++++ .../essentials/domain/NavigationParserTest.kt | 16 ++- .../navigation/NavigationTriggerTest.kt | 78 +++++++++++ 9 files changed, 252 insertions(+), 115 deletions(-) create mode 100644 app/src/test/java/com/sameerasw/essentials/island/plugins/navigation/NavigationTriggerTest.kt 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 90d2dbed7..c6cca601b 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 @@ -4073,15 +4073,13 @@ class SettingsRepository( fun isIslandShowNavigationEnabled(): Boolean = getBoolean(KEY_ISLAND_SHOW_NAVIGATION, true) fun setIslandShowNavigationEnabled(enabled: Boolean) = putBoolean(KEY_ISLAND_SHOW_NAVIGATION, enabled) - // What the island does when a new instruction arrives: expand, peek or off fun getIslandNavigationOnNew(): String = - getString(KEY_ISLAND_NAVIGATION_ON_NEW, ISLAND_NAVIGATION_ON_NEW_PEEK) ?: ISLAND_NAVIGATION_ON_NEW_PEEK + getString(KEY_ISLAND_NAVIGATION_ON_NEW, ISLAND_NAVIGATION_ON_NEW_EXPAND) ?: ISLAND_NAVIGATION_ON_NEW_EXPAND fun setIslandNavigationOnNew(mode: String) = putString(KEY_ISLAND_NAVIGATION_ON_NEW, mode) - fun isIslandNavigationPeekAfterExpandEnabled(): Boolean = getBoolean(KEY_ISLAND_NAVIGATION_PEEK_AFTER_EXPAND, true) + fun isIslandNavigationPeekAfterExpandEnabled(): Boolean = getBoolean(KEY_ISLAND_NAVIGATION_PEEK_AFTER_EXPAND, false) fun setIslandNavigationPeekAfterExpandEnabled(enabled: Boolean) = putBoolean(KEY_ISLAND_NAVIGATION_PEEK_AFTER_EXPAND, enabled) - // Meters before a turn at which the island expands; 0 = off fun getIslandNavigationExpandDistance(): Int = getInt(KEY_ISLAND_NAVIGATION_EXPAND_DISTANCE, 200) fun setIslandNavigationExpandDistance(meters: Int) = putInt(KEY_ISLAND_NAVIGATION_EXPAND_DISTANCE, meters) diff --git a/app/src/main/java/com/sameerasw/essentials/domain/model/NavigationNotificationData.kt b/app/src/main/java/com/sameerasw/essentials/domain/model/NavigationNotificationData.kt index 1916b2592..fa9dd97da 100644 --- a/app/src/main/java/com/sameerasw/essentials/domain/model/NavigationNotificationData.kt +++ b/app/src/main/java/com/sameerasw/essentials/domain/model/NavigationNotificationData.kt @@ -9,7 +9,6 @@ data class NavigationNotificationData( val instruction: String?, val distance: String?, val eta: String?, - // Turn arrow, usually the notification's large icon. val maneuver: Bitmap?, val contentIntent: PendingIntent?, val exitIntent: PendingIntent?, @@ -19,7 +18,13 @@ data class NavigationNotificationData( object NavigationParser { data class Parsed(val instruction: String?, val distance: String?, val eta: String?) - private val distanceRegex = Regex("""\d[\d.,]*\s?(km|m|mi|ft|yd)\b""", RegexOption.IGNORE_CASE) + private val unitMeters = mapOf( + "km" to 1000.0, "m" to 1.0, "mi" to 1609.344, "ft" to 0.3048, "yd" to 0.9144, + "км" to 1000.0, "м" to 1.0, + "公里" to 1000.0, "千米" to 1000.0, "公尺" to 1.0, "米" to 1.0, + ) + private val units = unitMeters.keys.sortedByDescending { it.length }.joinToString("|") + private val distanceRegex = Regex("""\d[\d.,]*\s?($units)(?!\p{L})""", RegexOption.IGNORE_CASE) private val exitRegex = Regex( """\b(exit|stop|end|close|quit|salir|terminar|detener|finalizar|beenden|quitter|arrêter|sair|encerrar)\b""", RegexOption.IGNORE_CASE, @@ -27,8 +32,6 @@ object NavigationParser { fun isDistance(s: String?) = s != null && s.length <= 24 && distanceRegex.containsMatchIn(s) - // Apps put the distance in either the title or the text, so take whichever looks like one. - // `chip` is the Live Update status-bar text, which navigation apps fill with the distance. fun parse(title: String?, text: String?, subText: String?, chip: String?): Parsed { val titleIsDistance = isDistance(title) val textIsDistance = !titleIsDistance && isDistance(text) @@ -50,10 +53,8 @@ object NavigationParser { ) } - private val meterRegex = Regex("""(\d{1,3}(?:,\d{3})+|\d+(?:[.,]\d+)?)\s?(km|m|mi|ft|yd)\b""", RegexOption.IGNORE_CASE) - private val unitMeters = mapOf("km" to 1000.0, "m" to 1.0, "mi" to 1609.344, "ft" to 0.3048, "yd" to 0.9144) + private val meterRegex = Regex("""(\d{1,3}(?:,\d{3})+|\d+(?:[.,]\d+)?)\s?($units)(?!\p{L})""", RegexOption.IGNORE_CASE) - // "1,200 ft" uses a thousands separator; "1,2 km" uses a decimal comma. fun meters(distance: String?): Double? { val match = meterRegex.find(distance ?: return null) ?: return null val (number, unit) = match.destructured @@ -61,13 +62,6 @@ object NavigationParser { return value.toDoubleOrNull()?.times(unitMeters.getValue(unit.lowercase())) } - // ponytail: matches the button label; an app with several unlabeled-looking actions gets no Exit button - fun exitActionIndex(titles: List): Int { - val match = titles.indexOfFirst { it != null && exitRegex.containsMatchIn(it) } - return when { - match >= 0 -> match - titles.size == 1 -> 0 - else -> -1 - } - } + fun exitActionIndex(titles: List): Int = + titles.indexOfFirst { it != null && exitRegex.containsMatchIn(it) } } diff --git a/app/src/main/java/com/sameerasw/essentials/island/plugins/navigation/NavigationPlugin.kt b/app/src/main/java/com/sameerasw/essentials/island/plugins/navigation/NavigationPlugin.kt index c577b7099..8ecb378b6 100644 --- a/app/src/main/java/com/sameerasw/essentials/island/plugins/navigation/NavigationPlugin.kt +++ b/app/src/main/java/com/sameerasw/essentials/island/plugins/navigation/NavigationPlugin.kt @@ -36,16 +36,9 @@ class NavigationPlugin : BaseIslandPlugin() { private var data: NavigationNotificationData? = null - private val expandedFor = HashSet() + private val trigger = NavigationTrigger() - private val surfaced = LinkedHashSet() - private var lastSurfacedAt = 0L - - private val endRoute = Runnable { - expandedFor.clear() - surfaced.clear() - lastSurfacedAt = 0L - } + private val endRoute = Runnable { trigger.reset() } private val endExpand = Runnable { val c = ctx ?: return@Runnable @@ -99,54 +92,30 @@ class NavigationPlugin : BaseIslandPlugin() { } c.mainHandler.removeCallbacks(endRoute) if (!enabled()) return - val instruction = next.instruction.orEmpty() - val threshold = settings.getIslandNavigationExpandDistance() - val meters = NavigationParser.meters(next.distance) - // Distance jumped back up: a new maneuver with the same text ("Turn right" twice in a row) - if (meters != null && meters > threshold * REARM_FACTOR) { - expandedFor.remove(instruction) - surfaced.remove(instruction) - } - val approaching = - threshold > 0 && meters != null && meters <= threshold && instruction !in expandedFor - val expandSoon = threshold > 0 && meters != null && meters <= threshold * REARM_FACTOR - val now = System.currentTimeMillis() - val announce = - turned && instruction !in surfaced && !expandSoon && now - lastSurfacedAt >= COOLDOWN_MS - - when { - approaching && expand(c) -> { - expandedFor += instruction - markSurfaced(instruction, now) - } - - !announce -> {} - settings.getIslandNavigationOnNew() == SettingsRepository.ISLAND_NAVIGATION_ON_NEW_EXPAND -> { - if (expand(c)) markSurfaced(instruction, now) - } - - settings.getIslandNavigationOnNew() == SettingsRepository.ISLAND_NAVIGATION_ON_NEW_PEEK && settings.isIslandLineStageEnabled() -> { - c.request(PluginRequest.Peek(ITEM_KEY, settings.getIslandPeekDurationMs())) - markSurfaced(instruction, now) - } + val focused = c.focusedKey() + val action = trigger.decide( + instruction = next.instruction.orEmpty(), + turned = turned, + meters = NavigationParser.meters(next.distance), + threshold = settings.getIslandNavigationExpandDistance(), + onNew = settings.getIslandNavigationOnNew(), + canPeek = settings.isIslandLineStageEnabled(), + canExpand = focused == null || focused == ITEM_KEY, + now = System.currentTimeMillis(), + ) + when (action) { + NavigationTrigger.Action.Expand -> expand(c) + NavigationTrigger.Action.Peek -> c.request(PluginRequest.Peek(ITEM_KEY, settings.getIslandPeekDurationMs())) + NavigationTrigger.Action.None -> {} } } - private fun markSurfaced(instruction: String, now: Long) { - surfaced += instruction - if (surfaced.size > MAX_SURFACED) surfaced.remove(surfaced.first()) - lastSurfacedAt = now - } - - private fun expand(c: IslandPluginContext): Boolean { - val focused = c.focusedKey() - if (focused != null && focused != ITEM_KEY) return false - if (focused == ITEM_KEY && c.currentStage() == IslandStage.Expanded) return true + private fun expand(c: IslandPluginContext) { + if (c.focusedKey() == ITEM_KEY && c.currentStage() == IslandStage.Expanded) return c.mainHandler.removeCallbacks(peekAfterCollapse) c.request(PluginRequest.Expand(ITEM_KEY)) c.mainHandler.removeCallbacks(endExpand) c.mainHandler.postDelayed(endExpand, EXPANDED_MS) - return true } private fun enabled() = settings.isIslandShowNavigationEnabled() @@ -202,10 +171,7 @@ class NavigationPlugin : BaseIslandPlugin() { private const val EXPANDED_MS = 6_000L private const val COLLAPSE_ANIM_MS = 600L - private const val REARM_FACTOR = 1.5 - private const val COOLDOWN_MS = 30_000L private const val ROUTE_GRACE_MS = 60_000L - private const val MAX_SURFACED = 50 } } @@ -221,3 +187,58 @@ internal fun Maneuver(d: NavigationNotificationData, size: Int) { ) } } + +internal class NavigationTrigger { + enum class Action { None, Expand, Peek } + + private val expandedFor = HashSet() + private val surfaced = LinkedHashSet() + private var lastSurfacedAt = 0L + + fun reset() { + expandedFor.clear() + surfaced.clear() + lastSurfacedAt = 0L + } + + fun decide( + instruction: String, + turned: Boolean, + meters: Double?, + threshold: Int, + onNew: String, + canPeek: Boolean, + canExpand: Boolean, + now: Long, + ): Action { + if (meters != null && meters > threshold * REARM_FACTOR) { + expandedFor.remove(instruction) + surfaced.remove(instruction) + } + val approaching = threshold > 0 && meters != null && meters <= threshold && instruction !in expandedFor + val expandSoon = threshold > 0 && meters != null && meters <= threshold * REARM_FACTOR + val announce = turned && instruction !in surfaced && !expandSoon && now - lastSurfacedAt >= COOLDOWN_MS + val action = when { + approaching && canExpand -> { + expandedFor += instruction + Action.Expand + } + !announce -> Action.None + onNew == SettingsRepository.ISLAND_NAVIGATION_ON_NEW_EXPAND && canExpand -> Action.Expand + onNew == SettingsRepository.ISLAND_NAVIGATION_ON_NEW_PEEK && canPeek -> Action.Peek + else -> Action.None + } + if (action != Action.None) { + surfaced += instruction + if (surfaced.size > MAX_SURFACED) surfaced.remove(surfaced.first()) + lastSurfacedAt = now + } + return action + } + + companion object { + const val REARM_FACTOR = 1.5 + const val COOLDOWN_MS = 30_000L + private const val MAX_SURFACED = 50 + } +} 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 b7ec5962c..92c1f4bbd 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/NotificationListener.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/NotificationListener.kt @@ -1048,7 +1048,6 @@ class NotificationListener : NotificationListenerService() { if (extras != null && (extras.getInt(Notification.EXTRA_PROGRESS_MAX, 0) > 0 || extras.containsKey(Notification.EXTRA_PROGRESS_INDETERMINATE))) { scheduleProgressRefresh() } else if (sbn.isOngoing && isNavigationNotification(sbn)) { - // Navigation shares the progress refresh pass scheduleProgressRefresh() } else if (HilightProgressTracker.isTracking(sbn.key)) { // Its progress bar was replaced, e.g. by "Download complete" @@ -1897,6 +1896,14 @@ class NotificationListener : NotificationListenerService() { private val progressRefreshRunnable = Runnable { progressExecutor.execute { + val navigation = + try { + extractLatestNavigationNotification() + } catch (e: Exception) { + Log.e("NotificationListener", "Failed to extract navigation notification", e) + null + } + progressHandler.post { notifyNavigationListeners(navigation) } val data = try { extractLatestProgressNotification() @@ -1905,17 +1912,7 @@ class NotificationListener : NotificationListenerService() { return@execute } HilightProgressTracker.onProgress(applicationContext, data) - val navigation = - try { - extractLatestNavigationNotification() - } catch (e: Exception) { - Log.e("NotificationListener", "Failed to extract navigation notification", e) - null - } - progressHandler.post { - notifyProgressListeners(data) - notifyNavigationListeners(navigation) - } + progressHandler.post { notifyProgressListeners(data) } } } @@ -1952,13 +1949,15 @@ class NotificationListener : NotificationListenerService() { return progressNotifs.maxByOrNull { it.postTime } } - // The island's navigation item replaces the progress ring for these private val islandShowsNavigation: Boolean get() = getSharedPreferences(SettingsRepository.PREFS_NAME, MODE_PRIVATE).let { it.getBoolean(SettingsRepository.KEY_ISLAND_ENABLED, false) && it.getBoolean(SettingsRepository.KEY_ISLAND_SHOW_NAVIGATION, true) } + @Volatile + private var navigationManeuverCache: Triple? = null + fun extractLatestNavigationNotification(): NavigationNotificationData? { val active = (safeActiveNotifications() ?: return null).toList() + LiveUpdateSnoozer.snoozedNotifications() return active @@ -1976,10 +1975,16 @@ class NotificationListener : NotificationListenerService() { subText = extras.getCharSequence(Notification.EXTRA_SUB_TEXT)?.toString(), chip = extras.getCharSequence(EXTRA_SHORT_CRITICAL_TEXT)?.toString(), ) - val maneuver = try { - notif.getLargeIcon()?.loadDrawable(this)?.let { AppUtil.drawableToBitmap(it) } - } catch (_: Exception) { - null + val postTime = if (sbn.postTime > 0) sbn.postTime else notif.`when` + val cached = navigationManeuverCache + val maneuver = if (cached != null && cached.first == sbn.key && cached.second == postTime) { + cached.third + } else { + try { + notif.getLargeIcon()?.loadDrawable(this)?.let { AppUtil.drawableToBitmap(it) } + } catch (_: Exception) { + null + }.also { navigationManeuverCache = Triple(sbn.key, postTime, it) } } val actions = notif.actions.orEmpty() val exit = NavigationParser.exitActionIndex(actions.map { it.title?.toString() }) @@ -1992,7 +1997,7 @@ class NotificationListener : NotificationListenerService() { maneuver = maneuver, contentIntent = notif.contentIntent, exitIntent = actions.getOrNull(exit)?.actionIntent, - postTime = if (sbn.postTime > 0) sbn.postTime else notif.`when`, + postTime = postTime, ) } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSettingsUI.kt index b4cf28bdb..01211468a 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSettingsUI.kt @@ -74,10 +74,12 @@ import com.sameerasw.essentials.ui.features.display.actions.HorizontalSlideModeS import com.sameerasw.essentials.ui.features.display.actions.horizontalSlideDescription import com.sameerasw.essentials.ui.features.display.sheets.IslandAlarmOptionsBottomSheet import com.sameerasw.essentials.ui.features.display.sheets.IslandBatteryOptionsBottomSheet +import com.sameerasw.essentials.ui.features.display.sheets.IslandBorderOutlineOptionsBottomSheet import com.sameerasw.essentials.ui.features.display.sheets.IslandBriefOptionsBottomSheet import com.sameerasw.essentials.ui.features.display.sheets.IslandAppOptionsBottomSheet import com.sameerasw.essentials.ui.features.display.sheets.IslandCallOptionsBottomSheet import com.sameerasw.essentials.ui.features.display.sheets.IslandDevicesBatteryBottomSheet +import com.sameerasw.essentials.ui.features.display.sheets.IslandPulseShadowOptionsBottomSheet import com.sameerasw.essentials.ui.features.display.sheets.IslandNavigationOptionsBottomSheet import com.sameerasw.essentials.ui.features.display.sheets.IslandSoundModeOptionsBottomSheet import com.sameerasw.essentials.ui.features.display.sheets.IslandTimeOptionsBottomSheet @@ -803,7 +805,10 @@ fun IslandSettingsUI( } if (showNavigationOptionsSheet) { - IslandNavigationOptionsBottomSheet(onDismissRequest = { showNavigationOptionsSheet = false }) + IslandNavigationOptionsBottomSheet( + viewModel = viewModel, + onDismissRequest = { showNavigationOptionsSheet = false }, + ) } if (showAlarmOptionsSheet) { diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandNavigationOptionsBottomSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandNavigationOptionsBottomSheet.kt index 432ed257d..7f15483f5 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandNavigationOptionsBottomSheet.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandNavigationOptionsBottomSheet.kt @@ -9,12 +9,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -27,18 +22,17 @@ import com.sameerasw.essentials.ui.core.containers.RoundedCardContainer import com.sameerasw.essentials.ui.core.pickers.SegmentedPicker import com.sameerasw.essentials.ui.core.sheets.EssentialsBottomSheet import com.sameerasw.essentials.utils.HapticUtil +import com.sameerasw.essentials.viewmodels.MainViewModel private val EXPAND_DISTANCE_OPTIONS = listOf(0, 50, 100, 200, 500) @OptIn(ExperimentalMaterial3Api::class) @Composable -fun IslandNavigationOptionsBottomSheet(onDismissRequest: () -> Unit) { - val context = LocalContext.current +fun IslandNavigationOptionsBottomSheet( + viewModel: MainViewModel, + onDismissRequest: () -> Unit, +) { val view = LocalView.current - val settings = remember { SettingsRepository(context) } - var onNew by remember { mutableStateOf(settings.getIslandNavigationOnNew()) } - var expandDistance by remember { mutableStateOf(settings.getIslandNavigationExpandDistance()) } - var peekAfterExpand by remember { mutableStateOf(settings.isIslandNavigationPeekAfterExpandEnabled()) } @Composable fun distanceLabel(meters: Int) = @@ -85,11 +79,8 @@ fun IslandNavigationOptionsBottomSheet(onDismissRequest: () -> Unit) { ) SegmentedPicker( items = labels.keys.toList(), - selectedItem = onNew, - onItemSelected = { - onNew = it - settings.setIslandNavigationOnNew(it) - }, + selectedItem = viewModel.islandNavigationOnNew.value, + onItemSelected = { viewModel.setIslandNavigationOnNew(it) }, labelProvider = { labels[it].orEmpty() }, title = R.string.island_navigation_on_new_title, modifier = Modifier.fillMaxWidth(), @@ -99,7 +90,7 @@ fun IslandNavigationOptionsBottomSheet(onDismissRequest: () -> Unit) { ConfigPickerItem( title = stringResource(R.string.island_navigation_expand_title), description = stringResource(R.string.island_navigation_expand_desc), - selectedValue = distanceLabel(expandDistance), + selectedValue = distanceLabel(viewModel.islandNavigationExpandDistance.intValue), iconRes = R.drawable.rounded_magnify_fullscreen_24, modifier = Modifier.fillMaxWidth(), ) { @@ -108,8 +99,7 @@ fun IslandNavigationOptionsBottomSheet(onDismissRequest: () -> Unit) { text = { Text(distanceLabel(meters)) }, onClick = { HapticUtil.performVirtualKeyHaptic(view) - expandDistance = meters - settings.setIslandNavigationExpandDistance(meters) + viewModel.setIslandNavigationExpandDistance(meters) }, ) } @@ -119,11 +109,10 @@ fun IslandNavigationOptionsBottomSheet(onDismissRequest: () -> Unit) { iconRes = R.drawable.round_navigation_24, title = stringResource(R.string.island_navigation_peek_after_expand_title), description = stringResource(R.string.island_navigation_peek_after_expand_desc), - isChecked = peekAfterExpand, + isChecked = viewModel.isIslandNavigationPeekAfterExpand.value, onCheckedChange = { HapticUtil.performVirtualKeyHaptic(view) - peekAfterExpand = it - settings.setIslandNavigationPeekAfterExpandEnabled(it) + viewModel.setIslandNavigationPeekAfterExpand(it) }, ) } 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 a595a2904..a3a58981b 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt @@ -242,6 +242,9 @@ class MainViewModel : ViewModel() { val isIslandShowTravel = mutableStateOf(true) val isIslandShowCaffeinate = mutableStateOf(true) val isIslandShowNavigation = mutableStateOf(true) + val islandNavigationOnNew = mutableStateOf(SettingsRepository.ISLAND_NAVIGATION_ON_NEW_EXPAND) + val islandNavigationExpandDistance = mutableIntStateOf(200) + val isIslandNavigationPeekAfterExpand = mutableStateOf(false) val isIslandShowHotspot = mutableStateOf(false) val isIslandShowDevices = mutableStateOf(true) val isIslandBriefEnabled = mutableStateOf(false) @@ -947,6 +950,18 @@ class MainViewModel : ViewModel() { SettingsRepository.KEY_ISLAND_NOTIF_TAP_TO_OPEN -> isIslandNotifTapToOpen.value = settingsRepository.isIslandNotifTapToOpenEnabled() + SettingsRepository.KEY_ISLAND_SHOW_NAVIGATION -> + isIslandShowNavigation.value = settingsRepository.isIslandShowNavigationEnabled() + + SettingsRepository.KEY_ISLAND_NAVIGATION_ON_NEW -> + islandNavigationOnNew.value = settingsRepository.getIslandNavigationOnNew() + + SettingsRepository.KEY_ISLAND_NAVIGATION_EXPAND_DISTANCE -> + islandNavigationExpandDistance.intValue = settingsRepository.getIslandNavigationExpandDistance() + + SettingsRepository.KEY_ISLAND_NAVIGATION_PEEK_AFTER_EXPAND -> + isIslandNavigationPeekAfterExpand.value = settingsRepository.isIslandNavigationPeekAfterExpandEnabled() + SettingsRepository.KEY_ISLAND_CATCH_UP_ENABLED -> isIslandCatchUpEnabled.value = settingsRepository.isIslandCatchUpEnabled() @@ -2417,6 +2432,9 @@ class MainViewModel : ViewModel() { isDuoDisableInteractionsInFullscreen.value = settingsRepository.isDuoDisableInteractionsInFullscreenEnabled() isIslandShowCaffeinate.value = settingsRepository.isIslandShowCaffeinateEnabled() isIslandShowNavigation.value = settingsRepository.isIslandShowNavigationEnabled() + islandNavigationOnNew.value = settingsRepository.getIslandNavigationOnNew() + islandNavigationExpandDistance.intValue = settingsRepository.getIslandNavigationExpandDistance() + isIslandNavigationPeekAfterExpand.value = settingsRepository.isIslandNavigationPeekAfterExpandEnabled() isIslandShowHotspot.value = settingsRepository.isIslandShowHotspotEnabled() isIslandShowDevices.value = settingsRepository.isIslandShowDevicesEnabled() isIslandBriefEnabled.value = settingsRepository.isIslandBriefEnabled() @@ -5511,6 +5529,21 @@ class MainViewModel : ViewModel() { settingsRepository.setIslandShowNavigationEnabled(enabled) } + fun setIslandNavigationOnNew(mode: String) { + islandNavigationOnNew.value = mode + settingsRepository.setIslandNavigationOnNew(mode) + } + + fun setIslandNavigationExpandDistance(meters: Int) { + islandNavigationExpandDistance.intValue = meters + settingsRepository.setIslandNavigationExpandDistance(meters) + } + + fun setIslandNavigationPeekAfterExpand(enabled: Boolean) { + isIslandNavigationPeekAfterExpand.value = enabled + settingsRepository.setIslandNavigationPeekAfterExpandEnabled(enabled) + } + fun setIslandShowHotspot(enabled: Boolean) { isIslandShowHotspot.value = enabled settingsRepository.setIslandShowHotspotEnabled(enabled) diff --git a/app/src/test/java/com/sameerasw/essentials/domain/NavigationParserTest.kt b/app/src/test/java/com/sameerasw/essentials/domain/NavigationParserTest.kt index cfeb434da..1efbc58c7 100644 --- a/app/src/test/java/com/sameerasw/essentials/domain/NavigationParserTest.kt +++ b/app/src/test/java/com/sameerasw/essentials/domain/NavigationParserTest.kt @@ -38,6 +38,13 @@ class NavigationParserTest { assertEquals("Toward Main St", p.eta) } + @Test + fun nonLatinDistanceInTitle() { + val p = NavigationParser.parse("300 м", "Поверните направо", null, null) + assertEquals("300 м", p.distance) + assertEquals("Поверните направо", p.instruction) + } + @Test fun longTextIsNotADistance() { val p = NavigationParser.parse("Continue on Highway 15D for 25 km", null, null, null) @@ -48,7 +55,8 @@ class NavigationParserTest { fun exitAction() { assertEquals(1, NavigationParser.exitActionIndex(listOf("Mute", "Exit navigation"))) assertEquals(0, NavigationParser.exitActionIndex(listOf("Salir"))) - assertEquals(0, NavigationParser.exitActionIndex(listOf("Something"))) + assertEquals(-1, NavigationParser.exitActionIndex(listOf("Something"))) + assertEquals(-1, NavigationParser.exitActionIndex(listOf("Mute"))) assertEquals(-1, NavigationParser.exitActionIndex(listOf("Mute", "Send"))) assertEquals(-1, NavigationParser.exitActionIndex(emptyList())) } @@ -61,6 +69,12 @@ class NavigationParserTest { assertEquals(0.2 * 1609.344, NavigationParser.meters("0.2 mi")!!, 0.01) assertEquals(300.0, NavigationParser.meters("300 m")!!, 0.01) assertEquals(50.0, NavigationParser.meters("In 50m")!!, 0.01) + assertEquals(300.0, NavigationParser.meters("300 м")!!, 0.01) + assertEquals(1500.0, NavigationParser.meters("1,5 км")!!, 0.01) + assertEquals(300.0, NavigationParser.meters("300米")!!, 0.01) + assertEquals(1200.0, NavigationParser.meters("1.2公里")!!, 0.01) + assertEquals(1200.0, NavigationParser.meters("1.2千米")!!, 0.01) + assertNull(NavigationParser.meters("5 мин")) assertNull(NavigationParser.meters("Western Road")) assertNull(NavigationParser.meters(null)) } diff --git a/app/src/test/java/com/sameerasw/essentials/island/plugins/navigation/NavigationTriggerTest.kt b/app/src/test/java/com/sameerasw/essentials/island/plugins/navigation/NavigationTriggerTest.kt new file mode 100644 index 000000000..026ab6e57 --- /dev/null +++ b/app/src/test/java/com/sameerasw/essentials/island/plugins/navigation/NavigationTriggerTest.kt @@ -0,0 +1,78 @@ +package com.sameerasw.essentials.island.plugins.navigation + +import com.sameerasw.essentials.data.repository.SettingsRepository +import com.sameerasw.essentials.island.plugins.navigation.NavigationTrigger.Action +import org.junit.Assert.assertEquals +import org.junit.Test + +class NavigationTriggerTest { + private val trigger = NavigationTrigger() + + private fun decide( + instruction: String = "Turn right", + turned: Boolean = false, + meters: Double? = null, + onNew: String = SettingsRepository.ISLAND_NAVIGATION_ON_NEW_PEEK, + canExpand: Boolean = true, + now: Long = 100_000L, + ) = trigger.decide(instruction, turned, meters, 200, onNew, canPeek = true, canExpand = canExpand, now = now) + + @Test + fun newInstructionPeeks() { + assertEquals(Action.Peek, decide(turned = true, meters = 1000.0)) + } + + @Test + fun newInstructionExpandsWhenSetToExpand() { + assertEquals(Action.Expand, decide(turned = true, meters = 1000.0, onNew = SettingsRepository.ISLAND_NAVIGATION_ON_NEW_EXPAND)) + } + + @Test + fun offDoesNothingOnNewInstruction() { + assertEquals(Action.None, decide(turned = true, meters = 1000.0, onNew = SettingsRepository.ISLAND_NAVIGATION_ON_NEW_OFF)) + } + + @Test + fun announceSkippedWhenTurnIsClose() { + assertEquals(Action.None, decide(turned = true, meters = 250.0)) + } + + @Test + fun approachingExpandsOncePerManeuver() { + assertEquals(Action.Expand, decide(meters = 150.0)) + assertEquals(Action.None, decide(meters = 100.0)) + } + + @Test + fun approachingRearmsAfterDistanceJumpsBack() { + assertEquals(Action.Expand, decide(meters = 150.0)) + decide(meters = 1000.0) + assertEquals(Action.Expand, decide(meters = 150.0)) + } + + @Test + fun approachingWaitsWhileOtherItemIsFocused() { + assertEquals(Action.None, decide(turned = true, meters = 150.0, canExpand = false)) + assertEquals(Action.Expand, decide(meters = 140.0)) + } + + @Test + fun surfacedInstructionNotAnnouncedAgain() { + assertEquals(Action.Peek, decide(turned = true)) + assertEquals(Action.None, decide(turned = true, now = 200_000L)) + } + + @Test + fun cooldownBlocksBackToBackAnnouncements() { + assertEquals(Action.Peek, decide(instruction = "A", turned = true, meters = 1000.0)) + assertEquals(Action.None, decide(instruction = "B", turned = true, meters = 1000.0, now = 110_000L)) + assertEquals(Action.Peek, decide(instruction = "C", turned = true, meters = 1000.0, now = 140_000L)) + } + + @Test + fun resetForgetsSurfaced() { + decide(turned = true, meters = 1000.0) + trigger.reset() + assertEquals(Action.Peek, decide(turned = true, meters = 1000.0)) + } +} From 981eb037f83c22e564a460a83f50342f22a393a7 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Sun, 18 Oct 2026 08:41:19 +0530 Subject: [PATCH 4/4] feat: remove descriptions and update priority of nav plugin --- .../essentials/data/repository/SettingsRepository.kt | 2 +- .../com/sameerasw/essentials/island/model/IslandModels.kt | 4 ++-- .../essentials/island/model/IslandPriorityEntries.kt | 2 +- .../essentials/ui/features/display/IslandSettingsUI.kt | 1 - .../display/sheets/IslandNavigationOptionsBottomSheet.kt | 1 - .../java/com/sameerasw/essentials/viewmodels/MainViewModel.kt | 2 +- app/src/main/res/values/strings.xml | 1 - 7 files changed, 5 insertions(+), 8 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 f8008aa39..0bb1d8e3c 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 @@ -4200,7 +4200,7 @@ class SettingsRepository( fun setIslandShowNavigationEnabled(enabled: Boolean) = putBoolean(KEY_ISLAND_SHOW_NAVIGATION, enabled) fun getIslandNavigationOnNew(): String = - getString(KEY_ISLAND_NAVIGATION_ON_NEW, ISLAND_NAVIGATION_ON_NEW_EXPAND) ?: ISLAND_NAVIGATION_ON_NEW_EXPAND + getString(KEY_ISLAND_NAVIGATION_ON_NEW, ISLAND_NAVIGATION_ON_NEW_PEEK) ?: ISLAND_NAVIGATION_ON_NEW_PEEK fun setIslandNavigationOnNew(mode: String) = putString(KEY_ISLAND_NAVIGATION_ON_NEW, mode) fun isIslandNavigationPeekAfterExpandEnabled(): Boolean = getBoolean(KEY_ISLAND_NAVIGATION_PEEK_AFTER_EXPAND, false) diff --git a/app/src/main/java/com/sameerasw/essentials/island/model/IslandModels.kt b/app/src/main/java/com/sameerasw/essentials/island/model/IslandModels.kt index 9f9a3c78a..4441c1e29 100644 --- a/app/src/main/java/com/sameerasw/essentials/island/model/IslandModels.kt +++ b/app/src/main/java/com/sameerasw/essentials/island/model/IslandModels.kt @@ -21,9 +21,9 @@ object IslandPriority { const val CALENDAR_OVERRIDE = 6 const val NOTIFICATION = 10 - const val NAVIGATION = 14 - const val PROGRESS = 15 + const val PROGRESS = 15 const val FLASHLIGHT = 20 + const val NAVIGATION = 22 const val TIMER = 25 const val MEDIA = 30 const val CONSCIOUS_GATE = 40 diff --git a/app/src/main/java/com/sameerasw/essentials/island/model/IslandPriorityEntries.kt b/app/src/main/java/com/sameerasw/essentials/island/model/IslandPriorityEntries.kt index 0ccfe2e66..a823c44e5 100644 --- a/app/src/main/java/com/sameerasw/essentials/island/model/IslandPriorityEntries.kt +++ b/app/src/main/java/com/sameerasw/essentials/island/model/IslandPriorityEntries.kt @@ -6,8 +6,8 @@ object IslandPriorityEntries { val all = listOf( Entry("battery", setOf("battery_alerts_plugin", "battery"), IslandPriority.BATTERY_ALERT), Entry("notifications", setOf("notifications"), IslandPriority.NOTIFICATION), - Entry("navigation", setOf("navigation"), IslandPriority.NAVIGATION), Entry("flashlight", setOf("flashlight"), IslandPriority.FLASHLIGHT), + Entry("navigation", setOf("navigation"), IslandPriority.NAVIGATION), Entry("timer", setOf("timer"), IslandPriority.TIMER), Entry("media", setOf("media"), IslandPriority.MEDIA), Entry("conscious_gate", setOf("conscious_gate"), IslandPriority.CONSCIOUS_GATE), diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSettingsUI.kt index 2df8d419d..9f643d314 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/features/display/IslandSettingsUI.kt @@ -437,7 +437,6 @@ fun IslandSettingsUI( dragHandle = handle, iconRes = R.drawable.round_navigation_24, title = stringResource(R.string.island_show_navigation_title), - description = stringResource(R.string.island_navigation_desc), isChecked = viewModel.isIslandShowNavigation.value, onCheckedChange = { checked -> HapticUtil.performVirtualKeyHaptic(view) diff --git a/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandNavigationOptionsBottomSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandNavigationOptionsBottomSheet.kt index 7f15483f5..12d3edf47 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandNavigationOptionsBottomSheet.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/features/display/sheets/IslandNavigationOptionsBottomSheet.kt @@ -89,7 +89,6 @@ fun IslandNavigationOptionsBottomSheet( ConfigPickerItem( title = stringResource(R.string.island_navigation_expand_title), - description = stringResource(R.string.island_navigation_expand_desc), selectedValue = distanceLabel(viewModel.islandNavigationExpandDistance.intValue), iconRes = R.drawable.rounded_magnify_fullscreen_24, modifier = Modifier.fillMaxWidth(), 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 be24f4d24..70c165ecf 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt @@ -249,7 +249,7 @@ class MainViewModel : ViewModel() { val isIslandShowTravel = mutableStateOf(true) val isIslandShowCaffeinate = mutableStateOf(true) val isIslandShowNavigation = mutableStateOf(true) - val islandNavigationOnNew = mutableStateOf(SettingsRepository.ISLAND_NAVIGATION_ON_NEW_EXPAND) + val islandNavigationOnNew = mutableStateOf(SettingsRepository.ISLAND_NAVIGATION_ON_NEW_PEEK) val islandNavigationExpandDistance = mutableIntStateOf(200) val isIslandNavigationPeekAfterExpand = mutableStateOf(false) val isIslandShowHotspot = mutableStateOf(false) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b61cec239..9bf002961 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3303,7 +3303,6 @@ Expand Line peek Expand before a turn - Expands with the direction when you get this close to the turn Off %d m Line peek after expanding