mirror of
https://github.com/android-password-store/Android-Password-Store
synced 2025-08-31 14:25:28 +00:00
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 commit4b20371dd4
. * Revert "temp: disable leakcanary" This reverts commit2db7d41bd6
. * 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:
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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"
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
212
app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt
Normal file
212
app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt
Normal 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"
|
||||
}
|
||||
}
|
@@ -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 = "^[^@]+@[^@]+$"
|
||||
}
|
||||
}
|
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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") }
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
18
app/src/main/java/com/zeapo/pwdstore/git/config/Protocol.kt
Normal file
18
app/src/main/java/com/zeapo/pwdstore/git/config/Protocol.kt
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
@@ -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 =
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
*
|
||||
|
6
app/src/main/res/color/toggle_button_selector.xml
Normal file
6
app/src/main/res/color/toggle_button_selector.xml
Normal 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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
||||
|
@@ -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" />
|
||||
|
@@ -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"
|
||||
|
@@ -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>
|
||||
|
@@ -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"
|
||||
|
@@ -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"
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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">Sí</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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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">
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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'
|
||||
],
|
||||
|
||||
|
Reference in New Issue
Block a user