Misc cleanups to build and extension functions (#1108)

This commit is contained in:
Harsh Shandilya 2020-09-18 18:14:52 +05:30 committed by GitHub
parent 9d63b11391
commit bad8e2b404
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 221 additions and 172 deletions

View file

@ -141,9 +141,6 @@ dependencies {
androidTestImplementation(Dependencies.Testing.kotlin_test_junit)
androidTestImplementation(Dependencies.Testing.AndroidX.runner)
androidTestImplementation(Dependencies.Testing.AndroidX.rules)
androidTestImplementation(Dependencies.Testing.AndroidX.junit)
androidTestImplementation(Dependencies.Testing.AndroidX.espresso_core)
androidTestImplementation(Dependencies.Testing.AndroidX.espresso_intents)
testImplementation(Dependencies.Testing.junit)
testImplementation(Dependencies.Testing.kotlin_test_junit)

View file

@ -54,7 +54,7 @@ import com.zeapo.pwdstore.utils.BiometricAuthenticator
import com.zeapo.pwdstore.utils.PasswordRepository
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.autofillManager
import com.zeapo.pwdstore.utils.getEncryptedPrefs
import com.zeapo.pwdstore.utils.getEncryptedGitPrefs
import com.zeapo.pwdstore.utils.getString
import com.zeapo.pwdstore.utils.sharedPrefs
import java.io.File
@ -81,7 +81,7 @@ class UserPreference : AppCompatActivity() {
prefsActivity = requireActivity() as UserPreference
val context = requireContext()
sharedPreferences = preferenceManager.sharedPreferences
encryptedPreferences = requireActivity().getEncryptedPrefs("git_operation")
encryptedPreferences = requireActivity().getEncryptedGitPrefs()
addPreferencesFromResource(R.xml.preference)

View file

@ -21,7 +21,7 @@ import com.zeapo.pwdstore.git.operation.PushOperation
import com.zeapo.pwdstore.git.operation.ResetToRemoteOperation
import com.zeapo.pwdstore.git.operation.SyncOperation
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.getEncryptedPrefs
import com.zeapo.pwdstore.utils.getEncryptedGitPrefs
import com.zeapo.pwdstore.utils.sharedPrefs
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -86,7 +86,7 @@ abstract class BaseGitActivity : AppCompatActivity() {
suspend fun promptOnErrorHandler(err: Throwable, onPromptDone: () -> Unit = {}) {
val error = rootCauseException(err)
if (!isExplicitlyUserInitiatedError(error)) {
getEncryptedPrefs("git_operation").edit {
getEncryptedGitPrefs().edit {
remove(PreferenceKeys.HTTPS_PASSWORD)
}
sharedPrefs.edit { remove(PreferenceKeys.SSH_OPENKEYSTORE_KEYID) }

View file

@ -10,7 +10,7 @@ import com.github.michaelbull.result.runCatching
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.getEncryptedGitPrefs
import com.zeapo.pwdstore.utils.getString
import com.zeapo.pwdstore.utils.sharedPrefs
import java.io.File
@ -53,7 +53,7 @@ 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") }
private val encryptedSettings by lazy { Application.instance.getEncryptedGitPrefs() }
var authMode
get() = AuthMode.fromString(settings.getString(PreferenceKeys.GIT_REMOTE_AUTH))

View file

@ -14,7 +14,7 @@ import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.git.config.AuthMode
import com.zeapo.pwdstore.git.sshj.InteractivePasswordFinder
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.getEncryptedPrefs
import com.zeapo.pwdstore.utils.getEncryptedGitPrefs
import com.zeapo.pwdstore.utils.requestInputFocusOnView
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
@ -25,7 +25,7 @@ class CredentialFinder(
) : InteractivePasswordFinder() {
override fun askForPassword(cont: Continuation<String?>, isRetry: Boolean) {
val gitOperationPrefs = callingActivity.getEncryptedPrefs("git_operation")
val gitOperationPrefs = callingActivity.getEncryptedGitPrefs()
val credentialPref: String
@StringRes val messageRes: Int
@StringRes val hintRes: Int

View file

@ -23,7 +23,7 @@ import com.github.michaelbull.result.runCatching
import com.zeapo.pwdstore.Application
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.getEncryptedPrefs
import com.zeapo.pwdstore.utils.getEncryptedGitPrefs
import com.zeapo.pwdstore.utils.getString
import com.zeapo.pwdstore.utils.sharedPrefs
import java.io.File
@ -169,7 +169,7 @@ object SshKey {
if (publicKeyFile.isFile) {
publicKeyFile.delete()
}
context.getEncryptedPrefs("git_operation").edit {
context.getEncryptedGitPrefs().edit {
remove(PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE)
}
type = null

View file

@ -20,7 +20,7 @@ import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.databinding.ActivitySshKeygenBinding
import com.zeapo.pwdstore.git.sshj.SshKey
import com.zeapo.pwdstore.utils.BiometricAuthenticator
import com.zeapo.pwdstore.utils.getEncryptedPrefs
import com.zeapo.pwdstore.utils.getEncryptedGitPrefs
import com.zeapo.pwdstore.utils.keyguardManager
import com.zeapo.pwdstore.utils.viewBinding
import kotlin.coroutines.resume
@ -128,7 +128,7 @@ class SshKeyGenActivity : AppCompatActivity() {
keyGenType.generateKey(requireAuthentication)
}
}
getEncryptedPrefs("git_operation").edit {
getEncryptedGitPrefs().edit {
remove("ssh_key_local_passphrase")
}
binding.generate.apply {

View file

@ -0,0 +1,172 @@
/*
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
package com.zeapo.pwdstore.utils
import android.app.KeyguardManager
import android.content.ClipboardManager
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.os.Build
import android.util.Base64
import android.util.TypedValue
import android.view.View
import android.view.autofill.AutofillManager
import android.view.inputmethod.InputMethodManager
import androidx.annotation.IdRes
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.fragment.app.FragmentActivity
import androidx.preference.PreferenceManager
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import com.github.ajalt.timberkt.d
import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result
import com.google.android.material.snackbar.Snackbar
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.git.operation.GitOperation
/**
* Extension function for [AlertDialog] that requests focus for the
* view whose id is [id]. Solution based on a StackOverflow
* answer: https://stackoverflow.com/a/13056259/297261
*/
fun <T : View> AlertDialog.requestInputFocusOnView(@IdRes id: Int) {
setOnShowListener {
findViewById<T>(id)?.apply {
setOnFocusChangeListener { v, _ ->
v.post {
context.getSystemService<InputMethodManager>()
?.showSoftInput(v, InputMethodManager.SHOW_IMPLICIT)
}
}
requestFocus()
}
}
}
/**
* Get an instance of [AutofillManager]. Only
* available on Android Oreo and above
*/
val Context.autofillManager: AutofillManager?
@RequiresApi(Build.VERSION_CODES.O)
get() = getSystemService()
/**
* Get an instance of [ClipboardManager]
*/
val Context.clipboard
get() = getSystemService<ClipboardManager>()
/**
* Wrapper for [getEncryptedPrefs] to avoid open-coding the file name at
* each call site
*/
fun Context.getEncryptedGitPrefs() = getEncryptedPrefs("git_operation")
/**
* Get an instance of [EncryptedSharedPreferences] with the given [fileName]
*/
private fun Context.getEncryptedPrefs(fileName: String): SharedPreferences {
val masterKeyAlias = MasterKey.Builder(applicationContext)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
return EncryptedSharedPreferences.create(
applicationContext,
fileName,
masterKeyAlias,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
/**
* Get an instance of [KeyguardManager]
*/
val Context.keyguardManager: KeyguardManager
get() = getSystemService()!!
/**
* Get the default [SharedPreferences] instance
*/
val Context.sharedPrefs: SharedPreferences
get() = PreferenceManager.getDefaultSharedPreferences(applicationContext)
/**
* Resolve [attr] from the [Context]'s theme
*/
fun Context.resolveAttribute(attr: Int): Int {
val typedValue = TypedValue()
this.theme.resolveAttribute(attr, typedValue, true)
return typedValue.data
}
/**
* Commit changes to the store from a [FragmentActivity] using
* a custom implementation of [GitOperation]
*/
suspend fun FragmentActivity.commitChange(
message: String,
): Result<Unit, Throwable> {
if (!PasswordRepository.isGitRepo()) {
return Ok(Unit)
}
return object : GitOperation(this@commitChange) {
override val commands = arrayOf(
// Stage all files
git.add().addFilepattern("."),
// Populate the changed files count
git.status(),
// Commit everything! If anything changed, that is.
git.commit().setAll(true).setMessage(message),
)
override fun preExecute(): Boolean {
d { "Committing with message: '$message'" }
return true
}
}.execute()
}
/**
* Check if [permission] has been granted to the app.
*/
fun FragmentActivity.isPermissionGranted(permission: String): Boolean {
return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
}
/**
* Show a [Snackbar] in a [FragmentActivity] and correctly
* anchor it to a [com.google.android.material.floatingactionbutton.FloatingActionButton]
* if one exists in the [view]
*/
fun FragmentActivity.snackbar(
view: View = findViewById(android.R.id.content),
message: String,
length: Int = Snackbar.LENGTH_SHORT,
): Snackbar {
val snackbar = Snackbar.make(view, message, length)
snackbar.anchorView = findViewById(R.id.fab)
snackbar.show()
return snackbar
}
/**
* Simplifies the common `getString(key, null) ?: defaultValue` case slightly
*/
fun SharedPreferences.getString(key: String): String? = getString(key, null)
/**
* Convert this [String] to its [Base64] representation
*/
fun String.base64(): String {
return Base64.encodeToString(encodeToByteArray(), Base64.NO_WRAP)
}

View file

@ -4,74 +4,33 @@
*/
package com.zeapo.pwdstore.utils
import android.app.KeyguardManager
import android.content.ClipboardManager
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.os.Build
import android.util.Base64
import android.util.TypedValue
import android.view.View
import android.view.autofill.AutofillManager
import android.view.inputmethod.InputMethodManager
import androidx.annotation.IdRes
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.fragment.app.FragmentActivity
import androidx.preference.PreferenceManager
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import com.github.ajalt.timberkt.d
import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result
import com.github.michaelbull.result.getOrElse
import com.github.michaelbull.result.runCatching
import com.google.android.material.snackbar.Snackbar
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.git.operation.GitOperation
import com.zeapo.pwdstore.utils.PasswordRepository.Companion.getRepositoryDirectory
import java.io.File
import java.util.Date
import org.eclipse.jgit.lib.ObjectId
import org.eclipse.jgit.revwalk.RevCommit
/**
* The default OpenPGP provider for the app
*/
const val OPENPGP_PROVIDER = "org.sufficientlysecure.keychain"
/**
* Clears the given [flag] from the value of this [Int]
*/
fun Int.clearFlag(flag: Int): Int {
return this and flag.inv()
}
/**
* Checks if this [Int] contains the given [flag]
*/
infix fun Int.hasFlag(flag: Int): Boolean {
return this and flag == flag
}
fun String.splitLines(): Array<String> {
return split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
}
fun String.base64(): String {
return Base64.encodeToString(encodeToByteArray(), Base64.NO_WRAP)
}
val Context.clipboard
get() = getSystemService<ClipboardManager>()
fun FragmentActivity.snackbar(
view: View = findViewById(android.R.id.content),
message: String,
length: Int = Snackbar.LENGTH_SHORT,
): Snackbar {
val snackbar = Snackbar.make(view, message, length)
snackbar.anchorView = findViewById(R.id.fab)
snackbar.show()
return snackbar
}
fun File.listFilesRecursively() = walkTopDown().filter { !it.isDirectory }.toList()
/**
* Checks whether this [File] is a directory that contains [other].
*/
@ -89,91 +48,23 @@ fun File.contains(other: File): Boolean {
return relativePath.path == other.name
}
fun Context.resolveAttribute(attr: Int): Int {
val typedValue = TypedValue()
this.theme.resolveAttribute(attr, typedValue, true)
return typedValue.data
}
fun Context.getEncryptedPrefs(fileName: String): SharedPreferences {
val masterKeyAlias = MasterKey.Builder(applicationContext)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
return EncryptedSharedPreferences.create(
applicationContext,
fileName,
masterKeyAlias,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
val Context.sharedPrefs: SharedPreferences
get() = PreferenceManager.getDefaultSharedPreferences(applicationContext)
fun SharedPreferences.getString(key: String): String? = getString(key, null)
suspend fun FragmentActivity.commitChange(
message: String,
): Result<Unit, Throwable> {
if (!PasswordRepository.isGitRepo()) {
return Ok(Unit)
}
return object : GitOperation(this@commitChange) {
override val commands = arrayOf(
// Stage all files
git.add().addFilepattern("."),
// Populate the changed files count
git.status(),
// Commit everything! If anything changed, that is.
git.commit().setAll(true).setMessage(message),
)
override fun preExecute(): Boolean {
d { "Committing with message: '$message'" }
return true
}
}.execute()
}
fun FragmentActivity.isPermissionGranted(permission: String): Boolean {
return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
}
/**
* Extension function for [AlertDialog] that requests focus for the
* view whose id is [id]. Solution based on a StackOverflow
* answer: https://stackoverflow.com/a/13056259/297261
* Checks if this [File] is in the password repository directory as given
* by [getRepositoryDirectory]
*/
fun <T : View> AlertDialog.requestInputFocusOnView(@IdRes id: Int) {
setOnShowListener {
findViewById<T>(id)?.apply {
setOnFocusChangeListener { v, _ ->
v.post {
context.getSystemService<InputMethodManager>()
?.showSoftInput(v, InputMethodManager.SHOW_IMPLICIT)
}
}
requestFocus()
}
}
}
val Context.autofillManager: AutofillManager?
@RequiresApi(Build.VERSION_CODES.O)
get() = getSystemService()
val Context.keyguardManager: KeyguardManager
get() = getSystemService()!!
fun File.isInsideRepository(): Boolean {
return canonicalPath.contains(getRepositoryDirectory().canonicalPath)
}
/**
* Recursively lists the files in this [File], skipping any directories it encounters.
*/
fun File.listFilesRecursively() = walkTopDown().filter { !it.isDirectory }.toList()
/**
* Unique SHA-1 hash of this commit as hexadecimal string.
*
* @see RevCommit.id
* @see RevCommit.getId
*/
val RevCommit.hash: String
get() = ObjectId.toString(id)
@ -189,3 +80,11 @@ val RevCommit.time: Date
val epochMilliseconds = epochSeconds * 1000
return Date(epochMilliseconds)
}
/**
* Splits this [String] into an [Array] of [String]s, split on the UNIX LF line ending
* and stripped of any empty lines.
*/
fun String.splitLines(): Array<String> {
return split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
}

View file

@ -1,34 +1,29 @@
package com.zeapo.pwdstore.utils
import android.content.pm.PackageManager
import android.view.View
import androidx.annotation.IdRes
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.commit
import com.zeapo.pwdstore.R
/**
* Check if [permission] is granted to the app. Aliases to [isPermissionGranted] internally.
*/
fun Fragment.isPermissionGranted(permission: String): Boolean {
return ContextCompat.checkSelfPermission(requireActivity(), permission) == PackageManager.PERMISSION_GRANTED
return requireActivity().isPermissionGranted(permission)
}
/**
* Calls `finish()` on the enclosing [androidx.fragment.app.FragmentActivity]
*/
fun Fragment.finish() = requireActivity().finish()
fun FragmentManager.performTransaction(destinationFragment: Fragment, @IdRes containerViewId: Int = android.R.id.content) {
this.commit {
beginTransaction()
setCustomAnimations(
R.animator.slide_in_left,
R.animator.slide_out_left,
R.animator.slide_in_right,
R.animator.slide_out_right)
replace(containerViewId, destinationFragment)
}
}
/**
* Perform a [commit] on this [FragmentManager] with custom animations and adding the [destinationFragment]
* to the fragment backstack
*/
fun FragmentManager.performTransactionWithBackStack(destinationFragment: Fragment, @IdRes containerViewId: Int = android.R.id.content) {
this.commit {
commit {
beginTransaction()
addToBackStack(destinationFragment.tag)
setCustomAnimations(
@ -39,14 +34,3 @@ fun FragmentManager.performTransactionWithBackStack(destinationFragment: Fragmen
replace(containerViewId, destinationFragment)
}
}
fun FragmentManager.performSharedElementTransaction(destinationFragment: Fragment, views: List<View>, @IdRes containerViewId: Int = android.R.id.content) {
this.commit {
beginTransaction()
for (view in views) {
addSharedElement(view, view.transitionName)
}
addToBackStack(destinationFragment.tag)
replace(containerViewId, destinationFragment)
}
}

View file

@ -83,9 +83,6 @@ object Dependencies {
const val runner = "androidx.test:runner:1.3.0"
const val rules = "androidx.test:rules:1.3.0"
const val junit = "androidx.test.ext:junit:1.1.2"
const val espresso_core = "androidx.test.espresso:espresso-core:3.3.0"
const val espresso_intents = "androidx.test.espresso:espresso-intents:3.3.0"
}
}
}