Skip to content

Commit 8bc1d14

Browse files
committed
Show app names in shared pickers and switch prompts
- Share the full-screen app picker and reusable name rows across launch actions, scanning rules, and favourites - Resolve app labels centrally with localized unavailable and unnamed fallbacks instead of package identifiers - Format configured switch actions with app names in lists, fields, and the hold HUD - Cover name-only search, favourite exclusions, label fallbacks, and long-name emulator fixtures 🤖 Auto-generated
1 parent 4ef8c05 commit 8bc1d14

18 files changed

Lines changed: 242 additions & 289 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
2+
<application android:label="A very long app name for checking accessible app selection and favourite rows">
3+
<activity android:name="com.enaboapps.switchify.AppLabelTestActivity" android:exported="true">
4+
<intent-filter>
5+
<action android:name="android.intent.action.MAIN" />
6+
<category android:name="android.intent.category.LAUNCHER" />
7+
</intent-filter>
8+
</activity>
9+
</application>
10+
</manifest>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.enaboapps.switchify
2+
3+
import android.app.Activity
4+
5+
class AppLabelTestActivity : Activity()

app/src/androidTest/java/com/enaboapps/switchify/LaunchAppTest.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.enaboapps.switchify
33
import androidx.test.ext.junit.runners.AndroidJUnit4
44
import androidx.test.platform.app.InstrumentationRegistry
55
import com.enaboapps.switchify.service.utils.AppLauncher
6+
import com.enaboapps.switchify.service.utils.AppLabelResolver
67
import com.enaboapps.switchify.service.utils.FavouriteAppsManager
78
import com.enaboapps.switchify.switches.SwitchAction
89
import com.enaboapps.switchify.switches.SwitchEvent
@@ -16,6 +17,21 @@ import org.junit.runner.RunWith
1617

1718
@RunWith(AndroidJUnit4::class)
1819
class LaunchAppTest {
20+
@Test fun configuredActionNamesUseAppLabelsWithoutPackageFallbacks() {
21+
val context = InstrumentationRegistry.getInstrumentation().targetContext
22+
val resolver = AppLabelResolver(context)
23+
val longName = "A very long app name for checking accessible app selection and favourite rows"
24+
assertEquals(longName, resolver.label(InstrumentationRegistry.getInstrumentation().context.packageName))
25+
assertEquals("Chrome", resolver.label("com.android.chrome"))
26+
assertEquals("Launch Chrome", SwitchAction(19, "com.android.chrome").getDisplayName(context))
27+
assertEquals("Launch Settings", SwitchAction(19, "com.android.settings").getDisplayName(context))
28+
assertEquals(context.getString(R.string.unavailable_app), resolver.label("invalid.missing.app"))
29+
assertEquals(context.getString(R.string.action_launch_app_target, context.getString(R.string.unavailable_app)),
30+
SwitchAction(19, "invalid.missing.app").getDisplayName(context))
31+
assertEquals(SwitchAction(19).getActionName(), SwitchAction(19).getDisplayName(context))
32+
assertEquals(SwitchAction(1).getActionName(), SwitchAction(1).getDisplayName(context))
33+
}
34+
1935
@Test fun launchTargetsSurviveStorageReloadAndOpenInstalledApps() = runBlocking {
2036
val context = InstrumentationRegistry.getInstrumentation().targetContext
2137
val persistence = SwitchProfileLocalPersistence(context)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.enaboapps.switchify.components
2+
3+
import androidx.compose.foundation.clickable
4+
import androidx.compose.foundation.layout.Arrangement
5+
import androidx.compose.foundation.layout.BoxWithConstraints
6+
import androidx.compose.foundation.layout.Column
7+
import androidx.compose.foundation.layout.Row
8+
import androidx.compose.foundation.layout.fillMaxWidth
9+
import androidx.compose.foundation.layout.padding
10+
import androidx.compose.material3.MaterialTheme
11+
import androidx.compose.material3.Text
12+
import androidx.compose.runtime.Composable
13+
import androidx.compose.ui.Alignment
14+
import androidx.compose.ui.Modifier
15+
import androidx.compose.ui.unit.dp
16+
17+
@Composable
18+
fun AppNameRow(
19+
name: String,
20+
modifier: Modifier = Modifier,
21+
onClick: (() -> Unit)? = null,
22+
leadingContent: (@Composable () -> Unit)? = null,
23+
trailingContent: (@Composable () -> Unit)? = null
24+
) {
25+
BoxWithConstraints(modifier.fillMaxWidth().then(
26+
if (onClick == null) Modifier else Modifier.clickable(onClick = onClick)
27+
).padding(horizontal = 16.dp, vertical = 12.dp)) {
28+
if (maxWidth < 360.dp && leadingContent != null) {
29+
Column {
30+
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween,
31+
verticalAlignment = Alignment.CenterVertically) {
32+
leadingContent()
33+
trailingContent?.invoke()
34+
}
35+
Text(name, style = MaterialTheme.typography.bodyLarge,
36+
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp))
37+
}
38+
} else {
39+
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
40+
leadingContent?.invoke()
41+
Text(name, style = MaterialTheme.typography.bodyLarge,
42+
modifier = Modifier.weight(1f).padding(horizontal = 8.dp))
43+
trailingContent?.invoke()
44+
}
45+
}
46+
}
47+
}
Lines changed: 66 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,101 @@
11
package com.enaboapps.switchify.components
22

