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
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() { override fun onCreate() {
super.onCreate() super.onCreate()
instance = this
prefs = PreferenceManager.getDefaultSharedPreferences(this) prefs = PreferenceManager.getDefaultSharedPreferences(this)
if (BuildConfig.ENABLE_DEBUG_FEATURES || prefs?.getBoolean(PreferenceKeys.ENABLE_DEBUG_LOGGING, false) == if (BuildConfig.ENABLE_DEBUG_FEATURES || prefs?.getBoolean(PreferenceKeys.ENABLE_DEBUG_LOGGING, false) ==
true) { true) {
@@ -52,4 +53,9 @@ class Application : android.app.Application(), SharedPreferences.OnSharedPrefere
else -> MODE_NIGHT_AUTO_BATTERY 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.appcompat.view.ActionMode
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.fragment.app.setFragmentResultListener
import androidx.lifecycle.observe
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager

View File

@@ -34,7 +34,6 @@ import androidx.fragment.app.FragmentManager
import androidx.fragment.app.commit import androidx.fragment.app.commit
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.observe
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.github.ajalt.timberkt.d import com.github.ajalt.timberkt.d
import com.github.ajalt.timberkt.e import com.github.ajalt.timberkt.e
@@ -581,7 +580,12 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
intent.putExtra("REPO_PATH", getRepositoryDirectory(applicationContext).absolutePath) intent.putExtra("REPO_PATH", getRepositoryDirectory(applicationContext).absolutePath)
registerForActivityResult(StartActivityForResult()) { result -> registerForActivityResult(StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) { 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() refreshPasswordList()
} }
}.launch(intent) }.launch(intent)
@@ -618,11 +622,15 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
selectedItems.map { item -> item.file.deleteRecursively() } selectedItems.map { item -> item.file.deleteRecursively() }
refreshPasswordList() refreshPasswordList()
AutofillMatcher.updateMatches(applicationContext, delete = filesToDelete) AutofillMatcher.updateMatches(applicationContext, delete = filesToDelete)
commitChange(resources.getString(R.string.git_commit_remove_text, val fmt = selectedItems.joinToString(separator = ", ") { item ->
selectedItems.joinToString(separator = ", ") { item -> item.file.toRelativeString(getRepositoryDirectory(this@PasswordStore))
item.file.toRelativeString(getRepositoryDirectory(this)) }
} lifecycleScope.launch {
)) commitChange(
resources.getString(R.string.git_commit_remove_text, fmt),
finishActivityOnEnd = false,
)
}
} }
.setNegativeButton(resources.getString(R.string.dialog_no), null) .setNegativeButton(resources.getString(R.string.dialog_no), null)
.show() .show()
@@ -688,14 +696,20 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
val sourceLongName = getLongName(requireNotNull(source.parent), repositoryPath, basename) val sourceLongName = getLongName(requireNotNull(source.parent), repositoryPath, basename)
val destinationLongName = getLongName(target.absolutePath, repositoryPath, basename) val destinationLongName = getLongName(target.absolutePath, repositoryPath, basename)
withContext(Dispatchers.Main) { 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 -> { else -> {
val repoDir = getRepositoryDirectory(applicationContext).absolutePath
val relativePath = getRelativePath("${target.absolutePath}/", repoDir)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
commitChange(resources.getString(R.string.git_commit_move_multiple_text, commitChange(
getRelativePath("${target.absolutePath}/", getRepositoryDirectory(applicationContext).absolutePath) 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) { else -> lifecycleScope.launch(Dispatchers.IO) {
moveFile(oldCategory.file, newCategory) moveFile(oldCategory.file, newCategory)
withContext(Dispatchers.Main) { 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.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.observe
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.zeapo.pwdstore.databinding.PasswordRecyclerViewBinding import com.zeapo.pwdstore.databinding.PasswordRecyclerViewBinding
import com.zeapo.pwdstore.ui.adapters.PasswordItemRecyclerAdapter 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.text.underline
import androidx.core.widget.addTextChangedListener import androidx.core.widget.addTextChangedListener
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.observe
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.github.ajalt.timberkt.e import com.github.ajalt.timberkt.e
import com.zeapo.pwdstore.FilterMode import com.zeapo.pwdstore.FilterMode

View File

@@ -15,6 +15,7 @@ import androidx.activity.result.contract.ActivityResultContracts.StartActivityFo
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.lifecycle.lifecycleScope
import com.github.ajalt.timberkt.e import com.github.ajalt.timberkt.e
import com.zeapo.pwdstore.R import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.autofill.oreo.AutofillAction 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.PasswordRepository
import com.zeapo.pwdstore.utils.commitChange import com.zeapo.pwdstore.utils.commitChange
import java.io.File import java.io.File
import kotlinx.coroutines.launch
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
class AutofillSaveActivity : AppCompatActivity() { class AutofillSaveActivity : AppCompatActivity() {
@@ -144,10 +146,12 @@ class AutofillSaveActivity : AppCompatActivity() {
} }
// PasswordCreationActivity delegates committing the added file to PasswordStore. Since // PasswordCreationActivity delegates committing the added file to PasswordStore. Since
// PasswordStore is not involved in an AutofillScenario, we have to commit the file ourselves. // PasswordStore is not involved in an AutofillScenario, we have to commit the file ourselves.
commitChange( lifecycleScope.launch {
getString(R.string.git_commit_add_text, longName), commitChange(
finishWithResultOnEnd = resultIntent getString(R.string.git_commit_add_text, longName),
) finishWithResultOnEnd = resultIntent
)
}
// GitAsyncTask will finish the activity for us. // GitAsyncTask will finish the activity for us.
} }
}.launch(saveIntent) }.launch(saveIntent)

View File

@@ -329,12 +329,14 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
gpgIdentifierFile.writeText(keyIds.joinToString("\n")) gpgIdentifierFile.writeText(keyIds.joinToString("\n"))
val repo = PasswordRepository.getRepository(null) val repo = PasswordRepository.getRepository(null)
if (repo != null) { if (repo != null) {
commitChange( lifecycleScope.launch {
getString( commitChange(
R.string.git_commit_gpg_id, getString(
getLongName(gpgIdentifierFile.parentFile!!.absolutePath, repoPath, gpgIdentifierFile.name) R.string.git_commit_gpg_id,
getLongName(gpgIdentifierFile.parentFile!!.absolutePath, repoPath, gpgIdentifierFile.name)
)
) )
) }
} }
encrypt(data) encrypt(data)
} }
@@ -422,7 +424,8 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
AutofillPreferences.directoryStructure(applicationContext) AutofillPreferences.directoryStructure(applicationContext)
val entry = PasswordEntry(content) val entry = PasswordEntry(content)
returnIntent.putExtra(RETURN_EXTRA_PASSWORD, entry.password) 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) returnIntent.putExtra(RETURN_EXTRA_USERNAME, username)
} }
@@ -430,12 +433,14 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
if (repo != null) { if (repo != null) {
val status = Git(repo).status().call() val status = Git(repo).status().call()
if (status.modified.isNotEmpty()) { if (status.modified.isNotEmpty()) {
commitChange( lifecycleScope.launch {
getString( commitChange(
R.string.git_commit_edit_text, getString(
getLongName(fullPath, repoPath, editName) 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.appcompat.app.AppCompatActivity
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.text.isDigitsOnly import androidx.core.text.isDigitsOnly
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.github.ajalt.timberkt.Timber.tag import com.github.ajalt.timberkt.Timber.tag
import com.github.ajalt.timberkt.e 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.ConnectionMode
import com.zeapo.pwdstore.git.config.Protocol import com.zeapo.pwdstore.git.config.Protocol
import com.zeapo.pwdstore.git.config.SshApiSessionFactory 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.PasswordRepository
import com.zeapo.pwdstore.utils.PreferenceKeys import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.getEncryptedPrefs import com.zeapo.pwdstore.utils.getEncryptedPrefs
import java.io.File import java.io.File
import java.net.URI import java.net.URI
import kotlinx.coroutines.launch
/** /**
* 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
@@ -166,7 +175,7 @@ abstract class BaseGitActivity : AppCompatActivity() {
* *
* @param operation The type of git operation to launch * @param operation The type of git operation to launch
*/ */
fun launchGitOperation(operation: Int) { suspend fun launchGitOperation(operation: Int) {
if (url == null) { if (url == null) {
setResult(RESULT_CANCELED) setResult(RESULT_CANCELED)
finish() finish()
@@ -190,12 +199,12 @@ abstract class BaseGitActivity : AppCompatActivity() {
val localDir = requireNotNull(PasswordRepository.getRepositoryDirectory(this)) val localDir = requireNotNull(PasswordRepository.getRepositoryDirectory(this))
val op = when (operation) { val op = when (operation) {
REQUEST_CLONE, GitOperation.GET_SSH_KEY_FROM_CLONE -> CloneOperation(localDir, this).setCommand(url!!) REQUEST_CLONE, GitOperation.GET_SSH_KEY_FROM_CLONE -> CloneOperation(localDir, url!!, this)
REQUEST_PULL -> PullOperation(localDir, this).setCommand() REQUEST_PULL -> PullOperation(localDir, this)
REQUEST_PUSH -> PushOperation(localDir, this).setCommand() REQUEST_PUSH -> PushOperation(localDir, this)
REQUEST_SYNC -> SyncOperation(localDir, this).setCommands() REQUEST_SYNC -> SyncOperation(localDir, this)
BREAK_OUT_OF_DETACHED -> BreakOutOfDetached(localDir, this).setCommands() BREAK_OUT_OF_DETACHED -> BreakOutOfDetached(localDir, this)
REQUEST_RESET -> ResetToRemoteOperation(localDir, this).setCommands() REQUEST_RESET -> ResetToRemoteOperation(localDir, this)
SshApiSessionFactory.POST_SIGNATURE -> return SshApiSessionFactory.POST_SIGNATURE -> return
else -> { else -> {
tag(TAG).e { "Operation not recognized : $operation" } tag(TAG).e { "Operation not recognized : $operation" }
@@ -239,7 +248,7 @@ abstract class BaseGitActivity : AppCompatActivity() {
if (identityBuilder != null) { if (identityBuilder != null) {
identityBuilder!!.consume(data) identityBuilder!!.consume(data)
} }
launchGitOperation(requestCode) lifecycleScope.launch { launchGitOperation(requestCode) }
} }
super.onActivityResult(requestCode, resultCode, data) 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. * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
package com.zeapo.pwdstore.git package com.zeapo.pwdstore.git
import android.app.ProgressDialog import android.app.Activity
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.AsyncTask import androidx.fragment.app.FragmentActivity
import androidx.appcompat.app.AppCompatActivity
import com.github.ajalt.timberkt.e import com.github.ajalt.timberkt.e
import com.google.android.material.snackbar.Snackbar
import com.zeapo.pwdstore.R 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 com.zeapo.pwdstore.git.config.SshjSessionFactory
import java.io.IOException import com.zeapo.pwdstore.git.operation.GitOperation
import java.lang.ref.WeakReference 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.DisconnectReason
import net.schmizz.sshj.common.SSHException import net.schmizz.sshj.common.SSHException
import net.schmizz.sshj.userauth.UserAuthException import net.schmizz.sshj.userauth.UserAuthException
import org.eclipse.jgit.api.CommitCommand import org.eclipse.jgit.api.CommitCommand
import org.eclipse.jgit.api.GitCommand
import org.eclipse.jgit.api.PullCommand import org.eclipse.jgit.api.PullCommand
import org.eclipse.jgit.api.PushCommand import org.eclipse.jgit.api.PushCommand
import org.eclipse.jgit.api.RebaseResult 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.RemoteRefUpdate
import org.eclipse.jgit.transport.SshSessionFactory import org.eclipse.jgit.transport.SshSessionFactory
class GitCommandExecutor(
class GitAsyncTask( private val activity: FragmentActivity,
activity: AppCompatActivity,
private val operation: GitOperation, private val operation: GitOperation,
private val finishWithResultOnEnd: Intent?, private val finishWithResultOnEnd: Intent? = Intent(),
private val silentlyExecute: Boolean = false private val finishActivityOnEnd: Boolean = true,
) : AsyncTask<GitCommand<*>, Int, GitAsyncTask.Result>() { ) {
private val activityWeakReference: WeakReference<AppCompatActivity> = WeakReference(activity) suspend fun execute() {
private val activity: AppCompatActivity? operation.setCredentialProvider()
get() = activityWeakReference.get() val snackbar = activity.snackbar(
private val context: Context = activity.applicationContext message = activity.resources.getString(R.string.git_operation_running),
private val dialog = ProgressDialog(activity) length = Snackbar.LENGTH_INDEFINITE,
)
sealed class Result { var nbChanges = 0
object Ok : Result() var operationResult: Result = Result.Ok
data class Err(val err: Exception) : Result() for (command in operation.commands) {
}
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) {
try { try {
when (command) { when (command) {
is StatusCommand -> { is StatusCommand -> {
// in case we have changes, we want to keep track of it // 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 nbChanges = status.changed.size + status.missing.size
} }
is CommitCommand -> { is CommitCommand -> {
// the previous status will eventually be used to avoid a commit // 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 -> { is PullCommand -> {
val result = command.call() val result = withContext(Dispatchers.IO) {
command.call()
}
val rr = result.rebaseResult val rr = result.rebaseResult
if (rr.status === RebaseResult.Status.STOPPED) { if (rr.status === RebaseResult.Status.STOPPED) {
return Result.Err(IOException(context.getString(R.string operationResult = Result.Err(PullException(PullException.Reason.REBASE_FAILED))
.git_pull_fail_error)))
} }
} }
is PushCommand -> { 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 // Code imported (modified) from Gerrit PushOp, license Apache v2
for (rru in result.remoteUpdates) { for (rru in result.remoteUpdates) {
val error = when (rru.status) { val error = when (rru.status) {
RemoteRefUpdate.Status.REJECTED_NONFASTFORWARD -> RemoteRefUpdate.Status.REJECTED_NONFASTFORWARD -> {
context.getString(R.string.git_push_nff_error) PushException(PushException.Reason.NON_FAST_FORWARD)
}
RemoteRefUpdate.Status.REJECTED_NODELETE, RemoteRefUpdate.Status.REJECTED_NODELETE,
RemoteRefUpdate.Status.REJECTED_REMOTE_CHANGED, RemoteRefUpdate.Status.REJECTED_REMOTE_CHANGED,
RemoteRefUpdate.Status.NON_EXISTING, RemoteRefUpdate.Status.NON_EXISTING,
RemoteRefUpdate.Status.NOT_ATTEMPTED RemoteRefUpdate.Status.NOT_ATTEMPTED,
-> -> {
(activity!!.getString(R.string.git_push_generic_error) + rru.status.name) PushException(PushException.Reason.GENERIC, rru.status.name)
}
RemoteRefUpdate.Status.REJECTED_OTHER_REASON -> { RemoteRefUpdate.Status.REJECTED_OTHER_REASON -> {
if if ("non-fast-forward" == rru.message) {
("non-fast-forward" == rru.message) { PushException(PushException.Reason.REMOTE_REJECTED)
context.getString(R.string.git_push_other_error)
} else { } else {
(context.getString(R.string.git_push_generic_error) PushException(PushException.Reason.GENERIC, rru.message)
+ rru.message)
} }
} }
else -> null else -> null
} }
if (error != null) if (error != null) {
Result.Err(IOException(error)) operationResult = Result.Err(error)
}
} }
} }
} }
else -> { else -> {
command.call() withContext(Dispatchers.IO) {
command.call()
}
} }
} }
} catch (e: Exception) { } 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 { private fun rootCauseException(e: Exception): Exception {
@@ -130,47 +161,4 @@ class GitAsyncTask(
} }
return rootCause 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 android.util.Patterns
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.os.postDelayed import androidx.core.os.postDelayed
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.zeapo.pwdstore.R 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.PasswordRepository
import com.zeapo.pwdstore.utils.PreferenceKeys import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.viewBinding import com.zeapo.pwdstore.utils.viewBinding
import kotlinx.coroutines.launch
import org.eclipse.jgit.lib.Constants import org.eclipse.jgit.lib.Constants
class GitConfigActivity : BaseGitActivity() { class GitConfigActivity : BaseGitActivity() {
@@ -47,8 +49,8 @@ class GitConfigActivity : BaseGitActivity() {
} catch (ignored: Exception) { } catch (ignored: Exception) {
} }
} }
binding.gitAbortRebase.setOnClickListener { launchGitOperation(BREAK_OUT_OF_DETACHED) } binding.gitAbortRebase.setOnClickListener { lifecycleScope.launch { launchGitOperation(BREAK_OUT_OF_DETACHED) } }
binding.gitResetToRemote.setOnClickListener { launchGitOperation(REQUEST_RESET) } binding.gitResetToRemote.setOnClickListener { lifecycleScope.launch { launchGitOperation(REQUEST_RESET) } }
binding.saveButton.setOnClickListener { binding.saveButton.setOnClickListener {
val email = binding.gitUserEmail.text.toString().trim() val email = binding.gitUserEmail.text.toString().trim()
val name = binding.gitUserName.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.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.R import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.UserPreference import com.zeapo.pwdstore.UserPreference
import com.zeapo.pwdstore.utils.PasswordRepository import com.zeapo.pwdstore.utils.PasswordRepository
import kotlinx.coroutines.launch
open class GitOperationActivity : BaseGitActivity() { open class GitOperationActivity : BaseGitActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
when (intent.extras?.getInt(REQUEST_ARG_OP)) { when (intent.extras?.getInt(REQUEST_ARG_OP)) {
REQUEST_PULL -> syncRepository(REQUEST_PULL) REQUEST_PULL -> lifecycleScope.launch { syncRepository(REQUEST_PULL) }
REQUEST_PUSH -> syncRepository(REQUEST_PUSH) REQUEST_PUSH -> lifecycleScope.launch { syncRepository(REQUEST_PUSH) }
REQUEST_SYNC -> syncRepository(REQUEST_SYNC) REQUEST_SYNC -> lifecycleScope.launch { syncRepository(REQUEST_SYNC) }
else -> { else -> {
setResult(RESULT_CANCELED) setResult(RESULT_CANCELED)
finish() finish()
@@ -54,7 +56,7 @@ open class GitOperationActivity : BaseGitActivity() {
* *
* @param operation the operation to execute can be REQUEST_PULL or REQUEST_PUSH * @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()) if (serverUser.isEmpty() || serverHostname.isEmpty() || url.isNullOrEmpty())
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(this)
.setMessage(getString(R.string.set_information_dialog_text)) .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.content.edit
import androidx.core.os.postDelayed import androidx.core.os.postDelayed
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.zeapo.pwdstore.R 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.PreferenceKeys
import com.zeapo.pwdstore.utils.viewBinding import com.zeapo.pwdstore.utils.viewBinding
import java.io.IOException import java.io.IOException
import kotlinx.coroutines.launch
/** /**
* Activity that encompasses both the initial clone as well as editing the server config for future * 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, _ -> .setPositiveButton(R.string.dialog_delete) { dialog, _ ->
try { try {
localDir.deleteRecursively() localDir.deleteRecursively()
launchGitOperation(REQUEST_CLONE) lifecycleScope.launch { launchGitOperation(REQUEST_CLONE) }
} catch (e: IOException) { } catch (e: IOException) {
// TODO Handle the exception correctly if we are unable to delete the directory... // TODO Handle the exception correctly if we are unable to delete the directory...
e.printStackTrace() e.printStackTrace()
@@ -201,7 +203,7 @@ class GitServerConfigActivity : BaseGitActivity() {
e.printStackTrace() e.printStackTrace()
MaterialAlertDialogBuilder(this).setMessage(e.message).show() 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. * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only * 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.content.Intent
import android.view.LayoutInflater import androidx.annotation.CallSuper
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.edit import androidx.core.content.edit
import androidx.fragment.app.FragmentActivity
import androidx.preference.PreferenceManager 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.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.R
import com.zeapo.pwdstore.UserPreference import com.zeapo.pwdstore.UserPreference
import com.zeapo.pwdstore.git.ErrorMessages
import com.zeapo.pwdstore.git.config.ConnectionMode import com.zeapo.pwdstore.git.config.ConnectionMode
import com.zeapo.pwdstore.git.config.InteractivePasswordFinder import com.zeapo.pwdstore.git.config.InteractivePasswordFinder
import com.zeapo.pwdstore.git.config.SshApiSessionFactory 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.PasswordRepository
import com.zeapo.pwdstore.utils.PreferenceKeys import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.getEncryptedPrefs import com.zeapo.pwdstore.utils.getEncryptedPrefs
import com.zeapo.pwdstore.utils.requestInputFocusOnView
import java.io.File import java.io.File
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import net.schmizz.sshj.userauth.password.PasswordFinder import net.schmizz.sshj.userauth.password.PasswordFinder
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.GitCommand import org.eclipse.jgit.api.GitCommand
import org.eclipse.jgit.api.TransportCommand
import org.eclipse.jgit.errors.UnsupportedCredentialItem import org.eclipse.jgit.errors.UnsupportedCredentialItem
import org.eclipse.jgit.lib.Repository
import org.eclipse.jgit.transport.CredentialItem import org.eclipse.jgit.transport.CredentialItem
import org.eclipse.jgit.transport.CredentialsProvider import org.eclipse.jgit.transport.CredentialsProvider
import org.eclipse.jgit.transport.SshSessionFactory import org.eclipse.jgit.transport.SshSessionFactory
import org.eclipse.jgit.transport.URIish 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 * Creates a new git operation
* *
* @param gitDir the git working tree directory * @param gitDir the git working tree directory
* @param callingActivity the calling activity * @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) abstract val commands: Array<GitCommand<out Any>>
internal var provider: CredentialsProvider? = null private var provider: CredentialsProvider? = null
internal var command: GitCommand<*>? = null
private val sshKeyFile = callingActivity.filesDir.resolve(".ssh_key") private val sshKeyFile = callingActivity.filesDir.resolve(".ssh_key")
private val hostKeyFile = callingActivity.filesDir.resolve(".host_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() { 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 * Executes the GitCommand in an async task
*/ */
abstract fun execute() abstract suspend fun execute()
fun executeAfterAuthentication( suspend fun executeAfterAuthentication(
connectionMode: ConnectionMode, connectionMode: ConnectionMode,
username: String, username: String,
identity: SshApiSessionFactory.ApiIdentity? identity: SshApiSessionFactory.ApiIdentity?
@@ -207,12 +136,12 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: AppCompa
callingActivity.finish() callingActivity.finish()
}.show() }.show()
} else { } else {
withPublicKeyAuthentication(username, GitOperationCredentialFinder(callingActivity, withPublicKeyAuthentication(username, CredentialFinder(callingActivity,
connectionMode)).execute() connectionMode)).execute()
} }
ConnectionMode.OpenKeychain -> withOpenKeychainAuthentication(username, identity).execute() ConnectionMode.OpenKeychain -> withOpenKeychainAuthentication(username, identity).execute()
ConnectionMode.Password -> withPasswordAuthentication( ConnectionMode.Password -> withPasswordAuthentication(
username, GitOperationCredentialFinder(callingActivity, connectionMode)).execute() username, CredentialFinder(callingActivity, connectionMode)).execute()
ConnectionMode.None -> execute() ConnectionMode.None -> execute()
} }
} }
@@ -220,6 +149,7 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: AppCompa
/** /**
* Action to execute on error * Action to execute on error
*/ */
@CallSuper
open fun onError(err: Exception) { open fun onError(err: Exception) {
// Clear various auth related fields on failure // Clear various auth related fields on failure
when (SshSessionFactory.getInstance()) { 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.autofill.AutofillManager
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.annotation.MainThread
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.fragment.app.FragmentActivity
import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey import androidx.security.crypto.MasterKey
import com.github.ajalt.timberkt.d import com.github.ajalt.timberkt.d
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.zeapo.pwdstore.git.GitAsyncTask import com.zeapo.pwdstore.git.GitCommandExecutor
import com.zeapo.pwdstore.git.GitOperation import com.zeapo.pwdstore.git.operation.GitOperation
import com.zeapo.pwdstore.utils.PasswordRepository.Companion.getRepositoryDirectory import com.zeapo.pwdstore.utils.PasswordRepository.Companion.getRepositoryDirectory
import java.io.File import java.io.File
import org.eclipse.jgit.api.Git
const val OPENPGP_PROVIDER = "org.sufficientlysecure.keychain" const val OPENPGP_PROVIDER = "org.sufficientlysecure.keychain"
@@ -51,12 +49,14 @@ fun CharArray.clear() {
val Context.clipboard get() = getSystemService<ClipboardManager>() val Context.clipboard get() = getSystemService<ClipboardManager>()
fun AppCompatActivity.snackbar( fun FragmentActivity.snackbar(
view: View = findViewById(android.R.id.content), view: View = findViewById(android.R.id.content),
message: String, message: String,
length: Int = Snackbar.LENGTH_SHORT length: Int = Snackbar.LENGTH_SHORT,
) { ): Snackbar {
Snackbar.make(view, message, length).show() val snackbar = Snackbar.make(view, message, length)
snackbar.show()
return snackbar
} }
fun File.listFilesRecursively() = walkTopDown().filter { !it.isDirectory }.toList() fun File.listFilesRecursively() = walkTopDown().filter { !it.isDirectory }.toList()
@@ -97,24 +97,33 @@ fun Context.getEncryptedPrefs(fileName: String): SharedPreferences {
) )
} }
@MainThread suspend fun FragmentActivity.commitChange(
fun AppCompatActivity.commitChange(message: String, finishWithResultOnEnd: Intent? = null) { message: String,
finishWithResultOnEnd: Intent? = null,
finishActivityOnEnd: Boolean = true,
) {
if (!PasswordRepository.isGitRepo()) { if (!PasswordRepository.isGitRepo()) {
if (finishWithResultOnEnd != null) { if (finishWithResultOnEnd != null) {
setResult(AppCompatActivity.RESULT_OK, finishWithResultOnEnd) setResult(FragmentActivity.RESULT_OK, finishWithResultOnEnd)
finish() finish()
} }
return return
} }
object : GitOperation(getRepositoryDirectory(this@commitChange), this@commitChange) { 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'" } d { "Comitting with message: '$message'" }
val git = Git(repository) GitCommandExecutor(
val task = GitAsyncTask(this@commitChange, this, finishWithResultOnEnd, silentlyExecute = true) this@commitChange,
task.execute( this,
git.add().addFilepattern("."), finishWithResultOnEnd,
git.commit().setAll(true).setMessage(message) finishActivityOnEnd,
) ).execute()
} }
}.execute() }.execute()
} }
@@ -124,7 +133,6 @@ fun AppCompatActivity.commitChange(message: String, finishWithResultOnEnd: Inten
* view whose id is [id]. Solution based on a StackOverflow * view whose id is [id]. Solution based on a StackOverflow
* answer: https://stackoverflow.com/a/13056259/297261 * answer: https://stackoverflow.com/a/13056259/297261
*/ */
@MainThread
fun <T : View> AlertDialog.requestInputFocusOnView(@IdRes id: Int) { fun <T : View> AlertDialog.requestInputFocusOnView(@IdRes id: Int) {
setOnShowListener { setOnShowListener {
findViewById<T>(id)?.apply { findViewById<T>(id)?.apply {
@@ -143,6 +151,6 @@ val Context.autofillManager: AutofillManager?
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
get() = getSystemService() get() = getSystemService()
fun AppCompatActivity.isInsideRepository(file: File): Boolean { fun FragmentActivity.isInsideRepository(file: File): Boolean {
return file.canonicalPath.contains(getRepositoryDirectory(this).canonicalPath) 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.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.observe
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import kotlin.properties.ReadOnlyProperty import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty 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 { companion object {
val TOTP_FIELDS = arrayOf( val TOTP_FIELDS = arrayOf(
"otpauth://totp", "otpauth://totp",
"totp:" "totp:"

View File

@@ -276,10 +276,6 @@
<string name="autofill_ins_1_hint">Screenshot of accessibility services</string> <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_2_hint">Screenshot of toggle in accessibility services</string>
<string name="autofill_ins_3_hint">Screenshot of autofill service in action</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="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_ssh">Clear saved passphrase for local SSH key</string>
<string name="clear_saved_passphrase_https">Clear saved HTTPS password</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="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="invalid_filename_text">File name must not contain \'/\', set directory above</string>
<string name="directory_hint">Directory</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> </resources>