aboutsummaryrefslogtreecommitdiff
path: root/app/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main')
-rw-r--r--app/src/main/AndroidManifest.xml4
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/LaunchActivity.kt10
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt18
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt56
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSaveActivity.kt16
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt39
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt104
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/GitCommandExecutor.kt86
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/GitConfigActivity.kt31
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/GitOperationActivity.kt79
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt13
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/operation/BreakOutOfDetached.kt30
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/operation/CloneOperation.kt20
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/operation/GitOperation.kt128
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/operation/PullOperation.kt17
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/operation/PushOperation.kt18
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/operation/ResetToRemoteOperation.kt14
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/operation/SyncOperation.kt14
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt1
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt29
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/utils/Result.kt16
-rw-r--r--app/src/main/res/layout/activity_git_clone.xml2
-rw-r--r--app/src/main/res/menu/git_clone.xml15
-rw-r--r--app/src/main/res/values/strings.xml2
24 files changed, 271 insertions, 491 deletions
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index cbacda10..a9fa83d7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -52,10 +52,6 @@
tools:node="replace" />
<activity
- android:name=".git.GitOperationActivity"
- android:theme="@style/NoBackgroundTheme" />
-
- <activity
android:name=".git.GitServerConfigActivity"
android:label="@string/title_activity_git_clone"
android:windowSoftInputMode="adjustResize" />
diff --git a/app/src/main/java/com/zeapo/pwdstore/LaunchActivity.kt b/app/src/main/java/com/zeapo/pwdstore/LaunchActivity.kt
index ed943451..268698e7 100644
--- a/app/src/main/java/com/zeapo/pwdstore/LaunchActivity.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/LaunchActivity.kt
@@ -40,17 +40,17 @@ class LaunchActivity : AppCompatActivity() {
}
private fun startTargetActivity(noAuth: Boolean) {
- if (intent.action == ACTION_DECRYPT_PASS) {
+ val intentToStart = if (intent.action == ACTION_DECRYPT_PASS)
Intent(this, DecryptActivity::class.java).apply {
putExtra("NAME", intent.getStringExtra("NAME"))
putExtra("FILE_PATH", intent.getStringExtra("FILE_PATH"))
putExtra("REPO_PATH", intent.getStringExtra("REPO_PATH"))
putExtra("LAST_CHANGED_TIMESTAMP", intent.getLongExtra("LAST_CHANGED_TIMESTAMP", 0L))
- startActivity(this)
}
- } else {
- startActivity(Intent(this, PasswordStore::class.java))
- }
+ else
+ Intent(this, PasswordStore::class.java)
+ startActivity(intentToStart)
+
Handler().postDelayed({ finish() }, if (noAuth) 0L else 500L)
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt b/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt
index 7e0ef418..2305ac94 100644
--- a/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt
@@ -19,12 +19,13 @@ import androidx.appcompat.view.ActionMode
import androidx.core.content.edit
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
+import com.github.michaelbull.result.fold
import com.google.android.material.snackbar.Snackbar
import com.zeapo.pwdstore.databinding.PasswordRecyclerViewBinding
import com.zeapo.pwdstore.git.BaseGitActivity
-import com.zeapo.pwdstore.git.GitOperationActivity
import com.zeapo.pwdstore.git.GitServerConfigActivity
import com.zeapo.pwdstore.git.config.AuthMode
import com.zeapo.pwdstore.git.config.GitSettings
@@ -39,6 +40,7 @@ import com.zeapo.pwdstore.utils.getString
import com.zeapo.pwdstore.utils.sharedPrefs
import com.zeapo.pwdstore.utils.viewBinding
import java.io.File
+import kotlinx.coroutines.launch
import me.zhanghai.android.fastscroll.FastScrollerBuilder
class PasswordFragment : Fragment(R.layout.password_recycler_view) {
@@ -99,9 +101,17 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
AuthMode.None -> BaseGitActivity.REQUEST_PULL
else -> BaseGitActivity.REQUEST_SYNC
}
- val intent = Intent(context, GitOperationActivity::class.java)
- intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, operationId)
- swipeResult.launch(intent)
+ requireStore().apply {
+ lifecycleScope.launch {
+ launchGitOperation(operationId).fold(
+ success = {
+ binding.swipeRefresher.isRefreshing = false
+ refreshPasswordList()
+ },
+ failure = ::finishAfterPromptOnErrorHandler,
+ )
+ }
+ }
}
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt
index d9afc7c9..7f8c4e55 100644
--- a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt
@@ -23,7 +23,6 @@ import android.view.MenuItem.OnActionExpandListener
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.activity.viewModels
-import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.AppCompatTextView
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.SearchView.OnQueryTextListener
@@ -38,6 +37,7 @@ import com.github.ajalt.timberkt.d
import com.github.ajalt.timberkt.e
import com.github.ajalt.timberkt.i
import com.github.ajalt.timberkt.w
+import com.github.michaelbull.result.fold
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputEditText
@@ -48,7 +48,6 @@ import com.zeapo.pwdstore.crypto.BasePgpActivity.Companion.getLongName
import com.zeapo.pwdstore.crypto.DecryptActivity
import com.zeapo.pwdstore.crypto.PasswordCreationActivity
import com.zeapo.pwdstore.git.BaseGitActivity
-import com.zeapo.pwdstore.git.GitOperationActivity
import com.zeapo.pwdstore.git.GitServerConfigActivity
import com.zeapo.pwdstore.git.config.AuthMode
import com.zeapo.pwdstore.git.config.GitSettings
@@ -81,7 +80,7 @@ import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.errors.GitAPIException
import org.eclipse.jgit.revwalk.RevCommit
-class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
+class PasswordStore : BaseGitActivity() {
private lateinit var activity: PasswordStore
private lateinit var searchItem: MenuItem
@@ -101,12 +100,6 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
}
}
- private val listRefreshAction = registerForActivityResult(StartActivityForResult()) { result ->
- if (result.resultCode == RESULT_OK) {
- refreshPasswordList()
- }
- }
-
private val repositoryInitAction = registerForActivityResult(StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
initializeRepositoryInfo()
@@ -136,7 +129,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
private val checkPermissionsAndCloneAction = registerForActivityResult(RequestPermission()) { granted ->
if (granted) {
val intent = Intent(activity, GitServerConfigActivity::class.java)
- intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_CLONE)
+ intent.putExtra(REQUEST_ARG_OP, REQUEST_CLONE)
cloneAction.launch(intent)
}
}
@@ -163,10 +156,6 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
@SuppressLint("NewApi")
override fun onCreate(savedInstanceState: Bundle?) {
activity = this
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
- shortcutManager = getSystemService()
- }
-
// If user opens app with permission granted then revokes and returns,
// prevent attempt to create password list fragment
var savedInstance = savedInstanceState
@@ -177,6 +166,10 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
savedInstance = null
}
super.onCreate(savedInstance)
+ setContentView(R.layout.activity_pwdstore)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
+ shortcutManager = getSystemService()
+ }
// If user is eligible for Oreo autofill, prompt them to switch.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
@@ -328,9 +321,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
initBefore.show()
return false
}
- intent = Intent(this, GitOperationActivity::class.java)
- intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_PUSH)
- startActivity(intent)
+ runGitOperation(REQUEST_PUSH)
return true
}
R.id.git_pull -> {
@@ -338,9 +329,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
initBefore.show()
return false
}
- intent = Intent(this, GitOperationActivity::class.java)
- intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_PULL)
- listRefreshAction.launch(intent)
+ runGitOperation(REQUEST_PULL)
return true
}
R.id.git_sync -> {
@@ -348,9 +337,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
initBefore.show()
return false
}
- intent = Intent(this, GitOperationActivity::class.java)
- intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_SYNC)
- listRefreshAction.launch(intent)
+ runGitOperation(REQUEST_SYNC)
return true
}
R.id.refresh -> {
@@ -415,6 +402,13 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
createRepository()
}
+ private fun runGitOperation(operation: Int) = lifecycleScope.launch {
+ launchGitOperation(operation).fold(
+ success = { refreshPasswordList() },
+ failure = ::finishAfterPromptOnErrorHandler,
+ )
+ }
+
/**
* Validates if storage permission is granted, and requests for it if not. The return value
* is true if the permission has been granted.
@@ -576,12 +570,6 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
intent.putExtra("REPO_PATH", getRepositoryDirectory().absolutePath)
registerForActivityResult(StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
- lifecycleScope.launch {
- commitChange(
- resources.getString(R.string.git_commit_add_text, result.data?.extras?.getString("LONG_NAME")),
- finishActivityOnEnd = false,
- )
- }
refreshPasswordList()
}
}.launch(intent)
@@ -624,7 +612,6 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
lifecycleScope.launch {
commitChange(
resources.getString(R.string.git_commit_remove_text, fmt),
- finishActivityOnEnd = false,
)
}
}
@@ -636,7 +623,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
val intent = Intent(this, SelectFolderActivity::class.java)
val fileLocations = values.map { it.file.absolutePath }.toTypedArray()
intent.putExtra("Files", fileLocations)
- intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, "SELECTFOLDER")
+ intent.putExtra(REQUEST_ARG_OP, "SELECTFOLDER")
registerForActivityResult(StartActivityForResult()) { result ->
val intentData = result.data ?: return@registerForActivityResult
val filesToMove = requireNotNull(intentData.getStringArrayExtra("Files"))
@@ -694,7 +681,6 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
withContext(Dispatchers.Main) {
commitChange(
resources.getString(R.string.git_commit_move_text, sourceLongName, destinationLongName),
- finishActivityOnEnd = false,
)
}
}
@@ -704,7 +690,6 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
withContext(Dispatchers.Main) {
commitChange(
resources.getString(R.string.git_commit_move_multiple_text, relativePath),
- finishActivityOnEnd = false,
)
}
}
@@ -769,7 +754,6 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
withContext(Dispatchers.Main) {
commitChange(
resources.getString(R.string.git_commit_move_text, oldCategory.name, newCategory.name),
- finishActivityOnEnd = false,
)
}
}
@@ -848,7 +832,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
NEW_REPO_BUTTON -> initializeRepositoryInfo()
CLONE_REPO_BUTTON -> {
val intent = Intent(activity, GitServerConfigActivity::class.java)
- intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_CLONE)
+ intent.putExtra(REQUEST_ARG_OP, REQUEST_CLONE)
cloneAction.launch(intent)
}
}
@@ -872,7 +856,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
NEW_REPO_BUTTON -> initializeRepositoryInfo()
CLONE_REPO_BUTTON -> {
val intent = Intent(activity, GitServerConfigActivity::class.java)
- intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_CLONE)
+ intent.putExtra(REQUEST_ARG_OP, REQUEST_CLONE)
cloneAction.launch(intent)
}
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSaveActivity.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSaveActivity.kt
index f6ef8757..0052ff65 100644
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSaveActivity.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSaveActivity.kt
@@ -15,9 +15,7 @@ import androidx.activity.result.contract.ActivityResultContracts.StartActivityFo
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf
-import androidx.lifecycle.lifecycleScope
import com.github.ajalt.timberkt.e
-import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.autofill.oreo.AutofillAction
import com.zeapo.pwdstore.autofill.oreo.AutofillMatcher
import com.zeapo.pwdstore.autofill.oreo.AutofillPreferences
@@ -26,9 +24,7 @@ import com.zeapo.pwdstore.autofill.oreo.FillableForm
import com.zeapo.pwdstore.autofill.oreo.FormOrigin
import com.zeapo.pwdstore.crypto.PasswordCreationActivity
import com.zeapo.pwdstore.utils.PasswordRepository
-import com.zeapo.pwdstore.utils.commitChange
import java.io.File
-import kotlinx.coroutines.launch
@RequiresApi(Build.VERSION_CODES.O)
class AutofillSaveActivity : AppCompatActivity() {
@@ -119,7 +115,6 @@ class AutofillSaveActivity : AppCompatActivity() {
formOrigin?.let {
AutofillMatcher.addMatchFor(this, it, File(createdPath))
}
- val longName = data.getStringExtra("LONG_NAME")!!
val password = data.getStringExtra("PASSWORD")
val resultIntent = if (password != null) {
// Password was generated and should be filled into a form.
@@ -144,15 +139,8 @@ class AutofillSaveActivity : AppCompatActivity() {
// Password was extracted from a form, there is nothing to fill.
Intent()
}
- // PasswordCreationActivity delegates committing the added file to PasswordStore. Since
- // PasswordStore is not involved in an AutofillScenario, we have to commit the file ourselves.
- lifecycleScope.launch {
- commitChange(
- getString(R.string.git_commit_add_text, longName),
- finishWithResultOnEnd = resultIntent
- )
- }
- // GitAsyncTask will finish the activity for us.
+ setResult(RESULT_OK, resultIntent)
+ finish()
}
}.launch(saveIntent)
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt b/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt
index ff5a8179..368182ee 100644
--- a/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt
@@ -20,6 +20,7 @@ import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged
import androidx.lifecycle.lifecycleScope
import com.github.ajalt.timberkt.e
+import com.github.michaelbull.result.onSuccess
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.zxing.integration.android.IntentIntegrator
import com.google.zxing.integration.android.IntentIntegrator.QR_CODE
@@ -331,14 +332,13 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
result.data?.getStringArrayExtra(OpenPgpApi.EXTRA_KEY_IDS)?.let { keyIds ->
gpgIdentifierFile.writeText(keyIds.joinToString("\n"))
lifecycleScope.launch {
- commitChange(
- getString(
- R.string.git_commit_gpg_id,
- getLongName(gpgIdentifierFile.parentFile!!.absolutePath, repoPath, gpgIdentifierFile.name)
- )
- )
+ commitChange(getString(
+ R.string.git_commit_gpg_id,
+ getLongName(gpgIdentifierFile.parentFile!!.absolutePath, repoPath, gpgIdentifierFile.name)
+ )).onSuccess {
+ encrypt(data)
+ }
}
- encrypt(data)
}
}
}.launch(Intent(this@PasswordCreationActivity, GetKeyIdsActivity::class.java))
@@ -440,17 +440,6 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
returnIntent.putExtra(RETURN_EXTRA_USERNAME, username)
}
- if (editing) {
- lifecycleScope.launch {
- commitChange(
- getString(
- R.string.git_commit_edit_text,
- getLongName(fullPath, repoPath, editName)
- )
- )
- }
- }
-
if (directoryInputLayout.isVisible && directoryInputLayout.isEnabled && oldFileName != null) {
val oldFile = File("$repoPath/${oldCategory?.trim('/')}/$oldFileName.gpg")
if (oldFile.path != file.path && !oldFile.delete()) {
@@ -463,13 +452,19 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
finish()
}
.show()
- } else {
+ return@executeApiAsync
+ }
+ }
+
+ val commitMessageRes = if (editing) R.string.git_commit_edit_text else R.string.git_commit_add_text
+ lifecycleScope.launch {
+ commitChange(resources.getString(
+ commitMessageRes,
+ getLongName(fullPath, repoPath, editName)
+ )).onSuccess {
setResult(RESULT_OK, returnIntent)
finish()
}
- } else {
- setResult(RESULT_OK, returnIntent)
- finish()
}
} catch (e: Exception) {
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt
index ed72c88d..e16b0c8b 100644
--- a/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt
@@ -6,9 +6,14 @@ package com.zeapo.pwdstore.git
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.edit
import com.github.ajalt.timberkt.Timber.tag
+import com.github.ajalt.timberkt.d
import com.github.ajalt.timberkt.e
+import com.github.michaelbull.result.Err
import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.github.michaelbull.result.Result
+import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.git.config.GitSettings
import com.zeapo.pwdstore.git.operation.BreakOutOfDetached
import com.zeapo.pwdstore.git.operation.CloneOperation
@@ -17,10 +22,15 @@ import com.zeapo.pwdstore.git.operation.PullOperation
import com.zeapo.pwdstore.git.operation.PushOperation
import com.zeapo.pwdstore.git.operation.ResetToRemoteOperation
import com.zeapo.pwdstore.git.operation.SyncOperation
-import com.zeapo.pwdstore.utils.PasswordRepository
+import com.zeapo.pwdstore.utils.PreferenceKeys
+import com.zeapo.pwdstore.utils.getEncryptedPrefs
+import com.zeapo.pwdstore.utils.sharedPrefs
+import net.schmizz.sshj.common.DisconnectReason
+import net.schmizz.sshj.common.SSHException
+import net.schmizz.sshj.userauth.UserAuthException
/**
- * Abstract AppCompatActivity that holds some information that is commonly shared across git-related
+ * Abstract [AppCompatActivity] that holds some information that is commonly shared across git-related
* tasks and makes sense to be held here.
*/
abstract class BaseGitActivity : AppCompatActivity() {
@@ -39,33 +49,77 @@ abstract class BaseGitActivity : AppCompatActivity() {
* Attempt to launch the requested Git operation.
* @param operation The type of git operation to launch
*/
- suspend fun launchGitOperation(operation: Int) {
+ suspend fun launchGitOperation(operation: Int): Result<Unit, Throwable> {
if (GitSettings.url == null) {
- setResult(RESULT_CANCELED)
- finish()
- return
+ return Err(IllegalStateException("Git url is not set!"))
}
- try {
- val localDir = requireNotNull(PasswordRepository.getRepositoryDirectory())
- val op = when (operation) {
- REQUEST_CLONE, GitOperation.GET_SSH_KEY_FROM_CLONE -> CloneOperation(localDir, GitSettings.url!!, this)
- REQUEST_PULL -> PullOperation(localDir, this)
- REQUEST_PUSH -> PushOperation(localDir, this)
- REQUEST_SYNC -> SyncOperation(localDir, this)
- BREAK_OUT_OF_DETACHED -> BreakOutOfDetached(localDir, this)
- REQUEST_RESET -> ResetToRemoteOperation(localDir, this)
- else -> {
- tag(TAG).e { "Operation not recognized : $operation" }
- setResult(RESULT_CANCELED)
- finish()
- return
- }
+ val op = when (operation) {
+ REQUEST_CLONE, GitOperation.GET_SSH_KEY_FROM_CLONE -> CloneOperation(this, GitSettings.url!!)
+ REQUEST_PULL -> PullOperation(this)
+ REQUEST_PUSH -> PushOperation(this)
+ REQUEST_SYNC -> SyncOperation(this)
+ BREAK_OUT_OF_DETACHED -> BreakOutOfDetached(this)
+ REQUEST_RESET -> ResetToRemoteOperation(this)
+ else -> {
+ tag(TAG).e { "Operation not recognized : $operation" }
+ return Err(IllegalArgumentException("$operation is not a valid Git operation"))
}
- op.executeAfterAuthentication(GitSettings.authMode)
- } catch (e: Exception) {
- e.printStackTrace()
- MaterialAlertDialogBuilder(this).setMessage(e.message).show()
}
+ return op.executeAfterAuthentication(GitSettings.authMode)
+ }
+
+ fun finishOnSuccessHandler(@Suppress("UNUSED_PARAMETER") nothing: Unit) {
+ finish()
+ }
+
+ fun finishAfterPromptOnErrorHandler(err: Throwable) {
+ val error = rootCauseException(err)
+ if (!isExplicitlyUserInitiatedError(error)) {
+ getEncryptedPrefs("git_operation").edit {
+ remove(PreferenceKeys.HTTPS_PASSWORD)
+ }
+ sharedPrefs.edit { remove(PreferenceKeys.SSH_OPENKEYSTORE_KEYID) }
+ d(error)
+ MaterialAlertDialogBuilder(this)
+ .setTitle(resources.getString(R.string.jgit_error_dialog_title))
+ .setMessage(ErrorMessages[error])
+ .setPositiveButton(resources.getString(R.string.dialog_ok)) { _, _ ->
+ finish()
+ }.show()
+ }
+ }
+
+ /**
+ * Check if a given [Throwable] is the result of an error caused by the user cancelling the
+ * operation.
+ */
+ private fun isExplicitlyUserInitiatedError(throwable: Throwable): Boolean {
+ var cause: Throwable? = throwable
+ while (cause != null) {
+ if (cause is SSHException &&
+ cause.disconnectReason == DisconnectReason.AUTH_CANCELLED_BY_USER)
+ return true
+ cause = cause.cause
+ }
+ return false
+ }
+
+ /**
+ * Get the real root cause of a [Throwable] by traversing until known wrapping exceptions are no
+ * longer found.
+ */
+ private fun rootCauseException(throwable: Throwable): Throwable {
+ var rootCause = throwable
+ // JGit's TransportException hides the more helpful SSHJ exceptions.
+ // Also, SSHJ's UserAuthException about exhausting available authentication methods hides
+ // more useful exceptions.
+ while ((rootCause is org.eclipse.jgit.errors.TransportException ||
+ rootCause is org.eclipse.jgit.api.errors.TransportException ||
+ (rootCause is UserAuthException &&
+ rootCause.message == "Exhausted available authentication methods"))) {
+ rootCause = rootCause.cause ?: break
+ }
+ return rootCause
}
companion object {
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitCommandExecutor.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitCommandExecutor.kt
index d5956b1f..8d8648bf 100644
--- a/app/src/main/java/com/zeapo/pwdstore/git/GitCommandExecutor.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/git/GitCommandExecutor.kt
@@ -5,25 +5,18 @@
package com.zeapo.pwdstore.git
-import android.app.Activity
-import android.content.Intent
import android.widget.Toast
import androidx.fragment.app.FragmentActivity
-import com.github.ajalt.timberkt.e
import com.google.android.material.snackbar.Snackbar
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.git.GitException.PullException
import com.zeapo.pwdstore.git.GitException.PushException
import com.zeapo.pwdstore.git.config.GitSettings
import com.zeapo.pwdstore.git.operation.GitOperation
-import com.zeapo.pwdstore.git.sshj.SshjSessionFactory
-import com.zeapo.pwdstore.utils.Result
import com.zeapo.pwdstore.utils.snackbar
+import com.github.michaelbull.result.Result
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
-import net.schmizz.sshj.common.DisconnectReason
-import net.schmizz.sshj.common.SSHException
-import net.schmizz.sshj.userauth.UserAuthException
import org.eclipse.jgit.api.CommitCommand
import org.eclipse.jgit.api.PullCommand
import org.eclipse.jgit.api.PushCommand
@@ -31,25 +24,20 @@ import org.eclipse.jgit.api.RebaseResult
import org.eclipse.jgit.api.StatusCommand
import org.eclipse.jgit.lib.PersonIdent
import org.eclipse.jgit.transport.RemoteRefUpdate
-import org.eclipse.jgit.transport.SshSessionFactory
class GitCommandExecutor(
private val activity: FragmentActivity,
private val operation: GitOperation,
- private val finishWithResultOnEnd: Intent? = Intent(),
- private val finishActivityOnEnd: Boolean = true,
) {
- suspend fun execute() {
- operation.setCredentialProvider()
+ suspend fun execute(): Result<Unit, Throwable> {
val snackbar = activity.snackbar(
message = activity.resources.getString(R.string.git_operation_running),
length = Snackbar.LENGTH_INDEFINITE,
)
// Count the number of uncommitted files
var nbChanges = 0
- var operationResult: Result = Result.Ok
- try {
+ return com.github.michaelbull.result.runCatching {
for (command in operation.commands) {
when (command) {
is StatusCommand -> {
@@ -74,7 +62,7 @@ class GitCommandExecutor(
}
val rr = result.rebaseResult
if (rr.status === RebaseResult.Status.STOPPED) {
- operationResult = Result.Err(PullException.PullRebaseFailed)
+ throw PullException.PullRebaseFailed
}
}
is PushCommand -> {
@@ -84,15 +72,15 @@ class GitCommandExecutor(
for (result in results) {
// Code imported (modified) from Gerrit PushOp, license Apache v2
for (rru in result.remoteUpdates) {
- val error = when (rru.status) {
- RemoteRefUpdate.Status.REJECTED_NONFASTFORWARD -> PushException.NonFastForward
+ when (rru.status) {
+ RemoteRefUpdate.Status.REJECTED_NONFASTFORWARD -> throw PushException.NonFastForward
RemoteRefUpdate.Status.REJECTED_NODELETE,
RemoteRefUpdate.Status.REJECTED_REMOTE_CHANGED,
RemoteRefUpdate.Status.NON_EXISTING,
RemoteRefUpdate.Status.NOT_ATTEMPTED,
- -> PushException.Generic(rru.status.name)
+ -> throw PushException.Generic(rru.status.name)
RemoteRefUpdate.Status.REJECTED_OTHER_REASON -> {
- if ("non-fast-forward" == rru.message) {
+ throw if ("non-fast-forward" == rru.message) {
PushException.RemoteRejected
} else {
PushException.Generic(rru.message)
@@ -106,13 +94,7 @@ class GitCommandExecutor(
Toast.LENGTH_SHORT
).show()
}
- null
}
- else -> null
-
- }
- if (error != null) {
- operationResult = Result.Err(error)
}
}
}
@@ -124,56 +106,8 @@ class GitCommandExecutor(
}
}
}
- } catch (e: Exception) {
- operationResult = Result.Err(e)
- }
- when (operationResult) {
- is Result.Err -> {
- activity.setResult(Activity.RESULT_CANCELED)
- if (isExplicitlyUserInitiatedError(operationResult.err)) {
- // Currently, this is only executed when the user cancels a password prompt
- // during authentication.
- if (finishActivityOnEnd) activity.finish()
- } else {
- e(operationResult.err)
- operation.onError(rootCauseException(operationResult.err))
- }
- }
- is Result.Ok -> {
- operation.onSuccess()
- activity.setResult(Activity.RESULT_OK, finishWithResultOnEnd)
- if (finishActivityOnEnd) activity.finish()
- }
- }
- snackbar.dismiss()
- withContext(Dispatchers.IO) {
- (SshSessionFactory.getInstance() as? SshjSessionFactory)?.close()
- }
- SshSessionFactory.setInstance(null)
- }
-
- private fun isExplicitlyUserInitiatedError(e: Exception): Boolean {
- var cause: Exception? = e
- while (cause != null) {
- if (cause is SSHException &&
- cause.disconnectReason == DisconnectReason.AUTH_CANCELLED_BY_USER)
- return true
- cause = cause.cause as? Exception
- }
- return false
- }
-
- private fun rootCauseException(e: Exception): Exception {
- var rootCause = e
- // JGit's TransportException hides the more helpful SSHJ exceptions.
- // Also, SSHJ's UserAuthException about exhausting available authentication methods hides
- // more useful exceptions.
- while ((rootCause is org.eclipse.jgit.errors.TransportException ||
- rootCause is org.eclipse.jgit.api.errors.TransportException ||
- (rootCause is UserAuthException &&
- rootCause.message == "Exhausted available authentication methods"))) {
- rootCause = rootCause.cause as? Exception ?: break
+ }.also {
+ snackbar.dismiss()
}
- return rootCause
}
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitConfigActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitConfigActivity.kt
index 89376bfd..c6ab3ca3 100644
--- a/app/src/main/java/com/zeapo/pwdstore/git/GitConfigActivity.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/git/GitConfigActivity.kt
@@ -11,6 +11,7 @@ import android.util.Patterns
import androidx.core.os.postDelayed
import androidx.lifecycle.lifecycleScope
import com.github.ajalt.timberkt.e
+import com.github.michaelbull.result.fold
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.zeapo.pwdstore.R
@@ -75,8 +76,34 @@ class GitConfigActivity : BaseGitActivity() {
e(ex) { "Failed to start GitLogActivity" }
}
}
- binding.gitAbortRebase.setOnClickListener { lifecycleScope.launch { launchGitOperation(BREAK_OUT_OF_DETACHED) } }
- binding.gitResetToRemote.setOnClickListener { lifecycleScope.launch { launchGitOperation(REQUEST_RESET) } }
+ binding.gitAbortRebase.setOnClickListener {
+ lifecycleScope.launch {
+ launchGitOperation(BREAK_OUT_OF_DETACHED).fold(
+ success = {
+ MaterialAlertDialogBuilder(this@GitConfigActivity)
+ .setTitle(resources.getString(R.string.git_abort_and_push_title))
+ .setMessage(resources.getString(
+ R.string.git_break_out_of_detached_success,
+ GitSettings.branch,
+ "conflicting-${GitSettings.branch}-...",
+ ))
+ .setOnCancelListener { finish() }
+ .setPositiveButton(resources.getString(R.string.dialog_ok)) { _, _ ->
+ finish()
+ }.show()
+ },
+ failure = ::finishAfterPromptOnErrorHandler,
+ )
+ }
+ }
+ binding.gitResetToRemote.setOnClickListener {
+ lifecycleScope.launch {
+ launchGitOperation(REQUEST_RESET).fold(
+ success = ::finishOnSuccessHandler,
+ failure = ::finishAfterPromptOnErrorHandler,
+ )
+ }
+ }
}
/**
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitOperationActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitOperationActivity.kt
deleted file mode 100644
index 8b772747..00000000
--- a/app/src/main/java/com/zeapo/pwdstore/git/GitOperationActivity.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * 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.content.Intent
-import android.os.Bundle
-import android.view.Menu
-import android.view.MenuItem
-import androidx.lifecycle.lifecycleScope
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.zeapo.pwdstore.R
-import com.zeapo.pwdstore.UserPreference
-import com.zeapo.pwdstore.git.config.GitSettings
-import com.zeapo.pwdstore.utils.PasswordRepository
-import kotlinx.coroutines.launch
-
-open class GitOperationActivity : BaseGitActivity() {
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- when (val reqCode = intent.extras?.getInt(REQUEST_ARG_OP)) {
- REQUEST_PULL -> lifecycleScope.launch { syncRepository(REQUEST_PULL) }
- REQUEST_PUSH -> lifecycleScope.launch { syncRepository(REQUEST_PUSH) }
- REQUEST_SYNC -> lifecycleScope.launch { syncRepository(REQUEST_SYNC) }
- else -> {
- throw IllegalArgumentException("Invalid request code: $reqCode")
- }
- }
- }
-
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- // Inflate the menu; this adds items to the action bar if it is present.
- menuInflater.inflate(R.menu.git_clone, menu)
- return true
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- return when (item.itemId) {
- R.id.user_pref -> try {
- val intent = Intent(this, UserPreference::class.java)
- startActivity(intent)
- true
- } catch (e: Exception) {
- println("Exception caught :(")
- e.printStackTrace()
- false
- }
- else -> super.onOptionsItemSelected(item)
- }
- }
-
- /**
- * Syncs the local repository with the remote one (either pull or push)
- *
- * @param operation the operation to execute can be REQUEST_PULL or REQUEST_PUSH
- */
- private suspend fun syncRepository(operation: Int) {
- if (GitSettings.url.isNullOrEmpty())
- MaterialAlertDialogBuilder(this)
- .setMessage(getString(R.string.set_information_dialog_text))
- .setPositiveButton(getString(R.string.dialog_positive)) { _, _ ->
- val intent = Intent(this, UserPreference::class.java)
- startActivityForResult(intent, REQUEST_PULL)
- }
- .setNegativeButton(getString(R.string.dialog_negative)) { _, _ ->
- // do nothing :(
- setResult(RESULT_OK)
- finish()
- }
- .show()
- else {
- // check that the remote origin is here, else add it
- PasswordRepository.addRemote("origin", GitSettings.url!!, true)
- launchGitOperation(operation)
- }
- }
-}
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt
index 5aa34201..27cd6379 100644
--- a/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt
@@ -9,6 +9,7 @@ import android.os.Handler
import android.view.View
import androidx.core.os.postDelayed
import androidx.lifecycle.lifecycleScope
+import com.github.michaelbull.result.fold
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.zeapo.pwdstore.R
@@ -122,7 +123,10 @@ class GitServerConfigActivity : BaseGitActivity() {
localDir.deleteRecursively()
}
snackbar.dismiss()
- launchGitOperation(REQUEST_CLONE)
+ launchGitOperation(REQUEST_CLONE).fold(
+ success = ::finishOnSuccessHandler,
+ failure = ::finishAfterPromptOnErrorHandler,
+ )
}
} catch (e: IOException) {
// TODO Handle the exception correctly if we are unable to delete the directory...
@@ -153,7 +157,12 @@ class GitServerConfigActivity : BaseGitActivity() {
e.printStackTrace()
MaterialAlertDialogBuilder(this).setMessage(e.message).show()
}
- lifecycleScope.launch { launchGitOperation(REQUEST_CLONE) }
+ lifecycleScope.launch {
+ launchGitOperation(REQUEST_CLONE).fold(
+ success = ::finishOnSuccessHandler,
+ failure = ::finishAfterPromptOnErrorHandler,
+ )
+ }
}
}
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/operation/BreakOutOfDetached.kt b/app/src/main/java/com/zeapo/pwdstore/git/operation/BreakOutOfDetached.kt
index b46b66cf..6c7fee3a 100644
--- a/app/src/main/java/com/zeapo/pwdstore/git/operation/BreakOutOfDetached.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/git/operation/BreakOutOfDetached.kt
@@ -7,46 +7,30 @@ package com.zeapo.pwdstore.git.operation
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.R
-import com.zeapo.pwdstore.git.GitCommandExecutor
-import java.io.File
import org.eclipse.jgit.api.RebaseCommand
-class BreakOutOfDetached(fileDir: File, callingActivity: AppCompatActivity) : GitOperation(fileDir, callingActivity) {
-
- private val branchName = "conflicting-$remoteBranch-${System.currentTimeMillis()}"
+class BreakOutOfDetached(callingActivity: AppCompatActivity) : GitOperation(callingActivity) {
override val commands = arrayOf(
// abort the rebase
git.rebase().setOperation(RebaseCommand.Operation.ABORT),
// git checkout -b conflict-branch
- git.checkout().setCreateBranch(true).setName(branchName),
+ git.checkout().setCreateBranch(true).setName("conflicting-$remoteBranch-${System.currentTimeMillis()}"),
// push the changes
git.push().setRemote("origin"),
// switch back to ${gitBranch}
git.checkout().setName(remoteBranch),
)
- override suspend fun execute() {
- if (!git.repository.repositoryState.isRebasing) {
- MaterialAlertDialogBuilder(callingActivity)
- .setTitle(callingActivity.resources.getString(R.string.git_abort_and_push_title))
- .setMessage("The repository is not rebasing, no need to push to another branch")
- .setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ ->
- callingActivity.finish()
- }.show()
- return
- }
- GitCommandExecutor(callingActivity, this).execute()
- }
-
- override fun onSuccess() {
+ override fun preExecute() = if (!git.repository.repositoryState.isRebasing) {
MaterialAlertDialogBuilder(callingActivity)
.setTitle(callingActivity.resources.getString(R.string.git_abort_and_push_title))
- .setMessage("There was a conflict when trying to rebase. " +
- "Your local $remoteBranch branch was pushed to another branch named conflicting-$remoteBranch-....\n" +
- "Use this branch to resolve conflict on your computer")
+ .setMessage(callingActivity.resources.getString(R.string.git_break_out_of_detached_unneeded))
.setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ ->
callingActivity.finish()
}.show()
+ false
+ } else {
+ true
}
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/operation/CloneOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/operation/CloneOperation.kt
index a032a669..256437a8 100644
--- a/app/src/main/java/com/zeapo/pwdstore/git/operation/CloneOperation.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/git/operation/CloneOperation.kt
@@ -5,34 +5,18 @@
package com.zeapo.pwdstore.git.operation
import androidx.appcompat.app.AppCompatActivity
-import com.zeapo.pwdstore.git.GitCommandExecutor
-import java.io.File
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.GitCommand
/**
* Creates a new clone operation
*
- * @param fileDir the git working tree directory
* @param uri URL to clone the repository from
* @param callingActivity the calling activity
*/
-class CloneOperation(fileDir: File, uri: String, callingActivity: AppCompatActivity) : GitOperation(fileDir, callingActivity) {
+class CloneOperation(callingActivity: AppCompatActivity, uri: String) : GitOperation(callingActivity) {
override val commands: Array<GitCommand<out Any>> = arrayOf(
- Git.cloneRepository().setBranch(remoteBranch).setDirectory(repository?.workTree).setURI(uri),
+ Git.cloneRepository().setBranch(remoteBranch).setDirectory(repository.workTree).setURI(uri),
)
-
- override suspend fun execute() {
- GitCommandExecutor(callingActivity, this, finishActivityOnEnd = false).execute()
- }
-
- override fun onSuccess() {
- callingActivity.finish()
- }
-
- override fun onError(err: Exception) {
- finishFromErrorDialog = false
- super.onError(err)
- }
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/operation/GitOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/operation/GitOperation.kt
index 1b47d79b..2a72005e 100644
--- a/app/src/main/java/com/zeapo/pwdstore/git/operation/GitOperation.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/git/operation/GitOperation.kt
@@ -6,30 +6,27 @@ package com.zeapo.pwdstore.git.operation
import android.content.Intent
import android.widget.Toast
-import androidx.annotation.CallSuper
-import androidx.core.content.edit
import androidx.fragment.app.FragmentActivity
-import com.github.ajalt.timberkt.Timber.d
+import com.github.michaelbull.result.Err
+import com.github.michaelbull.result.Ok
+import com.github.michaelbull.result.Result
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.UserPreference
-import com.zeapo.pwdstore.git.ErrorMessages
+import com.zeapo.pwdstore.git.GitCommandExecutor
import com.zeapo.pwdstore.git.config.AuthMode
import com.zeapo.pwdstore.git.config.GitSettings
-import com.zeapo.pwdstore.git.sshj.InteractivePasswordFinder
import com.zeapo.pwdstore.git.sshj.SshAuthData
import com.zeapo.pwdstore.git.sshj.SshKey
import com.zeapo.pwdstore.git.sshj.SshjSessionFactory
import com.zeapo.pwdstore.utils.BiometricAuthenticator
import com.zeapo.pwdstore.utils.PasswordRepository
-import com.zeapo.pwdstore.utils.PreferenceKeys
-import com.zeapo.pwdstore.utils.getEncryptedPrefs
-import com.zeapo.pwdstore.utils.sharedPrefs
-import java.io.File
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
+import net.schmizz.sshj.common.DisconnectReason
+import net.schmizz.sshj.common.SSHException
import net.schmizz.sshj.userauth.password.PasswordFinder
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.GitCommand
@@ -37,24 +34,22 @@ import org.eclipse.jgit.api.TransportCommand
import org.eclipse.jgit.errors.UnsupportedCredentialItem
import org.eclipse.jgit.transport.CredentialItem
import org.eclipse.jgit.transport.CredentialsProvider
-import org.eclipse.jgit.transport.SshSessionFactory
+import org.eclipse.jgit.transport.SshTransport
+import org.eclipse.jgit.transport.Transport
import org.eclipse.jgit.transport.URIish
-const val ANDROID_KEYSTORE_ALIAS_SSH_KEY = "ssh_key"
-
/**
* Creates a new git operation
*
- * @param gitDir the git working tree directory
* @param callingActivity the calling activity
*/
-abstract class GitOperation(gitDir: File, internal val callingActivity: FragmentActivity) {
+abstract class GitOperation(protected val callingActivity: FragmentActivity) {
abstract val commands: Array<GitCommand<out Any>>
- private var provider: CredentialsProvider? = null
private val hostKeyFile = callingActivity.filesDir.resolve(".host_key")
- protected var finishFromErrorDialog = true
- protected val repository = PasswordRepository.getRepository(gitDir)
+ private var sshSessionFactory: SshjSessionFactory? = null
+
+ protected val repository = PasswordRepository.getRepository(null)!!
protected val git = Git(repository)
protected val remoteBranch = GitSettings.branch
@@ -90,27 +85,6 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: Fragment
}
}
- private fun withPasswordAuthentication(passwordFinder: InteractivePasswordFinder): GitOperation {
- val sessionFactory = SshjSessionFactory(SshAuthData.Password(passwordFinder), hostKeyFile)
- SshSessionFactory.setInstance(sessionFactory)
- this.provider = HttpsCredentialsProvider(passwordFinder)
- return this
- }
-
- private fun withSshKeyAuthentication(passphraseFinder: InteractivePasswordFinder): GitOperation {
- val sessionFactory = SshjSessionFactory(SshAuthData.SshKey(passphraseFinder), hostKeyFile)
- SshSessionFactory.setInstance(sessionFactory)
- this.provider = null
- return this
- }
-
- private fun withOpenKeychainAuthentication(activity: FragmentActivity): GitOperation {
- val sessionFactory = SshjSessionFactory(SshAuthData.OpenKeychain(activity), hostKeyFile)
- SshSessionFactory.setInstance(sessionFactory)
- this.provider = null
- return this
- }
-
private fun getSshKey(make: Boolean) {
try {
// Ask the UserPreference to provide us with the ssh-key
@@ -124,16 +98,30 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: Fragment
}
}
- fun setCredentialProvider() {
- provider?.let { credentialsProvider ->
- commands.filterIsInstance<TransportCommand<*, *>>().forEach { it.setCredentialsProvider(credentialsProvider) }
+ private fun registerAuthProviders(authData: SshAuthData, credentialsProvider: CredentialsProvider? = null) {
+ sshSessionFactory = SshjSessionFactory(authData, hostKeyFile)
+ commands.filterIsInstance<TransportCommand<*, *>>().forEach { command ->
+ command.setTransportConfigCallback { transport: Transport ->
+ (transport as? SshTransport)?.sshSessionFactory = sshSessionFactory
+ credentialsProvider?.let { transport.credentialsProvider = it }
+ }
}
}
/**
- * Executes the GitCommand in an async task
+ * Executes the GitCommand in an async task.
*/
- abstract suspend fun execute()
+ suspend fun execute(): Result<Unit, Throwable> {
+ if (!preExecute()) {
+ return Ok(Unit)
+ }
+ val operationResult = GitCommandExecutor(
+ callingActivity,
+ this,
+ ).execute()
+ postExecute()
+ return operationResult
+ }
private fun onMissingSshKeyFile() {
MaterialAlertDialogBuilder(callingActivity)
@@ -151,9 +139,7 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: Fragment
}.show()
}
- suspend fun executeAfterAuthentication(
- authMode: AuthMode,
- ) {
+ suspend fun executeAfterAuthentication(authMode: AuthMode): Result<Unit, Throwable> {
when (authMode) {
AuthMode.SshKey -> if (SshKey.exists) {
if (SshKey.mustAuthenticate) {
@@ -167,9 +153,12 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: Fragment
}
when (result) {
is BiometricAuthenticator.Result.Success -> {
- withSshKeyAuthentication(CredentialFinder(callingActivity, authMode)).execute()
+ registerAuthProviders(
+ SshAuthData.SshKey(CredentialFinder(callingActivity, AuthMode.SshKey)))
+ }
+ is BiometricAuthenticator.Result.Cancelled -> {
+ return Err(SSHException(DisconnectReason.AUTH_CANCELLED_BY_USER))
}
- is BiometricAuthenticator.Result.Cancelled -> callingActivity.finish()
is BiometricAuthenticator.Result.Failure -> {
throw IllegalStateException("Biometric authentication failures should be ignored")
}
@@ -183,42 +172,37 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: Fragment
}
}
} else {
- withSshKeyAuthentication(CredentialFinder(callingActivity, authMode)).execute()
+ registerAuthProviders(SshAuthData.SshKey(CredentialFinder(callingActivity, AuthMode.SshKey)))
}
} else {
onMissingSshKeyFile()
}
- AuthMode.OpenKeychain -> withOpenKeychainAuthentication(callingActivity).execute()
- AuthMode.Password -> withPasswordAuthentication(
- CredentialFinder(callingActivity, authMode)).execute()
- AuthMode.None -> execute()
+ AuthMode.OpenKeychain -> registerAuthProviders(SshAuthData.OpenKeychain(callingActivity))
+ AuthMode.Password -> {
+ val credentialFinder = CredentialFinder(callingActivity, AuthMode.Password)
+ val httpsCredentialProvider = HttpsCredentialsProvider(credentialFinder)
+ registerAuthProviders(
+ SshAuthData.Password(CredentialFinder(callingActivity, AuthMode.Password)),
+ httpsCredentialProvider)
+ }
+ AuthMode.None -> {
+ }
}
+ return execute()
}
/**
- * Action to execute on error
+ * Called before execution of the Git operation.
+ * Return false to cancel.
*/
- @CallSuper
- open fun onError(err: Exception) {
- // Clear various auth related fields on failure
- callingActivity.getEncryptedPrefs("git_operation").edit {
- remove(PreferenceKeys.HTTPS_PASSWORD)
+ open fun preExecute() = true
+
+ private suspend fun postExecute() {
+ withContext(Dispatchers.IO) {
+ sshSessionFactory?.close()
}
- callingActivity.sharedPrefs.edit { remove(PreferenceKeys.SSH_OPENKEYSTORE_KEYID) }
- d(err)
- MaterialAlertDialogBuilder(callingActivity)
- .setTitle(callingActivity.resources.getString(R.string.jgit_error_dialog_title))
- .setMessage(ErrorMessages[err])
- .setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ ->
- if (finishFromErrorDialog) callingActivity.finish()
- }.show()
}
- /**
- * Action to execute on success
- */
- open fun onSuccess() {}
-
companion object {
const val GET_SSH_KEY_FROM_CLONE = 201
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/operation/PullOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/operation/PullOperation.kt
index d31b2aa4..d4ce6d01 100644
--- a/app/src/main/java/com/zeapo/pwdstore/git/operation/PullOperation.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/git/operation/PullOperation.kt
@@ -5,24 +5,11 @@
package com.zeapo.pwdstore.git.operation
import androidx.appcompat.app.AppCompatActivity
-import com.zeapo.pwdstore.git.GitCommandExecutor
-import java.io.File
-import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.GitCommand
-/**
- * Creates a new git operation
- *
- * @param fileDir the git working tree directory
- * @param callingActivity the calling activity
- */
-class PullOperation(fileDir: File, callingActivity: AppCompatActivity) : GitOperation(fileDir, callingActivity) {
+class PullOperation(callingActivity: AppCompatActivity) : GitOperation(callingActivity) {
override val commands: Array<GitCommand<out Any>> = arrayOf(
- Git(repository).pull().setRebase(true).setRemote("origin"),
+ git.pull().setRebase(true).setRemote("origin"),
)
-
- override suspend fun execute() {
- GitCommandExecutor(callingActivity, this).execute()
- }
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/operation/PushOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/operation/PushOperation.kt
index 33b20a06..d3bee209 100644
--- a/app/src/main/java/com/zeapo/pwdstore/git/operation/PushOperation.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/git/operation/PushOperation.kt
@@ -5,25 +5,11 @@
package com.zeapo.pwdstore.git.operation
import androidx.appcompat.app.AppCompatActivity
-import com.zeapo.pwdstore.git.GitCommandExecutor
-import java.io.File
-import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.GitCommand
-/**
- * Creates a new git operation
- *
- * @param fileDir the git working tree directory
- * @param callingActivity the calling activity
- */
-class PushOperation(fileDir: File, callingActivity: AppCompatActivity) : GitOperation(fileDir, callingActivity) {
+class PushOperation(callingActivity: AppCompatActivity) : GitOperation(callingActivity) {
override val commands: Array<GitCommand<out Any>> = arrayOf(
- Git(repository).push().setPushAll().setRemote("origin"),
+ git.push().setPushAll().setRemote("origin"),
)
-
- override suspend fun execute() {
- setCredentialProvider()
- GitCommandExecutor(callingActivity, this).execute()
- }
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/operation/ResetToRemoteOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/operation/ResetToRemoteOperation.kt
index 0d0dc019..2df7a2b4 100644
--- a/app/src/main/java/com/zeapo/pwdstore/git/operation/ResetToRemoteOperation.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/git/operation/ResetToRemoteOperation.kt
@@ -5,17 +5,9 @@
package com.zeapo.pwdstore.git.operation
import androidx.appcompat.app.AppCompatActivity
-import com.zeapo.pwdstore.git.GitCommandExecutor
-import java.io.File
import org.eclipse.jgit.api.ResetCommand
-/**
- * Creates a new git operation
- *
- * @param fileDir the git working tree directory
- * @param callingActivity the calling activity
- */
-class ResetToRemoteOperation(fileDir: File, callingActivity: AppCompatActivity) : GitOperation(fileDir, callingActivity) {
+class ResetToRemoteOperation(callingActivity: AppCompatActivity) : GitOperation(callingActivity) {
override val commands = arrayOf(
// Stage all files
@@ -28,8 +20,4 @@ class ResetToRemoteOperation(fileDir: File, callingActivity: AppCompatActivity)
// branches from 'master' to anything else.
git.branchCreate().setName(remoteBranch).setForce(true),
)
-
- override suspend fun execute() {
- GitCommandExecutor(callingActivity, this).execute()
- }
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/operation/SyncOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/operation/SyncOperation.kt
index 35b12810..a0a80fe3 100644
--- a/app/src/main/java/com/zeapo/pwdstore/git/operation/SyncOperation.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/git/operation/SyncOperation.kt
@@ -5,16 +5,8 @@
package com.zeapo.pwdstore.git.operation
import androidx.appcompat.app.AppCompatActivity
-import com.zeapo.pwdstore.git.GitCommandExecutor
-import java.io.File
-/**
- * Creates a new git operation
- *
- * @param fileDir the git working tree directory
- * @param callingActivity the calling activity
- */
-class SyncOperation(fileDir: File, callingActivity: AppCompatActivity) : GitOperation(fileDir, callingActivity) {
+class SyncOperation(callingActivity: AppCompatActivity) : GitOperation(callingActivity) {
override val commands = arrayOf(
// Stage all files
@@ -28,8 +20,4 @@ class SyncOperation(fileDir: File, callingActivity: AppCompatActivity) : GitOper
// Push it all back
git.push().setPushAll().setRemote("origin"),
)
-
- override suspend fun execute() {
- GitCommandExecutor(callingActivity, this).execute()
- }
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt b/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt
index b5295f17..1eb1c95d 100644
--- a/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt
@@ -77,7 +77,6 @@ class FolderCreationDialogFragment : DialogFragment() {
R.string.git_commit_gpg_id,
BasePgpActivity.getLongName(gpgIdentifierFile.parentFile!!.absolutePath, repoPath, gpgIdentifierFile.name)
),
- finishActivityOnEnd = false,
)
dismiss()
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt
index f41899b9..eea69dc4 100644
--- a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt
@@ -7,7 +7,6 @@ package com.zeapo.pwdstore.utils
import android.app.KeyguardManager
import android.content.ClipboardManager
import android.content.Context
-import android.content.Intent
import android.content.SharedPreferences
import android.os.Build
import android.util.Base64
@@ -24,8 +23,10 @@ import androidx.preference.PreferenceManager
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import com.github.ajalt.timberkt.d
+import com.github.michaelbull.result.Ok
+import com.github.michaelbull.result.Result
import com.google.android.material.snackbar.Snackbar
-import com.zeapo.pwdstore.git.GitCommandExecutor
+import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.git.operation.GitOperation
import com.zeapo.pwdstore.utils.PasswordRepository.Companion.getRepositoryDirectory
import java.io.File
@@ -59,6 +60,7 @@ fun FragmentActivity.snackbar(
length: Int = Snackbar.LENGTH_SHORT,
): Snackbar {
val snackbar = Snackbar.make(view, message, length)
+ snackbar.anchorView = findViewById(R.id.fab)
snackbar.show()
return snackbar
}
@@ -108,17 +110,11 @@ fun SharedPreferences.getString(key: String): String? = getString(key, null)
suspend fun FragmentActivity.commitChange(
message: String,
- finishWithResultOnEnd: Intent? = null,
- finishActivityOnEnd: Boolean = true,
-) {
+): Result<Unit, Throwable> {
if (!PasswordRepository.isGitRepo()) {
- if (finishWithResultOnEnd != null) {
- setResult(FragmentActivity.RESULT_OK, finishWithResultOnEnd)
- finish()
- }
- return
+ return Ok(Unit)
}
- object : GitOperation(getRepositoryDirectory(), this@commitChange) {
+ return object : GitOperation(this@commitChange) {
override val commands = arrayOf(
// Stage all files
git.add().addFilepattern("."),
@@ -128,14 +124,9 @@ suspend fun FragmentActivity.commitChange(
git.commit().setAll(true).setMessage(message),
)
- override suspend fun execute() {
- d { "Comitting with message: '$message'" }
- GitCommandExecutor(
- this@commitChange,
- this,
- finishWithResultOnEnd,
- finishActivityOnEnd,
- ).execute()
+ override fun preExecute(): Boolean {
+ d { "Committing with message: '$message'" }
+ return true
}
}.execute()
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/Result.kt b/app/src/main/java/com/zeapo/pwdstore/utils/Result.kt
deleted file mode 100644
index d152cba6..00000000
--- a/app/src/main/java/com/zeapo/pwdstore/utils/Result.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-/*
- * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-package com.zeapo.pwdstore.utils
-
-/**
- * Emulates the Rust Result enum but without returning a value in the [Ok] case.
- * https://doc.rust-lang.org/std/result/enum.Result.html
- */
-sealed class Result {
-
- object Ok : Result()
- data class Err(val err: Exception) : Result()
-}
diff --git a/app/src/main/res/layout/activity_git_clone.xml b/app/src/main/res/layout/activity_git_clone.xml
index b7ec6fc1..ad0a186b 100644
--- a/app/src/main/res/layout/activity_git_clone.xml
+++ b/app/src/main/res/layout/activity_git_clone.xml
@@ -11,7 +11,7 @@
android:background="?android:attr/windowBackground"
android:padding="@dimen/activity_horizontal_margin"
tools:background="@color/white"
- tools:context="com.zeapo.pwdstore.git.GitOperationActivity">
+ tools:context="com.zeapo.pwdstore.git.GitServerConfigActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
diff --git a/app/src/main/res/menu/git_clone.xml b/app/src/main/res/menu/git_clone.xml
deleted file mode 100644
index 5d5bd8c8..00000000
--- a/app/src/main/res/menu/git_clone.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-<!--
- ~ Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
- ~ SPDX-License-Identifier: GPL-3.0-only
- -->
-
-<menu xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:pwstore="http://schemas.android.com/apk/res-auto"
- xmlns:tools="http://schemas.android.com/tools"
- tools:context="com.zeapo.pwdstore.git.GitServerConfigActivity">
- <item
- android:id="@+id/user_pref"
- android:orderInCategory="100"
- android:title="@string/action_settings"
- pwstore:showAsAction="never" />
-</menu>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 5c8b75cd..99ff2223 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -402,6 +402,8 @@
<string name="git_push_other_error">Remote rejected non-fast-forward push. Check receive.denyNonFastForwards variable in config file of destination repository.</string>
<string name="git_unknown_host">Unknown host: %1$s</string>
<string name="git_operation_running">Running git operation…</string>
+ <string name="git_break_out_of_detached_success">There was a conflict when trying to rebase. Your local %1$s branch was pushed to another branch named %2$s\n Use this branch to resolve conflict on your computer</string>
+ <string name="git_break_out_of_detached_unneeded">The repository is not rebasing, no need to push to another branch</string>
<!-- OpenKeychain not installed -->
<string name="openkeychain_not_installed_title">OpenKeychain not installed</string>