3-
import androidx.compose.foundation.clickable
3+
import androidx.compose.foundation.layout.Arrangement
44
import androidx.compose.foundation.layout.Column
5+
import androidx.compose.foundation.layout.Row
6+
import androidx.compose.foundation.layout.fillMaxSize
57
import androidx.compose.foundation.layout.fillMaxWidth
6-
import androidx.compose.foundation.layout.heightIn
78
import androidx.compose.foundation.layout.padding
89
import androidx.compose.foundation.lazy.LazyColumn
910
import androidx.compose.foundation.lazy.items
10-
import androidx.compose.material3.AlertDialog
11+
import androidx.compose.material.icons.Icons
12+
import androidx.compose.material.icons.filled.Close
13+
import androidx.compose.material.icons.filled.Search
1114
import androidx.compose.material3.CircularProgressIndicator
12-
import androidx.compose.material3.OutlinedTextField
15+
import androidx.compose.material3.HorizontalDivider
16+
import androidx.compose.material3.Icon
17+
import androidx.compose.material3.IconButton
18+
import androidx.compose.material3.MaterialTheme
19+
import androidx.compose.material3.Surface
1320
import androidx.compose.material3.Text
14-
import androidx.compose.material3.TextButton
1521
import androidx.compose.runtime.Composable
1622
import androidx.compose.runtime.LaunchedEffect
1723
import androidx.compose.runtime.getValue
1824
import androidx.compose.runtime.mutableStateOf
1925
import androidx.compose.runtime.remember
2026
import androidx.compose.runtime.saveable.rememberSaveable
2127
import androidx.compose.runtime.setValue
28+
import androidx.compose.ui.Alignment
2229
import androidx.compose.ui.Modifier
2330
import androidx.compose.ui.platform.LocalContext
2431
import androidx.compose.ui.res.stringResource
2532
import androidx.compose.ui.unit.dp
33+
import androidx.compose.ui.window.Dialog
34+
import androidx.compose.ui.window.DialogProperties
2635
import com.enaboapps.switchify.R
2736
import com.enaboapps.switchify.service.utils.FavouriteAppsManager
37+
import com.enaboapps.switchify.service.utils.FavouriteAppsManager.FavouriteApp
38+
39+
internal object AppPickerFilter {
40+
fun filter(apps: List<FavouriteApp>, query: String, excludedPackages: Set<String>): List<FavouriteApp> =
41+
apps.filter { it.packageName !in excludedPackages && it.appName.contains(query.trim(), true) }
42+
}
2843

