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