Refactor git logic into separate parts

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
This commit is contained in:
Harsh Shandilya 2020-04-10 19:15:51 +05:30
parent f21b6426af
commit 9215cdc2eb
No known key found for this signature in database
GPG key ID: 366D7BBAD1031E80
24 changed files with 555 additions and 637 deletions

View file

@ -32,7 +32,6 @@ android {
}
viewBinding.enabled = true
defaultConfig {
applicationId versions.packageName
}

View file

@ -41,6 +41,14 @@
<activity android:name=".git.GitActivity" />
<activity android:name=".git.GitServerConfigActivity"
android:windowSoftInputMode="adjustResize"
android:label="@string/title_activity_git_clone" />
<activity android:name=".git.GitConfigActivity"
android:windowSoftInputMode="adjustResize"
android:label="@string/title_activity_git_config" />
<activity
android:name=".UserPreference"
android:parentActivityName=".PasswordStore"

View file

@ -23,6 +23,7 @@ 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.BaseGitActivity
import com.zeapo.pwdstore.git.GitActivity
import com.zeapo.pwdstore.ui.OnOffItemAnimator
import com.zeapo.pwdstore.ui.adapters.PasswordItemRecyclerAdapter
@ -78,8 +79,8 @@ class PasswordFragment : Fragment() {
swipeRefresher.isRefreshing = false
} else {
val intent = Intent(context, GitActivity::class.java)
intent.putExtra("Operation", GitActivity.REQUEST_SYNC)
startActivityForResult(intent, GitActivity.REQUEST_SYNC)
intent.putExtra("Operation", BaseGitActivity.REQUEST_SYNC)
startActivityForResult(intent, BaseGitActivity.REQUEST_SYNC)
}
}

View file

@ -38,6 +38,7 @@ 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.BaseGitActivity
import com.zeapo.pwdstore.git.GitActivity
import com.zeapo.pwdstore.git.GitAsyncTask
import com.zeapo.pwdstore.git.GitOperation
@ -250,8 +251,8 @@ class PasswordStore : AppCompatActivity() {
return false
}
intent = Intent(this, GitActivity::class.java)
intent.putExtra("Operation", GitActivity.REQUEST_PUSH)
startActivityForResult(intent, GitActivity.REQUEST_PUSH)
intent.putExtra("Operation", BaseGitActivity.REQUEST_PUSH)
startActivityForResult(intent, BaseGitActivity.REQUEST_PUSH)
return true
}
R.id.git_pull -> {
@ -260,8 +261,8 @@ class PasswordStore : AppCompatActivity() {
return false
}
intent = Intent(this, GitActivity::class.java)
intent.putExtra("Operation", GitActivity.REQUEST_PULL)
startActivityForResult(intent, GitActivity.REQUEST_PULL)
intent.putExtra("Operation", BaseGitActivity.REQUEST_PULL)
startActivityForResult(intent, BaseGitActivity.REQUEST_PULL)
return true
}
R.id.git_sync -> {
@ -270,8 +271,8 @@ class PasswordStore : AppCompatActivity() {
return false
}
intent = Intent(this, GitActivity::class.java)
intent.putExtra("Operation", GitActivity.REQUEST_SYNC)
startActivityForResult(intent, GitActivity.REQUEST_SYNC)
intent.putExtra("Operation", BaseGitActivity.REQUEST_SYNC)
startActivityForResult(intent, BaseGitActivity.REQUEST_SYNC)
return true
}
R.id.refresh -> {
@ -352,7 +353,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()
@ -585,7 +586,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 -> {
@ -619,8 +620,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,8 +640,8 @@ class PasswordStore : AppCompatActivity() {
}
}
val intent = Intent(activity, GitActivity::class.java)
intent.putExtra("Operation", GitActivity.REQUEST_CLONE)
startActivityForResult(intent, GitActivity.REQUEST_CLONE)
intent.putExtra("Operation", BaseGitActivity.REQUEST_CLONE)
startActivityForResult(intent, BaseGitActivity.REQUEST_CLONE)
}
REQUEST_CODE_SELECT_FOLDER -> {
Timber.tag(TAG)
@ -723,8 +724,8 @@ class PasswordStore : AppCompatActivity() {
CLONE_REPO_BUTTON -> {
initialize(this@PasswordStore)
val intent = Intent(activity, GitActivity::class.java)
intent.putExtra("Operation", GitActivity.REQUEST_CLONE)
startActivityForResult(intent, GitActivity.REQUEST_CLONE)
intent.putExtra("Operation", BaseGitActivity.REQUEST_CLONE)
startActivityForResult(intent, BaseGitActivity.REQUEST_CLONE)
}
}
}
@ -745,8 +746,8 @@ class PasswordStore : AppCompatActivity() {
CLONE_REPO_BUTTON -> {
initialize(this@PasswordStore)
val intent = Intent(activity, GitActivity::class.java)
intent.putExtra("Operation", GitActivity.REQUEST_CLONE)
startActivityForResult(intent, GitActivity.REQUEST_CLONE)
intent.putExtra("Operation", BaseGitActivity.REQUEST_CLONE)
startActivityForResult(intent, BaseGitActivity.REQUEST_CLONE)
}
}
}

