Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -5,7 +5,6 @@ import android.content.pm.PackageManager
|
||||
import android.provider.Settings
|
||||
import com.script.javascript.RhinoScriptEngine
|
||||
import io.legado.app.BuildConfig
|
||||
import io.legado.app.R
|
||||
import splitties.init.appCtx
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
@@ -57,7 +56,9 @@ object AppConst {
|
||||
)
|
||||
|
||||
@SuppressLint("PrivateResource")
|
||||
val sysElevation = appCtx.resources.getDimension(R.dimen.design_appbar_elevation).toInt()
|
||||
val sysElevation =
|
||||
appCtx.resources.getDimension(com.google.android.material.R.dimen.design_appbar_elevation)
|
||||
.toInt()
|
||||
|
||||
val androidId: String by lazy {
|
||||
Settings.System.getString(appCtx.contentResolver, Settings.Secure.ANDROID_ID) ?: "null"
|
||||
|
||||
@@ -3,9 +3,6 @@ package io.legado.app.help
|
||||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import androidx.annotation.Keep
|
||||
import cn.hutool.crypto.digest.DigestUtil
|
||||
import cn.hutool.crypto.symmetric.AES
|
||||
import cn.hutool.crypto.symmetric.DESede
|
||||
import io.legado.app.constant.AppConst
|
||||
import io.legado.app.constant.AppConst.dateFormat
|
||||
import io.legado.app.constant.AppLog
|
||||
@@ -299,14 +296,6 @@ interface JsExtensions {
|
||||
return EncoderUtils.base64Encode(str, flags)
|
||||
}
|
||||
|
||||
fun md5Encode(str: String): String {
|
||||
return MD5Utils.md5Encode(str)
|
||||
}
|
||||
|
||||
fun md5Encode16(str: String): String {
|
||||
return MD5Utils.md5Encode16(str)
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间
|
||||
*/
|
||||
@@ -602,6 +591,13 @@ interface JsExtensions {
|
||||
return UUID.randomUUID().toString()
|
||||
}
|
||||
|
||||
fun androidId(): String {
|
||||
return AppConst.androidId
|
||||
}
|
||||
|
||||
//******************对称加密解密************************//
|
||||
|
||||
/////AES
|
||||
/**
|
||||
* AES 解码为 ByteArray
|
||||
* @param str 传入的AES加密的数据
|
||||
@@ -640,6 +636,31 @@ interface JsExtensions {
|
||||
return aesDecodeToByteArray(str, key, transformation, iv)?.let { String(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* AES解码为String,算法参数经过Base64加密
|
||||
*
|
||||
* @param data 加密的字符串
|
||||
* @param key Base64后的密钥
|
||||
* @param mode 模式
|
||||
* @param padding 补码方式
|
||||
* @param iv Base64后的加盐
|
||||
* @return 解密后的字符串
|
||||
*/
|
||||
fun aesDecodeArgsBase64Str(
|
||||
data: String,
|
||||
key: String,
|
||||
mode: String,
|
||||
padding: String,
|
||||
iv: String
|
||||
): String? {
|
||||
return EncoderUtils.decryptAES(
|
||||
data.encodeToByteArray(),
|
||||
Base64.decode(key, Base64.NO_WRAP),
|
||||
"AES/${mode}/${padding}",
|
||||
Base64.decode(iv, Base64.NO_WRAP)
|
||||
)?.let { String(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* 已经base64的AES 解码为 ByteArray
|
||||
* @param str 传入的AES Base64加密的数据
|
||||
@@ -753,35 +774,78 @@ interface JsExtensions {
|
||||
return aesEncodeToBase64ByteArray(data, key, transformation, iv)?.let { String(it) }
|
||||
}
|
||||
|
||||
fun androidId(): String {
|
||||
return AppConst.androidId
|
||||
}
|
||||
|
||||
/**
|
||||
* AES解密,算法参数经过Base64加密
|
||||
* AES加密并转为Base64,算法参数经过Base64加密
|
||||
*
|
||||
* @param data 加密的字符串
|
||||
* @param data 被加密的字符串
|
||||
* @param key Base64后的密钥
|
||||
* @param mode 模式
|
||||
* @param padding 补码方式
|
||||
* @param iv Base64后的加盐
|
||||
* @return 解密后的字符串
|
||||
* @return 加密后的Base64
|
||||
*/
|
||||
fun aesDecodeArgsBase64Str(
|
||||
fun aesEncodeArgsBase64Str(
|
||||
data: String,
|
||||
key: String,
|
||||
mode: String,
|
||||
padding: String,
|
||||
iv: String
|
||||
): String? {
|
||||
return AES(
|
||||
mode,
|
||||
padding,
|
||||
return EncoderUtils.encryptAES2Base64(
|
||||
data.encodeToByteArray(),
|
||||
Base64.decode(key, Base64.NO_WRAP),
|
||||
"AES/${mode}/${padding}",
|
||||
Base64.decode(iv, Base64.NO_WRAP)
|
||||
).decryptStr(data)
|
||||
)?.let { String(it) }
|
||||
}
|
||||
|
||||
/////DES
|
||||
fun desDecodeToString(
|
||||
data: String, key: String, transformation: String, iv: String
|
||||
): String? {
|
||||
return EncoderUtils.decryptDES(
|
||||
data.encodeToByteArray(),
|
||||
key.encodeToByteArray(),
|
||||
transformation,
|
||||
iv.encodeToByteArray()
|
||||
)?.let { String(it) }
|
||||
}
|
||||
|
||||
fun desBase64DecodeToString(
|
||||
data: String, key: String, transformation: String, iv: String
|
||||
): String? {
|
||||
return EncoderUtils.decryptBase64DES(
|
||||
data.encodeToByteArray(),
|
||||
key.encodeToByteArray(),
|
||||
transformation,
|
||||
iv.encodeToByteArray()
|
||||
)?.let { String(it) }
|
||||
}
|
||||
|
||||
fun desEncodeToString(
|
||||
data: String, key: String, transformation: String, iv: String
|
||||
): String? {
|
||||
return EncoderUtils.encryptDES(
|
||||
data.encodeToByteArray(),
|
||||
key.encodeToByteArray(),
|
||||
transformation,
|
||||
iv.encodeToByteArray()
|
||||
)?.let { String(it) }
|
||||
}
|
||||
|
||||
fun desEncodeToBase64String(
|
||||
data: String, key: String, transformation: String, iv: String
|
||||
): String? {
|
||||
return EncoderUtils.encryptDES2Base64(
|
||||
data.encodeToByteArray(),
|
||||
key.encodeToByteArray(),
|
||||
transformation,
|
||||
iv.encodeToByteArray()
|
||||
)?.let { String(it) }
|
||||
}
|
||||
|
||||
//////3DES
|
||||
/**
|
||||
* 3DES解密
|
||||
*
|
||||
@@ -799,7 +863,12 @@ interface JsExtensions {
|
||||
padding: String,
|
||||
iv: String
|
||||
): String? {
|
||||
return DESede(mode, padding, key.toByteArray(), iv.toByteArray()).decryptStr(data)
|
||||
return EncoderUtils.decryptDESede(
|
||||
data.encodeToByteArray(),
|
||||
key.encodeToByteArray(),
|
||||
"DESede/${mode}/${padding}",
|
||||
iv.encodeToByteArray()
|
||||
)?.let { String(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -819,38 +888,14 @@ interface JsExtensions {
|
||||
padding: String,
|
||||
iv: String
|
||||
): String? {
|
||||
return DESede(
|
||||
mode,
|
||||
padding,
|
||||
return EncoderUtils.decryptDESede(
|
||||
data.encodeToByteArray(),
|
||||
Base64.decode(key, Base64.NO_WRAP),
|
||||
"DESede/${mode}/${padding}",
|
||||
Base64.decode(iv, Base64.NO_WRAP)
|
||||
).decryptStr(data)
|
||||
)?.let { String(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* AES加密并转为Base64,算法参数经过Base64加密
|
||||
*
|
||||
* @param data 被加密的字符串
|
||||
* @param key Base64后的密钥
|
||||
* @param mode 模式
|
||||
* @param padding 补码方式
|
||||
* @param iv Base64后的加盐
|
||||
* @return 加密后的Base64
|
||||
*/
|
||||
fun aesEncodeArgsBase64Str(
|
||||
data: String,
|
||||
key: String,
|
||||
mode: String,
|
||||
padding: String,
|
||||
iv: String
|
||||
): String? {
|
||||
return AES(
|
||||
mode,
|
||||
padding,
|
||||
Base64.decode(key, Base64.NO_WRAP),
|
||||
Base64.decode(iv, Base64.NO_WRAP)
|
||||
).encryptBase64(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 3DES加密并转为Base64
|
||||
@@ -869,7 +914,12 @@ interface JsExtensions {
|
||||
padding: String,
|
||||
iv: String
|
||||
): String? {
|
||||
return DESede(mode, padding, key.toByteArray(), iv.toByteArray()).encryptBase64(data)
|
||||
return EncoderUtils.encryptDESede2Base64(
|
||||
data.encodeToByteArray(),
|
||||
key.encodeToByteArray(),
|
||||
"DESede/${mode}/${padding}",
|
||||
iv.encodeToByteArray()
|
||||
)?.let { String(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -889,14 +939,16 @@ interface JsExtensions {
|
||||
padding: String,
|
||||
iv: String
|
||||
): String? {
|
||||
return DESede(
|
||||
mode,
|
||||
padding,
|
||||
return EncoderUtils.encryptDESede2Base64(
|
||||
data.encodeToByteArray(),
|
||||
Base64.decode(key, Base64.NO_WRAP),
|
||||
"DESede/${mode}/${padding}",
|
||||
Base64.decode(iv, Base64.NO_WRAP)
|
||||
).encryptBase64(data)
|
||||
)?.let { String(it) }
|
||||
}
|
||||
|
||||
//******************消息摘要/散列消息鉴别码************************//
|
||||
|
||||
/**
|
||||
* 生成摘要,并转为16进制字符串
|
||||
*
|
||||
@@ -907,8 +959,8 @@ interface JsExtensions {
|
||||
fun digestHex(
|
||||
data: String,
|
||||
algorithm: String,
|
||||
): String? {
|
||||
return DigestUtil.digester(algorithm).digestHex(data)
|
||||
): String {
|
||||
return DigestUtils.getDigest(algorithm, data)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -921,8 +973,48 @@ interface JsExtensions {
|
||||
fun digestBase64Str(
|
||||
data: String,
|
||||
algorithm: String,
|
||||
): String? {
|
||||
return Base64.encodeToString(DigestUtil.digester(algorithm).digest(data), Base64.NO_WRAP)
|
||||
): String {
|
||||
return Base64.encodeToString(DigestUtils.getDigest(algorithm, data.toByteArray()), Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成散列消息鉴别码,并转为16进制字符串
|
||||
*
|
||||
* @param data 被摘要数据
|
||||
* @param algorithm 签名算法
|
||||
* @param key 密钥
|
||||
* @return 16进制字符串
|
||||
*/
|
||||
fun HMacHex(
|
||||
data: String,
|
||||
algorithm: String,
|
||||
key: String
|
||||
): String {
|
||||
return DigestUtils.getHMac(algorithm, key, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成散列消息鉴别码,并转为Base64字符串
|
||||
*
|
||||
* @param data 被摘要数据
|
||||
* @param algorithm 签名算法
|
||||
* @param key 密钥
|
||||
* @return Base64字符串
|
||||
*/
|
||||
fun HMacBase64(
|
||||
data: String,
|
||||
algorithm: String,
|
||||
key: String
|
||||
): String {
|
||||
return Base64.encodeToString(DigestUtils.getHMac(algorithm, key.toByteArray(), data.toByteArray()), Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
fun md5Encode(str: String): String {
|
||||
return MD5Utils.md5Encode(str)
|
||||
}
|
||||
|
||||
fun md5Encode16(str: String): String {
|
||||
return MD5Utils.md5Encode16(str)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ class ColorPreference(context: Context, attrs: AttributeSet) : Preference(contex
|
||||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
||||
val v = io.legado.app.lib.prefs.Preference.bindView<ColorPanelView>(
|
||||
context, holder, icon, title, summary, widgetLayoutResource,
|
||||
io.legado.app.R.id.cpv_preference_preview_color_panel, 30, 30
|
||||
com.jaredrummler.android.colorpicker.R.id.cpv_preference_preview_color_panel, 30, 30
|
||||
)
|
||||
if (v is ColorPanelView) {
|
||||
v.color = mColor
|
||||
|
||||
@@ -30,7 +30,7 @@ class SwitchPreference(context: Context, attrs: AttributeSet) :
|
||||
title,
|
||||
summary,
|
||||
widgetLayoutResource,
|
||||
R.id.switchWidget,
|
||||
androidx.preference.R.id.switchWidget,
|
||||
isBottomBackground = isBottomBackground
|
||||
)
|
||||
if (v is SwitchCompat && !v.isInEditMode) {
|
||||
|
||||
@@ -19,34 +19,52 @@ import io.legado.app.utils.dpToPx
|
||||
@ColorInt
|
||||
fun Context.getPrimaryTextColor(dark: Boolean): Int {
|
||||
return if (dark) {
|
||||
ContextCompat.getColor(this, R.color.primary_text_default_material_light)
|
||||
} else ContextCompat.getColor(this, R.color.primary_text_default_material_dark)
|
||||
ContextCompat.getColor(this, androidx.appcompat.R.color.primary_text_default_material_light)
|
||||
} else ContextCompat.getColor(
|
||||
this,
|
||||
androidx.appcompat.R.color.primary_text_default_material_dark
|
||||
)
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun Context.getSecondaryTextColor(dark: Boolean): Int {
|
||||
return if (dark) {
|
||||
ContextCompat.getColor(this, R.color.secondary_text_default_material_light)
|
||||
ContextCompat.getColor(
|
||||
this,
|
||||
androidx.appcompat.R.color.secondary_text_default_material_light
|
||||
)
|
||||
} else {
|
||||
ContextCompat.getColor(this, R.color.secondary_text_default_material_dark)
|
||||
ContextCompat.getColor(
|
||||
this,
|
||||
androidx.appcompat.R.color.secondary_text_default_material_dark
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun Context.getPrimaryDisabledTextColor(dark: Boolean): Int {
|
||||
return if (dark) {
|
||||
ContextCompat.getColor(this, R.color.primary_text_disabled_material_light)
|
||||
ContextCompat.getColor(
|
||||
this,
|
||||
androidx.appcompat.R.color.primary_text_disabled_material_light
|
||||
)
|
||||
} else {
|
||||
ContextCompat.getColor(this, R.color.primary_text_disabled_material_dark)
|
||||
ContextCompat.getColor(this, androidx.appcompat.R.color.primary_text_disabled_material_dark)
|
||||
}
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun Context.getSecondaryDisabledTextColor(dark: Boolean): Int {
|
||||
return if (dark) {
|
||||
ContextCompat.getColor(this, R.color.secondary_text_disabled_material_light)
|
||||
ContextCompat.getColor(
|
||||
this,
|
||||
androidx.appcompat.R.color.secondary_text_disabled_material_light
|
||||
)
|
||||
} else {
|
||||
ContextCompat.getColor(this, R.color.secondary_text_disabled_material_dark)
|
||||
ContextCompat.getColor(
|
||||
this,
|
||||
androidx.appcompat.R.color.secondary_text_disabled_material_dark
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +142,7 @@ val Context.elevation: Float
|
||||
ThemeUtils.resolveFloat(
|
||||
this,
|
||||
android.R.attr.elevation,
|
||||
resources.getDimension(R.dimen.design_appbar_elevation)
|
||||
resources.getDimension(com.google.android.material.R.dimen.design_appbar_elevation)
|
||||
)
|
||||
} else {
|
||||
AppConfig.elevation.toFloat().dpToPx()
|
||||
|
||||
@@ -9,7 +9,6 @@ import androidx.annotation.CheckResult
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import io.legado.app.R
|
||||
import io.legado.app.utils.ColorUtils
|
||||
import splitties.init.appCtx
|
||||
|
||||
@@ -189,7 +188,11 @@ private constructor(private val mContext: Context) : ThemeStoreInterface {
|
||||
fun primaryColor(context: Context = appCtx): Int {
|
||||
return prefs(context).getInt(
|
||||
ThemeStorePrefKeys.KEY_PRIMARY_COLOR,
|
||||
ThemeUtils.resolveColor(context, R.attr.colorPrimary, Color.parseColor("#455A64"))
|
||||
ThemeUtils.resolveColor(
|
||||
context,
|
||||
androidx.appcompat.R.attr.colorPrimary,
|
||||
Color.parseColor("#455A64")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -200,7 +203,7 @@ private constructor(private val mContext: Context) : ThemeStoreInterface {
|
||||
ThemeStorePrefKeys.KEY_PRIMARY_COLOR_DARK,
|
||||
ThemeUtils.resolveColor(
|
||||
context,
|
||||
R.attr.colorPrimaryDark,
|
||||
androidx.appcompat.R.attr.colorPrimaryDark,
|
||||
Color.parseColor("#37474F")
|
||||
)
|
||||
)
|
||||
@@ -211,7 +214,11 @@ private constructor(private val mContext: Context) : ThemeStoreInterface {
|
||||
fun accentColor(context: Context = appCtx): Int {
|
||||
return prefs(context).getInt(
|
||||
ThemeStorePrefKeys.KEY_ACCENT_COLOR,
|
||||
ThemeUtils.resolveColor(context, R.attr.colorAccent, Color.parseColor("#263238"))
|
||||
ThemeUtils.resolveColor(
|
||||
context,
|
||||
androidx.appcompat.R.attr.colorAccent,
|
||||
Color.parseColor("#263238")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -31,9 +31,9 @@ object TintHelper {
|
||||
// Light ripple is actually translucent black, and vice versa
|
||||
return ContextCompat.getColor(
|
||||
context, if (useDarkRipple)
|
||||
R.color.ripple_material_light
|
||||
androidx.appcompat.R.color.ripple_material_light
|
||||
else
|
||||
R.color.ripple_material_dark
|
||||
androidx.appcompat.R.color.ripple_material_dark
|
||||
)
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ object TintHelper {
|
||||
val rd = view.background as RippleDrawable
|
||||
@SuppressLint("PrivateResource") val unchecked = ContextCompat.getColor(
|
||||
view.context,
|
||||
if (isDark) R.color.ripple_material_dark else R.color.ripple_material_light
|
||||
if (isDark) androidx.appcompat.R.color.ripple_material_dark else androidx.appcompat.R.color.ripple_material_light
|
||||
)
|
||||
val checked = ColorUtils.adjustAlpha(color, 0.4f)
|
||||
val sl = ColorStateList(
|
||||
|
||||
@@ -219,6 +219,8 @@ object LocalBook {
|
||||
fileName: String,
|
||||
source: BaseSource? = null,
|
||||
): Uri {
|
||||
AppConfig.defaultBookTreeUri
|
||||
?: throw NoStackTraceException("没有设置书籍保存位置!")
|
||||
val bytes = when {
|
||||
str.isAbsUrl() -> AnalyzeUrl(str, source = source).getByteArray()
|
||||
str.isDataUrl() -> Base64.decode(str.substringAfter("base64,"), Base64.DEFAULT)
|
||||
@@ -241,7 +243,7 @@ object LocalBook {
|
||||
return type ?: fileType
|
||||
}
|
||||
|
||||
private fun saveBookFile(
|
||||
fun saveBookFile(
|
||||
bytes: ByteArray,
|
||||
fileName: String
|
||||
): Uri {
|
||||
@@ -269,6 +271,26 @@ object LocalBook {
|
||||
}
|
||||
}
|
||||
|
||||
fun isOnBookShelf(
|
||||
fileName: String
|
||||
): Boolean {
|
||||
val defaultBookTreeUri = AppConfig.defaultBookTreeUri
|
||||
if (defaultBookTreeUri.isNullOrBlank()) throw NoStackTraceException("没有设置书籍保存位置!")
|
||||
val treeUri = Uri.parse(defaultBookTreeUri)
|
||||
var bookUrl: String = ""
|
||||
if (treeUri.isContentScheme()) {
|
||||
val treeDoc = DocumentFile.fromTreeUri(appCtx, treeUri)
|
||||
var doc = treeDoc!!.findFile(fileName) ?: return false
|
||||
bookUrl = doc.uri.toString()
|
||||
} else {
|
||||
val treeFile = File(treeUri.path!!)
|
||||
val file = treeFile.getFile(fileName)
|
||||
if (!file.exists()) return false
|
||||
bookUrl = file.absolutePath
|
||||
}
|
||||
return appDb.bookDao.getBook(bookUrl) != null
|
||||
}
|
||||
|
||||
//文件类书源 合并在线书籍信息 在线 > 本地
|
||||
fun mergeBook(localBook: Book, onLineBook: Book?): Book {
|
||||
onLineBook ?: return localBook
|
||||
|
||||
@@ -5,5 +5,6 @@ data class RemoteBook(
|
||||
val urlName: String,
|
||||
val size: Long,
|
||||
val contentType: String,
|
||||
val lastModify: Long
|
||||
val lastModify: Long,
|
||||
val isOnBookShelf: Boolean
|
||||
)
|
||||
@@ -2,11 +2,12 @@ package io.legado.app.ui.book.remote
|
||||
|
||||
import android.content.Context
|
||||
import android.view.ViewGroup
|
||||
import cn.hutool.core.date.LocalDateTimeUtil
|
||||
import io.legado.app.base.adapter.ItemViewHolder
|
||||
import io.legado.app.base.adapter.RecyclerAdapter
|
||||
import io.legado.app.databinding.ItemRemoteBookBinding
|
||||
import io.legado.app.utils.ConvertUtils
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
|
||||
|
||||
/**
|
||||
@@ -38,7 +39,7 @@ class RemoteBookAdapter (context: Context, val callBack: CallBack) :
|
||||
tvName.text = item.filename.substringBeforeLast(".")
|
||||
tvContentType.text = item.contentType
|
||||
tvSize.text = ConvertUtils.formatFileSize(item.size)
|
||||
tvDate.text = LocalDateTimeUtil.format(LocalDateTimeUtil.of(item.lastModify), "yyyy-MM-dd")
|
||||
tvDate.text = SimpleDateFormat("yyyy-MM-dd").format(Date(item.lastModify))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,8 @@ package io.legado.app.ui.book.remote
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
|
||||
|
||||
abstract class RemoteBookManager {
|
||||
protected val remoteBookFolder : String = "books"
|
||||
protected val contentTypeList: ArrayList<String> = arrayListOf("epub","txt")
|
||||
abstract suspend fun initRemoteContext()
|
||||
abstract suspend fun getRemoteBookList(): MutableList<RemoteBook>
|
||||
abstract suspend fun upload(localBookUri: Uri): Boolean
|
||||
@@ -15,5 +12,5 @@ abstract class RemoteBookManager {
|
||||
/**
|
||||
* @return String:下载到本地的路径
|
||||
*/
|
||||
abstract suspend fun getRemoteBook(remoteBook: RemoteBook): String?
|
||||
abstract suspend fun getRemoteBook(remoteBook: RemoteBook): Uri?
|
||||
}
|
||||
@@ -73,7 +73,7 @@ class RemoteBookViewModel(application: Application): BaseViewModel(application){
|
||||
execute {
|
||||
val downloadBookPath = RemoteBookWebDav.getRemoteBook(remoteBook)
|
||||
downloadBookPath?.let {
|
||||
LocalBook.importFile(Uri.parse(it))
|
||||
LocalBook.importFile(it)
|
||||
}
|
||||
}.onFinally {
|
||||
finally.invoke()
|
||||
|
||||
@@ -3,11 +3,13 @@ package io.legado.app.ui.book.remote.manager
|
||||
|
||||
import android.net.Uri
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.constant.AppPattern.bookFileRegex
|
||||
import io.legado.app.exception.NoStackTraceException
|
||||
import io.legado.app.help.AppWebDav
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.lib.webdav.WebDav
|
||||
import io.legado.app.lib.webdav.WebDavFile
|
||||
import io.legado.app.model.localBook.LocalBook
|
||||
import io.legado.app.ui.book.remote.RemoteBook
|
||||
import io.legado.app.ui.book.remote.RemoteBookManager
|
||||
import io.legado.app.utils.*
|
||||
@@ -17,7 +19,6 @@ import java.io.File
|
||||
|
||||
object RemoteBookWebDav : RemoteBookManager() {
|
||||
private val remoteBookUrl get() = "${rootWebDavUrl}${remoteBookFolder}"
|
||||
private val localSaveFolder get() = "${appCtx.externalFiles.absolutePath}${File.separator}${remoteBookFolder}"
|
||||
|
||||
init {
|
||||
runBlocking {
|
||||
@@ -68,31 +69,35 @@ object RemoteBookWebDav : RemoteBookManager() {
|
||||
val fileExtension = webDavFileName.substringAfterLast(".")
|
||||
|
||||
//扩展名符合阅读的格式则认为是书籍
|
||||
if (contentTypeList.contains(fileExtension)) {
|
||||
remoteBooks.add(RemoteBook(webDavFileName,webDavUrlName,webDavFile.size,fileExtension,webDavFile.lastModify))
|
||||
if (bookFileRegex.matches(webDavFileName)) {
|
||||
val isOnBookShelf = LocalBook.isOnBookShelf(webDavFileName)
|
||||
remoteBooks.add(
|
||||
RemoteBook(
|
||||
webDavFileName, webDavUrlName, webDavFile.size,
|
||||
fileExtension, webDavFile.lastModify, isOnBookShelf
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} ?: throw NoStackTraceException("webDav没有配置")
|
||||
return remoteBooks
|
||||
}
|
||||
|
||||
override suspend fun getRemoteBook(remoteBook: RemoteBook): String? {
|
||||
val saveFilePath= "${localSaveFolder}${File.separator}${remoteBook.filename}"
|
||||
kotlin.runCatching {
|
||||
override suspend fun getRemoteBook(remoteBook: RemoteBook): Uri? {
|
||||
return kotlin.runCatching {
|
||||
AppWebDav.authorization?.let {
|
||||
FileUtils.createFolderIfNotExist(localSaveFolder).run {
|
||||
val webdav = WebDav(
|
||||
remoteBook.urlName,
|
||||
it
|
||||
)
|
||||
webdav.downloadTo(saveFilePath, true)
|
||||
val webdav = WebDav(
|
||||
remoteBook.urlName,
|
||||
it
|
||||
)
|
||||
webdav.download().let { bytes ->
|
||||
LocalBook.saveBookFile(bytes, remoteBook.filename)
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
return null
|
||||
}
|
||||
return saveFilePath
|
||||
null
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,7 +17,6 @@ import android.widget.ImageView
|
||||
import android.widget.OverScroller
|
||||
import android.widget.Scroller
|
||||
import androidx.appcompat.widget.AppCompatImageView
|
||||
import io.legado.app.R
|
||||
import io.legado.app.ui.widget.image.photo.Info
|
||||
import io.legado.app.ui.widget.image.photo.OnRotateListener
|
||||
import io.legado.app.ui.widget.image.photo.RotateGestureDetector
|
||||
@@ -917,7 +916,7 @@ class PhotoView @JvmOverloads constructor(
|
||||
var viewParent: ViewParent = target.parent
|
||||
while (viewParent is View) {
|
||||
val view: View = viewParent
|
||||
if (view.id == R.id.content) return
|
||||
if (view.id == androidx.constraintlayout.widget.R.id.content) return
|
||||
position[0] -= view.scrollX
|
||||
position[1] -= view.scrollY
|
||||
position[0] += view.left
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package io.legado.app.utils
|
||||
|
||||
import java.security.MessageDigest
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
object DigestUtils {
|
||||
|
||||
/**
|
||||
* 消息摘要
|
||||
* 支持MD5 SHA-1 SHA-224 SHA-256 SHA-384 SHA-512
|
||||
* https://developer.android.google.cn/reference/java/security/MessageDigest?hl=en
|
||||
*/
|
||||
fun getDigest(
|
||||
algorithm: String,
|
||||
data: String?
|
||||
): String {
|
||||
data ?: return ""
|
||||
val bytes = getDigest(algorithm, data.toByteArray())
|
||||
return StringUtils.byteToHexString(bytes)
|
||||
}
|
||||
|
||||
fun getDigest(
|
||||
algorithm: String,
|
||||
data: ByteArray
|
||||
): ByteArray {
|
||||
return kotlin.runCatching {
|
||||
val messageDigest = MessageDigest.getInstance(algorithm)
|
||||
messageDigest.digest(data)
|
||||
}.getOrThrow()
|
||||
}
|
||||
|
||||
/**
|
||||
* 散列消息鉴别码
|
||||
* 支持DESMAC DESMAC/CFB8 DESedeMAC DESedeMAC/CFB8 DESedeMAC64 DESwithISO9797 HmacMD5 HmacSHA* ISO9797ALG3MAC PBEwithSHA*
|
||||
* https://developer.android.google.cn/reference/kotlin/javax/crypto/Mac?hl=en
|
||||
*/
|
||||
fun getHMac(
|
||||
algorithm: String,
|
||||
key: String,
|
||||
data: String?
|
||||
): String {
|
||||
data ?: return ""
|
||||
val bytes = getHMac(algorithm, key.toByteArray(), data.toByteArray())
|
||||
return StringUtils.byteToHexString(bytes)
|
||||
}
|
||||
|
||||
fun getHMac(
|
||||
algorithm: String,
|
||||
key: ByteArray,
|
||||
data: ByteArray
|
||||
): ByteArray {
|
||||
return kotlin.runCatching {
|
||||
val mac= Mac.getInstance(algorithm)
|
||||
val keySpec = SecretKeySpec(key, algorithm)
|
||||
mac.init(keySpec)
|
||||
mac.update(data)
|
||||
mac.doFinal()
|
||||
}.getOrThrow()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,6 +6,9 @@ import javax.crypto.Cipher
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
/**
|
||||
* transformations https://developer.android.google.cn/reference/kotlin/javax/crypto/Cipher?hl=en
|
||||
*/
|
||||
@Suppress("unused")
|
||||
object EncoderUtils {
|
||||
|
||||
@@ -47,7 +50,7 @@ object EncoderUtils {
|
||||
* @param data The data.
|
||||
* @param key The key.
|
||||
* @param transformation The name of the transformation,
|
||||
* 加密算法/加密模式/填充类型, *DES/CBC/PKCS5Padding*.
|
||||
* 加密算法/加密模式/填充类型, *AES/CBC/PKCS5Padding*.
|
||||
* @param iv The buffer with the IV. The contents of the
|
||||
* buffer are copied to protect against subsequent modification.
|
||||
* @return the Base64-encode bytes of AES encryption
|
||||
@@ -56,7 +59,7 @@ object EncoderUtils {
|
||||
fun encryptAES2Base64(
|
||||
data: ByteArray?,
|
||||
key: ByteArray?,
|
||||
transformation: String? = "DES/ECB/PKCS5Padding",
|
||||
transformation: String? = "AES/ECB/PKCS5Padding",
|
||||
iv: ByteArray? = null
|
||||
): ByteArray? {
|
||||
return Base64.encode(encryptAES(data, key, transformation, iv), Base64.NO_WRAP)
|
||||
@@ -68,7 +71,7 @@ object EncoderUtils {
|
||||
* @param data The data.
|
||||
* @param key The key.
|
||||
* @param transformation The name of the transformation,
|
||||
* 加密算法/加密模式/填充类型, *DES/CBC/PKCS5Padding*.
|
||||
* 加密算法/加密模式/填充类型, *AES/CBC/PKCS5Padding*.
|
||||
* @param iv The buffer with the IV. The contents of the
|
||||
* buffer are copied to protect against subsequent modification.
|
||||
* @return the bytes of AES encryption
|
||||
@@ -77,7 +80,7 @@ object EncoderUtils {
|
||||
fun encryptAES(
|
||||
data: ByteArray?,
|
||||
key: ByteArray?,
|
||||
transformation: String? = "DES/ECB/PKCS5Padding",
|
||||
transformation: String? = "AES/ECB/PKCS5Padding",
|
||||
iv: ByteArray? = null
|
||||
): ByteArray? {
|
||||
return symmetricTemplate(data, key, "AES", transformation!!, iv, true)
|
||||
@@ -90,7 +93,7 @@ object EncoderUtils {
|
||||
* @param data The data.
|
||||
* @param key The key.
|
||||
* @param transformation The name of the transformation,
|
||||
* 加密算法/加密模式/填充类型, *DES/CBC/PKCS5Padding*.
|
||||
* 加密算法/加密模式/填充类型, *AES/CBC/PKCS5Padding*.
|
||||
* @param iv The buffer with the IV. The contents of the
|
||||
* buffer are copied to protect against subsequent modification.
|
||||
* @return the bytes of AES decryption for Base64-encode bytes
|
||||
@@ -99,7 +102,7 @@ object EncoderUtils {
|
||||
fun decryptBase64AES(
|
||||
data: ByteArray?,
|
||||
key: ByteArray?,
|
||||
transformation: String = "DES/ECB/PKCS5Padding",
|
||||
transformation: String = "AES/ECB/PKCS5Padding",
|
||||
iv: ByteArray? = null
|
||||
): ByteArray? {
|
||||
return decryptAES(Base64.decode(data, Base64.NO_WRAP), key, transformation, iv)
|
||||
@@ -111,7 +114,7 @@ object EncoderUtils {
|
||||
* @param data The data.
|
||||
* @param key The key.
|
||||
* @param transformation The name of the transformation,
|
||||
* 加密算法/加密模式/填充类型, *DES/CBC/PKCS5Padding*.
|
||||
* 加密算法/加密模式/填充类型, *AES/CBC/PKCS5Padding*.
|
||||
* @param iv The buffer with the IV. The contents of the
|
||||
* buffer are copied to protect against subsequent modification.
|
||||
* @return the bytes of AES decryption
|
||||
@@ -120,12 +123,185 @@ object EncoderUtils {
|
||||
fun decryptAES(
|
||||
data: ByteArray?,
|
||||
key: ByteArray?,
|
||||
transformation: String = "DES/ECB/PKCS5Padding",
|
||||
transformation: String = "AES/ECB/PKCS5Padding",
|
||||
iv: ByteArray? = null
|
||||
): ByteArray? {
|
||||
return symmetricTemplate(data, key, "AES", transformation, iv, false)
|
||||
}
|
||||
|
||||
//////////DES Start
|
||||
|
||||
/**
|
||||
* Return the Base64-encode bytes of DES encryption.
|
||||
*
|
||||
* @param data The data.
|
||||
* @param key The key.
|
||||
* @param transformation The name of the transformation,
|
||||
* 加密算法/加密模式/填充类型, *DES/CBC/PKCS5Padding*.
|
||||
* @param iv The buffer with the IV. The contents of the
|
||||
* buffer are copied to protect against subsequent modification.
|
||||
* @return the Base64-encode bytes of AES encryption
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
fun encryptDES2Base64(
|
||||
data: ByteArray?,
|
||||
key: ByteArray?,
|
||||
transformation: String? = "DES/ECB/PKCS5Padding",
|
||||
iv: ByteArray? = null
|
||||
): ByteArray? {
|
||||
return Base64.encode(encryptDES(data, key, transformation, iv), Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the bytes of DES encryption.
|
||||
*
|
||||
* @param data The data.
|
||||
* @param key The key.
|
||||
* @param transformation The name of the transformation,
|
||||
* 加密算法/加密模式/填充类型, *DES/CBC/PKCS5Padding*.
|
||||
* @param iv The buffer with the IV. The contents of the
|
||||
* buffer are copied to protect against subsequent modification.
|
||||
* @return the bytes of AES encryption
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
fun encryptDES(
|
||||
data: ByteArray?,
|
||||
key: ByteArray?,
|
||||
transformation: String? = "DES/ECB/PKCS5Padding",
|
||||
iv: ByteArray? = null
|
||||
): ByteArray? {
|
||||
return symmetricTemplate(data, key, "DES", transformation!!, iv, true)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the bytes of DES decryption for Base64-encode bytes.
|
||||
*
|
||||
* @param data The data.
|
||||
* @param key The key.
|
||||
* @param transformation The name of the transformation,
|
||||
* 加密算法/加密模式/填充类型, *DES/CBC/PKCS5Padding*.
|
||||
* @param iv The buffer with the IV. The contents of the
|
||||
* buffer are copied to protect against subsequent modification.
|
||||
* @return the bytes of AES decryption for Base64-encode bytes
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
fun decryptBase64DES(
|
||||
data: ByteArray?,
|
||||
key: ByteArray?,
|
||||
transformation: String = "DES/ECB/PKCS5Padding",
|
||||
iv: ByteArray? = null
|
||||
): ByteArray? {
|
||||
return decryptDES(Base64.decode(data, Base64.NO_WRAP), key, transformation, iv)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the bytes of DES decryption.
|
||||
*
|
||||
* @param data The data.
|
||||
* @param key The key.
|
||||
* @param transformation The name of the transformation,
|
||||
* 加密算法/加密模式/填充类型, *DES/CBC/PKCS5Padding*.
|
||||
* @param iv The buffer with the IV. The contents of the
|
||||
* buffer are copied to protect against subsequent modification.
|
||||
* @return the bytes of AES decryption
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
fun decryptDES(
|
||||
data: ByteArray?,
|
||||
key: ByteArray?,
|
||||
transformation: String = "DES/ECB/PKCS5Padding",
|
||||
iv: ByteArray? = null
|
||||
): ByteArray? {
|
||||
return symmetricTemplate(data, key, "DES", transformation, iv, false)
|
||||
}
|
||||
|
||||
//////////DESede Start
|
||||
|
||||
/**
|
||||
* Return the Base64-encode bytes of DESede encryption.
|
||||
*
|
||||
* @param data The data.
|
||||
* @param key The key.
|
||||
* @param transformation The name of the transformation,
|
||||
* 加密算法/加密模式/填充类型, *DESede/CBC/PKCS5Padding*.
|
||||
* @param iv The buffer with the IV. The contents of the
|
||||
* buffer are copied to protect against subsequent modification.
|
||||
* @return the Base64-encode bytes of AES encryption
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
fun encryptDESede2Base64(
|
||||
data: ByteArray?,
|
||||
key: ByteArray?,
|
||||
transformation: String? = "DESede/ECB/PKCS5Padding",
|
||||
iv: ByteArray? = null
|
||||
): ByteArray? {
|
||||
return Base64.encode(encryptDESede(data, key, transformation, iv), Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the bytes of DESede encryption.
|
||||
*
|
||||
* @param data The data.
|
||||
* @param key The key.
|
||||
* @param transformation The name of the transformation,
|
||||
* 加密算法/加密模式/填充类型, *DESede/CBC/PKCS5Padding*.
|
||||
* @param iv The buffer with the IV. The contents of the
|
||||
* buffer are copied to protect against subsequent modification.
|
||||
* @return the bytes of AES encryption
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
fun encryptDESede(
|
||||
data: ByteArray?,
|
||||
key: ByteArray?,
|
||||
transformation: String? = "DESede/ECB/PKCS5Padding",
|
||||
iv: ByteArray? = null
|
||||
): ByteArray? {
|
||||
return symmetricTemplate(data, key, "DESede", transformation!!, iv, true)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the bytes of DESede decryption for Base64-encode bytes.
|
||||
*
|
||||
* @param data The data.
|
||||
* @param key The key.
|
||||
* @param transformation The name of the transformation,
|
||||
* 加密算法/加密模式/填充类型, *DESede/CBC/PKCS5Padding*.
|
||||
* @param iv The buffer with the IV. The contents of the
|
||||
* buffer are copied to protect against subsequent modification.
|
||||
* @return the bytes of AES decryption for Base64-encode bytes
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
fun decryptBase64DESede(
|
||||
data: ByteArray?,
|
||||
key: ByteArray?,
|
||||
transformation: String = "DESede/ECB/PKCS5Padding",
|
||||
iv: ByteArray? = null
|
||||
): ByteArray? {
|
||||
return decryptDESede(Base64.decode(data, Base64.NO_WRAP), key, transformation, iv)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the bytes of DESede decryption.
|
||||
*
|
||||
* @param data The data.
|
||||
* @param key The key.
|
||||
* @param transformation The name of the transformation,
|
||||
* 加密算法/加密模式/填充类型, *DESede/CBC/PKCS5Padding*.
|
||||
* @param iv The buffer with the IV. The contents of the
|
||||
* buffer are copied to protect against subsequent modification.
|
||||
* @return the bytes of AES decryption
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
fun decryptDESede(
|
||||
data: ByteArray?,
|
||||
key: ByteArray?,
|
||||
transformation: String = "DESede/ECB/PKCS5Padding",
|
||||
iv: ByteArray? = null
|
||||
): ByteArray? {
|
||||
return symmetricTemplate(data, key, "DESede", transformation, iv, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the bytes of symmetric encryption or decryption.
|
||||
@@ -166,4 +342,4 @@ object EncoderUtils {
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
package io.legado.app.utils
|
||||
|
||||
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
|
||||
/**
|
||||
* 将字符串转化为MD5
|
||||
*/
|
||||
@@ -11,25 +7,7 @@ import java.security.NoSuchAlgorithmException
|
||||
object MD5Utils {
|
||||
|
||||
fun md5Encode(str: String?): String {
|
||||
if (str == null) return ""
|
||||
var reStr = ""
|
||||
try {
|
||||
val md5: MessageDigest = MessageDigest.getInstance("MD5")
|
||||
val bytes: ByteArray = md5.digest(str.toByteArray())
|
||||
val stringBuffer: StringBuilder = StringBuilder()
|
||||
for (b in bytes) {
|
||||
val bt: Int = b.toInt() and 0xff
|
||||
if (bt < 16) {
|
||||
stringBuffer.append(0)
|
||||
}
|
||||
stringBuffer.append(Integer.toHexString(bt))
|
||||
}
|
||||
reStr = stringBuffer.toString()
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
e.printOnDebug()
|
||||
}
|
||||
|
||||
return reStr
|
||||
return DigestUtils.getDigest("MD5", str)
|
||||
}
|
||||
|
||||
fun md5Encode16(str: String): String {
|
||||
|
||||
Reference in New Issue
Block a user