mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Merge branch 'Dev-ClayP-master' into develop
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
name: Bug Report
|
||||
description: Report a bug.
|
||||
title: ""
|
||||
labels: bug
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
@@ -1,7 +1,6 @@
|
||||
name: Feature request
|
||||
description: Suggest an idea.
|
||||
title: ""
|
||||
labels: feature
|
||||
labels: ["feature"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -9,7 +9,7 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.kunzisoft.keepass"
|
||||
minSdkVersion 15
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 34
|
||||
versionCode = 139
|
||||
versionName = "4.1.7"
|
||||
|
||||
@@ -359,77 +359,40 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
||||
|
||||
isViewTranslateAnimationRunning = true
|
||||
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
imageView.run {
|
||||
val translationY = if (velY > 0) {
|
||||
originalViewBounds.top + height - top
|
||||
} else {
|
||||
originalViewBounds.top - height - top
|
||||
}
|
||||
animate()
|
||||
.setDuration(dismissAnimationDuration)
|
||||
.setInterpolator(dismissAnimationInterpolator)
|
||||
.translationY(translationY.toFloat())
|
||||
.setUpdateListener {
|
||||
val amount = calcTranslationAmount()
|
||||
changeBackgroundAlpha(amount)
|
||||
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
||||
}
|
||||
.setListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator) {
|
||||
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
onViewTranslateListener?.onDismiss(imageView)
|
||||
cleanup()
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, if (velY > 0) {
|
||||
originalViewBounds.top + imageView.height - imageView.top
|
||||
imageView.run {
|
||||
val translationY = if (velY > 0) {
|
||||
originalViewBounds.top + height - top
|
||||
} else {
|
||||
originalViewBounds.top - imageView.height - imageView.top
|
||||
}.toFloat()).apply {
|
||||
duration = dismissAnimationDuration
|
||||
interpolator = dismissAnimationInterpolator
|
||||
addUpdateListener {
|
||||
val amount = calcTranslationAmount()
|
||||
changeBackgroundAlpha(amount)
|
||||
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
||||
}
|
||||
addListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
onViewTranslateListener?.onDismiss(imageView)
|
||||
cleanup()
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
start()
|
||||
originalViewBounds.top - height - top
|
||||
}
|
||||
animate()
|
||||
.setDuration(dismissAnimationDuration)
|
||||
.setInterpolator(dismissAnimationInterpolator)
|
||||
.translationY(translationY.toFloat())
|
||||
.setUpdateListener {
|
||||
val amount = calcTranslationAmount()
|
||||
changeBackgroundAlpha(amount)
|
||||
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
||||
}
|
||||
.setListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator) {
|
||||
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
onViewTranslateListener?.onDismiss(imageView)
|
||||
cleanup()
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -657,137 +620,76 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
||||
|
||||
private fun restoreViewTransform() {
|
||||
val imageView = imageViewRef.get() ?: return
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
imageView.run {
|
||||
animate()
|
||||
.setDuration(restoreAnimationDuration)
|
||||
.setInterpolator(restoreAnimationInterpolator)
|
||||
.translationY((originalViewBounds.top - top).toFloat())
|
||||
.setUpdateListener {
|
||||
val amount = calcTranslationAmount()
|
||||
changeBackgroundAlpha(amount)
|
||||
onViewTranslateListener?.onViewTranslate(this, amount)
|
||||
imageView.run {
|
||||
animate()
|
||||
.setDuration(restoreAnimationDuration)
|
||||
.setInterpolator(restoreAnimationInterpolator)
|
||||
.translationY((originalViewBounds.top - top).toFloat())
|
||||
.setUpdateListener {
|
||||
val amount = calcTranslationAmount()
|
||||
changeBackgroundAlpha(amount)
|
||||
onViewTranslateListener?.onViewTranslate(this, amount)
|
||||
}
|
||||
.setListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
.setListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(p0: Animator) {
|
||||
onViewTranslateListener?.onRestore(imageView)
|
||||
}
|
||||
override fun onAnimationEnd(p0: Animator) {
|
||||
onViewTranslateListener?.onRestore(imageView)
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
override fun onAnimationCancel(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, (originalViewBounds.top - imageView.top).toFloat()).apply {
|
||||
duration = restoreAnimationDuration
|
||||
interpolator = restoreAnimationInterpolator
|
||||
addUpdateListener {
|
||||
val amount = calcTranslationAmount()
|
||||
changeBackgroundAlpha(amount)
|
||||
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
||||
}
|
||||
addListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(p0: Animator) {
|
||||
onViewTranslateListener?.onRestore(imageView)
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
start()
|
||||
}
|
||||
override fun onAnimationRepeat(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun startDragToDismissAnimation() {
|
||||
val imageView = imageViewRef.get() ?: return
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
imageView.run {
|
||||
val translationY = if (y - initialY > 0) {
|
||||
originalViewBounds.top + height - top
|
||||
} else {
|
||||
originalViewBounds.top - height - top
|
||||
}
|
||||
animate()
|
||||
.setDuration(dismissAnimationDuration)
|
||||
.setInterpolator(AccelerateDecelerateInterpolator())
|
||||
.translationY(translationY.toFloat())
|
||||
.setUpdateListener {
|
||||
val amount = calcTranslationAmount()
|
||||
changeBackgroundAlpha(amount)
|
||||
onViewTranslateListener?.onViewTranslate(this, amount)
|
||||
imageView.run {
|
||||
val translationY = if (y - initialY > 0) {
|
||||
originalViewBounds.top + height - top
|
||||
} else {
|
||||
originalViewBounds.top - height - top
|
||||
}
|
||||
animate()
|
||||
.setDuration(dismissAnimationDuration)
|
||||
.setInterpolator(AccelerateDecelerateInterpolator())
|
||||
.translationY(translationY.toFloat())
|
||||
.setUpdateListener {
|
||||
val amount = calcTranslationAmount()
|
||||
changeBackgroundAlpha(amount)
|
||||
onViewTranslateListener?.onViewTranslate(this, amount)
|
||||
}
|
||||
.setListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = true
|
||||
}
|
||||
.setListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = true
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
onViewTranslateListener?.onDismiss(imageView)
|
||||
cleanup()
|
||||
}
|
||||
override fun onAnimationEnd(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
onViewTranslateListener?.onDismiss(imageView)
|
||||
cleanup()
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
}
|
||||
override fun onAnimationCancel(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, imageView.translationY).apply {
|
||||
duration = dismissAnimationDuration
|
||||
interpolator = AccelerateDecelerateInterpolator()
|
||||
addUpdateListener {
|
||||
val amount = calcTranslationAmount()
|
||||
changeBackgroundAlpha(amount)
|
||||
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
||||
}
|
||||
addListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = true
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
onViewTranslateListener?.onDismiss(imageView)
|
||||
cleanup()
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
start()
|
||||
}
|
||||
override fun onAnimationRepeat(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun processFlingToDismiss(velocityY: Float) {
|
||||
|
||||
@@ -78,9 +78,8 @@ class AboutActivity : StylishActivity() {
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
text = HtmlCompat.fromHtml(getString(R.string.html_about_licence, DateTime().year),
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
textDirection = View.TEXT_DIRECTION_ANY_RTL
|
||||
}
|
||||
textDirection = View.TEXT_DIRECTION_ANY_RTL
|
||||
|
||||
}
|
||||
|
||||
findViewById<TextView>(R.id.activity_about_privacy_text).apply {
|
||||
|
||||
@@ -604,16 +604,12 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
isVisible = isEnabled
|
||||
}
|
||||
menu?.findItem(R.id.menu_add_attachment)?.apply {
|
||||
// Attachment not compatible below KitKat
|
||||
isEnabled = !mIsTemplate
|
||||
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
|
||||
isVisible = isEnabled
|
||||
}
|
||||
menu?.findItem(R.id.menu_add_otp)?.apply {
|
||||
// OTP not compatible below KitKat
|
||||
isEnabled = mAllowOTP
|
||||
&& !mIsTemplate
|
||||
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
|
||||
isVisible = isEnabled
|
||||
}
|
||||
return super.onPrepareOptionsMenu(menu)
|
||||
|
||||
@@ -70,7 +70,6 @@ import com.kunzisoft.keepass.utils.MagikeyboardUtil
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
|
||||
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||
import com.kunzisoft.keepass.utils.allowCreateDocumentByStorageAccessFramework
|
||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||
import com.kunzisoft.keepass.view.asError
|
||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||
@@ -330,13 +329,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
// Show open and create button or special mode
|
||||
when (mSpecialMode) {
|
||||
SpecialMode.DEFAULT -> {
|
||||
if (packageManager.allowCreateDocumentByStorageAccessFramework()) {
|
||||
// There is an activity which can handle this intent.
|
||||
createDatabaseButtonView?.visibility = View.VISIBLE
|
||||
} else{
|
||||
// No Activity found that can handle this intent.
|
||||
createDatabaseButtonView?.visibility = View.GONE
|
||||
}
|
||||
createDatabaseButtonView?.visibility = View.VISIBLE
|
||||
}
|
||||
else -> {
|
||||
// Disable create button if in selection mode or request for autofill
|
||||
|
||||
@@ -530,9 +530,8 @@ class NodesAdapter (
|
||||
holder?.otpToken?.apply {
|
||||
text = otpElement?.tokenString
|
||||
setTextSize(mTextSizeUnit, mOtpTokenTextDefaultDimension, mPrefSizeMultiplier)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
textDirection = View.TEXT_DIRECTION_LTR
|
||||
}
|
||||
textDirection = View.TEXT_DIRECTION_LTR
|
||||
|
||||
}
|
||||
holder?.otpContainer?.setOnClickListener {
|
||||
otpElement?.token?.let { token ->
|
||||
|
||||
@@ -38,7 +38,6 @@ class App : MultiDexApplication() {
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifecycleObserver)
|
||||
|
||||
Stylish.load(this)
|
||||
PRNGFixes.apply()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,399 +0,0 @@
|
||||
package com.kunzisoft.keepass.app;
|
||||
|
||||
/*
|
||||
* This software is provided 'as-is', without any express or implied
|
||||
* warranty. In no event will Google be held liable for any damages
|
||||
* arising from the use of this software.
|
||||
*
|
||||
* Permission is granted to anyone to use this software for any purpose,
|
||||
* including commercial applications, and to alter it and redistribute it
|
||||
* freely, as long as the origin is not misrepresented.
|
||||
*/
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.Process;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.Provider;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.SecureRandomSpi;
|
||||
import java.security.Security;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Fixes for the output of the default PRNG having low entropy.
|
||||
*
|
||||
* The fixes need to be applied via {@link #apply()} before any use of Java
|
||||
* Cryptography Architecture primitives. A good place to invoke them is in the
|
||||
* application's {@code onCreate}.
|
||||
*/
|
||||
public final class PRNGFixes {
|
||||
|
||||
private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL =
|
||||
getBuildFingerprintAndDeviceSerial();
|
||||
|
||||
/** Hidden constructor to prevent instantiation. */
|
||||
private PRNGFixes() {}
|
||||
|
||||
/**
|
||||
* Applies all fixes.
|
||||
*
|
||||
* @throws SecurityException if a fix is needed but could not be applied.
|
||||
*/
|
||||
public static void apply() {
|
||||
try {
|
||||
if (supportedOnThisDevice()) {
|
||||
applyOpenSSLFix();
|
||||
installLinuxPRNGSecureRandom();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Do nothing, do the best we can to implement the workaround
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean supportedOnThisDevice() {
|
||||
// Blacklist on samsung devices
|
||||
if (Build.MANUFACTURER.toLowerCase(Locale.ENGLISH).contains("samsung")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (onSELinuxEnforce()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
File urandom = new File("/dev/urandom");
|
||||
|
||||
// Test permissions
|
||||
if ( !(urandom.canRead() && urandom.canWrite()) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Test actually writing to urandom
|
||||
try {
|
||||
FileOutputStream fos = new FileOutputStream(urandom);
|
||||
fos.write(0);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
private static boolean onSELinuxEnforce() {
|
||||
try {
|
||||
ProcessBuilder builder = new ProcessBuilder("getenforce");
|
||||
builder.redirectErrorStream(true);
|
||||
java.lang.Process process = builder.start();
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
||||
process.waitFor();
|
||||
|
||||
String output = reader.readLine();
|
||||
|
||||
if (output == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return output.toLowerCase(Locale.US).startsWith("enforcing");
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the
|
||||
* fix is not needed.
|
||||
*
|
||||
* @throws SecurityException if the fix is needed but could not be applied.
|
||||
*/
|
||||
private static void applyOpenSSLFix() throws SecurityException {
|
||||
if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN)
|
||||
|| (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2)) {
|
||||
// No need to apply the fix
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Mix in the device- and invocation-specific seed.
|
||||
Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto")
|
||||
.getMethod("RAND_seed", byte[].class)
|
||||
.invoke(null, generateSeed());
|
||||
|
||||
// Mix output of Linux PRNG into OpenSSL's PRNG
|
||||
int bytesRead = (Integer) Class.forName(
|
||||
"org.apache.harmony.xnet.provider.jsse.NativeCrypto")
|
||||
.getMethod("RAND_load_file", String.class, long.class)
|
||||
.invoke(null, "/dev/urandom", 1024);
|
||||
if (bytesRead != 1024) {
|
||||
throw new IOException(
|
||||
"Unexpected number of bytes read from Linux PRNG: "
|
||||
+ bytesRead);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new SecurityException("Failed to seed OpenSSL PRNG", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs a Linux PRNG-backed {@code SecureRandom} implementation as the
|
||||
* default. Does nothing if the implementation is already the default or if
|
||||
* there is not need to install the implementation.
|
||||
*
|
||||
* @throws SecurityException if the fix is needed but could not be applied.
|
||||
*/
|
||||
private static void installLinuxPRNGSecureRandom()
|
||||
throws SecurityException {
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
// No need to apply the fix
|
||||
return;
|
||||
}
|
||||
|
||||
// Install a Linux PRNG-based SecureRandom implementation as the
|
||||
// default, if not yet installed.
|
||||
Provider[] secureRandomProviders =
|
||||
Security.getProviders("SecureRandom.SHA1PRNG");
|
||||
if ((secureRandomProviders == null)
|
||||
|| (secureRandomProviders.length < 1)
|
||||
|| (!LinuxPRNGSecureRandomProvider.class.equals(
|
||||
secureRandomProviders[0].getClass()))) {
|
||||
Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1);
|
||||
}
|
||||
|
||||
// Assert that new SecureRandom() and
|
||||
// SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed
|
||||
// by the Linux PRNG-based SecureRandom implementation.
|
||||
SecureRandom rng1 = new SecureRandom();
|
||||
if (!LinuxPRNGSecureRandomProvider.class.equals(
|
||||
rng1.getProvider().getClass())) {
|
||||
throw new SecurityException(
|
||||
"new SecureRandom() backed by wrong Provider: "
|
||||
+ rng1.getProvider().getClass());
|
||||
}
|
||||
|
||||
SecureRandom rng2;
|
||||
try {
|
||||
rng2 = SecureRandom.getInstance("SHA1PRNG");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new SecurityException("SHA1PRNG not available", e);
|
||||
}
|
||||
if (!LinuxPRNGSecureRandomProvider.class.equals(
|
||||
rng2.getProvider().getClass())) {
|
||||
throw new SecurityException(
|
||||
"SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong"
|
||||
+ " Provider: " + rng2.getProvider().getClass());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code Provider} of {@code SecureRandom} engines which pass through
|
||||
* all requests to the Linux PRNG.
|
||||
*/
|
||||
private static class LinuxPRNGSecureRandomProvider extends Provider {
|
||||
|
||||
public LinuxPRNGSecureRandomProvider() {
|
||||
super("LinuxPRNG",
|
||||
1.0,
|
||||
"A Linux-specific random number provider that uses"
|
||||
+ " /dev/urandom");
|
||||
// Although /dev/urandom is not a SHA-1 PRNG, some apps
|
||||
// explicitly request a SHA1PRNG SecureRandom and we thus need to
|
||||
// prevent them from getting the default implementation whose output
|
||||
// may have low entropy.
|
||||
put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName());
|
||||
put("SecureRandom.SHA1PRNG ImplementedIn", "Software");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link SecureRandomSpi} which passes all requests to the Linux PRNG
|
||||
* ({@code /dev/urandom}).
|
||||
*/
|
||||
public static class LinuxPRNGSecureRandom extends SecureRandomSpi {
|
||||
|
||||
/*
|
||||
* IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed
|
||||
* are passed through to the Linux PRNG (/dev/urandom). Instances of
|
||||
* this class seed themselves by mixing in the current time, PID, UID,
|
||||
* build fingerprint, and hardware serial number (where available) into
|
||||
* Linux PRNG.
|
||||
*
|
||||
* Concurrency: Read requests to the underlying Linux PRNG are
|
||||
* serialized (on sLock) to ensure that multiple threads do not get
|
||||
* duplicated PRNG output.
|
||||
*/
|
||||
|
||||
private static final File URANDOM_FILE = new File("/dev/urandom");
|
||||
|
||||
|
||||
private static final Object sLock = new Object();
|
||||
|
||||
/**
|
||||
* Input stream for reading from Linux PRNG or {@code null} if not yet
|
||||
* opened.
|
||||
*
|
||||
* @GuardedBy("sLock")
|
||||
*/
|
||||
private static DataInputStream sUrandomIn;
|
||||
|
||||
/**
|
||||
* Output stream for writing to Linux PRNG or {@code null} if not yet
|
||||
* opened.
|
||||
*
|
||||
* @GuardedBy("sLock")
|
||||
*/
|
||||
private static OutputStream sUrandomOut;
|
||||
|
||||
/**
|
||||
* Whether this engine instance has been seeded. This is needed because
|
||||
* each instance needs to seed itself if the client does not explicitly
|
||||
* seed it.
|
||||
*/
|
||||
private boolean mSeeded;
|
||||
|
||||
@Override
|
||||
protected void engineSetSeed(byte[] bytes) {
|
||||
try {
|
||||
OutputStream out;
|
||||
synchronized (sLock) {
|
||||
out = getUrandomOutputStream();
|
||||
}
|
||||
out.write(bytes);
|
||||
out.flush();
|
||||
mSeeded = true;
|
||||
} catch (IOException e) {
|
||||
throw new SecurityException(
|
||||
"Failed to mix seed into " + URANDOM_FILE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void engineNextBytes(byte[] bytes) {
|
||||
if (!mSeeded) {
|
||||
// Mix in the device- and invocation-specific seed.
|
||||
engineSetSeed(generateSeed());
|
||||
}
|
||||
|
||||
try {
|
||||
DataInputStream in;
|
||||
synchronized (sLock) {
|
||||
in = getUrandomInputStream();
|
||||
}
|
||||
synchronized (in) {
|
||||
in.readFully(bytes);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new SecurityException(
|
||||
"Failed to read from " + URANDOM_FILE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] engineGenerateSeed(int size) {
|
||||
byte[] seed = new byte[size];
|
||||
engineNextBytes(seed);
|
||||
return seed;
|
||||
}
|
||||
|
||||
private DataInputStream getUrandomInputStream() {
|
||||
synchronized (sLock) {
|
||||
if (sUrandomIn == null) {
|
||||
// NOTE: Consider inserting a BufferedInputStream between
|
||||
// DataInputStream and FileInputStream if you need higher
|
||||
// PRNG output performance and can live with future PRNG
|
||||
// output being pulled into this process prematurely.
|
||||
try {
|
||||
sUrandomIn = new DataInputStream(
|
||||
new FileInputStream(URANDOM_FILE));
|
||||
} catch (IOException e) {
|
||||
throw new SecurityException("Failed to open "
|
||||
+ URANDOM_FILE + " for reading", e);
|
||||
}
|
||||
}
|
||||
return sUrandomIn;
|
||||
}
|
||||
}
|
||||
|
||||
private OutputStream getUrandomOutputStream() {
|
||||
synchronized (sLock) {
|
||||
if (sUrandomOut == null) {
|
||||
try {
|
||||
sUrandomOut = new FileOutputStream(URANDOM_FILE);
|
||||
} catch (IOException e) {
|
||||
throw new SecurityException("Failed to open "
|
||||
+ URANDOM_FILE + " for writing", e);
|
||||
}
|
||||
}
|
||||
return sUrandomOut;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a device- and invocation-specific seed to be mixed into the
|
||||
* Linux PRNG.
|
||||
*/
|
||||
private static byte[] generateSeed() {
|
||||
try {
|
||||
ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream();
|
||||
DataOutputStream seedBufferOut =
|
||||
new DataOutputStream(seedBuffer);
|
||||
seedBufferOut.writeLong(System.currentTimeMillis());
|
||||
seedBufferOut.writeLong(System.nanoTime());
|
||||
seedBufferOut.writeInt(Process.myPid());
|
||||
seedBufferOut.writeInt(Process.myUid());
|
||||
seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL);
|
||||
seedBufferOut.close();
|
||||
return seedBuffer.toByteArray();
|
||||
} catch (IOException e) {
|
||||
throw new SecurityException("Failed to generate seed", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the hardware serial number of this device.
|
||||
*
|
||||
* @return serial number or {@code null} if not available.
|
||||
*/
|
||||
private static String getDeviceSerialNumber() {
|
||||
// We're using the Reflection API because Build.SERIAL is only available
|
||||
// since API Level 9 (Gingerbread, Android 2.3).
|
||||
try {
|
||||
return (String) Build.class.getField("SERIAL").get(null);
|
||||
} catch (Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] getBuildFingerprintAndDeviceSerial() {
|
||||
StringBuilder result = new StringBuilder();
|
||||
String fingerprint = Build.FINGERPRINT;
|
||||
if (fingerprint != null) {
|
||||
result.append(fingerprint);
|
||||
}
|
||||
String serial = getDeviceSerialNumber();
|
||||
if (serial != null) {
|
||||
result.append(serial);
|
||||
}
|
||||
try {
|
||||
return result.toString().getBytes("UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException("UTF-8 encoding not supported");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,27 +65,19 @@ object TimeoutHelper {
|
||||
(context.applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager?)?.let { alarmManager ->
|
||||
val triggerTime = System.currentTimeMillis() + timeout
|
||||
Log.d(TAG, "TimeoutHelper start")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
&& !alarmManager.canScheduleExactAlarms()) {
|
||||
alarmManager.set(
|
||||
AlarmManager.RTC,
|
||||
triggerTime,
|
||||
getLockPendingIntent(context)
|
||||
)
|
||||
} else {
|
||||
alarmManager.setExact(
|
||||
AlarmManager.RTC,
|
||||
triggerTime,
|
||||
getLockPendingIntent(context)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
&& !alarmManager.canScheduleExactAlarms()) {
|
||||
alarmManager.set(
|
||||
AlarmManager.RTC,
|
||||
triggerTime,
|
||||
getLockPendingIntent(context)
|
||||
)
|
||||
} else {
|
||||
alarmManager.setExact(
|
||||
AlarmManager.RTC,
|
||||
triggerTime,
|
||||
getLockPendingIntent(context)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,27 +77,19 @@ class LockReceiver(private var lockAction: () -> Unit) : BroadcastReceiver() {
|
||||
// Launch the effective action after a small time
|
||||
val first: Long = System.currentTimeMillis() + context.getString(R.string.timeout_screen_off).toLong()
|
||||
(context.getSystemService(ALARM_SERVICE) as AlarmManager?)?.let { alarmManager ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
&& !alarmManager.canScheduleExactAlarms()) {
|
||||
alarmManager.set(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
first,
|
||||
lockPendingIntent
|
||||
)
|
||||
} else {
|
||||
alarmManager.setExact(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
first,
|
||||
lockPendingIntent
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
&& !alarmManager.canScheduleExactAlarms()) {
|
||||
alarmManager.set(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
first,
|
||||
lockPendingIntent
|
||||
)
|
||||
} else {
|
||||
alarmManager.setExact(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
first,
|
||||
lockPendingIntent
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -67,55 +67,53 @@ object UriUtil {
|
||||
readOnly: Boolean) {
|
||||
try {
|
||||
// try to persist read and write permissions
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
contentResolver?.apply {
|
||||
var readPermissionAllowed = false
|
||||
var writePermissionAllowed = false
|
||||
// Check current permissions allowed
|
||||
persistedUriPermissions.find { uriPermission ->
|
||||
uriPermission.uri == uri
|
||||
}?.let { uriPermission ->
|
||||
Log.d(TAG, "Check URI permission : $uriPermission")
|
||||
if (uriPermission.isReadPermission) {
|
||||
readPermissionAllowed = true
|
||||
}
|
||||
if (uriPermission.isWritePermission) {
|
||||
writePermissionAllowed = true
|
||||
}
|
||||
contentResolver?.apply {
|
||||
var readPermissionAllowed = false
|
||||
var writePermissionAllowed = false
|
||||
// Check current permissions allowed
|
||||
persistedUriPermissions.find { uriPermission ->
|
||||
uriPermission.uri == uri
|
||||
}?.let { uriPermission ->
|
||||
Log.d(TAG, "Check URI permission : $uriPermission")
|
||||
if (uriPermission.isReadPermission) {
|
||||
readPermissionAllowed = true
|
||||
}
|
||||
|
||||
// Release permission
|
||||
if (release) {
|
||||
if (writePermissionAllowed) {
|
||||
Log.d(TAG, "Release write permission : $uri")
|
||||
val removeFlags: Int = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
releasePersistableUriPermission(uri, removeFlags)
|
||||
}
|
||||
if (readPermissionAllowed) {
|
||||
Log.d(TAG, "Release read permission $uri")
|
||||
val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
releasePersistableUriPermission(uri, takeFlags)
|
||||
}
|
||||
if (uriPermission.isWritePermission) {
|
||||
writePermissionAllowed = true
|
||||
}
|
||||
}
|
||||
|
||||
// Take missing permission
|
||||
if (!readPermissionAllowed) {
|
||||
Log.d(TAG, "Take read permission $uri")
|
||||
// Release permission
|
||||
if (release) {
|
||||
if (writePermissionAllowed) {
|
||||
Log.d(TAG, "Release write permission : $uri")
|
||||
val removeFlags: Int = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
releasePersistableUriPermission(uri, removeFlags)
|
||||
}
|
||||
if (readPermissionAllowed) {
|
||||
Log.d(TAG, "Release read permission $uri")
|
||||
val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
takePersistableUriPermission(uri, takeFlags)
|
||||
releasePersistableUriPermission(uri, takeFlags)
|
||||
}
|
||||
if (readOnly) {
|
||||
if (writePermissionAllowed) {
|
||||
Log.d(TAG, "Release write permission $uri")
|
||||
val removeFlags: Int = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
releasePersistableUriPermission(uri, removeFlags)
|
||||
}
|
||||
} else {
|
||||
if (!writePermissionAllowed) {
|
||||
Log.d(TAG, "Take write permission $uri")
|
||||
val takeFlags: Int = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
takePersistableUriPermission(uri, takeFlags)
|
||||
}
|
||||
}
|
||||
|
||||
// Take missing permission
|
||||
if (!readPermissionAllowed) {
|
||||
Log.d(TAG, "Take read permission $uri")
|
||||
val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
takePersistableUriPermission(uri, takeFlags)
|
||||
}
|
||||
if (readOnly) {
|
||||
if (writePermissionAllowed) {
|
||||
Log.d(TAG, "Release write permission $uri")
|
||||
val removeFlags: Int = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
releasePersistableUriPermission(uri, removeFlags)
|
||||
}
|
||||
} else {
|
||||
if (!writePermissionAllowed) {
|
||||
Log.d(TAG, "Take write permission $uri")
|
||||
val takeFlags: Int = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
takePersistableUriPermission(uri, takeFlags)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,42 +138,38 @@ object UriUtil {
|
||||
}
|
||||
|
||||
fun Context.releaseAllUnnecessaryPermissionUris() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
applicationContext?.let { appContext ->
|
||||
val fileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(appContext)
|
||||
fileDatabaseHistoryAction.getDatabaseFileList { databaseFileList ->
|
||||
val listToNotRemove = mutableListOf<Uri>()
|
||||
databaseFileList.forEach {
|
||||
it.databaseUri?.let { databaseUri ->
|
||||
listToNotRemove.add(databaseUri)
|
||||
}
|
||||
it.keyFileUri?.let { keyFileUri ->
|
||||
listToNotRemove.add(keyFileUri)
|
||||
}
|
||||
applicationContext?.let { appContext ->
|
||||
val fileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(appContext)
|
||||
fileDatabaseHistoryAction.getDatabaseFileList { databaseFileList ->
|
||||
val listToNotRemove = mutableListOf<Uri>()
|
||||
databaseFileList.forEach {
|
||||
it.databaseUri?.let { databaseUri ->
|
||||
listToNotRemove.add(databaseUri)
|
||||
}
|
||||
// Remove URI permission for not database files
|
||||
val resolver = appContext.contentResolver
|
||||
resolver.persistedUriPermissions.forEach { uriPermission ->
|
||||
val uri = uriPermission.uri
|
||||
if (!listToNotRemove.contains(uri))
|
||||
resolver.releaseUriPermission(uri)
|
||||
it.keyFileUri?.let { keyFileUri ->
|
||||
listToNotRemove.add(keyFileUri)
|
||||
}
|
||||
}
|
||||
// Remove URI permission for not database files
|
||||
val resolver = appContext.contentResolver
|
||||
resolver.persistedUriPermissions.forEach { uriPermission ->
|
||||
val uri = uriPermission.uri
|
||||
if (!listToNotRemove.contains(uri))
|
||||
resolver.releaseUriPermission(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Intent.getUri(key: String): Uri? {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
val clipData = this.clipData
|
||||
if (clipData != null) {
|
||||
if (clipData.description.label == key) {
|
||||
if (clipData.itemCount == 1) {
|
||||
val clipItem = clipData.getItemAt(0)
|
||||
if (clipItem != null) {
|
||||
return clipItem.uri
|
||||
}
|
||||
val clipData = this.clipData
|
||||
if (clipData != null) {
|
||||
if (clipData.description.label == key) {
|
||||
if (clipData.itemCount == 1) {
|
||||
val clipItem = clipData.getItemAt(0)
|
||||
if (clipItem != null) {
|
||||
return clipItem.uri
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
package com.kunzisoft.keepass.view
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.text.Spannable
|
||||
import android.util.AttributeSet
|
||||
import android.util.TypedValue
|
||||
@@ -104,18 +103,14 @@ class PasswordTextEditFieldView @JvmOverloads constructor(context: Context,
|
||||
id = passwordProgressViewId
|
||||
layoutParams = (layoutParams as LayoutParams?)?.also {
|
||||
it.addRule(LEFT_OF, actionImageButtonId)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
it.addRule(START_OF, actionImageButtonId)
|
||||
}
|
||||
it.addRule(START_OF, actionImageButtonId)
|
||||
}
|
||||
}
|
||||
mPasswordEntropyView.apply {
|
||||
id = passwordEntropyViewId
|
||||
layoutParams = (layoutParams as LayoutParams?)?.also {
|
||||
it.addRule(ALIGN_RIGHT, passwordProgressViewId)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
it.addRule(ALIGN_END, passwordProgressViewId)
|
||||
}
|
||||
it.addRule(ALIGN_END, passwordProgressViewId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,13 +123,11 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
|
||||
setMaxChars(templateAttribute.options.getNumberChars())
|
||||
setMaxLines(templateAttribute.options.getNumberLines())
|
||||
setActionClick(templateAttribute, field, this)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
if (field.protectedValue.isProtected) {
|
||||
textDirection = TEXT_DIRECTION_LTR
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_NO
|
||||
}
|
||||
if (field.protectedValue.isProtected) {
|
||||
textDirection = TEXT_DIRECTION_LTR
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_NO
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,9 +63,7 @@ class TemplateView @JvmOverloads constructor(context: Context,
|
||||
// Here the value is often empty
|
||||
|
||||
if (field.protectedValue.isProtected) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
textDirection = TEXT_DIRECTION_LTR
|
||||
}
|
||||
textDirection = TEXT_DIRECTION_LTR
|
||||
if (mFirstTimeAskAllowCopyProtectedFields) {
|
||||
setCopyButtonState(TextFieldView.ButtonState.DEACTIVATE)
|
||||
setCopyButtonClickListener { _, _ ->
|
||||
@@ -179,9 +177,7 @@ class TemplateView @JvmOverloads constructor(context: Context,
|
||||
otpElement.type.name,
|
||||
ProtectedString(false, otpElement.token)))
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
textDirection = TEXT_DIRECTION_LTR
|
||||
}
|
||||
textDirection = TEXT_DIRECTION_LTR
|
||||
mLastOtpTokenView = this
|
||||
mOtpRunnable = Runnable {
|
||||
if (otpElement.shouldRefreshToken()) {
|
||||
|
||||
@@ -51,9 +51,7 @@ open class TextEditFieldView @JvmOverloads constructor(context: Context,
|
||||
imeOptions = EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING
|
||||
importantForAutofill = IMPORTANT_FOR_AUTOFILL_NO
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO
|
||||
}
|
||||
importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO
|
||||
maxLines = 1
|
||||
}
|
||||
private var actionImageButton = AppCompatImageButton(
|
||||
@@ -70,11 +68,9 @@ open class TextEditFieldView @JvmOverloads constructor(context: Context,
|
||||
resources.displayMetrics
|
||||
).toInt()
|
||||
it.addRule(ALIGN_PARENT_RIGHT)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
it.addRule(ALIGN_PARENT_END)
|
||||
}
|
||||
it.addRule(ALIGN_PARENT_END)
|
||||
}
|
||||
visibility = View.GONE
|
||||
visibility = GONE
|
||||
contentDescription = context.getString(R.string.menu_edit)
|
||||
}
|
||||
|
||||
@@ -91,9 +87,7 @@ open class TextEditFieldView @JvmOverloads constructor(context: Context,
|
||||
id = labelViewId
|
||||
layoutParams = (layoutParams as LayoutParams?)?.also {
|
||||
it.addRule(LEFT_OF, actionImageButtonId)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
it.addRule(START_OF, actionImageButtonId)
|
||||
}
|
||||
it.addRule(START_OF, actionImageButtonId)
|
||||
}
|
||||
}
|
||||
valueView.apply {
|
||||
@@ -192,7 +186,7 @@ open class TextEditFieldView @JvmOverloads constructor(context: Context,
|
||||
actionImageButton.setImageDrawable(ContextCompat.getDrawable(context, it))
|
||||
}
|
||||
actionImageButton.setOnClickListener(onActionClickListener)
|
||||
actionImageButton.visibility = if (onActionClickListener == null) View.GONE else View.VISIBLE
|
||||
actionImageButton.visibility = if (onActionClickListener == null) GONE else VISIBLE
|
||||
}
|
||||
|
||||
override var isFieldVisible: Boolean
|
||||
|
||||
@@ -63,13 +63,12 @@ open class TextFieldView @JvmOverloads constructor(context: Context,
|
||||
4f,
|
||||
resources.displayMetrics
|
||||
).toInt()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
it.marginStart = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
4f,
|
||||
resources.displayMetrics
|
||||
).toInt()
|
||||
}
|
||||
it.marginStart = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
4f,
|
||||
resources.displayMetrics
|
||||
).toInt()
|
||||
|
||||
}
|
||||
}
|
||||
protected val valueView = AppCompatTextView(context).apply {
|
||||
@@ -88,13 +87,11 @@ open class TextFieldView @JvmOverloads constructor(context: Context,
|
||||
8f,
|
||||
resources.displayMetrics
|
||||
).toInt()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
it.marginStart = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
8f,
|
||||
resources.displayMetrics
|
||||
).toInt()
|
||||
}
|
||||
it.marginStart = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
8f,
|
||||
resources.displayMetrics
|
||||
).toInt()
|
||||
}
|
||||
setTextIsSelectable(true)
|
||||
}
|
||||
@@ -128,9 +125,7 @@ open class TextFieldView @JvmOverloads constructor(context: Context,
|
||||
id = copyButtonId
|
||||
layoutParams = (layoutParams as LayoutParams?)?.also {
|
||||
it.addRule(ALIGN_PARENT_RIGHT)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
it.addRule(ALIGN_PARENT_END)
|
||||
}
|
||||
it.addRule(ALIGN_PARENT_END)
|
||||
}
|
||||
}
|
||||
showButton.apply {
|
||||
@@ -138,14 +133,14 @@ open class TextFieldView @JvmOverloads constructor(context: Context,
|
||||
layoutParams = (layoutParams as LayoutParams?)?.also {
|
||||
if (copyButton.isVisible) {
|
||||
it.addRule(LEFT_OF, copyButtonId)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
it.addRule(START_OF, copyButtonId)
|
||||
}
|
||||
|
||||
it.addRule(START_OF, copyButtonId)
|
||||
|
||||
} else {
|
||||
it.addRule(ALIGN_PARENT_RIGHT)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
it.addRule(ALIGN_PARENT_END)
|
||||
}
|
||||
|
||||
it.addRule(ALIGN_PARENT_END)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,18 +148,14 @@ open class TextFieldView @JvmOverloads constructor(context: Context,
|
||||
id = labelViewId
|
||||
layoutParams = (layoutParams as LayoutParams?)?.also {
|
||||
it.addRule(LEFT_OF, showButtonId)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
it.addRule(START_OF, showButtonId)
|
||||
}
|
||||
it.addRule(START_OF, showButtonId)
|
||||
}
|
||||
}
|
||||
valueView.apply {
|
||||
id = valueViewId
|
||||
layoutParams = (layoutParams as LayoutParams?)?.also {
|
||||
it.addRule(LEFT_OF, showButtonId)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
it.addRule(START_OF, showButtonId)
|
||||
}
|
||||
it.addRule(START_OF, showButtonId)
|
||||
it.addRule(BELOW, labelViewId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,12 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.*
|
||||
import android.widget.AdapterView
|
||||
import android.widget.BaseAdapter
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.appcompat.widget.AppCompatImageButton
|
||||
import androidx.core.content.ContextCompat
|
||||
@@ -51,9 +56,7 @@ class TextSelectFieldView @JvmOverloads constructor(context: Context,
|
||||
imeOptions = EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING
|
||||
importantForAutofill = IMPORTANT_FOR_AUTOFILL_NO
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO
|
||||
}
|
||||
importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO
|
||||
val drawable = ContextCompat.getDrawable(context, R.drawable.ic_arrow_down_white_24dp)
|
||||
?.apply {
|
||||
mutate().colorFilter = BlendModeColorFilterCompat
|
||||
@@ -65,14 +68,12 @@ class TextSelectFieldView @JvmOverloads constructor(context: Context,
|
||||
drawable,
|
||||
null
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||
null,
|
||||
null,
|
||||
drawable,
|
||||
null
|
||||
)
|
||||
}
|
||||
setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||
null,
|
||||
null,
|
||||
drawable,
|
||||
null
|
||||
)
|
||||
isFocusable = false
|
||||
inputType = InputType.TYPE_NULL
|
||||
maxLines = 1
|
||||
@@ -94,9 +95,7 @@ class TextSelectFieldView @JvmOverloads constructor(context: Context,
|
||||
resources.displayMetrics
|
||||
).toInt()
|
||||
it.addRule(ALIGN_PARENT_RIGHT)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
it.addRule(ALIGN_PARENT_END)
|
||||
}
|
||||
it.addRule(ALIGN_PARENT_END)
|
||||
}
|
||||
visibility = View.GONE
|
||||
contentDescription = context.getString(R.string.menu_edit)
|
||||
@@ -132,18 +131,14 @@ class TextSelectFieldView @JvmOverloads constructor(context: Context,
|
||||
id = labelViewId
|
||||
layoutParams = (layoutParams as LayoutParams?).also {
|
||||
it?.addRule(LEFT_OF, actionImageButtonId)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
it?.addRule(START_OF, actionImageButtonId)
|
||||
}
|
||||
it?.addRule(START_OF, actionImageButtonId)
|
||||
}
|
||||
}
|
||||
valueSpinnerView.apply {
|
||||
id = valueViewId
|
||||
layoutParams = (layoutParams as LayoutParams?).also {
|
||||
it?.addRule(LEFT_OF, actionImageButtonId)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
it?.addRule(START_OF, actionImageButtonId)
|
||||
}
|
||||
it?.addRule(START_OF, actionImageButtonId)
|
||||
it?.addRule(BELOW, labelViewId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,11 +234,7 @@ fun View.updateLockPaddingStart() {
|
||||
R.dimen.hidden_lock_button_size
|
||||
}
|
||||
).let { lockPadding ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
updatePaddingRelative(lockPadding)
|
||||
} else {
|
||||
updatePadding(lockPadding)
|
||||
}
|
||||
updatePaddingRelative(lockPadding)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -122,8 +122,7 @@
|
||||
android:inputType="textPassword"
|
||||
android:importantForAccessibility="no"
|
||||
android:importantForAutofill="no"
|
||||
android:hint="@string/otp_secret"
|
||||
tools:targetApi="jelly_bean" />
|
||||
android:hint="@string/otp_secret" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -178,8 +177,7 @@
|
||||
tools:text="30"
|
||||
android:maxLength="3"
|
||||
android:digits="0123456789"
|
||||
android:imeOptions="actionNext"
|
||||
tools:targetApi="jelly_bean" />
|
||||
android:imeOptions="actionNext" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/setup_otp_counter_label"
|
||||
@@ -198,8 +196,7 @@
|
||||
android:importantForAutofill="no"
|
||||
android:hint="@string/otp_counter"
|
||||
tools:text="1"
|
||||
android:imeOptions="actionNext"
|
||||
tools:targetApi="jelly_bean" />
|
||||
android:imeOptions="actionNext" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
@@ -228,8 +225,7 @@
|
||||
tools:text="6"
|
||||
android:maxLength="2"
|
||||
android:digits="0123456789"
|
||||
android:imeOptions="actionNext"
|
||||
tools:targetApi="jelly_bean" />
|
||||
android:imeOptions="actionNext" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
||||
@@ -65,7 +65,6 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_arrow_right_white_24dp"
|
||||
app:tint="?attr/colorOnSurface"
|
||||
android:importantForAccessibility="no"
|
||||
tools:targetApi="jelly_bean" />
|
||||
android:importantForAccessibility="no" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
@@ -9,7 +9,7 @@ android {
|
||||
ndkVersion "21.4.7075529"
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 15
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 34
|
||||
multiDexEnabled true
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ android {
|
||||
compileSdkVersion 34
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 15
|
||||
minSdkVersion 19
|
||||
targetSdk 34
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -148,23 +148,6 @@ fun PackageManager.getPackageInfoCompat(packageName: String, flags: Int = 0): Pa
|
||||
@Suppress("DEPRECATION") getPackageInfo(packageName, flags)
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
fun PackageManager.allowCreateDocumentByStorageAccessFramework(): Boolean {
|
||||
return when {
|
||||
// To check if a custom file manager can manage the ACTION_CREATE_DOCUMENT
|
||||
// queries filter is in Manifest
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT -> {
|
||||
queryIntentActivitiesCompat(
|
||||
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "application/octet-stream"
|
||||
}, PackageManager.MATCH_DEFAULT_ONLY
|
||||
).isNotEmpty()
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("QueryPermissionsNeeded")
|
||||
private fun PackageManager.queryIntentActivitiesCompat(intent: Intent, flags: Int): List<ResolveInfo> {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
|
||||
Reference in New Issue
Block a user