Remove GitAsyncTask and replace with non-blocking coroutines (#865)

Co-authored-by: Fabian Henneke <fabian@henneke.me>
This commit is contained in:
Harsh Shandilya 2020-08-05 19:02:24 +05:30 committed by GitHub
parent 12a83e5c36
commit 14c44bf584
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 632 additions and 650 deletions

View file

@ -23,6 +23,7 @@ class Application : android.app.Application(), SharedPreferences.OnSharedPrefere
override fun onCreate() {
super.onCreate()
instance = this
prefs = PreferenceManager.getDefaultSharedPreferences(this)
if (BuildConfig.ENABLE_DEBUG_FEATURES || prefs?.getBoolean(PreferenceKeys.ENABLE_DEBUG_LOGGING, false) ==
true) {
@ -52,4 +53,9 @@ class Application : android.app.Application(), SharedPreferences.OnSharedPrefere
else -> MODE_NIGHT_AUTO_BATTERY
})
}
companion object {
lateinit var instance: Application
}
}

View file

@ -18,8 +18,6 @@ import androidx.activity.result.contract.ActivityResultContracts.StartActivityFo
import androidx.appcompat.view.ActionMode
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.setFragmentResultListener
import androidx.lifecycle.observe
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager

View file

@ -34,7 +34,6 @@ import androidx.fragment.app.FragmentManager
import androidx.fragment.app.commit
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.observe
import androidx.preference.PreferenceManager
import com.github.ajalt.timberkt.d
import com.github.ajalt.timberkt.e
@ -581,7 +580,12 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
intent.putExtra("REPO_PATH", getRepositoryDirectory(applicationContext).absolutePath)
registerForActivityResult(StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
commitChange(resources.getString(R.string.git_commit_add_text, result.data?.extras?.getString("LONG_NAME")))
lifecycleScope.launch {
commitChange(
resources.getString(R.string.git_commit_add_text, result.data?.extras?.getString("LONG_NAME")),
finishActivityOnEnd = false,
)
}
refreshPasswordList()
}
}.launch(intent)
@ -618,11 +622,15 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
selectedItems.map { item -> item.file.deleteRecursively() }
refreshPasswordList()
AutofillMatcher.updateMatches(applicationContext, delete = filesToDelete)
commitChange(resources.getString(R.string.git_commit_remove_text,
selectedItems.joinToString(separator = ", ") { item ->
item.file.toRelativeString(getRepositoryDirectory(this))
}
))
val fmt = selectedItems.joinToString(separator = ", ") { item ->
item.file.toRelativeString(getRepositoryDirectory(this@PasswordStore))
}
lifecycleScope.launch {
commitChange(
resources.getString(R.string.git_commit_remove_text, fmt),
finishActivityOnEnd = false,
)
}
}
.setNegativeButton(resources.getString(R.string.dialog_no), null)
.show()
@ -688,14 +696,20 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
val sourceLongName = getLongName(requireNotNull(source.parent), repositoryPath, basename)
val destinationLongName = getLongName(target.absolutePath, repositoryPath, basename)
withContext(Dispatchers.Main) {
commitChange(resources.getString(R.string.git_commit_move_text, sourceLongName, destinationLongName))
commitChange(
resources.getString(R.string.git_commit_move_text, sourceLongName, destinationLongName),
finishActivityOnEnd = false,
)
}
}
else -> {
val repoDir = getRepositoryDirectory(applicationContext).absolutePath
val relativePath = getRelativePath("${target.absolutePath}/", repoDir)
withContext(Dispatchers.Main) {
commitChange(resources.getString(R.string.git_commit_move_multiple_text,
getRelativePath("${target.absolutePath}/", getRepositoryDirectory(applicationContext).absolutePath)
))
commitChange(
resources.getString(R.string.git_commit_move_multiple_text, relativePath),
finishActivityOnEnd = false,
)
}
}
}
@ -746,7 +760,10 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
else -> lifecycleScope.launch(Dispatchers.IO) {
moveFile(oldCategory.file, newCategory)
withContext(Dispatchers.Main) {
commitChange(resources.getString(R.string.git_commit_move_text, oldCategory.name, newCategory.name))
commitChange(
resources.getString(R.string.git_commit_move_text, oldCategory.name, newCategory.name),
finishActivityOnEnd = false,
)
}
}
}

View file

@ -10,7 +10,6 @@ import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.observe
import androidx.recyclerview.widget.LinearLayoutManager
import com.zeapo.pwdstore.databinding.PasswordRecyclerViewBinding
import com.zeapo.pwdstore.ui.adapters.PasswordItemRecyclerAdapter

View file

@ -22,7 +22,6 @@ import androidx.core.text.buildSpannedString
import androidx.core.text.underline
import androidx.core.widget.addTextChangedListener
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.observe
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.ajalt.timberkt.e
import com.zeapo.pwdstore.FilterMode

View file

@ -15,6 +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
@ -27,6 +28,7 @@ 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() {
@ -144,10 +146,12 @@ class AutofillSaveActivity : AppCompatActivity() {
}
// PasswordCreationActivity delegates committing the added file to PasswordStore. Since
// PasswordStore is not involved in an AutofillScenario, we have to commit the file ourselves.
commitChange(
getString(R.string.git_commit_add_text, longName),
finishWithResultOnEnd = resultIntent
)
lifecycleScope.launch {
commitChange(
getString(R.string.git_commit_add_text, longName),
finishWithResultOnEnd = resultIntent
)
}
// GitAsyncTask will finish the activity for us.
}
}.launch(saveIntent)

View file

