diff options
Diffstat (limited to 'app/src/main/java/dev/msfjarvis/aps/util/extensions')
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) + } |