2944
@Composable
30-
fun LaunchableAppPicker(onDismiss: () -> Unit, onSelect: (String) -> Unit) {
45+
fun LaunchableAppPicker(
46+
onDismiss: () -> Unit,
47+
onSelect: (String) -> Unit,
48+
titleResId: Int = R.string.choose_app,
49+
excludedPackages: Set<String> = emptySet()
50+
) {
3151
val context = LocalContext.current
32-
var apps by remember { mutableStateOf<List<FavouriteAppsManager.FavouriteApp>?>(null) }
52+
var apps by remember { mutableStateOf<List<FavouriteApp>?>(null) }
3353
var query by rememberSaveable { mutableStateOf("") }
3454
LaunchedEffect(context) {
3555
apps = FavouriteAppsManager(context).getAllLaunchableApps()
3656
}
37-
AlertDialog(
38-
onDismissRequest = onDismiss,
39-
title = { Text(stringResource(R.string.choose_app)) },
40-
confirmButton = {},
41-
dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(android.R.string.cancel)) } },
42-
text = {
43-
Column {
44-
OutlinedTextField(value = query, onValueChange = { query = it },
45-
label = { Text(stringResource(R.string.search_apps)) }, modifier = Modifier.fillMaxWidth())
46-
val matches = apps?.filter {
47-
it.appName.contains(query, true) || it.packageName.contains(query, true)
57+
val matches = remember(apps, query, excludedPackages) {
58+
apps?.let { AppPickerFilter.filter(it, query, excludedPackages) }
59+
}
60+
Dialog(onDismissRequest = onDismiss, properties = DialogProperties(usePlatformDefaultWidth = false)) {
61+
Surface(
62+
modifier = Modifier.fillMaxSize().padding(16.dp),
63+
shape = MaterialTheme.shapes.large,
64+
tonalElevation = 6.dp
65+
) {
66+
Column(Modifier.fillMaxSize()) {
67+
Row(Modifier.fillMaxWidth().padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
68+
Text(stringResource(titleResId), modifier = Modifier.weight(1f),
69+
style = MaterialTheme.typography.titleLarge)
70+
IconButton(onClick = onDismiss) {
71+
Icon(Icons.Default.Close, contentDescription = stringResource(R.string.button_close))
72+
}
4873
}
49-
if (matches == null) CircularProgressIndicator()
50-
else if (matches.isEmpty()) Text(stringResource(R.string.no_matching_apps))
51-
else LazyColumn(Modifier.heightIn(max = 400.dp)) {
52-
items(matches, key = { it.packageName }) { app ->
53-
Column(Modifier.fillMaxWidth().clickable { onSelect(app.packageName) }.padding(12.dp)) {
54-
Text(app.appName)
55-
Text(app.packageName)
74+
SwitchifyTextField(
75+
value = query,
76+
onValueChange = { query = it },
77+
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
78+
placeholder = { Text(stringResource(R.string.search_apps)) },
79+
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
80+
singleLine = true
81+
)
82+
if (matches == null) {
83+
Column(Modifier.fillMaxSize().padding(32.dp),
84+
horizontalAlignment = Alignment.CenterHorizontally,
85+
verticalArrangement = Arrangement.Center) {
86+
CircularProgressIndicator()
87+
}
88+
} else if (matches.isEmpty()) {
89+
Text(stringResource(R.string.no_matching_apps), modifier = Modifier.padding(24.dp))
90+
} else {
91+
LazyColumn(Modifier.weight(1f)) {
92+
items(matches, key = { it.packageName }) { app ->
93+
AppNameRow(name = app.appName, onClick = { onSelect(app.packageName) })
94+
HorizontalDivider()
5695
}
5796
}
5897
}
5998
}
6099
}
61-
)
62-
}
100+
}
101+
}

0 commit comments

Comments
 (0)