Refactor Git related activities (#685)

* Refactor git logic into separate parts

* Extract hardcoded strings

* Add KDoc to updateHostname, remove unused field

* Cleanups

* Fix dialog message

* Wire in repository clone flow

* spotless

* Remove unused method

* Cleanup GitActivity

- Rename to GitOperationActivity.
- Ensure identityBuilder is always closed regardless of what fragment uses it.
- Remove hardcoded "Operation" strings and replace with REQUEST_ARG_OP.
- Apply a transparent theme to GitOperationActivity make the UI less jarring.

* Tweak some stupidly worded dialog messages

As pointed out in #629, these strings are shoddily worded and do not express any clear intent to the
user, leaving them confused and angry.

* GitOperationActivity: wrap Context to ensure right theme is used

* spotless

* undo build.gradle change

* Use correct parent theme, remove now useless wrapping

* GitServerConfigActivity: fix repository clone flow

* temp: disable leakcanary

framework leaks on Samsung are pissing me off

* Make system bars transparent in git activity

* Tweak HTTPS password layout

* Unhardcode wrong passphrase string

* Store SSH passphrase in EncryptedSharedPreferences

Also revamp the dialog to look a bit better

* Implement support for remembering HTTPS password

Fixes #521

* Try to patch HTTPS remote creation logic

* Update security-crypto

* Clear saved passphrase/password on auth failure

* Revert "Update security-crypto"

Broken on R DP2.1

This reverts commit 4b20371dd4.

* Revert "temp: disable leakcanary"

This reverts commit 2db7d41bd6.

* Update CHANGELOG

* Remove spacer

* Remove useless override

* Wrap git server activity in a ScrollView

* GitOperation: always finish calling activity when dialogs are dismissed

* Wipe saved password/passphrase when hostname changes

* Don't commit prefs updates

* Don't call listFiles excessively

* Finish activity after saving configuration

* Make ConnectionMode and Protocol enum classes

* Change SSH key passphrase key, don't wipe on host change

* Reimplement BaseGitActivity.updateUrl (was updateHostname)

* Use SharedPreferences.edit KTX extension

* Disable inapplicable connection modes depending on scheme

* BaseGitActivity: annotate onDestroy with CallSuper

We'll leak the identityBuilder connection otherwise

* Move input hack for AlertDialog into an extension function

We re-use this in many places

* Fix protocol/mode toggle issue and consistenly name options

* Fix a crash when opening GitServerConfigActivity without a repo

* Fix OpenKeychain callbacks by moving onActivityResult to BaseGitActivity

* Run spotlessApply

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
Co-authored-by: Fabian Henneke <fabian@henneke.me>
This commit is contained in:
Harsh Shandilya
2020-04-17 18:36:07 +05:30
committed by GitHub
parent 4ffd7ed9bf
commit b94b52a42d
41 changed files with 1018 additions and 1063 deletions

View File

@@ -5,10 +5,13 @@ All notable changes to this project will be documented in this file.
### Added
- Oreo Autofill support
- Securely remember HTTPS password/SSH key passphrase
### Fixed
- Text input box theming
- Password repository held in non-hidden storage no longer fails
- Remove ambiguous and confusing URL field in server config menu
and heavily improve UI for ease of use.
## [1.6.0] - 2020-03-20

View File

@@ -92,9 +92,10 @@ dependencies {
implementation deps.androidx.local_broadcast_manager
implementation deps.androidx.material
implementation deps.androidx.preference
implementation deps.androidx.swiperefreshlayout
implementation deps.androidx.recycler_view
implementation deps.androidx.recycler_view_selection
implementation deps.androidx.security
implementation deps.androidx.swiperefreshlayout
implementation deps.kotlin.coroutines.android
implementation deps.kotlin.coroutines.core

View File

@@ -40,7 +40,16 @@
</intent-filter>
</activity>
<activity android:name=".git.GitActivity" />
<activity android:name=".git.GitOperationActivity"
android:theme="@style/NoBackgroundTheme" />
<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"

View File

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

View File

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

View File

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

View File

@@ -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"
}
}

View File

