Switch to URL-based Git config and refactor Git settings (#1008)

* Make Git config URL-based and refactor

* Use Kotlin style null handling for string prefs

* Also show an error if generated URL can't be parsed

* Add some testcases for migration strategy

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>

Co-authored-by: Harsh Shandilya <me@msfjarvis.dev>
This commit is contained in:
Fabian Henneke
2020-08-11 18:11:39 +02:00
committed by GitHub
parent 8f957ca994
commit 15aa929802
44 changed files with 395 additions and 585 deletions

View File

@@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file.
### Changed ### Changed
- The Git repository URL can now be specified directly
- Slightly reduce APK size - Slightly reduce APK size
- Always show the parent path in entries - Always show the parent path in entries
- Passwords will no longer be copied to the clipboard by default - Passwords will no longer be copied to the clipboard by default

View File

@@ -0,0 +1,81 @@
/*
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
@file:Suppress("DEPRECATION")
package com.zeapo.pwdstore
import android.content.Context
import androidx.core.content.edit
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.getString
import com.zeapo.pwdstore.utils.sharedPrefs
import org.junit.Test
import org.junit.Assert.*
class MigrationsTest {
private fun checkOldKeysAreRemoved(context: Context) = with(context.sharedPrefs) {
assertNull(getString(PreferenceKeys.GIT_REMOTE_PORT))
assertNull(getString(PreferenceKeys.GIT_REMOTE_USERNAME))
assertNull(getString(PreferenceKeys.GIT_REMOTE_SERVER))
assertNull(getString(PreferenceKeys.GIT_REMOTE_LOCATION))
}
@Test
fun verifySshWithCustomPortMigration() {
val context = Application.instance.applicationContext
context.sharedPrefs.edit {
clear()
putString(PreferenceKeys.GIT_REMOTE_PORT, "2200")
putString(PreferenceKeys.GIT_REMOTE_USERNAME, "msfjarvis")
putString(PreferenceKeys.GIT_REMOTE_LOCATION, "/mnt/disk3/pass-repo")
putString(PreferenceKeys.GIT_REMOTE_SERVER, "192.168.0.102")
putString(PreferenceKeys.GIT_REMOTE_PROTOCOL, "ssh://")
}
runMigrations(context)
checkOldKeysAreRemoved(context)
assertEquals(
context.sharedPrefs.getString(PreferenceKeys.GIT_REMOTE_URL),
"ssh://msfjarvis@192.168.0.102:2200/mnt/disk3/pass-repo"
)
}
@Test
fun verifySshWithDefaultPortMigration() {
val context = Application.instance.applicationContext
context.sharedPrefs.edit {
clear()
putString(PreferenceKeys.GIT_REMOTE_USERNAME, "msfjarvis")
putString(PreferenceKeys.GIT_REMOTE_LOCATION, "/mnt/disk3/pass-repo")
putString(PreferenceKeys.GIT_REMOTE_SERVER, "192.168.0.102")
putString(PreferenceKeys.GIT_REMOTE_PROTOCOL, "ssh://")
}
runMigrations(context)
checkOldKeysAreRemoved(context)
assertEquals(
context.sharedPrefs.getString(PreferenceKeys.GIT_REMOTE_URL),
"msfjarvis@192.168.0.102:/mnt/disk3/pass-repo"
)
}
@Test
fun verifyHttpsWithGitHubMigration() {
val context = Application.instance.applicationContext
context.sharedPrefs.edit {
clear()
putString(PreferenceKeys.GIT_REMOTE_USERNAME, "msfjarvis")
putString(PreferenceKeys.GIT_REMOTE_LOCATION, "Android-Password-Store/pass-test")
putString(PreferenceKeys.GIT_REMOTE_SERVER, "github.com")
putString(PreferenceKeys.GIT_REMOTE_PROTOCOL, "https://")
}
runMigrations(context)
checkOldKeysAreRemoved(context)
assertEquals(
context.sharedPrefs.getString(PreferenceKeys.GIT_REMOTE_URL),
"https://github.com/Android-Password-Store/pass-test"
)
}
}

View File

@@ -1,126 +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.view.View
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.rule.ActivityTestRule
import com.google.android.material.button.MaterialButtonToggleGroup
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.git.BaseGitActivity.GitUpdateUrlResult
import kotlin.test.assertEquals
import org.hamcrest.Matcher
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class GitServerConfigActivityTest {
@Rule
@JvmField
val activityRule = ActivityTestRule(GitServerConfigActivity::class.java, true)
private val activity get() = activityRule.activity
@Test
fun invalidValuesFailPredictably() {
setDefaultSshValues()
onView(withId(R.id.server_port)).perform(replaceText("69420"))
assertEquals(activity.updateUrl(), GitUpdateUrlResult.CustomPortRequiresAbsoluteUrlError)
setDefaultSshValues()
onView(withId(R.id.server_url)).perform(replaceText(""))
assertEquals(activity.updateUrl(), GitUpdateUrlResult.EmptyHostnameError)
setDefaultSshValues()
onView(withId(R.id.server_port)).perform(replaceText("xyz_is_not_a_port"))
assertEquals(activity.updateUrl(), GitUpdateUrlResult.NonNumericPortError)
setDefaultHttpsValues()
onView(withId(R.id.server_port)).perform(replaceText("xyz_is_not_a_port"))
assertEquals(activity.updateUrl(), GitUpdateUrlResult.NonNumericPortError)
setDefaultHttpsValues()
onView(withId(R.id.server_url)).perform(replaceText(""))
assertEquals(activity.updateUrl(), GitUpdateUrlResult.EmptyHostnameError)
}
@Test
fun urlIsConstructedCorrectly() {
setDefaultSshValues()
activity.updateUrl()
assertEquals("john_doe@github.com:john_doe/my_secret_repository", activity.url)
setDefaultSshValues()
onView(withId(R.id.server_port)).perform(replaceText("69420"))
onView(withId(R.id.server_url)).perform(replaceText("192.168.0.102"))
onView(withId(R.id.server_path)).perform(replaceText("/home/john_doe/my_secret_repository"))
activity.updateUrl()
assertEquals("ssh://john_doe@192.168.0.102:69420/home/john_doe/my_secret_repository", activity.url)
setDefaultHttpsValues()
activity.updateUrl()
assertEquals("https://github.com/john_doe/my_secret_repository", activity.url)
setDefaultHttpsValues()
onView(withId(R.id.server_port)).perform(replaceText("69420"))
onView(withId(R.id.server_url)).perform(replaceText("192.168.0.102"))
onView(withId(R.id.server_path)).perform(replaceText("/home/john_doe/my_secret_repository"))
activity.updateUrl()
assertEquals("https://192.168.0.102:69420/home/john_doe/my_secret_repository", activity.url)
}
private fun <T> callMethod(message: String = "", viewMethod: (view: T) -> Unit): ViewAction {
return object : ViewAction {
override fun getDescription(): String {
return if (message.isBlank()) viewMethod.toString() else message
}
override fun getConstraints(): Matcher<View> {
return isEnabled()
}
@Suppress("UNCHECKED_CAST")
override fun perform(uiController: UiController?, view: View?) {
viewMethod(view!! as T)
}
}
}
private fun setDefaultHttpsValues() {
onView(withId(R.id.clone_protocol_group)).perform(callMethod<MaterialButtonToggleGroup> {
it.check(R.id.clone_protocol_https)
})
onView(withId(R.id.connection_mode_group)).perform(callMethod<MaterialButtonToggleGroup> {
it.check(R.id.connection_mode_password)
})
onView(withId(R.id.server_path)).perform(replaceText("john_doe/my_secret_repository"))
onView(withId(R.id.server_port)).perform(replaceText(""))
onView(withId(R.id.server_url)).perform(replaceText("github.com"))
onView(withId(R.id.server_user)).perform(replaceText("john_doe"))
}
private fun setDefaultSshValues() {
onView(withId(R.id.clone_protocol_group)).perform(callMethod<MaterialButtonToggleGroup> {
it.check(R.id.clone_protocol_ssh)
})
onView(withId(R.id.connection_mode_group)).perform(callMethod<MaterialButtonToggleGroup> {
it.check(R.id.connection_mode_ssh_key)
})
onView(withId(R.id.server_path)).perform(replaceText("john_doe/my_secret_repository"))
onView(withId(R.id.server_port)).perform(replaceText(""))
onView(withId(R.id.server_url)).perform(replaceText("github.com"))
onView(withId(R.id.server_user)).perform(replaceText("john_doe"))
}
}

View File

@@ -10,12 +10,12 @@ import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES
import androidx.preference.PreferenceManager
import com.github.ajalt.timberkt.Timber.DebugTree import com.github.ajalt.timberkt.Timber.DebugTree
import com.github.ajalt.timberkt.Timber.plant import com.github.ajalt.timberkt.Timber.plant
import com.zeapo.pwdstore.git.config.setUpBouncyCastleForSshj import com.zeapo.pwdstore.git.config.setUpBouncyCastleForSshj
import com.zeapo.pwdstore.utils.PreferenceKeys import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.sharedPrefs import com.zeapo.pwdstore.utils.sharedPrefs
import com.zeapo.pwdstore.utils.getString
@Suppress("Unused") @Suppress("Unused")
class Application : android.app.Application(), SharedPreferences.OnSharedPreferenceChangeListener { class Application : android.app.Application(), SharedPreferences.OnSharedPreferenceChangeListener {
@@ -30,6 +30,7 @@ class Application : android.app.Application(), SharedPreferences.OnSharedPrefere
sharedPrefs.registerOnSharedPreferenceChangeListener(this) sharedPrefs.registerOnSharedPreferenceChangeListener(this)
setNightMode() setNightMode()
setUpBouncyCastleForSshj() setUpBouncyCastleForSshj()
runMigrations(applicationContext)
} }
override fun onTerminate() { override fun onTerminate() {
@@ -44,7 +45,7 @@ class Application : android.app.Application(), SharedPreferences.OnSharedPrefere
} }
private fun setNightMode() { private fun setNightMode() {
AppCompatDelegate.setDefaultNightMode(when (sharedPrefs.getString(PreferenceKeys.APP_THEME, getString(R.string.app_theme_def))) { AppCompatDelegate.setDefaultNightMode(when (sharedPrefs.getString(PreferenceKeys.APP_THEME) ?: getString(R.string.app_theme_def)) {
"light" -> MODE_NIGHT_NO "light" -> MODE_NIGHT_NO
"dark" -> MODE_NIGHT_YES "dark" -> MODE_NIGHT_YES
"follow_system" -> MODE_NIGHT_FOLLOW_SYSTEM "follow_system" -> MODE_NIGHT_FOLLOW_SYSTEM

View File

@@ -15,10 +15,10 @@ import android.os.Build
import android.os.IBinder import android.os.IBinder
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.preference.PreferenceManager
import com.github.ajalt.timberkt.d import com.github.ajalt.timberkt.d
import com.zeapo.pwdstore.utils.PreferenceKeys import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.clipboard import com.zeapo.pwdstore.utils.clipboard
import com.zeapo.pwdstore.utils.getString
import com.zeapo.pwdstore.utils.sharedPrefs import com.zeapo.pwdstore.utils.sharedPrefs
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -46,7 +46,7 @@ class ClipboardService : Service() {
ACTION_START -> { ACTION_START -> {
val time = try { val time = try {
Integer.parseInt(settings.getString(PreferenceKeys.GENERAL_SHOW_TIME, "45") as String) Integer.parseInt(settings.getString(PreferenceKeys.GENERAL_SHOW_TIME) ?: "45")
} catch (e: NumberFormatException) { } catch (e: NumberFormatException) {
45 45
} }

View File

@@ -9,7 +9,6 @@ import android.os.Bundle
import android.os.Handler import android.os.Handler
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.edit import androidx.core.content.edit
import androidx.preference.PreferenceManager
import com.zeapo.pwdstore.crypto.DecryptActivity import com.zeapo.pwdstore.crypto.DecryptActivity
import com.zeapo.pwdstore.utils.BiometricAuthenticator import com.zeapo.pwdstore.utils.BiometricAuthenticator
import com.zeapo.pwdstore.utils.PreferenceKeys import com.zeapo.pwdstore.utils.PreferenceKeys

View File

@@ -0,0 +1,83 @@
/*
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
@file:Suppress("DEPRECATION")
package com.zeapo.pwdstore
import android.content.Context
import androidx.core.content.edit
import com.github.ajalt.timberkt.e
import com.github.ajalt.timberkt.i
import com.zeapo.pwdstore.git.config.GitSettings
import com.zeapo.pwdstore.git.config.Protocol
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.getString
import com.zeapo.pwdstore.utils.sharedPrefs
import java.net.URI
fun runMigrations(context: Context) {
migrateToGitUrlBasedConfig(context)
}
private fun migrateToGitUrlBasedConfig(context: Context) {
val serverHostname = context.sharedPrefs.getString(PreferenceKeys.GIT_REMOTE_SERVER)
?: return
i { "Migrating to URL-based Git config" }
val serverPort = context.sharedPrefs.getString(PreferenceKeys.GIT_REMOTE_PORT) ?: ""
val serverUser = context.sharedPrefs.getString(PreferenceKeys.GIT_REMOTE_USERNAME) ?: ""
val serverPath = context.sharedPrefs.getString(PreferenceKeys.GIT_REMOTE_LOCATION) ?: ""
val protocol = Protocol.fromString(context.sharedPrefs.getString(PreferenceKeys.GIT_REMOTE_PROTOCOL))
// Whether we need the leading ssh:// depends on the use of a custom port.
val hostnamePart = serverHostname.removePrefix("ssh://")
val url = when (protocol) {
Protocol.Ssh -> {
val userPart = if (serverUser.isEmpty()) "" else "${serverUser.trimEnd('@')}@"
val portPart =
if (serverPort == "22" || serverPort.isEmpty()) "" else ":$serverPort"
if (portPart.isEmpty()) {
"$userPart$hostnamePart:$serverPath"
} else {
// Only absolute paths are supported with custom ports.
if (!serverPath.startsWith('/'))
null
else
// We have to specify the ssh scheme as this is the only way to pass a custom
// port.
"ssh://$userPart$hostnamePart$portPart$serverPath"
}
}
Protocol.Https -> {
val portPart =
if (serverPort == "443" || serverPort.isEmpty()) "" else ":$serverPort"
val pathPart = serverPath.trimStart('/', ':')
val urlWithFreeEntryScheme = "$hostnamePart$portPart/$pathPart"
val url = when {
urlWithFreeEntryScheme.startsWith("https://") -> urlWithFreeEntryScheme
urlWithFreeEntryScheme.startsWith("http://") -> urlWithFreeEntryScheme.replaceFirst("http", "https")
else -> "https://$urlWithFreeEntryScheme"
}
try {
if (URI(url).rawAuthority != null)
url
else
null
} catch (_: Exception) {
null
}
}
}
context.sharedPrefs.edit {
remove(PreferenceKeys.GIT_REMOTE_LOCATION)
remove(PreferenceKeys.GIT_REMOTE_PORT)
remove(PreferenceKeys.GIT_REMOTE_SERVER)
remove(PreferenceKeys.GIT_REMOTE_USERNAME)
}
if (url == null || !GitSettings.updateUrlIfValid(url)) {
e { "Failed to migrate to URL-based Git config, generated URL is invalid" }
}
}

View File

@@ -26,12 +26,12 @@ import com.zeapo.pwdstore.git.BaseGitActivity
import com.zeapo.pwdstore.git.GitOperationActivity import com.zeapo.pwdstore.git.GitOperationActivity
import com.zeapo.pwdstore.git.GitServerConfigActivity import com.zeapo.pwdstore.git.GitServerConfigActivity
import com.zeapo.pwdstore.git.config.ConnectionMode import com.zeapo.pwdstore.git.config.ConnectionMode
import com.zeapo.pwdstore.git.config.GitSettings
import com.zeapo.pwdstore.ui.OnOffItemAnimator import com.zeapo.pwdstore.ui.OnOffItemAnimator
import com.zeapo.pwdstore.ui.adapters.PasswordItemRecyclerAdapter import com.zeapo.pwdstore.ui.adapters.PasswordItemRecyclerAdapter
import com.zeapo.pwdstore.ui.dialogs.ItemCreationBottomSheet import com.zeapo.pwdstore.ui.dialogs.ItemCreationBottomSheet
import com.zeapo.pwdstore.utils.PasswordItem import com.zeapo.pwdstore.utils.PasswordItem
import com.zeapo.pwdstore.utils.PasswordRepository import com.zeapo.pwdstore.utils.PasswordRepository
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.sharedPrefs import com.zeapo.pwdstore.utils.sharedPrefs
import com.zeapo.pwdstore.utils.viewBinding import com.zeapo.pwdstore.utils.viewBinding
import java.io.File import java.io.File
@@ -91,8 +91,7 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
} else { } else {
// When authentication is set to ConnectionMode.None then the only git operation we // When authentication is set to ConnectionMode.None then the only git operation we
// can run is a pull, so automatically fallback to that. // can run is a pull, so automatically fallback to that.
val operationId = when (ConnectionMode.fromString(settings.getString val operationId = when (GitSettings.connectionMode) {
(PreferenceKeys.GIT_REMOTE_AUTH, null))) {
ConnectionMode.None -> BaseGitActivity.REQUEST_PULL ConnectionMode.None -> BaseGitActivity.REQUEST_PULL
else -> BaseGitActivity.REQUEST_SYNC else -> BaseGitActivity.REQUEST_SYNC
} }

View File

@@ -7,7 +7,6 @@ package com.zeapo.pwdstore
import android.Manifest import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.ShortcutInfo.Builder import android.content.pm.ShortcutInfo.Builder
import android.content.pm.ShortcutManager import android.content.pm.ShortcutManager
@@ -34,7 +33,6 @@ import androidx.fragment.app.FragmentManager
import androidx.fragment.app.commit import androidx.fragment.app.commit
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import com.github.ajalt.timberkt.d import com.github.ajalt.timberkt.d
import com.github.ajalt.timberkt.e import com.github.ajalt.timberkt.e
import com.github.ajalt.timberkt.i import com.github.ajalt.timberkt.i
@@ -52,6 +50,7 @@ import com.zeapo.pwdstore.git.BaseGitActivity
import com.zeapo.pwdstore.git.GitOperationActivity import com.zeapo.pwdstore.git.GitOperationActivity
import com.zeapo.pwdstore.git.GitServerConfigActivity import com.zeapo.pwdstore.git.GitServerConfigActivity
import com.zeapo.pwdstore.git.config.ConnectionMode import com.zeapo.pwdstore.git.config.ConnectionMode
import com.zeapo.pwdstore.git.config.GitSettings
import com.zeapo.pwdstore.ui.dialogs.FolderCreationDialogFragment import com.zeapo.pwdstore.ui.dialogs.FolderCreationDialogFragment
import com.zeapo.pwdstore.utils.PasswordItem import com.zeapo.pwdstore.utils.PasswordItem
import com.zeapo.pwdstore.utils.PasswordRepository import com.zeapo.pwdstore.utils.PasswordRepository
@@ -66,6 +65,7 @@ import com.zeapo.pwdstore.utils.PasswordRepository.PasswordSortOrder.Companion.g
import com.zeapo.pwdstore.utils.PreferenceKeys import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.commitChange import com.zeapo.pwdstore.utils.commitChange
import com.zeapo.pwdstore.utils.contains import com.zeapo.pwdstore.utils.contains
import com.zeapo.pwdstore.utils.getString
import com.zeapo.pwdstore.utils.isInsideRepository import com.zeapo.pwdstore.utils.isInsideRepository
import com.zeapo.pwdstore.utils.listFilesRecursively import com.zeapo.pwdstore.utils.listFilesRecursively
import com.zeapo.pwdstore.utils.requestInputFocusOnView import com.zeapo.pwdstore.utils.requestInputFocusOnView
@@ -114,8 +114,8 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
private val directoryChangeAction = registerForActivityResult(StartActivityForResult()) { result -> private val directoryChangeAction = registerForActivityResult(StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) { if (result.resultCode == RESULT_OK) {
if (settings.getBoolean(PreferenceKeys.GIT_EXTERNAL, false) && if (settings.getBoolean(PreferenceKeys.GIT_EXTERNAL, false) &&
settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO, null) != null) { settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO) != null) {
val externalRepoPath = settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO, null) val externalRepoPath = settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO)
val dir = externalRepoPath?.let { File(it) } val dir = externalRepoPath?.let { File(it) }
if (dir != null && if (dir != null &&
dir.exists() && dir.exists() &&
@@ -252,8 +252,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
override fun onCreateOptionsMenu(menu: Menu?): Boolean { override fun onCreateOptionsMenu(menu: Menu?): Boolean {
val menuRes = when { val menuRes = when {
ConnectionMode.fromString(settings.getString(PreferenceKeys.GIT_REMOTE_AUTH, null)) GitSettings.connectionMode == ConnectionMode.None -> R.menu.main_menu_no_auth
== ConnectionMode.None -> R.menu.main_menu_no_auth
PasswordRepository.isGitRepo() -> R.menu.main_menu_git PasswordRepository.isGitRepo() -> R.menu.main_menu_git
else -> R.menu.main_menu_non_git else -> R.menu.main_menu_non_git
} }
@@ -403,7 +402,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
private fun initializeRepositoryInfo() { private fun initializeRepositoryInfo() {
val externalRepo = settings.getBoolean(PreferenceKeys.GIT_EXTERNAL, false) val externalRepo = settings.getBoolean(PreferenceKeys.GIT_EXTERNAL, false)
val externalRepoPath = settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO, null) val externalRepoPath = settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO)
if (externalRepo && !hasRequiredStoragePermissions()) { if (externalRepo && !hasRequiredStoragePermissions()) {
return return
} }
@@ -849,7 +848,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
} }
.setNegativeButton(resources.getString(R.string.location_sdcard)) { _, _ -> .setNegativeButton(resources.getString(R.string.location_sdcard)) { _, _ ->
settings.edit { putBoolean(PreferenceKeys.GIT_EXTERNAL, true) } settings.edit { putBoolean(PreferenceKeys.GIT_EXTERNAL, true) }
val externalRepo = settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO, null) val externalRepo = settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO)
if (externalRepo == null) { if (externalRepo == null) {
val intent = Intent(activity, UserPreference::class.java) val intent = Intent(activity, UserPreference::class.java)
intent.putExtra("operation", "git_external") intent.putExtra("operation", "git_external")

View File

@@ -15,7 +15,6 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asFlow import androidx.lifecycle.asFlow
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.preference.PreferenceManager
import androidx.recyclerview.selection.ItemDetailsLookup import androidx.recyclerview.selection.ItemDetailsLookup
import androidx.recyclerview.selection.ItemKeyProvider import androidx.recyclerview.selection.ItemKeyProvider
import androidx.recyclerview.selection.Selection import androidx.recyclerview.selection.Selection

View File

@@ -34,7 +34,6 @@ import androidx.preference.EditTextPreference
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
import com.github.ajalt.timberkt.Timber.tag import com.github.ajalt.timberkt.Timber.tag
import com.github.ajalt.timberkt.d import com.github.ajalt.timberkt.d
@@ -54,6 +53,7 @@ import com.zeapo.pwdstore.utils.PasswordRepository
import com.zeapo.pwdstore.utils.PreferenceKeys import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.autofillManager import com.zeapo.pwdstore.utils.autofillManager
import com.zeapo.pwdstore.utils.getEncryptedPrefs import com.zeapo.pwdstore.utils.getEncryptedPrefs
import com.zeapo.pwdstore.utils.getString
import com.zeapo.pwdstore.utils.sharedPrefs import com.zeapo.pwdstore.utils.sharedPrefs
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
@@ -141,11 +141,11 @@ class UserPreference : AppCompatActivity() {
// Misc preferences // Misc preferences
val appVersionPreference = findPreference<Preference>(PreferenceKeys.APP_VERSION) val appVersionPreference = findPreference<Preference>(PreferenceKeys.APP_VERSION)
selectExternalGitRepositoryPreference?.summary = sharedPreferences.getString(PreferenceKeys.GIT_EXTERNAL_REPO, getString(R.string.no_repo_selected)) selectExternalGitRepositoryPreference?.summary = sharedPreferences.getString(PreferenceKeys.GIT_EXTERNAL_REPO) ?: getString(R.string.no_repo_selected)
viewSshKeyPreference?.isVisible = sharedPreferences.getBoolean(PreferenceKeys.USE_GENERATED_KEY, false) viewSshKeyPreference?.isVisible = sharedPreferences.getBoolean(PreferenceKeys.USE_GENERATED_KEY, false)
deleteRepoPreference?.isVisible = !sharedPreferences.getBoolean(PreferenceKeys.GIT_EXTERNAL, false) deleteRepoPreference?.isVisible = !sharedPreferences.getBoolean(PreferenceKeys.GIT_EXTERNAL, false)
clearClipboard20xPreference?.isVisible = sharedPreferences.getString(PreferenceKeys.GENERAL_SHOW_TIME, "45")?.toInt() != 0 clearClipboard20xPreference?.isVisible = sharedPreferences.getString(PreferenceKeys.GENERAL_SHOW_TIME)?.toInt() != 0
openkeystoreIdPreference?.isVisible = sharedPreferences.getString(PreferenceKeys.SSH_OPENKEYSTORE_KEYID, null)?.isNotEmpty() openkeystoreIdPreference?.isVisible = sharedPreferences.getString(PreferenceKeys.SSH_OPENKEYSTORE_KEYID)?.isNotEmpty()
?: false ?: false
updateAutofillSettings() updateAutofillSettings()
@@ -171,9 +171,9 @@ class UserPreference : AppCompatActivity() {
clearSavedPassPreference?.onPreferenceClickListener = ClickListener { clearSavedPassPreference?.onPreferenceClickListener = ClickListener {
encryptedPreferences.edit { encryptedPreferences.edit {
if (encryptedPreferences.getString(PreferenceKeys.HTTPS_PASSWORD, null) != null) if (encryptedPreferences.getString(PreferenceKeys.HTTPS_PASSWORD) != null)
remove(PreferenceKeys.HTTPS_PASSWORD) remove(PreferenceKeys.HTTPS_PASSWORD)
else if (encryptedPreferences.getString(PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE, null) != null) else if (encryptedPreferences.getString(PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE) != null)
remove(PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE) remove(PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE)
} }
updateClearSavedPassphrasePrefs() updateClearSavedPassphrasePrefs()
@@ -226,7 +226,7 @@ class UserPreference : AppCompatActivity() {
} }
selectExternalGitRepositoryPreference?.summary = selectExternalGitRepositoryPreference?.summary =
sharedPreferences.getString(PreferenceKeys.GIT_EXTERNAL_REPO, context.getString(R.string.no_repo_selected)) sharedPreferences.getString(PreferenceKeys.GIT_EXTERNAL_REPO) ?: context.getString(R.string.no_repo_selected)
selectExternalGitRepositoryPreference?.onPreferenceClickListener = ClickListener { selectExternalGitRepositoryPreference?.onPreferenceClickListener = ClickListener {
prefsActivity.selectExternalGitRepository() prefsActivity.selectExternalGitRepository()
true true
@@ -321,7 +321,7 @@ class UserPreference : AppCompatActivity() {
prefsActivity.storeCustomDictionaryPath() prefsActivity.storeCustomDictionaryPath()
true true
} }
val dictUri = sharedPreferences.getString(PreferenceKeys.PREF_KEY_CUSTOM_DICT, "") val dictUri = sharedPreferences.getString(PreferenceKeys.PREF_KEY_CUSTOM_DICT) ?: ""
if (!TextUtils.isEmpty(dictUri)) { if (!TextUtils.isEmpty(dictUri)) {
setCustomDictSummary(prefCustomXkpwdDictionary, Uri.parse(dictUri)) setCustomDictSummary(prefCustomXkpwdDictionary, Uri.parse(dictUri))
@@ -377,8 +377,8 @@ class UserPreference : AppCompatActivity() {
private fun updateClearSavedPassphrasePrefs() { private fun updateClearSavedPassphrasePrefs() {
clearSavedPassPreference?.apply { clearSavedPassPreference?.apply {
val sshPass = encryptedPreferences.getString(PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE, null) val sshPass = encryptedPreferences.getString(PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE)
val httpsPass = encryptedPreferences.getString(PreferenceKeys.HTTPS_PASSWORD, null) val httpsPass = encryptedPreferences.getString(PreferenceKeys.HTTPS_PASSWORD)
if (sshPass == null && httpsPass == null) { if (sshPass == null && httpsPass == null) {
isVisible = false isVisible = false
return@apply return@apply

View File

@@ -23,6 +23,8 @@ import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.model.PasswordEntry import com.zeapo.pwdstore.model.PasswordEntry
import com.zeapo.pwdstore.utils.PasswordRepository import com.zeapo.pwdstore.utils.PasswordRepository
import com.zeapo.pwdstore.utils.PreferenceKeys import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.getString
import com.zeapo.pwdstore.utils.sharedPrefs
import java.io.File import java.io.File
import java.security.MessageDigest import java.security.MessageDigest
@@ -37,11 +39,7 @@ private fun ByteArray.base64(): String {
return Base64.encodeToString(this, Base64.NO_WRAP) return Base64.encodeToString(this, Base64.NO_WRAP)
} }
private fun Context.getDefaultUsername(): String? { private fun Context.getDefaultUsername() = sharedPrefs.getString(PreferenceKeys.OREO_AUTOFILL_DEFAULT_USERNAME)
return PreferenceManager
.getDefaultSharedPreferences(this)
.getString(PreferenceKeys.OREO_AUTOFILL_DEFAULT_USERNAME, null)
}
private fun stableHash(array: Collection<ByteArray>): String { private fun stableHash(array: Collection<ByteArray>): String {
val hashes = array.map { it.sha256().base64() } val hashes = array.map { it.sha256().base64() }

View File

@@ -5,10 +5,8 @@
package com.zeapo.pwdstore.autofill.oreo package com.zeapo.pwdstore.autofill.oreo
import android.content.Context import android.content.Context
import android.content.SharedPreferences
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.preference.PreferenceManager
import com.zeapo.pwdstore.utils.sharedPrefs import com.zeapo.pwdstore.utils.sharedPrefs
import java.io.File import java.io.File
import java.nio.file.Paths import java.nio.file.Paths

View File

@@ -6,8 +6,8 @@ package com.zeapo.pwdstore.autofill.oreo
import android.content.Context import android.content.Context
import android.util.Patterns import android.util.Patterns
import androidx.preference.PreferenceManager
import com.zeapo.pwdstore.utils.PreferenceKeys import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.getString
import com.zeapo.pwdstore.utils.sharedPrefs import com.zeapo.pwdstore.utils.sharedPrefs
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import mozilla.components.lib.publicsuffixlist.PublicSuffixList import mozilla.components.lib.publicsuffixlist.PublicSuffixList
@@ -69,9 +69,10 @@ fun getSuffixPlusUpToOne(domain: String, suffix: String): String? {
} }
fun getCustomSuffixes(context: Context): Sequence<String> { fun getCustomSuffixes(context: Context): Sequence<String> {
return context.sharedPrefs.getString(PreferenceKeys.OREO_AUTOFILL_CUSTOM_PUBLIC_SUFFIXES, "")!! return context.sharedPrefs.getString(PreferenceKeys.OREO_AUTOFILL_CUSTOM_PUBLIC_SUFFIXES)
.splitToSequence('\n') ?.splitToSequence('\n')
.filter { it.isNotBlank() && it.first() != '.' && it.last() != '.' } ?.filter { it.isNotBlank() && it.first() != '.' && it.last() != '.' }
?: emptySequence()
} }
suspend fun getCanonicalSuffix(context: Context, domain: String): String { suspend fun getCanonicalSuffix(context: Context, domain: String): String {

View File

@@ -30,6 +30,7 @@ import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.utils.OPENPGP_PROVIDER import com.zeapo.pwdstore.utils.OPENPGP_PROVIDER
import com.zeapo.pwdstore.utils.PreferenceKeys import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.clipboard import com.zeapo.pwdstore.utils.clipboard
import com.zeapo.pwdstore.utils.getString
import com.zeapo.pwdstore.utils.sharedPrefs import com.zeapo.pwdstore.utils.sharedPrefs
import com.zeapo.pwdstore.utils.snackbar import com.zeapo.pwdstore.utils.snackbar
import java.io.File import java.io.File
@@ -254,8 +255,7 @@ open class BasePgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBou
var clearAfter = 45 var clearAfter = 45
try { try {
clearAfter = (settings.getString(PreferenceKeys.GENERAL_SHOW_TIME, "45") clearAfter = (settings.getString(PreferenceKeys.GENERAL_SHOW_TIME) ?: "45").toInt()
?: "45").toInt()
} catch (_: NumberFormatException) { } catch (_: NumberFormatException) {
} }

View File

@@ -31,6 +31,7 @@ import com.zeapo.pwdstore.ui.dialogs.XkPasswordGeneratorDialogFragment
import com.zeapo.pwdstore.utils.PasswordRepository import com.zeapo.pwdstore.utils.PasswordRepository
import com.zeapo.pwdstore.utils.PreferenceKeys import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.commitChange import com.zeapo.pwdstore.utils.commitChange
import com.zeapo.pwdstore.utils.getString
import com.zeapo.pwdstore.utils.isInsideRepository import com.zeapo.pwdstore.utils.isInsideRepository
import com.zeapo.pwdstore.utils.snackbar import com.zeapo.pwdstore.utils.snackbar
import com.zeapo.pwdstore.utils.viewBinding import com.zeapo.pwdstore.utils.viewBinding
@@ -220,7 +221,7 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
} }
private fun generatePassword() { private fun generatePassword() {
when (settings.getString(PreferenceKeys.PREF_KEY_PWGEN_TYPE, KEY_PWGEN_TYPE_CLASSIC)) { when (settings.getString(PreferenceKeys.PREF_KEY_PWGEN_TYPE) ?: KEY_PWGEN_TYPE_CLASSIC) {
KEY_PWGEN_TYPE_CLASSIC -> PasswordGeneratorDialogFragment() KEY_PWGEN_TYPE_CLASSIC -> PasswordGeneratorDialogFragment()
.show(supportFragmentManager, "generator") .show(supportFragmentManager, "generator")
KEY_PWGEN_TYPE_XKPASSWD -> XkPasswordGeneratorDialogFragment() KEY_PWGEN_TYPE_XKPASSWD -> XkPasswordGeneratorDialogFragment()

View File

@@ -5,20 +5,15 @@
package com.zeapo.pwdstore.git package com.zeapo.pwdstore.git
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.edit
import androidx.core.text.isDigitsOnly
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.github.ajalt.timberkt.Timber.tag import com.github.ajalt.timberkt.Timber.tag
import com.github.ajalt.timberkt.e import com.github.ajalt.timberkt.e
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.git.config.ConnectionMode import com.zeapo.pwdstore.git.config.ConnectionMode
import com.zeapo.pwdstore.git.config.Protocol import com.zeapo.pwdstore.git.config.GitSettings
import com.zeapo.pwdstore.git.config.SshApiSessionFactory import com.zeapo.pwdstore.git.config.SshApiSessionFactory
import com.zeapo.pwdstore.git.operation.BreakOutOfDetached import com.zeapo.pwdstore.git.operation.BreakOutOfDetached
import com.zeapo.pwdstore.git.operation.CloneOperation import com.zeapo.pwdstore.git.operation.CloneOperation
@@ -28,11 +23,6 @@ import com.zeapo.pwdstore.git.operation.PushOperation
import com.zeapo.pwdstore.git.operation.ResetToRemoteOperation import com.zeapo.pwdstore.git.operation.ResetToRemoteOperation
import com.zeapo.pwdstore.git.operation.SyncOperation import com.zeapo.pwdstore.git.operation.SyncOperation
import com.zeapo.pwdstore.utils.PasswordRepository import com.zeapo.pwdstore.utils.PasswordRepository
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.getEncryptedPrefs
import com.zeapo.pwdstore.utils.sharedPrefs
import java.io.File
import java.net.URI
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
/** /**
@@ -41,39 +31,8 @@ import kotlinx.coroutines.launch
*/ */
abstract class BaseGitActivity : AppCompatActivity() { abstract class BaseGitActivity : AppCompatActivity() {
lateinit var protocol: Protocol
lateinit var connectionMode: ConnectionMode
var url: String? = null
lateinit var serverHostname: String
lateinit var serverPort: String
lateinit var serverUser: String
lateinit var serverPath: String
lateinit var username: String
lateinit var email: String
lateinit var branch: String
private var identityBuilder: SshApiSessionFactory.IdentityBuilder? = null private var identityBuilder: SshApiSessionFactory.IdentityBuilder? = null
private var identity: SshApiSessionFactory.ApiIdentity? = null private 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 = sharedPrefs
encryptedSettings = getEncryptedPrefs("git_operation")
protocol = Protocol.fromString(settings.getString(PreferenceKeys.GIT_REMOTE_PROTOCOL, null))
connectionMode = ConnectionMode.fromString(settings.getString(PreferenceKeys.GIT_REMOTE_AUTH, null))
serverHostname = settings.getString(PreferenceKeys.GIT_REMOTE_SERVER, null) ?: ""
serverPort = settings.getString(PreferenceKeys.GIT_REMOTE_PORT, null) ?: ""
serverUser = settings.getString(PreferenceKeys.GIT_REMOTE_USERNAME, null) ?: ""
serverPath = settings.getString(PreferenceKeys.GIT_REMOTE_LOCATION, null) ?: ""
username = settings.getString(PreferenceKeys.GIT_CONFIG_USER_NAME, null) ?: ""
email = settings.getString(PreferenceKeys.GIT_CONFIG_USER_EMAIL, null) ?: ""
branch = settings.getString(PreferenceKeys.GIT_BRANCH_NAME, null) ?: "master"
updateUrl()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) { return when (item.itemId) {
@@ -95,77 +54,6 @@ abstract class BaseGitActivity : AppCompatActivity() {
super.onDestroy() super.onDestroy()
} }
enum class GitUpdateUrlResult(val textRes: Int) {
Ok(0),
CustomPortRequiresAbsoluteUrlError(R.string.git_config_error_custom_port_absolute),
EmptyHostnameError(R.string.git_config_error_hostname_empty),
GenericError(R.string.git_config_error_generic),
NonNumericPortError(R.string.git_config_error_nonnumeric_port)
}
/**
* Update the [url] field with the values that build it up. This function returns a
* [GitUpdateUrlResult] indicating whether the values could be used to build a URL and only adds
* the `origin` remote when they were. This check is not perfect, it is mostly meant to catch
* syntax-related typos.
*/
fun updateUrl(): GitUpdateUrlResult {
if (serverHostname.isEmpty())
return GitUpdateUrlResult.EmptyHostnameError
if (!serverPort.isDigitsOnly())
return GitUpdateUrlResult.NonNumericPortError
val previousUrl = url ?: ""
// Whether we need the leading ssh:// depends on the use of a custom port.
val hostnamePart = serverHostname.removePrefix("ssh://")
val newUrl = when (protocol) {
Protocol.Ssh -> {
val userPart = if (serverUser.isEmpty()) "" else "${serverUser.trimEnd('@')}@"
val portPart =
if (serverPort == "22" || serverPort.isEmpty()) "" else ":$serverPort"
if (portPart.isEmpty()) {
"$userPart$hostnamePart:$serverPath"
} else {
// Only absolute paths are supported with custom ports.
if (!serverPath.startsWith('/'))
return GitUpdateUrlResult.CustomPortRequiresAbsoluteUrlError
val pathPart = serverPath
// 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"
val pathPart = serverPath.trimStart('/', ':')
val urlWithFreeEntryScheme = "$hostnamePart$portPart/$pathPart"
val url = when {
urlWithFreeEntryScheme.startsWith("https://") -> urlWithFreeEntryScheme
urlWithFreeEntryScheme.startsWith("http://") -> urlWithFreeEntryScheme.replaceFirst("http", "https")
else -> "https://$urlWithFreeEntryScheme"
}
try {
if (URI(url).rawAuthority != null)
url
else
return GitUpdateUrlResult.GenericError
} catch (_: Exception) {
return GitUpdateUrlResult.GenericError
}
}
}
if (PasswordRepository.isInitialized)
PasswordRepository.addRemote("origin", newUrl, true)
// When the server changes, remote password and host key file should be deleted.
if (previousUrl.isNotEmpty() && newUrl != previousUrl) {
encryptedSettings.edit { remove(PreferenceKeys.HTTPS_PASSWORD) }
File("$filesDir/.host_key").delete()
}
url = newUrl
return GitUpdateUrlResult.Ok
}
/** /**
* Attempt to launch the requested Git operation. Depending on the configured auth, it may not * 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 * be possible to launch the operation immediately. In that case, this function may launch an
@@ -176,7 +64,7 @@ abstract class BaseGitActivity : AppCompatActivity() {
* @param operation The type of git operation to launch * @param operation The type of git operation to launch
*/ */
suspend fun launchGitOperation(operation: Int) { suspend fun launchGitOperation(operation: Int) {
if (url == null) { if (GitSettings.url == null) {
setResult(RESULT_CANCELED) setResult(RESULT_CANCELED)
finish() finish()
return return
@@ -185,7 +73,7 @@ abstract class BaseGitActivity : AppCompatActivity() {
// Before launching the operation with OpenKeychain auth, we need to issue several requests // 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, // 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. // we just need to keep calling it until it returns a completed ApiIdentity.
if (connectionMode == ConnectionMode.OpenKeychain && identity == null) { if (GitSettings.connectionMode == ConnectionMode.OpenKeychain && identity == null) {
// Lazy initialization of the IdentityBuilder // Lazy initialization of the IdentityBuilder
if (identityBuilder == null) { if (identityBuilder == null) {
identityBuilder = SshApiSessionFactory.IdentityBuilder(this) identityBuilder = SshApiSessionFactory.IdentityBuilder(this)
@@ -199,7 +87,7 @@ abstract class BaseGitActivity : AppCompatActivity() {
val localDir = requireNotNull(PasswordRepository.getRepositoryDirectory()) val localDir = requireNotNull(PasswordRepository.getRepositoryDirectory())
val op = when (operation) { val op = when (operation) {
REQUEST_CLONE, GitOperation.GET_SSH_KEY_FROM_CLONE -> CloneOperation(localDir, url!!, this) REQUEST_CLONE, GitOperation.GET_SSH_KEY_FROM_CLONE -> CloneOperation(localDir, GitSettings.url!!, this)
REQUEST_PULL -> PullOperation(localDir, this) REQUEST_PULL -> PullOperation(localDir, this)
REQUEST_PUSH -> PushOperation(localDir, this) REQUEST_PUSH -> PushOperation(localDir, this)
REQUEST_SYNC -> SyncOperation(localDir, this) REQUEST_SYNC -> SyncOperation(localDir, this)
@@ -213,7 +101,7 @@ abstract class BaseGitActivity : AppCompatActivity() {
return return
} }
} }
op.executeAfterAuthentication(connectionMode, serverUser, identity) op.executeAfterAuthentication(GitSettings.connectionMode, identity)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
MaterialAlertDialogBuilder(this).setMessage(e.message).show() MaterialAlertDialogBuilder(this).setMessage(e.message).show()

View File

@@ -7,15 +7,14 @@ package com.zeapo.pwdstore.git
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.util.Patterns import android.util.Patterns
import androidx.core.content.edit
import androidx.core.os.postDelayed import androidx.core.os.postDelayed
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.zeapo.pwdstore.R import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.databinding.ActivityGitConfigBinding import com.zeapo.pwdstore.databinding.ActivityGitConfigBinding
import com.zeapo.pwdstore.git.config.GitSettings
import com.zeapo.pwdstore.utils.PasswordRepository import com.zeapo.pwdstore.utils.PasswordRepository
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.viewBinding import com.zeapo.pwdstore.utils.viewBinding
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.eclipse.jgit.lib.Constants import org.eclipse.jgit.lib.Constants
@@ -29,16 +28,16 @@ class GitConfigActivity : BaseGitActivity() {
setContentView(binding.root) setContentView(binding.root)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
if (username.isEmpty()) if (GitSettings.authorName.isEmpty())
binding.gitUserName.requestFocus() binding.gitUserName.requestFocus()
else else
binding.gitUserName.setText(username) binding.gitUserName.setText(GitSettings.authorName)
binding.gitUserEmail.setText(email) binding.gitUserEmail.setText(GitSettings.authorEmail)
val repo = PasswordRepository.getRepository(PasswordRepository.getRepositoryDirectory()) val repo = PasswordRepository.getRepository(PasswordRepository.getRepositoryDirectory())
if (repo != null) { if (repo != null) {
try { try {
val objectId = repo.resolve(Constants.HEAD) val objectId = repo.resolve(Constants.HEAD)
val ref = repo.getRef("refs/heads/$branch") val ref = repo.getRef("refs/heads/${GitSettings.branch}")
val head = if (ref.objectId.equals(objectId)) ref.name else "DETACHED" val head = if (ref.objectId.equals(objectId)) ref.name else "DETACHED"
binding.gitCommitHash.text = String.format("%s (%s)", objectId.abbreviate(8).name(), head) binding.gitCommitHash.text = String.format("%s (%s)", objectId.abbreviate(8).name(), head)
@@ -60,12 +59,10 @@ class GitConfigActivity : BaseGitActivity() {
.setPositiveButton(getString(R.string.dialog_ok), null) .setPositiveButton(getString(R.string.dialog_ok), null)
.show() .show()
} else { } else {
settings.edit { GitSettings.authorEmail = email
putString(PreferenceKeys.GIT_CONFIG_USER_EMAIL, email) GitSettings.authorName = name
putString(PreferenceKeys.GIT_CONFIG_USER_NAME, name) PasswordRepository.setGitAuthorEmail(email)
} PasswordRepository.setGitAuthorName(name)
PasswordRepository.setUserName(name)
PasswordRepository.setUserEmail(email)
Snackbar.make(binding.root, getString(R.string.git_server_config_save_success), Snackbar.LENGTH_SHORT).show() Snackbar.make(binding.root, getString(R.string.git_server_config_save_success), Snackbar.LENGTH_SHORT).show()
Handler().postDelayed(500) { finish() } Handler().postDelayed(500) { finish() }
} }

View File

@@ -12,6 +12,7 @@ import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.R import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.UserPreference import com.zeapo.pwdstore.UserPreference
import com.zeapo.pwdstore.git.config.GitSettings
import com.zeapo.pwdstore.utils.PasswordRepository import com.zeapo.pwdstore.utils.PasswordRepository
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -57,7 +58,7 @@ open class GitOperationActivity : BaseGitActivity() {
* @param operation the operation to execute can be REQUEST_PULL or REQUEST_PUSH * @param operation the operation to execute can be REQUEST_PULL or REQUEST_PUSH
*/ */
private suspend fun syncRepository(operation: Int) { private suspend fun syncRepository(operation: Int) {
if (serverUser.isEmpty() || serverHostname.isEmpty() || url.isNullOrEmpty()) if (GitSettings.url.isNullOrEmpty())
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(this)
.setMessage(getString(R.string.set_information_dialog_text)) .setMessage(getString(R.string.set_information_dialog_text))
.setPositiveButton(getString(R.string.dialog_positive)) { _, _ -> .setPositiveButton(getString(R.string.dialog_positive)) { _, _ ->
@@ -72,7 +73,7 @@ open class GitOperationActivity : BaseGitActivity() {
.show() .show()
else { else {
// check that the remote origin is here, else add it // check that the remote origin is here, else add it
PasswordRepository.addRemote("origin", url!!, true) PasswordRepository.addRemote("origin", GitSettings.url!!, true)
launchGitOperation(operation) launchGitOperation(operation)
} }
} }

View File

@@ -7,18 +7,16 @@ package com.zeapo.pwdstore.git
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.view.View import android.view.View
import androidx.core.content.edit
import androidx.core.os.postDelayed import androidx.core.os.postDelayed
import androidx.core.widget.doOnTextChanged
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.zeapo.pwdstore.R import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.databinding.ActivityGitCloneBinding import com.zeapo.pwdstore.databinding.ActivityGitCloneBinding
import com.zeapo.pwdstore.git.config.ConnectionMode import com.zeapo.pwdstore.git.config.ConnectionMode
import com.zeapo.pwdstore.git.config.GitSettings
import com.zeapo.pwdstore.git.config.Protocol import com.zeapo.pwdstore.git.config.Protocol
import com.zeapo.pwdstore.utils.PasswordRepository import com.zeapo.pwdstore.utils.PasswordRepository
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.viewBinding import com.zeapo.pwdstore.utils.viewBinding
import java.io.IOException import java.io.IOException
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -40,22 +38,22 @@ class GitServerConfigActivity : BaseGitActivity() {
setContentView(binding.root) setContentView(binding.root)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
binding.cloneProtocolGroup.check(when (protocol) { binding.cloneProtocolGroup.check(when (GitSettings.protocol) {
Protocol.Ssh -> R.id.clone_protocol_ssh Protocol.Ssh -> R.id.clone_protocol_ssh
Protocol.Https -> R.id.clone_protocol_https Protocol.Https -> R.id.clone_protocol_https
}) })
binding.cloneProtocolGroup.addOnButtonCheckedListener { _, checkedId, checked -> binding.cloneProtocolGroup.addOnButtonCheckedListener { _, checkedId, checked ->
if (checked) { if (checked) {
when (checkedId) { when (checkedId) {
R.id.clone_protocol_https -> protocol = Protocol.Https R.id.clone_protocol_https -> GitSettings.protocol = Protocol.Https
R.id.clone_protocol_ssh -> protocol = Protocol.Ssh R.id.clone_protocol_ssh -> GitSettings.protocol = Protocol.Ssh
} }
updateConnectionModeToggleGroup() updateConnectionModeToggleGroup()
} }
} }
binding.connectionModeGroup.apply { binding.connectionModeGroup.apply {
when (connectionMode) { when (GitSettings.connectionMode) {
ConnectionMode.SshKey -> check(R.id.connection_mode_ssh_key) ConnectionMode.SshKey -> check(R.id.connection_mode_ssh_key)
ConnectionMode.Password -> check(R.id.connection_mode_password) ConnectionMode.Password -> check(R.id.connection_mode_password)
ConnectionMode.OpenKeychain -> check(R.id.connection_mode_open_keychain) ConnectionMode.OpenKeychain -> check(R.id.connection_mode_open_keychain)
@@ -63,82 +61,37 @@ class GitServerConfigActivity : BaseGitActivity() {
} }
addOnButtonCheckedListener { _, _, _ -> addOnButtonCheckedListener { _, _, _ ->
when (checkedButtonId) { when (checkedButtonId) {
R.id.connection_mode_ssh_key -> connectionMode = ConnectionMode.SshKey R.id.connection_mode_ssh_key -> GitSettings.connectionMode = ConnectionMode.SshKey
R.id.connection_mode_open_keychain -> connectionMode = ConnectionMode.OpenKeychain R.id.connection_mode_open_keychain -> GitSettings.connectionMode = ConnectionMode.OpenKeychain
R.id.connection_mode_password -> connectionMode = ConnectionMode.Password R.id.connection_mode_password -> GitSettings.connectionMode = ConnectionMode.Password
View.NO_ID -> connectionMode = ConnectionMode.None View.NO_ID -> GitSettings.connectionMode = ConnectionMode.None
} }
} }
} }
updateConnectionModeToggleGroup() updateConnectionModeToggleGroup()
binding.serverUrl.apply { binding.serverUrl.setText(GitSettings.url)
setText(serverHostname) binding.serverBranch.setText(GitSettings.branch)
doOnTextChanged { text, _, _, _ ->
serverHostname = text.toString().trim()
}
}
binding.serverPort.apply {
setText(serverPort)
doOnTextChanged { text, _, _, _ ->
serverPort = text.toString().trim()
}
}
binding.serverUser.apply {
if (serverUser.isEmpty()) {
requestFocus()
} else {
setText(serverUser)
}
doOnTextChanged { text, _, _, _ ->
serverUser = text.toString().trim()
}
}
binding.serverPath.apply {
setText(serverPath)
doOnTextChanged { text, _, _, _ ->
serverPath = text.toString().trim()
}
}
binding.serverBranch.apply {
setText(branch)
doOnTextChanged { text, _, _, _ ->
branch = text.toString().trim()
}
}
binding.saveButton.setOnClickListener { binding.saveButton.setOnClickListener {
if (isClone && PasswordRepository.getRepository(null) == null) if (isClone && PasswordRepository.getRepository(null) == null)
PasswordRepository.initialize() PasswordRepository.initialize()
when (val result = updateUrl()) { GitSettings.branch = binding.serverBranch.text.toString().trim()
GitUpdateUrlResult.Ok -> { if (GitSettings.updateUrlIfValid(binding.serverUrl.text.toString().trim())) {
settings.edit { if (!isClone) {
putString(PreferenceKeys.GIT_REMOTE_PROTOCOL, protocol.pref) Snackbar.make(binding.root, getString(R.string.git_server_config_save_success), Snackbar.LENGTH_SHORT).show()
putString(PreferenceKeys.GIT_REMOTE_AUTH, connectionMode.pref) Handler().postDelayed(500) { finish() }
putString(PreferenceKeys.GIT_REMOTE_SERVER, serverHostname) } else {
putString(PreferenceKeys.GIT_REMOTE_PORT, serverPort) cloneRepository()
putString(PreferenceKeys.GIT_REMOTE_USERNAME, serverUser)
putString(PreferenceKeys.GIT_REMOTE_LOCATION, serverPath)
putString(PreferenceKeys.GIT_BRANCH_NAME, branch)
}
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_error_prefix, getString(result.textRes)), Snackbar.LENGTH_LONG).show() } else {
Snackbar.make(binding.root, getString(R.string.git_server_config_save_error), Snackbar.LENGTH_LONG).show()
} }
} }
} }
private fun updateConnectionModeToggleGroup() { private fun updateConnectionModeToggleGroup() {
if (protocol == Protocol.Ssh) { if (GitSettings.protocol == Protocol.Ssh) {
// Reset connection mode to SSH key if the current value (none) is not valid for SSH // Reset connection mode to SSH key if the current value (none) is not valid for SSH
if (binding.connectionModeGroup.checkedButtonIds.isEmpty()) if (binding.connectionModeGroup.checkedButtonIds.isEmpty())
binding.connectionModeGroup.check(R.id.connection_mode_ssh_key) binding.connectionModeGroup.check(R.id.connection_mode_ssh_key)
@@ -150,7 +103,7 @@ class GitServerConfigActivity : BaseGitActivity() {
// Reset connection mode to password if the current value is not valid for HTTPS // Reset connection mode to password if the current value is not valid for HTTPS
// Important note: This has to happen before disabling the other toggle buttons or they // Important note: This has to happen before disabling the other toggle buttons or they
// won't uncheck. // won't uncheck.
if (connectionMode !in listOf(ConnectionMode.None, ConnectionMode.Password)) if (GitSettings.connectionMode !in listOf(ConnectionMode.None, ConnectionMode.Password))
binding.connectionModeGroup.check(R.id.connection_mode_password) binding.connectionModeGroup.check(R.id.connection_mode_password)
binding.connectionModeSshKey.isEnabled = false binding.connectionModeSshKey.isEnabled = false
binding.connectionModeOpenKeychain.isEnabled = false binding.connectionModeOpenKeychain.isEnabled = false

View File

@@ -1,22 +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.config
enum class ConnectionMode(val pref: String) {
SshKey("ssh-key"),
Password("username/password"),
OpenKeychain("OpenKeychain"),
None("None"),
;
companion object {
private val map = values().associateBy(ConnectionMode::pref)
fun fromString(type: String?): ConnectionMode {
return map[type ?: return SshKey]
?: throw IllegalArgumentException("$type is not a valid ConnectionMode")
}
}
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
package com.zeapo.pwdstore.git.config
import androidx.core.content.edit
import com.zeapo.pwdstore.Application
import com.zeapo.pwdstore.utils.PasswordRepository
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.getEncryptedPrefs
import com.zeapo.pwdstore.utils.getString
import com.zeapo.pwdstore.utils.sharedPrefs
import java.io.File
import org.eclipse.jgit.transport.URIish
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")
}
}
}
enum class ConnectionMode(val pref: String) {
SshKey("ssh-key"),
Password("username/password"),
OpenKeychain("OpenKeychain"),
None("None"),
;
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")
}
}
}
object GitSettings {
private const val DEFAULT_BRANCH = "master"
private val settings by lazy { Application.instance.sharedPrefs }
private val encryptedSettings by lazy { Application.instance.getEncryptedPrefs("git_operation") }
var protocol
get() = Protocol.fromString(settings.getString(PreferenceKeys.GIT_REMOTE_PROTOCOL))
set(value) {
settings.edit {
putString(PreferenceKeys.GIT_REMOTE_PROTOCOL, value.pref)
}
}
var connectionMode
get() = ConnectionMode.fromString(settings.getString(PreferenceKeys.GIT_REMOTE_AUTH))
set(value) {
settings.edit {
putString(PreferenceKeys.GIT_REMOTE_AUTH, value.pref)
}
}
var url
get() = settings.getString(PreferenceKeys.GIT_REMOTE_URL)
private set(value) {
require(value != null)
settings.edit {
putString(PreferenceKeys.GIT_REMOTE_URL, value)
}
if (PasswordRepository.isInitialized)
PasswordRepository.addRemote("origin", value, true)
// When the server changes, remote password and host key file should be deleted.
encryptedSettings.edit { remove(PreferenceKeys.HTTPS_PASSWORD) }
File("${Application.instance.filesDir}/.host_key").delete()
}
var authorName
get() = settings.getString(PreferenceKeys.GIT_CONFIG_AUTHOR_NAME) ?: ""
set(value) {
settings.edit {
putString(PreferenceKeys.GIT_CONFIG_AUTHOR_NAME, value)
}
}
var authorEmail
get() = settings.getString(PreferenceKeys.GIT_CONFIG_AUTHOR_EMAIL) ?: ""
set(value) {
settings.edit {
putString(PreferenceKeys.GIT_CONFIG_AUTHOR_EMAIL, value)
}
}
var branch
get() = settings.getString(PreferenceKeys.GIT_BRANCH_NAME) ?: DEFAULT_BRANCH
set(value) {
settings.edit {
putString(PreferenceKeys.GIT_BRANCH_NAME, value)
}
}
fun updateUrlIfValid(newUrl: String): Boolean {
try {
URIish(newUrl)
} catch (_: Exception) {
return false
}
if (newUrl != url)
url = newUrl
return true
}
}

View File

@@ -1,20 +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.config
enum class Protocol(val pref: String) {
Ssh("ssh://"),
Https("https://"),
;
companion object {
private val map = values().associateBy(Protocol::pref)
fun fromString(type: String?): Protocol {
return map[type ?: return Ssh]
?: throw IllegalArgumentException("$type is not a valid Protocol")
}
}
}

View File

@@ -18,18 +18,12 @@ import com.jcraft.jsch.Identity;
import com.jcraft.jsch.JSch; import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException; import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session; import com.jcraft.jsch.Session;
import com.jcraft.jsch.UserInfo;
import com.zeapo.pwdstore.R; import com.zeapo.pwdstore.R;
import com.zeapo.pwdstore.git.BaseGitActivity; import com.zeapo.pwdstore.git.BaseGitActivity;
import com.zeapo.pwdstore.utils.PreferenceKeys; import com.zeapo.pwdstore.utils.PreferenceKeys;
import org.eclipse.jgit.errors.UnsupportedCredentialItem;
import org.eclipse.jgit.transport.CredentialItem;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.CredentialsProviderUserInfo;
import org.eclipse.jgit.transport.JschConfigSessionFactory; import org.eclipse.jgit.transport.JschConfigSessionFactory;
import org.eclipse.jgit.transport.OpenSshConfig; import org.eclipse.jgit.transport.OpenSshConfig;
import org.eclipse.jgit.transport.URIish;
import org.eclipse.jgit.util.Base64; import org.eclipse.jgit.util.Base64;
import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FS;
import org.openintents.ssh.authentication.ISshAuthenticationService; import org.openintents.ssh.authentication.ISshAuthenticationService;
@@ -52,11 +46,9 @@ public class SshApiSessionFactory extends JschConfigSessionFactory {
*/ */
public static final int POST_SIGNATURE = 301; public static final int POST_SIGNATURE = 301;
private final String username;
private final Identity identity; private final Identity identity;
public SshApiSessionFactory(String username, Identity identity) { public SshApiSessionFactory(Identity identity) {
this.username = username;
this.identity = identity; this.identity = identity;
} }
@@ -74,32 +66,6 @@ public class SshApiSessionFactory extends JschConfigSessionFactory {
protected void configure(@NonNull OpenSshConfig.Host hc, Session session) { protected void configure(@NonNull OpenSshConfig.Host hc, Session session) {
session.setConfig("StrictHostKeyChecking", "no"); session.setConfig("StrictHostKeyChecking", "no");
session.setConfig("PreferredAuthentications", "publickey"); session.setConfig("PreferredAuthentications", "publickey");
CredentialsProvider provider =
new CredentialsProvider() {
@Override
public boolean isInteractive() {
return false;
}
@Override
public boolean supports(CredentialItem... items) {
return true;
}
@Override
public boolean get(URIish uri, CredentialItem... items)
throws UnsupportedCredentialItem {
for (CredentialItem item : items) {
if (item instanceof CredentialItem.Username) {
((CredentialItem.Username) item).setValue(username);
}
}
return true;
}
};
UserInfo userInfo = new CredentialsProviderUserInfo(session, provider);
session.setUserInfo(userInfo);
} }
/** /**

View File

@@ -95,10 +95,10 @@ abstract class InteractivePasswordFinder : PasswordFinder {
final override fun shouldRetry(resource: Resource<*>?) = true final override fun shouldRetry(resource: Resource<*>?) = true
} }
class SshjSessionFactory(private val username: String, private val authData: SshAuthData, private val hostKeyFile: File) : SshSessionFactory() { class SshjSessionFactory(private val authData: SshAuthData, private val hostKeyFile: File) : SshSessionFactory() {
override fun getSession(uri: URIish, credentialsProvider: CredentialsProvider?, fs: FS?, tms: Int): RemoteSession { override fun getSession(uri: URIish, credentialsProvider: CredentialsProvider?, fs: FS?, tms: Int): RemoteSession {
return SshjSession(uri, username, authData, hostKeyFile).connect() return SshjSession(uri, uri.user, authData, hostKeyFile).connect()
} }
fun clearCredentials() { fun clearCredentials() {

View File

@@ -15,6 +15,7 @@ import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.UserPreference import com.zeapo.pwdstore.UserPreference
import com.zeapo.pwdstore.git.ErrorMessages import com.zeapo.pwdstore.git.ErrorMessages
import com.zeapo.pwdstore.git.config.ConnectionMode import com.zeapo.pwdstore.git.config.ConnectionMode
import com.zeapo.pwdstore.git.config.GitSettings
import com.zeapo.pwdstore.git.config.InteractivePasswordFinder import com.zeapo.pwdstore.git.config.InteractivePasswordFinder
import com.zeapo.pwdstore.git.config.SshApiSessionFactory import com.zeapo.pwdstore.git.config.SshApiSessionFactory
import com.zeapo.pwdstore.git.config.SshAuthData import com.zeapo.pwdstore.git.config.SshAuthData
@@ -47,18 +48,15 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: Fragment
private val hostKeyFile = callingActivity.filesDir.resolve(".host_key") private val hostKeyFile = callingActivity.filesDir.resolve(".host_key")
protected val repository = PasswordRepository.getRepository(gitDir) protected val repository = PasswordRepository.getRepository(gitDir)
protected val git = Git(repository) protected val git = Git(repository)
protected val remoteBranch = PreferenceManager protected val remoteBranch = GitSettings.branch
.getDefaultSharedPreferences(callingActivity.applicationContext)
.getString(PreferenceKeys.GIT_BRANCH_NAME, "master")
private class PasswordFinderCredentialsProvider(private val username: String, private val passwordFinder: PasswordFinder) : CredentialsProvider() { private class PasswordFinderCredentialsProvider(private val passwordFinder: PasswordFinder) : CredentialsProvider() {
override fun isInteractive() = true override fun isInteractive() = true
override fun get(uri: URIish?, vararg items: CredentialItem): Boolean { override fun get(uri: URIish?, vararg items: CredentialItem): Boolean {
for (item in items) { for (item in items) {
when (item) { when (item) {
is CredentialItem.Username -> item.value = username
is CredentialItem.Password -> item.value = passwordFinder.reqPassword(null) is CredentialItem.Password -> item.value = passwordFinder.reqPassword(null)
else -> UnsupportedCredentialItem(uri, item.javaClass.name) else -> UnsupportedCredentialItem(uri, item.javaClass.name)
} }
@@ -67,26 +65,26 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: Fragment
} }
override fun supports(vararg items: CredentialItem) = items.all { override fun supports(vararg items: CredentialItem) = items.all {
it is CredentialItem.Username || it is CredentialItem.Password it is CredentialItem.Password
} }
} }
private fun withPasswordAuthentication(username: String, passwordFinder: InteractivePasswordFinder): GitOperation { private fun withPasswordAuthentication(passwordFinder: InteractivePasswordFinder): GitOperation {
val sessionFactory = SshjSessionFactory(username, SshAuthData.Password(passwordFinder), hostKeyFile) val sessionFactory = SshjSessionFactory(SshAuthData.Password(passwordFinder), hostKeyFile)
SshSessionFactory.setInstance(sessionFactory) SshSessionFactory.setInstance(sessionFactory)
this.provider = PasswordFinderCredentialsProvider(username, passwordFinder) this.provider = PasswordFinderCredentialsProvider(passwordFinder)
return this return this
} }
private fun withPublicKeyAuthentication(username: String, passphraseFinder: InteractivePasswordFinder): GitOperation { private fun withPublicKeyAuthentication(passphraseFinder: InteractivePasswordFinder): GitOperation {
val sessionFactory = SshjSessionFactory(username, SshAuthData.PublicKeyFile(sshKeyFile, passphraseFinder), hostKeyFile) val sessionFactory = SshjSessionFactory(SshAuthData.PublicKeyFile(sshKeyFile, passphraseFinder), hostKeyFile)
SshSessionFactory.setInstance(sessionFactory) SshSessionFactory.setInstance(sessionFactory)
this.provider = null this.provider = null
return this return this
} }
private fun withOpenKeychainAuthentication(username: String, identity: SshApiSessionFactory.ApiIdentity?): GitOperation { private fun withOpenKeychainAuthentication(identity: SshApiSessionFactory.ApiIdentity?): GitOperation {
SshSessionFactory.setInstance(SshApiSessionFactory(username, identity)) SshSessionFactory.setInstance(SshApiSessionFactory(identity))
this.provider = null this.provider = null
return this return this
} }
@@ -117,7 +115,6 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: Fragment
suspend fun executeAfterAuthentication( suspend fun executeAfterAuthentication(
connectionMode: ConnectionMode, connectionMode: ConnectionMode,
username: String,
identity: SshApiSessionFactory.ApiIdentity? identity: SshApiSessionFactory.ApiIdentity?
) { ) {
when (connectionMode) { when (connectionMode) {
@@ -136,12 +133,12 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: Fragment
callingActivity.finish() callingActivity.finish()
}.show() }.show()
} else { } else {
withPublicKeyAuthentication(username, CredentialFinder(callingActivity, withPublicKeyAuthentication(
connectionMode)).execute() CredentialFinder(callingActivity, connectionMode)).execute()
} }
ConnectionMode.OpenKeychain -> withOpenKeychainAuthentication(username, identity).execute() ConnectionMode.OpenKeychain -> withOpenKeychainAuthentication(identity).execute()
ConnectionMode.Password -> withPasswordAuthentication( ConnectionMode.Password -> withPasswordAuthentication(
username, CredentialFinder(callingActivity, connectionMode)).execute() CredentialFinder(callingActivity, connectionMode)).execute()
ConnectionMode.None -> execute() ConnectionMode.None -> execute()
} }
} }

View File

@@ -5,9 +5,9 @@
package com.zeapo.pwdstore.pwgenxkpwd package com.zeapo.pwdstore.pwgenxkpwd
import android.content.Context import android.content.Context
import androidx.preference.PreferenceManager
import com.zeapo.pwdstore.R import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.utils.PreferenceKeys import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.getString
import com.zeapo.pwdstore.utils.sharedPrefs import com.zeapo.pwdstore.utils.sharedPrefs
import java.io.File import java.io.File
@@ -17,7 +17,7 @@ class XkpwdDictionary(context: Context) {
init { init {
val prefs = context.sharedPrefs val prefs = context.sharedPrefs
val uri = prefs.getString(PreferenceKeys.PREF_KEY_CUSTOM_DICT, "")!! val uri = prefs.getString(PreferenceKeys.PREF_KEY_CUSTOM_DICT) ?: ""
val customDictFile = File(context.filesDir, XKPWD_CUSTOM_DICT_FILE) val customDictFile = File(context.filesDir, XKPWD_CUSTOM_DICT_FILE)
val lines = if (prefs.getBoolean(PreferenceKeys.PREF_KEY_IS_CUSTOM_DICT, false) && val lines = if (prefs.getBoolean(PreferenceKeys.PREF_KEY_IS_CUSTOM_DICT, false) &&

View File

@@ -12,7 +12,6 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.jcraft.jsch.JSch import com.jcraft.jsch.JSch
import com.jcraft.jsch.KeyPair import com.jcraft.jsch.KeyPair

View File

@@ -10,7 +10,6 @@ import android.view.MotionEvent
import android.view.View import android.view.View
import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.AppCompatImageView
import androidx.appcompat.widget.AppCompatTextView import androidx.appcompat.widget.AppCompatTextView
import androidx.preference.PreferenceManager
import androidx.recyclerview.selection.ItemDetailsLookup import androidx.recyclerview.selection.ItemDetailsLookup
import androidx.recyclerview.selection.Selection import androidx.recyclerview.selection.Selection
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView

View File

@@ -101,6 +101,8 @@ fun Context.getEncryptedPrefs(fileName: String): SharedPreferences {
val Context.sharedPrefs: SharedPreferences val Context.sharedPrefs: SharedPreferences
get() = PreferenceManager.getDefaultSharedPreferences(applicationContext) get() = PreferenceManager.getDefaultSharedPreferences(applicationContext)
fun SharedPreferences.getString(key: String): String? = getString(key, null)
suspend fun FragmentActivity.commitChange( suspend fun FragmentActivity.commitChange(
message: String, message: String,
finishWithResultOnEnd: Intent? = null, finishWithResultOnEnd: Intent? = null,

View File

@@ -39,8 +39,7 @@ open class PasswordRepository protected constructor() {
@JvmStatic @JvmStatic
fun getSortOrder(settings: SharedPreferences): PasswordSortOrder { fun getSortOrder(settings: SharedPreferences): PasswordSortOrder {
return valueOf(settings.getString(PreferenceKeys.SORT_ORDER, null) return valueOf(settings.getString(PreferenceKeys.SORT_ORDER) ?: FOLDER_FIRST.name)
?: FOLDER_FIRST.name)
} }
} }
} }
@@ -155,7 +154,7 @@ open class PasswordRepository protected constructor() {
@JvmStatic @JvmStatic
fun getRepositoryDirectory(): File { fun getRepositoryDirectory(): File {
return if (settings.getBoolean(PreferenceKeys.GIT_EXTERNAL, false)) { return if (settings.getBoolean(PreferenceKeys.GIT_EXTERNAL, false)) {
val externalRepo = settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO, null) val externalRepo = settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO)
if (externalRepo != null) if (externalRepo != null)
File(externalRepo) File(externalRepo)
else else
@@ -239,7 +238,7 @@ open class PasswordRepository protected constructor() {
* @param username username * @param username username
*/ */
@JvmStatic @JvmStatic
fun setUserName(username: String) { fun setGitAuthorName(username: String) {
setStringConfig("user", null, "name", username) setStringConfig("user", null, "name", username)
} }
@@ -249,7 +248,7 @@ open class PasswordRepository protected constructor() {
* @param email email * @param email email
*/ */
@JvmStatic @JvmStatic
fun setUserEmail(email: String) { fun setGitAuthorEmail(email: String) {
setStringConfig("user", null, "email", email) setStringConfig("user", null, "email", email)
} }

View File

@@ -23,16 +23,21 @@ object PreferenceKeys {
const val FILTER_RECURSIVELY = "filter_recursively" const val FILTER_RECURSIVELY = "filter_recursively"
const val GENERAL_SHOW_TIME = "general_show_time" const val GENERAL_SHOW_TIME = "general_show_time"
const val GIT_CONFIG = "git_config" const val GIT_CONFIG = "git_config"
const val GIT_CONFIG_USER_EMAIL = "git_config_user_email" const val GIT_CONFIG_AUTHOR_EMAIL = "git_config_user_email"
const val GIT_CONFIG_USER_NAME = "git_config_user_name" const val GIT_CONFIG_AUTHOR_NAME = "git_config_user_name"
const val GIT_EXTERNAL = "git_external" const val GIT_EXTERNAL = "git_external"
const val GIT_EXTERNAL_REPO = "git_external_repo" const val GIT_EXTERNAL_REPO = "git_external_repo"
const val GIT_REMOTE_AUTH = "git_remote_auth" const val GIT_REMOTE_AUTH = "git_remote_auth"
@Deprecated("Use GIT_REMOTE_URL instead")
const val GIT_REMOTE_LOCATION = "git_remote_location" const val GIT_REMOTE_LOCATION = "git_remote_location"
@Deprecated("Use GIT_REMOTE_URL instead")
const val GIT_REMOTE_PORT = "git_remote_port" const val GIT_REMOTE_PORT = "git_remote_port"
const val GIT_REMOTE_PROTOCOL = "git_remote_protocol" const val GIT_REMOTE_PROTOCOL = "git_remote_protocol"
const val GIT_DELETE_REPO = "git_delete_repo" const val GIT_DELETE_REPO = "git_delete_repo"
@Deprecated("Use GIT_REMOTE_URL instead")
const val GIT_REMOTE_SERVER = "git_remote_server" const val GIT_REMOTE_SERVER = "git_remote_server"
const val GIT_REMOTE_URL = "git_remote_url"
@Deprecated("Use GIT_REMOTE_URL instead")
const val GIT_REMOTE_USERNAME = "git_remote_username" const val GIT_REMOTE_USERNAME = "git_remote_username"
const val GIT_SERVER_INFO = "git_server_info" const val GIT_SERVER_INFO = "git_server_info"
const val GIT_BRANCH_NAME = "git_branch" const val GIT_BRANCH_NAME = "git_branch"

View File

@@ -65,81 +65,21 @@
android:text="@string/clone_protocol_https" /> android:text="@string/clone_protocol_https" />
</com.google.android.material.button.MaterialButtonToggleGroup> </com.google.android.material.button.MaterialButtonToggleGroup>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/server_user_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:hint="@string/server_user"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
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:imeOptions="actionNext"
android:inputType="textWebEmailAddress"
android:nextFocusForward="@id/server_url" />
</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:id="@+id/label_server_url"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp" android:layout_margin="8dp"
android:hint="@string/server_url" android:hint="@string/server_url"
app:layout_constraintEnd_toStartOf="@id/label_server_port" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/server_user_layout"> app:layout_constraintTop_toBottomOf="@id/clone_protocol_group">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/server_url" android:id="@+id/server_url"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:imeOptions="actionNext" android:imeOptions="actionNext"
android:inputType="textWebEmailAddress"
android:nextFocusForward="@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_constraintDimensionRatio="1:0.8"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/label_server_url"
app:layout_constraintTop_toBottomOf="@id/server_user_layout">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/server_port"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionNext"
android:inputType="number"
android:nextFocusForward="@id/server_path" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/label_server_path"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:hint="@string/server_path"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_server_url">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/server_path"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionNext"
android:nextFocusForward="@id/server_branch" android:nextFocusForward="@id/server_branch"
android:inputType="textWebEmailAddress" /> android:inputType="textWebEmailAddress" />
@@ -150,10 +90,10 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp" android:layout_margin="8dp"
android:hint="Branch" android:hint="@string/server_branch"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_server_path"> app:layout_constraintTop_toBottomOf="@id/label_server_url">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/server_branch" android:id="@+id/server_branch"

View File

@@ -29,10 +29,6 @@
<string name="initialize">إستخدام مجلد محلي</string> <string name="initialize">إستخدام مجلد محلي</string>
<string name="server_name">الخادوم</string> <string name="server_name">الخادوم</string>
<string name="server_protocol">البروتوكول</string> <string name="server_protocol">البروتوكول</string>
<string name="server_url">عنوان الخادوم</string>
<string name="server_port_hint">22</string>
<string name="server_path">مسار المستودع</string>
<string name="server_user">إسم المستخدم</string>
<string name="connection_mode">نوع المصادقة</string> <string name="connection_mode">نوع المصادقة</string>

View File

@@ -57,10 +57,6 @@
<string name="external_repository_dialog_title">Vyberte kam ukládat hesla</string> <string name="external_repository_dialog_title">Vyberte kam ukládat hesla</string>
<string name="server_name">Server</string> <string name="server_name">Server</string>
<string name="server_protocol">Protokol</string> <string name="server_protocol">Protokol</string>
<string name="server_url">URL serveru</string>
<string name="server_port_hint">22</string>
<string name="server_path">Cesta k repozitáři</string>
<string name="server_user">Jméno</string>
<string name="connection_mode">Mód ověření</string> <string name="connection_mode">Mód ověření</string>

View File

@@ -40,10 +40,6 @@
<string name="initialize">Nutze lokalen Ordner</string> <string name="initialize">Nutze lokalen Ordner</string>
<string name="server_name">Server</string> <string name="server_name">Server</string>
<string name="server_protocol">Protokoll</string> <string name="server_protocol">Protokoll</string>
<string name="server_url">Server URL</string>
<string name="server_port_hint">22</string>
<string name="server_path">Repo-Pfad</string>
<string name="server_user">Nutzername</string>
<string name="connection_mode">Authentifizierungsmethode</string> <string name="connection_mode">Authentifizierungsmethode</string>

View File

@@ -54,10 +54,6 @@
<string name="server_name">Servidor</string> <string name="server_name">Servidor</string>
<string name="server_protocol">Protocolo</string> <string name="server_protocol">Protocolo</string>
<string name="server_url">URL de servidor</string>
<string name="server_port_hint">22</string>
<string name="server_path">Ruta del repositorio</string>
<string name="server_user">Nombre de usuario</string>
<string name="connection_mode">Modo de autenticación</string> <string name="connection_mode">Modo de autenticación</string>

View File

@@ -60,10 +60,6 @@
<string name="server_name">Serveur</string> <string name="server_name">Serveur</string>
<string name="server_protocol">Protocole</string> <string name="server_protocol">Protocole</string>
<string name="server_url">URL du serveur</string>
<string name="server_port_hint">22</string>
<string name="server_path">Chemin du dépôt</string>
<string name="server_user">Nom d\'utilisateur</string>
<string name="connection_mode">Méthode d\'authentification</string> <string name="connection_mode">Méthode d\'authentification</string>

View File

@@ -41,10 +41,6 @@
<string name="initialize">ローカルディレクトリーを使用する</string> <string name="initialize">ローカルディレクトリーを使用する</string>
<string name="server_name">サーバー</string> <string name="server_name">サーバー</string>
<string name="server_protocol">プロトコル</string> <string name="server_protocol">プロトコル</string>
<string name="server_url">サーバー URL</string>
<string name="server_port_hint">22</string>
<string name="server_path">リポジトリのパス</string>
<string name="server_user">ユーザー名</string>
<string name="connection_mode">認証モード</string> <string name="connection_mode">認証モード</string>

View File

@@ -68,10 +68,6 @@
<string name="external_repository_dialog_text">Você deve selecionar um diretório onde armazenar suas senhas. Se você deseja armazenar suas senhas dentro do armazenamento oculto do aplicativo, cancele esta caixa de diálogo e desative a opção \"Repositório Externo\".</string> <string name="external_repository_dialog_text">Você deve selecionar um diretório onde armazenar suas senhas. Se você deseja armazenar suas senhas dentro do armazenamento oculto do aplicativo, cancele esta caixa de diálogo e desative a opção \"Repositório Externo\".</string>
<string name="server_name">Servidor</string> <string name="server_name">Servidor</string>
<string name="server_protocol">Protocolo</string> <string name="server_protocol">Protocolo</string>
<string name="server_url">URL do servidor</string>
<string name="server_port_hint">Porta</string>
<string name="server_path">Caminho do repositório</string>
<string name="server_user">Usuário</string>
<string name="connection_mode">Modo de autenticação</string> <string name="connection_mode">Modo de autenticação</string>
<!-- Git Config fragment --> <!-- Git Config fragment -->
<string name="git_user_name_hint">Usuário</string> <string name="git_user_name_hint">Usuário</string>
@@ -295,7 +291,6 @@
<string name="connection_mode_ssh_key">Chave SSH</string> <string name="connection_mode_ssh_key">Chave SSH</string>
<string name="connection_mode_basic_authentication">Senha</string> <string name="connection_mode_basic_authentication">Senha</string>
<string name="git_server_config_save_success">Configuração salva com sucesso</string> <string name="git_server_config_save_success">Configuração salva com sucesso</string>
<string name="git_server_config_save_error_prefix">Erro de configuração: %s</string>
<string name="git_config_error_hostname_empty">hostname vazio</string> <string name="git_config_error_hostname_empty">hostname vazio</string>
<string name="git_config_error_generic">por favor, verifique suas configurações e tente novamente</string> <string name="git_config_error_generic">por favor, verifique suas configurações e tente novamente</string>
<string name="git_config_error_nonnumeric_port">porta deve ser numérica</string> <string name="git_config_error_nonnumeric_port">porta deve ser numérica</string>

View File

@@ -62,10 +62,6 @@
<string name="server_name">Сервер</string> <string name="server_name">Сервер</string>
<string name="server_protocol">Протокол</string> <string name="server_protocol">Протокол</string>
<string name="server_url">URL сервера</string>
<string name="server_port_hint">22</string>
<string name="server_path">Путь к репозиторию</string>
<string name="server_user">Имя пользователя</string>
<string name="connection_mode">Тип авторизации</string> <string name="connection_mode">Тип авторизации</string>

View File

@@ -41,10 +41,6 @@
<string name="initialize">使用本地目录</string> <string name="initialize">使用本地目录</string>
<string name="server_name">服务器</string> <string name="server_name">服务器</string>
<string name="server_protocol">接口</string> <string name="server_protocol">接口</string>
<string name="server_url">服务器 URL</string>
<string name="server_port_hint">22</string>
<string name="server_path">Repo 路径</string>
<string name="server_user">用户名</string>
<string name="connection_mode">认证模式</string> <string name="connection_mode">认证模式</string>

View File

@@ -38,10 +38,6 @@
<string name="initialize">使用本機目錄</string> <string name="initialize">使用本機目錄</string>
<string name="server_name">伺服器</string> <string name="server_name">伺服器</string>
<string name="server_protocol">port</string> <string name="server_protocol">port</string>
<string name="server_url">伺服器 URL</string>
<string name="server_port_hint">22</string>
<string name="server_path">Repo 路徑</string>
<string name="server_user">使用者名稱</string>
<string name="connection_mode">認證模式</string> <string name="connection_mode">認證模式</string>

View File

@@ -83,10 +83,8 @@
<string name="server_name">Server</string> <string name="server_name">Server</string>
<string name="server_protocol">Protocol</string> <string name="server_protocol">Protocol</string>
<string name="server_url">Server URL</string> <string name="server_url">Repository URL</string>
<string name="server_port_hint">Port</string> <string name="server_branch">Branch</string>
<string name="server_path">Repo path</string>
<string name="server_user">Username</string>
<string name="connection_mode">Authentication Mode</string> <string name="connection_mode">Authentication Mode</string>
@@ -326,7 +324,7 @@
<string name="connection_mode_basic_authentication">Password</string> <string name="connection_mode_basic_authentication">Password</string>
<string name="connection_mode_openkeychain" translatable="false">OpenKeychain</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_success">Successfully saved configuration</string>
<string name="git_server_config_save_error_prefix">Configuration error: %s</string> <string name="git_server_config_save_error">The provided repository URL is not valid</string>
<string name="git_config_error_hostname_empty">empty hostname</string> <string name="git_config_error_hostname_empty">empty hostname</string>
<string name="git_config_error_generic">please verify your settings and try again</string> <string name="git_config_error_generic">please verify your settings and try again</string>
<string name="git_config_error_nonnumeric_port">port must be numeric</string> <string name="git_config_error_nonnumeric_port">port must be numeric</string>