From b56bb7f4f43270dd8d019688f6659e70d60a7c7c Mon Sep 17 00:00:00 2001 From: Philip Porto Schiffer Date: Sat, 10 Oct 2026 17:54:11 +0200 Subject: [PATCH 01/11] Add package name patterns in a sheet that previews their matches The old dialog showed the signature icon, had no explanation and gave no feedback on what a pattern would match. The "Add filter" sheet now opens a pattern sheet with a live "N apps match" count, the first matching apps and a message for invalid patterns; Add is only enabled for a valid pattern. Co-Authored-By: Claude Opus 5.5 --- .../psdev/devdrawer/database/PackageFilter.kt | 4 + .../AddPackageNamePackageFilterDialog.kt | 128 -------------- ...PackageNamePackageFilterDialogViewModel.kt | 49 ------ .../profiles/ui/editor/PackagePatternSheet.kt | 92 ++++++++++ .../profiles/ui/editor/WidgetProfileEditor.kt | 37 ++-- .../ui/editor/WidgetProfileEditorViewModel.kt | 21 +++ .../ui/autocomplete/AutoCompleteTextView.kt | 158 ------------------ app/src/main/res/values-de/strings.xml | 6 +- app/src/main/res/values/strings.xml | 6 +- .../devdrawer/DialogStringResourcesTest.kt | 15 +- .../ui/editor/DialogButtonLabelTest.kt | 11 +- .../ui/editor/PackagePatternSheetTest.kt | 88 ++++++++++ .../WidgetProfileEditorViewModelTest.kt | 32 ++++ 13 files changed, 281 insertions(+), 366 deletions(-) delete mode 100644 app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/AddPackageNamePackageFilterDialog.kt delete mode 100644 app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/AddPackageNamePackageFilterDialogViewModel.kt create mode 100644 app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/PackagePatternSheet.kt delete mode 100644 app/src/main/java/de/psdev/devdrawer/ui/autocomplete/AutoCompleteTextView.kt create mode 100644 app/src/test/java/de/psdev/devdrawer/profiles/ui/editor/PackagePatternSheetTest.kt diff --git a/app/src/main/java/de/psdev/devdrawer/database/PackageFilter.kt b/app/src/main/java/de/psdev/devdrawer/database/PackageFilter.kt index 28501974..2b7130cf 100644 --- a/app/src/main/java/de/psdev/devdrawer/database/PackageFilter.kt +++ b/app/src/main/java/de/psdev/devdrawer/database/PackageFilter.kt @@ -37,6 +37,10 @@ data class PackageFilter( @delegate:Ignore private val filterRegex: Regex? by lazy { runCatching { filter.replace("*", ".*").toRegex() }.getOrNull() } + /** A package name filter that is non-blank and compiles; an invalid one matches nothing. */ + val isValidPattern: Boolean + get() = type == FilterType.PACKAGE_NAME && filter.isNotBlank() && filterRegex != null + fun matches(packageHashInfo: PackageHashInfo): Boolean = when (type) { FilterType.PACKAGE_NAME -> filterRegex?.matches(packageHashInfo.packageName) == true FilterType.SIGNATURE -> filter == packageHashInfo.signatureHashSha256 diff --git a/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/AddPackageNamePackageFilterDialog.kt b/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/AddPackageNamePackageFilterDialog.kt deleted file mode 100644 index fed7f656..00000000 --- a/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/AddPackageNamePackageFilterDialog.kt +++ /dev/null @@ -1,128 +0,0 @@ -package de.psdev.devdrawer.profiles.ui.editor - -import android.content.res.Configuration -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import de.psdev.devdrawer.R -import de.psdev.devdrawer.database.PackageFilter -import de.psdev.devdrawer.profiles.ui.editor.AddPackageNamePackageFilterDialogViewModel.ViewState.Error -import de.psdev.devdrawer.profiles.ui.editor.AddPackageNamePackageFilterDialogViewModel.ViewState.Loaded -import de.psdev.devdrawer.profiles.ui.editor.AddPackageNamePackageFilterDialogViewModel.ViewState.Loading -import de.psdev.devdrawer.ui.autocomplete.AutoCompleteTextView -import de.psdev.devdrawer.ui.loading.LoadingView -import de.psdev.devdrawer.ui.theme.DevDrawerTheme - -@Composable -fun AddPackageNamePackageFilterDialog( - currentFilters: List, - viewModel: AddPackageNamePackageFilterDialogViewModel = hiltViewModel(), - closeDialog: () -> Unit = {}, - addFilter: (String) -> Unit = {} -) { - val viewState by remember(viewModel) { viewModel.availablePackageFilters(currentFilters) } - .collectAsState(initial = Loading) - AddPackageNamePackageFilterDialog( - viewState = viewState, - closeDialog = closeDialog, - addFilter = addFilter - ) -} - -@Composable -internal fun AddPackageNamePackageFilterDialog( - viewState: AddPackageNamePackageFilterDialogViewModel.ViewState, - closeDialog: () -> Unit = {}, - addFilter: (String) -> Unit = {} -) { - Dialog( - onDismissRequest = closeDialog, - properties = DialogProperties(dismissOnClickOutside = false) - ) { - Surface(modifier = Modifier.padding(16.dp), color = MaterialTheme.colorScheme.surface, shape = MaterialTheme.shapes.medium) { - Column(modifier = Modifier.padding(8.dp)) { - Row(modifier = Modifier.padding(8.dp), verticalAlignment = Alignment.CenterVertically) { - Icon(painter = painterResource(id = R.drawable.ic_certificate), contentDescription = stringResource(id = R.string.app_signature)) - Spacer(modifier = Modifier.size(4.dp)) - Text(modifier = Modifier.weight(1f), text = stringResource(id = R.string.enter_package_name_filter)) - } - Spacer(modifier = Modifier.size(4.dp)) - HorizontalDivider() - Box( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp) - ) { - when (viewState) { - Loading -> LoadingView( - modifier = Modifier - .align(Alignment.Center) - .padding(16.dp), showText = false - ) - is Loaded -> { - Column { - var text by remember { mutableStateOf("") } - AutoCompleteTextView( - options = viewState.data, - label = { Text(text = stringResource(id = R.string.packagefilter)) }, - onTextChanged = { text = it } - ) - Spacer(modifier = Modifier.size(8.dp)) - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { - TextButton(onClick = closeDialog) { - Text(text = stringResource(id = R.string.cancel)) - } - TextButton(onClick = { addFilter(text) }, enabled = text.isNotBlank()) { - Text(text = stringResource(id = R.string.add)) - } - } - } - } - is Error -> Text(text = stringResource(id = R.string.error_message, viewState.message)) - } - } - } - } - } -} - -@Preview(showSystemUi = true) -@Preview(showSystemUi = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -fun Preview_AddPackageNamePackageFilterDialog() { - DevDrawerTheme { - Surface { - AddPackageNamePackageFilterDialog( - viewState = Loaded( - (1..6).map { "com.example.$it" } - ) - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/AddPackageNamePackageFilterDialogViewModel.kt b/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/AddPackageNamePackageFilterDialogViewModel.kt deleted file mode 100644 index bf9ec008..00000000 --- a/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/AddPackageNamePackageFilterDialogViewModel.kt +++ /dev/null @@ -1,49 +0,0 @@ -package de.psdev.devdrawer.profiles.ui.editor - -import androidx.lifecycle.ViewModel -import dagger.hilt.android.lifecycle.HiltViewModel -import de.psdev.devdrawer.apps.AppsService -import de.psdev.devdrawer.database.PackageFilter -import kotlinx.coroutines.flow.flow -import java.text.Collator -import javax.inject.Inject - -@HiltViewModel -class AddPackageNamePackageFilterDialogViewModel @Inject constructor( - private val appsService: AppsService -): ViewModel() { - - fun availablePackageFilters(currentFilters: List) = flow { - val packageNameFilters = appsService.getInstalledPackages() - .filter { currentFilters.none { packageFilter -> packageFilter.matches(it) } } - .map { it.packageName } - .splitIntoFilters() - emit(ViewState.Loaded(packageNameFilters)) - } - - private fun List.splitIntoFilters(): List { - val appSet = mutableSetOf() - forEach { packageName -> - var tempPackageName = packageName - appSet.add(tempPackageName) - while (tempPackageName.isNotEmpty()) { - val lastIndex = tempPackageName.lastIndexOf(".") - if (lastIndex > 0) { - tempPackageName = tempPackageName.substring(0, lastIndex) - appSet.add("$tempPackageName.*") - } else { - tempPackageName = "" - } - } - } - return appSet.toList().sortedWith(Collator.getInstance()) - } - - sealed class ViewState { - data object Loading : ViewState() - - data class Loaded(val data: List): ViewState() - data class Error(val message: String): ViewState() - } - -} \ No newline at end of file diff --git a/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/PackagePatternSheet.kt b/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/PackagePatternSheet.kt new file mode 100644 index 00000000..1d1bd396 --- /dev/null +++ b/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/PackagePatternSheet.kt @@ -0,0 +1,92 @@ +package de.psdev.devdrawer.profiles.ui.editor + +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.foundation.text.KeyboardOptions +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import de.psdev.devdrawer.R +import de.psdev.devdrawer.appwidget.AppInfo + +/** What a package name pattern being typed would match. */ +data class PatternPreview( + val pattern: String = "", + val isValid: Boolean = false, + val matchCount: Int = 0, + /** The first matching apps, by name. */ + val apps: List = emptyList() +) + +/** Enter a package name pattern and see which installed apps it matches before adding it. */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PackagePatternSheet( + preview: PatternPreview, + onPatternChange: (String) -> Unit, + onAdd: (String) -> Unit, + onDismiss: () -> Unit +) { + ModalBottomSheet(onDismissRequest = onDismiss, sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)) { + Column( + modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text(text = stringResource(R.string.setup_source_pattern), style = MaterialTheme.typography.titleLarge) + Text( + text = stringResource(R.string.add_filter_pattern_text), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + val invalid = preview.pattern.isNotBlank() && !preview.isValid + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = preview.pattern, + onValueChange = onPatternChange, + singleLine = true, + isError = invalid, + label = { Text(stringResource(R.string.packagefilter)) }, + placeholder = { Text(stringResource(R.string.setup_source_pattern_summary)) }, + supportingText = when { + invalid -> { + { Text(stringResource(R.string.pattern_invalid)) } + } + preview.pattern.isNotBlank() -> { + { Text(pluralStringResource(R.plurals.pattern_match_count, preview.matchCount, preview.matchCount)) } + } + else -> null + }, + keyboardOptions = KeyboardOptions(autoCorrectEnabled = false, keyboardType = KeyboardType.Uri) + ) + preview.apps.forEach { AppInfoItem(appInfo = it) } + val hidden = preview.matchCount - preview.apps.size + if (hidden > 0) { + Text( + text = pluralStringResource(R.plurals.widget_more_apps, hidden, hidden), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Button( + modifier = Modifier.align(Alignment.End), + enabled = preview.isValid, + onClick = { onAdd(preview.pattern.trim()) } + ) { + Text(stringResource(R.string.add)) + } + } + } +} diff --git a/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/WidgetProfileEditor.kt b/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/WidgetProfileEditor.kt index 4bb95c2e..1577d89f 100644 --- a/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/WidgetProfileEditor.kt +++ b/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/WidgetProfileEditor.kt @@ -109,7 +109,10 @@ fun WidgetProfileEditor( WidgetProfileEditorDialogs.AddFilter -> AddFilterSheet( onDismiss = { currentDialog = WidgetProfileEditorDialogs.None }, onSignatureClick = { currentDialog = WidgetProfileEditorDialogs.AddAppSignaturePackageFilter(viewState.packageFilters) }, - onPatternClick = { currentDialog = WidgetProfileEditorDialogs.AddPackageNamePackageFilter(viewState.packageFilters) } + onPatternClick = { + viewModel.onPatternChanged("") + currentDialog = WidgetProfileEditorDialogs.AddPackagePattern + } ) is WidgetProfileEditorDialogs.AddAppSignaturePackageFilter -> AddAppSignatureFilterSheet( currentFilters = dialog.currentPackageFilters, @@ -128,22 +131,20 @@ fun WidgetProfileEditor( currentDialog = WidgetProfileEditorDialogs.None } ) - is WidgetProfileEditorDialogs.AddPackageNamePackageFilter -> AddPackageNamePackageFilterDialog( - currentFilters = dialog.currentPackageFilters, - closeDialog = { - currentDialog = WidgetProfileEditorDialogs.None - }, - addFilter = { packageNameFilter -> - viewModel.addPackageFilter( - PackageFilter( - type = FilterType.PACKAGE_NAME, - filter = packageNameFilter, - profileId = viewState.widgetProfile?.id.orEmpty() + WidgetProfileEditorDialogs.AddPackagePattern -> { + val preview by viewModel.patternPreview.collectAsState() + PackagePatternSheet( + preview = preview, + onPatternChange = viewModel::onPatternChanged, + onAdd = { pattern -> + viewModel.addPackageFilter( + PackageFilter(type = FilterType.PACKAGE_NAME, filter = pattern, profileId = viewState.widgetProfile?.id.orEmpty()) ) - ) - currentDialog = WidgetProfileEditorDialogs.None - } - ) + currentDialog = WidgetProfileEditorDialogs.None + }, + onDismiss = { currentDialog = WidgetProfileEditorDialogs.None } + ) + } is WidgetProfileEditorDialogs.PackageFilterInfo -> PackageFilterInfoDialog( packageFilter = dialog.packageFilter, onDismiss = { @@ -156,9 +157,7 @@ fun WidgetProfileEditor( private sealed class WidgetProfileEditorDialogs { data object None : WidgetProfileEditorDialogs() data object AddFilter : WidgetProfileEditorDialogs() - data class AddPackageNamePackageFilter( - val currentPackageFilters: List - ) : WidgetProfileEditorDialogs() + data object AddPackagePattern : WidgetProfileEditorDialogs() data class AddAppSignaturePackageFilter( val currentPackageFilters: List diff --git a/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/WidgetProfileEditorViewModel.kt b/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/WidgetProfileEditorViewModel.kt index e7640692..65d09fe2 100644 --- a/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/WidgetProfileEditorViewModel.kt +++ b/app/src/main/java/de/psdev/devdrawer/profiles/ui/editor/WidgetProfileEditorViewModel.kt @@ -13,6 +13,7 @@ import de.psdev.devdrawer.appwidget.AppInfo import de.psdev.devdrawer.appwidget.PackageHashInfo import de.psdev.devdrawer.appwidget.SortOrder import de.psdev.devdrawer.database.DevDrawerDatabase +import de.psdev.devdrawer.database.FilterType import de.psdev.devdrawer.database.PackageFilter import de.psdev.devdrawer.profiles.IPackageFilterRepository import kotlinx.coroutines.Job @@ -20,6 +21,7 @@ import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -73,6 +75,24 @@ class WidgetProfileEditorViewModel @AssistedInject constructor( ) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), WidgetProfileEditorViewState.Empty) + private val patternState = MutableStateFlow("") + + /** What the package name pattern being entered would match; recomputed as it is typed. */ + val patternPreview: StateFlow = combine(patternState, installedPackages) { pattern, packages -> + val filter = PackageFilter(type = FilterType.PACKAGE_NAME, filter = pattern.trim(), profileId = profileId) + val matched = if (filter.isValidPattern) packages.matching(listOf(filter)) else emptyList() + PatternPreview( + pattern = pattern, + isValid = filter.isValidPattern, + matchCount = matched.size, + apps = appsService.appInfos(matched).sortedWith(SortOrder.NAME.comparator()).take(PATTERN_PREVIEW_APP_COUNT) + ) + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), PatternPreview()) + + fun onPatternChanged(pattern: String) { + patternState.value = pattern + } + fun onNameChanged(name: String) { widgetNameState.value = name pendingNameSave?.cancel() @@ -125,5 +145,6 @@ class WidgetProfileEditorViewModel @AssistedInject constructor( private companion object { const val NAME_SAVE_DELAY_MS = 500L + const val PATTERN_PREVIEW_APP_COUNT = 5 } } diff --git a/app/src/main/java/de/psdev/devdrawer/ui/autocomplete/AutoCompleteTextView.kt b/app/src/main/java/de/psdev/devdrawer/ui/autocomplete/AutoCompleteTextView.kt deleted file mode 100644 index 72ecc28b..00000000 --- a/app/src/main/java/de/psdev/devdrawer/ui/autocomplete/AutoCompleteTextView.kt +++ /dev/null @@ -1,158 +0,0 @@ -package de.psdev.devdrawer.ui.autocomplete - -import android.content.res.Configuration -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsFocusedAsState -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredWidth -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Clear -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Surface -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.geometry.Offset -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.layout.positionInParent -import androidx.compose.ui.layout.positionInWindow -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Popup -import de.psdev.devdrawer.R -import de.psdev.devdrawer.ui.theme.DevDrawerTheme -import kotlin.math.roundToInt - -@Composable -fun AutoCompleteTextView( - modifier: Modifier = Modifier, - options: List, - label: @Composable (() -> Unit)? = null, - onTextChanged: (String) -> Unit = {} -) { - val interactionSource = remember { MutableInteractionSource() } - val textFieldFocused by interactionSource.collectIsFocusedAsState() - var text by remember { mutableStateOf("") } - var textPosition by remember { - mutableStateOf(TextViewPositionData()) - } - SelectionContainer { - OutlinedTextField( - modifier = modifier - .fillMaxWidth() - .onGloballyPositioned { - textPosition = TextViewPositionData( - positionInWindow = it.positionInWindow(), - positionInParent = it.positionInParent(), - size = it.size - ) - }, - value = text, - trailingIcon = { - if (text.isNotBlank()) { - IconButton(onClick = { - text = "" - onTextChanged(text) - }) { - Icon( - imageVector = Icons.Filled.Clear, - contentDescription = stringResource(id = R.string.clear) - ) - } - } - }, - interactionSource = interactionSource, - onValueChange = { - text = it - onTextChanged(text) - }, - label = label, - keyboardOptions = KeyboardOptions(autoCorrectEnabled = false, keyboardType = KeyboardType.Ascii) - ) - } - if (textFieldFocused) { - val textFieldStart = textPosition.positionInParent.x.roundToInt() - val textFieldBottom = (textPosition.positionInParent.y + textPosition.size.height).roundToInt() - val popupOffset = IntOffset(textFieldStart, textFieldBottom) - val popupWidth = with(LocalDensity.current) { textPosition.size.width.toDp() } - val bottom = LocalView.current.height - val popupHeightMax = with(LocalDensity.current) { (bottom - textFieldBottom).toDp() } - Popup( - offset = popupOffset, - ) { - Surface( - modifier = Modifier - .requiredWidth(popupWidth) - .heightIn(max = popupHeightMax), - shadowElevation = 2.dp - ) { - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - ) { - items(options.filter { option -> option.contains(text) && option != text }) { packageName -> - Text( - modifier = Modifier - .fillMaxWidth() - .clickable { - text = packageName - onTextChanged(text) - } - .padding(16.dp), - text = packageName - ) - } - } - } - } - } -} - -data class TextViewPositionData( - val positionInWindow: Offset = Offset.Unspecified, - val positionInParent: Offset = Offset.Unspecified, - val size: IntSize = IntSize.Zero -) - -@Preview(showSystemUi = true) -@Preview(showSystemUi = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -fun Preview_AutoCompleteTextView() { - DevDrawerTheme { - Surface { - Column { - AutoCompleteTextView( - options = listOf( - "com.example.app1", - "com.example.app2", - "com.example.app3", - "com.example.app4", - ), - label = { Text(text = stringResource(id = R.string.packagefilter)) }, - ) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 671c5e9b..f96a295b 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -23,7 +23,6 @@ Play Store Github Paketfilter - Paketnamen-Filter eingeben Signatur aus App auswählen Abbrechen Keine Widgets erstellt @@ -168,4 +167,9 @@ Jede App, die mit demselben Schlüssel signiert ist, passt. Apps suchen Keine App passt zur Suche + Kein gültiges Muster + + %d App passt + %d Apps passen + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 53dcc261..d2763368 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -58,7 +58,6 @@ Delete Package name App icon - Enter package name filter Select signature from app Cancel No widgets created @@ -208,4 +207,9 @@ Every app signed with the same key will match. Search apps No apps match your search + Not a valid pattern + + %d app matches + %d apps match + diff --git a/app/src/test/java/de/psdev/devdrawer/DialogStringResourcesTest.kt b/app/src/test/java/de/psdev/devdrawer/DialogStringResourcesTest.kt index 45488be6..1ad131fc 100644 --- a/app/src/test/java/de/psdev/devdrawer/DialogStringResourcesTest.kt +++ b/app/src/test/java/de/psdev/devdrawer/DialogStringResourcesTest.kt @@ -12,8 +12,8 @@ import de.psdev.devdrawer.profiles.DeleteDialogState import de.psdev.devdrawer.profiles.WidgetInUseErrorAlertDialog import de.psdev.devdrawer.profiles.ui.editor.AddAppSignatureFilterSheet import de.psdev.devdrawer.profiles.ui.editor.AddAppSignaturePackageFilterDialogViewModel -import de.psdev.devdrawer.profiles.ui.editor.AddPackageNamePackageFilterDialog -import de.psdev.devdrawer.profiles.ui.editor.AddPackageNamePackageFilterDialogViewModel +import de.psdev.devdrawer.profiles.ui.editor.PackagePatternSheet +import de.psdev.devdrawer.profiles.ui.editor.PatternPreview import de.psdev.devdrawer.settings.ListPreference import de.psdev.devdrawer.ui.theme.DevDrawerTheme import org.junit.Rule @@ -86,21 +86,24 @@ class DialogStringResourcesTest { } @Test - fun `given a German locale, when a filter dialog fails to load, then the error reads German`() { + fun `given a German locale, when a filter sheet fails to load or a pattern is invalid, then the message reads German`() { // Given / When composeTestRule.setContent { DevDrawerTheme { AddAppSignatureFilterSheet( viewState = AddAppSignaturePackageFilterDialogViewModel.ViewState.Error("kaputt") ) - AddPackageNamePackageFilterDialog( - viewState = AddPackageNamePackageFilterDialogViewModel.ViewState.Error("defekt") + PackagePatternSheet( + preview = PatternPreview(pattern = "com.("), + onPatternChange = {}, + onAdd = {}, + onDismiss = {} ) } } // Then - assertTexts("Fehler: kaputt", "Fehler: defekt") + assertTexts("Fehler: kaputt", "Kein gültiges Muster") } private fun assertTexts(vararg texts: String) { diff --git a/app/src/test/java/de/psdev/devdrawer/profiles/ui/editor/DialogButtonLabelTest.kt b/app/src/test/java/de/psdev/devdrawer/profiles/ui/editor/DialogButtonLabelTest.kt index 62fb06e8..81950cce 100644 --- a/app/src/test/java/de/psdev/devdrawer/profiles/ui/editor/DialogButtonLabelTest.kt +++ b/app/src/test/java/de/psdev/devdrawer/profiles/ui/editor/DialogButtonLabelTest.kt @@ -30,18 +30,21 @@ class DialogButtonLabelTest { ) @Test - fun `given the add package name filter dialog, when loaded, then its buttons are in sentence case`() { + fun `given the package pattern sheet, when shown, then its button is in sentence case`() { // Given / When composeTestRule.setContent { DevDrawerTheme { - AddPackageNamePackageFilterDialog( - viewState = AddPackageNamePackageFilterDialogViewModel.ViewState.Loaded(listOf("com.example.app")) + PackagePatternSheet( + preview = PatternPreview(pattern = "com.example.*", isValid = true), + onPatternChange = {}, + onAdd = {}, + onDismiss = {} ) } } // Then - assertSentenceCase("Cancel", "Add") + assertSentenceCase("Add") } @Test diff --git a/app/src/test/java/de/psdev/devdrawer/profiles/ui/editor/PackagePatternSheetTest.kt b/app/src/test/java/de/psdev/devdrawer/profiles/ui/editor/PackagePatternSheetTest.kt new file mode 100644 index 00000000..629e6fc0 --- /dev/null +++ b/app/src/test/java/de/psdev/devdrawer/profiles/ui/editor/PackagePatternSheetTest.kt @@ -0,0 +1,88 @@ +package de.psdev.devdrawer.profiles.ui.editor + +import android.graphics.drawable.ColorDrawable +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput +import de.psdev.devdrawer.appwidget.AppInfo +import de.psdev.devdrawer.ui.theme.DevDrawerTheme +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +// SDK 36 requires Java 21; use SDK 33 which is compatible with the project's Java 17 toolchain. +@Config(sdk = [33]) +class PackagePatternSheetTest { + + @get:Rule + val composeTestRule = createComposeRule() + + private fun app(name: String, packageName: String) = AppInfo( + name = name, + packageName = packageName, + appIcon = ColorDrawable(0), + firstInstallTime = 0, + lastUpdateTime = 0, + signatureHashSha256 = "key" + ) + + private fun show(preview: PatternPreview, onPatternChange: (String) -> Unit = {}, onAdd: (String) -> Unit = {}) { + composeTestRule.setContent { + DevDrawerTheme { + PackagePatternSheet(preview = preview, onPatternChange = onPatternChange, onAdd = onAdd, onDismiss = {}) + } + } + } + + @Test + fun `given a pattern matching apps, when shown, then it counts and lists them and can be added`() { + // Given + var added: String? = null + val preview = PatternPreview( + pattern = "com.example.*", + isValid = true, + matchCount = 2, + apps = listOf(app("Client", "com.example.client"), app("Other", "com.example.other")) + ) + + // When + show(preview, onAdd = { added = it }) + + // Then + composeTestRule.onNodeWithText("2 apps match").assertExists() + composeTestRule.onNodeWithText("Client").assertExists() + composeTestRule.onNodeWithText("com.example.other", substring = true).assertExists() + composeTestRule.onNodeWithText("Add").assertIsEnabled().performClick() + assertEquals("com.example.*", added) + } + + @Test + fun `given an invalid pattern, when shown, then it says so and cannot be added`() { + // Given / When + show(PatternPreview(pattern = "com.(", isValid = false, matchCount = 0, apps = emptyList())) + + // Then + composeTestRule.onNodeWithText("Not a valid pattern").assertExists() + composeTestRule.onNodeWithText("Add").assertIsNotEnabled() + } + + @Test + fun `given the sheet, when typing, then the pattern is reported`() { + // Given + val typed = mutableListOf() + show(PatternPreview(), onPatternChange = { typed += it }) + + // When + composeTestRule.onNodeWithText("Package filter").performTextInput("com.") + + // Then + assertEquals("com.", typed.last()) + } +} diff --git a/app/src/test/java/de/psdev/devdrawer/profiles/ui/editor/WidgetProfileEditorViewModelTest.kt b/app/src/test/java/de/psdev/devdrawer/profiles/ui/editor/WidgetProfileEditorViewModelTest.kt index beffbe1e..f9b72488 100644 --- a/app/src/test/java/de/psdev/devdrawer/profiles/ui/editor/WidgetProfileEditorViewModelTest.kt +++ b/app/src/test/java/de/psdev/devdrawer/profiles/ui/editor/WidgetProfileEditorViewModelTest.kt @@ -17,6 +17,8 @@ import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test @@ -162,4 +164,34 @@ class WidgetProfileEditorViewModelTest { // Then assertEquals("Mine", database.widgetProfileDao().findById("p1")?.name) } + + @Test + fun `given a package pattern, when typed, then the preview counts and lists the apps it matches`() = runTest { + // Given + val viewModel = createViewModel() + backgroundScope.launch { viewModel.patternPreview.collect {} } + + // When + viewModel.onPatternChanged("com.*") + val preview = viewModel.patternPreview.first { it.matchCount == 2 } + + // Then + assertTrue(preview.isValid) + assertEquals(listOf("Client", "Other"), preview.apps.map { it.name }) + } + + @Test + fun `given an invalid package pattern, when typed, then the preview says so and matches nothing`() = runTest { + // Given + val viewModel = createViewModel() + backgroundScope.launch { viewModel.patternPreview.collect {} } + + // When + viewModel.onPatternChanged("com.(") + val preview = viewModel.patternPreview.first { it.pattern == "com.(" } + + // Then + assertFalse(preview.isValid) + assertEquals(0, preview.matchCount) + } } From 3599421f400f22342e9671bcf321f7780b57cff6 Mon Sep 17 00:00:00 2001 From: Philip Porto Schiffer Date: Sat, 10 Oct 2026 17:56:36 +0200 Subject: [PATCH 02/11] Pick an app's screen from a readable bottom sheet "Ask which screen to open" showed a legacy View dialog listing raw class names, including screens that can't be opened from outside the app. It is now a Compose bottom sheet with the app's icon and name, each screen's label and short class name, and the launcher's screen first and marked. Screens that aren't exported, are disabled or need a permission are left out, and an app with a single screen opens directly. Failures to open show a message instead of crashing. This removes the last View-based UI, so view binding, the chooser layouts and the textured list background go too. Co-Authored-By: Claude Opus 5.5 --- app/build.gradle | 1 - app/src/main/AndroidManifest.xml | 2 +- .../devdrawer/adapters/ActivityListAdapter.kt | 53 --------- .../psdev/devdrawer/appwidget/AppScreens.kt | 97 ++++++++++++++++ .../appwidget/ChooseActivityDialog.kt | 105 +++++++++--------- .../res/drawable-mdpi/background_texture.png | Bin 2737 -> 0 bytes .../res/drawable-night/background_texture.png | Bin 2738 -> 0 bytes .../main/res/drawable/background_repeat.xml | 9 -- app/src/main/res/layout/activity_choice.xml | 14 --- .../res/layout/activity_choice_list_item.xml | 29 ----- app/src/main/res/values-de/strings.xml | 3 + app/src/main/res/values/strings.xml | 3 + app/src/main/res/values/themes.xml | 15 ++- .../devdrawer/appwidget/AppScreensTest.kt | 86 ++++++++++++++ 14 files changed, 251 insertions(+), 166 deletions(-) delete mode 100644 app/src/main/java/de/psdev/devdrawer/adapters/ActivityListAdapter.kt create mode 100644 app/src/main/java/de/psdev/devdrawer/appwidget/AppScreens.kt delete mode 100644 app/src/main/res/drawable-mdpi/background_texture.png delete mode 100644 app/src/main/res/drawable-night/background_texture.png delete mode 100644 app/src/main/res/drawable/background_repeat.xml delete mode 100644 app/src/main/res/layout/activity_choice.xml delete mode 100644 app/src/main/res/layout/activity_choice_list_item.xml create mode 100644 app/src/test/java/de/psdev/devdrawer/appwidget/AppScreensTest.kt diff --git a/app/build.gradle b/app/build.gradle index b1043462..bbebc4d7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -33,7 +33,6 @@ android { } } buildFeatures { - viewBinding true // Required by ChooseActivityDialog (pending Compose migration) compose true buildConfig true } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b6f19dd5..0900c0f2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -60,7 +60,7 @@ android:excludeFromRecents="true" android:noHistory="true" android:taskAffinity="" - android:theme="@style/AppTheme.Dialog.NoActionBar" /> + android:theme="@style/AppTheme.Translucent" /> ): BaseAdapter() { - - private val layoutInflater: LayoutInflater = activity.layoutInflater - - // ========================================================================================================================== - // BaseAdapter - // ========================================================================================================================== - - override fun getCount(): Int = activityList.size - - override fun getItem(position: Int): String = activityList[position] - - override fun getItemId(position: Int): Long = activityList[position].hashCode().toLong() - - override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - val view = convertView ?: createView(parent) - bindView(position, view) - return view - } - - private fun createView(parent: ViewGroup): View { - return layoutInflater.inflate(R.layout.activity_choice_list_item, parent, false).apply { - tag = ViewHolder(this) - } - } - - private fun bindView(position: Int, view: View) { - val viewHolder = view.tag as ViewHolder - viewHolder.run { - txtActivityPath.text = activityList[position].substring(0, activityList[position].lastIndexOf('.')) - txtActivityName.text = activityList[position].substring(activityList[position].lastIndexOf('.'), activityList[position].length) - } - } - - // ========================================================================================================================== - // Inner classes - // ========================================================================================================================== - - class ViewHolder(view: View) { - val txtActivityPath: TextView = view.findViewById(R.id.activityPathTextView) - val txtActivityName: TextView = view.findViewById(R.id.activityNameTextView) - } -} diff --git a/app/src/main/java/de/psdev/devdrawer/appwidget/AppScreens.kt b/app/src/main/java/de/psdev/devdrawer/appwidget/AppScreens.kt new file mode 100644 index 00000000..01a1b02f --- /dev/null +++ b/app/src/main/java/de/psdev/devdrawer/appwidget/AppScreens.kt @@ -0,0 +1,97 @@ +package de.psdev.devdrawer.appwidget + +import android.content.pm.ActivityInfo +import android.graphics.drawable.Drawable +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import de.psdev.devdrawer.R +import de.psdev.devdrawer.ui.AppIcon + +/** One screen of an app that can be opened directly. */ +data class AppScreen( + val className: String, + val label: String, + /** The class name relative to the app's package, like ".settings.SettingsActivity". */ + val shortName: String, + /** The screen the launcher opens. */ + val isMain: Boolean +) + +/** + * The screens of [packageName] that another app may open: exported, enabled and not behind a permission. + * The launcher's screen comes first, then the rest by label. + */ +fun appScreens( + packageName: String, + activities: List, + mainClassName: String?, + labelOf: (ActivityInfo) -> String +): List = activities + .filter { it.exported && it.enabled && it.permission == null } + .map { activity -> + AppScreen( + className = activity.name, + label = labelOf(activity), + shortName = if (activity.name.startsWith("$packageName.")) activity.name.removePrefix(packageName) else activity.name, + isMain = activity.name == mainClassName + ) + } + .sortedWith(compareByDescending { it.isMain }.thenBy(String.CASE_INSENSITIVE_ORDER) { it.label }) + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ChooseScreenSheet( + appName: String, + appIcon: Drawable?, + screens: List, + onScreenSelected: (AppScreen) -> Unit, + onDismiss: () -> Unit +) { + ModalBottomSheet(onDismissRequest = onDismiss) { + Row( + modifier = Modifier.padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + AppIcon(icon = appIcon, size = 40.dp) + Text(text = stringResource(R.string.open_app_screen, appName), style = MaterialTheme.typography.titleLarge) + } + LazyColumn(modifier = Modifier.padding(vertical = 8.dp)) { + items(screens, key = { it.className }) { screen -> + Column( + modifier = Modifier + .fillMaxWidth() + .clickable { onScreenSelected(screen) } + .padding(horizontal = 16.dp, vertical = 12.dp) + ) { + Text( + text = screen.label, + style = MaterialTheme.typography.bodyLarge, + fontWeight = if (screen.isMain) FontWeight.Medium else null + ) + Text( + text = if (screen.isMain) stringResource(R.string.main_screen, screen.shortName) else screen.shortName, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + } +} diff --git a/app/src/main/java/de/psdev/devdrawer/appwidget/ChooseActivityDialog.kt b/app/src/main/java/de/psdev/devdrawer/appwidget/ChooseActivityDialog.kt index aeacc3fb..b0be11e9 100644 --- a/app/src/main/java/de/psdev/devdrawer/appwidget/ChooseActivityDialog.kt +++ b/app/src/main/java/de/psdev/devdrawer/appwidget/ChooseActivityDialog.kt @@ -1,22 +1,22 @@ package de.psdev.devdrawer.appwidget -import android.app.Activity +import android.content.ActivityNotFoundException import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.os.Bundle -import android.view.View -import android.widget.AdapterView import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent import de.psdev.devdrawer.R -import de.psdev.devdrawer.adapters.ActivityListAdapter -import de.psdev.devdrawer.databinding.ActivityChoiceBinding +import de.psdev.devdrawer.ui.theme.DevDrawerTheme import mu.KLogging -class ChooseActivityDialog: Activity(), AdapterView.OnItemClickListener { +/** Lets the user pick which screen of an app to open from the widget; opens it directly when there is only one. */ +class ChooseActivityDialog : ComponentActivity() { - companion object: KLogging() { + companion object : KLogging() { const val EXTRA_PACKAGE_NAME = "packageName" @JvmStatic @@ -25,57 +25,60 @@ class ChooseActivityDialog: Activity(), AdapterView.OnItemClickListener { } } - private lateinit var binding: ActivityChoiceBinding - private val appPackageName: String by lazy { intent.getStringExtra(EXTRA_PACKAGE_NAME)!! } - private lateinit var activitiesList: List - - // ========================================================================================================================== - // Android Lifecycle - // ========================================================================================================================== - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - binding = ActivityChoiceBinding.inflate(layoutInflater) - setContentView(binding.root) + val appPackageName = intent.getStringExtra(EXTRA_PACKAGE_NAME) + val screens = appPackageName?.let(::screensOf) + when { + appPackageName == null || screens == null -> finish() + screens.isEmpty() -> { + Toast.makeText(this, R.string.no_activities, Toast.LENGTH_LONG).show() + finish() + } + screens.size == 1 -> open(appPackageName, screens.single()) + else -> { + val appInfo = packageManager.getApplicationInfo(appPackageName, 0) + val appName = appInfo.loadLabel(packageManager).toString() + val appIcon = appInfo.loadIcon(packageManager) + setContent { + DevDrawerTheme { + ChooseScreenSheet( + appName = appName, + appIcon = appIcon, + screens = screens, + onScreenSelected = { open(appPackageName, it) }, + onDismiss = ::finish + ) + } + } + } + } } - override fun onContentChanged() { - super.onContentChanged() - try { - activitiesList = getActivityList(appPackageName) - } catch (e: PackageManager.NameNotFoundException) { - finish() + private fun screensOf(appPackageName: String): List? = try { + val packageInfo = packageManager.getPackageInfo(appPackageName, PackageManager.GET_ACTIVITIES) + val mainClassName = packageManager.getLaunchIntentForPackage(appPackageName)?.component?.className + appScreens(appPackageName, packageInfo.activities.orEmpty().toList(), mainClassName) { + it.loadLabel(packageManager).toString() } - if (activitiesList.isEmpty()) { - Toast.makeText(this, R.string.no_activities, Toast.LENGTH_LONG).show() - finish() - } - binding.listView.adapter = ActivityListAdapter(this, activitiesList) - binding.listView.onItemClickListener = this + } catch (e: PackageManager.NameNotFoundException) { + null } - // ========================================================================================================================== - // AdapterView.OnItemClickListener - // ========================================================================================================================== - - override fun onItemClick(adapterView: AdapterView<*>, view: View, i: Int, l: Long) { - val intent = Intent() - intent.component = ComponentName(appPackageName, activitiesList[i]) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP - startActivity(intent) + private fun open(appPackageName: String, screen: AppScreen) { + val intent = Intent().apply { + component = ComponentName(appPackageName, screen.className) + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + } + try { + startActivity(intent) + } catch (e: ActivityNotFoundException) { + logger.warn(e) { "Cannot open ${screen.className}" } + Toast.makeText(this, R.string.screen_open_failed, Toast.LENGTH_LONG).show() + } catch (e: SecurityException) { + logger.warn(e) { "Not allowed to open ${screen.className}" } + Toast.makeText(this, R.string.screen_open_failed, Toast.LENGTH_LONG).show() + } finish() } - - // ========================================================================================================================== - // Private API - // ========================================================================================================================== - - @Throws(PackageManager.NameNotFoundException::class) - private fun getActivityList(packageName: String): List { - val packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES) - return packageInfo.activities.orEmpty() - .filter { it.exported } - .map { it.name } - } - } diff --git a/app/src/main/res/drawable-mdpi/background_texture.png b/app/src/main/res/drawable-mdpi/background_texture.png deleted file mode 100644 index 4fb526db5ef39b3ccda5055b872ba2cced4a6bb8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2737 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 diff --git a/app/src/main/res/drawable-night/background_texture.png b/app/src/main/res/drawable-night/background_texture.png deleted file mode 100644 index 60ba775e2943fdf0d983fc01a61b13399c450c1c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2738 zcmV;j3QhHiP)X+uL$Nkc;* zP;zf(X>4Tx07wm;mUmQB*%pV-y*Itk5+Wca^cs2zAksTX6$DXM^`x7XQc?|s+0 z08spb1j2M!0f022SQPH-!CVp(%f$Br7!UytSOLJ{W@ZFO_(THK{JlMynW#v{v-a*T zfMmPdEWc1DbJqWVks>!kBnAKqMb$PuekK>?0+ds;#ThdH1j_W4DKdsJG8Ul;qO2n0 z#IJ1jr{*iW$(WZWsE0n`c;fQ!l&-AnmjxZO1uWyz`0VP>&nP`#itsL#`S=Q!g`M=rU9)45( zJ;-|dRq-b5&z?byo>|{)?5r=n76A4nTALlSzLiw~v~31J<>9PP?;rs31pu_(obw)r zY+jPY;tVGXi|p)da{-@gE-UCa`=5eu%D;v=_nFJ?`&K)q7e9d`Nfk3?MdhZarb|T3 z%nS~f&t(1g5dY)AIcd$w!z`Siz!&j_=v7hZlnI21XuE|xfmo0(WD10T)!}~_HYW!e zew}L+XmwuzeT6wtxJd`dZ#@7*BLgIEKY9Xv>st^p3dp{^Xswa2bB{85{^$B13tWnB z;Y>jyQ|9&zk7RNsqAVGs--K+z0uqo1bf5|}fi5rtEMN^BfHQCd-XH*kfJhJnmIE$G z0%<@5vOzxB0181d*a3EfYH$G5fqKvcPJ%XY23!PJzzuK<41h;K3WmW;Fah3yX$XSw z5EY_9s*o0>51B&N5F1(uc|$=^I1~fLLy3?Ol0f;;Ca4%HgQ}rJP(Ab`bQ-z{U4#0d z2hboi2K@njgb|nm(_szR0JebHusa+GN5aeCM0gdP2N%HG;Yzp`J`T6S7vUT504#-H z!jlL<$Or?`Mpy_N@kBz9SR?@vA#0H$qyni$nvf2p8@Y{0k#Xb$28W?xm>3qu8RLgp zjNxKdVb)?wFx8l2m{v>|<~C*!GlBVnrDD~wrdTJeKXwT=5u1%I#8zOBU|X=4u>;s) z>^mF|$G{ol9B_WP7+f-LHLe7=57&&lfa}8z;U@8Tyei%l?}87(bMRt(A-)QK9Dg3) zj~~XrCy)tR1Z#p1A(kK{Y$Q|=8VKhI{e%(1G*N-5Pjn)N5P8I0VkxnX*g?EW941ba z6iJ387g8iCnY4jaNopcpCOsy-A(P2EWJhusSwLP-t|XrzUnLKcKTwn?CKOLf97RIe zPB}`sKzTrUL#0v;sBY9)s+hW+T2H-1eM)^VN0T#`^Oxhvt&^*fYnAJldnHel*Ozyf zUoM{~Um<@={-*r60#U(0!Bc^wuvVc);k3d%g-J!4qLpHZVwz%!VuRu}#Ze`^l7W)9 z5>Kf>>9Eozr6C$Z)1`URxU@~QI@)F0FdauXr2Es8>BaOP=)Lp_WhG@>R;lZ?BJkMlIuMhw8ApiF&yDYW2hFJ?fJhni{?u z85&g@mo&yT8JcdI$(rSw=QPK(Xj%)k1X|@<=e1rim6`6$RAwc!i#egKuI;BS(LSWz zt39n_sIypSqfWEV6J3%nTQ@-4i zi$R;gsG*9XzhRzXqv2yCs*$VFDx+GXJH|L;wsDH_KI2;^u!)^Xl1YupO;gy^-c(?^ z&$Q1BYvyPsG^;hc$D**@Sy`+`)}T4VJji^bd7Jqw3q6Zii=7tT7GEswEK@D(EFW1Z zSp`^awCb?>!`j4}Yh7b~$A)U-W3$et-R8BesV(1jzwLcHnq9En7Q0Tn&-M=XBKs!$ zF$X<|c!#|X_tWYh)GZit z(Q)Cp9CDE^WG;+fcyOWARoj*0TI>4EP1lX*cEoMO-Pk?Z{kZ!p4@(b`M~lalr<3Oz z&kJ6Nm#vN_+kA5{dW4@^Vjg_`q%qU1ULk& z3Fr!>1V#i_2R;ij2@(Z$1jE4r!MlPVFVbHmT+|iPIq0wy5aS{>yK?9ZAjVh%SOwMWgFjair&;wpi!{CU}&@N=Eg#~ zLQ&zpEzVmGY{hI9Z0+4-0xS$$Xe-OToc?Y*V;rTcf_ zb_jRe-RZjXSeas3UfIyD;9afd%<`i0x4T#DzE)vdabOQ=k7SRuGN`h>O0Q~1)u-yD z>VX=Mn&!Rgd$;YK+Q-}1zu#?t(*cbG#Ronf6db&N$oEidtwC+YVcg-Y!_VuY>bk#Y ze_ww@?MU&F&qswvrN_dLb=5o6*Egs)ls3YRlE$&)amR1{;Ppd$6RYV^Go!iq1UMl% z@#4q$AMc(FJlT1QeX8jv{h#)>&{~RGq1N2iiMFIRX?sk2-|2wUogK~{EkB$8eDsX= znVPf8XG_nK&J~=SIiGia@9y}|z3FhX{g&gcj=lwb=lWgyFW&aLedUh- zof`v-2Kw$UzI*>(+&$@i-u=-BsSjR1%z8NeX#HdC`Hh-Z(6xI-`hmHDqv!v)W&&nrf>M(RhcN6(D;jNN*%^u_SYjF;2ng}*8Ow)d6M ztDk;%`@Lsk$;9w$(d(H%O5UixIr`T2ZRcd@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_choice.xml b/app/src/main/res/layout/activity_choice.xml deleted file mode 100644 index 23426d10..00000000 --- a/app/src/main/res/layout/activity_choice.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_choice_list_item.xml b/app/src/main/res/layout/activity_choice_list_item.xml deleted file mode 100644 index f60521a1..00000000 --- a/app/src/main/res/layout/activity_choice_list_item.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index f96a295b..741f5afa 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -172,4 +172,7 @@ %d App passt %d Apps passen + %1$s öffnen + Hauptbildschirm · %1$s + Dieser Bildschirm lässt sich nicht öffnen diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d2763368..df4f1326 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -212,4 +212,7 @@ %d app matches %d apps match + Open %1$s + Main screen · %1$s + Couldn\'t open this screen diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index abff3ca2..25a32adb 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -28,14 +28,13 @@ @style/Theme.Settings.Preferences -