View file

@ -35,7 +35,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
@ -188,16 +189,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
}

View file

@ -0,0 +1,169 @@
/*
* 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.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 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 hostname: String
lateinit var serverUrl: 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
@CallSuper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
settings = PreferenceManager.getDefaultSharedPreferences(this)
protocol = Protocol.fromString(settings.getString("git_remote_protocol", null))
connectionMode = ConnectionMode.fromString(settings.getString("git_remote_auth", null))
serverUrl = 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) ?: ""
updateHostname()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
finish()
true
}
else -> super.onOptionsItemSelected(item)
}
}
fun updateHostname(): Boolean {
var valid = false
hostname = when (protocol) {
Protocol.Ssh -> {
val hostname = StringBuilder()
hostname.append("$serverUser@${serverUrl.trim { it <= ' '}}:")
if (serverPort == "22") {
hostname.append(serverPath)
} else {
valid = !(!serverPath.matches("/.*".toRegex()) && serverPort.isNotEmpty())
hostname.append(serverPort + serverPath)
}
hostname.toString()
}
Protocol.Https -> {
val hostname = StringBuilder()
hostname.append(serverUrl.trim { it <= ' ' })
valid = if (serverPort == "443") {
hostname.append(serverPath)
false
} else {
hostname.append("/")
.append(serverPort)
.append(serverPath)
true
}
hostname.toString()
}
}
if (!valid)
PasswordRepository.addRemote("origin", hostname, true)
return valid
}
/**
* 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(hostname)
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,
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?) {
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 REQUEST_SYNC = 105
const val REQUEST_CREATE = 106
const val BREAK_OUT_OF_DETACHED = 107
const val REQUEST_RESET = 108
const val TAG = "AbstractGitActivity"
}
}

View file

@ -4,201 +4,26 @@
*/
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
open class GitActivity : BaseGitActivity() {
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)
@ -207,99 +32,8 @@ open class GitActivity : AppCompatActivity() {
}
}
/**
* 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() {
@ -318,163 +52,29 @@ open class GitActivity : AppCompatActivity() {
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
return when (item.itemId) {
R.id.user_pref -> try {
val intent = Intent(this, UserPreference::class.java)
startActivity(intent)
return true
true
} catch (e: Exception) {
println("Exception caught :(")
e.printStackTrace()
false
}
android.R.id.home -> {
finish()
return true
}
else -> super.onOptionsItemSelected(item)
}
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) {
@Suppress("Unused")
fun cloneRepository() {
if (PasswordRepository.getRepository(null) == null) {
PasswordRepository.initialize(this)
}
val localDir = requireNotNull(PasswordRepository.getRepositoryDirectory(context))
if (!saveConfiguration())
return
val localDir = requireNotNull(PasswordRepository.getRepositoryDirectory(this))
// Warn if non-empty folder unless it's a just-initialized store that has just a .git folder
if (localDir.exists() && localDir.listFiles()!!.isNotEmpty() &&
@ -512,7 +112,7 @@ open class GitActivity : AppCompatActivity() {
}
} catch (e: Exception) {
// This is what happens when jgit fails :(
// TODO Handle the diffent cases of exceptions
// TODO Handle the different cases of exceptions
e.printStackTrace()
MaterialAlertDialogBuilder(this).setMessage(e.message).show()
}
@ -527,97 +127,27 @@ open class GitActivity : AppCompatActivity() {
* @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())
if (serverUser.isEmpty() || serverUrl.isEmpty() || hostname.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)
.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(context.getString(R.string.dialog_negative)) { _, _ ->
.setNegativeButton(getString(R.string.dialog_negative)) { _, _ ->
// do nothing :(
setResult(AppCompatActivity.RESULT_OK)
setResult(RESULT_OK)
finish()
}
.show()
else {
// check that the remote origin is here, else add it
PasswordRepository.addRemote("origin", hostname, false)
PasswordRepository.addRemote("origin", hostname, true)
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?
) {
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
@ -636,10 +166,10 @@ open class GitActivity : AppCompatActivity() {
return
}
if (resultCode == AppCompatActivity.RESULT_CANCELED) {
setResult(AppCompatActivity.RESULT_CANCELED)
if (resultCode == RESULT_CANCELED) {
setResult(RESULT_CANCELED)
finish()
} else if (resultCode == AppCompatActivity.RESULT_OK) {
} 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.
@ -650,21 +180,4 @@ open class GitActivity : AppCompatActivity() {
}
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 = "^[^@]+@[^@]+$"
}
}

View file

@ -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()
val name = binding.gitUserName.text.toString()
if (!email.matches(Patterns.EMAIL_ADDRESS.toRegex())) {
MaterialAlertDialogBuilder(this)
.setMessage(getString(R.string.invalid_email_dialog_text))
.setPositiveButton(getString(R.string.dialog_oops), 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()
}
}
}
}

View file

@ -20,6 +20,7 @@ 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
@ -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,13 +115,13 @@ 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 (connectionMode == ConnectionMode.Ssh) {
if (sshKey == null || !sshKey.exists()) {
MaterialAlertDialogBuilder(callingActivity)
.setMessage(callingActivity.resources.getString(R.string.ssh_preferences_dialog_text))
@ -208,7 +209,7 @@ abstract class GitOperation(fileDir: File, internal val callingActivity: Activit
.show()
}
}
} else if (connectionMode.equals("OpenKeychain", ignoreCase = true)) {
} else if (connectionMode == ConnectionMode.OpenKeychain) {
setAuthentication(username, identity).execute()
} else {
val password = EditText(callingActivity)

View file

@ -0,0 +1,104 @@
/*
* 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 androidx.core.content.edit
import androidx.core.widget.doOnTextChanged
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
/**
* Activity that encompasses both the initial clone as well as editing the server config for future
* changes.
*/
class GitServerConfigActivity : BaseGitActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityGitCloneBinding.inflate(layoutInflater)
setContentView(binding.root)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
when (protocol) {
Protocol.Ssh -> binding.cloneProtocolSsh.isChecked = true
Protocol.Https -> binding.cloneProtocolHttps.isChecked = true
}
when (connectionMode) {
ConnectionMode.Username -> binding.connectionModeUsername.isChecked = true
ConnectionMode.OpenKeychain -> binding.connectionModeOpenkeychain.isChecked = true
ConnectionMode.Ssh -> binding.connectionModeSsh.isChecked = true
}
binding.cloneProtocolGroup.addOnButtonCheckedListener { _, checkedId, checked ->
if (checked) {
protocol = when (checkedId) {
R.id.clone_protocol_https -> Protocol.Https
R.id.clone_protocol_ssh -> Protocol.Ssh
else -> protocol
}
}
}
binding.connectionModeGroup.addOnButtonCheckedListener { _, checkedId, checked ->
if (checked) {
connectionMode = when (checkedId) {
R.id.connection_mode_ssh -> ConnectionMode.Ssh
R.id.connection_mode_openkeychain -> ConnectionMode.OpenKeychain
R.id.connection_mode_username -> ConnectionMode.Username
else -> connectionMode
}
}
}
binding.serverUrl.apply {
setText(serverUrl)
doOnTextChanged { text, _, _, _ ->
serverUrl = text.toString()
}
}
binding.serverPort.apply {
setText(serverPort)
doOnTextChanged { text, _, _, _ ->
serverPort = text.toString()
}
}
binding.serverUser.apply {
setText(serverUser)
doOnTextChanged { text, _, _, _ ->
serverUser = text.toString()
}
}
binding.serverPath.apply {
setText(serverPath)
doOnTextChanged { text, _, _, _ ->
serverPath = text.toString()
}
}
binding.saveButton.setOnClickListener {
if (updateHostname()) {
settings.edit(true) {
putString("git_remote_protocol", protocol.toString())
putString("git_remote_auth", connectionMode.toString())
putString("git_remote_server", serverUrl)
putString("git_remote_port", serverPort)
putString("git_remote_username", serverUser)
putString("git_remote_location", serverPath)
}
Snackbar.make(binding.root, "Successfully saved configuration", Snackbar.LENGTH_SHORT).show()
} else {
Snackbar.make(binding.root, "Configuration error: please verify your settings and try again", Snackbar.LENGTH_LONG).show()
}
}
}
}