@ -329,12 +329,14 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
gpgIdentifierFile.writeText(keyIds.joinToString("\n"))
val repo = PasswordRepository.getRepository(null)
if (repo != null) {
commitChange(
getString(
R.string.git_commit_gpg_id,
getLongName(gpgIdentifierFile.parentFile!!.absolutePath, repoPath, gpgIdentifierFile.name)
lifecycleScope.launch {
commitChange(
getString(
R.string.git_commit_gpg_id,
getLongName(gpgIdentifierFile.parentFile!!.absolutePath, repoPath, gpgIdentifierFile.name)
)
)
)
}
}
encrypt(data)
}
@ -422,7 +424,8 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
AutofillPreferences.directoryStructure(applicationContext)
val entry = PasswordEntry(content)
returnIntent.putExtra(RETURN_EXTRA_PASSWORD, entry.password)
val username = entry.username ?: directoryStructure.getUsernameFor(file)
val username = entry.username
?: directoryStructure.getUsernameFor(file)
returnIntent.putExtra(RETURN_EXTRA_USERNAME, username)
}
@ -430,12 +433,14 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
if (repo != null) {
val status = Git(repo).status().call()
if (status.modified.isNotEmpty()) {
commitChange(
getString(
R.string.git_commit_edit_text,
getLongName(fullPath, repoPath, editName)
lifecycleScope.launch {
commitChange(
getString(
R.string.git_commit_edit_text,
getLongName(fullPath, repoPath, editName)
)
)
)
}
}
}

View file

@ -12,6 +12,7 @@ import androidx.annotation.CallSuper
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.edit
import androidx.core.text.isDigitsOnly
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import com.github.ajalt.timberkt.Timber.tag
import com.github.ajalt.timberkt.e
@ -20,11 +21,19 @@ import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.git.config.ConnectionMode
import com.zeapo.pwdstore.git.config.Protocol
import com.zeapo.pwdstore.git.config.SshApiSessionFactory
import com.zeapo.pwdstore.git.operation.BreakOutOfDetached
import com.zeapo.pwdstore.git.operation.CloneOperation
import com.zeapo.pwdstore.git.operation.GitOperation
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 java.io.File
import java.net.URI
import kotlinx.coroutines.launch
/**
* Abstract AppCompatActivity that holds some information that is commonly shared across git-related
@ -166,7 +175,7 @@ abstract class BaseGitActivity : AppCompatActivity() {
*
* @param operation The type of git operation to launch
*/
fun launchGitOperation(operation: Int) {
suspend fun launchGitOperation(operation: Int) {
if (url == null) {
setResult(RESULT_CANCELED)
finish()
@ -190,12 +199,12 @@ abstract class BaseGitActivity : AppCompatActivity() {
val localDir = requireNotNull(PasswordRepository.getRepositoryDirectory(this))
val op = when (operation) {
REQUEST_CLONE, GitOperation.GET_SSH_KEY_FROM_CLONE -> CloneOperation(localDir, this).setCommand(url!!)
REQUEST_PULL -> PullOperation(localDir, this).setCommand()
REQUEST_PUSH -> PushOperation(localDir, this).setCommand()
REQUEST_SYNC -> SyncOperation(localDir, this).setCommands()
BREAK_OUT_OF_DETACHED -> BreakOutOfDetached(localDir, this).setCommands()
REQUEST_RESET -> ResetToRemoteOperation(localDir, this).setCommands()
REQUEST_CLONE, GitOperation.GET_SSH_KEY_FROM_CLONE -> CloneOperation(localDir, 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)
SshApiSessionFactory.POST_SIGNATURE -> return
else -> {
tag(TAG).e { "Operation not recognized : $operation" }
@ -239,7 +248,7 @@ abstract class BaseGitActivity : AppCompatActivity() {
if (identityBuilder != null) {
identityBuilder!!.consume(data)
}
launchGitOperation(requestCode)
lifecycleScope.launch { launchGitOperation(requestCode) }
}
super.onActivityResult(requestCode, resultCode, data)
}

View file

@ -1,90 +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 androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.utils.PreferenceKeys
import java.io.File
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.GitCommand
import org.eclipse.jgit.api.PushCommand
import org.eclipse.jgit.api.RebaseCommand
class BreakOutOfDetached(fileDir: File, callingActivity: AppCompatActivity) : GitOperation(fileDir, callingActivity) {
private lateinit var commands: List<GitCommand<out Any>>
private val gitBranch = PreferenceManager
.getDefaultSharedPreferences(callingActivity.applicationContext)
.getString(PreferenceKeys.GIT_BRANCH_NAME, "master")
/**
* Sets the command
*
* @return the current object
*/
fun setCommands(): BreakOutOfDetached {
val git = Git(repository)
val branchName = "conflicting-$gitBranch-${System.currentTimeMillis()}"
this.commands = listOf(
// abort the rebase
git.rebase().setOperation(RebaseCommand.Operation.ABORT),
// git checkout -b conflict-branch
git.checkout().setCreateBranch(true).setName(branchName),
// push the changes
git.push().setRemote("origin"),
// switch back to ${gitBranch}
git.checkout().setName(gitBranch)
)
return this
}
override fun execute() {
val git = Git(repository)
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
}
if (this.provider != null) {
// set the credentials for push command
this.commands.forEach { cmd ->
if (cmd is PushCommand) {
cmd.setCredentialsProvider(this.provider)
}
}
}
GitAsyncTask(callingActivity, this, null)
.execute(*this.commands.toTypedArray())
}
override fun onError(err: Exception) {
MaterialAlertDialogBuilder(callingActivity)
.setTitle(callingActivity.resources.getString(R.string.jgit_error_dialog_title))
.setMessage("Error occurred when checking out another branch operation ${err.message}")
.setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ ->
callingActivity.finish()
}.show()
}
override fun onSuccess() {
MaterialAlertDialogBuilder(callingActivity)
.setTitle(callingActivity.resources.getString(R.string.git_abort_and_push_title))
.setMessage("There was a conflict when trying to rebase. " +
"Your local $gitBranch branch was pushed to another branch named conflicting-$gitBranch-....\n" +
"Use this branch to resolve conflict on your computer")
.setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ ->
callingActivity.finish()
}.show()
}
}

View file

@ -1,53 +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 androidx.appcompat.app.AppCompatActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.R
import java.io.File
import org.eclipse.jgit.api.CloneCommand
import org.eclipse.jgit.api.Git
/**
* Creates a new clone operation
*
* @param fileDir the git working tree directory
* @param callingActivity the calling activity
*/
class CloneOperation(fileDir: File, callingActivity: AppCompatActivity) : GitOperation(fileDir, callingActivity) {
/**
* Sets the command using the repository uri
*
* @param uri the uri of the repository
* @return the current object
*/
fun setCommand(uri: String): CloneOperation {
this.command = Git.cloneRepository()
.setCloneAllBranches(true)
.setDirectory(repository?.workTree)
.setURI(uri)
return this
}
override fun execute() {
(this.command as? CloneCommand)?.setCredentialsProvider(this.provider)
GitAsyncTask(callingActivity, this, Intent()).execute(this.command)
}
override fun onError(err: Exception) {
super.onError(err)
MaterialAlertDialogBuilder(callingActivity)
.setTitle(callingActivity.resources.getString(R.string.jgit_error_dialog_title))
.setMessage("Error occurred during the clone operation, " +
callingActivity.resources.getString(R.string.jgit_error_dialog_text) +
err.message +
"\nPlease check the FAQ for possible reasons why this error might occur.")
.setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> }
.show()
}
}

View file

@ -0,0 +1,75 @@
/*
* 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.RemoteException
import com.zeapo.pwdstore.Application
import com.zeapo.pwdstore.R
/**
* Supertype for all Git-related [Exception]s that can be thrown by [GitCommandExecutor.execute].
*/
sealed class GitException(message: String? = null) : Exception(message) {
/**
* Encapsulates possible errors from a [org.eclipse.jgit.api.PullCommand].
*/
class PullException(val reason: Reason) : GitException() {
enum class Reason {
REBASE_FAILED,
}
}
/**
* Encapsulates possible errors from a [org.eclipse.jgit.api.PushCommand].
*/
class PushException(val reason: Reason, vararg val fmt: String) : GitException() {
enum class Reason {
NON_FAST_FORWARD,
REMOTE_REJECTED,
GENERIC,
}
}
}
object ErrorMessages {
private val PULL_REASON_MAP = mapOf(
GitException.PullException.Reason.REBASE_FAILED to R.string.git_pull_fail_error,
)
private val PUSH_REASON_MAP = mapOf(
GitException.PushException.Reason.NON_FAST_FORWARD to R.string.git_push_nff_error,
GitException.PushException.Reason.REMOTE_REJECTED to R.string.git_push_other_error,
GitException.PushException.Reason.GENERIC to R.string.git_push_generic_error,
)
operator fun get(throwable: Throwable?): String {
val resources = Application.instance.resources
if (throwable == null) return resources.getString(R.string.git_unknown_error)
return when (val rootCause = rootCause(throwable)) {
is GitException.PullException -> {
resources.getString(PULL_REASON_MAP.getValue(rootCause.reason))
}
is GitException.PushException -> {
resources.getString(PUSH_REASON_MAP.getValue(rootCause.reason), *rootCause.fmt)
}
else -> throwable.message ?: resources.getString(R.string.git_unknown_error)
}
}
private fun rootCause(throwable: Throwable): Throwable {
var cause = throwable
while (cause.cause != null) {
if (cause is GitException) break
val nextCause = cause.cause!!
if (nextCause is RemoteException) break
cause = nextCause
}
return cause
}
}

View file

@ -2,23 +2,27 @@
* 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.app.ProgressDialog
import android.content.Context
import android.app.Activity
import android.content.Intent
import android.os.AsyncTask
import androidx.appcompat.app.AppCompatActivity
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.SshjSessionFactory
import java.io.IOException
import java.lang.ref.WeakReference
import com.zeapo.pwdstore.git.operation.GitOperation
import com.zeapo.pwdstore.utils.Result
import com.zeapo.pwdstore.utils.snackbar
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.GitCommand
import org.eclipse.jgit.api.PullCommand
import org.eclipse.jgit.api.PushCommand
import org.eclipse.jgit.api.RebaseResult
@ -26,95 +30,122 @@ import org.eclipse.jgit.api.StatusCommand
import org.eclipse.jgit.transport.RemoteRefUpdate
import org.eclipse.jgit.transport.SshSessionFactory
class GitAsyncTask(
activity: AppCompatActivity,
class GitCommandExecutor(
private val activity: FragmentActivity,
private val operation: GitOperation,
private val finishWithResultOnEnd: Intent?,
private val silentlyExecute: Boolean = false
) : AsyncTask<GitCommand<*>, Int, GitAsyncTask.Result>() {
private val finishWithResultOnEnd: Intent? = Intent(),
private val finishActivityOnEnd: Boolean = true,
) {
private val activityWeakReference: WeakReference<AppCompatActivity> = WeakReference(activity)
private val activity: AppCompatActivity?
get() = activityWeakReference.get()
private val context: Context = activity.applicationContext
private val dialog = ProgressDialog(activity)
sealed class Result {
object Ok : Result()
data class Err(val err: Exception) : Result()
}
override fun onPreExecute() {
if (silentlyExecute) return
dialog.run {
setMessage(activity!!.resources.getString(R.string.running_dialog_text))
setCancelable(false)
show()
}
}
override fun doInBackground(vararg commands: GitCommand<*>): Result? {
var nbChanges: Int? = null
for (command in commands) {
suspend fun execute() {
operation.setCredentialProvider()
val snackbar = activity.snackbar(
message = activity.resources.getString(R.string.git_operation_running),
length = Snackbar.LENGTH_INDEFINITE,
)
var nbChanges = 0
var operationResult: Result = Result.Ok
for (command in operation.commands) {
try {
when (command) {
is StatusCommand -> {
// in case we have changes, we want to keep track of it
val status = command.call()
val status = withContext(Dispatchers.IO) {
command.call()
}
nbChanges = status.changed.size + status.missing.size
}
is CommitCommand -> {
// the previous status will eventually be used to avoid a commit
if (nbChanges == null || nbChanges > 0) command.call()
withContext(Dispatchers.IO) {
if (nbChanges > 0) command.call()
}
}
is PullCommand -> {
val result = command.call()
val result = withContext(Dispatchers.IO) {
command.call()
}
val rr = result.rebaseResult
if (rr.status === RebaseResult.Status.STOPPED) {
return Result.Err(IOException(context.getString(R.string
.git_pull_fail_error)))
operationResult = Result.Err(PullException(PullException.Reason.REBASE_FAILED))
}
}
is PushCommand -> {
for (result in command.call()) {
val results = withContext(Dispatchers.IO) {
command.call()
}
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 ->
context.getString(R.string.git_push_nff_error)
RemoteRefUpdate.Status.REJECTED_NONFASTFORWARD -> {
PushException(PushException.Reason.NON_FAST_FORWARD)
}
RemoteRefUpdate.Status.REJECTED_NODELETE,
RemoteRefUpdate.Status.REJECTED_REMOTE_CHANGED,
RemoteRefUpdate.Status.NON_EXISTING,
RemoteRefUpdate.Status.NOT_ATTEMPTED
->
(activity!!.getString(R.string.git_push_generic_error) + rru.status.name)
RemoteRefUpdate.Status.NOT_ATTEMPTED,
-> {
PushException(PushException.Reason.GENERIC, rru.status.name)
}
RemoteRefUpdate.Status.REJECTED_OTHER_REASON -> {
if
("non-fast-forward" == rru.message) {
context.getString(R.string.git_push_other_error)
if ("non-fast-forward" == rru.message) {
PushException(PushException.Reason.REMOTE_REJECTED)
} else {
(context.getString(R.string.git_push_generic_error)
+ rru.message)
PushException(PushException.Reason.GENERIC, rru.message)
}
}
else -> null
}
if (error != null)
Result.Err(IOException(error))
if (error != null) {
operationResult = Result.Err(error)
}
}
}
}
else -> {
command.call()
withContext(Dispatchers.IO) {
command.call()
}
}
}
} catch (e: Exception) {
return Result.Err(e)
operationResult = Result.Err(e)
}
}
return Result.Ok
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()
(SshSessionFactory.getInstance() as? SshjSessionFactory)?.clearCredentials()
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 {
@ -130,47 +161,4 @@ class GitAsyncTask(
}
return rootCause
}
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
}
override fun onPostExecute(maybeResult: Result?) {
if (!silentlyExecute) dialog.dismiss()
when (val result = maybeResult ?: Result.Err(IOException("Unexpected error"))) {
is Result.Err -> {
if (isExplicitlyUserInitiatedError(result.err)) {
// Currently, this is only executed when the user cancels a password prompt
// during authentication.
if (finishWithResultOnEnd != null) {
activity?.setResult(AppCompatActivity.RESULT_CANCELED)
activity?.finish()
}
} else {
e(result.err)
operation.onError(rootCauseException(result.err))
if (finishWithResultOnEnd != null) {
activity?.setResult(AppCompatActivity.RESULT_CANCELED)
}
}
}
is Result.Ok -> {
operation.onSuccess()
if (finishWithResultOnEnd != null) {
activity?.setResult(AppCompatActivity.RESULT_OK, finishWithResultOnEnd)
activity?.finish()
}
}
}
(SshSessionFactory.getInstance() as? SshjSessionFactory)?.clearCredentials()
SshSessionFactory.setInstance(null)
}
}

View file

@ -9,6 +9,7 @@ import android.os.Handler
import android.util.Patterns
import androidx.core.content.edit
import androidx.core.os.postDelayed
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.zeapo.pwdstore.R
@ -16,6 +17,7 @@ import com.zeapo.pwdstore.databinding.ActivityGitConfigBinding
import com.zeapo.pwdstore.utils.PasswordRepository
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.viewBinding
import kotlinx.coroutines.launch
import org.eclipse.jgit.lib.Constants
class GitConfigActivity : BaseGitActivity() {
@ -47,8 +49,8 @@ class GitConfigActivity : BaseGitActivity() {
} catch (ignored: Exception) {
}
}
binding.gitAbortRebase.setOnClickListener { launchGitOperation(BREAK_OUT_OF_DETACHED) }
binding.gitResetToRemote.setOnClickListener { launchGitOperation(REQUEST_RESET) }
binding.gitAbortRebase.setOnClickListener { lifecycleScope.launch { launchGitOperation(BREAK_OUT_OF_DETACHED) } }
binding.gitResetToRemote.setOnClickListener { lifecycleScope.launch { launchGitOperation(REQUEST_RESET) } }
binding.saveButton.setOnClickListener {
val email = binding.gitUserEmail.text.toString().trim()
val name = binding.gitUserName.text.toString().trim()

View file

@ -8,19 +8,21 @@ 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.utils.PasswordRepository
import kotlinx.coroutines.launch
open class GitOperationActivity : BaseGitActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
when (intent.extras?.getInt(REQUEST_ARG_OP)) {
REQUEST_PULL -> syncRepository(REQUEST_PULL)
REQUEST_PUSH -> syncRepository(REQUEST_PUSH)
REQUEST_SYNC -> syncRepository(REQUEST_SYNC)
REQUEST_PULL -> lifecycleScope.launch { syncRepository(REQUEST_PULL) }
REQUEST_PUSH -> lifecycleScope.launch { syncRepository(REQUEST_PUSH) }
REQUEST_SYNC -> lifecycleScope.launch { syncRepository(REQUEST_SYNC) }
else -> {
setResult(RESULT_CANCELED)
finish()
@ -54,7 +56,7 @@ open class GitOperationActivity : BaseGitActivity() {
*
* @param operation the operation to execute can be REQUEST_PULL or REQUEST_PUSH
*/
private fun syncRepository(operation: Int) {
private suspend fun syncRepository(operation: Int) {
if (serverUser.isEmpty() || serverHostname.isEmpty() || url.isNullOrEmpty())
MaterialAlertDialogBuilder(this)
.setMessage(getString(R.string.set_information_dialog_text))

View file

@ -10,6 +10,7 @@ import android.view.View
import androidx.core.content.edit
import androidx.core.os.postDelayed
import androidx.core.widget.doOnTextChanged
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.zeapo.pwdstore.R
@ -20,6 +21,7 @@ import com.zeapo.pwdstore.utils.PasswordRepository
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.viewBinding
import java.io.IOException
import kotlinx.coroutines.launch
/**
* Activity that encompasses both the initial clone as well as editing the server config for future
@ -171,7 +173,7 @@ class GitServerConfigActivity : BaseGitActivity() {
.setPositiveButton(R.string.dialog_delete) { dialog, _ ->
try {
localDir.deleteRecursively()
launchGitOperation(REQUEST_CLONE)
lifecycleScope.launch { launchGitOperation(REQUEST_CLONE) }
} catch (e: IOException) {
// TODO Handle the exception correctly if we are unable to delete the directory...
e.printStackTrace()
@ -201,7 +203,7 @@ class GitServerConfigActivity : BaseGitActivity() {
e.printStackTrace()
MaterialAlertDialogBuilder(this).setMessage(e.message).show()
}
launchGitOperation(REQUEST_CLONE)
lifecycleScope.launch { launchGitOperation(REQUEST_CLONE) }
}
}
}

View file

@ -1,52 +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 androidx.appcompat.app.AppCompatActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.R
import java.io.File
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.PullCommand
/**
* 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) {
/**
* Sets the command
*
* @return the current object
*/
fun setCommand(): PullOperation {
this.command = Git(repository)
.pull()
.setRebase(true)
.setRemote("origin")
return this
}
override fun execute() {
(this.command as? PullCommand)?.setCredentialsProvider(this.provider)
GitAsyncTask(callingActivity, this, Intent()).execute(this.command)
}
override fun onError(err: Exception) {
super.onError(err)
MaterialAlertDialogBuilder(callingActivity)
.setTitle(callingActivity.resources.getString(R.string.jgit_error_dialog_title))
.setMessage("Error occurred during the pull operation, " +
callingActivity.resources.getString(R.string.jgit_error_dialog_text) +
err.message +
"\nPlease check the FAQ for possible reasons why this error might occur.")
.setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> callingActivity.finish() }
.show()
}
}

View file

@ -1,50 +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 androidx.appcompat.app.AppCompatActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.R
import java.io.File
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.PushCommand
/**
* 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) {
/**
* Sets the command
*
* @return the current object
*/
fun setCommand(): PushOperation {
this.command = Git(repository)
.push()
.setPushAll()
.setRemote("origin")
return this
}
override fun execute() {
(this.command as? PushCommand)?.setCredentialsProvider(this.provider)
GitAsyncTask(callingActivity, this, Intent()).execute(this.command)
}
override fun onError(err: Exception) {
// TODO handle the "Nothing to push" case
super.onError(err)
MaterialAlertDialogBuilder(callingActivity)
.setTitle(callingActivity.resources.getString(R.string.jgit_error_dialog_title))
.setMessage(callingActivity.getString(R.string.jgit_error_push_dialog_text) + err.message)
.setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> callingActivity.finish() }
.show()
}
}

View file

@ -1,69 +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 androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.utils.PreferenceKeys
import java.io.File
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.GitCommand
import org.eclipse.jgit.api.ResetCommand
import org.eclipse.jgit.api.TransportCommand
/**
* 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) {
private lateinit var commands: List<GitCommand<out Any>>
/**
* Sets the command
*
* @return the current object
*/
fun setCommands(): ResetToRemoteOperation {
val remoteBranch = PreferenceManager
.getDefaultSharedPreferences(callingActivity.applicationContext)
.getString(PreferenceKeys.GIT_BRANCH_NAME, "master")
val git = Git(repository)
val cmds = arrayListOf(
git.add().addFilepattern("."),
git.fetch().setRemote("origin"),
git.reset().setRef("origin/$remoteBranch").setMode(ResetCommand.ResetType.HARD)
)
if (git.branchList().call().none { it.name == remoteBranch }) {
cmds.add(
git.branchCreate().setName(remoteBranch).setForce(true)
)
}
commands = cmds
return this
}
override fun execute() {
commands.filterIsInstance<TransportCommand<*, *>>().map { it.setCredentialsProvider(provider) }
GitAsyncTask(callingActivity, this, Intent()).execute(*commands.toTypedArray())
}
override fun onError(err: Exception) {
super.onError(err)
MaterialAlertDialogBuilder(callingActivity)
.setTitle(callingActivity.resources.getString(R.string.jgit_error_dialog_title))
.setMessage("Error occurred during the sync operation, " +
"\nPlease check the FAQ for possible reasons why this error might occur." +
callingActivity.resources.getString(R.string.jgit_error_dialog_text) +
err)
.setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> }
.show()
}
}

View file

@ -1,67 +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 androidx.appcompat.app.AppCompatActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.R
import java.io.File
import org.eclipse.jgit.api.AddCommand
import org.eclipse.jgit.api.CommitCommand
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.PullCommand
import org.eclipse.jgit.api.PushCommand
import org.eclipse.jgit.api.StatusCommand
/**
* 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) {
private var addCommand: AddCommand? = null
private var statusCommand: StatusCommand? = null
private var commitCommand: CommitCommand? = null
private var pullCommand: PullCommand? = null
private var pushCommand: PushCommand? = null
/**
* Sets the command
*
* @return the current object
*/
fun setCommands(): SyncOperation {
val git = Git(repository)
this.addCommand = git.add().addFilepattern(".")
this.statusCommand = git.status()
this.commitCommand = git.commit().setAll(true).setMessage("[Android Password Store] Sync")
this.pullCommand = git.pull().setRebase(true).setRemote("origin")
this.pushCommand = git.push().setPushAll().setRemote("origin")
return this
}
override fun execute() {
if (this.provider != null) {
this.pullCommand?.setCredentialsProvider(this.provider)
this.pushCommand?.setCredentialsProvider(this.provider)
}
GitAsyncTask(callingActivity, this, Intent()).execute(this.addCommand, this.statusCommand, this.commitCommand, this.pullCommand, this.pushCommand)
}
override fun onError(err: Exception) {
super.onError(err)
MaterialAlertDialogBuilder(callingActivity)
.setTitle(callingActivity.resources.getString(R.string.jgit_error_dialog_title))
.setMessage("Error occurred during the sync operation, " +
"\nPlease check the FAQ for possible reasons why this error might occur." +
callingActivity.resources.getString(R.string.jgit_error_dialog_text) +
err)
.setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> callingActivity.finish() }
.show()
}
}

View file

@ -0,0 +1,52 @@
/*
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
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()}"
override val commands = arrayOf(
// abort the rebase
git.rebase().setOperation(RebaseCommand.Operation.ABORT),
// git checkout -b conflict-branch
git.checkout().setCreateBranch(true).setName(branchName),
// 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() {
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")
.setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ ->
callingActivity.finish()
}.show()
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
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) {
override val commands: Array<GitCommand<out Any>> = arrayOf(
Git.cloneRepository().setBranch(remoteBranch).setDirectory(repository?.workTree).setURI(uri),
)
override suspend fun execute() {
GitCommandExecutor(callingActivity, this).execute()
}
}

View file

@ -0,0 +1,94 @@
package com.zeapo.pwdstore.git.operation
import android.annotation.SuppressLint
import android.view.LayoutInflater
import androidx.annotation.StringRes
import androidx.core.content.edit
import androidx.fragment.app.FragmentActivity
import com.google.android.material.checkbox.MaterialCheckBox
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.git.config.ConnectionMode
import com.zeapo.pwdstore.git.config.InteractivePasswordFinder
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.getEncryptedPrefs
import com.zeapo.pwdstore.utils.requestInputFocusOnView
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
class CredentialFinder(
val callingActivity: FragmentActivity,
val connectionMode: ConnectionMode
) : InteractivePasswordFinder() {
override fun askForPassword(cont: Continuation<String?>, isRetry: Boolean) {
val gitOperationPrefs = callingActivity.getEncryptedPrefs("git_operation")
val credentialPref: String
@StringRes val messageRes: Int
@StringRes val hintRes: Int
@StringRes val rememberRes: Int
@StringRes val errorRes: Int
when (connectionMode) {
ConnectionMode.SshKey -> {
credentialPref = PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE
messageRes = R.string.passphrase_dialog_text
hintRes = R.string.ssh_keygen_passphrase
rememberRes = R.string.git_operation_remember_passphrase
errorRes = R.string.git_operation_wrong_passphrase
}
ConnectionMode.Password -> {
// Could be either an SSH or an HTTPS password
credentialPref = PreferenceKeys.HTTPS_PASSWORD
messageRes = R.string.password_dialog_text
hintRes = R.string.git_operation_hint_password
rememberRes = R.string.git_operation_remember_password
errorRes = R.string.git_operation_wrong_password
}
else -> throw IllegalStateException("Only SshKey and Password connection mode ask for passwords")
}
val storedCredential = gitOperationPrefs.getString(credentialPref, null)
if (isRetry)
gitOperationPrefs.edit { remove(credentialPref) }
if (storedCredential == null) {
val layoutInflater = LayoutInflater.from(callingActivity)
@SuppressLint("InflateParams")
val dialogView = layoutInflater.inflate(R.layout.git_credential_layout, null)
val credentialLayout = dialogView.findViewById<TextInputLayout>(R.id.git_auth_passphrase_layout)
val editCredential = dialogView.findViewById<TextInputEditText>(R.id.git_auth_credential)
editCredential.setHint(hintRes)
val rememberCredential = dialogView.findViewById<MaterialCheckBox>(R.id.git_auth_remember_credential)
rememberCredential.setText(rememberRes)
if (isRetry)
credentialLayout.error = callingActivity.resources.getString(errorRes)
MaterialAlertDialogBuilder(callingActivity).run {
setTitle(R.string.passphrase_dialog_title)
setMessage(messageRes)
setView(dialogView)
setPositiveButton(R.string.dialog_ok) { _, _ ->
val credential = editCredential.text.toString()
if (rememberCredential.isChecked) {
gitOperationPrefs.edit {
putString(credentialPref, credential)
}
}
cont.resume(credential)
}
setNegativeButton(R.string.dialog_cancel) { _, _ ->
cont.resume(null)
}
setOnCancelListener {
cont.resume(null)
}
create()
}.run {
requestInputFocusOnView<TextInputEditText>(R.id.git_auth_credential)
show()
}
} else {
cont.resume(storedCredential)
}
}
}

View file

@ -2,21 +2,18 @@
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
package com.zeapo.pwdstore.git
package com.zeapo.pwdstore.git.operation
import android.annotation.SuppressLint
import android.content.Intent
import android.view.LayoutInflater
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.annotation.CallSuper
import androidx.core.content.edit
import androidx.fragment.app.FragmentActivity
import androidx.preference.PreferenceManager
import com.google.android.material.checkbox.MaterialCheckBox
import com.github.ajalt.timberkt.Timber.d
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.UserPreference
import com.zeapo.pwdstore.git.ErrorMessages
import com.zeapo.pwdstore.git.config.ConnectionMode
import com.zeapo.pwdstore.git.config.InteractivePasswordFinder
import com.zeapo.pwdstore.git.config.SshApiSessionFactory
@ -25,108 +22,34 @@ import com.zeapo.pwdstore.git.config.SshjSessionFactory
import com.zeapo.pwdstore.utils.PasswordRepository
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.getEncryptedPrefs
import com.zeapo.pwdstore.utils.requestInputFocusOnView
import java.io.File
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import net.schmizz.sshj.userauth.password.PasswordFinder
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.GitCommand
import org.eclipse.jgit.api.TransportCommand
import org.eclipse.jgit.errors.UnsupportedCredentialItem
import org.eclipse.jgit.lib.Repository
import org.eclipse.jgit.transport.CredentialItem
import org.eclipse.jgit.transport.CredentialsProvider
import org.eclipse.jgit.transport.SshSessionFactory
import org.eclipse.jgit.transport.URIish
private class GitOperationCredentialFinder(
val callingActivity: AppCompatActivity,
val connectionMode: ConnectionMode
) : InteractivePasswordFinder() {
override fun askForPassword(cont: Continuation<String?>, isRetry: Boolean) {
val gitOperationPrefs = callingActivity.getEncryptedPrefs("git_operation")
val credentialPref: String
@StringRes val messageRes: Int
@StringRes val hintRes: Int
@StringRes val rememberRes: Int
@StringRes val errorRes: Int
when (connectionMode) {
ConnectionMode.SshKey -> {
credentialPref = PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE
messageRes = R.string.passphrase_dialog_text
hintRes = R.string.ssh_keygen_passphrase
rememberRes = R.string.git_operation_remember_passphrase
errorRes = R.string.git_operation_wrong_passphrase
}
ConnectionMode.Password -> {
// Could be either an SSH or an HTTPS password
credentialPref = PreferenceKeys.HTTPS_PASSWORD
messageRes = R.string.password_dialog_text
hintRes = R.string.git_operation_hint_password
rememberRes = R.string.git_operation_remember_password
errorRes = R.string.git_operation_wrong_password
}
else -> throw IllegalStateException("Only SshKey and Password connection mode ask for passwords")
}
val storedCredential = gitOperationPrefs.getString(credentialPref, null)
if (isRetry)
gitOperationPrefs.edit { remove(credentialPref) }
if (storedCredential == null) {
val layoutInflater = LayoutInflater.from(callingActivity)
@SuppressLint("InflateParams")
val dialogView = layoutInflater.inflate(R.layout.git_credential_layout, null)
val credentialLayout = dialogView.findViewById<TextInputLayout>(R.id.git_auth_passphrase_layout)
val editCredential = dialogView.findViewById<TextInputEditText>(R.id.git_auth_credential)
editCredential.setHint(hintRes)
val rememberCredential = dialogView.findViewById<MaterialCheckBox>(R.id.git_auth_remember_credential)
rememberCredential.setText(rememberRes)
if (isRetry)
credentialLayout.error = callingActivity.resources.getString(errorRes)
MaterialAlertDialogBuilder(callingActivity).run {
setTitle(R.string.passphrase_dialog_title)
setMessage(messageRes)
setView(dialogView)
setPositiveButton(R.string.dialog_ok) { _, _ ->
val credential = editCredential.text.toString()
if (rememberCredential.isChecked) {
gitOperationPrefs.edit {
putString(credentialPref, credential)
}
}
cont.resume(credential)
}
setNegativeButton(R.string.dialog_cancel) { _, _ ->
cont.resume(null)
}
setOnCancelListener {
cont.resume(null)
}
create()
}.run {
requestInputFocusOnView<TextInputEditText>(R.id.git_auth_credential)
show()
}
} else {
cont.resume(storedCredential)
}
}
}
/**
* 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: AppCompatActivity) {
abstract class GitOperation(gitDir: File, internal val callingActivity: FragmentActivity) {
protected val repository: Repository? = PasswordRepository.getRepository(gitDir)
internal var provider: CredentialsProvider? = null
internal var command: GitCommand<*>? = null
abstract val commands: Array<GitCommand<out Any>>
private var provider: CredentialsProvider? = null
private val sshKeyFile = callingActivity.filesDir.resolve(".ssh_key")
private val hostKeyFile = callingActivity.filesDir.resolve(".host_key")
protected val repository = PasswordRepository.getRepository(gitDir)
protected val git = Git(repository)
protected val remoteBranch = PreferenceManager
.getDefaultSharedPreferences(callingActivity.applicationContext)
.getString(PreferenceKeys.GIT_BRANCH_NAME, "master")
private class PasswordFinderCredentialsProvider(private val username: String, private val passwordFinder: PasswordFinder) : CredentialsProvider() {
@ -181,12 +104,18 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: AppCompa
}
}
fun setCredentialProvider() {
provider?.let { credentialsProvider ->
commands.filterIsInstance<TransportCommand<*, *>>().forEach { it.setCredentialsProvider(credentialsProvider) }
}
}
/**
* Executes the GitCommand in an async task
*/
abstract fun execute()
abstract suspend fun execute()
fun executeAfterAuthentication(
suspend fun executeAfterAuthentication(
connectionMode: ConnectionMode,
username: String,
identity: SshApiSessionFactory.ApiIdentity?
@ -207,12 +136,12 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: AppCompa
callingActivity.finish()
}.show()
} else {
withPublicKeyAuthentication(username, GitOperationCredentialFinder(callingActivity,
withPublicKeyAuthentication(username, CredentialFinder(callingActivity,
connectionMode)).execute()
}
ConnectionMode.OpenKeychain -> withOpenKeychainAuthentication(username, identity).execute()
ConnectionMode.Password -> withPasswordAuthentication(
username, GitOperationCredentialFinder(callingActivity, connectionMode)).execute()
username, CredentialFinder(callingActivity, connectionMode)).execute()
ConnectionMode.None -> execute()
}
}
@ -220,6 +149,7 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: AppCompa
/**
* Action to execute on error
*/
@CallSuper
open fun onError(err: Exception) {
// Clear various auth related fields on failure
when (SshSessionFactory.getInstance()) {
@ -236,6 +166,13 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: AppCompa
}
}
}
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)) { _, _ ->
callingActivity.finish()
}.show()
}
/**

View file

@ -0,0 +1,28 @@
/*
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
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) {
override val commands: Array<GitCommand<out Any>> = arrayOf(
Git(repository).pull().setRebase(true).setRemote("origin"),
)
override suspend fun execute() {
GitCommandExecutor(callingActivity, this).execute()
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
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) {
override val commands: Array<GitCommand<out Any>> = arrayOf(
Git(repository).push().setPushAll().setRemote("origin"),
)
override suspend fun execute() {
setCredentialProvider()
GitCommandExecutor(callingActivity, this).execute()
}
}

View file

@ -0,0 +1,30 @@
/*
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
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) {
override val commands = arrayOf(
git.add().addFilepattern("."),
git.fetch().setRemote("origin"),
git.reset().setRef("origin/$remoteBranch").setMode(ResetCommand.ResetType.HARD),
git.branchCreate().setName(remoteBranch).setForce(true),
)
override suspend fun execute() {
GitCommandExecutor(callingActivity, this).execute()
}
}

View file

@ -0,0 +1,30 @@
/*
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
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) {
override val commands = arrayOf(
git.add().addFilepattern("."),
git.status(),
git.commit().setAll(true).setMessage("[Android Password Store] Sync"),
git.pull().setRebase(true).setRemote("origin"),
git.push().setPushAll().setRemote("origin"),
)
override suspend fun execute() {
GitCommandExecutor(callingActivity, this).execute()
}
}

View file

@ -14,20 +14,18 @@ import android.view.View
import android.view.autofill.AutofillManager
import android.view.inputmethod.InputMethodManager
import androidx.annotation.IdRes
import androidx.annotation.MainThread
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.getSystemService
import androidx.fragment.app.FragmentActivity
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import com.github.ajalt.timberkt.d
import com.google.android.material.snackbar.Snackbar
import com.zeapo.pwdstore.git.GitAsyncTask
import com.zeapo.pwdstore.git.GitOperation
import com.zeapo.pwdstore.git.GitCommandExecutor
import com.zeapo.pwdstore.git.operation.GitOperation
import com.zeapo.pwdstore.utils.PasswordRepository.Companion.getRepositoryDirectory
import java.io.File
import org.eclipse.jgit.api.Git
const val OPENPGP_PROVIDER = "org.sufficientlysecure.keychain"
@ -51,12 +49,14 @@ fun CharArray.clear() {
val Context.clipboard get() = getSystemService<ClipboardManager>()
fun AppCompatActivity.snackbar(
fun FragmentActivity.snackbar(
view: View = findViewById(android.R.id.content),
message: String,
length: Int = Snackbar.LENGTH_SHORT
) {
Snackbar.make(view, message, length).show()
length: Int = Snackbar.LENGTH_SHORT,
): Snackbar {
val snackbar = Snackbar.make(view, message, length)
snackbar.show()
return snackbar
}
fun File.listFilesRecursively() = walkTopDown().filter { !it.isDirectory }.toList()
@ -97,24 +97,33 @@ fun Context.getEncryptedPrefs(fileName: String): SharedPreferences {
)
}
@MainThread
fun AppCompatActivity.commitChange(message: String, finishWithResultOnEnd: Intent? = null) {
suspend fun FragmentActivity.commitChange(
message: String,
finishWithResultOnEnd: Intent? = null,
finishActivityOnEnd: Boolean = true,
) {
if (!PasswordRepository.isGitRepo()) {
if (finishWithResultOnEnd != null) {
setResult(AppCompatActivity.RESULT_OK, finishWithResultOnEnd)
setResult(FragmentActivity.RESULT_OK, finishWithResultOnEnd)
finish()
}
return
}
object : GitOperation(getRepositoryDirectory(this@commitChange), this@commitChange) {
override fun execute() {
override val commands = arrayOf(
git.add().addFilepattern("."),
git.status(),
git.commit().setAll(true).setMessage(message),
)
override suspend fun execute() {
d { "Comitting with message: '$message'" }
val git = Git(repository)
val task = GitAsyncTask(this@commitChange, this, finishWithResultOnEnd, silentlyExecute = true)
task.execute(
git.add().addFilepattern("."),
git.commit().setAll(true).setMessage(message)
)
GitCommandExecutor(
this@commitChange,
this,
finishWithResultOnEnd,
finishActivityOnEnd,
).execute()
}
}.execute()
}
@ -124,7 +133,6 @@ fun AppCompatActivity.commitChange(message: String, finishWithResultOnEnd: Inten
* view whose id is [id]. Solution based on a StackOverflow
* answer: https://stackoverflow.com/a/13056259/297261
*/
@MainThread
fun <T : View> AlertDialog.requestInputFocusOnView(@IdRes id: Int) {
setOnShowListener {
findViewById<T>(id)?.apply {
@ -143,6 +151,6 @@ val Context.autofillManager: AutofillManager?
@RequiresApi(Build.VERSION_CODES.O)
get() = getSystemService()
fun AppCompatActivity.isInsideRepository(file: File): Boolean {
fun FragmentActivity.isInsideRepository(file: File): Boolean {
return file.canonicalPath.contains(getRepositoryDirectory(this).canonicalPath)
}

View file

@ -13,7 +13,6 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.observe
import androidx.viewbinding.ViewBinding
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty

View file

@ -0,0 +1,16 @@
/*
* 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()
}

View file

@ -57,6 +57,7 @@ class UriTotpFinder : TotpFinder {
}
companion object {
val TOTP_FIELDS = arrayOf(
"otpauth://totp",
"totp:"

View file

@ -276,10 +276,6 @@
<string name="autofill_ins_1_hint">Screenshot of accessibility services</string>
<string name="autofill_ins_2_hint">Screenshot of toggle in accessibility services</string>
<string name="autofill_ins_3_hint">Screenshot of autofill service in action</string>
<string name="git_pull_fail_error">Pull has failed, you\'re in a detached head. Using "settings > git utils", save your changes to the remote in a new branch and resolve the conflict on your computer.</string>
<string name="git_push_nff_error">Push was rejected by remote, run pull before pushing again. You can use Synchronize rather than pull/push as it implements both</string>
<string name="git_push_generic_error">Push was rejected by remote, reason:</string>
<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="jgit_error_push_dialog_text">Error occurred during the push operation:</string>
<string name="clear_saved_passphrase_ssh">Clear saved passphrase for local SSH key</string>
<string name="clear_saved_passphrase_https">Clear saved HTTPS password</string>
@ -371,4 +367,12 @@
<string name="short_key_ids_unsupported">A key ID in .gpg-id is too short, please use either long key IDs (16 characters) or fingerprints (40 characters)</string>
<string name="invalid_filename_text">File name must not contain \'/\', set directory above</string>
<string name="directory_hint">Directory</string>
<!-- GitException messages -->
<string name="git_unknown_error">Unknown error</string>
<string name="git_pull_fail_error">Pull has failed, you\'re in a detached head. Using "settings > git utils", save your changes to the remote in a new branch and resolve the conflict on your computer.</string>
<string name="git_push_nff_error">Push was rejected by remote, run pull before pushing again. You can use Synchronize rather than pull/push as it implements both</string>
<string name="git_push_generic_error">Push was rejected by remote, reason: %1$s</string>
<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_operation_running">Running git operation…</string>
</resources>