aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java/dev/msfjarvis/aps/util/extensions
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/dev/msfjarvis/aps/util/extensions')
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/util/extensions/AndroidExtensions.kt179
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/util/extensions/Extensions.kt90
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/util/extensions/FragmentExtensions.kt36
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/util/extensions/FragmentViewBindingDelegate.kt65
4 files changed, 370 insertions, 0 deletions
diff --git a/app/src/main/java/dev/msfjarvis/aps/util/extensions/AndroidExtensions.kt b/app/src/main/java/dev/msfjarvis/aps/util/extensions/AndroidExtensions.kt
new file mode 100644
index 00000000..308c5966
--- /dev/null
+++ b/app/src/main/java/dev/msfjarvis/aps/util/extensions/AndroidExtensions.kt
@@ -0,0 +1,179 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package dev.msfjarvis.aps.util.extensions
+
+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 dev.msfjarvis.aps.R
+import dev.msfjarvis.aps.data.repo.PasswordRepository
+import dev.msfjarvis.aps.util.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")
+
+/**
+ * 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 {
+ 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/dev/msfjarvis/aps/util/extensions/Extensions.kt b/app/src/main/java/dev/msfjarvis/aps/util/extensions/Extensions.kt
new file mode 100644
index 00000000..dbfc0f63
--- /dev/null
+++ b/app/src/main/java/dev/msfjarvis/aps/util/extensions/Extensions.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+package dev.msfjarvis.aps.util.extensions
+
+import com.github.michaelbull.result.getOrElse
+import com.github.michaelbull.result.runCatching
+import dev.msfjarvis.aps.data.repo.PasswordRepository
+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
+}
+
+/**
+ * Checks whether this [File] is a directory that contains [other].
+ */
+fun File.contains(other: File): Boolean {
+ if (!isDirectory)
+ return false
+ if (!other.exists())
+ return false
+ val relativePath = runCatching {
+ other.relativeTo(this)
+ }.getOrElse {
+ return false
+ }
+ // Direct containment is equivalent to the relative path being equal to the filename.
+ return relativePath.path == other.name
+}
+
+/**
+ * Checks if this [File] is in the password repository directory as given
+ * by [PasswordRepository.getRepositoryDirectory]
+ */
+fun File.isInsideRepository(): Boolean {
+ return canonicalPath.contains(PasswordRepository.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.getId
+ */
+val RevCommit.hash: String
+ get() = ObjectId.toString(id)
+
+/**
+ * Time this commit was made with second precision.
+ *
+ * @see RevCommit.commitTime
+ */
+val RevCommit.time: Date
+ get() {
+ val epochSeconds = commitTime.toLong()
+ 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/dev/msfjarvis/aps/util/extensions/FragmentExtensions.kt b/app/src/main/java/dev/msfjarvis/aps/util/extensions/FragmentExtensions.kt
new file mode 100644
index 00000000..642e0f22
--- /dev/null
+++ b/app/src/main/java/dev/msfjarvis/aps/util/extensions/FragmentExtensions.kt
@@ -0,0 +1,36 @@
+package dev.msfjarvis.aps.util.extensions
+
+import androidx.annotation.IdRes
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.commit
+import dev.msfjarvis.aps.R
+
+/**
+ * Check if [permission] is granted to the app. Aliases to [isPermissionGranted] internally.
+ */
+fun Fragment.isPermissionGranted(permission: String): Boolean {
+ return requireActivity().isPermissionGranted(permission)
+}
+
+/**
+ * Calls `finish()` on the enclosing [androidx.fragment.app.FragmentActivity]
+ */
+fun Fragment.finish() = requireActivity().finish()
+
+/**
+ * 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) {
+ commit {
+ beginTransaction()
+ addToBackStack(destinationFragment.tag)
+ setCustomAnimations(
+ R.animator.slide_in_left,
+ R.animator.slide_out_left,
+ R.animator.slide_in_right,
+ R.animator.slide_out_right)
+ replace(containerViewId, destinationFragment)
+ }
+}
diff --git a/app/src/main/java/dev/msfjarvis/aps/util/extensions/FragmentViewBindingDelegate.kt b/app/src/main/java/dev/msfjarvis/aps/util/extensions/FragmentViewBindingDelegate.kt
new file mode 100644
index 00000000..03342d31
--- /dev/null
+++ b/app/src/main/java/dev/msfjarvis/aps/util/extensions/FragmentViewBindingDelegate.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package dev.msfjarvis.aps.util.extensions
+
+
+import android.view.LayoutInflater
+import android.view.View
+import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.viewbinding.ViewBinding
+import kotlin.properties.ReadOnlyProperty
+import kotlin.reflect.KProperty
+
+/**
+ * Imported from https://medium.com/@Zhuinden/simple-one-liner-viewbinding-in-fragments-and-activities-with-kotlin-961430c6c07c
+ */
+class FragmentViewBindingDelegate<T : ViewBinding>(
+ val fragment: Fragment,
+ val viewBindingFactory: (View) -> T
+) : ReadOnlyProperty<Fragment, T> {
+
+ private var binding: T? = null
+
+ init {
+ fragment.lifecycle.addObserver(object : DefaultLifecycleObserver {
+ override fun onCreate(owner: LifecycleOwner) {
+ fragment.viewLifecycleOwnerLiveData.observe(fragment) { viewLifecycleOwner ->
+ viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
+ override fun onDestroy(owner: LifecycleOwner) {
+ binding = null
+ }
+ })
+ }
+ }
+ })
+ }
+
+ override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
+ val binding = binding
+ if (binding != null) {
+ return binding
+ }
+
+ val lifecycle = fragment.viewLifecycleOwner.lifecycle
+ if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) {
+ throw IllegalStateException("Should not attempt to get bindings when Fragment views are destroyed.")
+ }
+
+ return viewBindingFactory(thisRef.requireView()).also { this.binding = it }
+ }
+}
+
+fun <T : ViewBinding> Fragment.viewBinding(viewBindingFactory: (View) -> T) =
+ FragmentViewBindingDelegate(this, viewBindingFactory)
+
+inline fun <T : ViewBinding> AppCompatActivity.viewBinding(crossinline bindingInflater: (LayoutInflater) -> T) =
+ lazy(LazyThreadSafetyMode.NONE) {
+ bindingInflater.invoke(layoutInflater)
+ }