View file

@ -0,0 +1,26 @@
/*
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
package com.zeapo.pwdstore.git.config
sealed class ConnectionMode {
object Ssh : ConnectionMode() {
override fun toString() = "ssh-key"
}
object Username : ConnectionMode() {
override fun toString() = "username/password"
}
object OpenKeychain : ConnectionMode() {
override fun toString() = "OpenKeychain"
}
companion object {
fun fromString(type: String?): ConnectionMode = when (type) {
"ssh-key", null -> Ssh
"username/password" -> Username
"OpenKeychain" -> OpenKeychain
else -> throw IllegalArgumentException("$type is not a valid ConnectionMode")
}
}
}

View file

@ -0,0 +1,22 @@
/*
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
package com.zeapo.pwdstore.git.config
sealed class Protocol {
object Ssh : Protocol() {
override fun toString() = "ssh://"
}
object Https : Protocol() {
override fun toString() = "https://"
}
companion object {
fun fromString(type: String?): Protocol = when (type) {
"ssh://", null -> Ssh
"https://" -> Https
else -> throw IllegalArgumentException("$type is not a valid Protocol")
}
}
}

View file

@ -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 =

View file

@ -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)

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="false"
android:color="#00FFFFFF" />
<item android:color="@color/button_color" />
</selector>

View file

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="?android:attr/textColor"/>
</shape>
</item>
<item android:bottom="2dp">
<shape android:shape="rectangle">
<solid android:color="?android:attr/windowBackground" />
</shape>
</item>
</layer-list>

View file

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle"
android:dither="true">
<corners android:radius="2dp"/>
<solid android:color="#ccc" />
</shape>
</item>
<item>
<shape android:shape="rectangle" android:dither="true">
<corners android:radius="2dp" />
<solid android:color="#FF0000" />
<padding android:bottom="8dp"
android:left="8dp"
android:right="8dp"
android:top="8dp" />
</shape>
</item>
</layer-list>

View file

@ -19,13 +19,24 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/spacer"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:background="?android:attr/textColorPrimary"
app:layout_constraintTop_toBottomOf="@id/server_label"
app:layout_constraintBottom_toTopOf="@id/label_server_protocol" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/TextAppearance.MaterialComponents.Headline6"
android:id="@+id/label_server_protocol"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_protocol"
android:layout_margin="8dp"
app:layout_constraintTop_toBottomOf="@id/server_label"
app:layout_constraintTop_toBottomOf="@id/spacer"
app:layout_constraintStart_toStartOf="parent" />
<Spinner
@ -33,16 +44,51 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/server_label"
app:layout_constraintStart_toEndOf="@id/label_server_protocol" />
<com.google.android.material.button.MaterialButtonToggleGroup
style="@style/TextAppearance.MaterialComponents.Headline1"
android:id="@+id/clone_protocol_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:layout_constraintTop_toBottomOf="@id/label_server_protocol"
app:layout_constraintStart_toStartOf="parent"
app:selectionRequired="true"
app:singleSelection="true">
<com.google.android.material.button.MaterialButton
style="?attr/materialButtonOutlinedStyle"
android:id="@+id/clone_protocol_ssh"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/clone_protocol_ssh"
android:textColor="?android:attr/textColorPrimary"
app:rippleColor="@color/ripple_color"
app:strokeColor="?attr/colorSecondary"
app:backgroundTint="@color/toggle_button_selector" />
<com.google.android.material.button.MaterialButton
style="?attr/materialButtonOutlinedStyle"
android:id="@+id/clone_protocol_https"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/clone_protocol_https"
android:textColor="?android:attr/textColorPrimary"
app:rippleColor="@color/ripple_color"
app:strokeColor="?attr/colorSecondary"
app:backgroundTint="@color/toggle_button_selector" />
</com.google.android.material.button.MaterialButtonToggleGroup>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/server_user_layout"
android:hint="@string/server_user"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:layout_constraintTop_toBottomOf="@id/label_server_protocol">
app:layout_constraintTop_toBottomOf="@id/clone_protocol_group">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/server_user"
android:layout_width="match_parent"
@ -104,64 +150,62 @@
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/label_clone_uri"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/repository_uri"
android:editable="false"
android:layout_margin="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_server_path">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/clone_uri"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textWebEmailAddress"/>
</com.google.android.material.textfield.TextInputLayout>
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/red_rectangle"
android:textColor="@android:color/white"
android:visibility="gone"
android:id="@+id/warn_url"
android:layout_margin="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_clone_uri"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/TextAppearance.MaterialComponents.Headline6"
android:id="@+id/label_connection_mode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/connection_mode"
android:layout_margin="16dp"
android:layout_margin="8dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/warn_url" />
app:layout_constraintTop_toBottomOf="@id/label_server_path" />
<Spinner
android:id="@+id/connection_mode"
<com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/connection_mode_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
app:layout_constraintTop_toBottomOf="@id/warn_url"
app:layout_constraintStart_toEndOf="@id/label_connection_mode" />
<com.google.android.material.button.MaterialButton
style="@style/Widget.MaterialComponents.Button"
android:id="@+id/clone_button"
android:text="@string/clone_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="cloneRepository"
android:textColor="?android:attr/windowBackground"
android:layout_marginTop="8dp"
app:backgroundTint="?attr/colorSecondary"
android:layout_margin="8dp"
app:layout_constraintTop_toBottomOf="@id/label_connection_mode"
app:layout_constraintStart_toStartOf="parent" />
app:layout_constraintStart_toStartOf="parent"
app:selectionRequired="true"
app:singleSelection="true" >
<com.google.android.material.button.MaterialButton
style="?attr/materialButtonOutlinedStyle"
android:id="@+id/connection_mode_ssh"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/connection_mode_ssh_key"
android:textColor="?android:attr/textColorPrimary"
app:rippleColor="@color/ripple_color"
app:strokeColor="?attr/colorSecondary"
app:backgroundTint="@color/toggle_button_selector" />
<com.google.android.material.button.MaterialButton
style="?attr/materialButtonOutlinedStyle"
android:id="@+id/connection_mode_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/connection_mode_basic_authentication"
android:textColor="?android:attr/textColorPrimary"
app:rippleColor="@color/ripple_color"
app:strokeColor="?attr/colorSecondary"
app:backgroundTint="@color/toggle_button_selector" />
<com.google.android.material.button.MaterialButton
style="?attr/materialButtonOutlinedStyle"
android:id="@+id/connection_mode_openkeychain"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/connection_mode_openkeychain"
android:textColor="?android:attr/textColorPrimary"
app:rippleColor="@color/ripple_color"
app:strokeColor="?attr/colorSecondary"
app:backgroundTint="@color/toggle_button_selector" />
</com.google.android.material.button.MaterialButtonToggleGroup>
<com.google.android.material.button.MaterialButton
style="@style/Widget.MaterialComponents.Button"
@ -169,10 +213,9 @@
android:text="@string/crypto_save"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="saveConfiguration"
android:textColor="?android:attr/windowBackground"
android:layout_marginTop="8dp"
app:backgroundTint="?attr/colorSecondary"
app:layout_constraintTop_toBottomOf="@id/label_connection_mode"
app:layout_constraintTop_toBottomOf="@id/connection_mode_group"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -51,7 +51,6 @@
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="@string/crypto_save"
android:onClick="applyGitConfigs"
android:textColor="?android:attr/windowBackground"
app:backgroundTint="?attr/colorSecondary"
app:layout_constraintTop_toBottomOf="@id/email_input_layout"
@ -95,7 +94,6 @@
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="@string/abort_rebase"
android:onClick="abortRebase"
android:textColor="?android:attr/windowBackground"
app:backgroundTint="?attr/colorSecondary"
app:layout_constraintTop_toBottomOf="@id/commit_hash_label" />
@ -107,7 +105,6 @@
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="@string/reset_to_remote"
android:onClick="resetToRemote"
android:textColor="?android:attr/windowBackground"
app:backgroundTint="?attr/colorSecondary"
app:layout_constraintTop_toBottomOf="@id/git_abort_rebase" />

View file

@ -19,7 +19,6 @@
android:textSize="16sp" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"

View file

@ -25,7 +25,6 @@
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<LinearLayout
android:id="@+id/create_options"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|center_horizontal"

View file

@ -16,4 +16,6 @@
<color name="navigation_bar_color">@color/primary_color</color>
<color name="list_multiselect_background">#66EEEEEE</color>
<color name="status_bar_color">@color/window_background</color>
<color name="ripple_color">#aaff7539</color>
<color name="button_color">#44ff7539</color>
</resources>

View file

@ -19,6 +19,8 @@
<color name="list_multiselect_background">#668eacbb</color>
<color name="navigation_bar_color">#000000</color>
<color name="status_bar_color">@color/primary_dark_color</color>
<color name="ripple_color">#aaff7043</color>
<color name="button_color">#44ff7043</color>
<!-- Override TextInputEditText stroke color like a boss -->
<color name="mtrl_textinput_default_box_stroke_color" tools:override="true">

View file

@ -343,4 +343,9 @@
<string name="theme_dark">Dark</string>
<string name="theme_battery_saver">Set by Battery Saver</string>
<string name="theme_follow_system">System default</string>
<string name="clone_protocol_ssh" translatable="false">SSH</string>
<string name="clone_protocol_https" translatable="false">HTTPS</string>
<string name="connection_mode_ssh_key" translatable="false">SSH key</string>
<string name="connection_mode_basic_authentication" translatable="false">Password</string>
<string name="connection_mode_openkeychain" translatable="false">OpenKeychain</string>
</resources>