diff options
Diffstat (limited to 'app/src/main/java')
9 files changed, 185 insertions, 49 deletions
diff --git a/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt b/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt index ccd3bf08..6831761b 100644 --- a/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/crypto/BasePGPActivity.kt @@ -23,7 +23,8 @@ import app.passwordstore.crypto.PGPIdentifier import app.passwordstore.data.crypto.CryptoRepository import app.passwordstore.data.repo.PasswordRepository import app.passwordstore.injection.prefs.SettingsPreferences -import app.passwordstore.ui.pgp.PGPKeyImportActivity +import app.passwordstore.ui.main.MainActivity +import app.passwordstore.ui.setup.SetupStart import app.passwordstore.util.coroutines.DispatcherProvider import app.passwordstore.util.extensions.clipboard import app.passwordstore.util.extensions.getString @@ -83,7 +84,9 @@ open class BasePGPActivity : AppCompatActivity() { .setMessage(resources.getString(R.string.no_keys_imported_dialog_message)) .setPositiveButton(resources.getString(R.string.button_label_import)) { _, _ -> onKeyImport = onKeysExist - keyImportAction.launch(Intent(this@BasePGPActivity, PGPKeyImportActivity::class.java)) + keyImportAction.launch( + MainActivity.setupIntent(this@BasePGPActivity, SetupStart.PgpImport) + ) } .show() } diff --git a/app/src/main/java/app/passwordstore/ui/entry/EntryDestination.kt b/app/src/main/java/app/passwordstore/ui/entry/EntryDestination.kt index 1b3a75d2..637c9b56 100644 --- a/app/src/main/java/app/passwordstore/ui/entry/EntryDestination.kt +++ b/app/src/main/java/app/passwordstore/ui/entry/EntryDestination.kt @@ -49,7 +49,7 @@ import app.passwordstore.ui.crypto.copyTextToClipboard import app.passwordstore.ui.crypto.getPGPIdentifiers import app.passwordstore.ui.main.LaunchActivity import app.passwordstore.ui.main.MainActivity -import app.passwordstore.ui.pgp.PGPKeyImportActivity +import app.passwordstore.ui.setup.SetupStart import app.passwordstore.util.auth.BiometricAuthenticator import app.passwordstore.util.auth.BiometricAuthenticator.Result as BiometricResult import app.passwordstore.util.extensions.sharedPrefs @@ -151,7 +151,7 @@ fun EntryDestination( TextButton( onClick = { viewModel.dismissPrompt() - keyImport.launch(Intent(activity, PGPKeyImportActivity::class.java)) + keyImport.launch(MainActivity.setupIntent(activity, SetupStart.PgpImport)) } ) { Text(stringResource(R.string.button_label_import)) 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 eff3cf02..a256fb1b 100644 --- a/app/src/main/java/app/passwordstore/ui/list/ListDestination.kt +++ b/app/src/main/java/app/passwordstore/ui/list/ListDestination.kt @@ -35,7 +35,6 @@ import app.passwordstore.R import app.passwordstore.data.repo.PasswordRepository import app.passwordstore.ui.compose.theme.SpacingMedium import app.passwordstore.ui.git.base.BaseGitActivity.GitOp -import app.passwordstore.ui.git.config.GitServerConfigActivity import app.passwordstore.ui.main.MainActivity import app.passwordstore.util.extensions.isInsideRepository import app.passwordstore.util.extensions.sharedPrefs @@ -60,7 +59,8 @@ 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. + * relative to the root, [onOpenEntry] an entry file; [onNewEntry] opens the editor in this folder, + * [onClone] the git server screen for a clone; [onNoStore] runs when the store is gone. */ @Composable fun ListDestination( @@ -69,6 +69,8 @@ fun ListDestination( onOpenFolder: (relativePath: String) -> Unit, onOpenEntry: (File) -> Unit, onNewEntry: () -> Unit, + onClone: () -> Unit, + onNoStore: () -> Unit, modifier: Modifier = Modifier, viewModel: SearchableRepositoryViewModel = hiltViewModel(), ) { @@ -98,7 +100,7 @@ fun ListDestination( // Every return to this folder (back from a child, or from a legacy activity) re-reads it. LifecycleResumeEffect(Unit) { - if (activity.checkStore()) refresh() + if (activity.checkStore()) refresh() else onNoStore() resumes++ onPauseOrDispose {} } @@ -270,7 +272,7 @@ fun ListDestination( confirm = stringResource(R.string.clone_button), onConfirm = { dialog = ListDialog.None - activity.startActivity(GitServerConfigActivity.createCloneIntent(activity)) + onClone() }, onDismiss = { dialog = ListDialog.None }, dismiss = stringResource(R.string.dialog_cancel), diff --git a/app/src/main/java/app/passwordstore/ui/main/MainActivity.kt b/app/src/main/java/app/passwordstore/ui/main/MainActivity.kt index 58ef4381..c8b1861a 100644 --- a/app/src/main/java/app/passwordstore/ui/main/MainActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/main/MainActivity.kt @@ -5,6 +5,8 @@ package app.passwordstore.ui.main +import android.content.Context +import android.content.Intent import android.os.Bundle import android.view.WindowManager import androidx.activity.compose.setContent @@ -28,8 +30,11 @@ import app.passwordstore.ui.entry.EntryViewModel import app.passwordstore.ui.git.base.BaseGitActivity import app.passwordstore.ui.list.ListDestination import app.passwordstore.ui.list.folderMoves -import app.passwordstore.ui.onboarding.activity.OnboardingActivity import app.passwordstore.ui.settings.SettingsActivity +import app.passwordstore.ui.setup.GitServerRoute +import app.passwordstore.ui.setup.SetupStart +import app.passwordstore.ui.setup.WelcomeRoute +import app.passwordstore.ui.setup.setupGraph import app.passwordstore.util.extensions.launchActivity import app.passwordstore.util.extensions.sharedPrefs import app.passwordstore.util.settings.AuthMode @@ -64,9 +69,9 @@ import kotlinx.serialization.Serializable /** * Single-activity host for the Compose UI. Not exported: it is only started by [LaunchActivity], - * after its biometric gate and path check. Extends [BaseGitActivity] (an AppCompatActivity, so also - * the FragmentActivity BiometricPrompt needs) because the git operations require one; it hosts no - * fragments. + * after its biometric gate and path check, and by the app itself through [setupIntent]. Extends + * [BaseGitActivity] (an AppCompatActivity, so also the FragmentActivity BiometricPrompt needs) + * because the git operations require one; it hosts no fragments. */ @AndroidEntryPoint class MainActivity : BaseGitActivity() { @@ -79,7 +84,18 @@ class MainActivity : BaseGitActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val filePath = intent.getStringExtra(BasePGPActivity.EXTRA_FILE_PATH) - val start: Any = if (filePath != null) EntryRoute(filePath) else ListRoute() + val setup = + intent.getStringExtra(EXTRA_SETUP)?.let { name -> + SetupStart.entries.firstOrNull { it.name == name } + } + // A restored back stack wins over this choice, so a recreation stays where it was. + val start: Any = + when { + filePath != null -> EntryRoute(filePath) + setup != null -> setup.route + checkStore() -> ListRoute() + else -> WelcomeRoute + } setContent { APSTheme { val nav = rememberNavController() @@ -106,6 +122,15 @@ class MainActivity : BaseGitActivity() { onNewEntry = { if (nav.currentBackStackEntry?.id == entry.id) nav.navigate(EditRoute(path)) }, + onClone = { + if (nav.currentBackStackEntry?.id == entry.id) + nav.navigate(GitServerRoute(clone = true)) + }, + onNoStore = { + // The store is gone (deleted from Settings): setup starts over. + if (nav.currentBackStackEntry?.id == entry.id) + nav.navigate(WelcomeRoute) { popUpTo(nav.graph.id) { inclusive = true } } + }, ) } composable<EntryRoute> { entry -> @@ -168,6 +193,7 @@ class MainActivity : BaseGitActivity() { }, ) } + setupGraph(this@MainActivity, nav) } } } @@ -183,12 +209,10 @@ class MainActivity : BaseGitActivity() { if (--secureHolds == 0) window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) } - /** Re-reads the store; starts onboarding and returns false when there is none yet. */ + /** Re-reads the store; false when there is none yet and setup has to run. */ fun checkStore(): Boolean { PasswordRepository.initialize() - if (sharedPrefs.getBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false)) return true - launchActivity(OnboardingActivity::class.java) - return false + return sharedPrefs.getBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false) } /** Git actions offered in the list's menu, chosen as the old list chose its menu. */ @@ -221,6 +245,22 @@ class MainActivity : BaseGitActivity() { private fun entryItem(file: File) = PasswordItem.newPassword(file.name, file, PasswordRepository.getRepositoryDirectory()) + + companion object { + + private const val EXTRA_SETUP = "SETUP" + + /** + * Opens one setup screen in an instance of its own, for callers outside the graph (legacy + * Settings, autofill, git operations) and for those waiting for a result (the entry view's + * missing key). Leaving that screen finishes the instance; the PGP keys screen sets RESULT_OK + * once a key was imported. + */ + // ponytail: a second instance instead of in-graph navigation, until steps 6 and 7 move + // Settings and autofill into the graph. + fun setupIntent(context: Context, start: SetupStart): Intent = + Intent(context, MainActivity::class.java).putExtra(EXTRA_SETUP, start.name) + } } /** diff --git a/app/src/main/java/app/passwordstore/ui/settings/PGPSettings.kt b/app/src/main/java/app/passwordstore/ui/settings/PGPSettings.kt index d2e63779..46e62faa 100644 --- a/app/src/main/java/app/passwordstore/ui/settings/PGPSettings.kt +++ b/app/src/main/java/app/passwordstore/ui/settings/PGPSettings.kt @@ -10,9 +10,9 @@ import androidx.fragment.app.FragmentActivity import androidx.lifecycle.lifecycleScope import app.passwordstore.R import app.passwordstore.data.crypto.PGPPassphraseCache -import app.passwordstore.ui.pgp.PGPKeyListActivity +import app.passwordstore.ui.main.MainActivity +import app.passwordstore.ui.setup.SetupStart import app.passwordstore.util.auth.BiometricAuthenticator -import app.passwordstore.util.extensions.launchActivity import app.passwordstore.util.extensions.sharedPrefs import app.passwordstore.util.features.Feature import app.passwordstore.util.settings.PreferenceKeys @@ -33,7 +33,7 @@ class PGPSettings( titleRes = R.string.pref_pgp_key_manager_title persistent = false onClick { - activity.launchActivity(PGPKeyListActivity::class.java) + activity.startActivity(MainActivity.setupIntent(activity, SetupStart.PgpKeys)) false } } diff --git a/app/src/main/java/app/passwordstore/ui/settings/RepositorySettings.kt b/app/src/main/java/app/passwordstore/ui/settings/RepositorySettings.kt index fac95949..4b60aae8 100644 --- a/app/src/main/java/app/passwordstore/ui/settings/RepositorySettings.kt +++ b/app/src/main/java/app/passwordstore/ui/settings/RepositorySettings.kt @@ -5,7 +5,6 @@ package app.passwordstore.ui.settings -import android.content.Intent import android.content.SharedPreferences import android.content.pm.ShortcutManager import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult @@ -16,11 +15,9 @@ import app.passwordstore.R import app.passwordstore.data.repo.PasswordRepository import app.passwordstore.injection.prefs.GitPreferences import app.passwordstore.ui.git.config.GitConfigActivity -import app.passwordstore.ui.git.config.GitServerConfigActivity +import app.passwordstore.ui.main.MainActivity import app.passwordstore.ui.proxy.ProxySelectorActivity -import app.passwordstore.ui.sshkeygen.ShowSshKeyFragment -import app.passwordstore.ui.sshkeygen.SshKeyGenActivity -import app.passwordstore.ui.sshkeygen.SshKeyImportActivity +import app.passwordstore.ui.setup.SetupStart import app.passwordstore.util.extensions.getString import app.passwordstore.util.extensions.launchActivity import app.passwordstore.util.extensions.sharedPrefs @@ -73,7 +70,7 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi titleRes = R.string.pref_edit_git_server_settings visible = PasswordRepository.isGitRepo() onClick { - activity.launchActivity(GitServerConfigActivity::class.java) + activity.startActivity(MainActivity.setupIntent(activity, SetupStart.GitServer)) true } } @@ -97,14 +94,14 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi titleRes = R.string.pref_import_ssh_key_title visible = PasswordRepository.isGitRepo() onClick { - activity.launchActivity(SshKeyImportActivity::class.java) + activity.startActivity(MainActivity.setupIntent(activity, SetupStart.Ssh)) true } } pref(PreferenceKeys.SSH_KEYGEN) { titleRes = R.string.pref_ssh_keygen_title onClick { - generateSshKey.launch(Intent(activity, SshKeyGenActivity::class.java)) + generateSshKey.launch(MainActivity.setupIntent(activity, SetupStart.SshGenerate)) true } } @@ -113,7 +110,7 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi titleRes = R.string.pref_ssh_see_key_title visible = PasswordRepository.isGitRepo() && SshKey.canShowSshPublicKey onClick { - ShowSshKeyFragment().show(activity.supportFragmentManager, "public_key") + activity.startActivity(MainActivity.setupIntent(activity, SetupStart.Ssh)) true } } diff --git a/app/src/main/java/app/passwordstore/ui/setup/PgpKeysScreen.kt b/app/src/main/java/app/passwordstore/ui/setup/PgpKeysScreen.kt index 2f060f56..1f0c9599 100644 --- a/app/src/main/java/app/passwordstore/ui/setup/PgpKeysScreen.kt +++ b/app/src/main/java/app/passwordstore/ui/setup/PgpKeysScreen.kt @@ -8,6 +8,7 @@ package app.passwordstore.ui.setup import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts.OpenDocument +import androidx.annotation.Keep import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -61,7 +62,8 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.launch import kotlinx.serialization.Serializable -/** What the PGP keys screen is for. */ +/** What the PGP keys screen is for. A route argument: kept from minification. */ +@Keep @Serializable enum class KeysMode { /** Import and delete keys (Settings). */ diff --git a/app/src/main/java/app/passwordstore/ui/setup/SetupGraph.kt b/app/src/main/java/app/passwordstore/ui/setup/SetupGraph.kt new file mode 100644 index 00000000..ffa47511 --- /dev/null +++ b/app/src/main/java/app/passwordstore/ui/setup/SetupGraph.kt @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2026 Danilo M. <danix@danix.xyz> + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.ui.setup + +import android.app.Activity +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.compose.composable +import androidx.navigation.toRoute +import app.passwordstore.ui.main.ListRoute +import app.passwordstore.ui.main.MainActivity +import kotlinx.serialization.Serializable + +/** The first screen without a store. */ +@Serializable data object WelcomeRoute + +/** Git server settings; with [clone], saving them clones the store. */ +@Serializable data class GitServerRoute(val clone: Boolean) + +/** The SSH key: state, public key, import. */ +@Serializable data object SshKeyRoute + +/** SSH key generation. */ +@Serializable data object SshKeyGenRoute + +/** The PGP keys, for [mode]. */ +@Serializable data class PgpKeysRoute(val mode: KeysMode = KeysMode.Manage) + +/** The setup screens [MainActivity.setupIntent] opens on their own. */ +enum class SetupStart(val route: Any) { + GitServer(GitServerRoute(clone = false)), + Ssh(SshKeyRoute), + SshGenerate(SshKeyGenRoute), + PgpKeys(PgpKeysRoute()), + PgpImport(PgpKeysRoute(KeysMode.Need)), +} + +/** + * The setup destinations. In an instance started for one of them ([MainActivity.setupIntent]), + * leaving its first screen finishes the activity. + */ +fun NavGraphBuilder.setupGraph(activity: MainActivity, nav: NavHostController) { + fun onTop(entry: NavBackStackEntry) = nav.currentBackStackEntry?.id == entry.id + + fun close(entry: NavBackStackEntry) { + if (!onTop(entry)) return + if (nav.previousBackStackEntry == null) activity.finish() else nav.popBackStack() + } + + // Setup is complete: the new store's list replaces the whole stack. An instance started for + // this screen alone just finishes. + fun complete(entry: NavBackStackEntry) { + if (!onTop(entry)) return + if (nav.previousBackStackEntry == null) activity.finish() + else nav.navigate(ListRoute()) { popUpTo(nav.graph.id) { inclusive = true } } + } + + composable<WelcomeRoute> { entry -> + WelcomeScreen( + onClone = { if (onTop(entry)) nav.navigate(GitServerRoute(clone = true)) }, + onCreate = { if (onTop(entry)) nav.navigate(PgpKeysRoute(KeysMode.Choose)) }, + onSettings = activity::openSettings, + ) + } + composable<GitServerRoute> { entry -> + GitServerDestination( + activity = activity, + clone = entry.toRoute<GitServerRoute>().clone, + onClose = { close(entry) }, + onSshKey = { if (onTop(entry)) nav.navigate(SshKeyRoute) }, + onCloned = { keyNeeded -> + if (!keyNeeded) complete(entry) + else if (onTop(entry)) nav.navigate(PgpKeysRoute(KeysMode.Need)) + }, + ) + } + composable<SshKeyRoute> { entry -> + SshKeyDestination( + activity = activity, + onClose = { close(entry) }, + onGenerate = { if (onTop(entry)) nav.navigate(SshKeyGenRoute) }, + ) + } + composable<SshKeyGenRoute> { entry -> + SshKeyGenDestination(activity = activity, onClose = { close(entry) }) + } + composable<PgpKeysRoute> { entry -> + PgpKeysDestination( + activity = activity, + mode = entry.toRoute<PgpKeysRoute>().mode, + onClose = { close(entry) }, + onDone = { complete(entry) }, + onImported = { activity.setResult(Activity.RESULT_OK) }, + ) + } +} diff --git a/app/src/main/java/app/passwordstore/util/git/operation/GitOperation.kt b/app/src/main/java/app/passwordstore/util/git/operation/GitOperation.kt index a3edac7d..2d8b46ca 100644 --- a/app/src/main/java/app/passwordstore/util/git/operation/GitOperation.kt +++ b/app/src/main/java/app/passwordstore/util/git/operation/GitOperation.kt @@ -9,8 +9,8 @@ import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.FragmentActivity import app.passwordstore.R import app.passwordstore.data.repo.PasswordRepository -import app.passwordstore.ui.sshkeygen.SshKeyGenActivity -import app.passwordstore.ui.sshkeygen.SshKeyImportActivity +import app.passwordstore.ui.main.MainActivity +import app.passwordstore.ui.setup.SetupStart import app.passwordstore.util.auth.BiometricAuthenticator import app.passwordstore.util.auth.BiometricAuthenticator.Result.CanceledBySystem import app.passwordstore.util.auth.BiometricAuthenticator.Result.CanceledByUser @@ -18,7 +18,6 @@ import app.passwordstore.util.auth.BiometricAuthenticator.Result.Failure import app.passwordstore.util.auth.BiometricAuthenticator.Result.Retry import app.passwordstore.util.auth.BiometricAuthenticator.Result.Success import app.passwordstore.util.coroutines.DispatcherProvider -import app.passwordstore.util.extensions.launchActivity import app.passwordstore.util.git.GitCommandExecutor import app.passwordstore.util.git.sshj.SshAuthMethod import app.passwordstore.util.git.sshj.SshKey @@ -27,8 +26,6 @@ import app.passwordstore.util.settings.AuthMode import com.github.michaelbull.result.Err import com.github.michaelbull.result.Ok import com.github.michaelbull.result.Result -import com.github.michaelbull.result.onFailure -import com.github.michaelbull.result.runCatching import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.EntryPoint import dagger.hilt.InstallIn @@ -37,9 +34,6 @@ import dagger.hilt.components.SingletonComponent import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine import kotlinx.coroutines.withContext -import logcat.LogPriority.ERROR -import logcat.asLog -import logcat.logcat import net.schmizz.sshj.common.DisconnectReason import net.schmizz.sshj.common.SSHException import net.schmizz.sshj.userauth.password.PasswordFinder @@ -106,12 +100,12 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) { } private fun getSshKey(generateKey: Boolean) { - runCatching { - callingActivity.launchActivity( - if (generateKey) SshKeyGenActivity::class.java else SshKeyImportActivity::class.java - ) - } - .onFailure { e -> logcat(ERROR) { e.asLog() } } + callingActivity.startActivity( + MainActivity.setupIntent( + callingActivity, + if (generateKey) SetupStart.SshGenerate else SetupStart.Ssh, + ) + ) } private fun registerAuthProviders( @@ -152,10 +146,8 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) { ) { _, _ -> getSshKey(true) } - .setNeutralButton(callingActivity.resources.getString(R.string.dialog_cancel)) { _, _ -> - // Finish the blank GitActivity so user doesn't have to press back - callingActivity.finish() - } + // Cancel only closes the dialog: the calling activity is the app's own screen now. + .setNeutralButton(callingActivity.resources.getString(R.string.dialog_cancel), null) .show() } @@ -197,7 +189,7 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) { Toast.LENGTH_LONG, ) .show() - callingActivity.finish() + return Err(SSHException(DisconnectReason.AUTH_CANCELLED_BY_USER)) } } } else { |
