Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0675978
feat(island): add navigation plugin and settings to island
isaacsa51 Oct 13, 2026
ba8aa79
Merge remote-tracking branch 'upstream/develop' into feat/island/navi…
isaacsa51 Oct 13, 2026
938dc1b
feat(island): add navigation plugin and settings to island
isaacsa51 Oct 13, 2026
6435d51
Merge branch 'develop' into feat/island/navigation_plugin_connection
sameerasw Oct 15, 2026
44caae0
feat: screenshots page with delayed share cleanup, auto-clean and stats
jawzf Oct 15, 2026
bd4e97d
feat(island): add navigation plugin and settings to island
isaacsa51 Oct 15, 2026
eabce7a
Merge branch 'develop' into feat/island/navigation_plugin_connection
isaacsa51 Oct 15, 2026
06206a6
feat: track seen notifications to prioritize unseen alerts during rot…
sameerasw Oct 16, 2026
f3f3ca3
Merge branch 'develop' into claude/screenshots-page
jawzf Oct 16, 2026
8753e40
Merge branch 'develop' into claude/screenshots-page
jawzf Oct 17, 2026
a35986c
Merge branch 'develop' into feat/island/navigation_plugin_connection
isaacsa51 Oct 18, 2026
981eb03
feat: remove descriptions and update priority of nav plugin
sameerasw Oct 18, 2026
b719232
feat(island): add navigation plugin and settings to island (#1369)
sameerasw Oct 18, 2026
cdb2744
Merge remote-tracking branch 'origin/develop' into claude/screenshots…
sameerasw Oct 18, 2026
c5883e9
feat: add screenshot cleanup undo support, debounced scanning, and im…
sameerasw Oct 18, 2026
d8bb0cf
feat: screenshots page with auto-clean (#1385)
sameerasw Oct 18, 2026
20c9f02
feat: widget stack remembers its position across reboots
jawzf Oct 16, 2026
35b92f2
Widget stack: drop comment and braces noise
sameerasw Oct 18, 2026
f778280
feat: widget stack remembers its position across reboots (#1409)
sameerasw Oct 18, 2026
e5f6420
feat: add Send notification DIY action
jawzf Oct 16, 2026
36f2e60
Send notification: open app on tap, create channel once, ellipsize lo…
sameerasw Oct 18, 2026
a4053d3
Send notification sheet: card-style fields like other sheets
sameerasw Oct 18, 2026
61ba376
feat: add Send notification DIY action (#1411)
sameerasw Oct 18, 2026
8094590
Merge branch 'main' into develop
sameerasw Oct 18, 2026
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
4 changes: 4 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1738,6 +1738,10 @@
android:name=".receivers.UrlShortenerActionReceiver"
android:exported="false" />

<receiver
android:name=".receivers.ScreenshotCleanupActionReceiver"
android:exported="false" />

<service
android:name=".appfunctions.EssentialsAppFunctionService"
android:exported="true"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,13 @@ class SettingsRepository(
const val KEY_ISLAND_SHOW_CAFFEINATE = "island_show_caffeinate"
const val KEY_ISLAND_CAFFEINATE_SHOW_TIME = "island_caffeinate_show_time"
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_SHOW_HOTSPOT = "island_show_hotspot"
const val KEY_ISLAND_BRIEF_ENABLED = "island_brief_enabled"
const val KEY_ISLAND_BRIEF_SHOW_ALARM = "island_brief_show_alarm"
Expand Down Expand Up @@ -776,6 +783,12 @@ class SettingsRepository(
const val KEY_POCKET_MODE_ENABLED = "pocket_mode_enabled"
const val KEY_FACE_UNLOCK_BRIGHTNESS_ENABLED = "face_unlock_brightness_enabled"
const val KEY_SCREENSHOT_SHARE_CLEANUP_ENABLED = "screenshot_share_cleanup_enabled"
const val KEY_SCREENSHOT_SHARE_CLEANUP_DELAY = "screenshot_share_cleanup_delay"
const val KEY_SCREENSHOT_AUTO_CLEAN_ENABLED = "screenshot_auto_clean_enabled"
const val KEY_SCREENSHOT_AUTO_CLEAN_DAYS = "screenshot_auto_clean_days"
const val KEY_SCREENSHOT_AUTO_CLEAN_LAST_ID = "screenshot_auto_clean_last_id"
const val KEY_SCREENSHOTS_CLEANED_COUNT = "screenshots_cleaned_count"
const val KEY_SCREENSHOTS_CLEANED_BYTES = "screenshots_cleaned_bytes"
const val KEY_FACE_UNLOCK_MAX_BRIGHTNESS = "face_unlock_max_brightness"
const val KEY_FACE_UNLOCK_TRIGGER_UNLOCK = "face_unlock_trigger_unlock"
const val KEY_FACE_UNLOCK_AUTO_ILLUMINATE = "face_unlock_auto_illuminate"
Expand Down Expand Up @@ -4189,6 +4202,19 @@ 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)

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, false)
fun setIslandNavigationPeekAfterExpandEnabled(enabled: Boolean) = putBoolean(KEY_ISLAND_NAVIGATION_PEEK_AFTER_EXPAND, enabled)

fun getIslandNavigationExpandDistance(): Int = getInt(KEY_ISLAND_NAVIGATION_EXPAND_DISTANCE, 200)
fun setIslandNavigationExpandDistance(meters: Int) = putInt(KEY_ISLAND_NAVIGATION_EXPAND_DISTANCE, meters)

fun isIslandShowHotspotEnabled(): Boolean = getBoolean(KEY_ISLAND_SHOW_HOTSPOT, false)
fun setIslandShowHotspotEnabled(enabled: Boolean) = putBoolean(KEY_ISLAND_SHOW_HOTSPOT, enabled)

Expand Down Expand Up @@ -4301,6 +4327,28 @@ class SettingsRepository(

fun isScreenshotShareCleanupEnabled(): Boolean = getBoolean(KEY_SCREENSHOT_SHARE_CLEANUP_ENABLED, false)
fun setScreenshotShareCleanupEnabled(enabled: Boolean) = putBoolean(KEY_SCREENSHOT_SHARE_CLEANUP_ENABLED, enabled)
fun getScreenshotShareCleanupDelayMinutes(): Int = getInt(KEY_SCREENSHOT_SHARE_CLEANUP_DELAY, 0)
fun setScreenshotShareCleanupDelayMinutes(minutes: Int) = putInt(KEY_SCREENSHOT_SHARE_CLEANUP_DELAY, minutes)
fun isScreenshotAutoCleanEnabled(): Boolean = getBoolean(KEY_SCREENSHOT_AUTO_CLEAN_ENABLED, false)
fun setScreenshotAutoCleanEnabled(enabled: Boolean) = putBoolean(KEY_SCREENSHOT_AUTO_CLEAN_ENABLED, enabled)
fun getScreenshotAutoCleanDays(): Int = getInt(KEY_SCREENSHOT_AUTO_CLEAN_DAYS, 7)
fun setScreenshotAutoCleanDays(days: Int) = putInt(KEY_SCREENSHOT_AUTO_CLEAN_DAYS, days)
fun getScreenshotAutoCleanLastId(): Long = getLong(KEY_SCREENSHOT_AUTO_CLEAN_LAST_ID, 0L)
fun setScreenshotAutoCleanLastId(id: Long) = putLong(KEY_SCREENSHOT_AUTO_CLEAN_LAST_ID, id)
fun getScreenshotsCleanedCount(): Int = getInt(KEY_SCREENSHOTS_CLEANED_COUNT, 0)
fun getScreenshotsCleanedBytes(): Long = getLong(KEY_SCREENSHOTS_CLEANED_BYTES, 0L)

fun recordScreenshotCleaned(bytes: Long) =
synchronized(SettingsRepository::class.java) {
putInt(KEY_SCREENSHOTS_CLEANED_COUNT, getScreenshotsCleanedCount() + 1)
putLong(KEY_SCREENSHOTS_CLEANED_BYTES, getScreenshotsCleanedBytes() + bytes)
}

fun recordScreenshotRestored(bytes: Long) =
synchronized(SettingsRepository::class.java) {
putInt(KEY_SCREENSHOTS_CLEANED_COUNT, (getScreenshotsCleanedCount() - 1).coerceAtLeast(0))
putLong(KEY_SCREENSHOTS_CLEANED_BYTES, (getScreenshotsCleanedBytes() - bytes).coerceAtLeast(0L))
}

fun getFaceUnlockMaxBrightness(): Int = getInt(KEY_FACE_UNLOCK_MAX_BRIGHTNESS, 100)
fun setFaceUnlockMaxBrightness(value: Int) = putInt(KEY_FACE_UNLOCK_MAX_BRIGHTNESS, value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ package com.sameerasw.essentials.data.repository

import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.sameerasw.essentials.domain.model.WidgetStackConfig
Expand Down Expand Up @@ -43,6 +44,32 @@ class WidgetStackRepository(

fun get(stackWidgetId: Int): WidgetStackConfig? = getAll().firstOrNull { it.stackWidgetId == stackWidgetId }

fun getPositions(): Map<Int, Int> {
val result = mutableMapOf<Int, Int>()
for ((key, value) in prefs.all) {
if (key.startsWith(KEY_POSITION_PREFIX) && value is Int) {
val id = key.removePrefix(KEY_POSITION_PREFIX).toIntOrNull() ?: continue
result[id] = value
}
}
return result
}

fun savePositions(positions: Map<Int, Int>) {
val toSave =
positions.filter { (id, pos) ->
val key = KEY_POSITION_PREFIX + id
val stored = if (prefs.contains(key)) prefs.getInt(key, 0) else null
stored != pos
}
if (toSave.isEmpty()) return
prefs.edit {
for ((id, pos) in toSave) {
putInt(KEY_POSITION_PREFIX + id, pos)
}
}
}

fun save(config: WidgetStackConfig) =
synchronized(LOCK) {
write(getAll().filterNot { it.stackWidgetId == config.stackWidgetId } + config)
Expand All @@ -51,6 +78,7 @@ class WidgetStackRepository(
fun delete(stackWidgetId: Int) =
synchronized(LOCK) {
write(getAll().filterNot { it.stackWidgetId == stackWidgetId })
prefs.edit { remove(KEY_POSITION_PREFIX + stackWidgetId) }
}

private fun write(stacks: List<WidgetStackConfig>) {
Expand All @@ -63,6 +91,7 @@ class WidgetStackRepository(
companion object {
private const val PREFS_NAME = "widget_stack_prefs"
private const val KEY_STACKS = "stacks"
private const val KEY_POSITION_PREFIX = "position_"
private val LOCK = Any()
private var cachedJson: String? = null
private var cachedStacks: List<WidgetStackConfig> = emptyList()
Expand Down
12 changes: 12 additions & 0 deletions app/src/main/java/com/sameerasw/essentials/domain/diy/Action.kt
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,18 @@ sealed interface Action {
override val isConfigurable: Boolean get() = true
}

@Keep
data class SendNotification(
@SerializedName("title") val notificationTitle: String = "",
@SerializedName("body") val body: String = "",
@SerializedName("label") val label: String = "",
) : Action {
override val title: Int get() = R.string.diy_action_send_notification
override val icon: Int get() = R.drawable.rounded_notifications_unread_24
override val permissions: List<String> get() = listOf("POST_NOTIFICATIONS")
override val isConfigurable: Boolean get() = true
}

@Keep
data object TurnOffLowPower : Action {
override val title: Int = R.string.diy_action_low_power_off
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ object ActionRegistry {
add(Action.SetChargingMode())
add(Action.CustomSettings())
add(Action.Wait())
add(Action.SendNotification())
add(Action.CircleToSearch)
add(Action.TurnOffBackGesture)
add(Action.TurnOnBackGesture)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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?,
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 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,
)

fun isDistance(s: String?) = s != null && s.length <= 24 && distanceRegex.containsMatchIn(s)

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?($units)(?!\p{L})""", RegexOption.IGNORE_CASE)

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()))
}

fun exitActionIndex(titles: List<String?>): Int =
titles.indexOfFirst { it != null && exitRegex.containsMatchIn(it) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import com.sameerasw.essentials.ui.features.display.ISLAND_PLACEMENT_FEATURE_ID
import com.sameerasw.essentials.ui.features.display.ISLAND_VISUALS_FEATURE_ID
import android.content.Context
import android.content.Intent
import android.os.Build
import com.sameerasw.essentials.EssentialsApp
import com.sameerasw.essentials.FeatureSettingsActivity
import com.sameerasw.essentials.R
Expand Down Expand Up @@ -825,6 +826,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.island_show_hotspot_title,
R.string.island_hotspot_desc,
Expand Down Expand Up @@ -2950,30 +2956,50 @@ object FeatureRegistry {
) = viewModel.setFaceUnlockBrightnessEnabled(enabled)
},
object : Feature(
id = "Screenshot share cleanup",
title = R.string.feat_screenshot_share_cleanup_title,
id = "Screenshots",
title = R.string.feat_screenshots_title,
iconRes = R.drawable.rounded_screenshot_region_24,
category = R.string.cat_tools,
description = R.string.feat_screenshot_share_cleanup_desc,
aboutDescription = R.string.about_desc_screenshot_share_cleanup,
description = R.string.feat_screenshots_desc,
aboutDescription = R.string.about_desc_screenshots,
permissionKeys = listOf("ACCESSIBILITY", "STORAGE"),
hasMoreSettings = false,
showToggle = false,
searchableSettings =
listOf(
SearchSetting(
R.string.feat_screenshot_share_cleanup_title,
R.string.feat_screenshots_title,
"screenshot_share_cleanup_enabled",
),
SearchSetting(
R.string.screenshot_share_cleanup_delay_title,
R.string.feat_screenshots_title,
"screenshot_share_cleanup_delay",
),
SearchSetting(
R.string.screenshot_auto_clean_title,
R.string.feat_screenshots_title,
"screenshot_auto_clean_enabled",
),
SearchSetting(
R.string.screenshot_auto_clean_days_title,
R.string.feat_screenshots_title,
"screenshot_auto_clean_days",
),
),
parentFeatureId = "Security",
) {
override fun isEnabled(viewModel: MainViewModel) = viewModel.isScreenshotShareCleanupEnabled.value

override fun isToggleEnabled(
viewModel: MainViewModel,
context: Context,
) = viewModel.isAccessibilityEnabled.value && viewModel.isStoragePermissionGranted.value
override fun isEnabled(viewModel: MainViewModel) =
viewModel.isScreenshotShareCleanupEnabled.value || viewModel.isScreenshotAutoCleanEnabled.value

override fun onToggle(
viewModel: MainViewModel,
context: Context,
enabled: Boolean,
) = viewModel.setScreenshotShareCleanupEnabled(enabled)
) {}

override fun isDeviceSupported(context: Context) = DeviceUtils.isGoogleDevice()
override fun isDeviceSupported(context: Context) =
DeviceUtils.isGoogleDevice() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
},
object : Feature(
id = "Lockdown mode",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,10 @@ fun initPermissionRegistry() {
// AOD Wallpaper feature
PermissionRegistry.register("STORAGE", R.string.feat_aod_wallpaper_title)

// Screenshot share cleanup feature
PermissionRegistry.register("ACCESSIBILITY", R.string.feat_screenshot_share_cleanup_title)
PermissionRegistry.register("STORAGE", R.string.feat_screenshot_share_cleanup_title)
// Screenshots feature
PermissionRegistry.register("ACCESSIBILITY", R.string.feat_screenshots_title)
PermissionRegistry.register("STORAGE", R.string.feat_screenshots_title)
PermissionRegistry.register("POST_NOTIFICATIONS", R.string.feat_screenshots_title)

// Pixel Search features
PermissionRegistry.register("STORAGE", R.string.pixel_search_results_files_title)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ object IslandPriority {
const val CALENDAR_OVERRIDE = 6

const val NOTIFICATION = 10
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ object IslandPriorityEntries {
Entry("battery", setOf("battery_alerts_plugin", "battery"), IslandPriority.BATTERY_ALERT),
Entry("notifications", setOf("notifications"), IslandPriority.NOTIFICATION),
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),
Expand Down
Loading
Loading