diff options
Diffstat (limited to 'app/src')
33 files changed, 592 insertions, 109 deletions
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3b0781f4..6cad066e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -36,6 +36,9 @@ android:name=".ui.onboarding.activity.OnboardingActivity" android:configChanges="orientation|screenSize" /> + <activity android:name=".ui.proxy.ProxySelectorActivity" + android:windowSoftInputMode="adjustResize" /> + <activity android:name=".LaunchActivity" android:configChanges="orientation|screenSize" diff --git a/app/src/main/java/com/zeapo/pwdstore/Application.kt b/app/src/main/java/com/zeapo/pwdstore/Application.kt index 91e47793..3f3963d0 100644 --- a/app/src/main/java/com/zeapo/pwdstore/Application.kt +++ b/app/src/main/java/com/zeapo/pwdstore/Application.kt @@ -14,27 +14,31 @@ import com.github.ajalt.timberkt.Timber.DebugTree import com.github.ajalt.timberkt.Timber.plant import com.zeapo.pwdstore.git.sshj.setUpBouncyCastleForSshj import com.zeapo.pwdstore.utils.PreferenceKeys +import com.zeapo.pwdstore.utils.ProxyUtils import com.zeapo.pwdstore.utils.getString import com.zeapo.pwdstore.utils.sharedPrefs @Suppress("Unused") class Application : android.app.Application(), SharedPreferences.OnSharedPreferenceChangeListener { + private val prefs by lazy { sharedPrefs } + override fun onCreate() { super.onCreate() instance = this if (BuildConfig.ENABLE_DEBUG_FEATURES || - sharedPrefs.getBoolean(PreferenceKeys.ENABLE_DEBUG_LOGGING, false)) { + prefs.getBoolean(PreferenceKeys.ENABLE_DEBUG_LOGGING, false)) { plant(DebugTree()) } - sharedPrefs.registerOnSharedPreferenceChangeListener(this) + prefs.registerOnSharedPreferenceChangeListener(this) setNightMode() setUpBouncyCastleForSshj() runMigrations(applicationContext) + ProxyUtils.setDefaultProxy() } override fun onTerminate() { - sharedPrefs.unregisterOnSharedPreferenceChangeListener(this) + prefs.unregisterOnSharedPreferenceChangeListener(this) super.onTerminate() } @@ -45,7 +49,7 @@ class Application : android.app.Application(), SharedPreferences.OnSharedPrefere } private fun setNightMode() { - AppCompatDelegate.setDefaultNightMode(when (sharedPrefs.getString(PreferenceKeys.APP_THEME) + AppCompatDelegate.setDefaultNightMode(when (prefs.getString(PreferenceKeys.APP_THEME) ?: getString(R.string.app_theme_def)) { "light" -> MODE_NIGHT_NO "dark" -> MODE_NIGHT_YES diff --git a/app/src/main/java/com/zeapo/pwdstore/ClipboardService.kt b/app/src/main/java/com/zeapo/pwdstore/ClipboardService.kt index 46c4ecd3..0f27e5ef 100644 --- a/app/src/main/java/com/zeapo/pwdstore/ClipboardService.kt +++ b/app/src/main/java/com/zeapo/pwdstore/ClipboardService.kt @@ -10,7 +10,6 @@ import android.app.PendingIntent import android.app.Service import android.content.ClipData import android.content.Intent -import android.content.SharedPreferences import android.os.Build import android.os.IBinder import androidx.core.app.NotificationCompat @@ -32,7 +31,6 @@ import kotlinx.coroutines.withContext class ClipboardService : Service() { private val scope = CoroutineScope(Job() + Dispatchers.Main) - private val settings: SharedPreferences by lazy { sharedPrefs } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (intent != null) { @@ -45,7 +43,7 @@ class ClipboardService : Service() { } ACTION_START -> { - val time = settings.getString(PreferenceKeys.GENERAL_SHOW_TIME)?.toIntOrNull() ?: 45 + val time = sharedPrefs.getString(PreferenceKeys.GENERAL_SHOW_TIME)?.toIntOrNull() ?: 45 if (time == 0) { stopSelf() @@ -80,7 +78,7 @@ class ClipboardService : Service() { } private fun clearClipboard() { - val deepClear = settings.getBoolean(PreferenceKeys.CLEAR_CLIPBOARD_20X, false) + val deepClear = sharedPrefs.getBoolean(PreferenceKeys.CLEAR_CLIPBOARD_20X, false) val clipboard = clipboard if (clipboard != null) { diff --git a/app/src/main/java/com/zeapo/pwdstore/SearchableRepositoryViewModel.kt b/app/src/main/java/com/zeapo/pwdstore/SearchableRepositoryViewModel.kt index 6da5de7a..a73dd40d 100644 --- a/app/src/main/java/com/zeapo/pwdstore/SearchableRepositoryViewModel.kt +++ b/app/src/main/java/com/zeapo/pwdstore/SearchableRepositoryViewModel.kt @@ -139,7 +139,7 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel private val root get() = PasswordRepository.getRepositoryDirectory() - private val settings by lazy { application.sharedPrefs } + private val settings by lazy(LazyThreadSafetyMode.NONE) { application.sharedPrefs } private val showHiddenContents get() = settings.getBoolean(PreferenceKeys.SHOW_HIDDEN_CONTENTS, false) private val defaultSearchMode diff --git a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt index 45915213..7f6727f2 100644 --- a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt +++ b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt @@ -50,6 +50,7 @@ import com.zeapo.pwdstore.git.sshj.SshKey import com.zeapo.pwdstore.pwgenxkpwd.XkpwdDictionary import com.zeapo.pwdstore.sshkeygen.ShowSshKeyFragment import com.zeapo.pwdstore.sshkeygen.SshKeyGenActivity +import com.zeapo.pwdstore.ui.proxy.ProxySelectorActivity import com.zeapo.pwdstore.utils.BiometricAuthenticator import com.zeapo.pwdstore.utils.PasswordRepository import com.zeapo.pwdstore.utils.PreferenceKeys @@ -418,6 +419,11 @@ class UserPreference : AppCompatActivity() { } } + findPreference<Preference>(PreferenceKeys.PROXY_SETTINGS)?.onPreferenceClickListener = ClickListener { + startActivity(Intent(requireContext(), ProxySelectorActivity::class.java)) + true + } + val prefCustomXkpwdDictionary = findPreference<Preference>(PreferenceKeys.PREF_KEY_CUSTOM_DICT) prefCustomXkpwdDictionary?.onPreferenceClickListener = ClickListener { prefsActivity.storeCustomDictionaryPath() diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillResponseBuilder.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillResponseBuilder.kt index 0e73b908..ec3d2b77 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillResponseBuilder.kt +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillResponseBuilder.kt @@ -15,12 +15,12 @@ import android.service.autofill.SaveInfo import android.widget.RemoteViews import androidx.annotation.RequiresApi import com.github.ajalt.timberkt.e -import com.github.michaelbull.result.fold import com.github.androidpasswordstore.autofillparser.AutofillAction import com.github.androidpasswordstore.autofillparser.AutofillScenario import com.github.androidpasswordstore.autofillparser.Credentials import com.github.androidpasswordstore.autofillparser.FillableForm import com.github.androidpasswordstore.autofillparser.fillWith +import com.github.michaelbull.result.fold import com.zeapo.pwdstore.autofill.oreo.ui.AutofillDecryptActivity import com.zeapo.pwdstore.autofill.oreo.ui.AutofillFilterView import com.zeapo.pwdstore.autofill.oreo.ui.AutofillPublisherChangedActivity @@ -88,8 +88,13 @@ class AutofillResponseBuilder(form: FillableForm) { publisherChangedException: AutofillPublisherChangedException ): Dataset { val remoteView = makeWarningRemoteView(context) + // If the user decides to trust the new publisher, they can choose reset the list of + // matches. In this case we need to immediately show a new `FillResponse` as if the app were + // autofilled for the first time. This `FillResponse` needs to be returned as a result from + // `AutofillPublisherChangedActivity`, which is why we create and pass it on here. + val fillResponseAfterReset = makeFillResponse(context, emptyList()) val intentSender = AutofillPublisherChangedActivity.makePublisherChangedIntentSender( - context, publisherChangedException + context, publisherChangedException, fillResponseAfterReset ) return makePlaceholderDataset(remoteView, intentSender, AutofillAction.Match) } diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillPublisherChangedActivity.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillPublisherChangedActivity.kt index 44ed3446..34edf8f5 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillPublisherChangedActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillPublisherChangedActivity.kt @@ -12,8 +12,10 @@ import android.content.IntentSender import android.content.pm.PackageManager import android.os.Build import android.os.Bundle +import android.service.autofill.FillResponse import android.text.format.DateUtils import android.view.View +import android.view.autofill.AutofillManager import androidx.appcompat.app.AppCompatActivity import com.github.ajalt.timberkt.e import com.github.androidpasswordstore.autofillparser.FormOrigin @@ -33,14 +35,18 @@ class AutofillPublisherChangedActivity : AppCompatActivity() { private const val EXTRA_APP_PACKAGE = "com.zeapo.pwdstore.autofill.oreo.ui.EXTRA_APP_PACKAGE" + private const val EXTRA_FILL_RESPONSE_AFTER_RESET = + "com.zeapo.pwdstore.autofill.oreo.ui.EXTRA_FILL_RESPONSE_AFTER_RESET" private var publisherChangedRequestCode = 1 fun makePublisherChangedIntentSender( context: Context, - publisherChangedException: AutofillPublisherChangedException + publisherChangedException: AutofillPublisherChangedException, + fillResponseAfterReset: FillResponse?, ): IntentSender { val intent = Intent(context, AutofillPublisherChangedActivity::class.java).apply { putExtra(EXTRA_APP_PACKAGE, publisherChangedException.formOrigin.identifier) + putExtra(EXTRA_FILL_RESPONSE_AFTER_RESET, fillResponseAfterReset) } return PendingIntent.getActivity( context, publisherChangedRequestCode++, intent, PendingIntent.FLAG_CANCEL_CURRENT @@ -66,12 +72,16 @@ class AutofillPublisherChangedActivity : AppCompatActivity() { with(binding) { okButton.setOnClickListener { finish() } advancedButton.setOnClickListener { - advancedButton.visibility = View.INVISIBLE + advancedButton.visibility = View.GONE warningAppAdvancedInfo.visibility = View.VISIBLE resetButton.visibility = View.VISIBLE } resetButton.setOnClickListener { AutofillMatcher.clearMatchesFor(this@AutofillPublisherChangedActivity, FormOrigin.App(appPackage)) + val fillResponse = intent.getParcelableExtra<FillResponse>(EXTRA_FILL_RESPONSE_AFTER_RESET) + setResult(RESULT_OK, Intent().apply { + putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillResponse) + }) finish() } } diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSaveActivity.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSaveActivity.kt index 7f83d483..f4c296ee 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSaveActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSaveActivity.kt @@ -82,7 +82,7 @@ class AutofillSaveActivity : AppCompatActivity() { } } - private val formOrigin: FormOrigin? by lazy { + private val formOrigin by lazy(LazyThreadSafetyMode.NONE) { val shouldMatchApp: String? = intent.getStringExtra(EXTRA_SHOULD_MATCH_APP) val shouldMatchWeb: String? = intent.getStringExtra(EXTRA_SHOULD_MATCH_WEB) if (shouldMatchApp != null && shouldMatchWeb == null) { diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/BasePgpActivity.kt b/app/src/main/java/com/zeapo/pwdstore/crypto/BasePgpActivity.kt index 8380d7d0..6b6c2032 100644 --- a/app/src/main/java/com/zeapo/pwdstore/crypto/BasePgpActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/crypto/BasePgpActivity.kt @@ -45,24 +45,24 @@ open class BasePgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBou /** * Full path to the repository */ - val repoPath: String by lazy { intent.getStringExtra("REPO_PATH") } + val repoPath: String by lazy(LazyThreadSafetyMode.NONE) { intent.getStringExtra("REPO_PATH") } /** * Full path to the password file being worked on */ - val fullPath: String by lazy { intent.getStringExtra("FILE_PATH") } + val fullPath: String by lazy(LazyThreadSafetyMode.NONE) { intent.getStringExtra("FILE_PATH") } /** * Name of the password file * * Converts personal/auth.foo.org/john_doe@example.org.gpg to john_doe.example.org */ - val name: String by lazy { File(fullPath).nameWithoutExtension } + val name: String by lazy(LazyThreadSafetyMode.NONE) { File(fullPath).nameWithoutExtension } /** * Get the timestamp for when this file was last modified. */ - val lastChangedString: CharSequence by lazy { + val lastChangedString: CharSequence by lazy(LazyThreadSafetyMode.NONE) { getLastChangedString( intent.getLongExtra( "LAST_CHANGED_TIMESTAMP", @@ -74,7 +74,7 @@ open class BasePgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBou /** * [SharedPreferences] instance used by subclasses to persist settings */ - val settings: SharedPreferences by lazy { sharedPrefs } + val settings: SharedPreferences by lazy(LazyThreadSafetyMode.NONE) { sharedPrefs } /** * Handle to the [OpenPgpApi] instance that is used by subclasses to interface with OpenKeychain. diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/DecryptActivity.kt b/app/src/main/java/com/zeapo/pwdstore/crypto/DecryptActivity.kt index 3685cb17..cb43534d 100644 --- a/app/src/main/java/com/zeapo/pwdstore/crypto/DecryptActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/crypto/DecryptActivity.kt @@ -39,7 +39,7 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound { private val binding by viewBinding(DecryptLayoutBinding::inflate) - private val relativeParentPath by lazy { getParentPath(fullPath, repoPath) } + private val relativeParentPath by lazy(LazyThreadSafetyMode.NONE) { getParentPath(fullPath, repoPath) } private var passwordEntry: PasswordEntry? = null private val userInteractionRequiredResult = registerForActivityResult(StartIntentSenderForResult()) { result -> diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt b/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt index 07694089..01d85f2b 100644 --- a/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt @@ -56,12 +56,12 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB private val binding by viewBinding(PasswordCreationActivityBinding::inflate) - private val suggestedName by lazy { intent.getStringExtra(EXTRA_FILE_NAME) } - private val suggestedPass by lazy { intent.getStringExtra(EXTRA_PASSWORD) } - private val suggestedExtra by lazy { intent.getStringExtra(EXTRA_EXTRA_CONTENT) } - private val shouldGeneratePassword by lazy { intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false) } - private val editing by lazy { intent.getBooleanExtra(EXTRA_EDITING, false) } - private val oldFileName by lazy { intent.getStringExtra(EXTRA_FILE_NAME) } + private val suggestedName by lazy(LazyThreadSafetyMode.NONE) { intent.getStringExtra(EXTRA_FILE_NAME) } + private val suggestedPass by lazy(LazyThreadSafetyMode.NONE) { intent.getStringExtra(EXTRA_PASSWORD) } + private val suggestedExtra by lazy(LazyThreadSafetyMode.NONE) { intent.getStringExtra(EXTRA_EXTRA_CONTENT) } + private val shouldGeneratePassword by lazy(LazyThreadSafetyMode.NONE) { intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false) } + private val editing by lazy(LazyThreadSafetyMode.NONE) { intent.getBooleanExtra(EXTRA_EDITING, false) } + private val oldFileName by lazy(LazyThreadSafetyMode.NONE) { intent.getStringExtra(EXTRA_FILE_NAME) } private var oldCategory: String? = null private var copy: Boolean = false private var encryptionIntent: Intent = Intent() @@ -277,6 +277,7 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB @OptIn(ExperimentalUnsignedTypes::class) private fun parseGpgIdentifier(identifier: String): GpgIdentifier? { + if (identifier.isEmpty()) return null // Match long key IDs: // FF22334455667788 or 0xFF22334455667788 val maybeLongKeyId = identifier.removePrefix("0x").takeIf { @@ -342,6 +343,9 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB .filter { it.isNotBlank() } .map { line -> parseGpgIdentifier(line) ?: run { + // The line being empty means this is most likely an empty `.gpg-id` file + // we created. Skip the validation so we can make the user add a real ID. + if (line.isEmpty()) return@run if (line.removePrefix("0x").matches("[a-fA-F0-9]{8}".toRegex())) { snackbar(message = resources.getString(R.string.short_key_ids_unsupported)) } else { diff --git a/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt index b0aed087..36b8b6fb 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt @@ -128,11 +128,12 @@ abstract class BaseGitActivity : AppCompatActivity() { */ private fun rootCauseException(throwable: Throwable): Throwable { var rootCause = throwable - // JGit's TransportException hides the more helpful SSHJ exceptions. + // JGit's InvalidRemoteException and TransportException hide the more helpful SSHJ exceptions. // Also, SSHJ's UserAuthException about exhausting available authentication methods hides // more useful exceptions. while ((rootCause is org.eclipse.jgit.errors.TransportException || rootCause is org.eclipse.jgit.api.errors.TransportException || + rootCause is org.eclipse.jgit.api.errors.InvalidRemoteException || (rootCause is UserAuthException && rootCause.message == "Exhausted available authentication methods"))) { rootCause = rootCause.cause ?: break diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitCommandExecutor.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitCommandExecutor.kt index ed5411ac..ef584cf7 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/GitCommandExecutor.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/GitCommandExecutor.kt @@ -51,8 +51,10 @@ class GitCommandExecutor( // the previous status will eventually be used to avoid a commit if (nbChanges > 0) { withContext(Dispatchers.IO) { + val name = GitSettings.authorName.ifEmpty { "root" } + val email = GitSettings.authorEmail.ifEmpty { "localhost" } command - .setAuthor(PersonIdent(GitSettings.authorName, GitSettings.authorEmail)) + .setAuthor(PersonIdent(name, email)) .call() } } diff --git a/app/src/main/java/com/zeapo/pwdstore/git/config/GitSettings.kt b/app/src/main/java/com/zeapo/pwdstore/git/config/GitSettings.kt index e43deaf4..27ceb5cb 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/config/GitSettings.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/config/GitSettings.kt @@ -11,6 +11,7 @@ import com.zeapo.pwdstore.Application import com.zeapo.pwdstore.utils.PasswordRepository import com.zeapo.pwdstore.utils.PreferenceKeys import com.zeapo.pwdstore.utils.getEncryptedGitPrefs +import com.zeapo.pwdstore.utils.getEncryptedProxyPrefs import com.zeapo.pwdstore.utils.getString import com.zeapo.pwdstore.utils.sharedPrefs import java.io.File @@ -52,8 +53,9 @@ object GitSettings { private const val DEFAULT_BRANCH = "master" - private val settings by lazy { Application.instance.sharedPrefs } - private val encryptedSettings by lazy { Application.instance.getEncryptedGitPrefs() } + private val settings by lazy(LazyThreadSafetyMode.PUBLICATION) { Application.instance.sharedPrefs } + private val encryptedSettings by lazy(LazyThreadSafetyMode.PUBLICATION) { Application.instance.getEncryptedGitPrefs() } + private val proxySettings by lazy(LazyThreadSafetyMode.PUBLICATION) { Application.instance.getEncryptedProxyPrefs() } var authMode get() = AuthMode.fromString(settings.getString(PreferenceKeys.GIT_REMOTE_AUTH)) @@ -108,6 +110,38 @@ object GitSettings { } } + var proxyHost + get() = proxySettings.getString(PreferenceKeys.PROXY_HOST) + set(value) { + proxySettings.edit { + putString(PreferenceKeys.PROXY_HOST, value) + } + } + + var proxyPort + get() = proxySettings.getInt(PreferenceKeys.PROXY_PORT, -1) + set(value) { + proxySettings.edit { + putInt(PreferenceKeys.PROXY_PORT, value) + } + } + + var proxyUsername + get() = settings.getString(PreferenceKeys.PROXY_USERNAME) + set(value) { + proxySettings.edit { + putString(PreferenceKeys.PROXY_USERNAME, value) + } + } + + var proxyPassword + get() = proxySettings.getString(PreferenceKeys.PROXY_PASSWORD) + set(value) { + proxySettings.edit { + putString(PreferenceKeys.PROXY_PASSWORD, value) + } + } + sealed class UpdateConnectionSettingsResult { class MissingUsername(val newProtocol: Protocol) : UpdateConnectionSettingsResult() class AuthModeMismatch(val newProtocol: Protocol, val validModes: List<AuthMode>) : UpdateConnectionSettingsResult() diff --git a/app/src/main/java/com/zeapo/pwdstore/git/log/GitLogModel.kt b/app/src/main/java/com/zeapo/pwdstore/git/log/GitLogModel.kt index ccd2f88a..15b9f64d 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/log/GitLogModel.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/log/GitLogModel.kt @@ -41,7 +41,7 @@ class GitLogModel { // This is because the commit graph is walked from HEAD to the last commit to obtain. // Additionally, tests with 1000 commits in the log have not produced a significant delay in the // user experience. - private val cache: MutableList<GitCommit> by lazy { + private val cache: MutableList<GitCommit> by lazy(LazyThreadSafetyMode.NONE) { commits().map { GitCommit(it.hash, it.shortMessage, it.authorIdent.name, it.time) }.toMutableList() diff --git a/app/src/main/java/com/zeapo/pwdstore/git/sshj/SshKey.kt b/app/src/main/java/com/zeapo/pwdstore/git/sshj/SshKey.kt index 73072f7d..5e6cfb1b 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/sshj/SshKey.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/sshj/SshKey.kt @@ -50,7 +50,7 @@ private const val PROVIDER_ANDROID_KEY_STORE = "AndroidKeyStore" private const val KEYSTORE_ALIAS = "sshkey" private const val ANDROIDX_SECURITY_KEYSET_PREF_NAME = "androidx_sshkey_keyset_prefs" -private val androidKeystore: KeyStore by lazy { +private val androidKeystore: KeyStore by lazy(LazyThreadSafetyMode.NONE) { KeyStore.getInstance(PROVIDER_ANDROID_KEY_STORE).apply { load(null) } } @@ -119,7 +119,7 @@ object SshKey { putString(PreferenceKeys.GIT_REMOTE_KEY_TYPE, value?.value) } - private val isStrongBoxSupported by lazy { + private val isStrongBoxSupported by lazy(LazyThreadSafetyMode.NONE) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) context.packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE) else diff --git a/app/src/main/java/com/zeapo/pwdstore/model/PasswordEntry.kt b/app/src/main/java/com/zeapo/pwdstore/model/PasswordEntry.kt index ea6255c4..556b2c87 100644 --- a/app/src/main/java/com/zeapo/pwdstore/model/PasswordEntry.kt +++ b/app/src/main/java/com/zeapo/pwdstore/model/PasswordEntry.kt @@ -59,7 +59,7 @@ class PasswordEntry(content: String, private val totpFinder: TotpFinder = UriTot return Otp.calculateCode(totpSecret, Date().time / (1000 * totpPeriod), totpAlgorithm, digits).get() } - val extraContentWithoutAuthData by lazy { + val extraContentWithoutAuthData by lazy(LazyThreadSafetyMode.NONE) { extraContent.splitToSequence("\n").filter { line -> return@filter when { USERNAME_FIELDS.any { prefix -> line.startsWith(prefix, ignoreCase = true) } -> { diff --git a/app/src/main/java/com/zeapo/pwdstore/ui/onboarding/fragments/CloneFragment.kt b/app/src/main/java/com/zeapo/pwdstore/ui/onboarding/fragments/CloneFragment.kt index 19a14695..acd0151c 100644 --- a/app/src/main/java/com/zeapo/pwdstore/ui/onboarding/fragments/CloneFragment.kt +++ b/app/src/main/java/com/zeapo/pwdstore/ui/onboarding/fragments/CloneFragment.kt @@ -25,7 +25,7 @@ class CloneFragment : Fragment(R.layout.fragment_clone) { private val binding by viewBinding(FragmentCloneBinding::bind) - private val settings by lazy { requireActivity().applicationContext.sharedPrefs } + private val settings by lazy(LazyThreadSafetyMode.NONE) { requireActivity().applicationContext.sharedPrefs } private val cloneAction = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == AppCompatActivity.RESULT_OK) { diff --git a/app/src/main/java/com/zeapo/pwdstore/ui/onboarding/fragments/KeySelectionFragment.kt b/app/src/main/java/com/zeapo/pwdstore/ui/onboarding/fragments/KeySelectionFragment.kt new file mode 100644 index 00000000..3dc03954 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/ui/onboarding/fragments/KeySelectionFragment.kt @@ -0,0 +1,66 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package com.zeapo.pwdstore.ui.onboarding.fragments + +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.edit +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import com.zeapo.pwdstore.R +import com.zeapo.pwdstore.crypto.GetKeyIdsActivity +import com.zeapo.pwdstore.databinding.FragmentKeySelectionBinding +import com.zeapo.pwdstore.utils.PasswordRepository +import com.zeapo.pwdstore.utils.PreferenceKeys +import com.zeapo.pwdstore.utils.commitChange +import com.zeapo.pwdstore.utils.finish +import com.zeapo.pwdstore.utils.sharedPrefs +import com.zeapo.pwdstore.utils.viewBinding +import java.io.File +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import me.msfjarvis.openpgpktx.util.OpenPgpApi + +class KeySelectionFragment : Fragment(R.layout.fragment_key_selection) { + + private val settings by lazy(LazyThreadSafetyMode.NONE) { requireActivity().applicationContext.sharedPrefs } + private val binding by viewBinding(FragmentKeySelectionBinding::bind) + + private val gpgKeySelectAction = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == AppCompatActivity.RESULT_OK) { + result.data?.getStringArrayExtra(OpenPgpApi.EXTRA_KEY_IDS)?.let { keyIds -> + lifecycleScope.launch { + withContext(Dispatchers.IO) { + val gpgIdentifierFile = File(PasswordRepository.getRepositoryDirectory(), ".gpg-id") + gpgIdentifierFile.writeText(keyIds.joinToString("\n")) + } + settings.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, true) } + requireActivity().commitChange(getString( + R.string.git_commit_gpg_id, + getString(R.string.app_name) + )) + } + } + } else { + throw IllegalStateException("Failed to initialize repository state.") + } + finish() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.selectKey.setOnClickListener { gpgKeySelectAction.launch(Intent(requireContext(), GetKeyIdsActivity::class.java)) } + } + + companion object { + + fun newInstance() = KeySelectionFragment() + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/ui/onboarding/fragments/RepoLocationFragment.kt b/app/src/main/java/com/zeapo/pwdstore/ui/onboarding/fragments/RepoLocationFragment.kt index 9e8a8f9d..d451fabe 100644 --- a/app/src/main/java/com/zeapo/pwdstore/ui/onboarding/fragments/RepoLocationFragment.kt +++ b/app/src/main/java/com/zeapo/pwdstore/ui/onboarding/fragments/RepoLocationFragment.kt @@ -14,6 +14,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.content.edit import androidx.fragment.app.Fragment import com.github.ajalt.timberkt.d +import com.github.ajalt.timberkt.e import com.github.michaelbull.result.onFailure import com.github.michaelbull.result.runCatching import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -26,14 +27,14 @@ import com.zeapo.pwdstore.utils.finish import com.zeapo.pwdstore.utils.getString import com.zeapo.pwdstore.utils.isPermissionGranted import com.zeapo.pwdstore.utils.listFilesRecursively +import com.zeapo.pwdstore.utils.performTransactionWithBackStack import com.zeapo.pwdstore.utils.sharedPrefs import com.zeapo.pwdstore.utils.viewBinding import java.io.File class RepoLocationFragment : Fragment(R.layout.fragment_repo_location) { - private val firstRunActivity by lazy { requireActivity() } - private val settings by lazy { firstRunActivity.applicationContext.sharedPrefs } + private val settings by lazy(LazyThreadSafetyMode.NONE) { requireActivity().applicationContext.sharedPrefs } private val binding by viewBinding(FragmentRepoLocationBinding::bind) private val sortOrder: PasswordRepository.PasswordSortOrder get() = PasswordRepository.PasswordSortOrder.getSortOrder(settings) @@ -151,18 +152,14 @@ class RepoLocationFragment : Fragment(R.layout.fragment_repo_location) { if (!PasswordRepository.isInitialized) { PasswordRepository.initialize() } - if (File(localDir.absolutePath + "/.gpg-id").createNewFile()) { - settings.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, true) } - } else { - throw IllegalStateException("Failed to initialize repository state.") - } + parentFragmentManager.performTransactionWithBackStack(KeySelectionFragment.newInstance()) }.onFailure { e -> - e.printStackTrace() + e(e) if (!localDir.delete()) { d { "Failed to delete local repository: $localDir" } } + finish() } - finish() } private fun initializeRepositoryInfo() { diff --git a/app/src/main/java/com/zeapo/pwdstore/ui/proxy/ProxySelectorActivity.kt b/app/src/main/java/com/zeapo/pwdstore/ui/proxy/ProxySelectorActivity.kt new file mode 100644 index 00000000..29c4d53a --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/ui/proxy/ProxySelectorActivity.kt @@ -0,0 +1,75 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package com.zeapo.pwdstore.ui.proxy + +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.util.Patterns +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.edit +import androidx.core.os.postDelayed +import androidx.core.widget.doOnTextChanged +import com.zeapo.pwdstore.R +import com.zeapo.pwdstore.databinding.ActivityProxySelectorBinding +import com.zeapo.pwdstore.git.config.GitSettings +import com.zeapo.pwdstore.utils.PreferenceKeys +import com.zeapo.pwdstore.utils.ProxyUtils +import com.zeapo.pwdstore.utils.getEncryptedProxyPrefs +import com.zeapo.pwdstore.utils.getString +import com.zeapo.pwdstore.utils.viewBinding + +private val IP_ADDRESS_REGEX = Patterns.IP_ADDRESS.toRegex() +private val WEB_ADDRESS_REGEX = Patterns.WEB_URL.toRegex() + +class ProxySelectorActivity : AppCompatActivity() { + + private val binding by viewBinding(ActivityProxySelectorBinding::inflate) + private val proxyPrefs by lazy(LazyThreadSafetyMode.NONE) { applicationContext.getEncryptedProxyPrefs() } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + with(binding) { + proxyHost.setText(proxyPrefs.getString(PreferenceKeys.PROXY_HOST)) + proxyUser.setText(proxyPrefs.getString(PreferenceKeys.PROXY_USERNAME)) + proxyPrefs.getInt(PreferenceKeys.PROXY_PORT, -1).takeIf { it != -1 }?.let { + proxyPort.setText("$it") + } + proxyPassword.setText(proxyPrefs.getString(PreferenceKeys.PROXY_PASSWORD)) + save.setOnClickListener { saveSettings() } + proxyHost.doOnTextChanged { text, _, _, _ -> + if (text != null) { + proxyHost.error = if (text.matches(IP_ADDRESS_REGEX) || text.matches(WEB_ADDRESS_REGEX)) { + null + } else { + getString(R.string.invalid_proxy_url) + } + } + } + } + + } + + private fun saveSettings() { + proxyPrefs.edit { + binding.proxyHost.text?.toString()?.takeIf { it.isNotEmpty() }.let { + GitSettings.proxyHost = it + } + binding.proxyUser.text?.toString()?.takeIf { it.isNotEmpty() }.let { + GitSettings.proxyUsername = it + } + binding.proxyPort.text?.toString()?.takeIf { it.isNotEmpty() }?.let { + GitSettings.proxyPort = it.toInt() + } + binding.proxyPassword.text?.toString()?.takeIf { it.isNotEmpty() }.let { + GitSettings.proxyPassword = it + } + } + ProxyUtils.setDefaultProxy() + Handler(Looper.getMainLooper()).postDelayed(500) { finish() } + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/AndroidExtensions.kt b/app/src/main/java/com/zeapo/pwdstore/utils/AndroidExtensions.kt index 3ee1820d..408e9d5e 100644 --- a/app/src/main/java/com/zeapo/pwdstore/utils/AndroidExtensions.kt +++ b/app/src/main/java/com/zeapo/pwdstore/utils/AndroidExtensions.kt @@ -72,6 +72,12 @@ val Context.clipboard fun Context.getEncryptedGitPrefs() = getEncryptedPrefs("git_operation") /** + * Wrapper for [getEncryptedPrefs] to get the encrypted preference set for the HTTP + * proxy. + */ +fun Context.getEncryptedProxyPrefs() = getEncryptedPrefs("http_proxy") + +/** * Get an instance of [EncryptedSharedPreferences] with the given [fileName] */ private fun Context.getEncryptedPrefs(fileName: String): SharedPreferences { diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/BiometricAuthenticator.kt b/app/src/main/java/com/zeapo/pwdstore/utils/BiometricAuthenticator.kt index a1b4a279..12ba84f1 100644 --- a/app/src/main/java/com/zeapo/pwdstore/utils/BiometricAuthenticator.kt +++ b/app/src/main/java/com/zeapo/pwdstore/utils/BiometricAuthenticator.kt @@ -6,7 +6,6 @@ package com.zeapo.pwdstore.utils import android.app.KeyguardManager import androidx.annotation.StringRes -import androidx.biometric.BiometricConstants import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager.Authenticators import androidx.biometric.BiometricPrompt @@ -43,12 +42,12 @@ object BiometricAuthenticator { super.onAuthenticationError(errorCode, errString) tag(TAG).d { "BiometricAuthentication error: errorCode=$errorCode, msg=$errString" } callback(when (errorCode) { - BiometricConstants.ERROR_CANCELED, BiometricConstants.ERROR_USER_CANCELED, - BiometricConstants.ERROR_NEGATIVE_BUTTON -> { + BiometricPrompt.ERROR_CANCELED, BiometricPrompt.ERROR_USER_CANCELED, + BiometricPrompt.ERROR_NEGATIVE_BUTTON -> { Result.Cancelled } - BiometricConstants.ERROR_HW_NOT_PRESENT, BiometricConstants.ERROR_HW_UNAVAILABLE, - BiometricConstants.ERROR_NO_BIOMETRICS, BiometricConstants.ERROR_NO_DEVICE_CREDENTIAL -> { + BiometricPrompt.ERROR_HW_NOT_PRESENT, BiometricPrompt.ERROR_HW_UNAVAILABLE, + BiometricPrompt.ERROR_NO_BIOMETRICS, BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL -> { Result.HardwareUnavailableOrDisabled } else -> Result.Failure(errorCode, activity.getString(R.string.biometric_auth_error_reason, errString)) diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.kt b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.kt index f1655b3b..6e090ba3 100644 --- a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.kt +++ b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.kt @@ -93,7 +93,7 @@ open class PasswordRepository protected constructor() { companion object { private var repository: Repository? = null - private val settings by lazy { Application.instance.sharedPrefs } + private val settings by lazy(LazyThreadSafetyMode.NONE) { Application.instance.sharedPrefs } private val filesDir get() = Application.instance.filesDir diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt b/app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt index 1b2c7abb..5ec40639 100644 --- a/app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt +++ b/app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt @@ -80,4 +80,10 @@ object PreferenceKeys { @Deprecated("To be used only in Migrations.kt") const val USE_GENERATED_KEY = "use_generated_key" + + const val PROXY_SETTINGS = "proxy_settings" + const val PROXY_HOST = "proxy_host" + const val PROXY_PORT = "proxy_port" + const val PROXY_USERNAME = "proxy_username" + const val PROXY_PASSWORD = "proxy_password" } diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/ProxyUtils.kt b/app/src/main/java/com/zeapo/pwdstore/utils/ProxyUtils.kt new file mode 100644 index 00000000..4bb3dfcb --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/utils/ProxyUtils.kt @@ -0,0 +1,66 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package com.zeapo.pwdstore.utils + +import com.zeapo.pwdstore.git.config.GitSettings +import java.io.IOException +import java.net.Authenticator +import java.net.InetSocketAddress +import java.net.PasswordAuthentication +import java.net.Proxy +import java.net.ProxySelector +import java.net.SocketAddress +import java.net.URI + +/** + * Utility class for [Proxy] handling. + */ +object ProxyUtils { + + private const val HTTP_PROXY_USER_PROPERTY = "http.proxyUser" + private const val HTTP_PROXY_PASSWORD_PROPERTY = "http.proxyPassword" + + /** + * Set the default [Proxy] and [Authenticator] for the app based on user provided settings. + */ + fun setDefaultProxy() { + ProxySelector.setDefault(object : ProxySelector() { + override fun select(uri: URI?): MutableList<Proxy> { + val host = GitSettings.proxyHost + val port = GitSettings.proxyPort + return if (host == null || port == -1) { + mutableListOf(Proxy.NO_PROXY) + } else { + mutableListOf(Proxy(Proxy.Type.HTTP, InetSocketAddress.createUnresolved(host, port))) + } + } + + override fun connectFailed(uri: URI?, sa: SocketAddress?, ioe: IOException?) { + if (uri == null || sa == null || ioe == null) { + throw IllegalArgumentException("Arguments can't be null.") + } + } + }) + val user = GitSettings.proxyUsername ?: "" + val password = GitSettings.proxyPassword ?: "" + if (user.isEmpty() || password.isEmpty()) { + System.clearProperty(HTTP_PROXY_USER_PROPERTY) + System.clearProperty(HTTP_PROXY_PASSWORD_PROPERTY) + } else { + System.setProperty(HTTP_PROXY_USER_PROPERTY, user) + System.setProperty(HTTP_PROXY_PASSWORD_PROPERTY, password) + } + Authenticator.setDefault(object : Authenticator() { + override fun getPasswordAuthentication(): PasswordAuthentication? { + return if (requestorType == RequestorType.PROXY) { + PasswordAuthentication(user, password.toCharArray()) + } else { + null + } + } + }) + } +} diff --git a/app/src/main/res/layout/activity_oreo_autofill_publisher_changed.xml b/app/src/main/res/layout/activity_oreo_autofill_publisher_changed.xml index 92d4e094..8a735e5b 100644 --- a/app/src/main/res/layout/activity_oreo_autofill_publisher_changed.xml +++ b/app/src/main/res/layout/activity_oreo_autofill_publisher_changed.xml @@ -6,57 +6,69 @@ <androidx.constraintlayout.widget.ConstraintLayout 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="280dp" + android:layout_width="match_parent" android:layout_height="wrap_content" android:animateLayoutChanges="true" android:elevation="2dp" + android:paddingBottom="16dp" android:scrollbars="vertical" tools:context="com.zeapo.pwdstore.autofill.oreo.ui.AutofillPublisherChangedActivity"> <ImageView android:id="@+id/cover" - android:layout_width="0dp" - android:layout_height="50dp" + android:layout_width="60dp" + android:layout_height="60dp" android:background="@color/primary_color" android:contentDescription="@string/app_name" android:src="@mipmap/ic_launcher_foreground" app:layout_constraintBottom_toTopOf="@id/warningSign" - app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toStartOf="@id/app_name" + app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintVertical_bias="0.0" - app:layout_constraintVertical_chainStyle="packed" /> + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/app_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/app_name" + android:textSize="18sp" + android:textStyle="bold" + app:layout_constraintBottom_toBottomOf="@id/cover" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/cover" + app:layout_constraintTop_toTopOf="@id/cover" /> <ImageView android:id="@+id/warningSign" - android:layout_width="0dp" - android:layout_height="50dp" + android:layout_width="40dp" + android:layout_height="40dp" + android:layout_marginStart="@dimen/activity_horizontal_margin" android:contentDescription="@string/oreo_autofill_warning_publisher_warning_sign_description" android:src="@drawable/ic_warning_red_24dp" - app:layout_constraintBottom_toTopOf="@id/warningHeader" - app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@id/warningHeader" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/cover" /> + app:layout_constraintBottom_toBottomOf="@id/warningHeader" /> <TextView android:id="@+id/warningHeader" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="@dimen/activity_horizontal_margin" + android:layout_marginTop="@dimen/activity_vertical_margin" + android:layout_marginStart="8dp" android:layout_marginEnd="@dimen/activity_horizontal_margin" - android:layout_marginBottom="@dimen/activity_vertical_margin" android:text="@string/oreo_autofill_warning_publisher_header" android:textSize="12sp" - app:layout_constraintBottom_toTopOf="@id/warningAppName" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/warningSign" /> + app:layout_constraintStart_toEndOf="@id/warningSign" + app:layout_constraintTop_toBottomOf="@id/cover" /> <TextView android:id="@+id/warningAppName" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="@dimen/activity_horizontal_margin" + android:layout_marginTop="@dimen/activity_vertical_margin" android:layout_marginEnd="@dimen/activity_horizontal_margin" android:gravity="center_horizontal" android:textSize="12sp" @@ -87,79 +99,77 @@ android:layout_marginStart="@dimen/activity_horizontal_margin" android:layout_marginTop="@dimen/activity_vertical_margin" android:layout_marginEnd="@dimen/activity_horizontal_margin" - android:layout_marginBottom="@dimen/activity_vertical_margin" android:text="@string/oreo_autofill_warning_publisher_footer" android:textSize="12sp" - app:layout_constraintBottom_toTopOf="@id/okButton" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/warningAppInstallDate" /> - <Button + <com.google.android.material.button.MaterialButton android:id="@+id/okButton" - style="@style/Widget.MaterialComponents.Button.OutlinedButton" + style="@style/AppTheme.OutlinedButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="@dimen/activity_horizontal_margin" + android:layout_marginTop="@dimen/activity_vertical_margin" android:layout_marginEnd="@dimen/activity_horizontal_margin" + android:minWidth="240dp" android:text="@string/oreo_autofill_warning_publisher_changed_disable_autofill_button" android:textSize="12sp" - app:layout_constraintBottom_toTopOf="@id/advancedButton" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintHorizontal_bias="1.0" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/warningAppFooter" /> - <Button + <com.google.android.material.button.MaterialButton android:id="@+id/advancedButton" - style="@style/Widget.MaterialComponents.Button.OutlinedButton" + style="@style/AppTheme.OutlinedButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="@dimen/activity_horizontal_margin" android:layout_marginEnd="@dimen/activity_horizontal_margin" + android:minWidth="240dp" android:text="@string/oreo_autofill_warning_publisher_advanced_info_button" android:textSize="12sp" - app:layout_constraintBottom_toTopOf="@id/warningAppAdvancedInfo" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintHorizontal_bias="1.0" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/okButton" /> + app:layout_constraintTop_toBottomOf="@id/okButton" + tools:visibility="visible" /> - <TextView - android:id="@+id/warningAppAdvancedInfo" - android:layout_width="0dp" + <com.google.android.material.button.MaterialButton + android:id="@+id/resetButton" + style="@style/AppTheme.OutlinedButton" + android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="@dimen/activity_horizontal_margin" android:layout_marginEnd="@dimen/activity_horizontal_margin" - android:fontFamily="monospace" - android:gravity="center_horizontal" - android:textIsSelectable="true" - android:textSize="10sp" - android:visibility="invisible" - app:layout_constraintBottom_toTopOf="@id/resetButton" + android:minWidth="240dp" + android:text="@string/oreo_autofill_warning_publisher_reenable_button" + android:textColor="?attr/colorOnSurface" + android:textSize="12sp" + android:visibility="gone" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/advancedButton" - tools:ignore="SmallSp" - tools:text="Package: com.example.banking\n\nHash:\n8P1sW0EPJcslw7UzRsiXL64w+O50Ed+RBICtay1g24M=" tools:visibility="visible" /> - <Button - android:id="@+id/resetButton" - style="@style/Widget.MaterialComponents.Button.TextButton" - android:layout_width="wrap_content" + <TextView + android:id="@+id/warningAppAdvancedInfo" + android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="@dimen/activity_horizontal_margin" + android:layout_marginTop="16dp" android:layout_marginEnd="@dimen/activity_horizontal_margin" - android:text="@string/oreo_autofill_warning_publisher_reenable_button" + android:fontFamily="monospace" + android:gravity="center_horizontal" + android:textIsSelectable="true" android:textSize="10sp" - android:visibility="invisible" + android:visibility="gone" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintHorizontal_bias="1.0" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/warningAppAdvancedInfo" + app:layout_constraintTop_toBottomOf="@id/resetButton" tools:ignore="SmallSp" + tools:text="Package: com.example.banking\n\nHash:\n8P1sW0EPJcslw7UzRsiXL64w+O50Ed+RBICtay1g24M=" tools:visibility="visible" /> </androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/activity_proxy_selector.xml b/app/src/main/res/layout/activity_proxy_selector.xml new file mode 100644 index 00000000..e732dfe0 --- /dev/null +++ b/app/src/main/res/layout/activity_proxy_selector.xml @@ -0,0 +1,106 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + ~ SPDX-License-Identifier: GPL-3.0-only + --> + +<androidx.constraintlayout.widget.ConstraintLayout 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"> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/proxy_host_input_layout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/activity_horizontal_margin" + android:layout_marginTop="@dimen/activity_vertical_margin" + android:layout_marginEnd="@dimen/activity_horizontal_margin" + android:hint="@string/proxy_hostname" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:layout_editor_absoluteY="64dp"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/proxy_host" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="textUri" + android:nextFocusForward="@id/proxy_user" /> + + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/proxy_user_input_layout" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/activity_horizontal_margin" + android:layout_marginTop="@dimen/normal_margin" + android:layout_marginEnd="@dimen/normal_margin" + android:hint="@string/username" + app:layout_constraintEnd_toStartOf="@id/proxy_port_input_layout" + app:layout_constraintHorizontal_weight="0.65" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/proxy_host_input_layout"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/proxy_user" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="textWebEmailAddress" + android:nextFocusForward="@id/proxy_port" /> + + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/proxy_port_input_layout" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/normal_margin" + android:layout_marginTop="@dimen/normal_margin" + android:layout_marginEnd="@dimen/activity_horizontal_margin" + android:hint="@string/port" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_weight="0.35" + app:layout_constraintStart_toEndOf="@id/proxy_user_input_layout" + app:layout_constraintTop_toBottomOf="@id/proxy_host_input_layout"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/proxy_port" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="number" + android:nextFocusForward="@id/proxy_password" /> + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/proxy_password_input_layout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/activity_horizontal_margin" + android:layout_marginTop="@dimen/normal_margin" + android:layout_marginEnd="@dimen/activity_horizontal_margin" + android:hint="@string/password" + app:endIconMode="password_toggle" + app:layout_constraintStart_toStartOf="@id/proxy_user_input_layout" + app:layout_constraintTop_toBottomOf="@id/proxy_user_input_layout"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/proxy_password" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="textPassword" /> + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.button.MaterialButton + android:id="@+id/save" + style="@style/AppTheme.OutlinedButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/normal_margin" + android:layout_marginEnd="@dimen/activity_horizontal_margin" + android:text="@string/crypto_save" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/proxy_password_input_layout" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/fragment_key_selection.xml b/app/src/main/res/layout/fragment_key_selection.xml new file mode 100644 index 00000000..9a391276 --- /dev/null +++ b/app/src/main/res/layout/fragment_key_selection.xml @@ -0,0 +1,81 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + ~ SPDX-License-Identifier: GPL-3.0-only + --> + +<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:background="?attr/colorPrimary" + android:orientation="vertical"> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/app_icon" + android:layout_width="64dp" + android:layout_height="64dp" + android:layout_marginStart="32dp" + android:layout_marginTop="100dp" + android:contentDescription="@string/app_icon_hint" + android:src="@mipmap/ic_launcher" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <androidx.appcompat.widget.AppCompatTextView + android:id="@+id/app_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerHorizontal="true" + android:layout_marginStart="16dp" + android:text="@string/app_name" + android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5" + android:textColor="@color/color_control_normal" + android:textStyle="bold" + app:layout_constraintBottom_toBottomOf="@id/app_icon" + app:layout_constraintStart_toEndOf="@id/app_icon" + app:layout_constraintTop_toTopOf="@+id/app_icon" /> + + <TextView + android:id="@+id/gpg_key" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="48dp" + android:text="@string/select_gpg_key_title" + android:textAppearance="@style/TextAppearance.MaterialComponents.Headline4" + android:textColor="@color/color_control_normal" + android:textStyle="bold" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintStart_toStartOf="@id/app_icon" + app:layout_constraintTop_toBottomOf="@id/app_icon" /> + + <TextView + android:id="@+id/gpg_key_text" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="48dp" + android:layout_marginEnd="16dp" + android:text="@string/select_gpg_key_message" + android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" + android:textColor="@color/color_control_normal" + android:textStyle="bold" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@id/gpg_key" + app:layout_constraintTop_toBottomOf="@id/gpg_key" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/select_key" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginTop="48dp" + android:layout_marginEnd="16dp" + android:maxWidth="300dp" + android:minWidth="100dp" + android:text="@string/gpg_key_select" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/gpg_key_text" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/values-v23/colors.xml b/app/src/main/res/values-v23/colors.xml deleted file mode 100644 index 7c3be280..00000000 --- a/app/src/main/res/values-v23/colors.xml +++ /dev/null @@ -1,8 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?><!-- - ~ Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. - ~ SPDX-License-Identifier: GPL-3.0-only - --> - -<resources> - <color name="navigation_bar_color">#000000</color> -</resources> diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index bea2e960..b090c761 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -15,7 +15,7 @@ <!-- Theme variables --> <color name="color_control_normal">@color/primary_text_color</color> <color name="list_multiselect_background">#668eacbb</color> - <color name="navigation_bar_color">@color/primary_color</color> + <color name="navigation_bar_color">#000000</color> <color name="status_bar_color">@color/primary_color</color> <color name="ripple_color">#aa003e5b</color> <color name="button_color">#44003e5b</color> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 57c32126..cc9f1f24 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -100,10 +100,10 @@ <!-- DECRYPT Layout --> <string name="action_search">Search</string> - <string name="password">Password:</string> - <string name="otp">OTP:</string> + <string name="password">Password</string> + <string name="otp">OTP</string> <string name="extra_content">Extra content:</string> - <string name="username">Username:</string> + <string name="username">Username</string> <string name="edit_password">Edit password</string> <string name="copy_password">Copy password</string> <string name="share_as_plaintext">Share as plaintext</string> @@ -400,9 +400,18 @@ <string name="select_repo_type_text">Select if you want to create a local repo or clone a remote repo.</string> <string name="clone_remote_repo">Clone Remote Repo</string> <string name="create_local_repo">Create Local Repo</string> + <string name="select_gpg_key_title">Select\nGPG\nKey</string> + <string name="select_gpg_key_message">Select a GPG key to initialize your store with</string> + <string name="gpg_key_select">Select key</string> <!-- SSH port validation --> <string name="ssh_scheme_needed_title">Potentially incorrect URL</string> <string name="ssh_scheme_needed_message">It appears that your URL contains a custom port, but does not specify the ssh:// scheme.\nThis can cause the port to be considered a part of your path. Press OK here to fix the URL.</string> + <!-- Proxy configuration activity --> + <string name="proxy_hostname">Proxy hostname</string> + <string name="port">Port</string> + <string name="pref_proxy_settings">HTTP(S) proxy settings</string> + <string name="invalid_proxy_url">Invalid URL</string> + </resources> diff --git a/app/src/main/res/xml/preference.xml b/app/src/main/res/xml/preference.xml index 1d82df18..58a173c5 100644 --- a/app/src/main/res/xml/preference.xml +++ b/app/src/main/res/xml/preference.xml @@ -49,6 +49,9 @@ app:key="git_server_info" app:title="@string/pref_edit_server_info" /> <Preference + app:key="proxy_settings" + app:title="@string/pref_proxy_settings" /> + <Preference app:key="git_config" app:title="@string/pref_edit_git_config" /> <Preference |