aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java')
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt9
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt52
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/UserPreference.kt17
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt212
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/GitActivity.kt670
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/GitConfigActivity.kt62
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/GitOperation.kt166
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/GitOperationActivity.kt72
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt185
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/config/ConnectionMode.kt19
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/config/Protocol.kt18
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/config/SshApiSessionFactory.java6
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt15
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt39
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.kt14
15 files changed, 750 insertions, 806 deletions
diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt b/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt
index e4551f1c..fd7bdf83 100644
--- a/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt
@@ -23,7 +23,8 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.snackbar.Snackbar
import com.zeapo.pwdstore.databinding.PasswordRecyclerViewBinding
-import com.zeapo.pwdstore.git.GitActivity
+import com.zeapo.pwdstore.git.BaseGitActivity
+import com.zeapo.pwdstore.git.GitOperationActivity
import com.zeapo.pwdstore.ui.OnOffItemAnimator
import com.zeapo.pwdstore.ui.adapters.PasswordItemRecyclerAdapter
import com.zeapo.pwdstore.utils.PasswordItem
@@ -77,9 +78,9 @@ class PasswordFragment : Fragment() {
.show()
swipeRefresher.isRefreshing = false
} else {
- val intent = Intent(context, GitActivity::class.java)
- intent.putExtra("Operation", GitActivity.REQUEST_SYNC)
- startActivityForResult(intent, GitActivity.REQUEST_SYNC)
+ val intent = Intent(context, GitOperationActivity::class.java)
+ intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_SYNC)
+ startActivityForResult(intent, BaseGitActivity.REQUEST_SYNC)
}
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt
index dc09eefe..90b2e631 100644
--- a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt
@@ -38,9 +38,11 @@ import com.google.android.material.snackbar.Snackbar
import com.zeapo.pwdstore.autofill.oreo.AutofillMatcher
import com.zeapo.pwdstore.crypto.PgpActivity
import com.zeapo.pwdstore.crypto.PgpActivity.Companion.getLongName
-import com.zeapo.pwdstore.git.GitActivity
+import com.zeapo.pwdstore.git.BaseGitActivity
import com.zeapo.pwdstore.git.GitAsyncTask
import com.zeapo.pwdstore.git.GitOperation
+import com.zeapo.pwdstore.git.GitOperationActivity
+import com.zeapo.pwdstore.git.GitServerConfigActivity
import com.zeapo.pwdstore.ui.dialogs.FolderCreationDialogFragment
import com.zeapo.pwdstore.utils.PasswordItem
import com.zeapo.pwdstore.utils.PasswordRepository
@@ -249,9 +251,9 @@ class PasswordStore : AppCompatActivity() {
initBefore.show()
return false
}
- intent = Intent(this, GitActivity::class.java)
- intent.putExtra("Operation", GitActivity.REQUEST_PUSH)
- startActivityForResult(intent, GitActivity.REQUEST_PUSH)
+ intent = Intent(this, GitOperationActivity::class.java)
+ intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_PUSH)
+ startActivityForResult(intent, BaseGitActivity.REQUEST_PUSH)
return true
}
R.id.git_pull -> {
@@ -259,9 +261,9 @@ class PasswordStore : AppCompatActivity() {
initBefore.show()
return false
}
- intent = Intent(this, GitActivity::class.java)
- intent.putExtra("Operation", GitActivity.REQUEST_PULL)
- startActivityForResult(intent, GitActivity.REQUEST_PULL)
+ intent = Intent(this, GitOperationActivity::class.java)
+ intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_PULL)
+ startActivityForResult(intent, BaseGitActivity.REQUEST_PULL)
return true
}
R.id.git_sync -> {
@@ -269,9 +271,9 @@ class PasswordStore : AppCompatActivity() {
initBefore.show()
return false
}
- intent = Intent(this, GitActivity::class.java)
- intent.putExtra("Operation", GitActivity.REQUEST_SYNC)
- startActivityForResult(intent, GitActivity.REQUEST_SYNC)
+ intent = Intent(this, GitOperationActivity::class.java)
+ intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_SYNC)
+ startActivityForResult(intent, BaseGitActivity.REQUEST_SYNC)
return true
}
R.id.refresh -> {
@@ -353,7 +355,7 @@ class PasswordStore : AppCompatActivity() {
.setMessage(this.resources.getString(R.string.key_dialog_text))
.setPositiveButton(this.resources.getString(R.string.dialog_positive)) { _, _ ->
val intent = Intent(activity, UserPreference::class.java)
- startActivityForResult(intent, GitActivity.REQUEST_INIT)
+ startActivityForResult(intent, BaseGitActivity.REQUEST_INIT)
}
.setNegativeButton(this.resources.getString(R.string.dialog_negative), null)
.show()
@@ -550,7 +552,7 @@ class PasswordStore : AppCompatActivity() {
fileLocations.add(file.absolutePath)
}
intent.putExtra("Files", fileLocations)
- intent.putExtra("Operation", "SELECTFOLDER")
+ intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, "SELECTFOLDER")
startActivityForResult(intent, REQUEST_CODE_SELECT_FOLDER)
}
@@ -586,7 +588,7 @@ class PasswordStore : AppCompatActivity() {
if (resultCode == Activity.RESULT_OK) {
when (requestCode) {
// if we get here with a RESULT_OK then it's probably OK :)
- GitActivity.REQUEST_CLONE -> settings.edit().putBoolean("repository_initialized", true).apply()
+ BaseGitActivity.REQUEST_CLONE -> settings.edit().putBoolean("repository_initialized", true).apply()
// if went from decrypt->edit and user saved changes or HOTP counter was
// incremented, we need to commitChange
REQUEST_CODE_DECRYPT_AND_VERIFY -> {
@@ -620,8 +622,8 @@ class PasswordStore : AppCompatActivity() {
data!!.extras!!.getString("LONG_NAME")))
refreshPasswordList()
}
- GitActivity.REQUEST_INIT, NEW_REPO_BUTTON -> initializeRepositoryInfo()
- GitActivity.REQUEST_SYNC, GitActivity.REQUEST_PULL -> resetPasswordList()
+ BaseGitActivity.REQUEST_INIT, NEW_REPO_BUTTON -> initializeRepositoryInfo()
+ BaseGitActivity.REQUEST_SYNC, BaseGitActivity.REQUEST_PULL -> resetPasswordList()
HOME -> checkLocalRepository()
// duplicate code
CLONE_REPO_BUTTON -> {
@@ -639,9 +641,9 @@ class PasswordStore : AppCompatActivity() {
return // if not empty, just show me the passwords!
}
}
- val intent = Intent(activity, GitActivity::class.java)
- intent.putExtra("Operation", GitActivity.REQUEST_CLONE)
- startActivityForResult(intent, GitActivity.REQUEST_CLONE)
+ val intent = Intent(activity, GitOperationActivity::class.java)
+ intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_CLONE)
+ startActivityForResult(intent, BaseGitActivity.REQUEST_CLONE)
}
REQUEST_CODE_SELECT_FOLDER -> {
Timber.tag(TAG)
@@ -722,10 +724,9 @@ class PasswordStore : AppCompatActivity() {
when (operation) {
NEW_REPO_BUTTON -> initializeRepositoryInfo()
CLONE_REPO_BUTTON -> {
- initialize(this@PasswordStore)
- val intent = Intent(activity, GitActivity::class.java)
- intent.putExtra("Operation", GitActivity.REQUEST_CLONE)
- startActivityForResult(intent, GitActivity.REQUEST_CLONE)
+ val intent = Intent(activity, GitServerConfigActivity::class.java)
+ intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_CLONE)
+ startActivityForResult(intent, BaseGitActivity.REQUEST_CLONE)
}
}
}
@@ -744,10 +745,9 @@ class PasswordStore : AppCompatActivity() {
when (operation) {
NEW_REPO_BUTTON -> initializeRepositoryInfo()
CLONE_REPO_BUTTON -> {
- initialize(this@PasswordStore)
- val intent = Intent(activity, GitActivity::class.java)
- intent.putExtra("Operation", GitActivity.REQUEST_CLONE)
- startActivityForResult(intent, GitActivity.REQUEST_CLONE)
+ val intent = Intent(activity, GitServerConfigActivity::class.java)
+ intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_CLONE)
+ startActivityForResult(intent, BaseGitActivity.REQUEST_CLONE)
}
}
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt
index 716a9192..eabc35f4 100644
--- a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt
@@ -37,7 +37,8 @@ import com.zeapo.pwdstore.autofill.AutofillPreferenceActivity
import com.zeapo.pwdstore.autofill.oreo.BrowserAutofillSupportLevel
import com.zeapo.pwdstore.autofill.oreo.getInstalledBrowsersWithAutofillSupportLevel
import com.zeapo.pwdstore.crypto.PgpActivity
-import com.zeapo.pwdstore.git.GitActivity
+import com.zeapo.pwdstore.git.GitConfigActivity
+import com.zeapo.pwdstore.git.GitServerConfigActivity
import com.zeapo.pwdstore.pwgenxkpwd.XkpwdDictionary
import com.zeapo.pwdstore.sshkeygen.ShowSshKeyFragment
import com.zeapo.pwdstore.sshkeygen.SshKeyGenActivity
@@ -45,6 +46,7 @@ import com.zeapo.pwdstore.utils.PasswordRepository
import com.zeapo.pwdstore.utils.auth.AuthenticationResult
import com.zeapo.pwdstore.utils.auth.Authenticator
import com.zeapo.pwdstore.utils.autofillManager
+import com.zeapo.pwdstore.utils.getEncryptedPrefs
import java.io.File
import java.io.IOException
import java.time.LocalDateTime
@@ -73,6 +75,7 @@ class UserPreference : AppCompatActivity() {
callingActivity = requireActivity() as UserPreference
val context = requireContext()
val sharedPreferences = preferenceManager.sharedPreferences
+ val encryptedPreferences = requireActivity().applicationContext.getEncryptedPrefs("git_operation")
addPreferencesFromResource(R.xml.preference)
@@ -121,7 +124,7 @@ class UserPreference : AppCompatActivity() {
selectExternalGitRepositoryPreference?.summary = sharedPreferences.getString("git_external_repo", getString(R.string.no_repo_selected))
viewSshKeyPreference?.isVisible = sharedPreferences.getBoolean("use_generated_key", false)
deleteRepoPreference?.isVisible = !sharedPreferences.getBoolean("git_external", false)
- sshClearPassphrasePreference?.isVisible = sharedPreferences.getString("ssh_key_passphrase", null)?.isNotEmpty()
+ sshClearPassphrasePreference?.isVisible = encryptedPreferences.getString("ssh_key_local_passphrase", null)?.isNotEmpty()
?: false
clearHotpIncrementPreference?.isVisible = sharedPreferences.getBoolean("hotp_remember_check", false)
clearAfterCopyPreference?.isVisible = sharedPreferences.getString("general_show_time", "45")?.toInt() != 0
@@ -172,7 +175,7 @@ class UserPreference : AppCompatActivity() {
}
sshClearPassphrasePreference?.onPreferenceClickListener = ClickListener {
- sharedPreferences.edit().putString("ssh_key_passphrase", null).apply()
+ encryptedPreferences.edit().putString("ssh_key_local_passphrase", null).apply()
it.isVisible = false
true
}
@@ -190,16 +193,12 @@ class UserPreference : AppCompatActivity() {
}
gitServerPreference?.onPreferenceClickListener = ClickListener {
- val intent = Intent(callingActivity, GitActivity::class.java)
- intent.putExtra("Operation", GitActivity.EDIT_SERVER)
- startActivityForResult(intent, EDIT_GIT_INFO)
+ startActivity(Intent(callingActivity, GitServerConfigActivity::class.java))
true
}
gitConfigPreference?.onPreferenceClickListener = ClickListener {
- val intent = Intent(callingActivity, GitActivity::class.java)
- intent.putExtra("Operation", GitActivity.EDIT_GIT_CONFIG)
- startActivityForResult(intent, EDIT_GIT_CONFIG)
+ startActivity(Intent(callingActivity, GitConfigActivity::class.java))
true
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt
new file mode 100644
index 00000000..36dd95c4
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt
@@ -0,0 +1,212 @@
+/*
+ * 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.content.SharedPreferences
+import android.os.Bundle
+import android.view.MenuItem
+import androidx.annotation.CallSuper
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.edit
+import androidx.core.text.isDigitsOnly
+import androidx.preference.PreferenceManager
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.zeapo.pwdstore.git.config.ConnectionMode
+import com.zeapo.pwdstore.git.config.Protocol
+import com.zeapo.pwdstore.git.config.SshApiSessionFactory
+import com.zeapo.pwdstore.utils.PasswordRepository
+import com.zeapo.pwdstore.utils.getEncryptedPrefs
+import java.io.File
+import timber.log.Timber
+
+/**
+ * Abstract AppCompatActivity that holds some information that is commonly shared across git-related
+ * tasks and makes sense to be held here.
+ */
+abstract class BaseGitActivity : AppCompatActivity() {
+ lateinit var protocol: Protocol
+ lateinit var connectionMode: ConnectionMode
+ lateinit var url: String
+ lateinit var serverHostname: String
+ lateinit var serverPort: String
+ lateinit var serverUser: String
+ lateinit var serverPath: String
+ lateinit var username: String
+ lateinit var email: String
+ var identityBuilder: SshApiSessionFactory.IdentityBuilder? = null
+ var identity: SshApiSessionFactory.ApiIdentity? = null
+ lateinit var settings: SharedPreferences
+ private set
+ private lateinit var encryptedSettings: SharedPreferences
+
+ @CallSuper
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ settings = PreferenceManager.getDefaultSharedPreferences(this)
+ encryptedSettings = getEncryptedPrefs("git_operation")
+ protocol = Protocol.fromString(settings.getString("git_remote_protocol", null))
+ connectionMode = ConnectionMode.fromString(settings.getString("git_remote_auth", null))
+ serverHostname = settings.getString("git_remote_server", null) ?: ""
+ serverPort = settings.getString("git_remote_port", null) ?: ""
+ serverUser = settings.getString("git_remote_username", null) ?: ""
+ serverPath = settings.getString("git_remote_location", null) ?: ""
+ username = settings.getString("git_config_user_name", null) ?: ""
+ email = settings.getString("git_config_user_email", null) ?: ""
+ updateUrl()
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when (item.itemId) {
+ android.R.id.home -> {
+ finish()
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
+
+ @CallSuper
+ override fun onDestroy() {
+ // Do not leak the service connection
+ if (identityBuilder != null) {
+ identityBuilder!!.close()
+ identityBuilder = null
+ }
+ super.onDestroy()
+ }
+
+ /**
+ * Update the [url] field with the values that build it up. This function returns a boolean
+ * indicating whether or not the values are likely valid or not, and only adds the `origin`
+ * remote when it is. This check is not perfect, it is mostly meant to catch typos.
+ */
+ fun updateUrl(): Boolean {
+ if (serverHostname.isEmpty() || !serverPort.isDigitsOnly())
+ return false
+
+ val previousUrl = if (::url.isInitialized) url else ""
+ val hostnamePart = serverHostname
+ val pathPart = if (serverPath.startsWith('/')) serverPath else "/$serverPath"
+ url = when (protocol) {
+ Protocol.Ssh -> {
+ val userPart = if (serverUser.isEmpty()) "" else "$serverUser@"
+ val portPart =
+ if (serverPort == "22" || serverPort.isEmpty()) "" else ":$serverPort"
+ // We have to specify the ssh scheme as this is the only way to pass a custom port.
+ "ssh://$userPart$hostnamePart$portPart$pathPart"
+ }
+ Protocol.Https -> {
+ val portPart =
+ if (serverPort == "443" || serverPort.isEmpty()) "" else ":$serverPort"
+ "https://$hostnamePart$portPart$pathPart"
+ }
+ }
+ if (PasswordRepository.isInitialized)
+ PasswordRepository.addRemote("origin", url, true)
+ // HTTPS authentication sends the password to the server, so we must wipe the password when
+ // the server is changed.
+ if (url != previousUrl && protocol == Protocol.Https)
+ encryptedSettings.edit { remove("https_password") }
+ return true
+ }
+
+ /**
+ * Attempt to launch the requested Git operation. Depending on the configured auth, it may not
+ * be possible to launch the operation immediately. In that case, this function may launch an
+ * intermediate activity instead, which will gather necessary information and post it back via
+ * onActivityResult, which will then re-call this function. This may happen multiple times,
+ * until either an error is encountered or the operation is successfully launched.
+ *
+ * @param operation The type of git operation to launch
+ */
+ fun launchGitOperation(operation: Int) {
+ val op: GitOperation
+ val localDir = requireNotNull(PasswordRepository.getRepositoryDirectory(this))
+ try {
+ // Before launching the operation with OpenKeychain auth, we need to issue several requests
+ // to the OpenKeychain API. IdentityBuild will take care of launching the relevant intents,
+ // we just need to keep calling it until it returns a completed ApiIdentity.
+ if (connectionMode == ConnectionMode.OpenKeychain && identity == null) {
+ // Lazy initialization of the IdentityBuilder
+ if (identityBuilder == null) {
+ identityBuilder = SshApiSessionFactory.IdentityBuilder(this)
+ }
+ // Try to get an ApiIdentity and bail if one is not ready yet. The builder will ensure
+ // that onActivityResult is called with operation again, which will re-invoke us here
+ identity = identityBuilder!!.tryBuild(operation)
+ if (identity == null)
+ return
+ }
+
+ op = when (operation) {
+ REQUEST_CLONE, GitOperation.GET_SSH_KEY_FROM_CLONE -> CloneOperation(localDir, this).setCommand(url)
+ REQUEST_PULL -> PullOperation(localDir, this).setCommand()
+ REQUEST_PUSH -> PushOperation(localDir, this).setCommand()
+ REQUEST_SYNC -> SyncOperation(localDir, this).setCommands()
+ BREAK_OUT_OF_DETACHED -> BreakOutOfDetached(localDir, this).setCommands()
+ REQUEST_RESET -> ResetToRemoteOperation(localDir, this).setCommands()
+ SshApiSessionFactory.POST_SIGNATURE -> return
+ else -> {
+ Timber.tag(TAG).e("Operation not recognized : $operation")
+ setResult(RESULT_CANCELED)
+ finish()
+ return
+ }
+ }
+ op.executeAfterAuthentication(connectionMode, serverUser,
+ File("$filesDir/.ssh_key"), identity)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ MaterialAlertDialogBuilder(this).setMessage(e.message).show()
+ }
+ }
+
+ public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ // In addition to the pre-operation-launch series of intents for OpenKeychain auth
+ // that will pass through here and back to launchGitOperation, there is one
+ // synchronous operation that happens /after/ the operation has been launched in the
+ // background thread - the actual signing of the SSH challenge. We pass through the
+ // completed signature to the ApiIdentity, which will be blocked in the other thread
+ // waiting for it.
+ if (requestCode == SshApiSessionFactory.POST_SIGNATURE && identity != null) {
+ identity!!.postSignature(data)
+
+ // If the signature failed (usually because it was cancelled), reset state
+ if (data == null) {
+ identity = null
+ identityBuilder = null
+ }
+ return
+ }
+
+ if (resultCode == RESULT_CANCELED) {
+ setResult(RESULT_CANCELED)
+ finish()
+ } else if (resultCode == RESULT_OK) {
+ // If an operation has been re-queued via this mechanism, let the
+ // IdentityBuilder attempt to extract some updated state from the intent before
+ // trying to re-launch the operation.
+ if (identityBuilder != null) {
+ identityBuilder!!.consume(data)
+ }
+ launchGitOperation(requestCode)
+ }
+ super.onActivityResult(requestCode, resultCode, data)
+ }
+
+ companion object {
+ const val REQUEST_ARG_OP = "OPERATION"
+ const val REQUEST_PULL = 101
+ const val REQUEST_PUSH = 102
+ const val REQUEST_CLONE = 103
+ const val REQUEST_INIT = 104
+ const val REQUEST_SYNC = 105
+ const val BREAK_OUT_OF_DETACHED = 106
+ const val REQUEST_RESET = 107
+ const val TAG = "AbstractGitActivity"
+ }
+}
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitActivity.kt
deleted file mode 100644
index 6341fbe1..00000000
--- a/app/src/main/java/com/zeapo/pwdstore/git/GitActivity.kt
+++ /dev/null
@@ -1,670 +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.Context
-import android.content.Intent
-import android.content.SharedPreferences
-import android.os.Bundle
-import android.text.Editable
-import android.text.TextWatcher
-import android.view.Menu
-import android.view.MenuItem
-import android.view.View
-import android.widget.AdapterView
-import android.widget.ArrayAdapter
-import android.widget.Spinner
-import androidx.appcompat.app.AppCompatActivity
-import androidx.appcompat.widget.AppCompatTextView
-import androidx.preference.PreferenceManager
-import com.google.android.material.button.MaterialButton
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.google.android.material.textfield.TextInputEditText
-import com.zeapo.pwdstore.R
-import com.zeapo.pwdstore.UserPreference
-import com.zeapo.pwdstore.git.config.SshApiSessionFactory
-import com.zeapo.pwdstore.utils.PasswordRepository
-import java.io.File
-import java.io.IOException
-import java.util.regex.Pattern
-import org.apache.commons.io.FileUtils
-import org.eclipse.jgit.lib.Constants
-import timber.log.Timber
-
-open class GitActivity : AppCompatActivity() {
- private lateinit var context: Context
- private lateinit var settings: SharedPreferences
- private lateinit var protocol: String
- private lateinit var connectionMode: String
- private lateinit var hostname: String
- private var identityBuilder: SshApiSessionFactory.IdentityBuilder? = null
- private var identity: SshApiSessionFactory.ApiIdentity? = null
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- context = requireNotNull(this)
-
- settings = PreferenceManager.getDefaultSharedPreferences(this)
-
- protocol = settings.getString("git_remote_protocol", null) ?: "ssh://"
- connectionMode = settings.getString("git_remote_auth", null) ?: "ssh-key"
- hostname = settings.getString("git_remote_location", null) ?: ""
- val operationCode = intent.extras!!.getInt("Operation")
-
- supportActionBar!!.setDisplayHomeAsUpEnabled(true)
-
- when (operationCode) {
- REQUEST_CLONE, EDIT_SERVER -> {
- setContentView(R.layout.activity_git_clone)
- setTitle(R.string.title_activity_git_clone)
-
- val protcolSpinner = findViewById<Spinner>(R.id.clone_protocol)
- val connectionModeSpinner = findViewById<Spinner>(R.id.connection_mode)
-
- // init the spinner for connection modes
- val connectionModeAdapter = ArrayAdapter.createFromResource(this,
- R.array.connection_modes, android.R.layout.simple_spinner_item)
- connectionModeAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
- connectionModeSpinner.adapter = connectionModeAdapter
- connectionModeSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
- override fun onItemSelected(adapterView: AdapterView<*>, view: View, i: Int, l: Long) {
- val selection = (findViewById<View>(R.id.connection_mode) as Spinner).selectedItem.toString()
- connectionMode = selection
- settings.edit().putString("git_remote_auth", selection).apply()
- }
-
- override fun onNothingSelected(adapterView: AdapterView<*>) {
- }
- }
-
- // init the spinner for protocols
- val protocolAdapter = ArrayAdapter.createFromResource(this,
- R.array.clone_protocols, android.R.layout.simple_spinner_item)
- protocolAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
- protcolSpinner.adapter = protocolAdapter
- protcolSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
- override fun onItemSelected(adapterView: AdapterView<*>, view: View, i: Int, l: Long) {
- protocol = (findViewById<View>(R.id.clone_protocol) as Spinner).selectedItem.toString()
- if (protocol == "ssh://") {
-
- // select ssh-key auth mode as default and enable the spinner in case it was disabled
- connectionModeSpinner.setSelection(0)
- connectionModeSpinner.isEnabled = true
-
- // however, if we have some saved that, that's more important!
- when {
- connectionMode.equals("ssh-key", ignoreCase = true) -> connectionModeSpinner.setSelection(0)
- connectionMode.equals("OpenKeychain", ignoreCase = true) -> connectionModeSpinner.setSelection(2)
- else -> connectionModeSpinner.setSelection(1)
- }
- } else {
- // select user/pwd auth-mode and disable the spinner
- connectionModeSpinner.setSelection(1)
- connectionModeSpinner.isEnabled = false
- }
-
- updateURI()
- }
-
- override fun onNothingSelected(adapterView: AdapterView<*>) {
- }
- }
-
- if (protocol == "ssh://") {
- protcolSpinner.setSelection(0)
- } else {
- protcolSpinner.setSelection(1)
- }
-
- // init the server information
- val serverUrl = findViewById<TextInputEditText>(R.id.server_url)
- val serverPort = findViewById<TextInputEditText>(R.id.server_port)
- val serverPath = findViewById<TextInputEditText>(R.id.server_path)
- val serverUser = findViewById<TextInputEditText>(R.id.server_user)
- val serverUri = findViewById<TextInputEditText>(R.id.clone_uri)
-
- serverUrl.setText(settings.getString("git_remote_server", ""))
- serverPort.setText(settings.getString("git_remote_port", ""))
- serverUser.setText(settings.getString("git_remote_username", ""))
- serverPath.setText(settings.getString("git_remote_location", ""))
-
- serverUrl.addTextChangedListener(object : TextWatcher {
- override fun beforeTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) {}
-
- override fun onTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) {
- if (serverUrl.isFocused)
- updateURI()
- }
-
- override fun afterTextChanged(editable: Editable) {}
- })
- serverPort.addTextChangedListener(object : TextWatcher {
- override fun beforeTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) {}
-
- override fun onTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) {
- if (serverPort.isFocused)
- updateURI()
- }
-
- override fun afterTextChanged(editable: Editable) {}
- })
- serverUser.addTextChangedListener(object : TextWatcher {
- override fun beforeTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) {}
-
- override fun onTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) {
- if (serverUser.isFocused)
- updateURI()
- }
-
- override fun afterTextChanged(editable: Editable) {}
- })
- serverPath.addTextChangedListener(object : TextWatcher {
- override fun beforeTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) {}
-
- override fun onTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) {
- if (serverPath.isFocused)
- updateURI()
- }
-
- override fun afterTextChanged(editable: Editable) {}
- })
-
- serverUri.addTextChangedListener(object : TextWatcher {
- override fun beforeTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) {}
-
- override fun onTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) {
- if (serverUri.isFocused)
- splitURI()
- }
-
- override fun afterTextChanged(editable: Editable) {}
- })
-
- if (operationCode == EDIT_SERVER) {
- findViewById<View>(R.id.clone_button).visibility = View.INVISIBLE
- findViewById<View>(R.id.save_button).visibility = View.VISIBLE
- } else {
- findViewById<View>(R.id.clone_button).visibility = View.VISIBLE
- findViewById<View>(R.id.save_button).visibility = View.INVISIBLE
- }
-
- updateURI()
- }
- EDIT_GIT_CONFIG -> {
- setContentView(R.layout.activity_git_config)
- setTitle(R.string.title_activity_git_config)
-
- showGitConfig()
- }
- REQUEST_PULL -> syncRepository(REQUEST_PULL)
-
- REQUEST_PUSH -> syncRepository(REQUEST_PUSH)
-
- REQUEST_SYNC -> syncRepository(REQUEST_SYNC)
- }
- }
-
- /**
- * Fills in the server_uri field with the information coming from other fields
- */
- private fun updateURI() {
- val uri = findViewById<TextInputEditText>(R.id.clone_uri)
- val serverUrl = findViewById<TextInputEditText>(R.id.server_url)
- val serverPort = findViewById<TextInputEditText>(R.id.server_port)
- val serverPath = findViewById<TextInputEditText>(R.id.server_path)
- val serverUser = findViewById<TextInputEditText>(R.id.server_user)
-
- if (uri != null) {
- when (protocol) {
- "ssh://" -> {
- var hostname = (serverUser.text.toString() +
- "@" +
- serverUrl.text.toString().trim { it <= ' ' } +
- ":")
- if (serverPort.text.toString() == "22") {
- hostname += serverPath.text.toString()
-
- findViewById<View>(R.id.warn_url).visibility = View.GONE
- } else {
- val warnUrl = findViewById<AppCompatTextView>(R.id.warn_url)
- if (!serverPath.text.toString().matches("/.*".toRegex()) && serverPort.text.toString().isNotEmpty()) {
- warnUrl.setText(R.string.warn_malformed_url_port)
- warnUrl.visibility = View.VISIBLE
- } else {
- warnUrl.visibility = View.GONE
- }
- hostname += serverPort.text.toString() + serverPath.text.toString()
- }
-
- if (hostname != "@:") uri.setText(hostname)
- }
- "https://" -> {
- val hostname = StringBuilder()
- hostname.append(serverUrl.text.toString().trim { it <= ' ' })
-
- if (serverPort.text.toString() == "443") {
- hostname.append(serverPath.text.toString())
-
- findViewById<View>(R.id.warn_url).visibility = View.GONE
- } else {
- hostname.append("/")
- hostname.append(serverPort.text.toString())
- .append(serverPath.text.toString())
- }
-
- if (hostname.toString() != "@/") uri.setText(hostname)
- }
- else -> {
- }
- }
- }
- }
-
- /**
- * Splits the information in server_uri into the other fields
- */
- private fun splitURI() {
- val serverUri = findViewById<TextInputEditText>(R.id.clone_uri)
- val serverUrl = findViewById<TextInputEditText>(R.id.server_url)
- val serverPort = findViewById<TextInputEditText>(R.id.server_port)
- val serverPath = findViewById<TextInputEditText>(R.id.server_path)
- val serverUser = findViewById<TextInputEditText>(R.id.server_user)
-
- val uri = serverUri.text.toString()
- val pattern = Pattern.compile("(.+)@([\\w\\d.]+):([\\d]+)*(.*)")
- val matcher = pattern.matcher(uri)
- if (matcher.find()) {
- val count = matcher.groupCount()
- if (count > 1) {
- serverUser.setText(matcher.group(1))
- serverUrl.setText(matcher.group(2))
- }
- if (count == 4) {
- serverPort.setText(matcher.group(3))
- serverPath.setText(matcher.group(4))
-
- val warnUrl = findViewById<AppCompatTextView>(R.id.warn_url)
- if (!serverPath.text.toString().matches("/.*".toRegex()) && serverPort.text.toString().isNotEmpty()) {
- warnUrl.setText(R.string.warn_malformed_url_port)
- warnUrl.visibility = View.VISIBLE
- } else {
- warnUrl.visibility = View.GONE
- }
- }
- }
- }
-
- public override fun onResume() {
- super.onResume()
- updateURI()
- }
-
- override fun onDestroy() {
- // Do not leak the service connection
- if (identityBuilder != null) {
- identityBuilder!!.close()
- identityBuilder = null
- }
- super.onDestroy()
- }
-
- 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 {
- when (item.itemId) {
- R.id.user_pref -> try {
- val intent = Intent(this, UserPreference::class.java)
- startActivity(intent)
- return true
- } catch (e: Exception) {
- println("Exception caught :(")
- e.printStackTrace()
- }
-
- android.R.id.home -> {
- finish()
- return true
- }
- }
- return super.onOptionsItemSelected(item)
- }
-
- /**
- * Saves the configuration found in the form
- */
- private fun saveConfiguration(): Boolean {
- // remember the settings
- val editor = settings.edit()
-
- editor.putString("git_remote_server", (findViewById<View>(R.id.server_url) as TextInputEditText).text.toString())
- editor.putString("git_remote_location", (findViewById<View>(R.id.server_path) as TextInputEditText).text.toString())
- editor.putString("git_remote_username", (findViewById<View>(R.id.server_user) as TextInputEditText).text.toString())
- editor.putString("git_remote_protocol", protocol)
- editor.putString("git_remote_auth", connectionMode)
- editor.putString("git_remote_port", (findViewById<View>(R.id.server_port) as TextInputEditText).text.toString())
- editor.putString("git_remote_uri", (findViewById<View>(R.id.clone_uri) as TextInputEditText).text.toString())
-
- // 'save' hostname variable for use by addRemote() either here or later
- // in syncRepository()
- hostname = (findViewById<View>(R.id.clone_uri) as TextInputEditText).text.toString()
- val port = (findViewById<View>(R.id.server_port) as TextInputEditText).text.toString()
- // don't ask the user, take off the protocol that he puts in
- hostname = hostname.replaceFirst("^.+://".toRegex(), "")
- (findViewById<View>(R.id.clone_uri) as TextInputEditText).setText(hostname)
-
- if (protocol != "ssh://") {
- hostname = protocol + hostname
- } else {
- // if the port is explicitly given, jgit requires the ssh://
- if (port.isNotEmpty() && port != "22")
- hostname = protocol + hostname
-
- // did he forget the username?
- if (!hostname.matches("^.+@.+".toRegex())) {
- MaterialAlertDialogBuilder(this)
- .setMessage(context.getString(R.string.forget_username_dialog_text))
- .setPositiveButton(context.getString(R.string.dialog_oops), null)
- .show()
- return false
- }
- }
- if (PasswordRepository.isInitialized && settings.getBoolean("repository_initialized", false)) {
- // don't just use the clone_uri text, need to use hostname which has
- // had the proper protocol prepended
- PasswordRepository.addRemote("origin", hostname, true)
- }
-
- editor.apply()
- return true
- }
-
- /**
- * Save the repository information to the shared preferences settings
- */
- @Suppress("UNUSED_PARAMETER")
- fun saveConfiguration(view: View) {
- if (!saveConfiguration())
- return
- finish()
- }
-
- private fun showGitConfig() {
- // init the server information
- val username = findViewById<TextInputEditText>(R.id.git_user_name)
- val email = findViewById<TextInputEditText>(R.id.git_user_email)
- val abort = findViewById<MaterialButton>(R.id.git_abort_rebase)
-
- username.setText(settings.getString("git_config_user_name", ""))
- email.setText(settings.getString("git_config_user_email", ""))
-
- // git status
- val repo = PasswordRepository.getRepository(PasswordRepository.getRepositoryDirectory(context))
- if (repo != null) {
- val commitHash = findViewById<AppCompatTextView>(R.id.git_commit_hash)
- try {
- val objectId = repo.resolve(Constants.HEAD)
- val ref = repo.getRef("refs/heads/master")
- val head = if (ref.objectId.equals(objectId)) ref.name else "DETACHED"
- commitHash.text = String.format("%s (%s)", objectId.abbreviate(8).name(), head)
-
- // enable the abort button only if we're rebasing
- val isRebasing = repo.repositoryState.isRebasing
- abort.isEnabled = isRebasing
- abort.alpha = if (isRebasing) 1.0f else 0.5f
- } catch (e: Exception) {
- // ignore
- }
- }
- }
-
- private fun saveGitConfigs(): Boolean {
- // remember the settings
- val editor = settings.edit()
-
- val email = (findViewById<View>(R.id.git_user_email) as TextInputEditText).text!!.toString()
- editor.putString("git_config_user_email", email)
- editor.putString("git_config_user_name", (findViewById<View>(R.id.git_user_name) as TextInputEditText).text.toString())
-
- if (!email.matches(emailPattern.toRegex())) {
- MaterialAlertDialogBuilder(this)
- .setMessage(context.getString(R.string.invalid_email_dialog_text))
- .setPositiveButton(context.getString(R.string.dialog_oops), null)
- .show()
- return false
- }
-
- editor.apply()
- return true
- }
-
- @Suppress("UNUSED_PARAMETER")
- fun applyGitConfigs(view: View) {
- if (!saveGitConfigs())
- return
- PasswordRepository.setUserName(settings.getString("git_config_user_name", null) ?: "")
- PasswordRepository.setUserEmail(settings.getString("git_config_user_email", null) ?: "")
- finish()
- }
-
- @Suppress("UNUSED_PARAMETER")
- fun abortRebase(view: View) {
- launchGitOperation(BREAK_OUT_OF_DETACHED)
- }
-
- @Suppress("UNUSED_PARAMETER")
- fun resetToRemote(view: View) {
- launchGitOperation(REQUEST_RESET)
- }
-
- /**
- * Clones the repository, the directory exists, deletes it
- */
- @Suppress("UNUSED_PARAMETER")
- fun cloneRepository(view: View) {
- if (PasswordRepository.getRepository(null) == null) {
- PasswordRepository.initialize(this)
- }
- val localDir = requireNotNull(PasswordRepository.getRepositoryDirectory(context))
-
- if (!saveConfiguration())
- return
-
- // Warn if non-empty folder unless it's a just-initialized store that has just a .git folder
- if (localDir.exists() && localDir.listFiles()!!.isNotEmpty() &&
- !(localDir.listFiles()!!.size == 1 && localDir.listFiles()!![0].name == ".git")) {
- MaterialAlertDialogBuilder(this)
- .setTitle(R.string.dialog_delete_title)
- .setMessage(resources.getString(R.string.dialog_delete_msg) + " " + localDir.toString())
- .setCancelable(false)
- .setPositiveButton(R.string.dialog_delete
- ) { dialog, _ ->
- try {
- FileUtils.deleteDirectory(localDir)
- launchGitOperation(REQUEST_CLONE)
- } catch (e: IOException) {
- // TODO Handle the exception correctly if we are unable to delete the directory...
- e.printStackTrace()
- MaterialAlertDialogBuilder(this).setMessage(e.message).show()
- }
-
- dialog.cancel()
- }
- .setNegativeButton(R.string.dialog_do_not_delete
- ) { dialog, _ -> dialog.cancel() }
- .show()
- } else {
- try {
- // Silently delete & replace the lone .git folder if it exists
- if (localDir.exists() && localDir.listFiles()!!.size == 1 && localDir.listFiles()!![0].name == ".git") {
- try {
- FileUtils.deleteDirectory(localDir)
- } catch (e: IOException) {
- e.printStackTrace()
- MaterialAlertDialogBuilder(this).setMessage(e.message).show()
- }
- }
- } catch (e: Exception) {
- // This is what happens when jgit fails :(
- // TODO Handle the diffent cases of exceptions
- e.printStackTrace()
- MaterialAlertDialogBuilder(this).setMessage(e.message).show()
- }
-
- launchGitOperation(REQUEST_CLONE)
- }
- }
-
- /**
- * 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 fun syncRepository(operation: Int) {
- if (settings.getString("git_remote_username", "")!!.isEmpty() ||
- settings.getString("git_remote_server", "")!!.isEmpty() ||
- settings.getString("git_remote_location", "")!!.isEmpty())
- MaterialAlertDialogBuilder(this)
- .setMessage(context.getString(R.string.set_information_dialog_text))
- .setPositiveButton(context.getString(R.string.dialog_positive)) { _, _ ->
- val intent = Intent(context, UserPreference::class.java)
- startActivityForResult(intent, REQUEST_PULL)
- }
- .setNegativeButton(context.getString(R.string.dialog_negative)) { _, _ ->
- // do nothing :(
- setResult(AppCompatActivity.RESULT_OK)
- finish()
- }
- .show()
- else {
- // check that the remote origin is here, else add it
- PasswordRepository.addRemote("origin", hostname, false)
- launchGitOperation(operation)
- }
- }
-
- /**
- * Attempt to launch the requested GIT operation. Depending on the configured auth, it may not
- * be possible to launch the operation immediately. In that case, this function may launch an
- * intermediate activity instead, which will gather necessary information and post it back via
- * onActivityResult, which will then re-call this function. This may happen multiple times,
- * until either an error is encountered or the operation is successfully launched.
- *
- * @param operation The type of GIT operation to launch
- */
- private fun launchGitOperation(operation: Int) {
- val op: GitOperation
- val localDir = requireNotNull(PasswordRepository.getRepositoryDirectory(context))
-
- try {
-
- // Before launching the operation with OpenKeychain auth, we need to issue several requests
- // to the OpenKeychain API. IdentityBuild will take care of launching the relevant intents,
- // we just need to keep calling it until it returns a completed ApiIdentity.
- if (connectionMode.equals("OpenKeychain", ignoreCase = true) && identity == null) {
- // Lazy initialization of the IdentityBuilder
- if (identityBuilder == null) {
- identityBuilder = SshApiSessionFactory.IdentityBuilder(this)
- }
-
- // Try to get an ApiIdentity and bail if one is not ready yet. The builder will ensure
- // that onActivityResult is called with operation again, which will re-invoke us here
- identity = identityBuilder!!.tryBuild(operation)
- if (identity == null)
- return
- }
-
- when (operation) {
- REQUEST_CLONE, GitOperation.GET_SSH_KEY_FROM_CLONE -> op = CloneOperation(localDir, this).setCommand(hostname)
-
- REQUEST_PULL -> op = PullOperation(localDir, this).setCommand()
-
- REQUEST_PUSH -> op = PushOperation(localDir, this).setCommand()
-
- REQUEST_SYNC -> op = SyncOperation(localDir, this).setCommands()
-
- BREAK_OUT_OF_DETACHED -> op = BreakOutOfDetached(localDir, this).setCommands()
-
- REQUEST_RESET -> op = ResetToRemoteOperation(localDir, this).setCommands()
-
- SshApiSessionFactory.POST_SIGNATURE -> return
-
- else -> {
- Timber.tag(TAG).e("Operation not recognized : $operation")
- setResult(AppCompatActivity.RESULT_CANCELED)
- finish()
- return
- }
- }
-
- op.executeAfterAuthentication(connectionMode,
- settings.getString("git_remote_username", "git")!!,
- File("$filesDir/.ssh_key"),
- identity)
- } catch (e: Exception) {
- e.printStackTrace()
- MaterialAlertDialogBuilder(this).setMessage(e.message).show()
- }
- }
-
- public override fun onActivityResult(
- requestCode: Int,
- resultCode: Int,
- data: Intent?
- ) {
-
- // In addition to the pre-operation-launch series of intents for OpenKeychain auth
- // that will pass through here and back to launchGitOperation, there is one
- // synchronous operation that happens /after/ the operation has been launched in the
- // background thread - the actual signing of the SSH challenge. We pass through the
- // completed signature to the ApiIdentity, which will be blocked in the other thread
- // waiting for it.
- if (requestCode == SshApiSessionFactory.POST_SIGNATURE && identity != null) {
- identity!!.postSignature(data)
-
- // If the signature failed (usually because it was cancelled), reset state
- if (data == null) {
- identity = null
- identityBuilder = null
- }
- return
- }
-
- if (resultCode == AppCompatActivity.RESULT_CANCELED) {
- setResult(AppCompatActivity.RESULT_CANCELED)
- finish()
- } else if (resultCode == AppCompatActivity.RESULT_OK) {
- // If an operation has been re-queued via this mechanism, let the
- // IdentityBuilder attempt to extract some updated state from the intent before
- // trying to re-launch the operation.
- if (identityBuilder != null) {
- identityBuilder!!.consume(data)
- }
- launchGitOperation(requestCode)
- }
- super.onActivityResult(requestCode, resultCode, data)
- }
-
- companion object {
- const val REQUEST_PULL = 101
- const val REQUEST_PUSH = 102
- const val REQUEST_CLONE = 103
- const val REQUEST_INIT = 104
- const val EDIT_SERVER = 105
- const val REQUEST_SYNC = 106
-
- @Suppress("Unused")
- const val REQUEST_CREATE = 107
- const val EDIT_GIT_CONFIG = 108
- const val BREAK_OUT_OF_DETACHED = 109
- const val REQUEST_RESET = 110
- private const val TAG = "GitAct"
- private const val emailPattern = "^[^@]+@[^@]+$"
- }
-}
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitConfigActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitConfigActivity.kt
new file mode 100644
index 00000000..8a05ca40
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/git/GitConfigActivity.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+package com.zeapo.pwdstore.git
+
+import android.os.Bundle
+import android.util.Patterns
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.zeapo.pwdstore.R
+import com.zeapo.pwdstore.databinding.ActivityGitConfigBinding
+import com.zeapo.pwdstore.utils.PasswordRepository
+import org.eclipse.jgit.lib.Constants
+
+class GitConfigActivity : BaseGitActivity() {
+
+ private lateinit var binding: ActivityGitConfigBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityGitConfigBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+
+ binding.gitUserName.setText(username)
+ binding.gitUserEmail.setText(email)
+ val repo = PasswordRepository.getRepository(PasswordRepository.getRepositoryDirectory(this))
+ if (repo != null) {
+ try {
+ val objectId = repo.resolve(Constants.HEAD)
+ val ref = repo.getRef("refs/heads/master")
+ val head = if (ref.objectId.equals(objectId)) ref.name else "DETACHED"
+ binding.gitCommitHash.text = String.format("%s (%s)", objectId.abbreviate(8).name(), head)
+
+ // enable the abort button only if we're rebasing
+ val isRebasing = repo.repositoryState.isRebasing
+ binding.gitAbortRebase.isEnabled = isRebasing
+ binding.gitAbortRebase.alpha = if (isRebasing) 1.0f else 0.5f
+ } catch (ignored: Exception) {
+ }
+ }
+ binding.gitAbortRebase.setOnClickListener { launchGitOperation(BREAK_OUT_OF_DETACHED) }
+ binding.gitResetToRemote.setOnClickListener { launchGitOperation(REQUEST_RESET) }
+ binding.saveButton.setOnClickListener {
+ val email = binding.gitUserEmail.text.toString().trim()
+ val name = binding.gitUserName.text.toString().trim()
+ if (!email.matches(Patterns.EMAIL_ADDRESS.toRegex())) {
+ MaterialAlertDialogBuilder(this)
+ .setMessage(getString(R.string.invalid_email_dialog_text))
+ .setPositiveButton(getString(R.string.dialog_ok), null)
+ .show()
+ } else {
+ val editor = settings.edit()
+ editor.putString("git_config_user_email", email)
+ editor.putString("git_config_user_name", name)
+ PasswordRepository.setUserName(name)
+ PasswordRepository.setUserEmail(email)
+ editor.apply()
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitOperation.kt
index ea9ee654..78a9ca69 100644
--- a/app/src/main/java/com/zeapo/pwdstore/git/GitOperation.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/git/GitOperation.kt
@@ -7,23 +7,24 @@ package com.zeapo.pwdstore.git
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
-import android.text.InputType
import android.view.LayoutInflater
-import android.view.View
-import android.widget.CheckBox
-import android.widget.EditText
-import android.widget.LinearLayout
+import androidx.core.content.edit
import androidx.preference.PreferenceManager
+import com.google.android.material.checkbox.MaterialCheckBox
import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.textfield.TextInputEditText
import com.jcraft.jsch.JSch
import com.jcraft.jsch.JSchException
import com.jcraft.jsch.KeyPair
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.UserPreference
+import com.zeapo.pwdstore.git.config.ConnectionMode
import com.zeapo.pwdstore.git.config.GitConfigSessionFactory
import com.zeapo.pwdstore.git.config.SshApiSessionFactory
import com.zeapo.pwdstore.git.config.SshConfigSessionFactory
import com.zeapo.pwdstore.utils.PasswordRepository
+import com.zeapo.pwdstore.utils.getEncryptedPrefs
+import com.zeapo.pwdstore.utils.requestInputFocusOnView
import java.io.File
import org.eclipse.jgit.api.GitCommand
import org.eclipse.jgit.lib.Repository
@@ -96,7 +97,7 @@ abstract class GitOperation(fileDir: File, internal val callingActivity: Activit
* @param identity the api identity to use for auth in OpenKeychain connection mode
*/
fun executeAfterAuthentication(
- connectionMode: String,
+ connectionMode: ConnectionMode,
username: String,
sshKey: File?,
identity: SshApiSessionFactory.ApiIdentity?
@@ -114,15 +115,17 @@ abstract class GitOperation(fileDir: File, internal val callingActivity: Activit
* @param showError show the passphrase edit text in red
*/
private fun executeAfterAuthentication(
- connectionMode: String,
+ connectionMode: ConnectionMode,
username: String,
sshKey: File?,
identity: SshApiSessionFactory.ApiIdentity?,
showError: Boolean
) {
- if (connectionMode.equals("ssh-key", ignoreCase = true)) {
- if (sshKey == null || !sshKey.exists()) {
- MaterialAlertDialogBuilder(callingActivity)
+ val encryptedSettings = callingActivity.applicationContext.getEncryptedPrefs("git_operation")
+ when (connectionMode) {
+ ConnectionMode.SshKey -> {
+ if (sshKey == null || !sshKey.exists()) {
+ MaterialAlertDialogBuilder(callingActivity)
.setMessage(callingActivity.resources.getString(R.string.ssh_preferences_dialog_text))
.setTitle(callingActivity.resources.getString(R.string.ssh_preferences_dialog_title))
.setPositiveButton(callingActivity.resources.getString(R.string.ssh_preferences_dialog_import)) { _, _ ->
@@ -152,82 +155,100 @@ abstract class GitOperation(fileDir: File, internal val callingActivity: Activit
// Finish the blank GitActivity so user doesn't have to press back
callingActivity.finish()
}.show()
- } else {
- val layoutInflater = LayoutInflater.from(callingActivity.applicationContext)
- @SuppressLint("InflateParams") val dialogView = layoutInflater.inflate(R.layout.git_passphrase_layout, null)
- val passphrase = dialogView.findViewById<EditText>(R.id.sshkey_passphrase)
- val settings = PreferenceManager.getDefaultSharedPreferences(callingActivity.applicationContext)
- val sshKeyPassphrase = settings.getString("ssh_key_passphrase", null)
- if (showError) {
- passphrase.error = "Wrong passphrase"
- }
- val jsch = JSch()
- try {
- val keyPair = KeyPair.load(jsch, callingActivity.filesDir.toString() + "/.ssh_key")
+ } else {
+ val layoutInflater = LayoutInflater.from(callingActivity)
+ @SuppressLint("InflateParams") val dialogView = layoutInflater.inflate(R.layout.git_passphrase_layout, null)
+ val passphrase = dialogView.findViewById<TextInputEditText>(R.id.git_auth_passphrase)
+ val sshKeyPassphrase = encryptedSettings.getString("ssh_key_local_passphrase", null)
+ if (showError) {
+ passphrase.error = callingActivity.resources.getString(R.string.git_operation_wrong_passphrase)
+ }
+ val jsch = JSch()
+ try {
+ val keyPair = KeyPair.load(jsch, callingActivity.filesDir.toString() + "/.ssh_key")
- if (keyPair.isEncrypted) {
- if (sshKeyPassphrase != null && sshKeyPassphrase.isNotEmpty()) {
- if (keyPair.decrypt(sshKeyPassphrase)) {
- // Authenticate using the ssh-key and then execute the command
- setAuthentication(sshKey, username, sshKeyPassphrase).execute()
+ if (keyPair.isEncrypted) {
+ if (sshKeyPassphrase != null && sshKeyPassphrase.isNotEmpty()) {
+ if (keyPair.decrypt(sshKeyPassphrase)) {
+ // Authenticate using the ssh-key and then execute the command
+ setAuthentication(sshKey, username, sshKeyPassphrase).execute()
+ } else {
+ // call back the method
+ executeAfterAuthentication(connectionMode, username, sshKey, identity, true)
+ }
} else {
- // call back the method
- executeAfterAuthentication(connectionMode, username, sshKey, identity, true)
- }
- } else {
- MaterialAlertDialogBuilder(callingActivity)
+ val dialog = MaterialAlertDialogBuilder(callingActivity)
.setTitle(callingActivity.resources.getString(R.string.passphrase_dialog_title))
.setMessage(callingActivity.resources.getString(R.string.passphrase_dialog_text))
.setView(dialogView)
.setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ ->
if (keyPair.decrypt(passphrase.text.toString())) {
- val rememberPassphrase = (dialogView.findViewById<View>(R.id.sshkey_remember_passphrase) as CheckBox).isChecked
+ val rememberPassphrase = dialogView.findViewById<MaterialCheckBox>(R.id.git_auth_remember_passphrase).isChecked
if (rememberPassphrase) {
- settings.edit().putString("ssh_key_passphrase", passphrase.text.toString()).apply()
+ encryptedSettings.edit().putString("ssh_key_local_passphrase", passphrase.text.toString()).apply()
}
// Authenticate using the ssh-key and then execute the command
setAuthentication(sshKey, username, passphrase.text.toString()).execute()
} else {
- settings.edit().putString("ssh_key_passphrase", null).apply()
+ encryptedSettings.edit().putString("ssh_key_local_passphrase", null).apply()
// call back the method
executeAfterAuthentication(connectionMode, username, sshKey, identity, true)
}
- }.setNegativeButton(callingActivity.resources.getString(R.string.dialog_cancel)) { _, _ ->
- // Do nothing.
- }.show()
+ }
+ .setNegativeButton(callingActivity.resources.getString(R.string.dialog_cancel)) { _, _ ->
+ callingActivity.finish()
+ }
+ .setOnCancelListener { callingActivity.finish() }
+ .create()
+ dialog.requestInputFocusOnView<TextInputEditText>(R.id.git_auth_passphrase)
+ dialog.show()
+ }
+ } else {
+ setAuthentication(sshKey, username, "").execute()
}
- } else {
- setAuthentication(sshKey, username, "").execute()
- }
- } catch (e: JSchException) {
- e.printStackTrace()
- MaterialAlertDialogBuilder(callingActivity)
- .setTitle("Unable to open the ssh-key")
- .setMessage("Please check that it was imported.")
- .setPositiveButton("Ok") { _, _ -> callingActivity.finish() }
+ } catch (e: JSchException) {
+ e.printStackTrace()
+ MaterialAlertDialogBuilder(callingActivity)
+ .setTitle(callingActivity.resources.getString(R.string.git_operation_unable_to_open_ssh_key_title))
+ .setMessage(callingActivity.resources.getString(R.string.git_operation_unable_to_open_ssh_key_message))
+ .setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ ->
+ callingActivity.finish()
+ }
.show()
+ }
+ }
+ }
+ ConnectionMode.OpenKeychain -> {
+ setAuthentication(username, identity).execute()
+ }
+ ConnectionMode.Password -> {
+ @SuppressLint("InflateParams") val dialogView = callingActivity.layoutInflater.inflate(R.layout.git_passphrase_layout, null)
+ val passwordView = dialogView.findViewById<TextInputEditText>(R.id.git_auth_passphrase)
+ val password = encryptedSettings.getString("https_password", null)
+ if (password != null && password.isNotEmpty()) {
+ setAuthentication(username, password).execute()
+ } else {
+ val dialog = MaterialAlertDialogBuilder(callingActivity)
+ .setTitle(callingActivity.resources.getString(R.string.passphrase_dialog_title))
+ .setMessage(callingActivity.resources.getString(R.string.password_dialog_text))
+ .setView(dialogView)
+ .setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ ->
+ val rememberPassphrase = dialogView.findViewById<MaterialCheckBox>(R.id.git_auth_remember_passphrase).isChecked
+ if (rememberPassphrase) {
+ encryptedSettings.edit().putString("https_password", passwordView.text.toString()).apply()
+ }
+ // authenticate using the user/pwd and then execute the command
+ setAuthentication(username, passwordView.text.toString()).execute()
+ }
+ .setNegativeButton(callingActivity.resources.getString(R.string.dialog_cancel)) { _, _ ->
+ callingActivity.finish()
+ }
+ .setOnCancelListener { callingActivity.finish() }
+ .create()
+ dialog.requestInputFocusOnView<TextInputEditText>(R.id.git_auth_passphrase)
+ dialog.show()
}
}
- } else if (connectionMode.equals("OpenKeychain", ignoreCase = true)) {
- setAuthentication(username, identity).execute()
- } else {
- val password = EditText(callingActivity)
- password.hint = "Password"
- password.width = LinearLayout.LayoutParams.MATCH_PARENT
- password.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
-
- MaterialAlertDialogBuilder(callingActivity)
- .setTitle(callingActivity.resources.getString(R.string.passphrase_dialog_title))
- .setMessage(callingActivity.resources.getString(R.string.password_dialog_text))
- .setView(password)
- .setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ ->
- // authenticate using the user/pwd and then execute the command
- setAuthentication(username, password.text.toString()).execute()
- }
- .setNegativeButton(callingActivity.resources.getString(R.string.dialog_cancel)) { _, _ ->
- callingActivity.finish()
- }
- .show()
}
}
@@ -235,10 +256,17 @@ abstract class GitOperation(fileDir: File, internal val callingActivity: Activit
* Action to execute on error
*/
open fun onError(errorMessage: String) {
+ // Clear various auth related fields on failure
if (SshSessionFactory.getInstance() is SshApiSessionFactory) {
- // Clear stored key id from settings on auth failure
PreferenceManager.getDefaultSharedPreferences(callingActivity.applicationContext)
- .edit().putString("ssh_openkeystore_keyid", null).apply()
+ .edit { putString("ssh_openkeystore_keyid", null) }
+ callingActivity.applicationContext
+ .getEncryptedPrefs("git_operation")
+ .edit { remove("ssh_key_local_passphrase") }
+ } else if (SshSessionFactory.getInstance() is GitConfigSessionFactory) {
+ callingActivity.applicationContext
+ .getEncryptedPrefs("git_operation")
+ .edit { remove("https_password") }
}
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitOperationActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitOperationActivity.kt
new file mode 100644
index 00000000..bf7d5acc
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/git/GitOperationActivity.kt
@@ -0,0 +1,72 @@
+/*
+ * 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 com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.zeapo.pwdstore.R
+import com.zeapo.pwdstore.UserPreference
+import com.zeapo.pwdstore.utils.PasswordRepository
+
+open class GitOperationActivity : BaseGitActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ when (intent.extras?.getInt(REQUEST_ARG_OP)) {
+ REQUEST_PULL -> syncRepository(REQUEST_PULL)
+ REQUEST_PUSH -> syncRepository(REQUEST_PUSH)
+ REQUEST_SYNC -> syncRepository(REQUEST_SYNC)
+ }
+ }
+
+ 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 fun syncRepository(operation: Int) {
+ if (serverUser.isEmpty() || serverHostname.isEmpty() || url.isEmpty())
+ 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", url, true)
+ launchGitOperation(operation)
+ }
+ }
+}
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt
new file mode 100644
index 00000000..a10f3460
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt
@@ -0,0 +1,185 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+package com.zeapo.pwdstore.git
+
+import android.os.Bundle
+import android.os.Handler
+import androidx.core.content.edit
+import androidx.core.os.postDelayed
+import androidx.core.widget.doOnTextChanged
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.snackbar.Snackbar
+import com.zeapo.pwdstore.R
+import com.zeapo.pwdstore.databinding.ActivityGitCloneBinding
+import com.zeapo.pwdstore.git.config.ConnectionMode
+import com.zeapo.pwdstore.git.config.Protocol
+import com.zeapo.pwdstore.utils.PasswordRepository
+import java.io.IOException
+
+/**
+ * Activity that encompasses both the initial clone as well as editing the server config for future
+ * changes.
+ */
+class GitServerConfigActivity : BaseGitActivity() {
+
+ lateinit var binding: ActivityGitCloneBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityGitCloneBinding.inflate(layoutInflater)
+ val isClone = intent?.extras?.getInt(REQUEST_ARG_OP) ?: -1 == REQUEST_CLONE
+ if (isClone) {
+ binding.saveButton.text = getString(R.string.clone_button)
+ }
+ setContentView(binding.root)
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+
+ val protocolIdToCheck = when (protocol) {
+ Protocol.Ssh -> R.id.clone_protocol_ssh
+ Protocol.Https -> R.id.clone_protocol_https
+ }
+ binding.cloneProtocolGroup.check(protocolIdToCheck)
+ binding.cloneProtocolGroup.addOnButtonCheckedListener { _, checkedId, checked ->
+ if (checked) {
+ when (checkedId) {
+ R.id.clone_protocol_https -> protocol = Protocol.Https
+ R.id.clone_protocol_ssh -> protocol = Protocol.Ssh
+ }
+ updateConnectionModeToggleGroup()
+ }
+ }
+
+ val connectionModeIdToCheck = when (connectionMode) {
+ ConnectionMode.SshKey -> R.id.connection_mode_ssh_key
+ ConnectionMode.Password -> R.id.connection_mode_password
+ ConnectionMode.OpenKeychain -> R.id.connection_mode_open_keychain
+ }
+ binding.connectionModeGroup.check(connectionModeIdToCheck)
+ binding.connectionModeGroup.addOnButtonCheckedListener { _, checkedId, checked ->
+ if (checked) {
+ when (checkedId) {
+ R.id.connection_mode_ssh_key -> connectionMode = ConnectionMode.SshKey
+ R.id.connection_mode_open_keychain -> connectionMode = ConnectionMode.OpenKeychain
+ R.id.connection_mode_password -> connectionMode = ConnectionMode.Password
+ }
+ }
+ }
+ updateConnectionModeToggleGroup()
+
+ binding.serverUrl.apply {
+ setText(serverHostname)
+ doOnTextChanged { text, _, _, _ ->
+ serverHostname = text.toString().trim()
+ }
+ }
+
+ binding.serverPort.apply {
+ setText(serverPort)
+ doOnTextChanged { text, _, _, _ ->
+ serverPort = text.toString().trim()
+ }
+ }
+
+ binding.serverUser.apply {
+ setText(serverUser)
+ doOnTextChanged { text, _, _, _ ->
+ serverUser = text.toString().trim()
+ }
+ }
+
+ binding.serverPath.apply {
+ setText(serverPath)
+ doOnTextChanged { text, _, _, _ ->
+ serverPath = text.toString().trim()
+ }
+ }
+
+ binding.saveButton.setOnClickListener {
+ if (isClone && PasswordRepository.getRepository(null) == null)
+ PasswordRepository.initialize(this)
+ if (updateUrl()) {
+ settings.edit {
+ putString("git_remote_protocol", protocol.pref)
+ putString("git_remote_auth", connectionMode.pref)
+ putString("git_remote_server", serverHostname)
+ putString("git_remote_port", serverPort)
+ putString("git_remote_username", serverUser)
+ putString("git_remote_location", serverPath)
+ }
+ if (!isClone) {
+ Snackbar.make(binding.root, getString(R.string.git_server_config_save_success), Snackbar.LENGTH_SHORT).show()
+ Handler().postDelayed(500) { finish() }
+ } else
+ cloneRepository()
+ } else {
+ Snackbar.make(binding.root, getString(R.string.git_server_config_save_failure), Snackbar.LENGTH_LONG).show()
+ }
+ }
+ }
+
+ private fun updateConnectionModeToggleGroup() {
+ if (protocol == Protocol.Ssh) {
+ binding.connectionModeSshKey.isEnabled = true
+ binding.connectionModeOpenKeychain.isEnabled = true
+ } else {
+ // Reset connection mode to the only one possible via HTTPS: password.
+ // Important note: This has to happen before disabling the other toggle buttons or they
+ // won't uncheck.
+ binding.connectionModeGroup.check(R.id.connection_mode_password)
+ binding.connectionModeSshKey.isEnabled = false
+ binding.connectionModeOpenKeychain.isEnabled = false
+ }
+ }
+
+ /**
+ * Clones the repository, the directory exists, deletes it
+ */
+ private fun cloneRepository() {
+ val localDir = requireNotNull(PasswordRepository.getRepositoryDirectory(this))
+ val localDirFiles = localDir.listFiles() ?: emptyArray()
+ // Warn if non-empty folder unless it's a just-initialized store that has just a .git folder
+ if (localDir.exists() && localDirFiles.isNotEmpty() &&
+ !(localDirFiles.size == 1 && localDirFiles[0].name == ".git")) {
+ MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.dialog_delete_title)
+ .setMessage(resources.getString(R.string.dialog_delete_msg) + " " + localDir.toString())
+ .setCancelable(false)
+ .setPositiveButton(R.string.dialog_delete) { dialog, _ ->
+ try {
+ localDir.deleteRecursively()
+ launchGitOperation(REQUEST_CLONE)
+ } catch (e: IOException) {
+ // TODO Handle the exception correctly if we are unable to delete the directory...
+ e.printStackTrace()
+ MaterialAlertDialogBuilder(this).setMessage(e.message).show()
+ } finally {
+ dialog.cancel()
+ }
+ }
+ .setNegativeButton(R.string.dialog_do_not_delete) { dialog, _ ->
+ dialog.cancel()
+ }
+ .show()
+ } else {
+ try {
+ // Silently delete & replace the lone .git folder if it exists
+ if (localDir.exists() && localDirFiles.size == 1 && localDirFiles[0].name == ".git") {
+ try {
+ localDir.deleteRecursively()
+ } catch (e: IOException) {
+ e.printStackTrace()
+ MaterialAlertDialogBuilder(this).setMessage(e.message).show()
+ }
+ }
+ } catch (e: Exception) {
+ // This is what happens when JGit fails :(
+ // TODO Handle the different cases of exceptions
+ e.printStackTrace()
+ MaterialAlertDialogBuilder(this).setMessage(e.message).show()
+ }
+ launchGitOperation(REQUEST_CLONE)
+ }
+ }
+}
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/config/ConnectionMode.kt b/app/src/main/java/com/zeapo/pwdstore/git/config/ConnectionMode.kt
new file mode 100644
index 00000000..9316e89f
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/git/config/ConnectionMode.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+package com.zeapo.pwdstore.git.config
+
+enum class ConnectionMode(val pref: String) {
+ SshKey("ssh-key"),
+ Password("username/password"),
+ OpenKeychain("OpenKeychain");
+
+ companion object {
+ private val map = values().associateBy(ConnectionMode::pref)
+ fun fromString(type: String?): ConnectionMode {
+ return map[type ?: return SshKey]
+ ?: throw IllegalArgumentException("$type is not a valid ConnectionMode")
+ }
+ }
+}
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/config/Protocol.kt b/app/src/main/java/com/zeapo/pwdstore/git/config/Protocol.kt
new file mode 100644
index 00000000..1909fe85
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/git/config/Protocol.kt
@@ -0,0 +1,18 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+package com.zeapo.pwdstore.git.config
+
+enum class Protocol(val pref: String) {
+ Ssh("ssh://"),
+ Https("https://");
+
+ companion object {
+ private val map = values().associateBy(Protocol::pref)
+ fun fromString(type: String?): Protocol {
+ return map[type ?: return Ssh]
+ ?: throw IllegalArgumentException("$type is not a valid Protocol")
+ }
+ }
+}
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/config/SshApiSessionFactory.java b/app/src/main/java/com/zeapo/pwdstore/git/config/SshApiSessionFactory.java
index a75ddef8..6b82cac9 100644
--- a/app/src/main/java/com/zeapo/pwdstore/git/config/SshApiSessionFactory.java
+++ b/app/src/main/java/com/zeapo/pwdstore/git/config/SshApiSessionFactory.java
@@ -18,7 +18,7 @@ import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.UserInfo;
import com.zeapo.pwdstore.R;
-import com.zeapo.pwdstore.git.GitActivity;
+import com.zeapo.pwdstore.git.BaseGitActivity;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import org.eclipse.jgit.errors.UnsupportedCredentialItem;
@@ -107,7 +107,7 @@ public class SshApiSessionFactory extends GitConfigSessionFactory {
private SshAuthenticationApi api;
private String keyId, description, alg;
private byte[] publicKey;
- private GitActivity callingActivity;
+ private BaseGitActivity callingActivity;
private SharedPreferences settings;
/**
@@ -116,7 +116,7 @@ public class SshApiSessionFactory extends GitConfigSessionFactory {
* @param callingActivity Activity that will be used to launch pending intents and that will
* receive and handle the results.
*/
- public IdentityBuilder(GitActivity callingActivity) {
+ public IdentityBuilder(BaseGitActivity callingActivity) {
this.callingActivity = callingActivity;
List<String> providers =
diff --git a/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt b/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt
index 1426ef1e..a5ff0bc1 100644
--- a/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt
@@ -6,13 +6,13 @@ package com.zeapo.pwdstore.ui.dialogs
import android.app.Dialog
import android.os.Bundle
-import android.view.inputmethod.InputMethodManager
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputEditText
import com.zeapo.pwdstore.PasswordStore
import com.zeapo.pwdstore.R
+import com.zeapo.pwdstore.utils.requestInputFocusOnView
import java.io.File
class FolderCreationDialogFragment : DialogFragment() {
@@ -28,18 +28,7 @@ class FolderCreationDialogFragment : DialogFragment() {
dismiss()
}
val dialog = alertDialogBuilder.create()
- dialog.setOnShowListener {
- // https://stackoverflow.com/a/13056259/297261
- dialog.findViewById<TextInputEditText>(R.id.folder_name_text)!!.apply {
- setOnFocusChangeListener { v, _ ->
- v.post {
- val imm = activity!!.getSystemService(InputMethodManager::class.java)
- imm?.showSoftInput(v, InputMethodManager.SHOW_IMPLICIT)
- }
- }
- requestFocus()
- }
- }
+ dialog.requestInputFocusOnView<TextInputEditText>(R.id.folder_name_text)
return dialog
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt
index f7a637c3..6f4ebab1 100644
--- a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt
@@ -5,10 +5,18 @@
package com.zeapo.pwdstore.utils
import android.content.Context
+import android.content.SharedPreferences
import android.os.Build
import android.util.TypedValue
+import android.view.View
import android.view.autofill.AutofillManager
+import android.view.inputmethod.InputMethodManager
+import androidx.annotation.IdRes
import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AlertDialog
+import androidx.core.content.getSystemService
+import androidx.security.crypto.EncryptedSharedPreferences
+import androidx.security.crypto.MasterKeys
infix fun Int.hasFlag(flag: Int): Boolean {
return this and flag == flag
@@ -24,6 +32,37 @@ fun Context.resolveAttribute(attr: Int): Int {
return typedValue.data
}
+fun Context.getEncryptedPrefs(fileName: String): SharedPreferences {
+ val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC
+ val masterKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec)
+ return EncryptedSharedPreferences.create(
+ fileName,
+ masterKeyAlias,
+ this,
+ EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
+ EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
+ )
+}
+
+/**
+ * Extension function for [AlertDialog] that requests focus for the
+ * view whose id is [id]. Solution based on a StackOverflow
+ * answer: https://stackoverflow.com/a/13056259/297261
+ */
+fun <T : View> AlertDialog.requestInputFocusOnView(@IdRes id: Int) {
+ setOnShowListener {
+ findViewById<T>(id)?.apply {
+ setOnFocusChangeListener { v, _ ->
+ v.post {
+ context.getSystemService<InputMethodManager>()
+ ?.showSoftInput(v, InputMethodManager.SHOW_IMPLICIT)
+ }
+ }
+ requestFocus()
+ }
+ }
+}
+
val Context.autofillManager: AutofillManager?
@RequiresApi(Build.VERSION_CODES.O)
get() = getSystemService(AutofillManager::class.java)
diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.kt b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.kt
index 1cf8fea0..2cc19ae3 100644
--- a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.kt
@@ -95,7 +95,7 @@ open class PasswordRepository protected constructor() {
// TODO add multiple remotes support for pull/push
@JvmStatic
- fun addRemote(name: String, url: String, replace: Boolean?) {
+ fun addRemote(name: String, url: String, replace: Boolean = false) {
val storedConfig = repository!!.config
val remotes = storedConfig.getSubsections("remote")
@@ -116,7 +116,7 @@ open class PasswordRepository protected constructor() {
} catch (e: Exception) {
e.printStackTrace()
}
- } else if (replace!!) {
+ } else if (replace) {
try {
val uri = URIish(url)
@@ -181,16 +181,6 @@ open class PasswordRepository protected constructor() {
}
/**
- * Gets the password items in the root directory
- *
- * @return a list of passwords in the root directory
- */
- @JvmStatic
- fun getPasswords(rootDir: File, sortOrder: PasswordSortOrder): ArrayList<PasswordItem> {
- return getPasswords(rootDir, rootDir, sortOrder)
- }
-
- /**
* Gets the .gpg files in a directory
*
* @param path the directory path