mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-11 00:12:24 +01:00
Merge #64: choose which apps the car's drawer shows
This commit is contained in:
commit
4172cf1a0a
8 files changed
+164
-5
No files matched your search
@@ -227,9 +227,10 @@ private fun groupOf(entry: Entry, mediaPackages: Set<String>): AppGroup {
|
|||||||
* The launcher's tiles in the order they're drawn: grouped by [AppGroup], alphabetical inside a
|
* The launcher's tiles in the order they're drawn: grouped by [AppGroup], alphabetical inside a
|
||||||
* group with Settings last of all, then with the driver's own moves from [custom] laid over it.
|
* group with Settings last of all, then with the driver's own moves from [custom] laid over it.
|
||||||
* Moved apps only trade places among themselves, so an app installed later still lands in its group.
|
* Moved apps only trade places among themselves, so an app installed later still lands in its group.
|
||||||
|
* [hidden] leaves tiles out of the drawer only: a pinned app stays on the nav bar.
|
||||||
*/
|
*/
|
||||||
internal fun launcherOrder(all: List<Entry>, showVehicleData: Boolean, custom: List<String>): List<Entry> {
|
internal fun launcherOrder(all: List<Entry>, showVehicleData: Boolean, custom: List<String>, hidden: Set<String>): List<Entry> {
|
||||||
val shown = all.filter { it.builtIn !== BuiltInApps.VehicleData || showVehicleData }
|
val shown = all.filter { (it.builtIn !== BuiltInApps.VehicleData || showVehicleData) && it.componentId !in hidden }
|
||||||
val mediaPackages = shown.mapNotNull { it.media?.component?.packageName }.toSet()
|
val mediaPackages = shown.mapNotNull { it.media?.component?.packageName }.toSet()
|
||||||
val grouped = shown.sortedWith(
|
val grouped = shown.sortedWith(
|
||||||
compareBy<Entry>({ groupOf(it, mediaPackages) }, { it.builtIn === BuiltInApps.Settings }, { it.label.lowercase() }),
|
compareBy<Entry>({ groupOf(it, mediaPackages) }, { it.builtIn === BuiltInApps.Settings }, { it.label.lowercase() }),
|
||||||
@@ -293,7 +294,8 @@ fun CarLauncher() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val appOrder by CarSettings.appOrder.collectAsState()
|
val appOrder by CarSettings.appOrder.collectAsState()
|
||||||
val tiles = launcherOrder(all, showVehicleData, appOrder).map(::tileOf)
|
val hidden by CarSettings.hiddenApps.collectAsState()
|
||||||
|
val tiles = launcherOrder(all, showVehicleData, appOrder, hidden).map(::tileOf)
|
||||||
|
|
||||||
LazyVerticalGrid(
|
LazyVerticalGrid(
|
||||||
columns = GridCells.Adaptive(minSize = 108.dp),
|
columns = GridCells.Adaptive(minSize = 108.dp),
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ private const val KEY_LOCATION_WARNING = "car_location_warning"
|
|||||||
private const val KEY_DARK_STYLE = "car_dark_style"
|
private const val KEY_DARK_STYLE = "car_dark_style"
|
||||||
private const val KEY_PINNED = "car_pinned_apps"
|
private const val KEY_PINNED = "car_pinned_apps"
|
||||||
private const val KEY_APP_ORDER = "car_app_order"
|
private const val KEY_APP_ORDER = "car_app_order"
|
||||||
|
private const val KEY_HIDDEN_APPS = "car_hidden_apps"
|
||||||
private const val KEY_EXPERIMENTAL_FEATURES = "car_experimental_features"
|
private const val KEY_EXPERIMENTAL_FEATURES = "car_experimental_features"
|
||||||
private const val KEY_SYSTEM_SPEECH = "car_system_speech"
|
private const val KEY_SYSTEM_SPEECH = "car_system_speech"
|
||||||
private const val KEY_CAR_SENSORS = "car_sensors_enabled"
|
private const val KEY_CAR_SENSORS = "car_sensors_enabled"
|
||||||
@@ -123,6 +124,7 @@ private const val KEY_CLUSTER_VIDEO = "cluster_video_enabled"
|
|||||||
val uiTheme: StateFlow<String> = uiThemeFlow
|
val uiTheme: StateFlow<String> = uiThemeFlow
|
||||||
private val pinnedFlow = MutableStateFlow<Set<String>>(emptySet())
|
private val pinnedFlow = MutableStateFlow<Set<String>>(emptySet())
|
||||||
private val appOrderFlow = MutableStateFlow<List<String>>(emptyList())
|
private val appOrderFlow = MutableStateFlow<List<String>>(emptyList())
|
||||||
|
private val hiddenAppsFlow = MutableStateFlow<Set<String>>(emptySet())
|
||||||
private val experimentalFeaturesFlow = MutableStateFlow(false)
|
private val experimentalFeaturesFlow = MutableStateFlow(false)
|
||||||
private val systemSpeechFlow = MutableStateFlow(false)
|
private val systemSpeechFlow = MutableStateFlow(false)
|
||||||
private val carSensorsFlow = MutableStateFlow(false)
|
private val carSensorsFlow = MutableStateFlow(false)
|
||||||
@@ -220,6 +222,9 @@ private const val KEY_CLUSTER_VIDEO = "cluster_video_enabled"
|
|||||||
/** The launcher's order once the driver has moved a tile; empty keeps the grouped default. */
|
/** The launcher's order once the driver has moved a tile; empty keeps the grouped default. */
|
||||||
val appOrder: StateFlow<List<String>> = appOrderFlow
|
val appOrder: StateFlow<List<String>> = appOrderFlow
|
||||||
|
|
||||||
|
/** The tiles left out of the car's app drawer, by [Entry.componentId]. */
|
||||||
|
val hiddenApps: StateFlow<Set<String>> = hiddenAppsFlow
|
||||||
|
|
||||||
/** Flattened component names of the map and media apps used last, brought back on connect. */
|
/** Flattened component names of the map and media apps used last, brought back on connect. */
|
||||||
val lastNav: StateFlow<String?> = lastNavFlow
|
val lastNav: StateFlow<String?> = lastNavFlow
|
||||||
val lastMedia: StateFlow<String?> = lastMediaFlow
|
val lastMedia: StateFlow<String?> = lastMediaFlow
|
||||||
@@ -359,6 +364,8 @@ private const val KEY_CLUSTER_VIDEO = "cluster_video_enabled"
|
|||||||
.split(",").filter { it.isNotEmpty() }.toSet()
|
.split(",").filter { it.isNotEmpty() }.toSet()
|
||||||
appOrderFlow.value = AppSettings.getString(app, KEY_APP_ORDER, "")
|
appOrderFlow.value = AppSettings.getString(app, KEY_APP_ORDER, "")
|
||||||
.split(",").filter { it.isNotEmpty() }
|
.split(",").filter { it.isNotEmpty() }
|
||||||
|
hiddenAppsFlow.value = AppSettings.getString(app, KEY_HIDDEN_APPS, "")
|
||||||
|
.split(",").filter { it.isNotEmpty() }.toSet()
|
||||||
experimentalFeaturesFlow.value = AppSettings.getString(app, KEY_EXPERIMENTAL_FEATURES, "false") == "true"
|
experimentalFeaturesFlow.value = AppSettings.getString(app, KEY_EXPERIMENTAL_FEATURES, "false") == "true"
|
||||||
systemSpeechFlow.value = AppSettings.getString(app, KEY_SYSTEM_SPEECH, "false") == "true"
|
systemSpeechFlow.value = AppSettings.getString(app, KEY_SYSTEM_SPEECH, "false") == "true"
|
||||||
carSensorsFlow.value = AppSettings.getString(app, KEY_CAR_SENSORS, "false") == "true"
|
carSensorsFlow.value = AppSettings.getString(app, KEY_CAR_SENSORS, "false") == "true"
|
||||||
@@ -516,6 +523,12 @@ private const val KEY_CLUSTER_VIDEO = "cluster_video_enabled"
|
|||||||
AppSettings.putString(app, KEY_PINNED, next.joinToString(","))
|
AppSettings.putString(app, KEY_PINNED, next.joinToString(","))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setAppHidden(id: String, hide: Boolean) {
|
||||||
|
val next = if (hide) hiddenAppsFlow.value + id else hiddenAppsFlow.value - id
|
||||||
|
hiddenAppsFlow.value = next
|
||||||
|
AppSettings.putString(app, KEY_HIDDEN_APPS, next.joinToString(","))
|
||||||
|
}
|
||||||
|
|
||||||
fun setAppOrder(ids: List<String>) {
|
fun setAppOrder(ids: List<String>) {
|
||||||
appOrderFlow.value = ids
|
appOrderFlow.value = ids
|
||||||
AppSettings.putString(app, KEY_APP_ORDER, ids.joinToString(","))
|
AppSettings.putString(app, KEY_APP_ORDER, ids.joinToString(","))
|
||||||
|
|||||||
@@ -294,7 +294,8 @@ internal fun AppMenu(navigator: CarNavigator, running: Map<String, RunningSlot>)
|
|||||||
@Composable
|
@Composable
|
||||||
private fun MoveButtons(id: String) {
|
private fun MoveButtons(id: String) {
|
||||||
val custom by CarSettings.appOrder.collectAsState()
|
val custom by CarSettings.appOrder.collectAsState()
|
||||||
val order = launcherOrder(rememberAllEntries(), rememberVehicleDataShown(), custom).mapNotNull { it.componentId }
|
val hidden by CarSettings.hiddenApps.collectAsState()
|
||||||
|
val order = launcherOrder(rememberAllEntries(), rememberVehicleDataShown(), custom, hidden).mapNotNull { it.componentId }
|
||||||
val index = order.indexOf(id)
|
val index = order.indexOf(id)
|
||||||
if (index < 0) return
|
if (index < 0) return
|
||||||
fun move(by: Int) {
|
fun move(by: Int) {
|
||||||
|
|||||||
@@ -4,19 +4,23 @@ import androidx.annotation.StringRes
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import app.seb3thehacker.gearslip.R
|
import app.seb3thehacker.gearslip.R
|
||||||
import app.seb3thehacker.gearslip.car.theme.DarkStyle
|
import app.seb3thehacker.gearslip.car.theme.DarkStyle
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.selection.selectable
|
import androidx.compose.foundation.selection.selectable
|
||||||
|
import androidx.compose.foundation.selection.toggleable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -24,6 +28,7 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -32,19 +37,24 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import app.seb3thehacker.gearslip.car.AppTheme
|
import app.seb3thehacker.gearslip.car.AppTheme
|
||||||
import app.seb3thehacker.gearslip.car.CarApps
|
import app.seb3thehacker.gearslip.car.CarApps
|
||||||
import app.seb3thehacker.gearslip.car.LauncherMode
|
import app.seb3thehacker.gearslip.car.LauncherMode
|
||||||
import app.seb3thehacker.gearslip.car.CarSettings
|
import app.seb3thehacker.gearslip.car.CarSettings
|
||||||
|
import app.seb3thehacker.gearslip.car.LauncherCache
|
||||||
import app.seb3thehacker.gearslip.car.NavBarMode
|
import app.seb3thehacker.gearslip.car.NavBarMode
|
||||||
import app.seb3thehacker.gearslip.car.NightMode
|
import app.seb3thehacker.gearslip.car.NightMode
|
||||||
import app.seb3thehacker.gearslip.car.Units
|
import app.seb3thehacker.gearslip.car.Units
|
||||||
import app.seb3thehacker.gearslip.car.theme.GsThemes
|
import app.seb3thehacker.gearslip.car.theme.GsThemes
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
/** The car's setting groups, each opening its own screen from the phone's home screen. */
|
/** The car's setting groups, each opening its own screen from the phone's home screen. */
|
||||||
enum class CarGroup(@param:StringRes val title: Int) {
|
enum class CarGroup(@param:StringRes val title: Int) {
|
||||||
|
APPS(R.string.crumb_apps),
|
||||||
SCREEN(R.string.car_group_screen),
|
SCREEN(R.string.car_group_screen),
|
||||||
NAV_BAR(R.string.car_group_nav_bar),
|
NAV_BAR(R.string.car_group_nav_bar),
|
||||||
SOUND(R.string.car_group_sound),
|
SOUND(R.string.car_group_sound),
|
||||||
@@ -108,6 +118,7 @@ internal fun HomeCarSettings(onOpen: (CarGroup) -> Unit, modifier: Modifier = Mo
|
|||||||
// Read again each time Home shows, so a place saved a moment ago is listed.
|
// Read again each time Home shows, so a place saved a moment ago is listed.
|
||||||
val places = androidx.compose.runtime.remember { app.seb3thehacker.gearslip.car.SavedPlaces.all(context) }
|
val places = androidx.compose.runtime.remember { app.seb3thehacker.gearslip.car.SavedPlaces.all(context) }
|
||||||
fun summary(group: CarGroup) = when (group) {
|
fun summary(group: CarGroup) = when (group) {
|
||||||
|
CarGroup.APPS -> listOf(str(R.string.car_apps_sub))
|
||||||
CarGroup.SCREEN -> listOf(opens.label(open), sizes.label(scale), themes.label(theme))
|
CarGroup.SCREEN -> listOf(opens.label(open), sizes.label(scale), themes.label(theme))
|
||||||
CarGroup.NAV_BAR -> listOf(navBars.label(navBar), str(if (playerLeft) R.string.summary_player_left else R.string.summary_player_right))
|
CarGroup.NAV_BAR -> listOf(navBars.label(navBar), str(if (playerLeft) R.string.summary_player_left else R.string.summary_player_right))
|
||||||
CarGroup.SOUND -> listOf(
|
CarGroup.SOUND -> listOf(
|
||||||
@@ -187,7 +198,8 @@ internal fun CarGroupScreen(group: CarGroup, onBack: () -> Unit) {
|
|||||||
}
|
}
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
Text(
|
// The car's own Settings has no list of apps.
|
||||||
|
if (group != CarGroup.APPS) Text(
|
||||||
stringResource(R.string.car_group_same),
|
stringResource(R.string.car_group_same),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
@@ -223,6 +235,7 @@ private fun GroupRows(group: CarGroup) {
|
|||||||
val clusterVideo by CarSettings.clusterVideoEnabled.collectAsState()
|
val clusterVideo by CarSettings.clusterVideoEnabled.collectAsState()
|
||||||
|
|
||||||
when (group) {
|
when (group) {
|
||||||
|
CarGroup.APPS -> AppRows()
|
||||||
CarGroup.SCREEN -> {
|
CarGroup.SCREEN -> {
|
||||||
Choice(stringResource(R.string.car_open_on_connect), openChoices(), open, CarSettings::setOpenOnConnect)
|
Choice(stringResource(R.string.car_open_on_connect), openChoices(), open, CarSettings::setOpenOnConnect)
|
||||||
SettingsDivider()
|
SettingsDivider()
|
||||||
@@ -319,6 +332,28 @@ private fun GroupRows(group: CarGroup) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** One row per tile the installed apps put in the car's drawer, ticked while it shows there. */
|
||||||
|
@Composable
|
||||||
|
private fun AppRows() {
|
||||||
|
val context = androidx.compose.ui.platform.LocalContext.current
|
||||||
|
val apps by LauncherCache.state.collectAsState()
|
||||||
|
val hidden by CarSettings.hiddenApps.collectAsState()
|
||||||
|
// Its own scan, so an app installed since the last drive is here to choose.
|
||||||
|
LaunchedEffect(Unit) { withContext(Dispatchers.IO) { LauncherCache.load(context, force = true) } }
|
||||||
|
val rows = apps.orEmpty().mapNotNull { entry -> entry.componentId?.let { it to entry } }
|
||||||
|
if (rows.isEmpty()) SettingsRow(stringResource(if (apps == null) R.string.car_apps_finding else R.string.car_apps_none))
|
||||||
|
rows.forEachIndexed { i, (id, entry) ->
|
||||||
|
val shown = id !in hidden
|
||||||
|
if (i > 0) SettingsDivider()
|
||||||
|
SettingsRow(
|
||||||
|
entry.label,
|
||||||
|
Modifier.toggleable(shown, role = Role.Checkbox) { CarSettings.setAppHidden(id, !it) },
|
||||||
|
leading = { entry.icon?.let { Image(it, contentDescription = null, Modifier.size(40.dp)) } },
|
||||||
|
trailing = { Checkbox(checked = shown, onCheckedChange = null) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun <T> List<Pair<T, String>>.label(value: T): String = firstOrNull { it.first == value }?.second.orEmpty()
|
private fun <T> List<Pair<T, String>>.label(value: T): String = firstOrNull { it.first == value }?.second.orEmpty()
|
||||||
|
|
||||||
/** A setting with a few named choices: its current one shown, the rest a tap away. */
|
/** A setting with a few named choices: its current one shown, the rest a tap away. */
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ internal fun SettingsRow(
|
|||||||
icon: ImageVector? = null,
|
icon: ImageVector? = null,
|
||||||
tint: Color = MaterialTheme.colorScheme.primary,
|
tint: Color = MaterialTheme.colorScheme.primary,
|
||||||
titleColor: Color = MaterialTheme.colorScheme.onSurface,
|
titleColor: Color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
leading: (@Composable () -> Unit)? = null,
|
||||||
trailing: (@Composable () -> Unit)? = null,
|
trailing: (@Composable () -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
@@ -99,6 +100,7 @@ internal fun SettingsRow(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
) {
|
) {
|
||||||
if (icon != null) Icon(icon, contentDescription = null, tint = tint, modifier = Modifier.size(24.dp))
|
if (icon != null) Icon(icon, contentDescription = null, tint = tint, modifier = Modifier.size(24.dp))
|
||||||
|
leading?.invoke()
|
||||||
Column(Modifier.weight(1f)) {
|
Column(Modifier.weight(1f)) {
|
||||||
Text(title, style = MaterialTheme.typography.bodyLarge, color = titleColor)
|
Text(title, style = MaterialTheme.typography.bodyLarge, color = titleColor)
|
||||||
subtitle?.let {
|
subtitle?.let {
|
||||||
|
|||||||
@@ -148,6 +148,9 @@
|
|||||||
<string name="cert_kind_self">Self-signed: %1$s</string>
|
<string name="cert_kind_self">Self-signed: %1$s</string>
|
||||||
|
|
||||||
<!-- Car settings on the phone -->
|
<!-- Car settings on the phone -->
|
||||||
|
<string name="car_apps_sub">Choose which apps appear in the car</string>
|
||||||
|
<string name="car_apps_finding">Finding apps…</string>
|
||||||
|
<string name="car_apps_none">No car apps found</string>
|
||||||
<string name="car_group_screen">Screen</string>
|
<string name="car_group_screen">Screen</string>
|
||||||
<string name="car_group_nav_bar">Nav bar and player</string>
|
<string name="car_group_nav_bar">Nav bar and player</string>
|
||||||
<string name="car_group_sound">Sound and voice</string>
|
<string name="car_group_sound">Sound and voice</string>
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import app.seb3thehacker.gearslip.notify.MessagingApp
|
||||||
|
import app.seb3thehacker.gearslip.ui.CarGroup
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class HiddenAppsTest {
|
||||||
|
private val context = InMemoryContext()
|
||||||
|
|
||||||
|
private fun app(name: String) = Entry(name, icon = null, messaging = MessagingApp(name, name))
|
||||||
|
private fun id(name: String) = MESSAGING_PREFIX + name
|
||||||
|
private fun labels(entries: List<Entry>) = entries.map { it.label }
|
||||||
|
|
||||||
|
private val apps = listOf(app("signal"), app("telegram"), app("whatsapp"))
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun startWithNothingSaved() = CarSettings.init(context)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun everyTileShowsUntilUnticked() {
|
||||||
|
assertEquals(emptySet<String>(), CarSettings.hiddenApps.value)
|
||||||
|
assertEquals(apps.map { it.label }, labels(launcherOrder(apps, false, emptyList(), CarSettings.hiddenApps.value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun anUntickedTileLeavesTheDrawer() {
|
||||||
|
val shown = launcherOrder(apps, false, emptyList(), setOf(id("telegram")))
|
||||||
|
assertEquals(listOf("signal", "whatsapp"), labels(shown))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun gearslipsOwnTilesStayWhenInstalledOnesAreHidden() {
|
||||||
|
val all = BuiltInApps.everyEntry + apps
|
||||||
|
val shown = launcherOrder(all, false, emptyList(), apps.mapNotNull { it.componentId }.toSet())
|
||||||
|
assertTrue(shown.isNotEmpty())
|
||||||
|
assertTrue(shown.all { it.builtIn != null })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun aHiddenTileLeavesTheDriversOrderIntactForTheRest() {
|
||||||
|
val custom = listOf(id("whatsapp"), id("telegram"), id("signal"))
|
||||||
|
val shown = launcherOrder(apps, false, custom, setOf(id("telegram")))
|
||||||
|
assertEquals(listOf("whatsapp", "signal"), labels(shown))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun aTileComesBackWhenTickedAgain() {
|
||||||
|
CarSettings.setAppHidden(id("signal"), true)
|
||||||
|
CarSettings.setAppHidden(id("telegram"), true)
|
||||||
|
CarSettings.setAppHidden(id("signal"), false)
|
||||||
|
assertEquals(setOf(id("telegram")), CarSettings.hiddenApps.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun theChoicesSurviveARestart() {
|
||||||
|
CarSettings.setAppHidden(id("signal"), true)
|
||||||
|
CarSettings.setAppHidden(id("whatsapp"), true)
|
||||||
|
CarSettings.init(context)
|
||||||
|
assertEquals(setOf(id("signal"), id("whatsapp")), CarSettings.hiddenApps.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun appsIsTheFirstCardOnThePhone() {
|
||||||
|
assertEquals(CarGroup.APPS, CarGroup.entries.first())
|
||||||
|
assertFalse(CarGroup.entries.drop(1).contains(CarGroup.APPS))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import android.content.ContextWrapper
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import java.lang.reflect.Proxy
|
||||||
|
|
||||||
|
/** Just enough Context for [CarSettings]: strings kept in a map, so a second init reads them back. */
|
||||||
|
class InMemoryContext : ContextWrapper(null) {
|
||||||
|
val stored = mutableMapOf<String, String>()
|
||||||
|
|
||||||
|
private val editor: SharedPreferences.Editor = proxy { self, name, args ->
|
||||||
|
if (name == "putString") stored[args[0] as String] = args[1] as String
|
||||||
|
self.takeIf { name == "putString" }
|
||||||
|
}
|
||||||
|
|
||||||
|
private val preferences: SharedPreferences = proxy { _, name, args ->
|
||||||
|
when (name) {
|
||||||
|
"edit" -> editor
|
||||||
|
"getString" -> stored[args[0] as String] ?: args[1]
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getApplicationContext() = this
|
||||||
|
|
||||||
|
override fun getSharedPreferences(name: String?, mode: Int) = preferences
|
||||||
|
|
||||||
|
private inline fun <reified T> proxy(crossinline call: (Any, String, Array<out Any?>) -> Any?): T =
|
||||||
|
Proxy.newProxyInstance(T::class.java.classLoader, arrayOf(T::class.java)) { self, method, args ->
|
||||||
|
call(self, method.name, args.orEmpty())
|
||||||
|
} as T
|
||||||
|
}
|
||||||
Reference in new issue
Block a user