@@ -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 = "^[^@]+@[^@]+$"
}
}

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().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()
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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)
@@ -180,16 +180,6 @@ open class PasswordRepository protected constructor() {
return getRepository(File(dir.absolutePath + "/.git"))
}
/**
* 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
*

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

@@ -1,178 +1,216 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:padding="@dimen/activity_horizontal_margin"
tools:context="com.zeapo.pwdstore.git.GitActivity"
tools:context="com.zeapo.pwdstore.git.GitOperationActivity"
android:background="?android:attr/windowBackground">
<androidx.appcompat.widget.AppCompatTextView
style="@style/TextAppearance.MaterialComponents.Headline5"
android:id="@+id/server_label"
android:textStyle="bold"
android:textSize="24sp"
android:text="@string/server_name"
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatTextView
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_constraintStart_toStartOf="parent" />
<Spinner
android:id="@+id/clone_protocol"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:layout_constraintTop_toBottomOf="@id/server_label"
app:layout_constraintStart_toEndOf="@id/label_server_protocol" />
<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">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/server_user"
<androidx.appcompat.widget.AppCompatTextView
style="@style/TextAppearance.MaterialComponents.Headline5"
android:id="@+id/server_label"
android:textStyle="bold"
android:textSize="24sp"
android:text="@string/server_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textWebEmailAddress" />
</com.google.android.material.textfield.TextInputLayout>
android:layout_margin="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/label_server_url"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:hint="@string/server_url"
app:layout_constraintTop_toBottomOf="@id/server_user_layout"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/label_server_port">
<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_constraintStart_toStartOf="parent" />
<com.google.android.material.textfield.TextInputEditText
<Spinner
android:id="@+id/clone_protocol"
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:id="@+id/server_url"
android:inputType="textWebEmailAddress" />
android:layout_margin="8dp"
app:layout_constraintTop_toBottomOf="@id/clone_protocol_group">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/server_user"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textWebEmailAddress" />
</com.google.android.material.textfield.TextInputLayout>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/label_server_url"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:hint="@string/server_url"
app:layout_constraintTop_toBottomOf="@id/server_user_layout"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/label_server_port">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/label_server_port"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:hint="@string/server_port_hint"
app:layout_constraintStart_toEndOf="@id/label_server_url"
app:layout_constraintTop_toBottomOf="@id/server_user_layout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintDimensionRatio="1:0.8">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/server_url"
android:inputType="textWebEmailAddress" />
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/server_port"
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/label_server_port"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:hint="@string/server_port_hint"
app:layout_constraintStart_toEndOf="@id/label_server_url"
app:layout_constraintTop_toBottomOf="@id/server_user_layout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintDimensionRatio="1:0.8">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/server_port"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/label_server_path"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number" />
android:layout_margin="8dp"
android:hint="@string/server_path"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_server_url">
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/server_path"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textWebEmailAddress"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/label_server_path"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:hint="@string/server_path"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_server_url">
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/server_path"
<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="8dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_server_path" />
<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="8dp"
app:layout_constraintTop_toBottomOf="@id/label_connection_mode"
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_key"
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_password"
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_open_keychain"
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"
android:id="@+id/save_button"
android:text="@string/crypto_save"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textWebEmailAddress"/>
</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
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"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/warn_url" />
<Spinner
android:id="@+id/connection_mode"
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"
app:layout_constraintTop_toBottomOf="@id/label_connection_mode"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.button.MaterialButton
style="@style/Widget.MaterialComponents.Button"
android:id="@+id/save_button"
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_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
android:textColor="?android:attr/windowBackground"
android:layout_marginTop="8dp"
app:backgroundTint="?attr/colorSecondary"
app:layout_constraintTop_toBottomOf="@id/connection_mode_group"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@@ -5,7 +5,7 @@
android:layout_height="match_parent"
android:padding="@dimen/activity_horizontal_margin"
android:background="?android:attr/windowBackground"
tools:context="com.zeapo.pwdstore.git.GitActivity"
tools:context="com.zeapo.pwdstore.git.GitConfigActivity"
tools:layout_editor_absoluteX="0dp"
tools:layout_editor_absoluteY="81dp">
@@ -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

@@ -2,31 +2,31 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:padding="16dp">
<EditText
android:id="@+id/sshkey_passphrase"
android:layout_width="0dp"
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/git_auth_passphrase_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:ems="10"
android:inputType="textPassword"
android:importantForAccessibility="no"
app:hintEnabled="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/git_auth_passphrase"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/ssh_keygen_passphrase"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<CheckBox
android:id="@+id/sshkey_remember_passphrase"
android:layout_width="0dp"
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/git_auth_remember_passphrase"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:text="@string/remember_the_passphrase"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/sshkey_passphrase" />
app:layout_constraintTop_toBottomOf="@+id/git_auth_passphrase_layout" />
</androidx.constraintlayout.widget.ConstraintLayout>

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

@@ -1,7 +1,7 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:pwstore="http://schemas.android.com/apk/res-auto"
tools:context="com.zeapo.pwdstore.git.GitActivity" >
tools:context="com.zeapo.pwdstore.git.GitServerConfigActivity" >
<item android:id="@+id/user_pref"
android:title="@string/action_settings"
android:orderInCategory="100"

View File

@@ -112,7 +112,6 @@
<string name="dialog_ok">حسناً</string>
<string name="dialog_yes">نعم</string>
<string name="dialog_no">لا</string>
<string name="dialog_negative">لا … لاحقاً</string>
<string name="dialog_cancel">إلغاء</string>
<string name="git_sync">زامن المستودع</string>
<string name="show_password_pref_title">إظهار كلمة السر</string>

View File

@@ -9,7 +9,6 @@
<string name="title_activity_git_clone">Informace repozitáře</string>
<!-- Password Store -->
<string name="creation_dialog_text">Naklonujte nebo vytvořte nový repozitář před pokusem přidat heslo nebo spustit synchronizaci.</string>
<string name="key_dialog_text">Před inicializací repozitáře je třeba vybrat "ID PGP klíče"</string>
<string name="delete_dialog_text">Opravdu chcete smazat heslo %1$s?</string>
<string name="move">Přesunout</string>
<string name="edit">Editovat</string>
@@ -43,7 +42,6 @@
<!-- Git Handler -->
<string name="forget_username_dialog_text">Zapomněli jste uvést přihlašovací jméno?</string>
<string name="set_information_dialog_text">Je třeba zadat informaci o serveru před vlastní synchronizací</string>
<string name="ssh_preferences_dialog_text">Importujte nebo si prosím vygenerujte svůj SSH klíč v nastavení aplikace</string>
<string name="ssh_preferences_dialog_title">Žádný SSH klíč</string>
<string name="ssh_preferences_dialog_import">Import</string>
@@ -163,8 +161,6 @@
<string name="dialog_ok">OK</string>
<string name="dialog_yes">Ano</string>
<string name="dialog_no">Ne</string>
<string name="dialog_positive">Je na cestě…</string>
<string name="dialog_negative">Ne… později</string>
<string name="dialog_oops">Ajaj…</string>
<string name="dialog_cancel">Zrušit</string>
<string name="git_sync">Synchronizovat repozitář</string>

View File

@@ -9,7 +9,6 @@
<string name="title_activity_git_clone">Repository Informationen</string>
<!-- Password Store -->
<string name="creation_dialog_text">Bitte klone oder erstelle ein neues Repository, bevor du versuchst ein Passwort hinzuzufügen oder jegliche Synchronisation-Operation durchführst.</string>
<string name="key_dialog_text">Du musst deine PGP-Key ID auwählen, bevor das Repository intialisiert wird.</string>
<string name="delete_dialog_text">Bist du dir sicher, dass du das Passwort löschen möchtest %1$s?</string>
<string name="move">Verschieben</string>
<string name="edit">Bearbeiten</string>
@@ -28,7 +27,6 @@
<!-- Git Handler -->
<string name="forget_username_dialog_text">Hast du vergessen einen Nutzernamen zu vergeben?</string>
<string name="set_information_dialog_text">You have to set the information about the server before synchronizing with the server</string>
<string name="ssh_preferences_dialog_text">Please import or generate your SSH key file in the preferences</string>
<string name="ssh_preferences_dialog_title">Kein SSH-Key angegeben</string>
<string name="ssh_preferences_dialog_import">Import</string>
@@ -141,8 +139,6 @@
<string name="dialog_ok">OK</string>
<string name="dialog_yes">Ja</string>
<string name="dialog_no">Nein</string>
<string name="dialog_positive">Auf dem Weg…</string>
<string name="dialog_negative">Nah… später</string>
<string name="dialog_oops">Oops…</string>
<string name="dialog_cancel">Abbruch</string>
<string name="git_sync">Synchronisiere Repository</string>

View File

@@ -9,7 +9,6 @@
<string name="title_activity_git_clone">Información de repositorio</string>
<!-- Password Store -->
<string name="creation_dialog_text">Por favor clona o crea un nuevo repositorio antes de añadir una contraseña o ejecutar una operación de sincronización.</string>
<string name="key_dialog_text">Tienes que seleccionar una llave PGP antes de inicializar el repositorio</string>
<string name="delete_dialog_text">Confirma que deseas eliminar la contraseña %1$s</string>
<string name="move">Mover</string>
<string name="edit">Editar</string>
@@ -39,7 +38,6 @@
<!-- Git Handler -->
<string name="forget_username_dialog_text">Olvidaste especificar un nombre de usuario?</string>
<string name="set_information_dialog_text">Necesitas configurar la información del servidor antes de sincronizar</string>
<string name="ssh_preferences_dialog_text">Por favor importa o genera tu llave SSH en los ajustes</string>
<string name="ssh_preferences_dialog_title">No hay llave SSH</string>
<string name="ssh_preferences_dialog_import">Importar</string>
@@ -177,8 +175,6 @@
<string name="dialog_ok">OK</string>
<string name="dialog_yes"></string>
<string name="dialog_no">No</string>
<string name="dialog_positive">Ok, Vamos…</string>
<string name="dialog_negative">Nah… después</string>
<string name="dialog_oops">Ups…</string>
<string name="dialog_cancel">Cancelar</string>
<string name="git_sync">Sincronizar con servidor</string>

View File

@@ -9,7 +9,6 @@
<string name="title_activity_git_clone">Information sur le dépôt Git</string>
<!-- Password Store -->
<string name="creation_dialog_text">Clonez ou créez un dépôt suivant avant d\'essayer d\'ajouter un mot de pass ou d\'effectuer une opération de synchornisation.</string>
<string name="key_dialog_text">Vous devez sélectionner votre "PGP-Key ID" avant d\'initialiser le dépôt</string>
<string name="delete_dialog_text">Êtes-vous sûr de vouloir supprimer le mot de passe %1$s?</string>
<string name="move">Déplacer</string>
<string name="edit">Éditer</string>
@@ -45,7 +44,6 @@
<!-- Git Handler -->
<string name="forget_username_dialog_text">Avez-vous oublié to renseigner votre nom d\'utilisateur ?</string>
<string name="set_information_dialog_text">Vous devez renseignez les informations à propos du serveur avant d\'effectuer une synchronisation avec celui-ci</string>
<string name="ssh_preferences_dialog_text">Vous devez importer ou générer votre fichier de clef SSH dans les préférences</string>
<string name="ssh_preferences_dialog_title">Absence de cled SSH</string>
<string name="ssh_preferences_dialog_import">Importer</string>
@@ -178,8 +176,6 @@
<string name="dialog_ok">OK</string>
<string name="dialog_yes">Oui</string>
<string name="dialog_no">Non</string>
<string name="dialog_positive">En chemin…</string>
<string name="dialog_negative">Non… plus tard</string>
<string name="dialog_oops">Oups…</string>
<string name="dialog_cancel">Annuler</string>
<string name="git_sync">Synchronisation du dépôt</string>

View File

@@ -9,7 +9,6 @@
<string name="title_activity_git_clone">リポジトリ情報</string>
<!-- Password Store -->
<string name="creation_dialog_text">パスワードや同期操作を追加する前に、以下の新しいリポジトリをクローンまたは作成してください。</string>
<string name="key_dialog_text">リポジトリを初期化する前に "PGP 鍵 ID"を選択する必要があります</string>
<string name="delete_dialog_text">パスワードを削除してもよろしいですか %1$s</string>
<string name="delete">削除</string>
<!-- git commits -->
@@ -28,7 +27,6 @@
<!-- Git Handler -->
<string name="forget_username_dialog_text">ユーザー名の指定を忘れましたか?</string>
<string name="set_information_dialog_text">サーバーと同期する前に、サーバーに関する情報を設定する必要があります</string>
<string name="ssh_preferences_dialog_text">プリファレンスで SSH 鍵ファイルをインポートまたは生成してください</string>
<string name="ssh_preferences_dialog_title">SSH 鍵がありませんkey</string>
<string name="ssh_preferences_dialog_import">インポート</string>
@@ -120,8 +118,6 @@
<string name="dialog_ok">OK</string>
<string name="dialog_yes">はい</string>
<string name="dialog_no">いいえ</string>
<string name="dialog_positive">途中…</string>
<string name="dialog_negative">いや…あとで</string>
<string name="dialog_oops">おっと…</string>
<string name="dialog_cancel">キャンセル</string>
<string name="git_sync">リポジトリを同期</string>

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

@@ -9,7 +9,6 @@
<string name="title_activity_git_clone">Информация о репозитории</string>
<!-- Password Store -->
<string name="creation_dialog_text">Пожалуйста, клонируйте или создайте новый репозиторий перед тем, как добавлять пароль или выполнять синхронизацию.</string>
<string name="key_dialog_text">Вы должны выбрать PGP ключ перед инициализацией хранилища</string>
<string name="delete_dialog_text">Вы уверены что хотите удалить пароль %1$s</string>
<string name="move">Переместить</string>
<string name="edit">Редактировать</string>
@@ -47,7 +46,6 @@
<!-- Git Handler -->
<string name="forget_username_dialog_text">Вы забыли указать имя пользователя?</string>
<string name="set_information_dialog_text">Вы должны указать информацию о сервере до выполнения синхронизации</string>
<string name="ssh_preferences_dialog_text">Пожалуйста, импортируйте или сгенерируйте новый SSH ключ в настройках</string>
<string name="ssh_preferences_dialog_title">Нет SSH ключа</string>
<string name="ssh_preferences_dialog_import">Импортировать</string>
@@ -211,8 +209,6 @@
<string name="dialog_ok">OK</string>
<string name="dialog_yes">Да</string>
<string name="dialog_no">Нет</string>
<string name="dialog_positive">On my way…</string>
<string name="dialog_negative">Не … позже</string>
<string name="dialog_oops">Упс…</string>
<string name="dialog_cancel">Отмена</string>
<string name="git_sync">Синхронизировать репозиторий</string>

View File

@@ -9,7 +9,6 @@
<string name="title_activity_git_clone">Repo 信息</string>
<!-- Password Store -->
<string name="creation_dialog_text">在尝试添加密码或任何同步操作前请在下方克隆或添加一个新的 Repo</string>
<string name="key_dialog_text">在初始化 Repo 之前你必须选择你的\"PGP-Key ID\"</string>
<string name="delete_dialog_text">你确定要删除密码 %1$s</string>
<string name="delete">删除</string>
<!-- git commits -->
@@ -28,7 +27,6 @@
<!-- Git Handler -->
<string name="forget_username_dialog_text">你忘了提供用户名了吗?</string>
<string name="set_information_dialog_text">你必须在与服务器同步前设置服务器信息</string>
<string name="ssh_preferences_dialog_text">请在设置中导入或生成你的SSH密钥文件</string>
<string name="ssh_preferences_dialog_title">无SSH密钥</string>
<string name="ssh_preferences_dialog_import">导入</string>
@@ -117,8 +115,6 @@
<string name="dialog_ok">确定</string>
<string name="dialog_yes">确定</string>
<string name="dialog_no"></string>
<string name="dialog_positive">现在就去</string>
<string name="dialog_negative">呃… 算了吧</string>
<string name="dialog_oops">糟糕…</string>
<string name="dialog_cancel">取消</string>
<string name="git_sync">同步 Repo</string>

View File

@@ -9,7 +9,6 @@
<string name="title_activity_git_clone">Repo 訊息</string>
<!-- Password Store -->
<string name="creation_dialog_text">在嘗試新增密碼或任何同步操作之前請在下方 clone 或新增一個新的 Repo</string>
<string name="key_dialog_text">在初始化 Repo 之前你必須選擇你的\"PGP-Key ID\"</string>
<string name="delete_dialog_text">你確定要刪除密碼 %1$s</string>
<string name="delete">刪除</string>
<!-- PGPHandler -->
@@ -25,7 +24,6 @@
<!-- Git Handler -->
<string name="forget_username_dialog_text">你忘記輸入使用者名稱了嗎?</string>
<string name="set_information_dialog_text">你必須在與伺服器同步前設定伺服器資訊</string>
<string name="ssh_preferences_dialog_text">請在設定中匯入或產生你的 SSH 金鑰</string>
<string name="ssh_preferences_dialog_title">無 SSH 金鑰</string>
<string name="ssh_preferences_dialog_import">匯入</string>
@@ -114,8 +112,6 @@
<string name="dialog_ok">確定</string>
<string name="dialog_yes">確定</string>
<string name="dialog_no"></string>
<string name="dialog_positive">確定</string>
<string name="dialog_negative">呃… 算了吧</string>
<string name="dialog_oops">糟糕…</string>
<string name="dialog_cancel">取消</string>
<string name="git_sync">同步 Repo</string>

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

@@ -19,7 +19,7 @@
<!-- Password Store -->
<string name="creation_dialog_text">Please clone or create a new repository below before trying to add a password or running any synchronization operation.</string>
<string name="key_dialog_text">You have to select your PGP key ID before initializing the repository</string>
<string name="key_dialog_text">A valid PGP key must be selected in Settings before initializing the repository</string>
<string name="delete_dialog_text">Are you sure you want to delete the password %1$s?</string>
<string name="move">Move</string>
<string name="edit">Edit</string>
@@ -57,7 +57,7 @@
<!-- Git Handler -->
<string name="forget_username_dialog_text">Did you forget to specify a username?</string>
<string name="set_information_dialog_text">You have to set the information about the server before synchronizing with the server</string>
<string name="set_information_dialog_text">Please fix the remote server configuration in settings before proceeding</string>
<string name="ssh_preferences_dialog_text">Please import or generate your SSH key file in the preferences</string>
<string name="ssh_preferences_dialog_title">No SSH key</string>
<string name="ssh_preferences_dialog_import">Import</string>
@@ -228,8 +228,8 @@
<string name="dialog_ok">OK</string>
<string name="dialog_yes">Yes</string>
<string name="dialog_no">No</string>
<string name="dialog_positive">On my way…</string>
<string name="dialog_negative">Nah… later</string>
<string name="dialog_positive">Go to Settings</string>
<string name="dialog_negative">Go back</string>
<string name="dialog_oops">Oops…</string>
<string name="dialog_cancel">Cancel</string>
<string name="git_sync">Synchronize repository</string>
@@ -309,7 +309,7 @@
<string name="jgit_error_push_dialog_text">Error occurred during the push operation:</string>
<string name="ssh_key_clear_passphrase">Clear ssh-key saved passphrase</string>
<string name="hotp_remember_clear_choice">Clear saved preference for HOTP incrementing</string>
<string name="remember_the_passphrase">Remember the passphrase in the app configuration (insecure)</string>
<string name="remember_the_passphrase">Remember key passphrase</string>
<string name="hackish_tools">Hackish tools</string>
<string name="abort_rebase">Abort rebase and push new branch</string>
<string name="reset_to_remote">Hard reset to remote branch</string>
@@ -349,4 +349,14 @@
<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>
<string name="git_server_config_save_success">Successfully saved configuration</string>
<string name="git_server_config_save_failure">Configuration error: please verify your settings and try again</string>
<string name="git_operation_unable_to_open_ssh_key_title">Unable to open the ssh-key</string>
<string name="git_operation_unable_to_open_ssh_key_message">Please check that it was imported.</string>
<string name="git_operation_wrong_passphrase">Wrong passphrase</string>
</resources>

View File

@@ -33,6 +33,21 @@
<item name="background">@color/primary_color</item>
</style>
<style name="NoBackgroundTheme" parent="@style/AppTheme">
<item name="android:background">@android:color/transparent</item>
<item name="android:backgroundDimEnabled">true</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowActionBar">false</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowEnterAnimation">@android:anim/fade_in</item>
<item name="android:windowExitAnimation">@android:anim/fade_out</item>
<item name="colorPrimaryDark">@android:color/transparent</item>
<item name="windowNoTitle">true</item>
</style>
<style name="ThemeOverlay.AppTheme.TextInputEditText.OutlinedBox" parent="ThemeOverlay.MaterialComponents.TextInputEditText.OutlinedBox">
<item name="colorControlActivated">@color/color_control_normal</item>
</style>

View File

@@ -43,6 +43,7 @@ ext.deps = [
preference: 'androidx.preference:preference:1.1.0',
recycler_view: 'androidx.recyclerview:recyclerview:1.2.0-alpha02',
recycler_view_selection: 'androidx.recyclerview:recyclerview-selection:1.1.0-rc01',
security: 'androidx.security:security-crypto:1.0.0-beta01',
swiperefreshlayout: 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-beta01'
],