aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt')
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt212
1 files changed, 212 insertions, 0 deletions
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"
+ }
+}