diff options
| author | Danilo M. <danix@danix.xyz> | 2026-11-04 17:35:40 +0100 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-11-04 17:35:40 +0100 |
| commit | 35b2c6ab50a7b8e141f88f9ae88570ec43e6fa7b (patch) | |
| tree | 3c9b391ea9852270f448f4f0aa2ef69cd3bf40f1 /app/src/main/java | |
| parent | 7f2c9770c701903d6c09e1e6d553e1cd6c974bab (diff) | |
| download | HardenedPass-35b2c6ab50a7b8e141f88f9ae88570ec43e6fa7b.tar.gz HardenedPass-35b2c6ab50a7b8e141f88f9ae88570ec43e6fa7b.zip | |
refactor(app): drop dead Fragment, view and navigation stack helpers
The Compose screens use no Fragments or view binding, so
FragmentExtensions (with its four slide animators) and
FragmentViewBindingDelegate have no users, nor do resolveAttribute,
isPermissionGranted, launchActivity and the unused Modifier.conditional.
SearchableRepositoryViewModel keeps one navigateTo: the Compose list owns
the back stack, so the RecyclerView-era navigation stack, stableId,
isFiltered and the unused DirectoriesOnly mode go. PasswordItem loses
the parent overloads, @JvmStatic and the unread NAME extra, and the
leftover sentry.properties goes too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'app/src/main/java')
6 files changed, 14 insertions, 214 deletions
diff --git a/app/src/main/java/app/passwordstore/data/password/PasswordItem.kt b/app/src/main/java/app/passwordstore/data/password/PasswordItem.kt index 765e886d..999ad050 100644 --- a/app/src/main/java/app/passwordstore/data/password/PasswordItem.kt +++ b/app/src/main/java/app/passwordstore/data/password/PasswordItem.kt @@ -10,13 +10,8 @@ import app.passwordstore.ui.crypto.BasePGPActivity import app.passwordstore.ui.main.LaunchActivity import java.io.File -data class PasswordItem( - val name: String, - val parent: PasswordItem? = null, - val type: Char, - val file: File, - val rootDir: File, -) : Comparable<PasswordItem> { +data class PasswordItem(val name: String, val type: Char, val file: File, val rootDir: File) : + Comparable<PasswordItem> { val fullPathToParent = file.absolutePath.replace(rootDir.absolutePath, "").replace(file.name, "") @@ -41,7 +36,6 @@ data class PasswordItem( /** Creates an [Intent] to launch this [PasswordItem] through the authentication process. */ fun createAuthEnabledIntent(context: Context): Intent { val intent = Intent(context, LaunchActivity::class.java) - intent.putExtra("NAME", toString()) intent.putExtra("FILE_PATH", file.absolutePath) intent.action = LaunchActivity.ACTION_DECRYPT_PASS return intent @@ -52,24 +46,12 @@ data class PasswordItem( const val TYPE_CATEGORY = 'c' const val TYPE_PASSWORD = 'p' - @JvmStatic - fun newCategory(name: String, file: File, parent: PasswordItem, rootDir: File): PasswordItem { - return PasswordItem(name, parent, TYPE_CATEGORY, file, rootDir) - } - - @JvmStatic fun newCategory(name: String, file: File, rootDir: File): PasswordItem { - return PasswordItem(name, null, TYPE_CATEGORY, file, rootDir) - } - - @JvmStatic - fun newPassword(name: String, file: File, parent: PasswordItem, rootDir: File): PasswordItem { - return PasswordItem(name, parent, TYPE_PASSWORD, file, rootDir) + return PasswordItem(name, TYPE_CATEGORY, file, rootDir) } - @JvmStatic fun newPassword(name: String, file: File, rootDir: File): PasswordItem { - return PasswordItem(name, null, TYPE_PASSWORD, file, rootDir) + return PasswordItem(name, TYPE_PASSWORD, file, rootDir) } } } diff --git a/app/src/main/java/app/passwordstore/ui/list/ListDestination.kt b/app/src/main/java/app/passwordstore/ui/list/ListDestination.kt index adae3649..a9228484 100644 --- a/app/src/main/java/app/passwordstore/ui/list/ListDestination.kt +++ b/app/src/main/java/app/passwordstore/ui/list/ListDestination.kt @@ -58,10 +58,9 @@ private enum class ListDialog { /** * The list at [path] (relative to the store root): wires [ListScreen] to the [viewModel], the - * folder picker, the legacy activities and the store operations. [onOpenFolder] takes a path - * relative to the root, [onOpenEntry] an entry file; [onNewEntry] opens the editor in this folder, - * [onClone] the git server screen for a clone, [onSettings] the settings; [onNoStore] runs when the - * store is gone. + * folder picker and the store operations. [onOpenFolder] takes a path relative to the root, + * [onOpenEntry] an entry file; [onNewEntry] opens the editor in this folder, [onClone] the git + * server screen for a clone, [onSettings] the settings; [onNoStore] runs when the store is gone. */ @Composable fun ListDestination( @@ -89,7 +88,7 @@ fun ListDestination( // Declared before the collection below on purpose: effects start in order, so the ViewModel // already points at this folder when the first rows are built (no flash of the store root). LaunchedEffect(path, query) { - viewModel.navigateTo(dir, pushPreviousLocation = false) + viewModel.navigateTo(dir) if (query.isNotBlank()) viewModel.search(query.trim()) } val rows by viewModel.rows.collectAsStateWithLifecycle(ListRows()) @@ -100,7 +99,7 @@ fun ListDestination( if (path.isNotEmpty() && !dir.isDirectory) onOpenFolder("") else viewModel.forceRefresh() } - // Every return to this folder (back from a child, or from a legacy activity) re-reads it. + // Every return to this folder (back from a child, or from another activity) re-reads it. LifecycleResumeEffect(Unit) { if (activity.checkStore()) refresh() else onNoStore() resumes++ diff --git a/app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt b/app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt index bad55de4..6528334d 100644 --- a/app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt +++ b/app/src/main/java/app/passwordstore/util/extensions/AndroidExtensions.kt @@ -8,7 +8,6 @@ package app.passwordstore.util.extensions import android.app.KeyguardManager import android.content.ClipboardManager import android.content.Context -import android.content.Intent import android.content.SharedPreferences import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo @@ -17,12 +16,8 @@ import android.content.pm.PackageManager.ApplicationInfoFlags import android.content.pm.PackageManager.PackageInfoFlags import android.os.Build import android.util.Base64 -import android.util.TypedValue import android.view.View import android.view.autofill.AutofillManager -import androidx.activity.ComponentActivity -import androidx.compose.ui.Modifier -import androidx.core.content.ContextCompat import androidx.core.content.getSystemService import androidx.fragment.app.FragmentActivity import app.passwordstore.BuildConfig @@ -56,13 +51,6 @@ val Context.keyguardManager: KeyguardManager val Context.sharedPrefs: SharedPreferences get() = getSharedPreferences("${BuildConfig.APPLICATION_ID}_preferences", 0) -/** Resolve [attr] from the [Context]'s theme */ -fun Context.resolveAttribute(attr: Int): Int { - val typedValue = TypedValue() - this.theme.resolveAttribute(attr, typedValue, true) - return typedValue.data -} - /** * Commit changes to the store from a [FragmentActivity] using a custom implementation of * [GitOperation] @@ -90,11 +78,6 @@ suspend fun FragmentActivity.commitChange(message: String): Result<Unit, Throwab .execute() } -/** Check if [permission] has been granted to the app. */ -fun FragmentActivity.isPermissionGranted(permission: String): Boolean { - return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED -} - /** Show a [Snackbar] in a [FragmentActivity]. */ fun FragmentActivity.snackbar( view: View = findViewById(android.R.id.content), @@ -106,11 +89,6 @@ fun FragmentActivity.snackbar( return snackbar } -/** Launch an activity denoted by [clazz]. */ -fun <T : ComponentActivity> ComponentActivity.launchActivity(clazz: Class<T>) { - startActivity(Intent(this, clazz).setAction(Intent.ACTION_VIEW)) -} - /** Simplifies the common `getString(key, null) ?: defaultValue` case slightly */ fun SharedPreferences.getString(key: String): String? = getString(key, null) @@ -134,12 +112,3 @@ fun PackageManager.getApplicationInfoCompat(packageName: String, flags: Int): Ap getApplicationInfo(packageName, flags) } } - -/** Allows conditionally applying the given [modifier] if [isEnabled] is `true`. */ -fun Modifier.conditional(isEnabled: Boolean, modifier: Modifier.() -> Modifier): Modifier { - return if (isEnabled) { - then(modifier()) - } else { - this - } -} diff --git a/app/src/main/java/app/passwordstore/util/extensions/FragmentExtensions.kt b/app/src/main/java/app/passwordstore/util/extensions/FragmentExtensions.kt deleted file mode 100644 index 2dd3af0c..00000000 --- a/app/src/main/java/app/passwordstore/util/extensions/FragmentExtensions.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.util.extensions - -import androidx.annotation.IdRes -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager -import androidx.fragment.app.commit -import app.passwordstore.R - -/** Check if [permission] is granted to the app. Aliases to [isPermissionGranted] internally. */ -fun Fragment.isPermissionGranted(permission: String): Boolean { - return requireActivity().isPermissionGranted(permission) -} - -/** Calls `finish()` on the enclosing [androidx.fragment.app.FragmentActivity] */ -fun Fragment.finish() = requireActivity().finish() - -/** - * Perform a [commit] on this [FragmentManager] with custom animations and adding the - * [destinationFragment] to the fragment backstack - */ -fun FragmentManager.performTransactionWithBackStack( - destinationFragment: Fragment, - @IdRes containerViewId: Int = android.R.id.content, -) { - commit { - addToBackStack(destinationFragment.tag) - setCustomAnimations( - R.animator.slide_in_left, - R.animator.slide_out_left, - R.animator.slide_in_right, - R.animator.slide_out_right, - ) - replace(containerViewId, destinationFragment) - } -} diff --git a/app/src/main/java/app/passwordstore/util/extensions/FragmentViewBindingDelegate.kt b/app/src/main/java/app/passwordstore/util/extensions/FragmentViewBindingDelegate.kt deleted file mode 100644 index 0050f2e5..00000000 --- a/app/src/main/java/app/passwordstore/util/extensions/FragmentViewBindingDelegate.kt +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.util.extensions - -import android.view.LayoutInflater -import android.view.View -import androidx.appcompat.app.AppCompatActivity -import androidx.fragment.app.Fragment -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner -import androidx.viewbinding.ViewBinding -import kotlin.properties.ReadOnlyProperty -import kotlin.reflect.KProperty - -/** - * Imported from - * https://medium.com/@Zhuinden/simple-one-liner-viewbinding-in-fragments-and-activities-with-kotlin-961430c6c07c - */ -class FragmentViewBindingDelegate<T : ViewBinding>( - val fragment: Fragment, - val viewBindingFactory: (View) -> T, -) : ReadOnlyProperty<Fragment, T> { - - private var binding: T? = null - - init { - fragment.lifecycle.addObserver( - object : DefaultLifecycleObserver { - override fun onCreate(owner: LifecycleOwner) { - fragment.viewLifecycleOwnerLiveData.observe(fragment) { viewLifecycleOwner -> - viewLifecycleOwner.lifecycle.addObserver( - object : DefaultLifecycleObserver { - override fun onDestroy(owner: LifecycleOwner) { - binding = null - } - } - ) - } - } - } - ) - } - - override fun getValue(thisRef: Fragment, property: KProperty<*>): T { - val binding = binding - if (binding != null) { - return binding - } - - val lifecycle = fragment.viewLifecycleOwner.lifecycle - if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) { - throw IllegalStateException( - "Should not attempt to get bindings when Fragment views are destroyed." - ) - } - - return viewBindingFactory(thisRef.requireView()).also { this.binding = it } - } -} - -fun <T : ViewBinding> Fragment.viewBinding(viewBindingFactory: (View) -> T) = - FragmentViewBindingDelegate(this, viewBindingFactory) - -inline fun <T : ViewBinding> AppCompatActivity.viewBinding( - crossinline bindingInflater: (LayoutInflater) -> T -) = unsafeLazy { bindingInflater.invoke(layoutInflater) } diff --git a/app/src/main/java/app/passwordstore/util/viewmodel/SearchableRepositoryViewModel.kt b/app/src/main/java/app/passwordstore/util/viewmodel/SearchableRepositoryViewModel.kt index 25ea2cf7..5f35644d 100644 --- a/app/src/main/java/app/passwordstore/util/viewmodel/SearchableRepositoryViewModel.kt +++ b/app/src/main/java/app/passwordstore/util/viewmodel/SearchableRepositoryViewModel.kt @@ -6,7 +6,6 @@ package app.passwordstore.util.viewmodel import android.app.Application import android.content.SharedPreferences -import android.os.Parcelable import androidx.annotation.VisibleForTesting import androidx.lifecycle.AndroidViewModel import app.passwordstore.data.password.PasswordItem @@ -68,9 +67,6 @@ private fun PasswordItem.Companion.makeComparator( ) } -val PasswordItem.stableId: String - get() = file.absolutePath - enum class FilterMode { NoFilter, StrictDomain, @@ -85,7 +81,6 @@ enum class SearchMode { enum class ListMode { FilesOnly, - DirectoriesOnly, AllEntries, } @@ -171,7 +166,7 @@ constructor( ) ) - data class SearchResult(val passwordItems: List<PasswordItem>, val isFiltered: Boolean) + data class SearchResult(val passwordItems: List<PasswordItem>) val searchResult = searchActionFlow @@ -187,8 +182,6 @@ constructor( when (searchAction.listMode) { ListMode.FilesOnly -> listResultFlow.filter { it.isFile }.flowOn(dispatcherProvider.io()) - ListMode.DirectoriesOnly -> - listResultFlow.filter { it.isDirectory }.flowOn(dispatcherProvider.io()) ListMode.AllEntries -> listResultFlow } val passwordList = @@ -244,7 +237,7 @@ constructor( .map { it.second } } } - SearchResult(passwordList, isFiltered = searchAction.filterMode != FilterMode.NoFilter) + SearchResult(passwordList) } .flowOn(dispatcherProvider.io()) @@ -288,55 +281,22 @@ constructor( .map { result -> listRows(result.passwordItems.map { it.file }, currentDir.value) } .flowOn(dispatcherProvider.io()) - data class NavigationStackEntry(val dir: File, val recyclerViewState: Parcelable?) - - private val navigationStack = ArrayDeque<NavigationStackEntry>() - - fun navigateTo( - newDirectory: File = root, - listMode: ListMode = ListMode.AllEntries, - recyclerViewState: Parcelable? = null, - pushPreviousLocation: Boolean = true, - ) { + /** Lists [newDirectory]; the Compose list keeps the back stack (one route per folder). */ + fun navigateTo(newDirectory: File) { if (!newDirectory.exists()) return require(newDirectory.isDirectory) { "Can only navigate to a directory" } - if (pushPreviousLocation) { - navigationStack.addFirst(NavigationStackEntry(_currentDir.value, recyclerViewState)) - } searchActionFlow.update { makeSearchAction( filter = "", baseDirectory = newDirectory, filterMode = FilterMode.NoFilter, searchMode = SearchMode.InCurrentDirectoryOnly, - listMode = listMode, + listMode = ListMode.AllEntries, ) } _currentDir.update { newDirectory } } - val canNavigateBack - get() = navigationStack.isNotEmpty() - - /** - * Navigate back to the last location on the [navigationStack] and restore a cached scroll - * position if possible. - * - * Returns the old RecyclerView's LinearLayoutManager state as a [Parcelable] if it was cached. - */ - fun navigateBack(): Parcelable? { - if (!canNavigateBack) return null - val (oldDir, oldRecyclerViewState) = navigationStack.removeFirst() - navigateTo(oldDir, pushPreviousLocation = false) - return oldRecyclerViewState - } - - fun reset() { - navigationStack.clear() - forceUpdateOnNextSearchAction() - navigateTo(pushPreviousLocation = false) - } - fun search( filter: String, baseDirectory: File? = null, |
