diff options
Diffstat (limited to 'app/src/main/java/com/zeapo/pwdstore/utils')
3 files changed, 209 insertions, 154 deletions
diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/AndroidExtensions.kt b/app/src/main/java/com/zeapo/pwdstore/utils/AndroidExtensions.kt new file mode 100644 index 00000000..3ee1820d --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/utils/AndroidExtensions.kt @@ -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) +} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt index 2a11f56a..10a29cf0 100644 --- a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt +++ b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt @@ -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() +} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/FragmentExtensions.kt b/app/src/main/java/com/zeapo/pwdstore/utils/FragmentExtensions.kt index 01103d19..251f5259 100644 --- a/app/src/main/java/com/zeapo/pwdstore/utils/FragmentExtensions.kt +++ b/app/src/main/java/com/zeapo/pwdstore/utils/FragmentExtensions.kt @@ -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) - } -} |
