,
val index: Int = 0
) {
fun nextInterceptor(): IDbSyncInterceptor? {
if (index == interceptors.size) {
return null
}
return interceptors[index + 1]
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/util/cloud/interceptor/DbSyncResponse.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.util.cloud.interceptor
/**
* @Author laoyuyu
* @Description
* @Date 4:38 下午 2021/12/24
**/
class DbSyncResponse constructor(var code: Int, var msg: String) {
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/util/cloud/interceptor/DbSyncUploadInterceptor.kt
================================================
package com.lyy.keepassa.util.cloud.interceptor
import com.lyy.keepassa.base.BaseApp
import com.lyy.keepassa.util.cloud.DbSynUtil
import timber.log.Timber
/**
* @Author laoyuyu
* @Description
* @Date 5:08 下午 2021/12/24
**/
class DbSyncUploadInterceptor : IDbSyncInterceptor {
override suspend fun intercept(request: DbSyncRequest): DbSyncResponse {
val util = request.syncUtil
val record = request.record
val b = util.uploadFile(BaseApp.APP, record)
val msg = "上传文件${if (b) "成功" else "失败"}, fileKey = ${record.cloudDiskPath}"
Timber.d(msg)
return DbSyncResponse(if (b) DbSynUtil.STATE_SUCCEED else DbSynUtil.STATE_FAIL, msg)
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/util/cloud/interceptor/IDbSyncInterceptor.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.util.cloud.interceptor
import timber.log.Timber
/**
* @Author laoyuyu
* @Description
* @Date 4:34 下午 2021/12/24
**/
interface IDbSyncInterceptor {
suspend fun intercept(request: DbSyncRequest): DbSyncResponse
fun error(code: Int, msg: String): DbSyncResponse {
Timber.e(msg)
return DbSyncResponse(code, msg)
}
fun normal(code: Int, msg: String): DbSyncResponse {
Timber.i(msg)
return DbSyncResponse(code, msg)
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/util/totp/Base32String.java
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.util.totp;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
/**
* Encodes arbitrary byte arrays as case-insensitive base-32 strings.
*
* The implementation is slightly different than in RFC 4648. During encoding, padding is not
* added, and during decoding the last incomplete chunk is not taken into account. The result is
* that multiple strings decode to the same byte array, for example, string of sixteen 7s ("7...7")
* and seventeen 7s both decode to the same byte array.
*
*
TODO: Revisit this encoding and whether this ambiguity needs fixing.
*/
public class Base32String {
private static final String SEPARATOR = "-";
private static final char[] DIGITS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".toCharArray();
private static final int MASK = DIGITS.length - 1;
private static final int SHIFT = Integer.numberOfTrailingZeros(DIGITS.length);
private static final Map CHAR_MAP = new HashMap<>();
static {
for (int i = 0; i < DIGITS.length; i++) {
CHAR_MAP.put(DIGITS[i], i);
}
}
public static byte[] decode(String encoded) throws DecodingException {
// Remove whitespace and separators
encoded = encoded.trim().replaceAll(SEPARATOR, "").replaceAll(" ", "");
// Remove padding. Note: the padding is used as hint to determine how many
// bits to decode from the last incomplete chunk (which is commented out
// below, so this may have been wrong to start with).
encoded = encoded.replaceFirst("[=]*$", "");
// Canonicalize to all upper case
encoded = encoded.toUpperCase(Locale.US);
if (encoded.length() == 0) {
return new byte[0];
}
int encodedLength = encoded.length();
int outLength = encodedLength * SHIFT / 8;
byte[] result = new byte[outLength];
int buffer = 0;
int next = 0;
int bitsLeft = 0;
for (char c : encoded.toCharArray()) {
if (!CHAR_MAP.containsKey(c)) {
throw new DecodingException("Illegal character: " + c);
}
buffer <<= SHIFT;
buffer |= CHAR_MAP.get(c) & MASK;
bitsLeft += SHIFT;
if (bitsLeft >= 8) {
result[next++] = (byte) (buffer >> (bitsLeft - 8));
bitsLeft -= 8;
}
}
// We'll ignore leftover bits for now.
//
// if (next != outLength || bitsLeft >= SHIFT) {
// throw new DecodingException("Bits left: " + bitsLeft);
// }
return result;
}
public static String encode(byte[] data) {
int dataLength = data.length;
if (dataLength == 0) {
return "";
}
// SHIFT is the number of bits per output character, so the length of the
// output is the length of the input multiplied by 8/SHIFT, rounded up.
if (dataLength >= (1 << 28)) {
// The computation below will fail, so don't do it.
throw new IllegalArgumentException();
}
int outputLength = (dataLength * 8 + SHIFT - 1) / SHIFT;
StringBuilder result = new StringBuilder(outputLength);
int buffer = data[0];
int next = 1;
int bitsLeft = 8;
while (bitsLeft > 0 || next < dataLength) {
if (bitsLeft < SHIFT) {
if (next < dataLength) {
buffer <<= 8;
buffer |= (data[next++] & 0xff);
bitsLeft += 8;
} else {
int pad = SHIFT - bitsLeft;
buffer <<= pad;
bitsLeft += pad;
}
}
int index = MASK & (buffer >> (bitsLeft - SHIFT));
bitsLeft -= SHIFT;
result.append(DIGITS[index]);
}
return result.toString();
}
/** Exception thrown when decoding fails */
public static class DecodingException extends Exception {
public DecodingException(String message) {
super(message);
}
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/util/totp/ComposeKeeOtp.kt
================================================
package com.lyy.keepassa.util.totp
import android.net.Uri
import com.blankj.utilcode.util.ConvertUtils
import com.blankj.utilcode.util.EncodeUtils
import com.keepassdroid.database.PwEntryV4
import com.lyy.keepassa.util.totp.TokenCalculator.HashAlgorithm
import com.lyy.keepassa.util.totp.TokenCalculator.HashAlgorithm.SHA256
import com.lyy.keepassa.util.totp.TokenCalculator.HashAlgorithm.SHA512
object ComposeKeeOtp : IOtpCompose {
override fun getOtpPass(entry: PwEntryV4): Pair {
// keeotp
val keepOtp = entry.strings["otp"]
if (keepOtp != null) {
val uri = Uri.parse("otp://laoyuyu.me/?$keepOtp")
val key = uri.getQueryParameter("key")
val type = uri.getQueryParameter("type")
val len = uri.getQueryParameter("size") ?: TokenCalculator.TOTP_DEFAULT_DIGITS.toString()
val hashMode = uri.getQueryParameter("otpHashMode")
val encoding = uri.getQueryParameter("encoding")
val counter = uri.getQueryParameter("counter") ?: "0"
val period = uri.getQueryParameter("step") ?: TokenCalculator.TOTP_DEFAULT_PERIOD.toString()
val algorithm = when (hashMode) {
"Sha256" -> SHA256
"Sha512" -> SHA512
else -> HashAlgorithm.SHA1
}
val seedByte = when (encoding) {
"Base64" -> {
EncodeUtils.base64Decode(key)
}
"UTF8" -> {
key!!.toByteArray(Charsets.UTF_8)
}
"Hex" -> {
// hex
ConvertUtils.hexString2Bytes(key)
}
else -> {
// base32
Base32String.decode(key)
}
}
val token = when (type) {
"Hotp" -> {
TokenCalculator.HOTP(seedByte, counter.toLong(), len.toInt(), algorithm)
}
"Steam" -> {
TokenCalculator.TOTP_Steam(seedByte, period.toInt(), len.toInt(), algorithm)
}
else -> {
// totp
TokenCalculator.TOTP_RFC6238(seedByte, period.toInt(), len.toInt(), algorithm)
}
}
return Pair(if (type == "Hotp") counter.toInt() else period.toInt(), token)
}
return Pair(TokenCalculator.TOTP_DEFAULT_PERIOD, null)
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/util/totp/ComposeKeeOtp2.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.util.totp
import com.blankj.utilcode.util.ConvertUtils
import com.blankj.utilcode.util.EncodeUtils
import com.keepassdroid.database.PwEntryV4
import com.lyy.keepassa.util.totp.ComposeKeepass.HMAC_SHA_256
import com.lyy.keepassa.util.totp.ComposeKeepass.HMAC_SHA_512
import com.lyy.keepassa.util.totp.ComposeKeepass.HmacOtp_Algorithm
import com.lyy.keepassa.util.totp.ComposeKeepass.HmacOtp_Counter
import com.lyy.keepassa.util.totp.ComposeKeepass.HmacOtp_Secret
import com.lyy.keepassa.util.totp.ComposeKeepass.HmacOtp_Secret_Base32
import com.lyy.keepassa.util.totp.ComposeKeepass.HmacOtp_Secret_Base64
import com.lyy.keepassa.util.totp.ComposeKeepass.HmacOtp_Secret_Hex
import com.lyy.keepassa.util.totp.ComposeKeepass.TimeOtp_Algorithm
import com.lyy.keepassa.util.totp.ComposeKeepass.TimeOtp_Length
import com.lyy.keepassa.util.totp.ComposeKeepass.TimeOtp_Period
import com.lyy.keepassa.util.totp.ComposeKeepass.TimeOtp_Secret
import com.lyy.keepassa.util.totp.ComposeKeepass.TimeOtp_Secret_Base32
import com.lyy.keepassa.util.totp.ComposeKeepass.TimeOtp_Secret_Base64
import com.lyy.keepassa.util.totp.ComposeKeepass.TimeOtp_Secret_Hex
import com.lyy.keepassa.util.totp.TokenCalculator.HashAlgorithm
import com.lyy.keepassa.util.totp.TokenCalculator.HashAlgorithm.SHA256
import com.lyy.keepassa.util.totp.TokenCalculator.HashAlgorithm.SHA512
/**
* 兼容KeeOtp2插件的totp获取
* @Author laoyuyu
* @Description
* @Date 4:12 PM 2023/9/20
**/
@Deprecated("有异常,未实现")
object ComposeKeeOtp2 : IOtpCompose {
override fun getOtpPass(entry: PwEntryV4): Pair {
return getKeeOtp2Totp(entry)
}
/**
* 兼容KeeOtp2插件的totp获取
*/
private fun getKeeOtp2Totp(entry: PwEntryV4): Pair {
// 默认的32位
val def32 = entry.strings[TimeOtp_Secret_Base32]
if (def32.toString().isNotEmpty()) {
return Pair(
TokenCalculator.TOTP_DEFAULT_PERIOD,
getTotpPass(
def32.toString(),
TokenCalculator.TOTP_DEFAULT_PERIOD,
TokenCalculator.TOTP_DEFAULT_DIGITS,
false
)
)
}
// 自定义的totp
val secretTotp = entry.strings.keys.find { it.startsWith(TimeOtp_Secret) }
if (secretTotp != null) {
val lenStr = entry.strings[TimeOtp_Length]
val algorithmStr = entry.strings[TimeOtp_Algorithm]
val periodStr = entry.strings[TimeOtp_Period]
val len = lenStr?.toString()?.toInt() ?: TokenCalculator.TOTP_DEFAULT_PERIOD
var algorithm = HashAlgorithm.SHA1
if (algorithmStr != null) {
algorithm = when (algorithmStr.toString()) {
HMAC_SHA_256 -> SHA256
HMAC_SHA_512 -> SHA512
else -> HashAlgorithm.SHA1
}
}
val period = periodStr?.toString()?.toInt() ?: TokenCalculator.TOTP_DEFAULT_PERIOD
val seedByte = when {
entry.strings[TimeOtp_Secret_Base64] != null -> {
EncodeUtils.base64Decode(secretTotp)
}
entry.strings[TimeOtp_Secret_Base32] != null -> {
Base32String.decode(secretTotp)
}
entry.strings[TimeOtp_Secret_Hex] != null -> {
// hex
ConvertUtils.hexString2Bytes(secretTotp)
}
else -> {
// utf-8
secretTotp.toByteArray(Charsets.UTF_8)
}
}
return Pair(
period,
TokenCalculator.TOTP_RFC6238(seedByte, period, len, algorithm)
)
}
return Pair(-1, null)
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/util/totp/ComposeKeeTrayTotp.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.util.totp
import com.arialyy.frame.router.Routerfit
import com.keepassdroid.database.PwEntryV4
import com.keepassdroid.database.security.ProtectedString
import com.lyy.keepassa.entity.TrayTotpBean
import com.lyy.keepassa.router.ServiceRouter
import com.lyy.keepassa.util.otpIsKeeTraySteam
/**
* 兼容KeeTrayTOTP插件的totp获取
*/
object ComposeKeeTrayTotp : IOtpCompose {
const val KEY_SETTING = "TOTP Settings"
const val KEY_SEED = "TOTP Seed"
private val kdbService by lazy {
Routerfit.create(ServiceRouter::class.java).getDbSaveService()
}
override fun getOtpPass(entry: PwEntryV4): Pair {
// 修复1.7之前的bug
if (isSteamEntry(entry)) {
fix1_7bug(entry)
}
return getKeeTrayTotp(entry)
}
/**
* 判断是否是steam的条目,1.7之前的版本创建totp时,会将 TOTP Settings 字段设置为 30;S,而S表示的是Steam
*/
private fun isSteamEntry(entry: PwEntryV4): Boolean {
return entry.url
.contains("steampowered", ignoreCase = true) ||
entry.customData.any {
it.value.equals(
"androidapp://com.valvesoftware.android.steam.community",
true
)
}
}
/**
* 1.7之前的版本创建totp时,会将 TOTP Settings 字段设置为 30;S,而S表示的是Steam
*/
private fun fix1_7bug(entry: PwEntryV4) {
val totpSetting = entry.strings["TOTP Settings"]
if (totpSetting != null) {
val tempArray = totpSetting.toString()
.split(";")
if (tempArray.isNotEmpty() && tempArray.size == 2 && !tempArray[1].equals("S", true)) {
entry.strings["TOTP Settings"] = ProtectedString(false, "${tempArray[0]};S")
kdbService.saveDbByBackground()
}
}
}
/**
* 兼容KeeTrayTOTP插件的totp获取
*/
private fun getKeeTrayTotp(entry: PwEntryV4): Pair {
val totpSetting = entry.strings[KEY_SETTING]
val isSteam = entry.otpIsKeeTraySteam()
var period = TokenCalculator.TOTP_DEFAULT_PERIOD
var digits = TokenCalculator.TOTP_DEFAULT_DIGITS
val s = totpSetting.toString()
.split(";")
if (s.isNotEmpty() && s.size == 2) {
period = s[0].toInt()
digits = if (isSteam) TokenCalculator.STEAM_DEFAULT_DIGITS else s[1].toInt()
}
return Pair(
period,
getTotpPass(entry.strings[KEY_SEED].toString(), period, digits, isSteam)
)
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/util/totp/ComposeKeepass.kt
================================================
package com.lyy.keepassa.util.totp
import com.blankj.utilcode.util.ConvertUtils
import com.keepassdroid.database.PwEntryV4
import com.lyy.keepassa.entity.HmacOtpBean
import com.lyy.keepassa.entity.TimeOtp2Bean
import com.lyy.keepassa.util.getKeepassBean
object ComposeKeepass : IOtpCompose {
const val HmacOtp = "HmacOtp"
const val TimeOtp = "TimeOtp"
const val TimeOtp_Secret = "TimeOtp-Secret"
const val TimeOtp_Length = "TimeOtp-Length"
const val TimeOtp_Algorithm = "TimeOtp-Algorithm"
const val TimeOtp_Period = "TimeOtp-Period"
const val TimeOtp_Secret_Hex = "TimeOtp-Secret-Hex"
const val TimeOtp_Secret_Base32 = "TimeOtp-Secret-Base32"
const val TimeOtp_Secret_Base64 = "TimeOtp-Secret-Base64"
const val HMAC_SHA_1 = "HMAC-SHA-1"
const val HMAC_SHA_256 = "HMAC-SHA-256"
const val HMAC_SHA_512 = "HMAC-SHA-512"
//hOtp
const val HmacOtp_Counter = "HmacOtp-Counter"
const val HmacOtp_Secret = "HmacOtp-Secret"
const val HmacOtp_Algorithm = "HmacOtp-Algorithm"
const val HmacOtp_Secret_Hex = "HmacOtp-Secret-Hex"
const val HmacOtp_Secret_Base32 = "HmacOtp-Secret-Base32"
const val HmacOtp_Secret_Base64 = "HmacOtp-Secret-Base64"
const val HmacOtp_Length = "HmacOtp-Length"
override fun getOtpPass(entry: PwEntryV4): Pair {
val bean = entry.getKeepassBean()
if (bean.hmac != null) return handleHotp(bean.hmac)
if (bean.otpBean != null) return handleTotp(bean.otpBean)
return Pair(-1, null)
}
private fun handleHotp(hmacBean: HmacOtpBean): Pair {
val secret = when (hmacBean.secretType) {
SecretHexType.BASE_32 -> {
Base32String.decode(hmacBean.secret)
}
SecretHexType.BASE_64 -> {
Base32String.decode(hmacBean.secret)
}
SecretHexType.HEX -> {
ConvertUtils.hexString2Bytes(hmacBean.secret)
}
else -> {
hmacBean.secret.toByteArray(Charsets.UTF_8)
}
}
return Pair(
TokenCalculator.TOTP_DEFAULT_PERIOD,
TokenCalculator.HOTP(secret, hmacBean.counter.toLong(), hmacBean.len, hmacBean.algorithm)
)
}
private fun handleTotp(otpBean: TimeOtp2Bean): Pair {
val secret = when (otpBean.secretType) {
SecretHexType.BASE_32 -> {
Base32String.decode(otpBean.secret)
}
SecretHexType.BASE_64 -> {
Base32String.decode(otpBean.secret)
}
SecretHexType.HEX -> {
ConvertUtils.hexString2Bytes(otpBean.secret)
}
else -> {
otpBean.secret.toByteArray(Charsets.UTF_8)
}
}
val token = TokenCalculator.TOTP_RFC6238(
secret,
otpBean.period,
otpBean.digits,
otpBean.algorithm
)
return Pair(otpBean.period, token)
}
fun getSecretType(secretType: SecretHexType): String {
return when (secretType) {
SecretHexType.UTF_8 -> TimeOtp_Secret
SecretHexType.HEX -> TimeOtp_Secret_Hex
SecretHexType.BASE_32 -> TimeOtp_Secret_Base32
SecretHexType.BASE_64 -> TimeOtp_Secret_Base64
}
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/util/totp/ComposeKeepassxc.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.util.totp
import android.text.TextUtils
import com.keepassdroid.database.PwEntryV4
import com.keepassdroid.database.security.ProtectedString
import com.lyy.keepassa.R.string
import com.lyy.keepassa.base.BaseApp
import com.lyy.keepassa.entity.GoogleOtpBean
import com.lyy.keepassa.entity.KeepassXcBean
import com.lyy.keepassa.util.HitUtil
import com.lyy.keepassa.util.getKeepassXcBean
import com.lyy.keepassa.util.otpIsKeepassXcSteam
import com.lyy.keepassa.util.totp.TokenCalculator.HashAlgorithm
import timber.log.Timber
import java.util.Locale
object ComposeKeepassxc : IOtpCompose {
const val KEY_SEED = "otp"
const val KEY_ENCODER = "encoder"
const val KEY_STEAM = "steam"
const val KEY_SECRET = "secret"
const val KEY_COUNTER = "counter"
const val KEY_ISSUER = "issuer"
const val KEY_PERIOD = "period"
const val KEY_DIGITS = "digits"
const val KEY_ALGORITHM = "algorithm"
override fun getOtpPass(entry: PwEntryV4): Pair {
return getPass(entry)
}
private fun getPass(entry: PwEntryV4): Pair {
val bean = entry.getKeepassXcBean()
if (TextUtils.isEmpty(bean.secret)) {
return Pair(0, null)
}
try {
val b = Base32String.decode(bean.secret)
if (entry.otpIsKeepassXcSteam()) {
val pass = TokenCalculator.TOTP_Steam(
b, TokenCalculator.TOTP_DEFAULT_PERIOD, TokenCalculator.STEAM_DEFAULT_DIGITS,
TokenCalculator.DEFAULT_ALGORITHM
)
return Pair(bean.period, pass)
}
val arithmetic = when (bean.algorithm) {
HashAlgorithm.SHA256 -> "SHA256"
HashAlgorithm.SHA512 -> "SHA512"
else -> "SHA1"
}
val pass = when (bean.host) {
"totp", "TOTP" -> {
TokenCalculator.TOTP_RFC6238(
b,
bean.period,
bean.digits,
HashAlgorithm.valueOf(arithmetic.toUpperCase(Locale.ROOT))
)
}
"hotp", "HOTP" -> {
TokenCalculator.HOTP(
b,
bean.counter?.toLong() ?: TokenCalculator.HOTP_INITIAL_COUNTER.toLong(),
bean.digits,
HashAlgorithm.valueOf(arithmetic.toUpperCase(Locale.ROOT))
)
}
else -> {
Timber.e("不识别的类型:${bean.host}")
null
}
}
return Pair(bean.period, pass)
} catch (e: Exception) {
HitUtil.toaskShort(BaseApp.APP.getString(string.totp_key_error))
Timber.e(e)
}
return Pair(bean.period, null)
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/util/totp/IOtpCompose.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.util.totp
import com.keepassdroid.database.PwEntryV4
import com.lyy.keepassa.R.string
import com.lyy.keepassa.base.BaseApp
import com.lyy.keepassa.util.HitUtil
import timber.log.Timber
/**
* otp
* @Author laoyuyu
* @Description
* @Date 4:02 PM 2023/9/20
**/
interface IOtpCompose {
fun getTotpPass(
seed: String?,
period: Int,
digits: Int = TokenCalculator.TOTP_DEFAULT_DIGITS,
isSteam: Boolean
): String? {
// 适配keepass totp插件的密码
try {
val b = Base32String.decode(seed)
return if (isSteam) {
TokenCalculator.TOTP_Steam(
b, TokenCalculator.TOTP_DEFAULT_PERIOD, TokenCalculator.STEAM_DEFAULT_DIGITS,
TokenCalculator.DEFAULT_ALGORITHM
)
} else {
TokenCalculator.TOTP_RFC6238(b, period, digits, TokenCalculator.DEFAULT_ALGORITHM)
}
} catch (e: Exception) {
HitUtil.toaskShort(BaseApp.APP.getString(string.totp_key_error))
Timber.e(e)
}
return null
}
/**
* 获取totp密码
* @return first period, second 密码
*/
fun getOtpPass(entry: PwEntryV4): Pair
// fun toOtpStringMap(bean: OtpBean): Map {
// return hashMapOf()
// }
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/util/totp/OtpEnum.kt
================================================
package com.lyy.keepassa.util.totp
/**
* @Author laoyuyu
* @Description
* @Date 5:24 PM 2024/1/11
**/
enum class OtpEnum {
GOOGLE_OTP,
KEEPASSXC,
TRAY_TOTP,
KEEPASS
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/util/totp/OtpUtil.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.util.totp
import android.annotation.SuppressLint
import com.keepassdroid.database.PwEntryV4
import com.lyy.keepassa.util.otpIsKeeOtp2
import com.lyy.keepassa.util.otpIsKeeTrayTotp
import com.lyy.keepassa.util.otpIsKeepOtp
import com.lyy.keepassa.util.otpKeepass
import com.lyy.keepassa.util.otpKeepassXC
object OtpUtil {
/**
* 获取totp密码
* @return first period, second 密码
*/
@SuppressLint("DefaultLocale") fun getOtpPass(entry: PwEntryV4): Pair {
val otpCompose = when {
entry.otpIsKeeTrayTotp() -> {
ComposeKeeTrayTotp
}
entry.otpIsKeepOtp() -> {
ComposeKeeOtp
}
entry.otpKeepassXC() -> {
ComposeKeepassxc
}
entry.otpKeepass() -> {
ComposeKeepass
}
entry.otpIsKeeOtp2() -> {
ComposeKeeOtp2
}
else -> null
}
return otpCompose?.getOtpPass(entry) ?: Pair(-1, null)
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/util/totp/SecretHexType.kt
================================================
package com.lyy.keepassa.util.totp
enum class SecretHexType {
UTF_8, HEX, BASE_32, BASE_64
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/util/totp/TokenCalculator.java
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.util.totp;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.NumberFormat;
import java.util.Locale;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import timber.log.Timber;
/**
* 地址:https://github.com/andOTP/andOTP.git
*/
public class TokenCalculator {
public static final int TOTP_DEFAULT_PERIOD = 30;
public static final int TOTP_DEFAULT_DIGITS = 6;
public static final int HOTP_INITIAL_COUNTER = 1;
public static final int STEAM_DEFAULT_DIGITS = 5;
private static final char[] STEAMCHARS = new char[] {
'2', '3', '4', '5', '6', '7', '8', '9', 'B', 'C',
'D', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q',
'R', 'T', 'V', 'W', 'X', 'Y'
};
public enum HashAlgorithm {
SHA1, SHA256, SHA512
}
public static final HashAlgorithm DEFAULT_ALGORITHM = HashAlgorithm.SHA1;
private static byte[] generateHash(HashAlgorithm algorithm, byte[] key, byte[] data)
throws NoSuchAlgorithmException, InvalidKeyException {
String algo = "Hmac" + algorithm.toString();
Mac mac = Mac.getInstance(algo);
mac.init(new SecretKeySpec(key, algo));
return mac.doFinal(data);
}
public static int TOTP_RFC6238(byte[] secret, int period, long time, int digits,
HashAlgorithm algorithm) {
int fullToken = TOTP(secret, period, time, algorithm);
int div = (int) Math.pow(10, digits);
return fullToken % div;
}
/**
* TOTP_RFC6238 协议的TOTP
*
* @param secret seed
* @param period {@link #TOTP_DEFAULT_PERIOD}、
* @param digits {@link #TOTP_DEFAULT_DIGITS}、{@link #STEAM_DEFAULT_DIGITS}
* @param algorithm {@link HashAlgorithm}
*/
public static String TOTP_RFC6238(byte[] secret, int period, int digits,
HashAlgorithm algorithm) {
return formatTokenString(
TOTP_RFC6238(secret, period, System.currentTimeMillis() / 1000, digits, algorithm), digits);
}
/**
* TOTP_RFC6238 协议的TOTP
*
* @param secret seed
* @param period {@link #TOTP_DEFAULT_PERIOD}、
* @param digits {@link #TOTP_DEFAULT_DIGITS}、{@link #STEAM_DEFAULT_DIGITS}
* @param algorithm {@link HashAlgorithm}
*/
public static String TOTP_Steam(byte[] secret, int period, int digits, HashAlgorithm algorithm) {
int fullToken = TOTP(secret, period, System.currentTimeMillis() / 1000, algorithm);
StringBuilder tokenBuilder = new StringBuilder();
for (int i = 0; i < digits; i++) {
tokenBuilder.append(STEAMCHARS[fullToken % STEAMCHARS.length]);
fullToken /= STEAMCHARS.length;
}
return tokenBuilder.toString();
}
/**
* TOTP_RFC6238 协议的HOTP
*
* @param secret seed
* @param counter {@link #HOTP_INITIAL_COUNTER}
* @param digits {@link #TOTP_DEFAULT_DIGITS}、{@link #STEAM_DEFAULT_DIGITS}
* @param algorithm {@link HashAlgorithm}
*/
public static String HOTP(byte[] secret, long counter, int digits, HashAlgorithm algorithm) {
int fullToken = HOTP(secret, counter, algorithm);
int div = (int) Math.pow(10, digits);
return formatTokenString(fullToken % div, digits);
}
private static int TOTP(byte[] key, int period, long time, HashAlgorithm algorithm) {
return HOTP(key, time / period, algorithm);
}
private static int HOTP(byte[] key, long counter, HashAlgorithm algorithm) {
int r = 0;
try {
byte[] data = ByteBuffer.allocate(8).putLong(counter).array();
byte[] hash = generateHash(algorithm, key, data);
int offset = hash[hash.length - 1] & 0xF;
int binary = (hash[offset] & 0x7F) << 0x18;
binary |= (hash[offset + 1] & 0xFF) << 0x10;
binary |= (hash[offset + 2] & 0xFF) << 0x08;
binary |= (hash[offset + 3] & 0xFF);
r = binary;
} catch (Exception e) {
Timber.e(e);
}
return r;
}
private static String formatTokenString(int token, int digits) {
NumberFormat numberFormat = NumberFormat.getInstance(Locale.ENGLISH);
numberFormat.setMinimumIntegerDigits(digits);
numberFormat.setGroupingUsed(false);
return numberFormat.format(token);
}
public static String formatToken(String s, int chunkSize) {
if (chunkSize == 0 || s == null) {
return s;
}
StringBuilder ret = new StringBuilder("");
int index = s.length();
while (index > 0) {
ret.insert(0, s.substring(Math.max(index - chunkSize, 0), index));
ret.insert(0, " ");
index = index - chunkSize;
}
return ret.toString().trim();
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/view/ChoseDirModule.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.view
import androidx.fragment.app.FragmentActivity
import com.keepassdroid.database.PwDatabaseV4
import com.keepassdroid.database.PwEntryV4
import com.keepassdroid.database.PwGroup
import com.keepassdroid.database.PwGroupId
import com.keepassdroid.database.PwGroupV4
import com.lyy.keepassa.R
import com.lyy.keepassa.base.BaseApp
import com.lyy.keepassa.base.BaseModule
import com.lyy.keepassa.event.MoveEvent
import com.lyy.keepassa.util.HitUtil
import com.lyy.keepassa.util.KpaUtil
import org.greenrobot.eventbus.EventBus
import java.util.UUID
class ChoseDirModule : BaseModule() {
/**
* 恢复群组
* @param groupId 需要恢复的群组
* @param curGroup 当前群组
*/
fun moveGroup(
ac: FragmentActivity,
groupId: PwGroupId,
curGroup: PwGroup
) {
val group = BaseApp.KDB.pm.groups[groupId] as PwGroupV4
if (group.parent == BaseApp.KDB.pm.recycleBin) {
(BaseApp.KDB.pm as PwDatabaseV4).undoRecycle(group, curGroup)
} else {
(BaseApp.KDB.pm as PwDatabaseV4).moveGroup(group, curGroup)
}
KpaUtil.kdbHandlerService.saveDbByBackground()
EventBus.getDefault().post(MoveEvent(MoveEvent.MOVE_TYPE_GROUP, null, group))
HitUtil.toaskShort(ac.getString(R.string.undo_grouped))
ac.finishAfterTransition()
}
/**
* 恢复条目
*/
fun moveEntry(
ac: FragmentActivity,
entryId: UUID,
curGroup: PwGroupV4
) {
val entry = BaseApp.KDB.pm.entries[entryId] ?: return
val entryV4 = entry as PwEntryV4
KpaUtil.kdbHandlerService.moveEntry(entryV4, curGroup)
HitUtil.toaskShort(ac.getString(R.string.undo_entryed))
ac.finishAfterTransition()
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/view/KpaCaptureManager.java
================================================
package com.lyy.keepassa.view;
import android.Manifest;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.view.Display;
import android.view.KeyEvent;
import android.view.Surface;
import android.view.Window;
import android.view.WindowManager;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.DecodeHintType;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.ResultMetadataType;
import com.google.zxing.ResultPoint;
import com.google.zxing.client.android.BeepManager;
import com.google.zxing.client.android.DecodeFormatManager;
import com.google.zxing.client.android.DecodeHintManager;
import com.google.zxing.client.android.InactivityTimer;
import com.google.zxing.client.android.Intents;
import com.google.zxing.client.android.R;
import com.journeyapps.barcodescanner.BarcodeCallback;
import com.journeyapps.barcodescanner.BarcodeResult;
import com.journeyapps.barcodescanner.BarcodeView;
import com.journeyapps.barcodescanner.CameraPreview;
import com.journeyapps.barcodescanner.DecoratedBarcodeView;
import com.journeyapps.barcodescanner.DefaultDecoderFactory;
import com.journeyapps.barcodescanner.camera.CameraSettings;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import timber.log.Timber;
/**
* Manages barcode scanning for a CaptureActivity. This class may be used to have a custom Activity
* (e.g. with a customized look and feel, or a different superclass), but not the barcode scanning
* process itself.
*
* This is intended for an Activity that is dedicated to capturing a single barcode and returning
* it via setResult(). For other use cases, use DefaultBarcodeScannerView or BarcodeView directly.
*
* The following is managed by this class:
* - Orientation lock
* - InactivityTimer
* - BeepManager
* - Initializing from an Intent (via IntentIntegrator)
* - Setting the result and finishing the Activity when a barcode is scanned
* - Displaying camera errors
*/
public class KpaCaptureManager {
private static int cameraPermissionReqCode = 250;
private final Activity activity;
private int orientationLock = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
private static final String SAVED_ORIENTATION_LOCK = "SAVED_ORIENTATION_LOCK";
private boolean returnBarcodeImagePath = false;
private boolean showDialogIfMissingCameraPermission = true;
private String missingCameraPermissionDialogMessage = "";
private boolean destroyed = false;
private final InactivityTimer inactivityTimer;
private final BeepManager beepManager;
private final Handler handler;
private final BarcodeView barcodeView;
/**
* The instance of @link TorchListener to send events callback.
*/
private DecoratedBarcodeView.TorchListener torchListener;
private boolean finishWhenClosed = false;
private final BarcodeCallback callback = new BarcodeCallback() {
@Override
public void barcodeResult(final BarcodeResult result) {
barcodeView.pause();
beepManager.playBeepSoundAndVibrate();
handler.post(() -> returnResult(result));
}
@Override
public void possibleResultPoints(List resultPoints) {
}
};
public KpaCaptureManager(Activity activity, BarcodeView barcodeView) {
this.activity = activity;
this.barcodeView = barcodeView;
CameraPreview.StateListener stateListener = new CameraPreview.StateListener() {
@Override
public void previewSized() {
}
@Override
public void previewStarted() {
}
@Override
public void previewStopped() {
}
@Override
public void cameraError(Exception error) {
displayFrameworkBugMessageAndExit(
activity.getString(R.string.zxing_msg_camera_framework_bug)
);
}
@Override
public void cameraClosed() {
if (finishWhenClosed) {
Timber.d("Camera closed; finishing activity");
finish();
}
}
};
barcodeView.addStateListener(stateListener);
handler = new Handler();
inactivityTimer = new InactivityTimer(activity, () -> {
Timber.d("Finishing due to inactivity");
finish();
});
beepManager = new BeepManager(activity);
}
/**
* Perform initialization, according to preferences set in the intent.
*
* @param intent the intent containing the scanning preferences
* @param savedInstanceState saved state, containing orientation lock
*/
public void initializeFromIntent(Intent intent, Bundle savedInstanceState) {
Window window = activity.getWindow();
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
if (savedInstanceState != null) {
// If the screen was locked and unlocked again, we may start in a different orientation
// (even one not allowed by the manifest). In this case we restore the orientation we were
// previously locked to.
this.orientationLock = savedInstanceState.getInt(SAVED_ORIENTATION_LOCK,
ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
}
if (intent != null) {
// Only lock the orientation if it's not locked to something else yet
boolean orientationLocked = intent.getBooleanExtra(Intents.Scan.ORIENTATION_LOCKED, true);
if (orientationLocked) {
lockOrientation();
}
if (Intents.Scan.ACTION.equals(intent.getAction())) {
initializeFromIntent(intent);
}
if (!intent.getBooleanExtra(Intents.Scan.BEEP_ENABLED, true)) {
beepManager.setBeepEnabled(false);
}
if (intent.hasExtra(Intents.Scan.SHOW_MISSING_CAMERA_PERMISSION_DIALOG)) {
setShowMissingCameraPermissionDialog(
intent.getBooleanExtra(Intents.Scan.SHOW_MISSING_CAMERA_PERMISSION_DIALOG, true),
intent.getStringExtra(Intents.Scan.MISSING_CAMERA_PERMISSION_DIALOG_MESSAGE)
);
}
if (intent.hasExtra(Intents.Scan.TIMEOUT)) {
handler.postDelayed(this::returnResultTimeout,
intent.getLongExtra(Intents.Scan.TIMEOUT, 0L));
}
if (intent.getBooleanExtra(Intents.Scan.BARCODE_IMAGE_ENABLED, false)) {
returnBarcodeImagePath = true;
}
}
}
public void initializeFromIntent(Intent intent) {
// Scan the formats the intent requested, and return the result to the calling activity.
Set decodeFormats = DecodeFormatManager.parseDecodeFormats(intent);
Map decodeHints = DecodeHintManager.parseDecodeHints(intent);
CameraSettings settings = new CameraSettings();
if (intent.hasExtra(Intents.Scan.CAMERA_ID)) {
int cameraId = intent.getIntExtra(Intents.Scan.CAMERA_ID, -1);
if (cameraId >= 0) {
settings.setRequestedCameraId(cameraId);
}
}
if (intent.hasExtra(Intents.Scan.TORCH_ENABLED)) {
if (intent.getBooleanExtra(Intents.Scan.TORCH_ENABLED, false)) {
this.setTorchOn();
}
}
// Check what type of scan. Default: normal scan
int scanType = intent.getIntExtra(Intents.Scan.SCAN_TYPE, 0);
String characterSet = intent.getStringExtra(Intents.Scan.CHARACTER_SET);
MultiFormatReader reader = new MultiFormatReader();
reader.setHints(decodeHints);
barcodeView.setCameraSettings(settings);
barcodeView.setDecoderFactory(
new DefaultDecoderFactory(decodeFormats, decodeHints, characterSet, scanType));
}
public void setTorchOn() {
barcodeView.setTorch(true);
if (torchListener != null) {
torchListener.onTorchOn();
}
}
/**
* Lock display to current orientation.
*/
protected void lockOrientation() {
// Only get the orientation if it's not locked to one yet.
if (this.orientationLock == ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
// Adapted from http://stackoverflow.com/a/14565436
Display display = activity.getWindowManager().getDefaultDisplay();
int rotation = display.getRotation();
int baseOrientation = activity.getResources().getConfiguration().orientation;
int orientation = 0;
if (baseOrientation == Configuration.ORIENTATION_LANDSCAPE) {
if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) {
orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
} else {
orientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
}
} else if (baseOrientation == Configuration.ORIENTATION_PORTRAIT) {
if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_270) {
orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
} else {
orientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
}
}
this.orientationLock = orientation;
}
//noinspection ResourceType
activity.setRequestedOrientation(this.orientationLock);
}
/**
* Start decoding.
*/
public void decode() {
barcodeView.decodeSingle(callback);
}
/**
* Call from Activity#onResume().
*/
public void onResume() {
if (Build.VERSION.SDK_INT >= 23) {
openCameraWithPermission();
} else {
barcodeView.resume();
}
inactivityTimer.start();
}
private boolean askedPermission = false;
@TargetApi(23)
private void openCameraWithPermission() {
if (ContextCompat.checkSelfPermission(this.activity, Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED) {
barcodeView.resume();
} else if (!askedPermission) {
ActivityCompat.requestPermissions(this.activity,
new String[] { Manifest.permission.CAMERA },
cameraPermissionReqCode);
askedPermission = true;
} // else wait for permission result
}
/**
* Call from Activity#onRequestPermissionsResult
*
* @param requestCode The request code passed in {@link androidx.core.app.ActivityCompat#requestPermissions(Activity,
* String[], int)}.
* @param permissions The requested permissions.
* @param grantResults The grant results for the corresponding permissions
* which is either {@link android.content.pm.PackageManager#PERMISSION_GRANTED}
* or {@link android.content.pm.PackageManager#PERMISSION_DENIED}. Never null.
*/
public void onRequestPermissionsResult(int requestCode, String permissions[],
int[] grantResults) {
if (requestCode == cameraPermissionReqCode) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// permission was granted
barcodeView.resume();
} else {
setMissingCameraPermissionResult();
if (showDialogIfMissingCameraPermission) {
displayFrameworkBugMessageAndExit(missingCameraPermissionDialogMessage);
} else {
closeAndFinish();
}
}
}
}
/**
* Call from Activity#onPause().
*/
public void onPause() {
inactivityTimer.cancel();
barcodeView.pauseAndWait();
}
/**
* Call from Activity#onDestroy().
*/
public void onDestroy() {
destroyed = true;
inactivityTimer.cancel();
handler.removeCallbacksAndMessages(null);
}
/**
* Call from Activity#onSaveInstanceState().
*/
public void onSaveInstanceState(Bundle outState) {
outState.putInt(SAVED_ORIENTATION_LOCK, this.orientationLock);
}
/**
* Create a intent to return as the Activity result.
*
* @param rawResult the BarcodeResult, must not be null.
* @param barcodeImagePath a path to an exported file of the Barcode Image, can be null.
* @return the Intent
*/
public static Intent resultIntent(BarcodeResult rawResult, String barcodeImagePath) {
Intent intent = new Intent(Intents.Scan.ACTION);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
intent.putExtra(Intents.Scan.RESULT, rawResult.toString());
intent.putExtra(Intents.Scan.RESULT_FORMAT, rawResult.getBarcodeFormat().toString());
byte[] rawBytes = rawResult.getRawBytes();
if (rawBytes != null && rawBytes.length > 0) {
intent.putExtra(Intents.Scan.RESULT_BYTES, rawBytes);
}
Map metadata = rawResult.getResultMetadata();
if (metadata != null) {
if (metadata.containsKey(ResultMetadataType.UPC_EAN_EXTENSION)) {
intent.putExtra(Intents.Scan.RESULT_UPC_EAN_EXTENSION,
metadata.get(ResultMetadataType.UPC_EAN_EXTENSION).toString());
}
Number orientation = (Number) metadata.get(ResultMetadataType.ORIENTATION);
if (orientation != null) {
intent.putExtra(Intents.Scan.RESULT_ORIENTATION, orientation.intValue());
}
String ecLevel = (String) metadata.get(ResultMetadataType.ERROR_CORRECTION_LEVEL);
if (ecLevel != null) {
intent.putExtra(Intents.Scan.RESULT_ERROR_CORRECTION_LEVEL, ecLevel);
}
@SuppressWarnings("unchecked")
Iterable byteSegments =
(Iterable) metadata.get(ResultMetadataType.BYTE_SEGMENTS);
if (byteSegments != null) {
int i = 0;
for (byte[] byteSegment : byteSegments) {
intent.putExtra(Intents.Scan.RESULT_BYTE_SEGMENTS_PREFIX + i, byteSegment);
i++;
}
}
}
if (barcodeImagePath != null) {
intent.putExtra(Intents.Scan.RESULT_BARCODE_IMAGE_PATH, barcodeImagePath);
}
return intent;
}
/**
* Save the barcode image to a temporary file stored in the application's cache, and return its
* path.
* Only does so if returnBarcodeImagePath is enabled.
*
* @param rawResult the BarcodeResult, must not be null
* @return the path or null
*/
private String getBarcodeImagePath(BarcodeResult rawResult) {
String barcodeImagePath = null;
if (returnBarcodeImagePath) {
Bitmap bmp = rawResult.getBitmap();
try {
File bitmapFile = File.createTempFile("barcodeimage", ".jpg", activity.getCacheDir());
FileOutputStream outputStream = new FileOutputStream(bitmapFile);
bmp.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
outputStream.close();
barcodeImagePath = bitmapFile.getAbsolutePath();
} catch (IOException e) {
Timber.w("Unable to create temporary file and store bitmap! %s", e);
}
}
return barcodeImagePath;
}
private void finish() {
activity.finish();
}
protected void closeAndFinish() {
if (barcodeView.isCameraClosed()) {
finish();
} else {
finishWhenClosed = true;
}
barcodeView.pause();
inactivityTimer.cancel();
}
private void setMissingCameraPermissionResult() {
Intent intent = new Intent(Intents.Scan.ACTION);
intent.putExtra(Intents.Scan.MISSING_CAMERA_PERMISSION, true);
activity.setResult(Activity.RESULT_CANCELED, intent);
}
protected void returnResultTimeout() {
Intent intent = new Intent(Intents.Scan.ACTION);
intent.putExtra(Intents.Scan.TIMEOUT, true);
activity.setResult(Activity.RESULT_CANCELED, intent);
closeAndFinish();
}
protected void returnResult(BarcodeResult rawResult) {
Intent intent = resultIntent(rawResult, getBarcodeImagePath(rawResult));
activity.setResult(Activity.RESULT_OK, intent);
closeAndFinish();
}
protected void displayFrameworkBugMessageAndExit(String message) {
if (activity.isFinishing() || this.destroyed || finishWhenClosed) {
return;
}
if (message.isEmpty()) {
message = activity.getString(R.string.zxing_msg_camera_framework_bug);
}
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(activity.getString(R.string.zxing_app_name));
builder.setMessage(message);
builder.setPositiveButton(R.string.zxing_button_ok, (dialog, which) -> finish());
builder.setOnCancelListener(dialog -> finish());
builder.show();
}
public static int getCameraPermissionReqCode() {
return cameraPermissionReqCode;
}
public static void setCameraPermissionReqCode(int cameraPermissionReqCode) {
KpaCaptureManager.cameraPermissionReqCode = cameraPermissionReqCode;
}
/**
* If set to true, shows the default error dialog if camera permission is missing.
*
* If set to false, instead the capture manager just finishes.
*
* In both cases, the activity result is set to {@link Intents.Scan#MISSING_CAMERA_PERMISSION}
* and cancelled
*/
public void setShowMissingCameraPermissionDialog(boolean visible) {
setShowMissingCameraPermissionDialog(visible, "");
}
/**
* If set to true, shows the specified error dialog message if camera permission is missing.
*
* If set to false, instead the capture manager just finishes.
*
* In both cases, the activity result is set to {@link Intents.Scan#MISSING_CAMERA_PERMISSION}
* and cancelled
*/
public void setShowMissingCameraPermissionDialog(boolean visible, String message) {
showDialogIfMissingCameraPermission = visible;
missingCameraPermissionDialogMessage = message != null ? message : "";
}
public void setTorchListener(DecoratedBarcodeView.TorchListener listener) {
this.torchListener = listener;
}
/**
* Turn off the device's flashlight.
*/
public void setTorchOff() {
barcodeView.setTorch(false);
if (torchListener != null) {
torchListener.onTorchOff();
}
}
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_FOCUS:
case KeyEvent.KEYCODE_CAMERA:
// Handle these events so they don't launch the Camera app
return true;
// Use volume up/down to turn on light
case KeyEvent.KEYCODE_VOLUME_DOWN:
setTorchOff();
return true;
case KeyEvent.KEYCODE_VOLUME_UP:
setTorchOn();
return true;
}
return false;
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/view/MarkDownEditorActivity.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.view
import android.app.Activity
import android.app.ActivityOptions
import android.content.Context
import android.content.Intent
import android.os.Bundle
import com.lyy.keepassa.R
import com.lyy.keepassa.base.BaseActivity
import com.lyy.keepassa.databinding.ActivityMarkdownEditorBinding
import com.lyy.keepassa.event.EditorEvent
import com.lyy.keepassa.widget.editor.MarkDownEditor
import org.greenrobot.eventbus.EventBus
import timber.log.Timber
/**
* @Author laoyuyu
* @Description
* @Date 2020/12/2
**/
class MarkDownEditorActivity : BaseActivity() {
private var reqCode: Int = 0
private var content: CharSequence? = null
companion object {
private val KEY_REQUESTOIN_CODE = "KEY_REQUESTOIN_CODE"
private var KEY_CONTENT = "KEY_CONTENT"
fun turnMarkDownEditor(
context: Context,
requestCode: Int,
content: CharSequence?
) {
val intent = Intent(context, MarkDownEditorActivity::class.java)
intent.putExtra(KEY_REQUESTOIN_CODE, requestCode)
intent.putExtra(KEY_CONTENT, content)
if (context is Activity) {
context.startActivity(
intent,
ActivityOptions.makeSceneTransitionAnimation(context)
.toBundle()
)
return
}
context.startActivity(intent)
}
}
override fun setLayoutId(): Int {
return R.layout.activity_markdown_editor
}
override fun initData(savedInstanceState: Bundle?) {
super.initData(savedInstanceState)
reqCode = intent.getIntExtra(KEY_REQUESTOIN_CODE, -1)
content = intent.getCharSequenceExtra(KEY_CONTENT)
if (reqCode == -1) {
Timber.e( "没有设置请求码")
finishAfterTransition()
return
}
binding.mdeEditor.setText(content)
binding.mdeEditor.setOnSaveListener(object : MarkDownEditor.OnSaveListener {
override fun onSave(content: CharSequence?) {
EventBus.getDefault()
.post(EditorEvent(reqCode, content))
finishAfterTransition()
}
})
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/view/QrCodeScannerActivity.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.view
import android.os.Bundle
import android.view.KeyEvent
import com.lyy.keepassa.R
import com.lyy.keepassa.base.BaseActivity
import com.lyy.keepassa.databinding.ActivityQrCodeScannerBinding
/**
* @Author laoyuyu
* @Description
* @Date 2022/1/11
**/
internal class QrCodeScannerActivity : BaseActivity() {
private lateinit var capture: KpaCaptureManager
override fun setLayoutId(): Int {
return R.layout.activity_qr_code_scanner
}
override fun initData(savedInstanceState: Bundle?) {
super.initData(savedInstanceState)
capture = KpaCaptureManager(this, binding.barcodeView)
capture.initializeFromIntent(this.intent, savedInstanceState)
capture.decode()
}
override fun onResume() {
super.onResume()
capture.onResume()
}
override fun onPause() {
super.onPause()
capture.onPause()
}
override fun onDestroy() {
super.onDestroy()
capture.onDestroy()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
capture.onSaveInstanceState(outState)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
capture.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
return capture.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event)
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/view/SimpleAdapter.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.view
import android.content.Context
import android.view.View
import android.widget.TextView
import androidx.appcompat.widget.AppCompatImageView
import androidx.preference.PreferenceManager
import com.arialyy.frame.util.adapter.AbsHolder
import com.arialyy.frame.util.adapter.AbsRVAdapter
import com.google.android.material.imageview.ShapeableImageView
import com.google.android.material.shape.CornerFamily
import com.google.android.material.shape.ShapeAppearanceModel
import com.keepassdroid.database.PwGroup
import com.lyy.keepassa.R
import com.lyy.keepassa.base.BaseApp
import com.lyy.keepassa.entity.SimpleItemEntity
import com.lyy.keepassa.view.SimpleAdapter.Holder
import com.lyy.keepassa.widget.toPx
/**
* list适配器
*/
class SimpleAdapter(
context: Context,
data: List
) : AbsRVAdapter(context, data) {
private val useRoundedCorners by lazy {
PreferenceManager.getDefaultSharedPreferences(BaseApp.APP)
.getBoolean(BaseApp.APP.getString(R.string.set_key_fillet_bg_icon), true)
}
private val shapeMode by lazy {
ShapeAppearanceModel.Builder()
.setAllCorners(
CornerFamily.ROUNDED,
8.toPx()
.toFloat()
)
.build()
}
override fun getViewHolder(
convertView: View?,
viewType: Int
): Holder {
return Holder(convertView!!)
}
override fun setLayoutId(type: Int): Int {
return R.layout.item_path_type
}
override fun bindData(
holder: Holder,
position: Int,
item: SimpleItemEntity
) {
// if (useRoundedCorners) {
// holder.icon.shapeAppearanceModel = shapeMode
// }
holder.icon.setImageResource(item.icon)
holder.title.text = item.title
holder.des.text = item.subTitle
}
class Holder(view: View) : AbsHolder(view) {
val icon: ShapeableImageView = view.findViewById(R.id.icon)
val title: TextView = view.findViewById(R.id.title)
val des: TextView = view.findViewById(R.id.des)
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/view/SimpleEntryAdapter.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.view
import android.content.Context
import android.graphics.Paint
import android.view.View
import android.widget.TextView
import androidx.appcompat.widget.AppCompatImageView
import com.arialyy.frame.util.adapter.AbsHolder
import com.arialyy.frame.util.adapter.AbsRVAdapter
import com.keepassdroid.database.PwEntry
import com.keepassdroid.database.PwGroup
import com.lyy.keepassa.R
import com.lyy.keepassa.entity.EntryType
import com.lyy.keepassa.entity.SimpleItemEntity
import com.lyy.keepassa.util.IconUtil
import com.lyy.keepassa.util.loadImg
import com.lyy.keepassa.view.SimpleEntryAdapter.Holder
import java.util.Date
/**
* list适配器
*/
class SimpleEntryAdapter(
context: Context,
data: List
) : AbsRVAdapter(context, data) {
override fun getViewHolder(
convertView: View?,
viewType: Int
): Holder {
return Holder(convertView!!)
}
override fun setLayoutId(type: Int): Int {
return R.layout.item_entry
}
override fun bindData(
holder: Holder,
position: Int,
item: SimpleItemEntity
) { if (item.obj is PwGroup) {
IconUtil.setGroupIcon(context, item.obj as PwGroup, holder.icon)
} else if (item.obj is PwEntry) {
IconUtil.setEntryIcon(item.obj as PwEntry, holder.icon)
val paint = holder.title.paint
if ((item.obj as PwEntry).expires()
&& (item.obj as PwEntry).expiryTime != null
&& (item.obj as PwEntry).expiryTime.before(Date(System.currentTimeMillis()))
) {
paint.flags = Paint.STRIKE_THRU_TEXT_FLAG
paint.isAntiAlias = true
} else {
paint.flags = 0
}
} else if (item.obj == EntryType.TYPE_COLLECTION) {
holder.icon.loadImg(item.icon)
}
holder.title.text = item.title
holder.des.text = item.subTitle
holder.des.visibility = if (item.subTitle.isBlank()) View.GONE else View.VISIBLE
}
class Holder(view: View) : AbsHolder(view) {
val icon: AppCompatImageView = view.findViewById(R.id.icon)
val title: TextView = view.findViewById(R.id.title)
val des: TextView = view.findViewById(R.id.des)
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/view/StorageType.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.view
import androidx.annotation.DrawableRes
import com.lyy.keepassa.R
import com.lyy.keepassa.base.BaseApp
enum class StorageType(
var type: Int,
@DrawableRes var icon: Int,
var lable: String
) {
AFS(0, R.drawable.ic_android, BaseApp.APP.getString(R.string.afs)),
DROPBOX(1, R.drawable.ic_dropbox, "Dropbox"),
ONE_DRIVE(2, R.drawable.ic_onedrive, "OneDrive"),
GOOGLE_DRIVE(3, R.drawable.ic_google_drive, "GoogleDrive"),
WEBDAV(4, R.drawable.ic_http, "WebDav"),
FTP(5, R.drawable.ic_ftp, "Ftp"),
UNKNOWN(-1, R.drawable.ic_android, "Unknown")
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/view/UpgradeLogDialog.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.view
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.provider.Settings
import android.text.TextUtils
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.edit
import com.arialyy.frame.router.Routerfit
import com.arialyy.frame.util.AndroidUtils
import com.arialyy.frame.util.ResUtil
import com.lyy.keepassa.R
import com.lyy.keepassa.base.BaseDialog
import com.lyy.keepassa.base.Constance
import com.lyy.keepassa.databinding.DialogUpgradeBinding
import com.lyy.keepassa.router.ActivityRouter
import com.lyy.keepassa.router.DialogRouter
import com.lyy.keepassa.util.FingerprintUtil
import com.lyy.keepassa.util.KpaUtil
import com.lyy.keepassa.util.LanguageUtil
import com.lyy.keepassa.view.dialog.DonateDialog
import com.lyy.keepassa.view.fingerprint.FingerprintActivity
import com.lyy.keepassa.widget.DrawableTextView
import com.lyy.keepassa.widget.toPx
import com.zzhoujay.richtext.RichText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.InputStream
/**
* 版本升级对话框
*/
class UpgradeLogDialog : BaseDialog() {
private val scope = MainScope()
override fun setLayoutId(): Int {
return R.layout.dialog_upgrade
}
override fun initData() {
super.initData()
scope.launch {
var context = ""
// val fileName = "version_log/version_log_${getVersionSuffix()}.md"
val fileName = "version_log/version_log_${if (KpaUtil.isChina()) "zh_CN" else "en"}.md"
withContext(Dispatchers.IO) {
// val ins = requireContext().assets.open(fileName)
// context = String(ins.readBytes())
// ins.close()
var ins: InputStream? = null
try {
ins = requireContext().assets.open(fileName)
} catch (e: Exception) {
ins = requireContext().assets.open("version_log/version_log_en.md")
Timber.e(e)
}
ins?.let {
context = String(it.readBytes())
it.close()
}
}
RichText.fromMarkdown(context)
.urlClick { url ->
if (handlerUrlClick(url)) {
dismiss()
}
return@urlClick true
}
.into(binding.tvContent)
}
binding.btEnter.setOnClickListener {
dismiss()
}
binding.btDonate.setDrawable(
DrawableTextView.LEFT,
ResUtil.getSvgIcon(R.drawable.ic_favorite_24px, R.color.text_blue_color),
16.toPx(),
16.toPx()
)
binding.btDonate.setOnClickListener {
DonateDialog().show()
}
}
override fun onStart() {
super.onStart()
dialog?.window?.setLayout(360.toPx(), 600.toPx())
}
override fun dismiss() {
super.dismiss()
requireContext().getSharedPreferences(Constance.PRE_FILE_NAME, Context.MODE_PRIVATE)
.edit {
putInt(Constance.VERSION_CODE, AndroidUtils.getVersionCode(requireContext()))
}
}
/**
* 根据语言获取版本日志后缀名
*/
private fun getVersionSuffix(): String {
var defLocal = LanguageUtil.getDefLanguage(requireContext())
if (defLocal == null) {
defLocal = LanguageUtil.getSysCurrentLan()
}
return if (TextUtils.isEmpty(defLocal.country)) {
defLocal.language
} else {
"${defLocal.language}_${defLocal.country}"
}
}
/**
* 除了url点击
* @return true 已处理
*/
private fun handlerUrlClick(url: String): Boolean {
val uri = Uri.parse(url)
if (uri.scheme == "route") {
val activity = uri.getQueryParameter("activity")
if (!activity.isNullOrEmpty()) {
when (activity) {
"FingerprintActivity" -> {
if (FingerprintUtil.hasBiometricPrompt(requireContext())) {
FingerprintActivity.toFingerprintActivity(requireActivity())
return true
}
}
"WebDavLoginDialog" -> {
Routerfit.create(DialogRouter::class.java).showWebDavLoginDialog()
return true
}
"SettingActivity" -> {
val type = uri.getQueryParameter("type")
when (type) {
"db" -> {
Routerfit.create(ActivityRouter::class.java, requireActivity()).toAppSetting(
opt = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity())
)
}
"app" -> {
val scrollKey = uri.getQueryParameter("scrollKey")
Routerfit.create(ActivityRouter::class.java, requireActivity()).toAppSetting(
opt = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity()),
scrollKey = scrollKey
)
}
}
return true
}
"ime" -> {
startActivity(Intent(Settings.ACTION_INPUT_METHOD_SETTINGS))
return true
}
}
}
} else {
Timber.d("url = $url")
startActivity(Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse(url)
})
}
return false
}
override fun onDestroy() {
super.onDestroy()
scope.cancel()
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/view/collection/CollectionActivity.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.view.collection
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.View
import androidx.appcompat.widget.AppCompatImageView
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.alibaba.android.arouter.facade.annotation.Route
import com.alibaba.android.arouter.launcher.ARouter
import com.arialyy.frame.router.Routerfit
import com.arialyy.frame.util.ResUtil
import com.keepassdroid.database.PwEntry
import com.lyy.keepassa.R
import com.lyy.keepassa.base.BaseActivity
import com.lyy.keepassa.databinding.ActivityCollectionBinding
import com.lyy.keepassa.event.CollectionEventType.COLLECTION_STATE_ADD
import com.lyy.keepassa.event.CollectionEventType.COLLECTION_STATE_REMOVE
import com.lyy.keepassa.event.CollectionEventType.COLLECTION_STATE_TOTAL
import com.lyy.keepassa.router.DialogRouter
import com.lyy.keepassa.util.KeepassAUtil
import com.lyy.keepassa.util.KpaUtil
import com.lyy.keepassa.util.doOnItemClickListener
import com.lyy.keepassa.view.SimpleEntryAdapter
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
/**
* @Author laoyuyu
* @Description
* @Date 19:43 上午 2022/3/29
**/
@Route(path = "/collection/ac")
internal class CollectionActivity : BaseActivity() {
private lateinit var module: CollectionModule
private lateinit var adapter: SimpleEntryAdapter
override fun setLayoutId(): Int {
return R.layout.activity_collection
}
override fun initData(savedInstanceState: Bundle?) {
super.initData(savedInstanceState)
toolbar.title = ResUtil.getString(R.string.my_collection)
module = ViewModelProvider(this)[CollectionModule::class.java]
adapter = SimpleEntryAdapter(this, module.itemDataList)
binding.rvList.let {
it.layoutManager = LinearLayoutManager(this)
it.setHasFixedSize(true)
it.adapter = adapter
}
binding.rvList.doOnItemClickListener { _, position, v ->
val item = module.itemDataList[position]
val icon = v.findViewById(R.id.icon)
KeepassAUtil.instance.turnEntryDetail(this, item.obj as PwEntry, icon)
}
binding.emptyView.setText(ResUtil.getString(R.string.no_collection))
listenerCollection()
listenerGetData()
module.getData()
}
@SuppressLint("NotifyDataSetChanged")
private fun listenerGetData() {
lifecycleScope.launch {
module.itemDataFlow.collectLatest {
if (it.isNullOrEmpty()) {
binding.emptyView.visibility = View.VISIBLE
return@collectLatest
}
binding.emptyView.visibility = View.GONE
adapter.notifyDataSetChanged()
}
}
}
@SuppressLint("NotifyDataSetChanged")
private fun listenerCollection() {
lifecycleScope.launch {
KpaUtil.kdbHandlerService.collectionStateFlow.collectLatest {
if (it.collectionNum == 0) {
binding.emptyView.visibility = View.VISIBLE
return@collectLatest
}
binding.emptyView.visibility = View.GONE
when (it.state) {
COLLECTION_STATE_TOTAL -> {
adapter.notifyDataSetChanged()
}
COLLECTION_STATE_ADD -> {
module.addNewItem(adapter, it.pwEntryV4)
}
COLLECTION_STATE_REMOVE -> {
module.removeItem(adapter, it.pwEntryV4)
}
}
}
}
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/view/collection/CollectionModule.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.view.collection
import androidx.lifecycle.viewModelScope
import com.keepassdroid.database.PwEntryV4
import com.lyy.keepassa.base.BaseModule
import com.lyy.keepassa.entity.SimpleItemEntity
import com.lyy.keepassa.util.KeepassAUtil
import com.lyy.keepassa.util.KpaUtil
import com.lyy.keepassa.view.SimpleEntryAdapter
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
/**
* @Author laoyuyu
* @Description
* @Date 19:48 上午 2022/3/29
**/
internal class CollectionModule : BaseModule() {
val itemDataList = arrayListOf()
val itemDataFlow = MutableStateFlow?>(null)
fun removeItem(adapter: SimpleEntryAdapter, newEntry: PwEntryV4?) {
if (newEntry == null) {
Timber.d("entry is null")
return
}
val newItem = KeepassAUtil.instance.convertPwEntry2Item(newEntry)
var removePosition = -1
itemDataList.forEachIndexed { index, simpleItemEntity ->
if (simpleItemEntity.obj == newItem.obj) {
removePosition = index
return@forEachIndexed
}
}
if (removePosition == -1) {
Timber.d("the entry is not in the list, title = ${newEntry.title}")
return
}
itemDataList.removeAt(removePosition)
adapter.notifyItemRemoved(removePosition)
}
/**
* add new collection
*/
fun addNewItem(adapter: SimpleEntryAdapter, newEntry: PwEntryV4?) {
if (newEntry == null) {
Timber.d("entry is null")
return
}
val newItem = KeepassAUtil.instance.convertPwEntry2Item(newEntry)
val temp = itemDataList.find { it.obj == newItem.obj }
if (temp != null) {
Timber.d("already has the entry, title = ${newEntry.title}")
return
}
val newPosition = itemDataList.size
itemDataList.add(newItem)
adapter.notifyItemInserted(newPosition)
}
fun getData() {
itemDataList.clear()
KpaUtil.kdbHandlerService.getCollectionEntries().forEach {
itemDataList.add(KeepassAUtil.instance.convertPwEntry2Item(it))
}
viewModelScope.launch {
itemDataFlow.emit(itemDataList)
}
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/view/create/CreateCustomStrDialog.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.view.create
import android.view.View
import androidx.lifecycle.lifecycleScope
import com.alibaba.android.arouter.facade.annotation.Autowired
import com.alibaba.android.arouter.facade.annotation.Route
import com.alibaba.android.arouter.launcher.ARouter
import com.keepassdroid.database.security.ProtectedString
import com.lyy.keepassa.R
import com.lyy.keepassa.base.BaseDialog
import com.lyy.keepassa.databinding.DialogAddAttrStrBinding
import com.lyy.keepassa.entity.CommonState
import com.lyy.keepassa.event.AttrStrEvent
import com.lyy.keepassa.util.HitUtil
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
/**
* 创建自定义字段的对话框
*/
@Route(path = "/dialog/customStrDialog")
class CreateCustomStrDialog : BaseDialog(),
View.OnClickListener {
companion object {
val CustomStrFlow = MutableSharedFlow(0)
}
@Autowired(name = "key")
@JvmField
var key: String? = null
@Autowired(name = "value")
@JvmField
var value: ProtectedString? = null
@Autowired(name = "position")
@JvmField
var position: Int = 0
override fun setLayoutId(): Int {
return R.layout.dialog_add_attr_str
}
override fun initData() {
super.initData()
ARouter.getInstance().inject(this)
binding.cancel.setOnClickListener(this)
binding.enter.setOnClickListener(this)
if (key != null) {
binding.strKey.setText(key)
}
if (value != null) {
binding.strValue.setText(value.toString())
binding.cb.isChecked = value!!.isProtected
}
}
override fun onClick(v: View?) {
if (v!!.id == R.id.enter) {
if (binding.strKey.text.toString().trim().isEmpty()) {
HitUtil.toaskShort(getString(R.string.error_attr_str_null))
return
}
lifecycleScope.launch {
CustomStrFlow.emit(
AttrStrEvent(
if (key != null) CommonState.MODIFY else CommonState.CREATE,
binding.strKey.text.toString(),
ProtectedString(binding.cb.isChecked, binding.strValue.text.toString()),
position
)
)
}
}
dismiss()
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/view/create/CreateDbActivity.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.view.create
import android.os.Bundle
import android.text.TextUtils
import android.view.View
import android.view.ViewAnimationUtils
import androidx.appcompat.widget.Toolbar
import androidx.core.app.ActivityOptionsCompat
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.transition.Transition
import androidx.transition.TransitionInflater
import com.alibaba.android.arouter.facade.annotation.Route
import com.arialyy.frame.router.Routerfit
import com.blankj.utilcode.util.ToastUtils
import com.lyy.keepassa.R
import com.lyy.keepassa.base.BaseActivity
import com.lyy.keepassa.base.BaseApp
import com.lyy.keepassa.databinding.ActivityCreateDbBinding
import com.lyy.keepassa.router.ActivityRouter
import com.lyy.keepassa.util.HitUtil
import com.lyy.keepassa.util.KeepassAUtil
import com.lyy.keepassa.util.KpaUtil
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import timber.log.Timber
/**
* 创建见数据库页面
*/
@Route(path = "/launcher/createDb")
class CreateDbActivity : BaseActivity(), View.OnClickListener {
private var curSetup = 1
private lateinit var firstFragment: CreateDbFirstFragment
private var secondFragment: CreateDbSecondFragment? = null
private lateinit var module: CreateDbModule
override fun initData(savedInstanceState: Bundle?) {
super.initData(savedInstanceState)
module = ViewModelProvider(this).get(CreateDbModule::class.java)
toolbar.setTitle(R.string.create_db)
binding.next.setOnClickListener(this)
binding.up.setOnClickListener(this)
firstFragment = CreateDbFirstFragment()
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.content, firstFragment)
transaction.commitNow()
listenerOpenDb()
}
private fun listenerOpenDb() {
lifecycleScope.launch {
KpaUtil.kdbOpenService.openDbFlow.collectLatest {
if (it == null) {
ToastUtils.showShort("${getString(R.string.open_db)}${getString(R.string.fail)}")
return@collectLatest
}
Timber.d("创建数据库成功")
HitUtil.toaskShort(getString(R.string.hint_db_create_success, module.dbName))
Routerfit.create(ActivityRouter::class.java, this@CreateDbActivity).toMainActivity(
opt = ActivityOptionsCompat.makeSceneTransitionAnimation(this@CreateDbActivity)
)
KeepassAUtil.instance.saveLastOpenDbHistory(BaseApp.dbRecord)
finishAfterTransition()
}
}
}
/**
* 右 -> 左
*/
private fun getRlAnim(): Transition {
return TransitionInflater.from(this)
.inflateTransition(R.transition.slide_enter)
}
/**
* 左 -> 右
*/
private fun getLrAnim(): Transition {
return TransitionInflater.from(this)
.inflateTransition(R.transition.slide_exit)
}
override fun setLayoutId(): Int {
return R.layout.activity_create_db
}
override fun onBackPressed() {
if (curSetup == 2) {
upFragment()
} else {
finishAfterTransition()
}
}
override fun onClick(v: View?) {
if (KeepassAUtil.instance.isFastClick()) {
return
}
when (v!!.id) {
R.id.next -> {
if (curSetup == 1) {
// startNextFragment()
firstFragment.startNext()
} else { // 完成
done()
}
}
R.id.up -> upFragment()
}
}
/**
* 完成信息输入,并创建数据库
*/
private fun done() {
// 密码需要重新获取,将密码设置到module中
secondFragment?.getPass()
module.createAndOpenDb()
}
/**
* 开始设置密码
*/
fun startNextFragment() {
if (TextUtils.isEmpty(firstFragment.getDbName())) {
firstFragment.handleDbNameNull()
return
} else if (module.localDbUri == null) {
firstFragment.showSaveTypeDialog()
return
}
curSetup = 2
binding.next.setText(R.string.done)
binding.up.visibility = View.VISIBLE
if (secondFragment == null) {
secondFragment = CreateDbSecondFragment()
}
/*
* 重新设置动画:
* fragment1 (进入)左 -> 右;(退出)左 -> 右
* fragment2 (进入)右 -> 左;(退出)左 -> 右
*/
firstFragment.exitTransition = getLrAnim()
secondFragment!!.enterTransition = getRlAnim()
val changeBoundsTransition = TransitionInflater.from(this)
// .inflateTransition(R.transition.changebounds_with_arcmotion)
.inflateTransition(android.R.transition.move)
secondFragment!!.sharedElementEnterTransition = changeBoundsTransition
supportFragmentManager.beginTransaction()
.replace(R.id.content, secondFragment!!)
.addSharedElement(firstFragment.getShareElement(), getString(R.string.transition_db_name))
.commit()
// changeBg(true)
}
/**
* 返回设置数据库路径
*/
private fun upFragment() {
curSetup = 1
binding.next.setText(R.string.next)
binding.up.visibility = View.GONE
/*
* 重新设置动画:
* fragment1 (进入)左 -> 右;(退出)右 -> 左
* fragment2 (进入)右 -> 左;(退出)右 -> 左
*/
firstFragment.enterTransition = getLrAnim()
firstFragment.exitTransition = getRlAnim()
secondFragment!!.exitTransition = getRlAnim()
val changeBoundsTransition = TransitionInflater.from(this)
// .inflateTransition(R.transition.changebounds_with_arcmotion)
.inflateTransition(android.R.transition.move)
firstFragment.sharedElementEnterTransition = changeBoundsTransition
supportFragmentManager.beginTransaction()
.replace(R.id.content, firstFragment)
.addSharedElement(
secondFragment!!.getShareElement(), getString(R.string.transition_db_name)
)
.commitNow()
// changeBg(false)
}
/**
* 切换fragment改变背景
*/
private fun changeBg(toSecondFragment: Boolean) {
val view = findViewById(R.id.kpa_toolbar)
val finalRadius = view.width.coerceAtLeast(view.height)
val anim = ViewAnimationUtils.createCircularReveal(
view, if (toSecondFragment) view.right else 0, 0, 0f, finalRadius.toFloat()
)
view.setBackgroundResource(
if (toSecondFragment) R.color.colorPrimary else R.color.white
)
anim.duration = resources.getInteger(R.integer.anim_duration_long)
.toLong()
// anim.interpolator = AccelerateInterpolator()
view.visibility = View.VISIBLE
anim.start()
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/view/create/CreateDbFirstFragment.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.view.create
import android.content.Intent
import android.text.TextUtils
import android.view.View
import android.view.inputmethod.EditorInfo
import androidx.collection.arrayMapOf
import androidx.lifecycle.ViewModelProvider
import com.arialyy.frame.router.Routerfit
import com.arialyy.frame.util.ResUtil
import com.lyy.keepassa.R
import com.lyy.keepassa.base.BaseFragment
import com.lyy.keepassa.databinding.FragmentCreateDbFirstBinding
import com.lyy.keepassa.event.DbPathEvent
import com.lyy.keepassa.router.DialogRouter
import com.lyy.keepassa.util.HitUtil
import com.lyy.keepassa.util.KeepassAUtil
import com.lyy.keepassa.util.cloud.DbSynUtil
import com.lyy.keepassa.view.StorageType
import com.lyy.keepassa.view.StorageType.AFS
import com.lyy.keepassa.view.StorageType.DROPBOX
import com.lyy.keepassa.view.StorageType.ONE_DRIVE
import com.lyy.keepassa.view.StorageType.UNKNOWN
import com.lyy.keepassa.view.StorageType.WEBDAV
import com.lyy.keepassa.view.create.auth.AuthFlowFactory
import com.lyy.keepassa.view.create.auth.IAuthCallback
import com.lyy.keepassa.view.create.auth.IAuthFlow
import com.lyy.keepassa.view.create.auth.OnNextFinishCallback
import com.lyy.keepassa.widget.BubbleTextView
import com.lyy.keepassa.widget.BubbleTextView.OnIconClickListener
import timber.log.Timber
/**
* 创建数据库的第一步
* 1、设置数据库保存类型
* 2、设置数据库名
*/
class CreateDbFirstFragment : BaseFragment() {
private lateinit var module: CreateDbModule
private lateinit var pathTypeDialog: PathTypeDialog
private var authFlow: IAuthFlow? = null
private var isAuthorized: Boolean = false
private val flowMap = arrayMapOf()
override fun initData() {
module = ViewModelProvider(requireActivity()).get(CreateDbModule::class.java)
initView()
}
private fun initView() {
setPathTypeInfo()
showSaveTypeDialog()
binding.pathType.setOnIconClickListener(object : OnIconClickListener {
override fun onClick(
view: BubbleTextView,
index: Int
) {
if (index == 2) {
Routerfit.create(DialogRouter::class.java)
.showMsgDialog(
msgContent = ResUtil.getString(R.string.help_create_db_path),
showCancelBt = false
)
}
}
})
// 设置键盘确定按钮属性
binding.dbName.setOnEditorActionListener { _, actionId, _ ->
if (!isAdded) {
return@setOnEditorActionListener false
}
// actionId 和android:imeOptions 属性要保持一致
if (actionId == EditorInfo.IME_ACTION_DONE && !TextUtils.isEmpty(binding.dbName.text)) {
KeepassAUtil.instance.toggleKeyBord(requireContext())
// showPathDialog()
startNext()
true
} else {
false
}
}
}
/**
* 处理数据库名没有设置的情况
*/
fun handleDbNameNull() {
val hint = getString(R.string.error_db_name_null)
binding.dbNameLayout.error = hint
binding.dbName.requestFocus()
HitUtil.toaskShort(hint)
KeepassAUtil.instance.toggleKeyBord(requireContext())
}
/**
* 和其它fragment共享的元素
*/
fun getShareElement(): View {
return binding.dbName
}
/**
* 获取数据库名
*/
fun getDbName(): String {
module.dbName = binding.dbName.text.toString()
.trim()
return module.dbName
}
fun showSaveTypeDialog() {
pathTypeDialog = PathTypeDialog(
binding.dbName.text.toString()
.trim()
)
pathTypeDialog.showNow(childFragmentManager, "PathDialog")
pathTypeDialog.setOnDismissListener {
if (module.storageType == UNKNOWN) {
requireActivity().finishAfterTransition()
return@setOnDismissListener
}
setPathTypeInfo()
authFlow = flowMap[module.storageType]
if (authFlow == null) {
authFlow = AuthFlowFactory.getAuthFlow(module.storageType)
flowMap[module.storageType] = authFlow
lifecycle.addObserver(authFlow!!)
}
authFlow?.let {
it.initContent(requireContext(), object : IAuthCallback {
override fun callback(success: Boolean) {
isAuthorized = success
binding.dbName.requestFocus()
}
})
it.startFlow()
}
}
}
/**
* 流程结束
*/
private fun finishFlow(event: DbPathEvent) {
if (event.fileUri == null && event.storageType == AFS) {
Timber.e("uri 获取失败")
return
}
// 直接启动下一界面
val startNextFragment = true
when (event.storageType) {
AFS -> {
binding.dbNameLayout.visibility = View.VISIBLE
module.localDbUri = event.fileUri!!
module.dbName = event.dbName
}
DROPBOX -> {
binding.dbNameLayout.visibility = View.VISIBLE
module.localDbUri = DbSynUtil.getCloudDbTempPath(DROPBOX.name, event.dbName)
module.cloudPath = event.cloudDiskPath!!
module.dbName = event.dbName
}
WEBDAV -> {
binding.dbNameLayout.visibility = View.GONE
module.dbName = event.dbName
module.localDbUri = DbSynUtil.getCloudDbTempPath(WEBDAV.name, event.dbName)
module.cloudPath = event.cloudDiskPath!!
}
ONE_DRIVE -> {
binding.dbNameLayout.visibility = View.VISIBLE
module.localDbUri = DbSynUtil.getCloudDbTempPath(ONE_DRIVE.name, event.dbName)
module.cloudPath = event.cloudDiskPath!!
module.dbName = event.dbName
}
else -> {
throw IllegalArgumentException("不支持的类型: ${event.storageType.lable}")
}
}
binding.dbName.setText(module.dbName)
setPathTypeInfo()
if (startNextFragment) {
(activity as CreateDbActivity).startNextFragment()
}
}
/**
* 检查是否可以进入下一步
*/
fun startNext(): Boolean {
val temp = binding.dbName.text.toString()
.trim()
if (TextUtils.isEmpty(temp)) {
HitUtil.toaskShort(getString(R.string.error_db_name_null))
return false
}
authFlow?.doNext(this, temp, object : OnNextFinishCallback {
override fun onFinish(event: DbPathEvent) {
finishFlow(event)
}
})
return true
}
/**
* 设置文件路径类型提示
*/
private fun setPathTypeInfo() {
binding.pathType.text = module.storageType.lable
binding.pathType.setLeftIcon(module.storageType.icon)
setDbNameHint(module.storageType)
}
/**
* 设置数据名输入提示
*/
private fun setDbNameHint(storageType: StorageType) {
binding.dbNameLayout.helperText = getString(R.string.help_create_db)
binding.dbNameLayout.hint = getString(R.string.db_name)
}
override fun setLayoutId(): Int {
return R.layout.fragment_create_db_first
}
override fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: Intent?
) {
super.onActivityResult(requestCode, resultCode, data)
authFlow?.onActivityResult(requestCode, resultCode, data)
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/view/create/CreateDbModule.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.view.create
import android.content.Context
import android.net.Uri
import androidx.core.app.ActivityOptionsCompat
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.liveData
import com.arialyy.frame.router.Routerfit
import com.lyy.keepassa.R
import com.lyy.keepassa.base.BaseApp
import com.lyy.keepassa.base.BaseModule
import com.lyy.keepassa.entity.SimpleItemEntity
import com.lyy.keepassa.router.ActivityRouter
import com.lyy.keepassa.util.HitUtil
import com.lyy.keepassa.util.KeepassAUtil
import com.lyy.keepassa.util.KpaUtil
import com.lyy.keepassa.util.NotificationUtil
import com.lyy.keepassa.view.StorageType
import com.lyy.keepassa.view.StorageType.UNKNOWN
import timber.log.Timber
class CreateDbModule : BaseModule() {
/**
* 设置的数据库密码
*/
var dbPass: String = ""
/**
* 数据库名,包含.kdbx
*/
var dbName: String = ""
/**
* 数据库uri
*/
var localDbUri: Uri? = null
/**
* key uri
*/
var keyUri: Uri? = null
/**
* key 的名字
*/
var keyName: String = ""
/**
* 数据库类型
*/
var storageType: StorageType = UNKNOWN
/**
* 云盘路径
*/
var cloudPath: String = ""
/**
* 创建并打开数据库
*/
fun createAndOpenDb() {
KpaUtil.kdbOpenService.createDb(dbName, localDbUri, dbPass, keyUri, cloudPath, storageType)
}
/**
* 数据库打开方式
*/
fun getDbOpenTypeData(context: Context) = liveData {
val titles = context.resources.getStringArray(R.array.cloud_names)
val icons = context.resources.obtainTypedArray(R.array.path_type_img)
val items = ArrayList()
for ((index, title) in titles.withIndex()) {
val item = SimpleItemEntity()
item.title = title
item.subTitle = titles[index]
item.id = index
item.icon = icons.getResourceId(index, 0)
items.add(item)
}
icons.recycle()
emit(items)
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/view/create/CreateDbSecondFragment.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.view.create
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.text.InputType
import android.text.TextUtils
import android.view.View
import android.view.animation.LinearInterpolator
import android.widget.RadioButton
import androidx.lifecycle.ViewModelProvider
import com.arialyy.frame.router.Routerfit
import com.lyy.keepassa.R
import com.lyy.keepassa.base.BaseFragment
import com.lyy.keepassa.databinding.FragmentCreateDbSecondBinding
import com.lyy.keepassa.event.KeyPathEvent
import com.lyy.keepassa.router.DialogRouter
import com.lyy.keepassa.util.EventBusHelper
import com.lyy.keepassa.util.HitUtil
import com.lyy.keepassa.util.KeepassAUtil
import com.lyy.keepassa.widget.BubbleTextView
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode.MAIN
/**
* 设置密码、密钥等信息
*/
class CreateDbSecondFragment : BaseFragment(),
BubbleTextView.OnIconClickListener {
private var keyPassLayoutH: Int = 0
private var isShowPass = false
private lateinit var module: CreateDbModule
override fun setLayoutId(): Int {
return R.layout.fragment_create_db_second
}
@SuppressLint("RestrictedApi")
override fun initData() {
EventBusHelper.reg(this)
module = ViewModelProvider(requireActivity()).get(CreateDbModule::class.java)
binding.dbName.setText(module.dbName)
val leftDrawable = resources.getDrawable(module.storageType.icon, requireContext().theme)
val iconSize = resources.getDimension(R.dimen.icon_size)
leftDrawable.setBounds(0, 0, iconSize.toInt(), iconSize.toInt())
binding.dbHint.setCompoundDrawables(leftDrawable, null, null, null)
binding.encryptGroup.setOnCheckedChangeListener { group, checkedId ->
val rb = group.findViewById(checkedId)
if (rb.tag == "1") {
// binding.passKeyLayout.visibility = View.GONE
hintPassLayout()
} else {
// binding.passKeyLayout.visibility = View.VISIBLE
showPassLayout()
}
}
binding.encryptType.setOnIconClickListener(this)
binding.passKey.setOnIconClickListener(this)
(binding.encryptGroup.getChildAt(0) as RadioButton).isChecked = true
binding.passKeyLayout.post {
keyPassLayoutH = binding.passKeyLayout.height
}
binding.chooseBt.setOnClickListener {
val dialog = CreatePassKeyDialog()
dialog.show(childFragmentManager, "passKeyDialog")
}
KeepassAUtil.instance.toggleKeyBord(requireContext())
binding.password.requestFocus()
binding.passwordLayout.endIconDrawable = resources.getDrawable(R.drawable.ic_view_off)
// binding.password.imeOptions = EditorInfo.IME_ACTION_NEXT
binding.passwordLayout.setEndIconOnClickListener {
isShowPass = !isShowPass
if (isShowPass) {
binding.passwordLayout.endIconDrawable = resources.getDrawable(R.drawable.ic_view)
binding.enterPasswordLayout.visibility = View.GONE
binding.password.inputType = InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
// 重新修改确认按钮
// binding.password.imeOptions = EditorInfo.IME_ACTION_NEXT
} else {
binding.passwordLayout.endIconDrawable = resources.getDrawable(R.drawable.ic_view_off)
binding.enterPasswordLayout.visibility = View.VISIBLE
binding.password.setRawInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT)
// 重新修改确认按钮
// binding.password.imeOptions = EditorInfo.IME_ACTION_DONE
}
// 将光标移动到最后
binding.password.setSelection(binding.password.text!!.length)
binding.password.requestFocus()
}
}
private fun showPassLayout() {
val h = resources.getDimension(R.dimen.create_pass_key_h)
.toInt()
binding.passKeyLayoutWrap.visibility = View.VISIBLE
binding.passKeyLayout.layoutParams.height = 0
binding.passKeyLayout.visibility = View.VISIBLE
val anim = ValueAnimator.ofInt(0, h)
anim.addUpdateListener { animation ->
binding.passKeyLayout.layoutParams.height = animation.animatedValue as Int
binding.passKeyLayout.requestLayout()
}
anim.interpolator = LinearInterpolator()
anim.duration = 400
anim.start()
}
private fun hintPassLayout() {
val h = resources.getDimension(R.dimen.create_pass_key_h)
.toInt()
binding.passKeyLayoutWrap.visibility = View.VISIBLE
binding.passKeyLayout.layoutParams.height = 0
binding.passKeyLayout.visibility = View.VISIBLE
module.keyUri = null
val anim = ValueAnimator.ofInt(h, 0)
anim.addUpdateListener { animation ->
binding.passKeyLayout.layoutParams.height = animation.animatedValue as Int
binding.passKeyLayout.requestLayout()
}
anim.interpolator = LinearInterpolator()
anim.duration = 400
anim.start()
anim.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
binding.passKeyLayout.visibility = View.GONE
binding.passKeyLayoutWrap.visibility = View.GONE
}
})
}
/**
* 获取密码,如果两次密码不一致,返回null
*/
fun getPass(): String? {
val pass = binding.password.text.toString()
.trim()
val enterPass = binding.enterPassword.text.toString()
.trim()
if (TextUtils.isEmpty(pass)) {
HitUtil.toaskShort(getString(R.string.error_pass_null))
return null
}
// 如果没有显示密码,需要判断两次输入的密码是否一致
if (!isShowPass) {
if (TextUtils.isEmpty(enterPass)) {
HitUtil.toaskShort(getString(R.string.error_enter_pass_null))
binding.enterPassword.requestFocus()
KeepassAUtil.instance.toggleKeyBord(requireContext())
return null
}
if (!pass.equals(enterPass, false)) {
HitUtil.toaskShort(getString(R.string.error_pass_unfit))
return null
}
}
module.dbPass = pass
return module.dbPass
}
/**
* 获取key的路径
*/
@Subscribe(threadMode = MAIN)
fun onKeyEvent(event: KeyPathEvent) {
module.keyUri = event.keyUri
module.keyName = event.keyName
binding.passKeyName.setText(module.keyName)
}
fun getShareElement(): View {
return binding.dbName
}
override fun onClick(
view: BubbleTextView,
index: Int
) {
var msg = ""
when (view.id) {
R.id.encrypt_type -> {
msg = getString(R.string.help_pass_type)
}
R.id.pass_key -> {
msg = getString(R.string.help_pass_key)
}
}
Routerfit.create(DialogRouter::class.java).showMsgDialog(msgContent = msg, showCancelBt = false)
}
override fun onDestroy() {
super.onDestroy()
EventBusHelper.unReg(this)
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/view/create/CreateGroupDialog.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.view.create
import android.view.View
import androidx.lifecycle.ViewModelProvider
import com.alibaba.android.arouter.facade.annotation.Autowired
import com.alibaba.android.arouter.facade.annotation.Route
import com.alibaba.android.arouter.launcher.ARouter
import com.keepassdroid.database.PwGroupV4
import com.keepassdroid.database.PwIconCustom
import com.keepassdroid.database.PwIconStandard
import com.lyy.keepassa.R
import com.lyy.keepassa.base.BaseApp
import com.lyy.keepassa.base.BaseDialog
import com.lyy.keepassa.databinding.DialogAddGroupBinding
import com.lyy.keepassa.util.HitUtil
import com.lyy.keepassa.util.IconUtil
import com.lyy.keepassa.view.create.entry.CreateEntryModule
import com.lyy.keepassa.view.icon.IconBottomSheetDialog
import com.lyy.keepassa.view.icon.IconItemCallback
/**
* 创建或编辑群组dialog
*/
@Route(path = "/dialog/createGroup")
class CreateGroupDialog : BaseDialog(), View.OnClickListener {
private var icon = PwIconStandard(48)
private var csIcon: PwIconCustom? = null
private lateinit var module: CreateEntryModule
@Autowired(name = "parentGroup")
@JvmField
var parentGroup: PwGroupV4 = BaseApp.KDB!!.pm.rootGroup as PwGroupV4
override fun setLayoutId(): Int {
return R.layout.dialog_add_group
}
override fun initData() {
super.initData()
ARouter.getInstance().inject(this)
module = ViewModelProvider(this).get(CreateEntryModule::class.java)
binding.groupNameLayout.setEndIconOnClickListener {
showIconDialog()
}
binding.enter.setOnClickListener(this)
binding.cancel.setOnClickListener(this)
}
private fun showIconDialog() {
val iconDialog = IconBottomSheetDialog()
iconDialog.setCallback(object : IconItemCallback {
override fun onDefaultIcon(defIcon: PwIconStandard) {
icon = defIcon
binding.groupNameLayout.endIconDrawable =
resources.getDrawable(IconUtil.getIconById(icon.iconId), requireContext().theme)
csIcon = PwIconCustom.ZERO
}
override fun onCustomIcon(customIcon: PwIconCustom) {
csIcon = customIcon
binding.groupNameLayout.endIconDrawable =
IconUtil.convertCustomIcon2Drawable(requireContext(), csIcon!!)
}
})
iconDialog.show(childFragmentManager, IconBottomSheetDialog::class.java.simpleName)
}
override fun onClick(v: View?) {
when (v!!.id) {
R.id.enter -> {
val title = binding.groupName.text.toString()
.trim()
if (title.isEmpty()) {
HitUtil.toaskShort(getString(R.string.error_group_name_null))
return
}
if (title.length > 16) {
HitUtil.toaskShort(requireContext().getString(R.string.title_too_long))
return
}
createGroup()
}
R.id.cancel -> {
dismiss()
}
}
}
/**
* 创建群组
*/
private fun createGroup() {
module.createGroup(
binding.groupName.text.toString(),
parentGroup,
icon,
csIcon
) {
HitUtil.toaskShort(
"${BaseApp.APP.getString(R.string.create_group)}${
BaseApp.APP.getString(
R.string.success
)
}"
)
dismiss()
}
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/view/create/CreatePassKeyDialog.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.view.create
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.Toast
import com.arialyy.frame.util.StringUtil
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.keepassdroid.utils.UriUtil
import com.lyy.keepassa.R
import com.lyy.keepassa.base.BaseBottomSheetDialogFragment
import com.lyy.keepassa.databinding.DialogPassKeyBinding
import com.lyy.keepassa.event.KeyPathEvent
import com.lyy.keepassa.util.HitUtil
import com.lyy.keepassa.util.KeepassAUtil
import com.lyy.keepassa.util.PasswordBuildUtil
import com.lyy.keepassa.util.takePermission
import org.greenrobot.eventbus.EventBus
import timber.log.Timber
import java.io.FileOutputStream
import java.io.IOException
/**
* 创建key对话框
*/
class CreatePassKeyDialog : BaseBottomSheetDialogFragment(),
View.OnClickListener {
private lateinit var behavior: BottomSheetBehavior<*>
private val openFileReqCode = 0xB1
private val createFileReqCode = 0xB2
override fun setLayoutId(): Int {
return R.layout.dialog_pass_key
}
override fun init(savedInstanceState: Bundle?) {
super.init(savedInstanceState)
behavior = BottomSheetBehavior.from(binding.content)
binding.close.setOnClickListener(this)
binding.item1.setOnClickListener(this)
binding.item2.setOnClickListener(this)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
override fun onClick(v: View?) {
when (v!!.id) {
R.id.close -> dismiss()
R.id.item_1 -> {
KeepassAUtil.instance.openSysFileManager(this, "*/*", openFileReqCode)
}
R.id.item_2 -> {
KeepassAUtil.instance.createFile(
this, "*/*", "${getString(R.string.app_name)}.passkey", createFileReqCode
)
}
}
}
/**
* 将一个随机字符串写入密钥文件中
*/
private fun writeData(uri: Uri?) {
val fos = requireContext().contentResolver.openOutputStream(uri!!) as FileOutputStream
try {
val str = PasswordBuildUtil.getInstance()
.addLowerChar()
.addNumChar()
.addMinus()
.addSymbolChar()
.builder(128)
fos.write(
StringUtil.keyToHashKey(str)
.toByteArray()
)
fos.flush()
} catch (e: IOException) {
Timber.e(e)
} finally {
fos.close()
}
}
override fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: Intent?
) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK && data != null && data.data != null) {
// 申请长期的uri权限
data.data?.takePermission()
when (requestCode) {
openFileReqCode -> {
EventBus.getDefault()
.post(
KeyPathEvent(
keyName = UriUtil.getFileNameFromUri(requireContext(), data.data),
keyUri = data.data!!
)
)
}
createFileReqCode -> {
writeData(data.data)
Toast.makeText(
context, getString(
R.string.create_pass_key_success,
UriUtil.getFileNameFromUri(requireContext(), data.data!!)
), Toast.LENGTH_SHORT
)
.show()
EventBus.getDefault()
.post(
KeyPathEvent(
keyName = UriUtil.getFileNameFromUri(requireContext(), data.data),
keyUri = data.data!!
)
)
}
else -> {
Timber.e("未知请求码:$requestCode")
}
}
dismiss()
} else {
HitUtil.toaskShort("${getString(R.string.invalid)} ${getString(R.string.key)}")
Timber.e("选择密钥文件失败,data为空")
}
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/view/create/GeneratePassActivity.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.view.create
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.text.TextUtils
import android.widget.CompoundButton
import android.widget.CompoundButton.OnCheckedChangeListener
import androidx.core.widget.doAfterTextChanged
import com.google.android.material.slider.Slider
import com.google.android.material.slider.Slider.OnSliderTouchListener
import com.lyy.keepassa.R
import com.lyy.keepassa.base.BaseActivity
import com.lyy.keepassa.databinding.ActivityGeneratePassNewBinding
import com.lyy.keepassa.util.ClipboardUtil
import com.lyy.keepassa.util.HitUtil
import com.lyy.keepassa.util.PasswordBuildUtil
import com.lyy.keepassa.util.doClick
/**
* 密码生成器
*/
class GeneratePassActivity : BaseActivity(),
OnCheckedChangeListener {
private lateinit var generater: PasswordBuildUtil
private var passLen = 16
private var isUserInputPass = false
companion object {
const val DATA_PASS_WORD = "DATA_PASS_WORD"
}
override fun setLayoutId(): Int {
return R.layout.activity_generate_pass_new
}
override fun initData(savedInstanceState: Bundle?) {
super.initData(savedInstanceState)
toolbar.title = getString(R.string.pass_generater)
// binding.cancel.setOnClickListener {
// finishAfterTransition()
// }
binding.edPassLen.setText("$passLen")
binding.slider.addOnSliderTouchListener(object : OnSliderTouchListener {
override fun onStartTrackingTouch(slider: Slider) {
}
override fun onStopTrackingTouch(slider: Slider) {
isUserInputPass = false
passLen = slider.value.toInt()
binding.edPassLen.setText("$passLen")
}
})
generater = PasswordBuildUtil.getInstance()
binding.scUAZ.setOnCheckedChangeListener(this)
binding.scLAZ.setOnCheckedChangeListener(this)
binding.scNum.setOnCheckedChangeListener(this)
binding.scCh.setOnCheckedChangeListener(this)
binding.scBracketChar.setOnCheckedChangeListener(this)
binding.scSpace.setOnCheckedChangeListener(this)
binding.scUAZ.isChecked = true
binding.scLAZ.isChecked = true
binding.scNum.isChecked = true
binding.scCh.isChecked = true
generatePass(passLen)
binding.ivRefresh.doClick {
if (checkParamsIsInvalid()) {
HitUtil.toaskShort(getString(R.string.error_genera_params))
return@doClick
}
generatePass(passLen)
}
binding.ivCopy.doClick {
if (checkParamsIsInvalid()) {
HitUtil.toaskShort(getString(R.string.error_genera_params))
return@doClick
}
ClipboardUtil.get()
.copyDataToClip(binding.edPass.text.toString())
}
binding.edPassLen.doAfterTextChanged { text ->
if (!TextUtils.isEmpty(text)) {
passLen = text.toString()
.toInt()
generatePass(passLen)
}
}
binding.slider.setLabelFormatter {
"${it.toInt()}"
}
}
override fun finishAfterTransition() {
val intent = Intent()
intent.putExtra(DATA_PASS_WORD, binding.edPass.text.toString().trim())
setResult(Activity.RESULT_OK, intent)
super.finishAfterTransition()
}
/**
* 检查密码生成条件
* @return true: 条件无效
*/
private fun checkParamsIsInvalid(): Boolean {
return !binding.scUAZ.isChecked
&& !binding.scLAZ.isChecked
&& !binding.scNum.isChecked
&& !binding.scCh.isChecked
&& !binding.scBracketChar.isChecked
&& !binding.scSpace.isChecked
}
/**
* 生产密码
* @param len 密码长度
*/
private fun generatePass(len: Int): String {
if (checkParamsIsInvalid()) {
binding.edPass.setText("")
return ""
}
generater.clear()
if (binding.scUAZ.isChecked) {
generater.addUpChar()
}
if (binding.scLAZ.isChecked) {
generater.addLowerChar()
}
if (binding.scNum.isChecked) {
generater.addNumChar()
}
if (binding.scCh.isChecked) {
generater.addMinus()
generater.addUnderline()
generater.addSymbolChar()
}
if (binding.scSpace.isChecked) {
generater.addSpaceChar()
}
if (binding.scBracketChar.isChecked) {
generater.addBracketChar()
}
val pass = generater.builder(len)
binding.edPass.setText(pass)
return pass
}
override fun onCheckedChanged(
buttonView: CompoundButton?,
isChecked: Boolean
) {
generatePass(passLen)
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/view/create/PathTypeDialog.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.view.create
import android.os.Bundle
import android.view.View
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.arialyy.frame.util.adapter.RvItemClickSupport
import com.lyy.keepassa.R
import com.lyy.keepassa.base.BaseBottomSheetDialogFragment
import com.lyy.keepassa.databinding.DialogPathTypeBinding
import com.lyy.keepassa.entity.SimpleItemEntity
import com.lyy.keepassa.view.StorageType.AFS
import com.lyy.keepassa.view.StorageType.DROPBOX
import com.lyy.keepassa.view.StorageType.ONE_DRIVE
import com.lyy.keepassa.view.StorageType.WEBDAV
import com.lyy.keepassa.view.SimpleAdapter
/**
* 数据库路径选择
* @param dbName 如果是webdav,该字段为文件的http url
*/
class PathTypeDialog(
private val dbName: String
) : BaseBottomSheetDialogFragment(), View.OnClickListener {
private lateinit var module: CreateDbModule
override fun setLayoutId(): Int {
return R.layout.dialog_path_type
}
override fun init(savedInstanceState: Bundle?) {
super.init(savedInstanceState)
module = ViewModelProvider(requireActivity())
.get(CreateDbModule::class.java)
val data: ArrayList = ArrayList()
val adapter = SimpleAdapter(requireContext(), data)
binding.list.layoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false)
binding.list.adapter = adapter
binding.content.post {
module.getDbOpenTypeData(requireContext())
.observe(this, Observer { items ->
data.addAll(items)
adapter.notifyDataSetChanged()
})
}
mRootView.setOnClickListener(this)
binding.close.setOnClickListener(this)
RvItemClickSupport.addTo(binding.list)
.setOnItemClickListener { _, position, _ ->
val item = data[position]
when (item.icon) {
R.drawable.ic_android -> {//使用系统文件管理器
module.storageType = AFS
}
R.drawable.ic_dropbox -> { // dropbox
module.storageType = DROPBOX
}
R.drawable.ic_http -> { // webDav
module.storageType = WEBDAV
}
R.drawable.ic_onedrive -> { // onedrive
module.storageType = ONE_DRIVE
}
}
dismiss()
}
}
override fun onClick(v: View?) {
when (v!!.id) {
mRootView.id, R.id.close -> dismiss()
}
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/view/create/auth/AFSAuthFlow.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.view.create.auth
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import com.keepassdroid.utils.UriUtil
import com.lyy.keepassa.event.DbPathEvent
import com.lyy.keepassa.util.KeepassAUtil
import com.lyy.keepassa.util.takePermission
import com.lyy.keepassa.view.StorageType.AFS
import com.lyy.keepassa.view.create.CreateDbFirstFragment
/**
* @Author laoyuyu
* @Description
* @Date 2021/2/25
**/
class AFSAuthFlow : IAuthFlow {
private val PATH_REQUEST_CODE = 0xA1
private var context: Context? = null
private lateinit var authCallback: IAuthCallback
private lateinit var nextCallback: OnNextFinishCallback
private var dbUri: Uri? = null
override fun initContent(
context: Context,
callback: IAuthCallback
) {
this.context = context
this.authCallback = callback
}
override fun startFlow() {
authCallback.callback(true)
}
override fun onResume() {
}
override fun doNext(
fragment: CreateDbFirstFragment,
dbName: String,
callback: OnNextFinishCallback
) {
nextCallback = callback
if (dbUri == null) {
KeepassAUtil.instance.createFile(
fragment, "*/*", "$dbName.kdbx", PATH_REQUEST_CODE
)
return
}
nextCallback.onFinish(
DbPathEvent(
dbName = UriUtil.getFileNameFromUri(context, dbUri),
fileUri = dbUri,
storageType = AFS
)
)
}
override fun onDestroy() {
}
override fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: Intent?
) {
dbUri = data?.data
if (resultCode == Activity.RESULT_OK
&& requestCode == PATH_REQUEST_CODE
&& data != null
&& data.data != null
&& context != null
) {
// 申请长期的uri权限
// 防止一个不可思议的空指针,data.data 有可能还是为空
data.data?.apply {
takePermission()
nextCallback.onFinish(
DbPathEvent(
dbName = UriUtil.getFileNameFromUri(context, this),
fileUri = this,
storageType = AFS
)
)
}
}
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/view/create/auth/DropboxAuthFlow.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.view.create.auth
import android.content.Context
import android.content.Intent
import android.text.Html
import android.text.TextUtils
import android.widget.Button
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.OnLifecycleEvent
import com.arialyy.frame.router.Routerfit
import com.dropbox.core.android.Auth
import com.lyy.keepassa.R
import com.lyy.keepassa.event.DbPathEvent
import com.lyy.keepassa.router.DialogRouter
import com.lyy.keepassa.util.HitUtil
import com.lyy.keepassa.util.cloud.DbSynUtil
import com.lyy.keepassa.util.cloud.DropboxUtil
import com.lyy.keepassa.view.StorageType.DROPBOX
import com.lyy.keepassa.view.create.CreateDbFirstFragment
import com.lyy.keepassa.view.dialog.OnMsgBtClickListener
import timber.log.Timber
/**
* @Author laoyuyu
* @Description default save db to root path (eg: "/")
* @Date 2021/2/25
**/
class DropboxAuthFlow : IAuthFlow {
private val TAG = javaClass.simpleName
private lateinit var context: Context
private var isNeedAuth = false
private lateinit var callback: IAuthCallback
override fun initContent(
context: Context,
callback: IAuthCallback
) {
this.context = context
this.callback = callback
}
override fun onResume() {
Timber.d("onResume")
if (!isNeedAuth || DropboxUtil.isAuthorized()) {
return
}
val token = Auth.getOAuth2Token()
if (!TextUtils.isEmpty(token)) {
DropboxUtil.saveToken(token)
HitUtil.toaskShort("dropbox ${context.getString(R.string.auth)}${context.getString(R.string.success)}")
callback.callback(true)
return
}
HitUtil.toaskShort("dropbox ${context.getString(R.string.auth)}${context.getString(R.string.fail)}")
callback.callback(false)
}
override fun doNext(
fragment: CreateDbFirstFragment,
dbName: String,
callback: OnNextFinishCallback
) {
if (!DropboxUtil.isAuthorized()) {
authDropbox()
return
}
val name = "$dbName.kdbx"
callback.onFinish(
DbPathEvent(
dbName = name,
fileUri = DbSynUtil.getCloudDbTempPath(DROPBOX.name, name),
storageType = DROPBOX,
cloudDiskPath = "/$name"
)
)
}
override fun startFlow() {
isNeedAuth = true
if (!DropboxUtil.isAuthorized()) {
isNeedAuth = true
authDropbox()
return
}
}
/**
* 选择dropbox路径
* 只有dropbox为授权才显示该对话框
*/
private fun authDropbox() {
Routerfit.create(DialogRouter::class.java)
.showMsgDialog(
msgContent = Html.fromHtml(context.getString(R.string.dropbox_msg)),
showCancelBt = false,
btnClickListener = object : OnMsgBtClickListener {
override fun onCover(v: Button) {
}
override fun onEnter(v: Button) {
Auth.startOAuth2Authentication(context, DropboxUtil.APP_KEY)
}
override fun onCancel(v: Button) {
}
}
)
}
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
override fun onDestroy() {
Timber.d("onDestroy")
}
override fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: Intent?
) {
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/view/create/auth/IAuthFlow.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.view.create.auth
import android.content.Context
import android.content.Intent
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import com.lyy.keepassa.event.DbPathEvent
import com.lyy.keepassa.view.StorageType
import com.lyy.keepassa.view.StorageType.AFS
import com.lyy.keepassa.view.StorageType.DROPBOX
import com.lyy.keepassa.view.StorageType.ONE_DRIVE
import com.lyy.keepassa.view.StorageType.WEBDAV
import com.lyy.keepassa.view.create.CreateDbFirstFragment
/**
* @Author laoyuyu
* @Description cloud file create
* @Date 2021/2/25
**/
interface IAuthFlow : LifecycleObserver {
fun initContent(
context: Context,
callback: IAuthCallback
)
fun startFlow()
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun onResume()
/**
* 点下一步的处理事件
*/
fun doNext(
fragment: CreateDbFirstFragment,
dbName: String,
callback: OnNextFinishCallback
)
fun onDestroy()
fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: Intent?
)
}
/**
* 验证回调
*/
interface IAuthCallback {
fun callback(success: Boolean)
}
/**
* 完成选择云服务的回调
*/
interface OnNextFinishCallback {
fun onFinish(event: DbPathEvent)
}
object AuthFlowFactory {
fun getAuthFlow(type: StorageType): IAuthFlow? = when (type) {
DROPBOX -> DropboxAuthFlow()
AFS -> AFSAuthFlow()
WEBDAV -> WebDavAuthFlow()
ONE_DRIVE -> OneDriveAuthFlow()
else -> null
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/view/create/auth/OneDriveAuthFlow.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.view.create.auth
import android.content.Context
import android.content.Intent
import com.arialyy.frame.router.Routerfit
import com.lyy.keepassa.event.DbPathEvent
import com.lyy.keepassa.router.DialogRouter
import com.lyy.keepassa.util.cloud.DbSynUtil
import com.lyy.keepassa.util.cloud.OneDriveUtil
import com.lyy.keepassa.view.StorageType.ONE_DRIVE
import com.lyy.keepassa.view.create.CreateDbFirstFragment
import timber.log.Timber
/**
* @Author laoyuyu
* @Description
* @Date 2021/4/26
**/
class OneDriveAuthFlow : IAuthFlow {
private lateinit var context: Context
private lateinit var callback: IAuthCallback
private var loginCallback: OneDriveUtil.OnLoginCallback? = null
private var isAuthid = false
private val loadingDialog by lazy {
Routerfit.create(DialogRouter::class.java).getLoadingDialog()
}
override fun initContent(
context: Context,
callback: IAuthCallback
) {
this.context = context
this.callback = callback
}
override fun startFlow() {
auth()
}
override fun onResume() {
}
override fun doNext(
fragment: CreateDbFirstFragment,
dbName: String,
callback: OnNextFinishCallback
) {
if (!isAuthid) {
auth()
return
}
val name = "$dbName.kdbx"
callback.onFinish(
DbPathEvent(
dbName = name,
fileUri = DbSynUtil.getCloudDbTempPath(ONE_DRIVE.name, name),
storageType = ONE_DRIVE,
cloudDiskPath = "/$name"
)
)
}
private fun auth() {
if (isAuthid) {
Timber.d("已经完成授权")
return
}
loadingDialog.show()
OneDriveUtil.initOneDrive {
if (it) {
OneDriveUtil.loadAccount()
return@initOneDrive
}
this.callback.callback(false)
loadingDialog.dismiss()
}
OneDriveUtil.loginCallback = object : OneDriveUtil.OnLoginCallback {
override fun callback(success: Boolean) {
isAuthid = success
this@OneDriveAuthFlow.callback.callback(success)
loadingDialog.dismiss()
}
}
}
override fun onDestroy() {
loginCallback = null
}
override fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: Intent?
) {
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/view/create/auth/WebDavAuthFlow.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.view.create.auth
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.text.Spannable
import android.text.SpannableString
import android.text.style.ForegroundColorSpan
import android.widget.Button
import com.arialyy.frame.router.Routerfit
import com.arialyy.frame.util.ResUtil
import com.lyy.keepassa.R
import com.lyy.keepassa.base.BaseApp
import com.lyy.keepassa.entity.CloudServiceInfo
import com.lyy.keepassa.event.DbPathEvent
import com.lyy.keepassa.router.DialogRouter
import com.lyy.keepassa.util.QuickUnLockUtil
import com.lyy.keepassa.util.cloud.DbSynUtil
import com.lyy.keepassa.util.cloud.WebDavUtil
import com.lyy.keepassa.view.StorageType.WEBDAV
import com.lyy.keepassa.view.create.CreateDbFirstFragment
import com.lyy.keepassa.view.dialog.OnMsgBtClickListener
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
/**
* @Author laoyuyu
* @Description webdav auth flow
* @Date 2021/2/25
**/
class WebDavAuthFlow : IAuthFlow {
private var context: Context? = null
private lateinit var callback: IAuthCallback
private var webDavUri: String? = null
private var nextCallback: OnNextFinishCallback? = null
private var dbName: String? = null
private val scope = MainScope()
private var isLogin = false
private val loginDialog by lazy {
Routerfit.create(DialogRouter::class.java).getWebDavLoginDialog()
}
private val fileSelectDialog by lazy {
Routerfit.create(DialogRouter::class.java).getCloudFileListDialog(WEBDAV, true)
}
override fun initContent(
context: Context,
callback: IAuthCallback
) {
this.context = context
this.callback = callback
}
override fun startFlow() {
// changeWebDav(false)
scope.launch {
loginDialog.webDavLoginFlow.collectLatest {
isLogin = it.loginSuccess
if (it.loginSuccess) {
fileSelectDialog.show()
}
}
}
loginDialog.show()
scope.launch {
fileSelectDialog.cloudFileSelectFlow.collectLatest {
if (it.storageType == WEBDAV) {
webDavUri = it.fileFullPath
callback.callback(true)
}
}
}
}
override fun onResume() {
}
override fun doNext(
fragment: CreateDbFirstFragment,
dbName: String,
callback: OnNextFinishCallback
) {
this.dbName = "${dbName}.kdbx"
if (!isLogin) {
loginDialog.show()
return
}
if (webDavUri == null) {
fileSelectDialog.show()
return
}
nextCallback = callback
scope.launch {
val fileExist = withContext(Dispatchers.IO) {
return@withContext WebDavUtil.fileExists("${webDavUri!!}${this@WebDavAuthFlow.dbName}")
}
if (fileExist) {
val content =
ResUtil.getString(
R.string.hint_cloud_file_already_exist,
this@WebDavAuthFlow.dbName!!
)
val color = ResUtil.getColor(R.color.red)
val ss = SpannableString(content)
ss.setSpan(
ForegroundColorSpan(color), 0, this@WebDavAuthFlow.dbName!!.length,
Spannable.SPAN_INCLUSIVE_EXCLUSIVE
)
Routerfit.create(DialogRouter::class.java).showMsgDialog(
msgTitle = ResUtil.getString(R.string.hint),
msgContent = ss,
btnClickListener = object : OnMsgBtClickListener{
override fun onCover(v: Button) {
}
override fun onEnter(v: Button) {
sendFinishEvent()
}
override fun onCancel(v: Button) {
}
}
)
return@launch
}
sendFinishEvent()
}
}
private fun sendFinishEvent() {
if (dbName == null) {
return
}
scope.launch {
saveWebDavServiceInfo("${webDavUri!!}${dbName}", WebDavUtil.userName, WebDavUtil.password)
nextCallback?.onFinish(
DbPathEvent(
dbName = dbName!!,
storageType = WEBDAV,
fileUri = DbSynUtil.getCloudDbTempPath(WEBDAV.name, dbName!!),
cloudDiskPath = "${webDavUri}${dbName}"
)
)
}
}
private suspend fun saveWebDavServiceInfo(
uri: String,
userName: String,
pass: String
) {
withContext(Dispatchers.IO) {
Timber.d("开始保存webDav登陆记录,uri = $uri")
val dao = BaseApp.appDatabase.cloudServiceInfoDao()
var data = dao.queryServiceInfo(uri)
if (data == null) {
data = CloudServiceInfo(
userName = QuickUnLockUtil.encryptStr(userName),
password = QuickUnLockUtil.encryptStr(pass),
cloudPath = uri
)
dao.saveServiceInfo(data)
} else {
data.userName = QuickUnLockUtil.encryptStr(userName)
data.password = QuickUnLockUtil.encryptStr(pass)
dao.updateServiceInfo(data)
}
}
}
override fun onDestroy() {
context = null
}
override fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: Intent?
) {
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/view/create/entry/CardListHelper.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.view.create.entry
import android.animation.ObjectAnimator
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.animation.doOnEnd
import androidx.recyclerview.widget.LinearLayoutManager
import com.lyy.keepassa.databinding.LayoutEntryCreateStrCardBinding
import com.lyy.keepassa.util.doClick
/**
* @Author laoyuyu
* @Description
* @Date 3:27 PM 2023/10/25
**/
internal class CardListHelper(val binding: LayoutEntryCreateStrCardBinding) {
companion object {
private val ARROW_ANIM_DURATION = 100L
}
private var isExpand = false
fun handleList() {
binding.rvList.apply {
setHasFixedSize(true)
layoutManager = object : LinearLayoutManager(context) {
override fun canScrollVertically(): Boolean {
return false
}
}
isNestedScrollingEnabled = false
}
handleArrow()
}
private fun handleArrow() {
binding.vClick.doClick {
if (!isExpand) {
expand()
return@doClick
}
hind()
}
}
private fun hind() {
val anim = ObjectAnimator.ofFloat(binding.ivArrow, "rotation", 180f, 0f)
anim.duration = ARROW_ANIM_DURATION
anim.doOnEnd {
isExpand = false
binding.rvList.visibility = ConstraintLayout.GONE
}
anim.start()
}
/**
* 展开列表
*/
private fun expand() {
val anim = ObjectAnimator.ofFloat(binding.ivArrow, "rotation", 0f, 180f)
anim.duration = ARROW_ANIM_DURATION
anim.doOnEnd {
isExpand = true
binding.rvList.visibility = ConstraintLayout.VISIBLE
}
anim.start()
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/view/create/entry/CreateEntryActivity.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.view.create.entry
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.IntentSender
import android.os.Bundle
import android.text.InputType
import android.view.View
import android.widget.ArrayAdapter
import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.app.ActivityOptionsCompat
import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import com.alibaba.android.arouter.facade.annotation.Autowired
import com.alibaba.android.arouter.facade.annotation.Route
import com.alibaba.android.arouter.launcher.ARouter
import com.arialyy.frame.router.Routerfit
import com.arialyy.frame.util.ResUtil
import com.keepassdroid.database.PwGroupId
import com.keepassdroid.database.PwIconCustom
import com.keepassdroid.database.PwIconStandard
import com.lyy.keepassa.R
import com.lyy.keepassa.base.BaseActivity
import com.lyy.keepassa.databinding.ActivityEntryEditNewBinding
import com.lyy.keepassa.entity.AutoFillParam
import com.lyy.keepassa.entity.CommonState.DELETE
import com.lyy.keepassa.entity.GoogleOtpBean
import com.lyy.keepassa.entity.KeepassBean
import com.lyy.keepassa.entity.KeepassXcBean
import com.lyy.keepassa.entity.SimpleItemEntity
import com.lyy.keepassa.entity.TagBean
import com.lyy.keepassa.entity.TrayTotpBean
import com.lyy.keepassa.entity.toOtpStringMap
import com.lyy.keepassa.router.DialogRouter
import com.lyy.keepassa.util.IconUtil
import com.lyy.keepassa.util.KdbUtil
import com.lyy.keepassa.util.KeepassAUtil
import com.lyy.keepassa.util.doClick
import com.lyy.keepassa.util.hasTOTP
import com.lyy.keepassa.util.loadImg
import com.lyy.keepassa.util.takePermission
import com.lyy.keepassa.util.totp.OtpEnum
import com.lyy.keepassa.view.create.CreateCustomStrDialog
import com.lyy.keepassa.view.create.GeneratePassActivity
import com.lyy.keepassa.view.create.entry.CreateEnum.CREATE
import com.lyy.keepassa.view.create.entry.CreateEnum.MODIFY
import com.lyy.keepassa.view.dialog.AddMoreDialog
import com.lyy.keepassa.view.dialog.ChooseTagDialog
import com.lyy.keepassa.view.dialog.CreateTagDialog
import com.lyy.keepassa.view.dialog.TimeChangeDialog
import com.lyy.keepassa.view.dialog.otp.CreateOtpModule
import com.lyy.keepassa.view.dir.ChooseGroupActivity
import com.lyy.keepassa.view.icon.IconBottomSheetDialog
import com.lyy.keepassa.view.icon.IconItemCallback
import com.lyy.keepassa.view.launcher.LauncherActivity
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import timber.log.Timber
import kotlin.math.abs
/**
* @Author laoyuyu
* @Description
* @Date 7:24 PM 2023/10/13
**/
@Route(path = "/entry/create")
class CreateEntryActivity : BaseActivity() {
companion object {
const val KEY_ENTRY = "KEY_ENTRY"
/**
* 类型,1:新建条目,2:利用模版新建条目,3:编辑条目
*/
const val KEY_TYPE = "KEY_IS_TYPE"
const val IS_SHORTCUTS = "isShortcuts"
const val PARENT_GROUP_ID = "PARENT_GROUP_ID"
/**
* 数据库未解锁,保存数据时打开数据库,并保存
*/
internal fun authAndSaveDb(
context: Context,
autoFillParam: AutoFillParam,
): IntentSender {
val intent = Intent(context, CreateEntryActivity::class.java).also {
it.putExtra(LauncherActivity.KEY_AUTO_FILL_PARAM, autoFillParam)
it.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
return PendingIntent.getActivity(
context,
1,
intent,
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
.intentSender
}
}
@Autowired(name = IS_SHORTCUTS)
@JvmField
var isShortcuts: Boolean = false
@Autowired(name = KEY_TYPE)
@JvmField
var createEnum: CreateEnum = CREATE
internal lateinit var module: CreateEntryModule
private lateinit var createHandler: ICreateHandler
private var isShowPass = false
private var addMoreDialog: AddMoreDialog? = null
private lateinit var addMoreData: ArrayList
private val getFileLauncher =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
uri?.let {
it.takePermission()
module.addAttrFile(this, it)
}
}
/**
* 密码创建器
*/
private val passGenerateLauncher =
registerForActivityResult(object : ActivityResultContract() {
override fun createIntent(context: Context, input: String?): Intent {
return Intent(context, GeneratePassActivity::class.java)
}
override fun parseResult(resultCode: Int, intent: Intent?): String {
return intent?.getStringExtra(GeneratePassActivity.DATA_PASS_WORD) ?: ""
}
}) {
if (it.isEmpty()) {
return@registerForActivityResult
}
binding.edPassword.setText(it)
binding.tvConfirm.setText(it)
}
/**
* 选择群组
*/
private val chooseGroupLauncher =
registerForActivityResult(object : ActivityResultContract() {
override fun createIntent(context: Context, input: String?): Intent {
return Intent(context, ChooseGroupActivity::class.java).apply {
putExtra(ChooseGroupActivity.KEY_TYPE, ChooseGroupActivity.DATA_SELECT_GROUP)
}
}
override fun parseResult(resultCode: Int, intent: Intent?): PwGroupId? {
return intent?.getSerializableExtra(ChooseGroupActivity.DATA_PARENT) as PwGroupId?
}
}) {
if (it == null) {
Timber.d("pwGroupId is null")
return@registerForActivityResult
}
module.updateEntryGroupIdAndSave(this, it)
}
fun launchGroupChoose() {
chooseGroupLauncher.launch(null, ActivityOptionsCompat.makeSceneTransitionAnimation(this))
}
override fun initData(savedInstanceState: Bundle?) {
super.initData(savedInstanceState)
ARouter.getInstance().inject(this)
module = ViewModelProvider(this)[CreateEntryModule::class.java]
createHandler = if (createEnum == MODIFY) {
ModifyEntryHandler(this)
} else {
CreateEntryHandler(this)
}
createHandler.bindData()
handleTopBarLayout()
handlePassLayout()
handleIconClick()
handlerAddMore()
handlerUserLayout()
handleTimeLayout()
handleTagLayout()
handleTotpLayout()
handleStr()
handleAttrFile()
}
private fun handleAttrFile() {
lifecycleScope.launch {
CreateEntryModule.attrFlow.collectLatest { bean ->
if (bean.state != DELETE) {
module.fileCacheMap[bean.key] = bean.file
binding.cardFile.isVisible = true
binding.cardFile.bindData(module.fileCacheMap)
return@collectLatest
}
module.fileCacheMap.remove(bean.key)
binding.cardFile.removeItem(bean.key)
}
}
}
private fun handleStr() {
lifecycleScope.launch {
CreateCustomStrDialog.CustomStrFlow.collectLatest { bean ->
if (bean == null) {
Timber.d("attr is null")
return@collectLatest
}
checkAddMoreBtn()
if (bean.state != DELETE) {
module.strCacheMap[bean.key] = bean.str
binding.cardStr.isVisible = true
binding.cardStr.bindDate(module.strCacheMap)
return@collectLatest
}
module.strCacheMap.remove(bean.key)
if (!module.strCacheMap.hasTOTP()) {
binding.groupOtp.isVisible = false
}
binding.cardStr.removeItem(bean.key)
}
}
}
private fun handleTotpLayout() {
fun startOtp() {
binding.groupOtp.isVisible = true
KdbUtil.startAutoGetOtp(module.pwEntry, binding.pbRound, binding.edOtp)
}
lifecycleScope.launch {
CreateOtpModule.otpFlow.collectLatest {
val map = when (it.first) {
OtpEnum.TRAY_TOTP -> (it.second as? TrayTotpBean)?.toOtpStringMap()
OtpEnum.KEEPASSXC -> (it.second as? KeepassXcBean)?.toOtpStringMap()
OtpEnum.GOOGLE_OTP -> (it.second as? GoogleOtpBean)?.toOtpStringMap()
OtpEnum.KEEPASS -> (it.second as? KeepassBean)?.toOtpStringMap()
}
map?.let { strs ->
strs.forEach { kv ->
module.strCacheMap[kv.key] = kv.value
}
startOtp()
binding.cardStr.bindDate(module.strCacheMap)
}
checkAddMoreBtn()
}
}
if (module.strCacheMap.hasTOTP()) {
startOtp()
}
binding.edOtp.doClick {
if (module.strCacheMap.hasTOTP()) {
Routerfit.create(DialogRouter::class.java).showModifyOtpDialog(module.pwEntry.uuid)
return@doClick
}
Routerfit.create(DialogRouter::class.java)
.showCreateOtpDialog(module.pwEntry.title, module.pwEntry.username)
}
}
private fun handlerUserLayout() {
binding.edUser.threshold = 1 // 设置输入几个字符后开始出现提示 默认是2
binding.edUser.setOnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
binding.edUser.showDropDown()
}
}
lifecycleScope.launch {
CreateEntryModule.userNameFlow.collectLatest {
if (it.isNullOrEmpty()) {
return@collectLatest
}
binding.edUser.setAdapter(
ArrayAdapter(
this@CreateEntryActivity,
R.layout.android_simple_dropdown_item_1line,
it
)
)
}
}
lifecycleScope.launch {
module.getUserNameCache()
}
}
private fun handleTimeLayout() {
binding.edLoseTime.doClick {
Routerfit.create(DialogRouter::class.java).showTimeChangeDialog()
}
lifecycleScope.launch {
TimeChangeDialog.timeFlow.collectLatest { event ->
if (event == null) {
return@collectLatest
}
val time = "${event.year}/${event.month}/${event.dayOfMonth} ${event.hour}:${event.minute}"
binding.edLoseTime.setText(time)
binding.tlLoseTime.visibility = View.VISIBLE
checkAddMoreBtn()
}
}
}
private fun handleTagLayout() {
binding.edTag.doClick {
Routerfit.create(DialogRouter::class.java).showChooseTagDialog(module.pwEntry)
}
lifecycleScope.launch {
ChooseTagDialog.chooseTagFlow.collectLatest { tagBeanList ->
val tagStrList = arrayListOf()
tagBeanList.forEach {
tagStrList.add(it.tag)
}
val tags = tagStrList.joinToString(separator = ",")
binding.edTag.setText(tags)
binding.tlTag.visibility = View.VISIBLE
checkAddMoreBtn()
}
}
lifecycleScope.launch {
CreateTagDialog.createTagFlow.collectLatest {
Routerfit.create(DialogRouter::class.java)
.showChooseTagDialog(module.pwEntry, if (it.isNullOrEmpty()) null else TagBean(it, true))
}
}
}
private fun handlerAddMore() {
binding.btnAddMore.doClick {
if (addMoreDialog == null) {
addMoreData = module.getMoreItem(this)
addMoreDialog = AddMoreDialog(addMoreData)
addMoreDialog!!.setOnItemClickListener(object : AddMoreDialog.OnItemClickListener {
override fun onItemClick(
position: Int,
item: SimpleItemEntity,
view: View
) {
when (item.icon) {
R.drawable.ic_attr_str -> { // 自定义字段
Routerfit.create(DialogRouter::class.java).showCreateCustomDialog()
}
R.drawable.ic_attr_file -> { // file
changeFile()
}
R.drawable.ic_token_grey -> { // totp
Routerfit.create(DialogRouter::class.java)
.showCreateOtpDialog(module.pwEntry.title, module.pwEntry.username)
}
R.drawable.ic_notice -> { // notice
binding.tlNote.visibility = View.VISIBLE
binding.tlNote.requestFocus()
}
R.drawable.ic_net -> { //url
binding.tlUrl.visibility = View.VISIBLE
binding.tlUrl.requestFocus()
}
R.drawable.ic_tag -> {
Routerfit.create(DialogRouter::class.java).showChooseTagDialog(module.pwEntry)
}
R.drawable.ic_lose_time -> {
Routerfit.create(DialogRouter::class.java).showTimeChangeDialog()
}
}
checkAddMoreBtn()
addMoreDialog!!.dismiss()
}
})
}
if (binding.tlLoseTime.isVisible) {
addMoreData.remove(addMoreData.find { it.icon == R.drawable.ic_lose_time })
}
if (binding.cardStr.isVisible) {
addMoreData.remove(addMoreData.find { it.icon == R.drawable.ic_attr_str })
}
if (binding.cardFile.isVisible) {
addMoreData.remove(addMoreData.find { it.icon == R.drawable.ic_attr_file })
}
if (module.pwEntry.hasTOTP()) {
addMoreData.remove(addMoreData.find { it.icon == R.drawable.ic_token_grey })
}
if (binding.tlTag.isVisible) {
addMoreData.remove(addMoreData.find { it.icon == R.drawable.ic_tag })
}
if (binding.tlNote.isVisible) {
addMoreData.remove(addMoreData.find { it.icon == R.drawable.ic_notice })
}
if (binding.tlUrl.isVisible) {
addMoreData.remove(addMoreData.find { it.icon == R.drawable.ic_net })
}
addMoreDialog!!.notifyData()
addMoreDialog!!.show(supportFragmentManager, "add_more_dialog")
}
checkAddMoreBtn()
}
private fun checkAddMoreBtn() {
if (binding.tlLoseTime.isVisible
&& binding.cardStr.isVisible
&& binding.cardFile.isVisible
&& module.pwEntry.hasTOTP()
&& binding.tlTag.isVisible
&& binding.tlNote.isVisible
&& binding.tlUrl.isVisible
) {
binding.btnAddMore.visibility = View.GONE
} else {
binding.btnAddMore.visibility = View.VISIBLE
}
}
fun changeFile() {
getFileLauncher.launch(arrayOf("*/*"))
}
/**
* 标题栏
*/
private fun handleTopBarLayout() {
binding.topAppBar.title = createHandler.getTitle()
toolbar = binding.topAppBar
toolbar.setNavigationOnClickListener {
finishAfterTransition()
}
toolbar.inflateMenu(R.menu.menu_entry_edit)
toolbar.setOnMenuItemClickListener { item ->
if (KeepassAUtil.instance.isFastClick()) {
return@setOnMenuItemClickListener true
}
when (item.itemId) {
R.id.save -> {
createHandler.saveDb(module.pwEntry)
}
R.id.cancel -> {
finishAfterTransition()
}
}
true
}
binding.appBarLayout.addOnOffsetChangedListener { _, verticalOffset ->
if (verticalOffset == 0) {
binding.topAppBar.title = ""
return@addOnOffsetChangedListener
}
if (abs(verticalOffset) >= binding.appBarLayout.totalScrollRange) {
binding.topAppBar.title = createHandler.getTitle()
return@addOnOffsetChangedListener
}
}
}
private fun handleIconClick() {
binding.ivIcon.doClick {
val iconDialog = IconBottomSheetDialog()
iconDialog.setCallback(object : IconItemCallback {
override fun onDefaultIcon(defIcon: PwIconStandard) {
module.icon = defIcon
binding.ivIcon.loadImg(ResUtil.getDrawable(IconUtil.getIconById(module.icon.iconId)))
module.customIcon = PwIconCustom.ZERO
}
override fun onCustomIcon(customIcon: PwIconCustom) {
module.customIcon = customIcon
binding.ivIcon.loadImg(
IconUtil.convertCustomIcon2Drawable(
this@CreateEntryActivity,
module.customIcon!!
)
)
}
})
iconDialog.show(supportFragmentManager, IconBottomSheetDialog::class.java.simpleName)
}
}
/**
* 处理密码
*/
private fun handlePassLayout() {
binding.tlPass.endIconDrawable = ResUtil.getDrawable(R.drawable.ic_view_off)
binding.tlPass.setEndIconOnClickListener {
isShowPass = !isShowPass
if (isShowPass) {
binding.tlPass.endIconDrawable = ResUtil.getDrawable(R.drawable.ic_view)
binding.tlConfirm.visibility = View.GONE
binding.edPassword.inputType = InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
} else {
binding.tlPass.endIconDrawable =
ResUtil.getDrawable(R.drawable.ic_view_off)
binding.tlConfirm.visibility = View.VISIBLE
binding.edPassword.inputType =
InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT
}
// 将光标移动到最后
binding.edPassword.setSelection(binding.edPassword.text?.length ?: 0)
binding.edPassword.requestFocus()
}
binding.ivGeneratePw.setOnClickListener {
passGenerateLauncher.launch(null, ActivityOptionsCompat.makeSceneTransitionAnimation(this))
}
}
override fun setLayoutId(): Int {
return R.layout.activity_entry_edit_new
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/view/create/entry/CreateEntryHandler.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.view.create.entry
import android.view.View
import androidx.core.view.isVisible
import com.arialyy.frame.util.ResUtil
import com.keepassdroid.database.PwEntryV4
import com.keepassdroid.database.PwGroupId
import com.keepassdroid.database.PwGroupV4
import com.lyy.keepassa.R
import com.lyy.keepassa.base.BaseApp
/**
* @Author laoyuyu
* @Description
* @Date 7:34 PM 2023/10/13
**/
internal class CreateEntryHandler(val context: CreateEntryActivity) : ICreateHandler {
override fun bindData() {
val groupId =
context.intent.getSerializableExtra(CreateEntryActivity.PARENT_GROUP_ID) as? PwGroupId
val binding = context.binding
val group =
(if (groupId != null) BaseApp.KDB.pm.groups[groupId] else BaseApp.KDB.pm.rootGroup) as PwGroupV4
val entry = PwEntryV4(group, true, true)
context.module.pwEntry = entry
context.module.initCache()
binding.cardStr.visibility = View.GONE
binding.cardFile.visibility = View.GONE
binding.tlLoseTime.visibility = View.GONE
binding.tlUrl.visibility = View.GONE
binding.tlNote.visibility = View.GONE
binding.tlTag.visibility = View.GONE
binding.groupOtp.isVisible = false
}
override fun getTitle(): String {
return ResUtil.getString(R.string.create_entry)
}
override fun saveDb(pwEntryV4: PwEntryV4) {
checkAttr(context, pwEntryV4)
context.launchGroupChoose()
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/view/create/entry/CreateEntryModule.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.view.create.entry
import KDBAutoFillRepository
import android.content.Context
import android.graphics.Bitmap.CompressFormat.PNG
import android.net.Uri
import android.text.TextUtils
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewModelScope
import com.arialyy.frame.util.ResUtil
import com.keepassdroid.database.PwDatabaseV4
import com.keepassdroid.database.PwEntry
import com.keepassdroid.database.PwEntryV4
import com.keepassdroid.database.PwGroupId
import com.keepassdroid.database.PwGroupV4
import com.keepassdroid.database.PwIconCustom
import com.keepassdroid.database.PwIconStandard
import com.keepassdroid.database.security.ProtectedBinary
import com.keepassdroid.database.security.ProtectedString
import com.keepassdroid.utils.UriUtil
import com.lyy.keepassa.R
import com.lyy.keepassa.base.BaseApp
import com.lyy.keepassa.base.BaseModule
import com.lyy.keepassa.entity.AutoFillParam
import com.lyy.keepassa.entity.CommonState.CREATE
import com.lyy.keepassa.entity.SimpleItemEntity
import com.lyy.keepassa.entity.TagBean
import com.lyy.keepassa.event.AttrFileEvent
import com.lyy.keepassa.util.HitUtil
import com.lyy.keepassa.util.IconUtil
import com.lyy.keepassa.util.KdbUtil
import com.lyy.keepassa.util.KpaUtil
import com.lyy.keepassa.util.getFileInfo
import com.lyy.keepassa.util.getRealUserName
import com.lyy.keepassa.util.hasNote
import com.lyy.keepassa.util.hasTOTP
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.ByteArrayOutputStream
import java.util.UUID
/**
* 创建条目、群组的module
*/
class CreateEntryModule : BaseModule() {
companion object {
val attrFlow = MutableSharedFlow(0)
val userNameFlow = MutableStateFlow?>(null)
private val userNameCache = arrayListOf()
}
/**
* 已经选中的标签
*/
var selectedTagBeanCache = mutableListOf()
var customIcon: PwIconCustom? = null
var icon = PwIconStandard(0)
var autoFillParam: AutoFillParam? = null
var strCacheMap = hashMapOf()
var fileCacheMap = hashMapOf()
lateinit var pwEntry: PwEntryV4
fun updateEntryGroupIdAndSave(context: CreateEntryActivity, groupId: PwGroupId) {
viewModelScope.launch {
KpaUtil.kdbHandlerService.createEntry(pwEntry)
KpaUtil.kdbHandlerService.saveOnly(true) {
context.finishAfterTransition()
}
}
}
fun initCache() {
pwEntry.strings.forEach {
strCacheMap[it.key] = it.value
}
pwEntry.binaries.forEach {
fileCacheMap[it.key] = it.value
}
customIcon = pwEntry.customIcon ?: PwIconCustom.ZERO
icon = pwEntry.icon
}
fun cacheTag(tagList: List) {
selectedTagBeanCache.clear()
selectedTagBeanCache.addAll(tagList.filter { it.isSet }.map {
it.tag
})
}
/**
* 添加附件
*/
fun addAttrFile(context: CreateEntryActivity, uri: Uri?) {
val rootView = context.rootView
if (uri == null) {
Timber.e("附件uri为空")
HitUtil.snackShort(
rootView,
"${ResUtil.getString(R.string.add_attr_file)}${ResUtil.getString(R.string.fail)}"
)
return
}
val fileInfo = uri.getFileInfo(context)
if (TextUtils.isEmpty(fileInfo.first) || fileInfo.second == null) {
Timber.e("获取文件名失败")
HitUtil.snackShort(
rootView,
"${ResUtil.getString(R.string.add_attr_file)}${ResUtil.getString(R.string.fail)}"
)
return
}
val fileName = fileInfo.first!!
val fileSize = fileInfo.second!!
if (fileSize >= 1024 * 1024 * 10) {
HitUtil.snackShort(rootView, ResUtil.getString(R.string.error_attr_file_too_large))
return
}
val pbf = ProtectedBinary(
false, UriUtil.getUriInputStream(context, uri)
.readBytes()
)
(BaseApp.KDB.pm as PwDatabaseV4).binPool.poolAdd(pbf)
context.lifecycleScope.launch {
attrFlow.emit(AttrFileEvent(CREATE, fileName, pbf))
}
}
/**
* Traverse database and get all userName
*/
suspend fun getUserNameCache() {
if (userNameCache.isNotEmpty()) {
userNameFlow.emit(userNameCache)
return
}
val temp = hashSetOf()
withContext(Dispatchers.IO) {
for (map in BaseApp.KDB.pm.entries) {
if (map.value.username.isNullOrEmpty()) {
continue
}
temp.add(map.value.getRealUserName())
}
}
userNameCache.addAll(temp)
userNameFlow.emit(userNameCache)
}
/**
* 自动填充进行保存数据时,搜索条目信息,如果条目不存在,新建条目
*/
fun getEntryFromAutoFillSave(
context: Context,
apkPkgName: String,
userName: String?,
pass: String?
): PwEntryV4 {
val listStorage = ArrayList()
KdbUtil.searchEntriesByPackageName(apkPkgName, listStorage)
val entry: PwEntryV4
if (listStorage.isEmpty()) {
entry = PwEntryV4(BaseApp.KDB.pm.rootGroup as PwGroupV4)
val icon = IconUtil.getAppIcon(context, apkPkgName)
if (icon != null) {
val baos = ByteArrayOutputStream()
icon.compress(PNG, 100, baos)
val datas: ByteArray = baos.toByteArray()
val customIcon = PwIconCustom(UUID.randomUUID(), datas)
entry.customIcon = customIcon
(BaseApp.KDB.pm as PwDatabaseV4).putCustomIcons(customIcon)
entry.strings["KP2A_URL_1"] = ProtectedString(false, "androidapp://$apkPkgName")
}
val appName = KDBAutoFillRepository.getAppName(context, apkPkgName)
entry.setTitle(appName ?: "newEntry", BaseApp.KDB.pm)
entry.icon = PwIconStandard(0)
} else {
entry = listStorage[0] as PwEntryV4
Timber.w("已存在含有【$apkPkgName】的条目,将更新条目")
}
if (!userName.isNullOrEmpty()) {
entry.setUsername(userName, BaseApp.KDB.pm)
}
if (!pass.isNullOrEmpty()) {
entry.setPassword(pass, BaseApp.KDB.pm)
}
return entry
}
/**
* 创建群组
* @param groupName 群组名
* @param parentGroup 父群组
* @param icon 标准图标
* @param customIcon 自定义图标
*/
fun createGroup(
groupName: String,
parentGroup: PwGroupV4,
icon: PwIconStandard,
customIcon: PwIconCustom?,
callback: (PwGroupV4) -> Unit
) {
KpaUtil.kdbHandlerService.createGroup(groupName, icon, customIcon, parentGroup, callback)
}
/**
* 构建的更多选择项目
*/
fun getMoreItem(context: Context): ArrayList {
val list = ArrayList()
val titles = context.resources.getStringArray(R.array.v4_add_mor_item)
val icons = context.resources.obtainTypedArray(R.array.v4_add_more_icon)
val len = titles.size - 1
for (i in 0..len) {
val item = SimpleItemEntity()
item.title = titles[i]
item.icon = icons.getResourceId(i, 0)
if (item.icon == R.drawable.ic_token_grey && pwEntry.hasTOTP()) {
Timber.d("Already used totp")
continue
}
if (item.icon == R.drawable.ic_notice && pwEntry.hasNote()) {
Timber.d("Already used note")
continue
}
list.add(item)
}
icons.recycle()
return list
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/view/create/entry/CreateEnum.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.view.create.entry
/**
* @Author laoyuyu
* @Description
* @Date 7:28 PM 2023/10/13
**/
enum class CreateEnum {
MODIFY,
CREATE
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/view/create/entry/CreateFileCard.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.view.create.entry
import android.content.Context
import android.util.AttributeSet
import android.view.Gravity
import android.view.LayoutInflater
import androidx.appcompat.widget.PopupMenu
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
import com.arialyy.frame.util.ResUtil
import com.keepassdroid.database.security.ProtectedBinary
import com.lyy.keepassa.R
import com.lyy.keepassa.base.AbsViewBindingAdapter
import com.lyy.keepassa.databinding.LayoutEntryAttachmentBinding
import com.lyy.keepassa.databinding.LayoutEntryCreateStrCardBinding
import com.lyy.keepassa.entity.CommonState.DELETE
import com.lyy.keepassa.event.AttrFileEvent
import com.lyy.keepassa.util.KdbUtil
import com.lyy.keepassa.util.KpaUtil
import com.lyy.keepassa.util.doOnItemClickListener
import com.lyy.keepassa.util.init
import kotlinx.coroutines.launch
/**
* @Author laoyuyu
* @Description
* @Date 3:26 PM 2023/10/25
**/
class CreateFileCard(context: Context, attributeSet: AttributeSet) :
ConstraintLayout(context, attributeSet) {
companion object {
val ADD_MORE_DATA = Pair("addMore", ProtectedBinary(false, null))
}
private val binding =
LayoutEntryCreateStrCardBinding.inflate(LayoutInflater.from(context), this, true)
private val fileList = mutableListOf>()
private val fileAdapter = FileAdapter()
private val helper = CardListHelper(binding)
init {
binding.tvTitle.text = ResUtil.getString(R.string.attachment)
}
fun bindData(fileMap: HashMap) {
fileList.clear()
fileMap.entries.forEach {
fileList.add(Pair(it.key, it.value))
}
fileList.add(ADD_MORE_DATA)
binding.rvList.apply {
this.adapter = this@CreateFileCard.fileAdapter
this@CreateFileCard.fileAdapter.setData(fileList)
}
binding.rvList.doOnItemClickListener { _, position, v ->
val data = fileList[position]
if (data == ADD_MORE_DATA) {
(context as CreateEntryActivity).changeFile()
return@doOnItemClickListener
}
PopupMenu(context, v, Gravity.END).init(R.menu.entry_modify_file_summary) {
when (it.itemId) {
R.id.remove_file -> {
KpaUtil.scope.launch {
CreateEntryModule.attrFlow.emit(AttrFileEvent(DELETE, data.first, data.second))
}
}
R.id.open_file -> {
KdbUtil.openFile(data.first, data.second)
}
}
}.show()
}
helper.handleList()
}
fun removeItem(key: String) {
fileList.find { it.first == key }?.let {
val pos = fileList.indexOf(it)
if (pos >= 0) {
fileList.removeAt(pos)
fileAdapter.notifyItemRemoved(pos)
}
}
if (fileList.size == 1) {
visibility = GONE
}
}
private class FileAdapter :
AbsViewBindingAdapter, LayoutEntryAttachmentBinding>() {
override fun bindData(
binding: LayoutEntryAttachmentBinding,
item: Pair
) {
if (item == ADD_MORE_DATA) {
binding.value.isVisible = false
binding.addMore.isVisible = true
binding.addMore.text = ResUtil.getString(R.string.add_attr_file)
return
}
binding.value.isVisible = true
binding.addMore.isVisible = false
binding.value.text = item.first
}
}
}
================================================
FILE: app/src/main/java/com/lyy/keepassa/view/create/entry/CreateStrCard.kt
================================================
/*
* Copyright (C) 2020 AriaLyy(https://github.com/AriaLyy/KeepassA)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.lyy.keepassa.view.create.entry
import android.content.Context
import android.os.Build
import android.util.AttributeSet
import android.view.Gravity
import android.view.LayoutInflater
import androidx.appcompat.widget.PopupMenu
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isGone
import com.arialyy.frame.router.Routerfit
import com.keepassdroid.database.security.ProtectedString
import com.lyy.keepassa.R
import com.lyy.keepassa.base.AbsViewBindingAdapter
import com.lyy.keepassa.databinding.LayoutEntryCreateStrCardBinding
import com.lyy.keepassa.databinding.LayoutEntryStrBinding
import com.lyy.keepassa.entity.CommonState.DELETE
import com.lyy.keepassa.event.AttrStrEvent
import com.lyy.keepassa.router.DialogRouter
import com.lyy.keepassa.util.KdbUtil
import com.lyy.keepassa.util.KpaUtil
import com.lyy.keepassa.util.doOnItemClickListener
import com.lyy.keepassa.util.init
import com.lyy.keepassa.view.create.CreateCustomStrDialog
import kotlinx.coroutines.launch
/**
* @Author laoyuyu
* @Description
* @Date 3:12 PM 2023/10/12
**/
class CreateStrCard(context: Context, attributeSet: AttributeSet) :
ConstraintLayout(context, attributeSet) {
private val binding =
LayoutEntryCreateStrCardBinding.inflate(LayoutInflater.from(context), this, true)
private val strList = mutableListOf