From b94b52a42ddc2325b539d0956fd0adcf35308b52 Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Fri, 17 Apr 2020 18:36:07 +0530 Subject: Refactor Git related activities (#685) * Refactor git logic into separate parts * Extract hardcoded strings * Add KDoc to updateHostname, remove unused field * Cleanups * Fix dialog message * Wire in repository clone flow * spotless * Remove unused method * Cleanup GitActivity - Rename to GitOperationActivity. - Ensure identityBuilder is always closed regardless of what fragment uses it. - Remove hardcoded "Operation" strings and replace with REQUEST_ARG_OP. - Apply a transparent theme to GitOperationActivity make the UI less jarring. * Tweak some stupidly worded dialog messages As pointed out in #629, these strings are shoddily worded and do not express any clear intent to the user, leaving them confused and angry. * GitOperationActivity: wrap Context to ensure right theme is used * spotless * undo build.gradle change * Use correct parent theme, remove now useless wrapping * GitServerConfigActivity: fix repository clone flow * temp: disable leakcanary framework leaks on Samsung are pissing me off * Make system bars transparent in git activity * Tweak HTTPS password layout * Unhardcode wrong passphrase string * Store SSH passphrase in EncryptedSharedPreferences Also revamp the dialog to look a bit better * Implement support for remembering HTTPS password Fixes #521 * Try to patch HTTPS remote creation logic * Update security-crypto * Clear saved passphrase/password on auth failure * Revert "Update security-crypto" Broken on R DP2.1 This reverts commit 4b20371dd42c512a3dd3b759859abb6c1ffd2961. * Revert "temp: disable leakcanary" This reverts commit 2db7d41bd67b79c6dc8c5b359a7b27100379f45f. * Update CHANGELOG * Remove spacer * Remove useless override * Wrap git server activity in a ScrollView * GitOperation: always finish calling activity when dialogs are dismissed * Wipe saved password/passphrase when hostname changes * Don't commit prefs updates * Don't call listFiles excessively * Finish activity after saving configuration * Make ConnectionMode and Protocol enum classes * Change SSH key passphrase key, don't wipe on host change * Reimplement BaseGitActivity.updateUrl (was updateHostname) * Use SharedPreferences.edit KTX extension * Disable inapplicable connection modes depending on scheme * BaseGitActivity: annotate onDestroy with CallSuper We'll leak the identityBuilder connection otherwise * Move input hack for AlertDialog into an extension function We re-use this in many places * Fix protocol/mode toggle issue and consistenly name options * Fix a crash when opening GitServerConfigActivity without a repo * Fix OpenKeychain callbacks by moving onActivityResult to BaseGitActivity * Run spotlessApply Signed-off-by: Harsh Shandilya Co-authored-by: Fabian Henneke --- .../zeapo/pwdstore/git/GitServerConfigActivity.kt | 185 +++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt (limited to 'app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt') diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt new file mode 100644 index 00000000..a10f3460 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt @@ -0,0 +1,185 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ +package com.zeapo.pwdstore.git + +import android.os.Bundle +import android.os.Handler +import androidx.core.content.edit +import androidx.core.os.postDelayed +import androidx.core.widget.doOnTextChanged +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.Snackbar +import com.zeapo.pwdstore.R +import com.zeapo.pwdstore.databinding.ActivityGitCloneBinding +import com.zeapo.pwdstore.git.config.ConnectionMode +import com.zeapo.pwdstore.git.config.Protocol +import com.zeapo.pwdstore.utils.PasswordRepository +import java.io.IOException + +/** + * Activity that encompasses both the initial clone as well as editing the server config for future + * changes. + */ +class GitServerConfigActivity : BaseGitActivity() { + + lateinit var binding: ActivityGitCloneBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityGitCloneBinding.inflate(layoutInflater) + val isClone = intent?.extras?.getInt(REQUEST_ARG_OP) ?: -1 == REQUEST_CLONE + if (isClone) { + binding.saveButton.text = getString(R.string.clone_button) + } + setContentView(binding.root) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + val protocolIdToCheck = when (protocol) { + Protocol.Ssh -> R.id.clone_protocol_ssh + Protocol.Https -> R.id.clone_protocol_https + } + binding.cloneProtocolGroup.check(protocolIdToCheck) + binding.cloneProtocolGroup.addOnButtonCheckedListener { _, checkedId, checked -> + if (checked) { + when (checkedId) { + R.id.clone_protocol_https -> protocol = Protocol.Https + R.id.clone_protocol_ssh -> protocol = Protocol.Ssh + } + updateConnectionModeToggleGroup() + } + } + + val connectionModeIdToCheck = when (connectionMode) { + ConnectionMode.SshKey -> R.id.connection_mode_ssh_key + ConnectionMode.Password -> R.id.connection_mode_password + ConnectionMode.OpenKeychain -> R.id.connection_mode_open_keychain + } + binding.connectionModeGroup.check(connectionModeIdToCheck) + binding.connectionModeGroup.addOnButtonCheckedListener { _, checkedId, checked -> + if (checked) { + when (checkedId) { + R.id.connection_mode_ssh_key -> connectionMode = ConnectionMode.SshKey + R.id.connection_mode_open_keychain -> connectionMode = ConnectionMode.OpenKeychain + R.id.connection_mode_password -> connectionMode = ConnectionMode.Password + } + } + } + updateConnectionModeToggleGroup() + + binding.serverUrl.apply { + setText(serverHostname) + doOnTextChanged { text, _, _, _ -> + serverHostname = text.toString().trim() + } + } + + binding.serverPort.apply { + setText(serverPort) + doOnTextChanged { text, _, _, _ -> + serverPort = text.toString().trim() + } + } + + binding.serverUser.apply { + setText(serverUser) + doOnTextChanged { text, _, _, _ -> + serverUser = text.toString().trim() + } + } + + binding.serverPath.apply { + setText(serverPath) + doOnTextChanged { text, _, _, _ -> + serverPath = text.toString().trim() + } + } + + binding.saveButton.setOnClickListener { + if (isClone && PasswordRepository.getRepository(null) == null) + PasswordRepository.initialize(this) + if (updateUrl()) { + settings.edit { + putString("git_remote_protocol", protocol.pref) + putString("git_remote_auth", connectionMode.pref) + putString("git_remote_server", serverHostname) + putString("git_remote_port", serverPort) + putString("git_remote_username", serverUser) + putString("git_remote_location", serverPath) + } + if (!isClone) { + Snackbar.make(binding.root, getString(R.string.git_server_config_save_success), Snackbar.LENGTH_SHORT).show() + Handler().postDelayed(500) { finish() } + } else + cloneRepository() + } else { + Snackbar.make(binding.root, getString(R.string.git_server_config_save_failure), Snackbar.LENGTH_LONG).show() + } + } + } + + private fun updateConnectionModeToggleGroup() { + if (protocol == Protocol.Ssh) { + binding.connectionModeSshKey.isEnabled = true + binding.connectionModeOpenKeychain.isEnabled = true + } else { + // Reset connection mode to the only one possible via HTTPS: password. + // Important note: This has to happen before disabling the other toggle buttons or they + // won't uncheck. + binding.connectionModeGroup.check(R.id.connection_mode_password) + binding.connectionModeSshKey.isEnabled = false + binding.connectionModeOpenKeychain.isEnabled = false + } + } + + /** + * Clones the repository, the directory exists, deletes it + */ + private fun cloneRepository() { + val localDir = requireNotNull(PasswordRepository.getRepositoryDirectory(this)) + val localDirFiles = localDir.listFiles() ?: emptyArray() + // Warn if non-empty folder unless it's a just-initialized store that has just a .git folder + if (localDir.exists() && localDirFiles.isNotEmpty() && + !(localDirFiles.size == 1 && localDirFiles[0].name == ".git")) { + MaterialAlertDialogBuilder(this) + .setTitle(R.string.dialog_delete_title) + .setMessage(resources.getString(R.string.dialog_delete_msg) + " " + localDir.toString()) + .setCancelable(false) + .setPositiveButton(R.string.dialog_delete) { dialog, _ -> + try { + localDir.deleteRecursively() + launchGitOperation(REQUEST_CLONE) + } catch (e: IOException) { + // TODO Handle the exception correctly if we are unable to delete the directory... + e.printStackTrace() + MaterialAlertDialogBuilder(this).setMessage(e.message).show() + } finally { + dialog.cancel() + } + } + .setNegativeButton(R.string.dialog_do_not_delete) { dialog, _ -> + dialog.cancel() + } + .show() + } else { + try { + // Silently delete & replace the lone .git folder if it exists + if (localDir.exists() && localDirFiles.size == 1 && localDirFiles[0].name == ".git") { + try { + localDir.deleteRecursively() + } catch (e: IOException) { + e.printStackTrace() + MaterialAlertDialogBuilder(this).setMessage(e.message).show() + } + } + } catch (e: Exception) { + // This is what happens when JGit fails :( + // TODO Handle the different cases of exceptions + e.printStackTrace() + MaterialAlertDialogBuilder(this).setMessage(e.message).show() + } + launchGitOperation(REQUEST_CLONE) + } + } +} -- cgit v1.2.3