Rename
This commit is contained in:
1 parent
b86d90021c
commit
6c0757c91b
972 files changed
+6885
-7321
No files matched your search
@@ -0,0 +1,783 @@
|
||||
//Copyright (c) 2017. 章钦豪. All rights reserved.
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import splitties.init.appCtx
|
||||
import java.io.*
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import kotlin.math.min
|
||||
|
||||
|
||||
/**
|
||||
* 本地缓存
|
||||
*/
|
||||
@Suppress("unused", "MemberVisibilityCanBePrivate")
|
||||
class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int) {
|
||||
|
||||
companion object {
|
||||
const val TIME_HOUR = 60 * 60
|
||||
const val TIME_DAY = TIME_HOUR * 24
|
||||
private const val MAX_SIZE = 1000 * 1000 * 50 // 50 mb
|
||||
private const val MAX_COUNT = Integer.MAX_VALUE // 不限制存放数据的数量
|
||||
private val mInstanceMap = HashMap<String, ACache>()
|
||||
|
||||
@JvmOverloads
|
||||
fun get(
|
||||
cacheName: String = "ACache",
|
||||
maxSize: Long = MAX_SIZE.toLong(),
|
||||
maxCount: Int = MAX_COUNT,
|
||||
cacheDir: Boolean = true
|
||||
): ACache {
|
||||
val f =
|
||||
if (cacheDir) File(appCtx.cacheDir, cacheName) else File(appCtx.filesDir, cacheName)
|
||||
return get(f, maxSize, maxCount)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun get(
|
||||
cacheDir: File,
|
||||
maxSize: Long = MAX_SIZE.toLong(),
|
||||
maxCount: Int = MAX_COUNT
|
||||
): ACache {
|
||||
synchronized(this) {
|
||||
var manager = mInstanceMap[cacheDir.absoluteFile.toString() + myPid()]
|
||||
if (manager == null) {
|
||||
manager = ACache(cacheDir, maxSize, maxCount)
|
||||
mInstanceMap[cacheDir.absolutePath + myPid()] = manager
|
||||
}
|
||||
return manager
|
||||
}
|
||||
}
|
||||
|
||||
private fun myPid(): String {
|
||||
return "_" + android.os.Process.myPid()
|
||||
}
|
||||
}
|
||||
|
||||
private var mCache: ACacheManager? = null
|
||||
|
||||
init {
|
||||
try {
|
||||
if (!cacheDir.exists() && !cacheDir.mkdirs()) {
|
||||
DebugLog.i(javaClass.name, "can't make dirs in %s" + cacheDir.absolutePath)
|
||||
}
|
||||
mCache = ACacheManager(cacheDir, max_size, max_count)
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// =======================================
|
||||
// ============ String数据 读写 ==============
|
||||
// =======================================
|
||||
|
||||
/**
|
||||
* 保存 String数据 到 缓存中
|
||||
*
|
||||
* @param key 保存的key
|
||||
* @param value 保存的String数据
|
||||
*/
|
||||
fun put(key: String, value: String) {
|
||||
mCache?.let { mCache ->
|
||||
try {
|
||||
val file = mCache.newFile(key)
|
||||
file.writeText(value)
|
||||
mCache.put(file)
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 String数据 到 缓存中
|
||||
*
|
||||
* @param key 保存的key
|
||||
* @param value 保存的String数据
|
||||
* @param saveTime 保存的时间,单位:秒
|
||||
*/
|
||||
fun put(key: String, value: String, saveTime: Int) {
|
||||
if (saveTime == 0) put(key, value) else put(
|
||||
key,
|
||||
Utils.newStringWithDateInfo(saveTime, value)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取 String数据
|
||||
*
|
||||
* @return String 数据
|
||||
*/
|
||||
fun getAsString(key: String): String? {
|
||||
mCache?.let { mCache ->
|
||||
val file = mCache[key]
|
||||
if (!file.exists())
|
||||
return null
|
||||
var removeFile = false
|
||||
try {
|
||||
val text = file.readText()
|
||||
if (!Utils.isDue(text)) {
|
||||
return Utils.clearDateInfo(text)
|
||||
} else {
|
||||
removeFile = true
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
e.printOnDebug()
|
||||
} finally {
|
||||
if (removeFile)
|
||||
remove(key)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// =======================================
|
||||
// ========== JSONObject 数据 读写 =========
|
||||
// =======================================
|
||||
|
||||
/**
|
||||
* 保存 JSONObject数据 到 缓存中
|
||||
*
|
||||
* @param key 保存的key
|
||||
* @param value 保存的JSON数据
|
||||
*/
|
||||
fun put(key: String, value: JSONObject) {
|
||||
put(key, value.toString())
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 JSONObject数据 到 缓存中
|
||||
*
|
||||
* @param key 保存的key
|
||||
* @param value 保存的JSONObject数据
|
||||
* @param saveTime 保存的时间,单位:秒
|
||||
*/
|
||||
fun put(key: String, value: JSONObject, saveTime: Int) {
|
||||
put(key, value.toString(), saveTime)
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取JSONObject数据
|
||||
*
|
||||
* @return JSONObject数据
|
||||
*/
|
||||
fun getAsJSONObject(key: String): JSONObject? {
|
||||
val json = getAsString(key) ?: return null
|
||||
return try {
|
||||
JSONObject(json)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// =======================================
|
||||
// ============ JSONArray 数据 读写 =============
|
||||
// =======================================
|
||||
|
||||
/**
|
||||
* 保存 JSONArray数据 到 缓存中
|
||||
*
|
||||
* @param key 保存的key
|
||||
* @param value 保存的JSONArray数据
|
||||
*/
|
||||
fun put(key: String, value: JSONArray) {
|
||||
put(key, value.toString())
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 JSONArray数据 到 缓存中
|
||||
*
|
||||
* @param key 保存的key
|
||||
* @param value 保存的JSONArray数据
|
||||
* @param saveTime 保存的时间,单位:秒
|
||||
*/
|
||||
fun put(key: String, value: JSONArray, saveTime: Int) {
|
||||
put(key, value.toString(), saveTime)
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取JSONArray数据
|
||||
*
|
||||
* @return JSONArray数据
|
||||
*/
|
||||
fun getAsJSONArray(key: String): JSONArray? {
|
||||
val json = getAsString(key)
|
||||
return try {
|
||||
JSONArray(json)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// =======================================
|
||||
// ============== byte 数据 读写 =============
|
||||
// =======================================
|
||||
|
||||
/**
|
||||
* 保存 byte数据 到 缓存中
|
||||
*
|
||||
* @param key 保存的key
|
||||
* @param value 保存的数据
|
||||
*/
|
||||
fun put(key: String, value: ByteArray) {
|
||||
mCache?.let { mCache ->
|
||||
val file = mCache.newFile(key)
|
||||
file.writeBytes(value)
|
||||
mCache.put(file)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 byte数据 到 缓存中
|
||||
*
|
||||
* @param key 保存的key
|
||||
* @param value 保存的数据
|
||||
* @param saveTime 保存的时间,单位:秒
|
||||
*/
|
||||
fun put(key: String, value: ByteArray, saveTime: Int) {
|
||||
if (saveTime == 0) put(key, value)
|
||||
else put(key, Utils.newByteArrayWithDateInfo(saveTime, value))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 byte 数据
|
||||
*
|
||||
* @return byte 数据
|
||||
*/
|
||||
fun getAsBinary(key: String): ByteArray? {
|
||||
mCache?.let { mCache ->
|
||||
var removeFile = false
|
||||
try {
|
||||
val file = mCache[key]
|
||||
if (!file.exists())
|
||||
return null
|
||||
|
||||
val byteArray = file.readBytes()
|
||||
return if (!Utils.isDue(byteArray)) {
|
||||
Utils.clearDateInfo(byteArray)
|
||||
} else {
|
||||
removeFile = true
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
} finally {
|
||||
if (removeFile)
|
||||
remove(key)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 Serializable数据到 缓存中
|
||||
*
|
||||
* @param key 保存的key
|
||||
* @param value 保存的value
|
||||
* @param saveTime 保存的时间,单位:秒
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun put(key: String, value: Serializable, saveTime: Int = -1) {
|
||||
try {
|
||||
val byteArrayOutputStream = ByteArrayOutputStream()
|
||||
ObjectOutputStream(byteArrayOutputStream).use { oos ->
|
||||
oos.writeObject(value)
|
||||
val data = byteArrayOutputStream.toByteArray()
|
||||
if (saveTime != -1) {
|
||||
put(key, data, saveTime)
|
||||
} else {
|
||||
put(key, data)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取 Serializable数据
|
||||
*
|
||||
* @return Serializable 数据
|
||||
*/
|
||||
fun getAsObject(key: String): Any? {
|
||||
val data = getAsBinary(key)
|
||||
if (data != null) {
|
||||
var bis: ByteArrayInputStream? = null
|
||||
var ois: ObjectInputStream? = null
|
||||
try {
|
||||
bis = ByteArrayInputStream(data)
|
||||
ois = ObjectInputStream(bis)
|
||||
return ois.readObject()
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
} finally {
|
||||
try {
|
||||
bis?.close()
|
||||
} catch (e: IOException) {
|
||||
e.printOnDebug()
|
||||
}
|
||||
|
||||
try {
|
||||
ois?.close()
|
||||
} catch (e: IOException) {
|
||||
e.printOnDebug()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return null
|
||||
|
||||
}
|
||||
|
||||
// =======================================
|
||||
// ============== bitmap 数据 读写 =============
|
||||
// =======================================
|
||||
|
||||
/**
|
||||
* 保存 bitmap 到 缓存中
|
||||
*
|
||||
* @param key 保存的key
|
||||
* @param value 保存的bitmap数据
|
||||
*/
|
||||
fun put(key: String, value: Bitmap) {
|
||||
put(key, Utils.bitmap2Bytes(value))
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 bitmap 到 缓存中
|
||||
*
|
||||
* @param key 保存的key
|
||||
* @param value 保存的 bitmap 数据
|
||||
* @param saveTime 保存的时间,单位:秒
|
||||
*/
|
||||
fun put(key: String, value: Bitmap, saveTime: Int) {
|
||||
put(key, Utils.bitmap2Bytes(value), saveTime)
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取 bitmap 数据
|
||||
*
|
||||
* @return bitmap 数据
|
||||
*/
|
||||
fun getAsBitmap(key: String): Bitmap? {
|
||||
return if (getAsBinary(key) == null) {
|
||||
null
|
||||
} else Utils.bytes2Bitmap(getAsBinary(key)!!)
|
||||
}
|
||||
|
||||
// =======================================
|
||||
// ============= drawable 数据 读写 =============
|
||||
// =======================================
|
||||
|
||||
/**
|
||||
* 保存 drawable 到 缓存中
|
||||
*
|
||||
* @param key 保存的key
|
||||
* @param value 保存的drawable数据
|
||||
*/
|
||||
fun put(key: String, value: Drawable) {
|
||||
put(key, Utils.drawable2Bitmap(value))
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 drawable 到 缓存中
|
||||
*
|
||||
* @param key 保存的key
|
||||
* @param value 保存的 drawable 数据
|
||||
* @param saveTime 保存的时间,单位:秒
|
||||
*/
|
||||
fun put(key: String, value: Drawable, saveTime: Int) {
|
||||
put(key, Utils.drawable2Bitmap(value), saveTime)
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取 Drawable 数据
|
||||
*
|
||||
* @return Drawable 数据
|
||||
*/
|
||||
fun getAsDrawable(key: String): Drawable? {
|
||||
return if (getAsBinary(key) == null) {
|
||||
null
|
||||
} else Utils.bitmap2Drawable(
|
||||
Utils.bytes2Bitmap(
|
||||
getAsBinary(key)!!
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存文件
|
||||
*
|
||||
* @return value 缓存的文件
|
||||
*/
|
||||
fun file(key: String): File? {
|
||||
mCache?.let { mCache ->
|
||||
try {
|
||||
val f = mCache.newFile(key)
|
||||
if (f.exists()) {
|
||||
return f
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除某个key
|
||||
*
|
||||
* @return 是否移除成功
|
||||
*/
|
||||
fun remove(key: String): Boolean {
|
||||
return mCache?.remove(key) == true
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有数据
|
||||
*/
|
||||
fun clear() {
|
||||
mCache?.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* @author 杨福海(michael) www.yangfuhai.com
|
||||
* @version 1.0
|
||||
* title 时间计算工具类
|
||||
*/
|
||||
private object Utils {
|
||||
|
||||
private const val mSeparator = ' '
|
||||
|
||||
/**
|
||||
* 判断缓存的String数据是否到期
|
||||
*
|
||||
* @return true:到期了 false:还没有到期
|
||||
*/
|
||||
fun isDue(str: String): Boolean {
|
||||
return isDue(str.toByteArray())
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断缓存的byte数据是否到期
|
||||
*
|
||||
* @return true:到期了 false:还没有到期
|
||||
*/
|
||||
fun isDue(data: ByteArray): Boolean {
|
||||
try {
|
||||
val text = getDateInfoFromDate(data)
|
||||
if (text != null && text.size == 2) {
|
||||
var saveTimeStr = text[0]
|
||||
while (saveTimeStr.startsWith("0")) {
|
||||
saveTimeStr = saveTimeStr
|
||||
.substring(1)
|
||||
}
|
||||
val saveTime = java.lang.Long.valueOf(saveTimeStr)
|
||||
val deleteAfter = java.lang.Long.valueOf(text[1])
|
||||
if (System.currentTimeMillis() > saveTime + deleteAfter * 1000) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
fun newStringWithDateInfo(second: Int, strInfo: String): String {
|
||||
return createDateInfo(second) + strInfo
|
||||
}
|
||||
|
||||
fun newByteArrayWithDateInfo(second: Int, data2: ByteArray): ByteArray {
|
||||
val data1 = createDateInfo(second).toByteArray()
|
||||
val retData = ByteArray(data1.size + data2.size)
|
||||
System.arraycopy(data1, 0, retData, 0, data1.size)
|
||||
System.arraycopy(data2, 0, retData, data1.size, data2.size)
|
||||
return retData
|
||||
}
|
||||
|
||||
fun clearDateInfo(strInfo: String?): String? {
|
||||
strInfo?.let {
|
||||
if (hasDateInfo(strInfo.toByteArray())) {
|
||||
return strInfo.substring(strInfo.indexOf(mSeparator) + 1)
|
||||
}
|
||||
}
|
||||
return strInfo
|
||||
}
|
||||
|
||||
fun clearDateInfo(data: ByteArray): ByteArray {
|
||||
return if (hasDateInfo(data)) {
|
||||
copyOfRange(
|
||||
data, indexOf(data, mSeparator) + 1,
|
||||
data.size
|
||||
)
|
||||
} else data
|
||||
}
|
||||
|
||||
fun hasDateInfo(data: ByteArray?): Boolean {
|
||||
return (data != null && data.size > 15 && data[13] == '-'.code.toByte()
|
||||
&& indexOf(data, mSeparator) > 14)
|
||||
}
|
||||
|
||||
fun getDateInfoFromDate(data: ByteArray): Array<String>? {
|
||||
if (hasDateInfo(data)) {
|
||||
val saveDate = String(copyOfRange(data, 0, 13))
|
||||
val deleteAfter = String(
|
||||
copyOfRange(
|
||||
data, 14,
|
||||
indexOf(data, mSeparator)
|
||||
)
|
||||
)
|
||||
return arrayOf(saveDate, deleteAfter)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@Suppress("SameParameterValue")
|
||||
private fun indexOf(data: ByteArray, c: Char): Int {
|
||||
for (i in data.indices) {
|
||||
if (data[i] == c.code.toByte()) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
private fun copyOfRange(original: ByteArray, from: Int, to: Int): ByteArray {
|
||||
val newLength = to - from
|
||||
require(newLength >= 0) { "$from > $to" }
|
||||
val copy = ByteArray(newLength)
|
||||
System.arraycopy(
|
||||
original, from, copy, 0,
|
||||
min(original.size - from, newLength)
|
||||
)
|
||||
return copy
|
||||
}
|
||||
|
||||
private fun createDateInfo(second: Int): String {
|
||||
val currentTime = StringBuilder(System.currentTimeMillis().toString() + "")
|
||||
while (currentTime.length < 13) {
|
||||
currentTime.insert(0, "0")
|
||||
}
|
||||
return "$currentTime-$second$mSeparator"
|
||||
}
|
||||
|
||||
/*
|
||||
* Bitmap → byte[]
|
||||
*/
|
||||
fun bitmap2Bytes(bm: Bitmap): ByteArray {
|
||||
val byteArrayOutputStream = ByteArrayOutputStream()
|
||||
bm.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)
|
||||
return byteArrayOutputStream.toByteArray()
|
||||
}
|
||||
|
||||
/*
|
||||
* byte[] → Bitmap
|
||||
*/
|
||||
fun bytes2Bitmap(b: ByteArray): Bitmap? {
|
||||
return if (b.isEmpty()) {
|
||||
null
|
||||
} else BitmapFactory.decodeByteArray(b, 0, b.size)
|
||||
}
|
||||
|
||||
/*
|
||||
* Drawable → Bitmap
|
||||
*/
|
||||
fun drawable2Bitmap(drawable: Drawable): Bitmap {
|
||||
// 取 drawable 的长宽
|
||||
val w = drawable.intrinsicWidth
|
||||
val h = drawable.intrinsicHeight
|
||||
// 取 drawable 的颜色格式
|
||||
@Suppress("DEPRECATION")
|
||||
val config = if (drawable.opacity != PixelFormat.OPAQUE)
|
||||
Bitmap.Config.ARGB_8888
|
||||
else
|
||||
Bitmap.Config.RGB_565
|
||||
// 建立对应 bitmap
|
||||
val bitmap = Bitmap.createBitmap(w, h, config)
|
||||
// 建立对应 bitmap 的画布
|
||||
val canvas = Canvas(bitmap)
|
||||
drawable.setBounds(0, 0, w, h)
|
||||
// 把 drawable 内容画到画布中
|
||||
drawable.draw(canvas)
|
||||
return bitmap
|
||||
}
|
||||
|
||||
/*
|
||||
* Bitmap → Drawable
|
||||
*/
|
||||
fun bitmap2Drawable(bm: Bitmap?): Drawable? {
|
||||
return if (bm == null) {
|
||||
null
|
||||
} else BitmapDrawable(appCtx.resources, bm)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @author 杨福海(michael) www.yangfuhai.com
|
||||
* @version 1.0
|
||||
* title 缓存管理器
|
||||
*/
|
||||
open inner class ACacheManager(
|
||||
private var cacheDir: File,
|
||||
private val sizeLimit: Long,
|
||||
private val countLimit: Int
|
||||
) {
|
||||
private val cacheSize: AtomicLong = AtomicLong()
|
||||
private val cacheCount: AtomicInteger = AtomicInteger()
|
||||
private val lastUsageDates = Collections
|
||||
.synchronizedMap(HashMap<File, Long>())
|
||||
|
||||
init {
|
||||
calculateCacheSizeAndCacheCount()
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算 cacheSize和cacheCount
|
||||
*/
|
||||
private fun calculateCacheSizeAndCacheCount() {
|
||||
Thread {
|
||||
|
||||
try {
|
||||
var size = 0
|
||||
var count = 0
|
||||
val cachedFiles = cacheDir.listFiles()
|
||||
if (cachedFiles != null) {
|
||||
for (cachedFile in cachedFiles) {
|
||||
size += calculateSize(cachedFile).toInt()
|
||||
count += 1
|
||||
lastUsageDates[cachedFile] = cachedFile.lastModified()
|
||||
}
|
||||
cacheSize.set(size.toLong())
|
||||
cacheCount.set(count)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
}
|
||||
|
||||
|
||||
}.start()
|
||||
}
|
||||
|
||||
fun put(file: File) {
|
||||
|
||||
try {
|
||||
var curCacheCount = cacheCount.get()
|
||||
while (curCacheCount + 1 > countLimit) {
|
||||
val freedSize = removeNext()
|
||||
cacheSize.addAndGet(-freedSize)
|
||||
|
||||
curCacheCount = cacheCount.addAndGet(-1)
|
||||
}
|
||||
cacheCount.addAndGet(1)
|
||||
|
||||
val valueSize = calculateSize(file)
|
||||
var curCacheSize = cacheSize.get()
|
||||
while (curCacheSize + valueSize > sizeLimit) {
|
||||
val freedSize = removeNext()
|
||||
curCacheSize = cacheSize.addAndGet(-freedSize)
|
||||
}
|
||||
cacheSize.addAndGet(valueSize)
|
||||
|
||||
val currentTime = System.currentTimeMillis()
|
||||
file.setLastModified(currentTime)
|
||||
lastUsageDates[file] = currentTime
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
operator fun get(key: String): File {
|
||||
val file = newFile(key)
|
||||
val currentTime = System.currentTimeMillis()
|
||||
file.setLastModified(currentTime)
|
||||
lastUsageDates[file] = currentTime
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
fun newFile(key: String): File {
|
||||
return File(cacheDir, key.hashCode().toString() + "")
|
||||
}
|
||||
|
||||
fun remove(key: String): Boolean {
|
||||
val image = get(key)
|
||||
return image.delete()
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
try {
|
||||
lastUsageDates.clear()
|
||||
cacheSize.set(0)
|
||||
val files = cacheDir.listFiles()
|
||||
if (files != null) {
|
||||
for (f in files) {
|
||||
f.delete()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除旧的文件
|
||||
*/
|
||||
private fun removeNext(): Long {
|
||||
try {
|
||||
if (lastUsageDates.isEmpty()) {
|
||||
return 0
|
||||
}
|
||||
|
||||
var oldestUsage: Long? = null
|
||||
var mostLongUsedFile: File? = null
|
||||
val entries = lastUsageDates.entries
|
||||
synchronized(lastUsageDates) {
|
||||
for ((key, lastValueUsage) in entries) {
|
||||
if (mostLongUsedFile == null) {
|
||||
mostLongUsedFile = key
|
||||
oldestUsage = lastValueUsage
|
||||
} else {
|
||||
if (lastValueUsage < oldestUsage!!) {
|
||||
oldestUsage = lastValueUsage
|
||||
mostLongUsedFile = key
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var fileSize: Long = 0
|
||||
if (mostLongUsedFile != null) {
|
||||
fileSize = calculateSize(mostLongUsedFile!!)
|
||||
if (mostLongUsedFile!!.delete()) {
|
||||
lastUsageDates.remove(mostLongUsedFile)
|
||||
}
|
||||
}
|
||||
return fileSize
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
return 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun calculateSize(file: File): Long {
|
||||
return file.length()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowInsets
|
||||
import android.view.WindowInsetsController
|
||||
import android.view.WindowManager
|
||||
import android.view.WindowMetrics
|
||||
import android.widget.FrameLayout
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.ui.widget.dialog.TextDialog
|
||||
|
||||
inline fun <reified T : DialogFragment> AppCompatActivity.showDialogFragment(
|
||||
arguments: Bundle.() -> Unit = {}
|
||||
) {
|
||||
@Suppress("DEPRECATION")
|
||||
val dialog = T::class.java.newInstance()
|
||||
val bundle = Bundle()
|
||||
bundle.apply(arguments)
|
||||
dialog.arguments = bundle
|
||||
dialog.show(supportFragmentManager, T::class.simpleName)
|
||||
}
|
||||
|
||||
inline fun <reified T : DialogFragment> AppCompatActivity.dismissDialogFragment() {
|
||||
supportFragmentManager.fragments.forEach {
|
||||
if (it is T) {
|
||||
it.dismissAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun AppCompatActivity.showDialogFragment(dialogFragment: DialogFragment) {
|
||||
dialogFragment.show(supportFragmentManager, dialogFragment::class.simpleName)
|
||||
}
|
||||
|
||||
val WindowManager.windowSize: DisplayMetrics
|
||||
get() {
|
||||
val displayMetrics = DisplayMetrics()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val windowMetrics: WindowMetrics = currentWindowMetrics
|
||||
val insets = windowMetrics.windowInsets
|
||||
.getInsetsIgnoringVisibility(
|
||||
WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout()
|
||||
)
|
||||
val windowWidth = windowMetrics.bounds.width()
|
||||
val windowHeight = windowMetrics.bounds.height()
|
||||
var insetsWidth = insets.left + insets.right
|
||||
var insetsHeight = insets.top + insets.bottom
|
||||
if (windowWidth > windowHeight) {
|
||||
val tmp = insetsWidth
|
||||
insetsWidth = insetsHeight
|
||||
insetsHeight = tmp
|
||||
}
|
||||
displayMetrics.widthPixels = windowWidth - insetsWidth
|
||||
displayMetrics.heightPixels = windowHeight - insetsHeight
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
defaultDisplay.getMetrics(displayMetrics)
|
||||
}
|
||||
return displayMetrics
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun Activity.fullScreen() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
window.setDecorFitsSystemWindows(true)
|
||||
}
|
||||
window.decorView.systemUiVisibility =
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
window.clearFlags(
|
||||
WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
|
||||
or WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION
|
||||
)
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置状态栏颜色
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
fun Activity.setStatusBarColorAuto(
|
||||
@ColorInt color: Int,
|
||||
isTransparent: Boolean,
|
||||
fullScreen: Boolean
|
||||
) {
|
||||
val isLightBar = ColorUtils.isColorLight(color)
|
||||
if (fullScreen) {
|
||||
if (isTransparent) {
|
||||
window.statusBarColor = Color.TRANSPARENT
|
||||
} else {
|
||||
window.statusBarColor = getCompatColor(R.color.status_bar_bag)
|
||||
}
|
||||
} else {
|
||||
window.statusBarColor = color
|
||||
}
|
||||
setLightStatusBar(isLightBar)
|
||||
}
|
||||
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
fun Activity.setLightStatusBar(isLightBar: Boolean) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
window.insetsController?.let {
|
||||
if (isLightBar) {
|
||||
it.setSystemBarsAppearance(
|
||||
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS,
|
||||
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
|
||||
)
|
||||
} else {
|
||||
it.setSystemBarsAppearance(
|
||||
0,
|
||||
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@Suppress("DEPRECATION")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
val decorView = window.decorView
|
||||
val systemUiVisibility = decorView.systemUiVisibility
|
||||
if (isLightBar) {
|
||||
decorView.systemUiVisibility =
|
||||
systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
|
||||
} else {
|
||||
decorView.systemUiVisibility =
|
||||
systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置导航栏颜色
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
fun Activity.setNavigationBarColorAuto(@ColorInt color: Int) {
|
||||
val isLightBor = ColorUtils.isColorLight(color)
|
||||
window.navigationBarColor = color
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
window.insetsController?.let {
|
||||
if (isLightBor) {
|
||||
it.setSystemBarsAppearance(
|
||||
WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS,
|
||||
WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS
|
||||
)
|
||||
} else {
|
||||
it.setSystemBarsAppearance(
|
||||
0,
|
||||
WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@Suppress("DEPRECATION")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val decorView = window.decorView
|
||||
var systemUiVisibility = decorView.systemUiVisibility
|
||||
systemUiVisibility = if (isLightBor) {
|
||||
systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
|
||||
} else {
|
||||
systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv()
|
||||
}
|
||||
decorView.systemUiVisibility = systemUiVisibility
|
||||
}
|
||||
}
|
||||
|
||||
fun Activity.keepScreenOn(on: Boolean) {
|
||||
val isScreenOn =
|
||||
(window.attributes.flags and WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) != 0
|
||||
if (on == isScreenOn) return
|
||||
if (on) {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
} else {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
}
|
||||
}
|
||||
|
||||
fun Activity.toggleSystemBar(show: Boolean) {
|
||||
WindowCompat.getInsetsController(window, window.decorView).run {
|
||||
if (show) {
|
||||
show(WindowInsetsCompat.Type.systemBars())
|
||||
} else {
|
||||
hide(WindowInsetsCompat.Type.systemBars())
|
||||
systemBarsBehavior = BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/////以下方法需要在View完全被绘制出来之后调用,否则判断不了,在比如 onWindowFocusChanged()方法中可以得到正确的结果/////
|
||||
|
||||
/**
|
||||
* 返回NavigationBar
|
||||
*/
|
||||
val Activity.navigationBar: View?
|
||||
get() {
|
||||
val viewGroup = (window.decorView as? ViewGroup) ?: return null
|
||||
for (i in 0 until viewGroup.childCount) {
|
||||
val child = viewGroup.getChildAt(i)
|
||||
val childId = child.id
|
||||
if (childId != View.NO_ID
|
||||
&& resources.getResourceEntryName(childId) == "navigationBarBackground"
|
||||
) {
|
||||
return child
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回NavigationBar是否存在
|
||||
*/
|
||||
val Activity.isNavigationBarExist: Boolean
|
||||
get() = navigationBar != null
|
||||
|
||||
/**
|
||||
* 返回NavigationBar高度
|
||||
*/
|
||||
val Activity.navigationBarHeight: Int
|
||||
@SuppressLint("InternalInsetResource", "DiscouragedApi")
|
||||
get() {
|
||||
if (isNavigationBarExist) {
|
||||
val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")
|
||||
return resources.getDimensionPixelSize(resourceId)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回navigationBar位置
|
||||
*/
|
||||
val Activity.navigationBarGravity: Int
|
||||
get() {
|
||||
val gravity = (navigationBar?.layoutParams as? FrameLayout.LayoutParams)?.gravity
|
||||
return gravity ?: Gravity.BOTTOM
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示目录help下的帮助文档
|
||||
*/
|
||||
fun AppCompatActivity.showHelp(fileName: String) {
|
||||
val mdText = String(assets.open("web/help/md/${fileName}.md").readBytes())
|
||||
showDialogFragment(TextDialog(getString(R.string.help), mdText, TextDialog.Mode.MD))
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import kotlinx.coroutines.CancellableContinuation
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
fun <I, O> AppCompatActivity.registerForActivityResult(contract: ActivityResultContract<I, O>): ActivityResultLauncherAwait<I, O> {
|
||||
lateinit var cout: CancellableContinuation<O>
|
||||
val launcher = registerForActivityResult(contract) {
|
||||
if (cout.isActive) {
|
||||
cout.resume(it)
|
||||
}
|
||||
}
|
||||
return object : ActivityResultLauncherAwait<I, O>() {
|
||||
override suspend fun launch(input: I, options: ActivityOptionsCompat?): O {
|
||||
return suspendCancellableCoroutine {
|
||||
cout = it
|
||||
launcher.launch(input, options)
|
||||
}
|
||||
}
|
||||
|
||||
override fun unregister() {
|
||||
launcher.unregister()
|
||||
}
|
||||
|
||||
override fun getContract(): ActivityResultContract<I, *> {
|
||||
return launcher.contract
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class ActivityResultLauncherAwait<I, O> {
|
||||
|
||||
suspend fun launch(input: I): O {
|
||||
return launch(input, null)
|
||||
}
|
||||
|
||||
abstract suspend fun launch(input: I, options: ActivityOptionsCompat?): O
|
||||
|
||||
abstract fun unregister()
|
||||
|
||||
abstract fun getContract(): ActivityResultContract<I, *>
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.PickVisualMediaRequest
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import splitties.init.appCtx
|
||||
|
||||
fun <T> ActivityResultLauncher<T?>.launch() {
|
||||
launch(null)
|
||||
}
|
||||
|
||||
class SelectImageContract : ActivityResultContract<Int?, SelectImageContract.Result>() {
|
||||
|
||||
private val delegate = ActivityResultContracts.PickVisualMedia()
|
||||
private var requestCode: Int? = null
|
||||
private var useFallback = false
|
||||
|
||||
override fun createIntent(context: Context, input: Int?): Intent {
|
||||
requestCode = input
|
||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
||||
.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
.setType("image/*")
|
||||
if (intent.resolveActivity(appCtx.packageManager) == null) {
|
||||
useFallback = true
|
||||
val request = PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
|
||||
return delegate.createIntent(context, request)
|
||||
}
|
||||
return intent
|
||||
}
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?): Result {
|
||||
val uri = if (useFallback) {
|
||||
delegate.parseResult(resultCode, intent)
|
||||
} else if (resultCode == RESULT_OK) {
|
||||
intent?.data
|
||||
} else {
|
||||
null
|
||||
}
|
||||
return Result(requestCode, uri)
|
||||
}
|
||||
|
||||
data class Result(
|
||||
val requestCode: Int?,
|
||||
val uri: Uri? = null
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
class StartActivityContract(private val cls: Class<*>) :
|
||||
ActivityResultContract<(Intent.() -> Unit)?, ActivityResult>() {
|
||||
|
||||
override fun createIntent(context: Context, input: (Intent.() -> Unit)?): Intent {
|
||||
val intent = Intent(context, cls)
|
||||
input?.let {
|
||||
intent.apply(input)
|
||||
}
|
||||
return intent
|
||||
}
|
||||
|
||||
override fun parseResult(
|
||||
resultCode: Int, intent: Intent?
|
||||
): ActivityResult {
|
||||
return ActivityResult(resultCode, intent)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
|
||||
/**
|
||||
* 排序比较
|
||||
*/
|
||||
object AlphanumComparator : Comparator<String> {
|
||||
|
||||
|
||||
override fun compare(s1: String, s2: String): Int {
|
||||
var thisMarker = 0
|
||||
var thatMarker = 0
|
||||
val s1Length = s1.length
|
||||
val s2Length = s2.length
|
||||
|
||||
while (thisMarker < s1Length && thatMarker < s2Length) {
|
||||
val thisChunk = getChunk(s1, s1Length, thisMarker)
|
||||
thisMarker += thisChunk.length
|
||||
|
||||
val thatChunk = getChunk(s2, s2Length, thatMarker)
|
||||
thatMarker += thatChunk.length
|
||||
|
||||
// If both chunks contain numeric characters, sort them numerically.
|
||||
var result: Int
|
||||
if (isDigit(thisChunk[0]) && isDigit(thatChunk[0])) {
|
||||
// Simple chunk comparison by length.
|
||||
val thisChunkLength = thisChunk.length
|
||||
result = thisChunkLength - thatChunk.length
|
||||
// If equal, the first different number counts.
|
||||
if (result == 0) {
|
||||
for (i in 0 until thisChunkLength) {
|
||||
result = thisChunk[i] - thatChunk[i]
|
||||
if (result != 0) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result = thisChunk.compareTo(thatChunk)
|
||||
}
|
||||
|
||||
if (result != 0) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return s1Length - s2Length
|
||||
}
|
||||
|
||||
private fun getChunk(string: String, length: Int, marker: Int): String {
|
||||
var current = marker
|
||||
val chunk = StringBuilder()
|
||||
var c = string[current]
|
||||
chunk.append(c)
|
||||
current++
|
||||
if (isDigit(c)) {
|
||||
while (current < length) {
|
||||
c = string[current]
|
||||
if (!isDigit(c)) {
|
||||
break
|
||||
}
|
||||
chunk.append(c)
|
||||
current++
|
||||
}
|
||||
} else {
|
||||
while (current < length) {
|
||||
c = string[current]
|
||||
if (isDigit(c)) {
|
||||
break
|
||||
}
|
||||
chunk.append(c)
|
||||
current++
|
||||
}
|
||||
}
|
||||
return chunk.toString()
|
||||
}
|
||||
|
||||
private fun isDigit(ch: Char): Boolean {
|
||||
return ch in '0'..'9'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.AnimationUtils
|
||||
import androidx.annotation.AnimRes
|
||||
import io.legato.kazusa.help.config.AppConfig
|
||||
|
||||
fun loadAnimation(context: Context, @AnimRes id: Int): Animation {
|
||||
val animation = AnimationUtils.loadAnimation(context, id)
|
||||
if (AppConfig.isEInkMode) {
|
||||
animation.duration = 0
|
||||
}
|
||||
return animation
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import io.legato.kazusa.constant.AppPattern.archiveFileRegex
|
||||
import io.legato.kazusa.utils.compress.LibArchiveUtils
|
||||
import splitties.init.appCtx
|
||||
import java.io.File
|
||||
|
||||
/* 自动判断压缩文件后缀 然后再调用具体的实现 */
|
||||
@Suppress("unused", "MemberVisibilityCanBePrivate")
|
||||
object ArchiveUtils {
|
||||
|
||||
const val TEMP_FOLDER_NAME = "ArchiveTemp"
|
||||
|
||||
// 临时目录 下次启动自动删除
|
||||
val TEMP_PATH: String by lazy {
|
||||
appCtx.externalCache.getFile(TEMP_FOLDER_NAME).createFolderReplace().absolutePath
|
||||
}
|
||||
|
||||
fun deCompress(
|
||||
archiveUri: Uri,
|
||||
path: String = TEMP_PATH,
|
||||
filter: ((String) -> Boolean)? = null
|
||||
): List<File> {
|
||||
return deCompress(FileDoc.fromUri(archiveUri, false), path, filter)
|
||||
}
|
||||
|
||||
fun deCompress(
|
||||
archivePath: String,
|
||||
path: String = TEMP_PATH,
|
||||
filter: ((String) -> Boolean)? = null
|
||||
): List<File> {
|
||||
return deCompress(Uri.parse(archivePath), path, filter)
|
||||
}
|
||||
|
||||
fun deCompress(
|
||||
archiveFile: File,
|
||||
path: String = TEMP_PATH,
|
||||
filter: ((String) -> Boolean)? = null
|
||||
): List<File> {
|
||||
return deCompress(FileDoc.fromFile(archiveFile), path, filter)
|
||||
}
|
||||
|
||||
fun deCompress(
|
||||
archiveDoc: DocumentFile,
|
||||
path: String = TEMP_PATH,
|
||||
filter: ((String) -> Boolean)? = null
|
||||
): List<File> {
|
||||
return deCompress(FileDoc.fromDocumentFile(archiveDoc), path, filter)
|
||||
}
|
||||
|
||||
fun deCompress(
|
||||
archiveFileDoc: FileDoc,
|
||||
path: String = TEMP_PATH,
|
||||
filter: ((String) -> Boolean)? = null
|
||||
): List<File> {
|
||||
if (archiveFileDoc.isDir) throw IllegalArgumentException("Unexpected Folder input")
|
||||
val name = archiveFileDoc.name
|
||||
checkAchieve(name)
|
||||
val workPathFileDoc = getCacheFolderFileDoc(name, path)
|
||||
val workPath = workPathFileDoc.toString()
|
||||
|
||||
return archiveFileDoc.openReadPfd().getOrThrow().use {
|
||||
LibArchiveUtils.unArchive(it, File(workPath), filter)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* 遍历目录获取文件名 */
|
||||
fun getArchiveFilesName(fileUri: Uri, filter: ((String) -> Boolean)? = null): List<String> =
|
||||
getArchiveFilesName(FileDoc.fromUri(fileUri, false), filter)
|
||||
|
||||
|
||||
fun getArchiveFilesName(
|
||||
fileDoc: FileDoc,
|
||||
filter: ((String) -> Boolean)? = null
|
||||
): List<String> {
|
||||
val name = fileDoc.name
|
||||
checkAchieve(name)
|
||||
|
||||
return fileDoc.openReadPfd().getOrThrow().use {
|
||||
try {
|
||||
LibArchiveUtils.getFilesName(it, filter)
|
||||
} catch (e: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
fun isArchive(name: String): Boolean {
|
||||
return archiveFileRegex.matches(name)
|
||||
}
|
||||
|
||||
private fun checkAchieve(name: String) {
|
||||
if (!isArchive(name))
|
||||
throw IllegalArgumentException("Unexpected file suffix: Only 7z rar zip Accepted")
|
||||
}
|
||||
|
||||
private fun getCacheFolderFileDoc(
|
||||
archiveName: String,
|
||||
workPath: String
|
||||
): FileDoc {
|
||||
return FileDoc.fromUri(Uri.parse(workPath), true)
|
||||
.createFolderIfNotExist(MD5Utils.md5Encode16(archiveName))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import io.legato.kazusa.help.globalExecutor
|
||||
import java.util.logging.FileHandler
|
||||
import java.util.logging.LogRecord
|
||||
|
||||
class AsyncFileHandler(pattern: String) : FileHandler(pattern) {
|
||||
|
||||
override fun publish(record: LogRecord?) {
|
||||
if (!isLoggable(record)) {
|
||||
return
|
||||
}
|
||||
globalExecutor.execute {
|
||||
super.publish(record)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Bitmap.Config
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Color
|
||||
import com.google.android.renderscript.Toolkit
|
||||
import java.io.*
|
||||
import kotlin.math.*
|
||||
|
||||
|
||||
@Suppress("WeakerAccess", "MemberVisibilityCanBePrivate")
|
||||
object BitmapUtils {
|
||||
|
||||
/**
|
||||
* 从path中获取图片信息,在通过BitmapFactory.decodeFile(String path)方法将突破转成Bitmap时,
|
||||
* 遇到大一些的图片,我们经常会遇到OOM(Out Of Memory)的问题。所以用到了我们上面提到的BitmapFactory.Options这个类。
|
||||
*
|
||||
* @param path 文件路径
|
||||
* @param width 想要显示的图片的宽度
|
||||
* @param height 想要显示的图片的高度
|
||||
* @return
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun decodeBitmap(path: String, width: Int, height: Int? = null): Bitmap? {
|
||||
val fis = FileInputStream(path)
|
||||
return fis.use {
|
||||
val op = BitmapFactory.Options()
|
||||
// inJustDecodeBounds如果设置为true,仅仅返回图片实际的宽和高,宽和高是赋值给opts.outWidth,opts.outHeight;
|
||||
op.inJustDecodeBounds = true
|
||||
BitmapFactory.decodeFileDescriptor(fis.fd, null, op)
|
||||
op.inSampleSize = calculateInSampleSize(op, width, height)
|
||||
op.inJustDecodeBounds = false
|
||||
BitmapFactory.decodeFileDescriptor(fis.fd, null, op)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*计算 InSampleSize。缺省返回1
|
||||
* @param options BitmapFactory.Options,
|
||||
* @param width 想要显示的图片的宽度
|
||||
* @param height 想要显示的图片的高度
|
||||
* @return
|
||||
*/
|
||||
private fun calculateInSampleSize(
|
||||
options: BitmapFactory.Options,
|
||||
width: Int? = null,
|
||||
height: Int? = null
|
||||
): Int {
|
||||
//获取比例大小
|
||||
val wRatio = width?.let { options.outWidth / it } ?: -1
|
||||
val hRatio = height?.let { options.outHeight / it } ?: -1
|
||||
//如果超出指定大小,则缩小相应的比例
|
||||
return when {
|
||||
wRatio > 1 && hRatio > 1 -> max(wRatio, hRatio)
|
||||
wRatio > 1 -> wRatio
|
||||
hRatio > 1 -> hRatio
|
||||
else -> 1
|
||||
}
|
||||
}
|
||||
|
||||
/** 从path中获取Bitmap图片
|
||||
* @param path 图片路径
|
||||
* @return
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun decodeBitmap(path: String): Bitmap? {
|
||||
val fis = FileInputStream(path)
|
||||
return fis.use {
|
||||
val opts = BitmapFactory.Options()
|
||||
opts.inJustDecodeBounds = true
|
||||
|
||||
BitmapFactory.decodeFileDescriptor(fis.fd, null, opts)
|
||||
opts.inSampleSize = computeSampleSize(opts, -1, 128 * 128)
|
||||
opts.inJustDecodeBounds = false
|
||||
BitmapFactory.decodeFileDescriptor(fis.fd, null, opts)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 以最省内存的方式读取本地资源的图片
|
||||
* @param context 设备上下文
|
||||
* @param resId 资源ID
|
||||
* @return
|
||||
*/
|
||||
fun decodeBitmap(context: Context, resId: Int): Bitmap? {
|
||||
val opt = BitmapFactory.Options()
|
||||
opt.inPreferredConfig = Config.RGB_565
|
||||
return BitmapFactory.decodeResource(context.resources, resId, opt)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param context 设备上下文
|
||||
* @param resId 资源ID
|
||||
* @param width
|
||||
* @param height
|
||||
* @return
|
||||
*/
|
||||
fun decodeBitmap(context: Context, resId: Int, width: Int, height: Int): Bitmap? {
|
||||
val op = BitmapFactory.Options()
|
||||
// inJustDecodeBounds如果设置为true,仅仅返回图片实际的宽和高,宽和高是赋值给opts.outWidth,opts.outHeight;
|
||||
op.inJustDecodeBounds = true
|
||||
BitmapFactory.decodeResource(context.resources, resId, op) //获取尺寸信息
|
||||
op.inSampleSize = calculateInSampleSize(op, width, height)
|
||||
op.inJustDecodeBounds = false
|
||||
return BitmapFactory.decodeResource(context.resources, resId, op)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param context 设备上下文
|
||||
* @param fileNameInAssets Assets里面文件的名称
|
||||
* @param width 图片的宽度
|
||||
* @param height 图片的高度
|
||||
* @return Bitmap
|
||||
* @throws IOException
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun decodeAssetsBitmap(
|
||||
context: Context,
|
||||
fileNameInAssets: String,
|
||||
width: Int,
|
||||
height: Int
|
||||
): Bitmap? {
|
||||
var inputStream = context.assets.open(fileNameInAssets)
|
||||
return inputStream.use {
|
||||
val op = BitmapFactory.Options()
|
||||
// inJustDecodeBounds如果设置为true,仅仅返回图片实际的宽和高,宽和高是赋值给opts.outWidth,opts.outHeight;
|
||||
op.inJustDecodeBounds = true
|
||||
BitmapFactory.decodeStream(inputStream, null, op) //获取尺寸信息
|
||||
op.inSampleSize = calculateInSampleSize(op, width, height)
|
||||
inputStream = context.assets.open(fileNameInAssets)
|
||||
op.inJustDecodeBounds = false
|
||||
BitmapFactory.decodeStream(inputStream, null, op)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param options
|
||||
* @param minSideLength
|
||||
* @param maxNumOfPixels
|
||||
* @return
|
||||
* 设置恰当的inSampleSize是解决该问题的关键之一。BitmapFactory.Options提供了另一个成员inJustDecodeBounds。
|
||||
* 设置inJustDecodeBounds为true后,decodeFile并不分配空间,但可计算出原始图片的长度和宽度,即opts.width和opts.height。
|
||||
* 有了这两个参数,再通过一定的算法,即可得到一个恰当的inSampleSize。
|
||||
* 查看Android源码,Android提供了下面这种动态计算的方法。
|
||||
*/
|
||||
fun computeSampleSize(
|
||||
options: BitmapFactory.Options,
|
||||
minSideLength: Int,
|
||||
maxNumOfPixels: Int
|
||||
): Int {
|
||||
val initialSize = computeInitialSampleSize(options, minSideLength, maxNumOfPixels)
|
||||
var roundedSize: Int
|
||||
if (initialSize <= 8) {
|
||||
roundedSize = 1
|
||||
while (roundedSize < initialSize) {
|
||||
roundedSize = roundedSize shl 1
|
||||
}
|
||||
} else {
|
||||
roundedSize = (initialSize + 7) / 8 * 8
|
||||
}
|
||||
return roundedSize
|
||||
}
|
||||
|
||||
|
||||
private fun computeInitialSampleSize(
|
||||
options: BitmapFactory.Options,
|
||||
minSideLength: Int,
|
||||
maxNumOfPixels: Int
|
||||
): Int {
|
||||
|
||||
val w = options.outWidth.toDouble()
|
||||
val h = options.outHeight.toDouble()
|
||||
|
||||
val lowerBound = when (maxNumOfPixels) {
|
||||
-1 -> 1
|
||||
else -> ceil(sqrt(w * h / maxNumOfPixels)).toInt()
|
||||
}
|
||||
|
||||
val upperBound = when (minSideLength) {
|
||||
-1 -> 128
|
||||
else -> min(
|
||||
floor(w / minSideLength),
|
||||
floor(h / minSideLength)
|
||||
).toInt()
|
||||
}
|
||||
|
||||
if (upperBound < lowerBound) {
|
||||
// return the larger one when there is no overlapping zone.
|
||||
return lowerBound
|
||||
}
|
||||
|
||||
return when {
|
||||
maxNumOfPixels == -1 && minSideLength == -1 -> {
|
||||
1
|
||||
}
|
||||
minSideLength == -1 -> {
|
||||
lowerBound
|
||||
}
|
||||
else -> {
|
||||
upperBound
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将Bitmap转换成InputStream
|
||||
*
|
||||
* @param bitmap
|
||||
* @return
|
||||
*/
|
||||
fun toInputStream(bitmap: Bitmap): InputStream {
|
||||
val bos = ByteArrayOutputStream()
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 90 /*ignored for PNG*/, bos)
|
||||
return ByteArrayInputStream(bos.toByteArray()).also { bos.close() }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定宽高的图片
|
||||
*/
|
||||
fun Bitmap.resizeAndRecycle(newWidth: Int, newHeight: Int): Bitmap {
|
||||
//获取新的bitmap
|
||||
val bitmap = Toolkit.resize(this, newWidth, newHeight)
|
||||
recycle()
|
||||
return bitmap
|
||||
}
|
||||
|
||||
/**
|
||||
* 高斯模糊
|
||||
*/
|
||||
fun Bitmap.stackBlur(radius: Int = 8): Bitmap {
|
||||
return Toolkit.blur(this, radius)
|
||||
}
|
||||
|
||||
/**
|
||||
* 取平均色
|
||||
*/
|
||||
fun Bitmap.getMeanColor(): Int {
|
||||
val width: Int = this.width
|
||||
val height: Int = this.height
|
||||
var pixel: Int
|
||||
var pixelSumRed = 0
|
||||
var pixelSumBlue = 0
|
||||
var pixelSumGreen = 0
|
||||
for (i in 0..99) {
|
||||
for (j in 70..99) {
|
||||
pixel = this.getPixel(
|
||||
(i * width / 100.toFloat()).roundToInt(),
|
||||
(j * height / 100.toFloat()).roundToInt()
|
||||
)
|
||||
pixelSumRed += Color.red(pixel)
|
||||
pixelSumGreen += Color.green(pixel)
|
||||
pixelSumBlue += Color.blue(pixel)
|
||||
}
|
||||
}
|
||||
val averagePixelRed = pixelSumRed / 3000
|
||||
val averagePixelBlue = pixelSumBlue / 3000
|
||||
val averagePixelGreen = pixelSumGreen / 3000
|
||||
return Color.rgb(
|
||||
averagePixelRed + 3,
|
||||
averagePixelGreen + 3,
|
||||
averagePixelBlue + 3
|
||||
)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import io.legato.kazusa.data.entities.BookChapter
|
||||
|
||||
fun BookChapter.internString() {
|
||||
title = title.intern()
|
||||
bookUrl = bookUrl.intern()
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
|
||||
/**
|
||||
* Search the data byte array for the first occurrence
|
||||
* of the byte array pattern.
|
||||
*/
|
||||
fun ByteArray.indexOf(pattern: ByteArray, start: Int = 0, stop: Int = size): Int {
|
||||
val data = this
|
||||
val failure: IntArray = computeFailure(pattern)
|
||||
|
||||
var j = 0
|
||||
|
||||
for (i in start until stop) {
|
||||
while (j > 0 && pattern[j] != data[i]) {
|
||||
j = failure[j - 1]
|
||||
}
|
||||
if (pattern[j] == data[i]) {
|
||||
j++
|
||||
}
|
||||
if (j == pattern.size) {
|
||||
return i - pattern.size + 1
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the failure function using a boot-strapping process,
|
||||
* where the pattern is matched against itself.
|
||||
*/
|
||||
private fun computeFailure(pattern: ByteArray): IntArray {
|
||||
val failure = IntArray(pattern.size)
|
||||
var j = 0
|
||||
for (i in 1 until pattern.size) {
|
||||
while (j > 0 && pattern[j] != pattern[i]) {
|
||||
j = failure[j - 1]
|
||||
}
|
||||
if (pattern[j] == pattern[i]) {
|
||||
j++
|
||||
}
|
||||
failure[i] = j
|
||||
}
|
||||
return failure
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import com.github.liuyueyi.quick.transfer.ChineseUtils
|
||||
import com.github.liuyueyi.quick.transfer.constants.TransType
|
||||
|
||||
object ChineseUtils {
|
||||
|
||||
private var fixed = false
|
||||
|
||||
fun s2t(content: String): String {
|
||||
return ChineseUtils.s2t(content)
|
||||
}
|
||||
|
||||
fun t2s(content: String): String {
|
||||
if (!fixed) {
|
||||
fixT2sDict()
|
||||
}
|
||||
return ChineseUtils.t2s(content)
|
||||
}
|
||||
|
||||
fun preLoad(async: Boolean, vararg transType: TransType) {
|
||||
ChineseUtils.preLoad(async, *transType)
|
||||
}
|
||||
|
||||
fun unLoad(vararg transType: TransType) {
|
||||
ChineseUtils.unLoad(*transType)
|
||||
}
|
||||
|
||||
fun fixT2sDict() {
|
||||
fixed = true
|
||||
val excludeList = listOf(
|
||||
"槃",
|
||||
"划槳", "列根", "雪梨", "雪糕", "多士", "起司", "芝士", "沙芬", "母音",
|
||||
"华乐", "民乐", "晶元", "晶片", "映像", "明覆", "明瞭", "新力", "新喻",
|
||||
"零錢", "零钱", "離線", "碟片", "模組", "桌球", "案頭", "機車", "電漿",
|
||||
"鳳梨", "魔戒", "載入", "菲林", "整合", "變數", "解碼", "散钱", "插水",
|
||||
"房屋", "房价", "快取", "德士", "建立", "常式", "席丹", "布殊", "布希",
|
||||
"巴哈", "巨集", "夜学", "向量", "半形", "加彭", "列印", "函式", "全形",
|
||||
"光碟", "介面", "乳酪", "沈船", "永珍", "演化", "牛油", "相容", "磁碟",
|
||||
"菲林", "規則", "酵素", "雷根", "饭盒",
|
||||
"路易斯", "非同步", "出租车", "周杰倫", "马铃薯", "馬鈴薯", "機械人", "電單車",
|
||||
"電扶梯", "音效卡", "飆車族", "點陣圖", "個入球", "顆進球", "沃尓沃", "晶片集",
|
||||
"斯瓦巴", "斜角巷", "战列舰", "快速面", "希特拉", "太空梭", "吐瓦魯", "吉布堤",
|
||||
"吉布地", "史太林", "南冰洋", "区域网", "波札那", "解析度", "酷洛米", "金夏沙",
|
||||
"魔獸紀元", "高空彈跳", "铁达尼号", "太空战士", "埃及妖后", "吉里巴斯", "附加元件",
|
||||
"魔鬼終結者", "純文字檔案", "奇幻魔法Melody", "列支敦斯登"
|
||||
)
|
||||
ChineseUtils.loadExcludeDict(TransType.TRADITIONAL_TO_SIMPLE, excludeList)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
fun List<Float>.fastSum(): Float {
|
||||
var sum = 0f
|
||||
for (i in indices) {
|
||||
sum += this[i]
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
inline fun <T> List<T>.fastBinarySearch(
|
||||
fromIndex: Int = 0,
|
||||
toIndex: Int = size,
|
||||
comparison: (T) -> Int
|
||||
): Int {
|
||||
when {
|
||||
fromIndex > toIndex ->
|
||||
throw IllegalArgumentException(
|
||||
"fromIndex ($fromIndex) is greater than toIndex ($toIndex)."
|
||||
)
|
||||
|
||||
fromIndex < 0 ->
|
||||
throw IndexOutOfBoundsException("fromIndex ($fromIndex) is less than zero.")
|
||||
|
||||
toIndex > size ->
|
||||
throw IndexOutOfBoundsException("toIndex ($toIndex) is greater than size ($size).")
|
||||
}
|
||||
|
||||
var low = fromIndex
|
||||
var high = toIndex - 1
|
||||
|
||||
while (low <= high) {
|
||||
val mid = (low + high).ushr(1) // safe from overflows
|
||||
val midVal = get(mid)
|
||||
val cmp = comparison(midVal)
|
||||
|
||||
if (cmp < 0)
|
||||
low = mid + 1
|
||||
else if (cmp > 0)
|
||||
high = mid - 1
|
||||
else
|
||||
return mid // key found
|
||||
}
|
||||
return -(low + 1) // key not found
|
||||
}
|
||||
|
||||
inline fun <T, K : Comparable<K>> List<T>.fastBinarySearchBy(
|
||||
key: K?,
|
||||
fromIndex: Int = 0,
|
||||
toIndex: Int = size,
|
||||
crossinline selector: (T) -> K?
|
||||
): Int = fastBinarySearch(fromIndex, toIndex) { compareValues(selector(it), key) }
|
||||
|
||||
fun <T> MutableList<T>.removeLastElement(): T {
|
||||
return if (isEmpty()) {
|
||||
throw NoSuchElementException("List is empty.")
|
||||
} else {
|
||||
removeAt(lastIndex)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.graphics.Color
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.sqrt
|
||||
|
||||
@Suppress("unused", "MemberVisibilityCanBePrivate")
|
||||
object ColorUtils {
|
||||
|
||||
fun isColorLight(@ColorInt color: Int): Boolean {
|
||||
return ColorUtils.calculateLuminance(color) >= 0.5
|
||||
}
|
||||
|
||||
fun intToString(intColor: Int): String {
|
||||
return String.format("#%06X", 0xFFFFFF and intColor)
|
||||
}
|
||||
|
||||
fun stripAlpha(@ColorInt color: Int): Int {
|
||||
return -0x1000000 or color
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun shiftColor(@ColorInt color: Int, @FloatRange(from = 0.0, to = 2.0) by: Float): Int {
|
||||
if (by == 1f) return color
|
||||
val alpha = Color.alpha(color)
|
||||
val hsv = FloatArray(3)
|
||||
Color.colorToHSV(color, hsv)
|
||||
hsv[2] *= by // value component
|
||||
return (alpha shl 24) + (0x00ffffff and Color.HSVToColor(hsv))
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun darkenColor(@ColorInt color: Int): Int {
|
||||
return shiftColor(color, 0.9f)
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun lightenColor(@ColorInt color: Int): Int {
|
||||
return shiftColor(color, 1.1f)
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun invertColor(@ColorInt color: Int): Int {
|
||||
val r = 255 - Color.red(color)
|
||||
val g = 255 - Color.green(color)
|
||||
val b = 255 - Color.blue(color)
|
||||
return Color.argb(Color.alpha(color), r, g, b)
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun adjustAlpha(@ColorInt color: Int, @FloatRange(from = 0.0, to = 1.0) factor: Float): Int {
|
||||
val alpha = (Color.alpha(color) * factor).roundToInt()
|
||||
val red = Color.red(color)
|
||||
val green = Color.green(color)
|
||||
val blue = Color.blue(color)
|
||||
return Color.argb(alpha, red, green, blue)
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun withAlpha(@ColorInt baseColor: Int, @FloatRange(from = 0.0, to = 1.0) alpha: Float): Int {
|
||||
val a = min(255, max(0, (alpha * 255).toInt())) shl 24
|
||||
val rgb = 0x00ffffff and baseColor
|
||||
return a + rgb
|
||||
}
|
||||
|
||||
/**
|
||||
* Taken from CollapsingToolbarLayout's CollapsingTextHelper class.
|
||||
*/
|
||||
fun blendColors(color1: Int, color2: Int, @FloatRange(from = 0.0, to = 1.0) ratio: Float): Int {
|
||||
val inverseRatio = 1f - ratio
|
||||
val a = Color.alpha(color1) * inverseRatio + Color.alpha(color2) * ratio
|
||||
val r = Color.red(color1) * inverseRatio + Color.red(color2) * ratio
|
||||
val g = Color.green(color1) * inverseRatio + Color.green(color2) * ratio
|
||||
val b = Color.blue(color1) * inverseRatio + Color.blue(color2) * ratio
|
||||
return Color.argb(a.toInt(), r.toInt(), g.toInt(), b.toInt())
|
||||
}
|
||||
|
||||
fun argb(r: Int, g: Int, b: Int): Int {
|
||||
return argb(Byte.MAX_VALUE.toInt(), r, g, b)
|
||||
}
|
||||
|
||||
fun argb(alpha: Int, r: Int, g: Int, b: Int): Int {
|
||||
val colorByteArr =
|
||||
byteArrayOf(alpha.toByte(), r.toByte(), g.toByte(), b.toByte())
|
||||
return byteArrToInt(colorByteArr)
|
||||
}
|
||||
|
||||
fun rgb(argb: Int): IntArray {
|
||||
return intArrayOf(argb shr 16 and 0xFF, argb shr 8 and 0xFF, argb and 0xFF)
|
||||
}
|
||||
|
||||
fun byteArrToInt(colorByteArr: ByteArray): Int {
|
||||
return ((colorByteArr[0].toInt() shl 24) + (colorByteArr[1].toInt() and 0xFF shl 16)
|
||||
+ (colorByteArr[2].toInt() and 0xFF shl 8) + (colorByteArr[3].toInt() and 0xFF))
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the difference between two RGB colors by converting them to the L*a*b scale and
|
||||
* comparing them using the CIE76 algorithm { http://en.wikipedia.org/wiki/Color_difference#CIE76}
|
||||
*/
|
||||
fun getColorDifference(a: Int, b: Int): Double {
|
||||
val lab1 = DoubleArray(3)
|
||||
val lab2 = DoubleArray(3)
|
||||
ColorUtils.colorToLAB(a, lab1)
|
||||
ColorUtils.colorToLAB(b, lab2)
|
||||
return sqrt(
|
||||
(lab2[0] - lab1[0])
|
||||
.pow(2.0) + (lab2[1] - lab1[1])
|
||||
.pow(2.0) + (lab2[2] - lab1[2])
|
||||
.pow(2.0)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
|
||||
val sysConfiguration: Configuration = Resources.getSystem().configuration
|
||||
|
||||
val Configuration.isNightMode: Boolean
|
||||
get() {
|
||||
val mode = uiMode and Configuration.UI_MODE_NIGHT_MASK
|
||||
return mode == Configuration.UI_MODE_NIGHT_YES
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.lifecycle.LiveData
|
||||
|
||||
/**
|
||||
* 合并发送,只发送最新数据
|
||||
* @param delay 发送时间间隔
|
||||
*/
|
||||
class ConflateLiveData<T>(val delay: Int) : LiveData<T>() {
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private val sendRunnable = Runnable { sendData() }
|
||||
private var postTime = 0L
|
||||
private var data: T? = null
|
||||
|
||||
private fun sendData() {
|
||||
data?.let {
|
||||
super.postValue(it)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
public override fun postValue(value: T) {
|
||||
data = value
|
||||
val postDelay = postTime + delay - System.currentTimeMillis()
|
||||
if (postDelay > 0) {
|
||||
handler.removeCallbacks(sendRunnable)
|
||||
handler.postDelayed(sendRunnable, postDelay)
|
||||
} else {
|
||||
handler.removeCallbacks(sendRunnable)
|
||||
postTime = System.currentTimeMillis()
|
||||
super.postValue(value)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import androidx.transition.TransitionManager
|
||||
|
||||
@Suppress("unused")
|
||||
fun ConstraintLayout.modifyBegin(withAnim: Boolean = false): ConstraintModify.ConstraintBegin {
|
||||
val begin = ConstraintModify(this).begin
|
||||
if (withAnim) {
|
||||
TransitionManager.beginDelayedTransition(this)
|
||||
}
|
||||
return begin
|
||||
}
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate", "unused")
|
||||
class ConstraintModify(private val constraintLayout: ConstraintLayout) {
|
||||
|
||||
val begin: ConstraintBegin by lazy {
|
||||
applyConstraintSet.clone(constraintLayout)
|
||||
ConstraintBegin(constraintLayout, applyConstraintSet)
|
||||
}
|
||||
private val applyConstraintSet = ConstraintSet()
|
||||
private val resetConstraintSet = ConstraintSet()
|
||||
|
||||
init {
|
||||
resetConstraintSet.clone(constraintLayout)
|
||||
}
|
||||
|
||||
/**
|
||||
* 带动画的修改
|
||||
* @return
|
||||
*/
|
||||
fun beginWithAnim(): ConstraintBegin {
|
||||
TransitionManager.beginDelayedTransition(constraintLayout)
|
||||
return begin
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置
|
||||
*/
|
||||
fun reSet() {
|
||||
resetConstraintSet.applyTo(constraintLayout)
|
||||
}
|
||||
|
||||
/**
|
||||
* 带动画的重置
|
||||
*/
|
||||
fun reSetWidthAnim() {
|
||||
TransitionManager.beginDelayedTransition(constraintLayout)
|
||||
resetConstraintSet.applyTo(constraintLayout)
|
||||
}
|
||||
|
||||
|
||||
@Suppress("unused", "MemberVisibilityCanBePrivate")
|
||||
class ConstraintBegin(
|
||||
private val constraintLayout: ConstraintLayout,
|
||||
private val applyConstraintSet: ConstraintSet
|
||||
) {
|
||||
|
||||
/**
|
||||
* 清除关系,这里不仅仅会清除关系,还会清除对应控件的宽高为 w:0,h:0
|
||||
* @param viewId 视图ID
|
||||
* @return
|
||||
*/
|
||||
fun clear(viewId: Int): ConstraintBegin {
|
||||
applyConstraintSet.clear(viewId)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除某个控件的,某个关系
|
||||
* @param viewId 控件ID
|
||||
* @param anchor 要解除的关系
|
||||
* @return
|
||||
*/
|
||||
fun clear(viewId: Int, anchor: Anchor): ConstraintBegin {
|
||||
applyConstraintSet.clear(viewId, anchor.toInt())
|
||||
return this
|
||||
}
|
||||
|
||||
fun setHorizontalWeight(viewId: Int, weight: Float): ConstraintBegin {
|
||||
applyConstraintSet.setHorizontalWeight(viewId, weight)
|
||||
return this
|
||||
}
|
||||
|
||||
fun setVerticalWeight(viewId: Int, weight: Float): ConstraintBegin {
|
||||
applyConstraintSet.setVerticalWeight(viewId, weight)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* 为某个控件设置 margin
|
||||
* @param viewId 某个控件ID
|
||||
* @param left marginLeft
|
||||
* @param top marginTop
|
||||
* @param right marginRight
|
||||
* @param bottom marginBottom
|
||||
* @return
|
||||
*/
|
||||
fun setMargin(
|
||||
@IdRes viewId: Int,
|
||||
left: Int,
|
||||
top: Int,
|
||||
right: Int,
|
||||
bottom: Int
|
||||
): ConstraintBegin {
|
||||
setMarginLeft(viewId, left)
|
||||
setMarginTop(viewId, top)
|
||||
setMarginRight(viewId, right)
|
||||
setMarginBottom(viewId, bottom)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* 为某个控件设置 marginLeft
|
||||
* @param viewId 某个控件ID
|
||||
* @param left marginLeft
|
||||
* @return
|
||||
*/
|
||||
fun setMarginLeft(@IdRes viewId: Int, left: Int): ConstraintBegin {
|
||||
applyConstraintSet.setMargin(viewId, ConstraintSet.LEFT, left)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* 为某个控件设置 marginRight
|
||||
* @param viewId 某个控件ID
|
||||
* @param right marginRight
|
||||
* @return
|
||||
*/
|
||||
fun setMarginRight(@IdRes viewId: Int, right: Int): ConstraintBegin {
|
||||
applyConstraintSet.setMargin(viewId, ConstraintSet.RIGHT, right)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* 为某个控件设置 marginTop
|
||||
* @param viewId 某个控件ID
|
||||
* @param top marginTop
|
||||
* @return
|
||||
*/
|
||||
fun setMarginTop(@IdRes viewId: Int, top: Int): ConstraintBegin {
|
||||
applyConstraintSet.setMargin(viewId, ConstraintSet.TOP, top)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* 为某个控件设置marginBottom
|
||||
* @param viewId 某个控件ID
|
||||
* @param bottom marginBottom
|
||||
* @return
|
||||
*/
|
||||
fun setMarginBottom(@IdRes viewId: Int, bottom: Int): ConstraintBegin {
|
||||
applyConstraintSet.setMargin(viewId, ConstraintSet.BOTTOM, bottom)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* 为某个控件设置关联关系 left_to_left_of
|
||||
* @param startId
|
||||
* @param endId
|
||||
* @return
|
||||
*/
|
||||
fun leftToLeftOf(@IdRes startId: Int, @IdRes endId: Int): ConstraintBegin {
|
||||
applyConstraintSet.connect(startId, ConstraintSet.LEFT, endId, ConstraintSet.LEFT)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* 为某个控件设置关联关系 left_to_right_of
|
||||
* @param startId
|
||||
* @param endId
|
||||
* @return
|
||||
*/
|
||||
fun leftToRightOf(@IdRes startId: Int, @IdRes endId: Int): ConstraintBegin {
|
||||
applyConstraintSet.connect(startId, ConstraintSet.LEFT, endId, ConstraintSet.RIGHT)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* 为某个控件设置关联关系 top_to_top_of
|
||||
* @param startId
|
||||
* @param endId
|
||||
* @return
|
||||
*/
|
||||
fun topToTopOf(@IdRes startId: Int, @IdRes endId: Int): ConstraintBegin {
|
||||
applyConstraintSet.connect(startId, ConstraintSet.TOP, endId, ConstraintSet.TOP)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* 为某个控件设置关联关系 top_to_bottom_of
|
||||
* @param startId
|
||||
* @param endId
|
||||
* @return
|
||||
*/
|
||||
fun topToBottomOf(@IdRes startId: Int, @IdRes endId: Int): ConstraintBegin {
|
||||
applyConstraintSet.connect(startId, ConstraintSet.TOP, endId, ConstraintSet.BOTTOM)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* 为某个控件设置关联关系 right_to_left_of
|
||||
* @param startId
|
||||
* @param endId
|
||||
* @return
|
||||
*/
|
||||
fun rightToLeftOf(@IdRes startId: Int, @IdRes endId: Int): ConstraintBegin {
|
||||
applyConstraintSet.connect(startId, ConstraintSet.RIGHT, endId, ConstraintSet.LEFT)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* 为某个控件设置关联关系 right_to_right_of
|
||||
* @param startId
|
||||
* @param endId
|
||||
* @return
|
||||
*/
|
||||
fun rightToRightOf(@IdRes startId: Int, @IdRes endId: Int): ConstraintBegin {
|
||||
applyConstraintSet.connect(startId, ConstraintSet.RIGHT, endId, ConstraintSet.RIGHT)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* 为某个控件设置关联关系 bottom_to_bottom_of
|
||||
* @param startId
|
||||
* @param endId
|
||||
* @return
|
||||
*/
|
||||
fun bottomToBottomOf(@IdRes startId: Int, @IdRes endId: Int): ConstraintBegin {
|
||||
applyConstraintSet.connect(startId, ConstraintSet.BOTTOM, endId, ConstraintSet.BOTTOM)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* 为某个控件设置关联关系 bottom_to_top_of
|
||||
* @param startId
|
||||
* @param endId
|
||||
* @return
|
||||
*/
|
||||
fun bottomToTopOf(@IdRes startId: Int, @IdRes endId: Int): ConstraintBegin {
|
||||
applyConstraintSet.connect(startId, ConstraintSet.BOTTOM, endId, ConstraintSet.TOP)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* 为某个控件设置宽度
|
||||
* @param viewId
|
||||
* @param width
|
||||
* @return
|
||||
*/
|
||||
fun setWidth(@IdRes viewId: Int, width: Int): ConstraintBegin {
|
||||
applyConstraintSet.constrainWidth(viewId, width)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* 某个控件设置高度
|
||||
* @param viewId
|
||||
* @param height
|
||||
* @return
|
||||
*/
|
||||
fun setHeight(@IdRes viewId: Int, height: Int): ConstraintBegin {
|
||||
applyConstraintSet.constrainHeight(viewId, height)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交应用生效
|
||||
*/
|
||||
fun commit() {
|
||||
constraintLayout.post {
|
||||
applyConstraintSet.applyTo(constraintLayout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class Anchor {
|
||||
LEFT, RIGHT, TOP, BOTTOM, BASELINE, START, END, CIRCLE_REFERENCE;
|
||||
|
||||
fun toInt(): Int {
|
||||
return when (this) {
|
||||
LEFT -> ConstraintSet.LEFT
|
||||
RIGHT -> ConstraintSet.RIGHT
|
||||
TOP -> ConstraintSet.TOP
|
||||
BOTTOM -> ConstraintSet.BOTTOM
|
||||
BASELINE -> ConstraintSet.BASELINE
|
||||
START -> ConstraintSet.START
|
||||
END -> ConstraintSet.END
|
||||
CIRCLE_REFERENCE -> ConstraintSet.CIRCLE_REFERENCE
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
@file:Suppress("unused", "UnusedReceiverParameter")
|
||||
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.PendingIntent
|
||||
import android.app.PendingIntent.FLAG_MUTABLE
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.app.PendingIntent.getActivity
|
||||
import android.app.PendingIntent.getBroadcast
|
||||
import android.app.PendingIntent.getService
|
||||
import android.app.Service
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.ColorStateList
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Uri
|
||||
import android.os.BatteryManager
|
||||
import android.os.Build
|
||||
import android.os.Process
|
||||
import android.provider.Settings
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.constant.AppConst
|
||||
import io.legato.kazusa.data.entities.Book
|
||||
import io.legato.kazusa.help.IntentHelp
|
||||
import io.legato.kazusa.help.book.isAudio
|
||||
import io.legato.kazusa.help.book.isImage
|
||||
import io.legato.kazusa.help.config.AppConfig
|
||||
import io.legato.kazusa.ui.book.audio.AudioPlayActivity
|
||||
import io.legato.kazusa.ui.book.manga.ReadMangaActivity
|
||||
import io.legato.kazusa.ui.book.read.ReadBookActivity
|
||||
import splitties.systemservices.clipboardManager
|
||||
import splitties.systemservices.connectivityManager
|
||||
import splitties.systemservices.uiModeManager
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
inline fun <reified A : Activity> Context.startActivity(configIntent: Intent.() -> Unit = {}) {
|
||||
val intent = Intent(this, A::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
intent.apply(configIntent)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
fun Context.startActivityForBook(
|
||||
book: Book,
|
||||
configIntent: Intent.() -> Unit = {},
|
||||
) {
|
||||
val cls = when {
|
||||
book.isAudio -> AudioPlayActivity::class.java
|
||||
book.isImage && AppConfig.showMangaUi -> ReadMangaActivity::class.java
|
||||
else -> ReadBookActivity::class.java
|
||||
}
|
||||
val intent = Intent(this, cls)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
intent.putExtra("bookUrl", book.bookUrl)
|
||||
intent.apply(configIntent)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
|
||||
inline fun <reified T : Service> Context.startService(configIntent: Intent.() -> Unit = {}) {
|
||||
startService(Intent(this, T::class.java).apply(configIntent))
|
||||
}
|
||||
|
||||
inline fun <reified T : Service> Context.stopService() {
|
||||
stopService(Intent(this, T::class.java))
|
||||
}
|
||||
|
||||
@SuppressLint("UnspecifiedImmutableFlag")
|
||||
inline fun <reified T : Service> Context.servicePendingIntent(
|
||||
action: String,
|
||||
requestCode: Int = 0,
|
||||
configIntent: Intent.() -> Unit = {},
|
||||
): PendingIntent? {
|
||||
val intent = Intent(this, T::class.java)
|
||||
intent.action = action
|
||||
configIntent.invoke(intent)
|
||||
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
FLAG_UPDATE_CURRENT or FLAG_MUTABLE
|
||||
} else {
|
||||
FLAG_UPDATE_CURRENT
|
||||
}
|
||||
return getService(this, requestCode, intent, flags)
|
||||
}
|
||||
|
||||
@SuppressLint("UnspecifiedImmutableFlag")
|
||||
fun Context.activityPendingIntent(
|
||||
intent: Intent,
|
||||
action: String,
|
||||
): PendingIntent? {
|
||||
intent.action = action
|
||||
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
FLAG_UPDATE_CURRENT or FLAG_MUTABLE
|
||||
} else {
|
||||
FLAG_UPDATE_CURRENT
|
||||
}
|
||||
return getActivity(this, 0, intent, flags)
|
||||
}
|
||||
|
||||
@SuppressLint("UnspecifiedImmutableFlag")
|
||||
inline fun <reified T : Activity> Context.activityPendingIntent(
|
||||
action: String,
|
||||
configIntent: Intent.() -> Unit = {},
|
||||
): PendingIntent? {
|
||||
val intent = Intent(this, T::class.java)
|
||||
intent.action = action
|
||||
configIntent.invoke(intent)
|
||||
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
FLAG_UPDATE_CURRENT or FLAG_MUTABLE
|
||||
} else {
|
||||
FLAG_UPDATE_CURRENT
|
||||
}
|
||||
return getActivity(this, 0, intent, flags)
|
||||
}
|
||||
|
||||
@SuppressLint("UnspecifiedImmutableFlag")
|
||||
inline fun <reified T : BroadcastReceiver> Context.broadcastPendingIntent(
|
||||
action: String,
|
||||
configIntent: Intent.() -> Unit = {},
|
||||
): PendingIntent? {
|
||||
val intent = Intent(this, T::class.java)
|
||||
intent.action = action
|
||||
configIntent.invoke(intent)
|
||||
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
FLAG_UPDATE_CURRENT or FLAG_MUTABLE
|
||||
} else {
|
||||
FLAG_UPDATE_CURRENT
|
||||
}
|
||||
return getBroadcast(this, 0, intent, flags)
|
||||
}
|
||||
|
||||
fun Context.startForegroundServiceCompat(intent: Intent) {
|
||||
try {
|
||||
startService(intent)
|
||||
} catch (e: IllegalStateException) {
|
||||
ContextCompat.startForegroundService(this, intent)
|
||||
}
|
||||
}
|
||||
|
||||
val Context.defaultSharedPreferences: SharedPreferences
|
||||
get() = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
fun Context.getPrefBoolean(key: String, defValue: Boolean = false) =
|
||||
defaultSharedPreferences.getBoolean(key, defValue)
|
||||
|
||||
fun Context.putPrefBoolean(key: String, value: Boolean = false) =
|
||||
defaultSharedPreferences.edit { putBoolean(key, value) }
|
||||
|
||||
fun Context.getPrefInt(key: String, defValue: Int = 0) =
|
||||
defaultSharedPreferences.getInt(key, defValue)
|
||||
|
||||
fun Context.putPrefInt(key: String, value: Int) =
|
||||
defaultSharedPreferences.edit { putInt(key, value) }
|
||||
|
||||
fun Context.getPrefLong(key: String, defValue: Long = 0L) =
|
||||
defaultSharedPreferences.getLong(key, defValue)
|
||||
|
||||
fun Context.putPrefLong(key: String, value: Long) =
|
||||
defaultSharedPreferences.edit { putLong(key, value) }
|
||||
|
||||
fun Context.getPrefString(key: String, defValue: String? = null) =
|
||||
defaultSharedPreferences.getString(key, defValue)
|
||||
|
||||
fun Context.putPrefString(key: String, value: String?) =
|
||||
defaultSharedPreferences.edit { putString(key, value) }
|
||||
|
||||
fun Context.getPrefStringSet(
|
||||
key: String,
|
||||
defValue: MutableSet<String>? = null,
|
||||
): MutableSet<String>? = defaultSharedPreferences.getStringSet(key, defValue)
|
||||
|
||||
fun Context.putPrefStringSet(key: String, value: MutableSet<String>) =
|
||||
defaultSharedPreferences.edit { putStringSet(key, value) }
|
||||
|
||||
fun Context.removePref(key: String) =
|
||||
defaultSharedPreferences.edit { remove(key) }
|
||||
|
||||
|
||||
fun Context.getCompatColor(@ColorRes id: Int): Int = ContextCompat.getColor(this, id)
|
||||
|
||||
fun Context.getCompatDrawable(@DrawableRes id: Int): Drawable? = ContextCompat.getDrawable(this, id)
|
||||
|
||||
fun Context.getCompatColorStateList(@ColorRes id: Int): ColorStateList? =
|
||||
ContextCompat.getColorStateList(this, id)
|
||||
|
||||
fun Context.checkSelfUriPermission(uri: Uri, modeFlags: Int): Int =
|
||||
checkUriPermission(uri, Process.myPid(), Process.myUid(), modeFlags)
|
||||
|
||||
fun Context.restart() {
|
||||
val intent: Intent? = packageManager.getLaunchIntentForPackage(packageName)
|
||||
intent?.let {
|
||||
intent.addFlags(
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
)
|
||||
startActivity(intent)
|
||||
//杀掉以前进程
|
||||
Process.killProcess(Process.myPid())
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统息屏时间
|
||||
*/
|
||||
val Context.sysScreenOffTime: Int
|
||||
get() {
|
||||
return kotlin.runCatching {
|
||||
Settings.System.getInt(contentResolver, Settings.System.SCREEN_OFF_TIMEOUT)
|
||||
}.onFailure {
|
||||
it.printOnDebug()
|
||||
}.getOrDefault(0)
|
||||
}
|
||||
|
||||
val Context.statusBarHeight: Int
|
||||
@SuppressLint("DiscouragedApi", "InternalInsetResource")
|
||||
get() {
|
||||
if (Build.BOARD == "windows") {
|
||||
return 0
|
||||
}
|
||||
val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android")
|
||||
return resources.getDimensionPixelSize(resourceId)
|
||||
}
|
||||
|
||||
val Context.navigationBarHeight: Int
|
||||
@SuppressLint("DiscouragedApi", "InternalInsetResource")
|
||||
get() {
|
||||
val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")
|
||||
return resources.getDimensionPixelSize(resourceId)
|
||||
}
|
||||
|
||||
fun Context.share(text: String, title: String = getString(R.string.share)) {
|
||||
kotlin.runCatching {
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, title)
|
||||
intent.putExtra(Intent.EXTRA_TEXT, text)
|
||||
intent.type = "text/plain"
|
||||
startActivity(Intent.createChooser(intent, title))
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.share(file: File, type: String = "text/*") {
|
||||
val fileUri = FileProvider.getUriForFile(this, AppConst.authority, file)
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.type = type
|
||||
intent.putExtra(Intent.EXTRA_STREAM, fileUri)
|
||||
intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(
|
||||
Intent.createChooser(
|
||||
intent,
|
||||
getString(R.string.share_selected_source)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("SetWorldReadable")
|
||||
fun Context.shareWithQr(
|
||||
text: String,
|
||||
title: String = getString(R.string.share),
|
||||
errorCorrectionLevel: ErrorCorrectionLevel = ErrorCorrectionLevel.H,
|
||||
) {
|
||||
val bitmap = QRCodeUtils.createQRCode(text, errorCorrectionLevel = errorCorrectionLevel)
|
||||
if (bitmap == null) {
|
||||
toastOnUi(R.string.text_too_long_qr_error)
|
||||
} else {
|
||||
try {
|
||||
val file = File(externalCacheDir, "qr.png")
|
||||
val fOut = FileOutputStream(file)
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fOut)
|
||||
fOut.flush()
|
||||
fOut.close()
|
||||
file.setReadable(true, false)
|
||||
val contentUri = FileProvider.getUriForFile(this, AppConst.authority, file)
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
intent.putExtra(Intent.EXTRA_STREAM, contentUri)
|
||||
intent.type = "image/png"
|
||||
startActivity(Intent.createChooser(intent, title))
|
||||
} catch (e: Exception) {
|
||||
toastOnUi(e.localizedMessage ?: "ERROR")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.sendToClip(text: String) {
|
||||
val clipData = ClipData.newPlainText(null, text)
|
||||
clipboardManager.setPrimaryClip(clipData)
|
||||
longToastOnUi(R.string.copy_complete)
|
||||
}
|
||||
|
||||
fun Context.getClipText(): String? {
|
||||
clipboardManager.primaryClip?.let {
|
||||
if (it.itemCount > 0) {
|
||||
return it.getItemAt(0).text.toString().trim()
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun Context.sendMail(mail: String) {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_SENDTO)
|
||||
intent.data = Uri.parse("mailto:$mail")
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
toastOnUi(e.localizedMessage ?: "Error")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取电量
|
||||
*/
|
||||
val Context.sysBattery: Int
|
||||
get() {
|
||||
val iFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
|
||||
val batteryStatus = registerReceiver(null, iFilter)
|
||||
return batteryStatus?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1
|
||||
}
|
||||
|
||||
val Context.externalFiles: File
|
||||
get() = this.getExternalFilesDir(null) ?: this.filesDir
|
||||
|
||||
val Context.externalCache: File
|
||||
get() = this.externalCacheDir ?: this.cacheDir
|
||||
|
||||
fun Context.openUrl(url: String) {
|
||||
try {
|
||||
startActivity(IntentHelp.getBrowserIntent(url))
|
||||
} catch (e: Exception) {
|
||||
toastOnUi(e.localizedMessage ?: "open url error")
|
||||
e.printOnDebug()
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.openUrl(uri: Uri) {
|
||||
try {
|
||||
startActivity(IntentHelp.getBrowserIntent(uri))
|
||||
} catch (e: Exception) {
|
||||
toastOnUi(e.localizedMessage ?: "open url error")
|
||||
e.printOnDebug()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
fun Context.openFileUri(uri: Uri, type: String? = null) {
|
||||
val intent = Intent()
|
||||
intent.action = Intent.ACTION_VIEW
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
//7.0版本以上
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
val uri = if (uri.isContentScheme()) uri
|
||||
else FileProvider.getUriForFile(this, AppConst.authority, File(uri.path!!))
|
||||
intent.setDataAndType(uri, type ?: IntentType.from(uri))
|
||||
try {
|
||||
startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
toastOnUi(e.stackTraceStr)
|
||||
e.printOnDebug()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val Context.isWifiConnect: Boolean
|
||||
@SuppressLint("MissingPermission")
|
||||
get() {
|
||||
val info = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI)
|
||||
return info?.isConnected == true
|
||||
}
|
||||
|
||||
val Context.isPad: Boolean
|
||||
get() {
|
||||
return (resources.configuration.screenLayout and Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_LARGE
|
||||
}
|
||||
|
||||
val Context.isTv: Boolean
|
||||
get() = uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION
|
||||
|
||||
val Context.channel: String
|
||||
get() {
|
||||
try {
|
||||
val pm = packageManager
|
||||
val appInfo = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA)
|
||||
return appInfo.metaData.getString("channel") ?: ""
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
val Context.isDebuggable: Boolean
|
||||
get() = applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0
|
||||
@@ -0,0 +1,131 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.text.DecimalFormat
|
||||
import kotlin.math.log10
|
||||
import kotlin.math.pow
|
||||
|
||||
/**
|
||||
* 数据类型转换、单位转换
|
||||
*
|
||||
* @author 李玉江[QQ:1023694760]
|
||||
* @since 2014-4-18
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
object ConvertUtils {
|
||||
const val GB: Long = 1073741824
|
||||
const val MB: Long = 1048576
|
||||
const val KB: Long = 1024
|
||||
|
||||
fun toInt(obj: Any): Int {
|
||||
return kotlin.runCatching {
|
||||
Integer.parseInt(obj.toString())
|
||||
}.getOrDefault(-1)
|
||||
}
|
||||
|
||||
fun toInt(bytes: ByteArray): Int {
|
||||
var result = 0
|
||||
var byte: Byte
|
||||
for (i in bytes.indices) {
|
||||
byte = bytes[i]
|
||||
result += (byte.toInt() and 0xFF).shl(8 * i)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun toFloat(obj: Any): Float {
|
||||
return kotlin.runCatching {
|
||||
java.lang.Float.parseFloat(obj.toString())
|
||||
}.getOrDefault(-1f)
|
||||
}
|
||||
|
||||
fun toString(objects: Array<Any>, tag: String): String {
|
||||
val sb = StringBuilder()
|
||||
for (`object` in objects) {
|
||||
sb.append(`object`)
|
||||
sb.append(tag)
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun toBitmap(bytes: ByteArray, width: Int = -1, height: Int = -1): Bitmap? {
|
||||
var bitmap: Bitmap? = null
|
||||
if (bytes.isNotEmpty()) {
|
||||
kotlin.runCatching {
|
||||
val options = BitmapFactory.Options()
|
||||
// 设置让解码器以最佳方式解码
|
||||
options.inPreferredConfig = null
|
||||
if (width > 0 && height > 0) {
|
||||
options.outWidth = width
|
||||
options.outHeight = height
|
||||
}
|
||||
bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options)
|
||||
bitmap!!.density = 96// 96 dpi
|
||||
}
|
||||
}
|
||||
return bitmap
|
||||
}
|
||||
|
||||
private fun toDrawable(bitmap: Bitmap?): Drawable? {
|
||||
return if (bitmap == null) null else BitmapDrawable(Resources.getSystem(), bitmap)
|
||||
}
|
||||
|
||||
fun toDrawable(bytes: ByteArray): Drawable? {
|
||||
return toDrawable(toBitmap(bytes))
|
||||
}
|
||||
|
||||
fun formatFileSize(length: Long): String {
|
||||
if (length <= 0) return "0"
|
||||
val units = arrayOf("b", "kb", "M", "G", "T")
|
||||
//计算单位的,原理是利用lg,公式是 lg(1024^n) = nlg(1024),最后 nlg(1024)/lg(1024) = n。
|
||||
val digitGroups = (log10(length.toDouble()) / log10(1024.0)).toInt()
|
||||
//计算原理是,size/单位值。单位值指的是:比如说b = 1024,KB = 1024^2
|
||||
return DecimalFormat("#,##0.##").format(length / 1024.0.pow(digitGroups.toDouble())) + " " + units[digitGroups]
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun toString(`is`: InputStream, charset: String = "utf-8"): String {
|
||||
val sb = StringBuilder()
|
||||
kotlin.runCatching {
|
||||
val reader = BufferedReader(InputStreamReader(`is`, charset))
|
||||
while (true) {
|
||||
val line = reader.readLine()
|
||||
if (line == null) {
|
||||
break
|
||||
} else {
|
||||
sb.append(line).append("\n")
|
||||
}
|
||||
}
|
||||
reader.close()
|
||||
`is`.close()
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
val Int.hexString: String
|
||||
get() = Integer.toHexString(this)
|
||||
|
||||
fun Int.dpToPx(): Int = this.toFloat().dpToPx().toInt()
|
||||
|
||||
fun Int.spToPx(): Int = this.toFloat().spToPx().toInt()
|
||||
|
||||
fun Float.dpToPx(): Float = android.util.TypedValue.applyDimension(
|
||||
android.util.TypedValue.COMPLEX_UNIT_DIP, this, Resources.getSystem().displayMetrics
|
||||
)
|
||||
|
||||
fun Float.spToPx(): Float = android.util.TypedValue.applyDimension(
|
||||
android.util.TypedValue.COMPLEX_UNIT_SP, this, Resources.getSystem().displayMetrics
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
@file:Suppress("UnusedReceiverParameter")
|
||||
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.webkit.CookieManager
|
||||
|
||||
|
||||
@Suppress("unused")
|
||||
fun CookieManager.removeCookie(url: String) {
|
||||
val cm = CookieManager.getInstance()
|
||||
val domains = arrayOf(
|
||||
NetworkUtils.getDomain(url),
|
||||
NetworkUtils.getSubDomain(url)
|
||||
)
|
||||
domains.forEach { dm ->
|
||||
val cookieGlob: String? = cm.getCookie(dm)
|
||||
cookieGlob?.splitNotBlank(";")?.forEach {
|
||||
val cookieName = it.substringBefore("=")
|
||||
cm.setCookie(dm, "$cookieName=; Expires=Wed, 31 Dec 2000 23:59:59 GMT")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import io.legato.kazusa.help.coroutine.Coroutine
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
class TimeoutCancellationException(msg: String) : CancellationException(msg)
|
||||
|
||||
suspend fun <T> withTimeoutAsync(delayMillis: Long, block: suspend CoroutineScope.() -> T): T {
|
||||
return suspendCancellableCoroutine { cout ->
|
||||
Coroutine.async(context = cout.context) {
|
||||
launch {
|
||||
delay(delayMillis)
|
||||
if (!cout.isCompleted) {
|
||||
cout.resumeWithException(TimeoutCancellationException("Timed out waiting for $delayMillis ms"))
|
||||
}
|
||||
}
|
||||
val result = block()
|
||||
if (!cout.isCompleted) {
|
||||
cout.resume(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T> withTimeoutOrNullAsync(delayMillis: Long, block: suspend CoroutineScope.() -> T): T? {
|
||||
return try {
|
||||
withTimeoutAsync(delayMillis, block)
|
||||
} catch (e: TimeoutCancellationException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import io.legato.kazusa.help.config.AppConfig
|
||||
|
||||
// 匹配待“输入的章节”字符串
|
||||
private val regexEpisode = Regex("\\d+(-\\d+)?(,\\d+(-\\d+)?)*")
|
||||
|
||||
/**
|
||||
* 是否启用自定义导出
|
||||
*
|
||||
* @author Discut
|
||||
*/
|
||||
fun enableCustomExport(): Boolean {
|
||||
return AppConfig.enableCustomExport && AppConfig.exportType == 1
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 输入的范围 是否正确
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @author Discut
|
||||
* @param text 输入的范围 字符串
|
||||
* @return 是否正确
|
||||
*/
|
||||
fun verificationField(text: String): Boolean {
|
||||
return text.matches(regexEpisode)
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.os.SystemClock
|
||||
import kotlin.math.max
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
open class Debounce<T>(
|
||||
var wait: Long = 0L,
|
||||
var maxWait: Long = -1L,
|
||||
var leading: Boolean = false,
|
||||
var trailing: Boolean = true,
|
||||
private val func: () -> T
|
||||
) {
|
||||
companion object {
|
||||
private val handler by lazy { buildMainHandler() }
|
||||
}
|
||||
|
||||
private var lastCallTime = -1L
|
||||
private var lastInvokeTime = 0L
|
||||
private val maxing get() = maxWait != -1L
|
||||
private var result: T? = null
|
||||
private var hasTimer = false
|
||||
private val timerExpiredRunnable = Runnable {
|
||||
timerExpired()
|
||||
}
|
||||
|
||||
init {
|
||||
maxWait = if (maxing) max(maxWait, wait) else maxWait
|
||||
}
|
||||
|
||||
private fun invokeFunc(time: Long): T {
|
||||
lastInvokeTime = time
|
||||
return func.invoke().also { result = it }
|
||||
}
|
||||
|
||||
private fun startTimer(wait: Long) {
|
||||
hasTimer = true
|
||||
handler.postDelayed(timerExpiredRunnable, wait)
|
||||
}
|
||||
|
||||
private fun cancelTimer() {
|
||||
handler.removeCallbacks(timerExpiredRunnable)
|
||||
}
|
||||
|
||||
private fun leadingEdge(time: Long): T? {
|
||||
lastInvokeTime = time
|
||||
startTimer(wait)
|
||||
return if (leading) invokeFunc(time) else result
|
||||
}
|
||||
|
||||
private fun trailingEdge(time: Long): T? {
|
||||
hasTimer = false
|
||||
return if (trailing) invokeFunc(time) else result
|
||||
}
|
||||
|
||||
private fun remainingWait(time: Long): Long {
|
||||
val timeSinceLastCall = time - lastCallTime
|
||||
val timeSinceLastInvoke = time - lastInvokeTime
|
||||
val timeWaiting = wait - timeSinceLastCall
|
||||
|
||||
return if (maxing) timeWaiting.coerceAtMost(maxWait - timeSinceLastInvoke) else timeWaiting
|
||||
}
|
||||
|
||||
private fun shouldInvoke(time: Long): Boolean {
|
||||
val timeSinceLastCall = time - lastCallTime
|
||||
val timeSinceLastInvoke = time - lastInvokeTime
|
||||
|
||||
return lastCallTime == -1L
|
||||
|| timeSinceLastCall >= wait
|
||||
|| timeSinceLastCall < 0
|
||||
|| maxing && timeSinceLastInvoke >= maxWait
|
||||
}
|
||||
|
||||
private fun timerExpired() {
|
||||
val time = SystemClock.uptimeMillis()
|
||||
if (shouldInvoke(time)) {
|
||||
trailingEdge(time)
|
||||
} else {
|
||||
startTimer(remainingWait(time))
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
if (hasTimer) {
|
||||
cancelTimer()
|
||||
}
|
||||
lastInvokeTime = 0
|
||||
lastCallTime = -1L
|
||||
hasTimer = false
|
||||
}
|
||||
|
||||
fun flush(): T? {
|
||||
return if (hasTimer) trailingEdge(SystemClock.uptimeMillis()) else result
|
||||
}
|
||||
|
||||
fun pending(): Boolean = hasTimer
|
||||
|
||||
operator fun invoke(): T? {
|
||||
val time = SystemClock.uptimeMillis()
|
||||
val isInvoking = shouldInvoke(time)
|
||||
|
||||
lastCallTime = time
|
||||
|
||||
if (isInvoking) {
|
||||
if (!hasTimer) {
|
||||
return leadingEdge(lastCallTime)
|
||||
}
|
||||
if (maxing) {
|
||||
startTimer(wait)
|
||||
return invokeFunc(lastCallTime)
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasTimer) {
|
||||
startTimer(wait)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun <T> debounce(
|
||||
wait: Long = 0L,
|
||||
maxWait: Long = -1L,
|
||||
leading: Boolean = false,
|
||||
trailing: Boolean = true,
|
||||
func: () -> T
|
||||
) = Debounce(wait, maxWait, leading, trailing, func)
|
||||
@@ -0,0 +1,54 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.util.Log
|
||||
import io.legato.kazusa.BuildConfig
|
||||
|
||||
object DebugLog {
|
||||
|
||||
fun e(tag: String, throwable: Throwable) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.e(tag, throwable.stackTraceToString())
|
||||
}
|
||||
}
|
||||
|
||||
fun e(tag: String, msg: String, throwable: Throwable? = null) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
if (throwable == null) {
|
||||
Log.e(tag, msg)
|
||||
} else {
|
||||
Log.e(tag, msg, throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun d(tag: String, msg: String, throwable: Throwable? = null) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
if (throwable == null) {
|
||||
Log.d(tag, msg)
|
||||
} else {
|
||||
Log.d(tag, msg, throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun i(tag: String, msg: String, throwable: Throwable? = null) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
if (throwable == null) {
|
||||
Log.i(tag, msg)
|
||||
} else {
|
||||
Log.i(tag, msg, throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun w(tag: String, msg: String, throwable: Throwable? = null) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
if (throwable == null) {
|
||||
Log.w(tag, msg)
|
||||
} else {
|
||||
Log.w(tag, msg, throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.app.Dialog
|
||||
import android.view.WindowManager
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.forEach
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import io.legato.kazusa.lib.theme.Selector
|
||||
import io.legato.kazusa.lib.theme.ThemeStore
|
||||
//import io.legado.app.lib.theme.accentColor
|
||||
//import io.legado.app.lib.theme.filletBackground
|
||||
import splitties.systemservices.windowManager
|
||||
|
||||
fun AlertDialog.applyTint(): AlertDialog {
|
||||
//window?.setBackgroundDrawable(context.filletBackground)
|
||||
val colorStateList = Selector.colorBuild()
|
||||
.setDefaultColor(ThemeStore.accentColor(context))
|
||||
.setPressedColor(ColorUtils.darkenColor(ThemeStore.accentColor(context)))
|
||||
.create()
|
||||
if (getButton(AlertDialog.BUTTON_NEGATIVE) != null) {
|
||||
getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(colorStateList)
|
||||
}
|
||||
if (getButton(AlertDialog.BUTTON_POSITIVE) != null) {
|
||||
getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(colorStateList)
|
||||
}
|
||||
if (getButton(AlertDialog.BUTTON_NEUTRAL) != null) {
|
||||
getButton(AlertDialog.BUTTON_NEUTRAL).setTextColor(colorStateList)
|
||||
}
|
||||
window?.decorView?.post {
|
||||
listView?.forEach {
|
||||
//it.applyTint(context.accentColor)
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun AlertDialog.requestInputMethod() {
|
||||
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
|
||||
}
|
||||
|
||||
fun DialogFragment.setLayout(widthMix: Float, heightMix: Float) {
|
||||
dialog?.setLayout(widthMix, heightMix)
|
||||
}
|
||||
|
||||
fun Dialog.setLayout(widthMix: Float, heightMix: Float) {
|
||||
val dm = context.windowManager.windowSize
|
||||
window?.setLayout(
|
||||
(dm.widthPixels * widthMix).toInt(),
|
||||
(dm.heightPixels * heightMix).toInt()
|
||||
)
|
||||
}
|
||||
|
||||
fun DialogFragment.setLayout(width: Int, heightMix: Float) {
|
||||
dialog?.setLayout(width, heightMix)
|
||||
}
|
||||
|
||||
fun Dialog.setLayout(width: Int, heightMix: Float) {
|
||||
val dm = context.windowManager.windowSize
|
||||
window?.setLayout(
|
||||
width,
|
||||
(dm.heightPixels * heightMix).toInt()
|
||||
)
|
||||
}
|
||||
|
||||
fun DialogFragment.setLayout(widthMix: Float, height: Int) {
|
||||
dialog?.setLayout(widthMix, height)
|
||||
}
|
||||
|
||||
fun Dialog.setLayout(widthMix: Float, height: Int) {
|
||||
val dm = context.windowManager.windowSize
|
||||
window?.setLayout(
|
||||
(dm.widthPixels * widthMix).toInt(),
|
||||
height
|
||||
)
|
||||
}
|
||||
|
||||
fun DialogFragment.setLayout(width: Int, height: Int) {
|
||||
dialog?.setLayout(width, height)
|
||||
}
|
||||
|
||||
fun Dialog.setLayout(width: Int, height: Int) {
|
||||
window?.setLayout(width, height)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
object DocumentUtils {
|
||||
|
||||
fun exists(root: DocumentFile, fileName: String, vararg subDirs: String): Boolean {
|
||||
val parent = getDirDocument(root, *subDirs) ?: return false
|
||||
return parent.findFile(fileName)?.exists() ?: false
|
||||
}
|
||||
|
||||
fun delete(root: DocumentFile, fileName: String, vararg subDirs: String) {
|
||||
val parent: DocumentFile? = createFolderIfNotExist(root, *subDirs)
|
||||
parent?.findFile(fileName)?.delete()
|
||||
}
|
||||
|
||||
fun createFileIfNotExist(
|
||||
root: DocumentFile,
|
||||
fileName: String,
|
||||
vararg subDirs: String
|
||||
): DocumentFile? {
|
||||
val parent: DocumentFile? = createFolderIfNotExist(root, *subDirs)
|
||||
return parent?.findFile(fileName) ?: parent?.createFile("", fileName)
|
||||
}
|
||||
|
||||
fun createFolderIfNotExist(root: DocumentFile, vararg subDirs: String): DocumentFile? {
|
||||
var parent: DocumentFile? = root
|
||||
for (subDirName in subDirs) {
|
||||
val subDir = parent?.findFile(subDirName)
|
||||
?: parent?.createDirectory(subDirName)
|
||||
parent = subDir
|
||||
}
|
||||
return parent
|
||||
}
|
||||
|
||||
fun getDirDocument(root: DocumentFile, vararg subDirs: String): DocumentFile? {
|
||||
var parent = root
|
||||
for (subDirName in subDirs) {
|
||||
val subDir = parent.findFile(subDirName)
|
||||
parent = subDir ?: return null
|
||||
}
|
||||
return parent
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.TransitionDrawable
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
|
||||
/**
|
||||
* @author Karim Abou Zeid (kabouzeid)
|
||||
*/
|
||||
@Suppress("unused")
|
||||
object DrawableUtils {
|
||||
|
||||
fun createTransitionDrawable(
|
||||
@ColorInt startColor: Int,
|
||||
@ColorInt endColor: Int
|
||||
): TransitionDrawable {
|
||||
return createTransitionDrawable(ColorDrawable(startColor), ColorDrawable(endColor))
|
||||
}
|
||||
|
||||
fun createTransitionDrawable(start: Drawable, end: Drawable): TransitionDrawable {
|
||||
val drawables = arrayOfNulls<Drawable>(2)
|
||||
|
||||
drawables[0] = start
|
||||
drawables[1] = end
|
||||
|
||||
return TransitionDrawable(drawables)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun Drawable.setTintListMutate(
|
||||
tint: ColorStateList,
|
||||
tintMode: PorterDuff.Mode = PorterDuff.Mode.SRC_ATOP
|
||||
) {
|
||||
val wrappedDrawable = DrawableCompat.wrap(this)
|
||||
wrappedDrawable.mutate()
|
||||
DrawableCompat.setTintMode(wrappedDrawable, tintMode)
|
||||
DrawableCompat.setTintList(wrappedDrawable, tint)
|
||||
}
|
||||
|
||||
fun Drawable.setTintMutate(
|
||||
@ColorInt tint: Int,
|
||||
tintMode: PorterDuff.Mode = PorterDuff.Mode.SRC_ATOP
|
||||
) {
|
||||
val wrappedDrawable = DrawableCompat.wrap(this)
|
||||
wrappedDrawable.mutate()
|
||||
DrawableCompat.setTintMode(wrappedDrawable, tintMode)
|
||||
DrawableCompat.setTint(wrappedDrawable, tint)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.util.Base64
|
||||
|
||||
/**
|
||||
* 编码工具 escape base64
|
||||
*/
|
||||
@Suppress("unused")
|
||||
object EncoderUtils {
|
||||
|
||||
fun escape(src: String): String {
|
||||
val tmp = StringBuilder()
|
||||
for (char in src) {
|
||||
val charCode = char.code
|
||||
if (charCode in 48..57 || charCode in 65..90 || charCode in 97..122) {
|
||||
tmp.append(char)
|
||||
continue
|
||||
}
|
||||
|
||||
val prefix = when {
|
||||
charCode < 16 -> "%0"
|
||||
charCode < 256 -> "%"
|
||||
else -> "%u"
|
||||
}
|
||||
tmp.append(prefix).append(charCode.toString(16))
|
||||
}
|
||||
return tmp.toString()
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun base64Decode(str: String, flags: Int = Base64.DEFAULT): String {
|
||||
val bytes = Base64.decode(str, flags)
|
||||
return String(bytes)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun base64Encode(str: String, flags: Int = Base64.NO_WRAP): String? {
|
||||
return Base64.encodeToString(str.toByteArray(), flags)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun base64Encode(bytes: ByteArray, flags: Int = Base64.NO_WRAP): String {
|
||||
return Base64.encodeToString(bytes, flags)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun base64DecodeToByteArray(str: String, flags: Int = Base64.DEFAULT): ByteArray {
|
||||
return Base64.decode(str, flags)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.text.TextUtils
|
||||
import io.legato.kazusa.lib.icu4j.CharsetDetector
|
||||
import org.jsoup.Jsoup
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* 自动获取文件的编码
|
||||
* */
|
||||
@Suppress("MemberVisibilityCanBePrivate", "unused")
|
||||
object EncodingDetect {
|
||||
|
||||
private val headTagRegex = "(?i)<head>[\\s\\S]*?</head>".toRegex()
|
||||
private val headOpenBytes = "<head>".toByteArray()
|
||||
private val headCloseBytes = "</head>".toByteArray()
|
||||
|
||||
fun getHtmlEncode(bytes: ByteArray): String {
|
||||
try {
|
||||
var head: String? = null
|
||||
val startIndex = bytes.indexOf(headOpenBytes)
|
||||
if (startIndex > -1) {
|
||||
val endIndex = bytes.indexOf(headCloseBytes, startIndex)
|
||||
if (endIndex > -1) {
|
||||
head = String(bytes.copyOfRange(startIndex, endIndex + headCloseBytes.size))
|
||||
}
|
||||
}
|
||||
val doc = Jsoup.parseBodyFragment(head ?: headTagRegex.find(String(bytes))!!.value)
|
||||
val metaTags = doc.getElementsByTag("meta")
|
||||
var charsetStr: String
|
||||
for (metaTag in metaTags) {
|
||||
charsetStr = metaTag.attr("charset")
|
||||
if (!TextUtils.isEmpty(charsetStr)) {
|
||||
return charsetStr
|
||||
}
|
||||
val httpEquiv = metaTag.attr("http-equiv")
|
||||
if (httpEquiv.equals("content-type", true)) {
|
||||
val content = metaTag.attr("content")
|
||||
val idx = content.indexOf("charset=", ignoreCase = true)
|
||||
charsetStr = if (idx > -1) {
|
||||
content.substring(idx + "charset=".length)
|
||||
} else {
|
||||
content.substringAfter(";")
|
||||
}
|
||||
if (!TextUtils.isEmpty(charsetStr)) {
|
||||
return charsetStr
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
return getEncode(bytes)
|
||||
}
|
||||
|
||||
fun getEncode(bytes: ByteArray): String {
|
||||
val match = CharsetDetector().setText(bytes).detect()
|
||||
return match?.name ?: "UTF-8"
|
||||
}
|
||||
|
||||
/**
|
||||
* 得到文件的编码
|
||||
*/
|
||||
fun getEncode(filePath: String): String {
|
||||
return getEncode(File(filePath))
|
||||
}
|
||||
|
||||
/**
|
||||
* 得到文件的编码
|
||||
*/
|
||||
fun getEncode(file: File): String {
|
||||
val tempByte = getFileBytes(file)
|
||||
if (tempByte.isEmpty()) {
|
||||
return "UTF-8"
|
||||
}
|
||||
return getEncode(tempByte)
|
||||
}
|
||||
|
||||
private fun getFileBytes(file: File): ByteArray {
|
||||
val byteArray = ByteArray(8000)
|
||||
var pos = 0
|
||||
try {
|
||||
file.inputStream().buffered().use {
|
||||
while (pos < byteArray.size) {
|
||||
val n = it.read(byteArray, pos, 1)
|
||||
if (n == -1) {
|
||||
break
|
||||
}
|
||||
if (byteArray[pos] < 0) {
|
||||
pos++
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
System.err.println("Error: $e")
|
||||
}
|
||||
return byteArray.copyOf(pos)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.Observer
|
||||
import com.jeremyliao.liveeventbus.LiveEventBus
|
||||
import com.jeremyliao.liveeventbus.core.Observable
|
||||
|
||||
inline fun <reified EVENT> eventObservable(tag: String): Observable<EVENT> {
|
||||
return LiveEventBus.get(tag, EVENT::class.java)
|
||||
}
|
||||
|
||||
inline fun <reified EVENT> postEvent(tag: String, event: EVENT) {
|
||||
LiveEventBus.get<EVENT>(tag).post(event)
|
||||
}
|
||||
|
||||
inline fun <reified EVENT> postEventDelay(tag: String, event: EVENT, delay: Long) {
|
||||
LiveEventBus.get<EVENT>(tag).postDelay(event, delay)
|
||||
}
|
||||
|
||||
inline fun <reified EVENT> postEventOrderly(tag: String, event: EVENT) {
|
||||
LiveEventBus.get<EVENT>(tag).postOrderly(event)
|
||||
}
|
||||
|
||||
inline fun <reified EVENT> AppCompatActivity.observeEvent(
|
||||
vararg tags: String,
|
||||
noinline observer: (EVENT) -> Unit
|
||||
) {
|
||||
val o = Observer<EVENT> {
|
||||
observer(it)
|
||||
}
|
||||
tags.forEach {
|
||||
eventObservable<EVENT>(it).observe(this, o)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified EVENT> AppCompatActivity.observeEventSticky(
|
||||
vararg tags: String,
|
||||
noinline observer: (EVENT) -> Unit
|
||||
) {
|
||||
val o = Observer<EVENT> {
|
||||
observer(it)
|
||||
}
|
||||
tags.forEach {
|
||||
eventObservable<EVENT>(it).observeSticky(this, o)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified EVENT> Fragment.observeEvent(
|
||||
vararg tags: String,
|
||||
noinline observer: (EVENT) -> Unit
|
||||
) {
|
||||
val o = Observer<EVENT> {
|
||||
observer(it)
|
||||
}
|
||||
tags.forEach {
|
||||
eventObservable<EVENT>(it).observe(this, o)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified EVENT> Fragment.observeEventSticky(
|
||||
vararg tags: String,
|
||||
noinline observer: (EVENT) -> Unit
|
||||
) {
|
||||
val o = Observer<EVENT> {
|
||||
observer(it)
|
||||
}
|
||||
tags.forEach {
|
||||
eventObservable<EVENT>(it).observeSticky(this, o)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified EVENT> LifecycleService.observeEvent(
|
||||
vararg tags: String,
|
||||
noinline observer: (EVENT) -> Unit
|
||||
) {
|
||||
val o = Observer<EVENT> {
|
||||
observer(it)
|
||||
}
|
||||
tags.forEach {
|
||||
eventObservable<EVENT>(it).observe(this, o)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified EVENT> LifecycleService.observeEventSticky(
|
||||
vararg tags: String,
|
||||
noinline observer: (EVENT) -> Unit
|
||||
) {
|
||||
val o = Observer<EVENT> {
|
||||
observer(it)
|
||||
}
|
||||
tags.forEach {
|
||||
eventObservable<EVENT>(it).observeSticky(this, o)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.app.DownloadManager
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.provider.DocumentsContract
|
||||
import androidx.core.net.toUri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import io.legato.kazusa.exception.NoStackTraceException
|
||||
import splitties.init.appCtx
|
||||
import splitties.systemservices.downloadManager
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.charset.Charset
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
|
||||
data class FileDoc(
|
||||
val name: String,
|
||||
val isDir: Boolean,
|
||||
val size: Long,
|
||||
val lastModified: Long,
|
||||
val uri: Uri
|
||||
) {
|
||||
|
||||
override fun toString(): String {
|
||||
return if (uri.isContentScheme()) uri.toString() else uri.path!!
|
||||
}
|
||||
|
||||
val isContentScheme get() = uri.isContentScheme()
|
||||
|
||||
fun readBytes(): ByteArray {
|
||||
return uri.readBytes(appCtx)
|
||||
}
|
||||
|
||||
fun readText(): String {
|
||||
return uri.readText(appCtx)
|
||||
}
|
||||
|
||||
fun asDocumentFile(): DocumentFile? {
|
||||
if (isContentScheme) {
|
||||
return if (isDir) {
|
||||
treeDocumentFileConstructor.newInstance(null, appCtx, uri) as DocumentFile
|
||||
} else {
|
||||
DocumentFile.fromSingleUri(appCtx, uri)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun asFile(): File? {
|
||||
if (isContentScheme) {
|
||||
return null
|
||||
}
|
||||
return File(uri.path!!)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val treeDocumentFileConstructor by lazy {
|
||||
Class.forName("androidx.documentfile.provider.TreeDocumentFile")
|
||||
.getDeclaredConstructor(
|
||||
DocumentFile::class.java,
|
||||
Context::class.java,
|
||||
Uri::class.java
|
||||
).apply {
|
||||
isAccessible = true
|
||||
}
|
||||
}
|
||||
|
||||
fun fromDir(path: String): FileDoc {
|
||||
return fromUri(path.toUri(), true)
|
||||
}
|
||||
|
||||
fun fromFile(path: String): FileDoc {
|
||||
return fromUri(path.toUri(), false)
|
||||
}
|
||||
|
||||
fun fromUri(uri: Uri, isDir: Boolean): FileDoc {
|
||||
if (uri.isContentScheme()) {
|
||||
val doc = if (isDir) {
|
||||
DocumentFile.fromTreeUri(appCtx, uri)!!
|
||||
} else if (uri.host == "downloads") {
|
||||
val query = DownloadManager.Query()
|
||||
query.setFilterById(uri.lastPathSegment!!.toLong())
|
||||
downloadManager.query(query).use {
|
||||
if (it.moveToFirst()) {
|
||||
val lUriColum = it.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)
|
||||
val lUri = it.getString(lUriColum)
|
||||
DocumentFile.fromSingleUri(appCtx, Uri.parse(lUri))!!
|
||||
} else {
|
||||
DocumentFile.fromSingleUri(appCtx, uri)!!
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DocumentFile.fromSingleUri(appCtx, uri)!!
|
||||
}
|
||||
return FileDoc(doc.name ?: "", isDir, doc.length(), doc.lastModified(), doc.uri)
|
||||
}
|
||||
val file = File(uri.path!!)
|
||||
return FileDoc(file.name, isDir, file.length(), file.lastModified(), uri)
|
||||
}
|
||||
|
||||
fun fromDocumentFile(doc: DocumentFile): FileDoc {
|
||||
return FileDoc(
|
||||
name = doc.name ?: "",
|
||||
isDir = doc.isDirectory,
|
||||
size = doc.length(),
|
||||
lastModified = doc.lastModified(),
|
||||
uri = doc.uri
|
||||
)
|
||||
}
|
||||
|
||||
fun fromFile(file: File): FileDoc {
|
||||
return FileDoc(
|
||||
name = file.name,
|
||||
isDir = file.isDirectory,
|
||||
size = file.length(),
|
||||
lastModified = file.lastModified(),
|
||||
uri = Uri.fromFile(file)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤器
|
||||
*/
|
||||
typealias FileDocFilter = (file: FileDoc) -> Boolean
|
||||
|
||||
private val projection by lazy {
|
||||
arrayOf(
|
||||
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
||||
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||
DocumentsContract.Document.COLUMN_LAST_MODIFIED,
|
||||
DocumentsContract.Document.COLUMN_SIZE,
|
||||
DocumentsContract.Document.COLUMN_MIME_TYPE
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回子文件列表,如果不是文件夹则返回null
|
||||
*/
|
||||
fun FileDoc.list(filter: FileDocFilter? = null): ArrayList<FileDoc>? {
|
||||
if (isDir) {
|
||||
if (uri.isContentScheme()) {
|
||||
/**
|
||||
* DocumentFile 的 listFiles() 非常的慢,所以这里直接从数据库查询
|
||||
*/
|
||||
val childrenUri = DocumentsContract
|
||||
.buildChildDocumentsUriUsingTree(uri, DocumentsContract.getDocumentId(uri))
|
||||
val docList = arrayListOf<FileDoc>()
|
||||
var cursor: Cursor? = null
|
||||
try {
|
||||
cursor = appCtx.contentResolver.query(
|
||||
childrenUri,
|
||||
projection,
|
||||
null,
|
||||
null,
|
||||
DocumentsContract.Document.COLUMN_DISPLAY_NAME
|
||||
)
|
||||
cursor?.let {
|
||||
val ici = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID)
|
||||
val nci = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME)
|
||||
val sci = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_SIZE)
|
||||
val mci = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_MIME_TYPE)
|
||||
val dci = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_LAST_MODIFIED)
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
val item = FileDoc(
|
||||
name = cursor.getString(nci),
|
||||
isDir = cursor.getString(mci) ==
|
||||
DocumentsContract.Document.MIME_TYPE_DIR,
|
||||
size = cursor.getLong(sci),
|
||||
lastModified = cursor.getLong(dci),
|
||||
uri = DocumentsContract.buildDocumentUriUsingTree(
|
||||
uri,
|
||||
cursor.getString(ici)
|
||||
)
|
||||
)
|
||||
if (filter == null || filter.invoke(item)) {
|
||||
docList.add(item)
|
||||
}
|
||||
} while (cursor.moveToNext())
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
cursor?.close()
|
||||
}
|
||||
return docList
|
||||
} else {
|
||||
return File(uri.path!!).listFileDocs(filter)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找文档, 如果存在则返回文档,如果不存在返回空
|
||||
* @param name 文件名
|
||||
* @param depth 查找文件夹深度
|
||||
*/
|
||||
fun FileDoc.find(name: String, depth: Int = 0): FileDoc? {
|
||||
val list = list()
|
||||
list?.forEach {
|
||||
if (it.name == name) {
|
||||
return it
|
||||
}
|
||||
}
|
||||
if (depth > 0) {
|
||||
list?.forEach {
|
||||
if (it.isDir) {
|
||||
val fileDoc = it.find(name, depth - 1)
|
||||
if (fileDoc != null) {
|
||||
return fileDoc
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找文档, 如果存在则返回文档,如果不存在返回空
|
||||
* @param name 文件名
|
||||
* @param depth 查找文件夹深度
|
||||
* @param maxFinds 最大查找文件夹数量
|
||||
*/
|
||||
fun FileDoc.find(name: String, depth: Int = 0, maxFinds: Int = Int.MAX_VALUE): FileDoc? {
|
||||
return find(name, depth, AtomicInteger(maxFinds))
|
||||
}
|
||||
|
||||
private fun FileDoc.find(name: String, depth: Int, maxFinds: AtomicInteger): FileDoc? {
|
||||
if (maxFinds.getAndDecrement() <= 0) {
|
||||
return null
|
||||
}
|
||||
val list = list()
|
||||
list?.forEach {
|
||||
if (it.name == name) {
|
||||
return it
|
||||
}
|
||||
}
|
||||
if (depth > 0) {
|
||||
list?.forEach {
|
||||
if (it.isDir) {
|
||||
val fileDoc = it.find(name, depth - 1, maxFinds)
|
||||
if (fileDoc != null) {
|
||||
return fileDoc
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun FileDoc.createFileIfNotExist(
|
||||
fileName: String,
|
||||
vararg subDirs: String
|
||||
): FileDoc {
|
||||
return if (uri.isContentScheme()) {
|
||||
val documentFile = asDocumentFile()!!
|
||||
val tmp = DocumentUtils.createFileIfNotExist(documentFile, fileName, *subDirs)!!
|
||||
FileDoc.fromDocumentFile(tmp)
|
||||
} else {
|
||||
val path = FileUtils.getPath(uri.path!!, *subDirs) + File.separator + fileName
|
||||
val tmp = FileUtils.createFileIfNotExist(path)
|
||||
FileDoc.fromFile(tmp)
|
||||
}
|
||||
}
|
||||
|
||||
fun FileDoc.createFolderIfNotExist(
|
||||
vararg subDirs: String
|
||||
): FileDoc {
|
||||
return if (uri.isContentScheme()) {
|
||||
val documentFile = asDocumentFile()!!
|
||||
val tmp = DocumentUtils.createFolderIfNotExist(documentFile, *subDirs)!!
|
||||
FileDoc.fromDocumentFile(tmp)
|
||||
} else {
|
||||
val path = FileUtils.getPath(uri.path!!, *subDirs)
|
||||
val tmp = FileUtils.createFolderIfNotExist(path)
|
||||
FileDoc.fromFile(tmp)
|
||||
}
|
||||
}
|
||||
|
||||
fun FileDoc.openInputStream(): Result<InputStream> {
|
||||
return uri.inputStream(appCtx)
|
||||
}
|
||||
|
||||
fun FileDoc.openOutputStream(): Result<OutputStream> {
|
||||
return uri.outputStream(appCtx)
|
||||
}
|
||||
|
||||
fun FileDoc.openReadPfd(): Result<ParcelFileDescriptor> {
|
||||
return uri.toReadPfd(appCtx)
|
||||
}
|
||||
|
||||
fun FileDoc.openWritePfd(): Result<ParcelFileDescriptor> {
|
||||
return uri.toWritePfd(appCtx)
|
||||
}
|
||||
|
||||
fun FileDoc.exists(
|
||||
fileName: String,
|
||||
vararg subDirs: String
|
||||
): Boolean {
|
||||
return if (uri.isContentScheme()) {
|
||||
DocumentUtils.exists(asDocumentFile()!!, fileName, *subDirs)
|
||||
} else {
|
||||
val path = FileUtils.getPath(uri.path!!, *subDirs) + File.separator + fileName
|
||||
FileUtils.exist(path)
|
||||
}
|
||||
}
|
||||
|
||||
fun FileDoc.exists(): Boolean {
|
||||
return if (uri.isContentScheme()) {
|
||||
asDocumentFile()!!.exists()
|
||||
} else {
|
||||
FileUtils.exist(uri.path!!)
|
||||
}
|
||||
}
|
||||
|
||||
fun FileDoc.writeText(text: String) {
|
||||
if (uri.isContentScheme()) {
|
||||
uri.writeText(appCtx, text)
|
||||
} else {
|
||||
File(uri.path!!).writeText(text)
|
||||
}
|
||||
}
|
||||
|
||||
fun FileDoc.writeFile(file: File) {
|
||||
openOutputStream().getOrThrow().use { out ->
|
||||
file.inputStream().use {
|
||||
it.copyTo(out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun FileDoc.delete() {
|
||||
asFile()?.let {
|
||||
FileUtils.delete(it, true)
|
||||
}
|
||||
asDocumentFile()?.delete()
|
||||
}
|
||||
|
||||
fun FileDoc.checkWrite(): Boolean {
|
||||
if (!isDir) {
|
||||
throw NoStackTraceException("只能检查目录")
|
||||
}
|
||||
asFile()?.let {
|
||||
return it.checkWrite()
|
||||
}
|
||||
return asDocumentFile()!!.checkWrite()
|
||||
}
|
||||
|
||||
/**
|
||||
* DocumentFile 的 listFiles() 非常的慢,尽量不要使用
|
||||
*/
|
||||
fun DocumentFile.listFileDocs(filter: FileDocFilter? = null): ArrayList<FileDoc>? {
|
||||
return FileDoc.fromDocumentFile(this).list(filter)
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun DocumentFile.openInputStream(): InputStream? {
|
||||
return appCtx.contentResolver.openInputStream(uri)
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun DocumentFile.openOutputStream(): OutputStream? {
|
||||
return appCtx.contentResolver.openOutputStream(uri)
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun DocumentFile.writeText(context: Context, data: String, charset: Charset = Charsets.UTF_8) {
|
||||
uri.writeText(context, data, charset)
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun DocumentFile.writeBytes(context: Context, data: ByteArray) {
|
||||
uri.writeBytes(context, data)
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun DocumentFile.readText(context: Context): String {
|
||||
return String(readBytes(context))
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun DocumentFile.readBytes(context: Context): ByteArray {
|
||||
return context.contentResolver.openInputStream(uri)?.let {
|
||||
val len: Int = it.available()
|
||||
val buffer = ByteArray(len)
|
||||
it.read(buffer)
|
||||
it.close()
|
||||
return buffer
|
||||
} ?: throw NoStackTraceException("打开文件失败\n${uri}")
|
||||
}
|
||||
|
||||
fun DocumentFile.checkWrite(): Boolean {
|
||||
var file: DocumentFile? = null
|
||||
return try {
|
||||
val filename = System.currentTimeMillis().toString()
|
||||
file = createFile(FileUtils.getMimeType(filename), filename)
|
||||
file?.openOutputStream()?.let { out ->
|
||||
out.bufferedWriter().use { it.write(filename) }
|
||||
file.openInputStream()?.let { input ->
|
||||
input.bufferedReader().use {
|
||||
return it.readText() == filename
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
} finally {
|
||||
file?.delete()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.net.Uri
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
fun File.getFile(vararg subDirFiles: String): File {
|
||||
val path = FileUtils.getPath(this, *subDirFiles)
|
||||
return File(path)
|
||||
}
|
||||
|
||||
fun File.exists(vararg subDirFiles: String): Boolean {
|
||||
return getFile(*subDirFiles).exists()
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun File.listFileDocs(filter: FileDocFilter? = null): ArrayList<FileDoc> {
|
||||
val docList = arrayListOf<FileDoc>()
|
||||
listFiles()?.forEach {
|
||||
val item = FileDoc(
|
||||
it.name,
|
||||
it.isDirectory,
|
||||
it.length(),
|
||||
it.lastModified(),
|
||||
Uri.fromFile(it)
|
||||
)
|
||||
if (filter == null || filter.invoke(item)) {
|
||||
docList.add(item)
|
||||
}
|
||||
}
|
||||
return docList
|
||||
}
|
||||
|
||||
fun File.createFileIfNotExist(): File {
|
||||
if (!exists()) {
|
||||
parentFile?.createFolderIfNotExist()
|
||||
createNewFile()
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun File.createFileReplace(): File {
|
||||
if (!exists()) {
|
||||
parent?.let {
|
||||
File(it).mkdirs()
|
||||
}
|
||||
createNewFile()
|
||||
} else {
|
||||
delete()
|
||||
createNewFile()
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun File.createFolderIfNotExist(): File {
|
||||
if (!exists()) {
|
||||
mkdirs()
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun File.createFolderReplace(): File {
|
||||
if (exists()) {
|
||||
FileUtils.delete(this, true)
|
||||
}
|
||||
mkdirs()
|
||||
return this
|
||||
}
|
||||
|
||||
fun File.checkWrite(): Boolean {
|
||||
var file: File? = null
|
||||
return try {
|
||||
val filename = System.currentTimeMillis().toString()
|
||||
file = FileUtils.createFileIfNotExist(this, filename)
|
||||
file.outputStream().bufferedWriter().use { it.write(filename) }
|
||||
file.inputStream().bufferedReader().use { it.readText() == filename }
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
} finally {
|
||||
file?.delete()
|
||||
}
|
||||
}
|
||||
|
||||
fun File.outputStream(append: Boolean = false): FileOutputStream {
|
||||
return FileOutputStream(this, append)
|
||||
}
|
||||
@@ -0,0 +1,763 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.os.Environment
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.annotation.IntDef
|
||||
import splitties.init.appCtx
|
||||
import java.io.*
|
||||
import java.nio.charset.Charset
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.regex.Pattern
|
||||
|
||||
@Suppress("unused", "MemberVisibilityCanBePrivate")
|
||||
object FileUtils {
|
||||
|
||||
fun createFileIfNotExist(root: File, vararg subDirFiles: String): File {
|
||||
val filePath = getPath(root, *subDirFiles)
|
||||
return createFileIfNotExist(filePath)
|
||||
}
|
||||
|
||||
fun createFolderIfNotExist(root: File, vararg subDirs: String): File {
|
||||
val filePath = getPath(root, *subDirs)
|
||||
return createFolderIfNotExist(filePath)
|
||||
}
|
||||
|
||||
fun createFolderIfNotExist(filePath: String): File {
|
||||
val file = File(filePath)
|
||||
//如果文件夹不存在,就创建它
|
||||
if (!file.exists()) {
|
||||
file.mkdirs()
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun createFileIfNotExist(filePath: String): File {
|
||||
val file = File(filePath)
|
||||
try {
|
||||
if (!file.exists()) {
|
||||
//创建父类文件夹
|
||||
file.parent?.let {
|
||||
createFolderIfNotExist(it)
|
||||
}
|
||||
//创建文件
|
||||
file.createNewFile()
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
e.printOnDebug()
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
fun createFileWithReplace(filePath: String): File {
|
||||
val file = File(filePath)
|
||||
if (!file.exists()) {
|
||||
//创建父类文件夹
|
||||
file.parent?.let {
|
||||
createFolderIfNotExist(it)
|
||||
}
|
||||
//创建文件
|
||||
file.createNewFile()
|
||||
} else {
|
||||
file.delete()
|
||||
file.createNewFile()
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
fun getPath(rootPath: String, vararg subDirFiles: String): String {
|
||||
val path = StringBuilder(rootPath)
|
||||
subDirFiles.forEach {
|
||||
if (it.isNotEmpty()) {
|
||||
if (!path.endsWith(File.separator)) {
|
||||
path.append(File.separator)
|
||||
}
|
||||
path.append(it)
|
||||
}
|
||||
}
|
||||
return path.toString()
|
||||
}
|
||||
|
||||
fun getPath(root: File, vararg subDirFiles: String): String {
|
||||
val path = StringBuilder(root.absolutePath)
|
||||
subDirFiles.forEach {
|
||||
if (it.isNotEmpty()) {
|
||||
path.append(File.separator).append(it)
|
||||
}
|
||||
}
|
||||
return path.toString()
|
||||
}
|
||||
|
||||
fun getCachePath(): String {
|
||||
return appCtx.externalCache.absolutePath
|
||||
}
|
||||
|
||||
fun getSdCardPath(): String {
|
||||
var sdCardDirectory = Environment.getExternalStorageDirectory().absolutePath
|
||||
try {
|
||||
sdCardDirectory = File(sdCardDirectory).canonicalPath
|
||||
} catch (e: IOException) {
|
||||
e.printOnDebug()
|
||||
}
|
||||
return sdCardDirectory
|
||||
}
|
||||
|
||||
const val BY_NAME_ASC = 0
|
||||
const val BY_NAME_DESC = 1
|
||||
const val BY_TIME_ASC = 2
|
||||
const val BY_TIME_DESC = 3
|
||||
const val BY_SIZE_ASC = 4
|
||||
const val BY_SIZE_DESC = 5
|
||||
const val BY_EXTENSION_ASC = 6
|
||||
const val BY_EXTENSION_DESC = 7
|
||||
|
||||
@IntDef(value = [BY_NAME_ASC, BY_NAME_DESC, BY_TIME_ASC, BY_TIME_DESC, BY_SIZE_ASC, BY_SIZE_DESC, BY_EXTENSION_ASC, BY_EXTENSION_DESC])
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
annotation class SortType
|
||||
|
||||
/**
|
||||
* 将目录分隔符统一为平台默认的分隔符,并为目录结尾添加分隔符
|
||||
*/
|
||||
fun separator(path: String): String {
|
||||
var path1 = path
|
||||
val separator = File.separator
|
||||
path1 = path1.replace("\\", separator)
|
||||
if (!path1.endsWith(separator)) {
|
||||
path1 += separator
|
||||
}
|
||||
return path1
|
||||
}
|
||||
|
||||
fun closeSilently(c: Closeable?) {
|
||||
if (c == null) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
c.close()
|
||||
} catch (ignored: IOException) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出指定目录下的所有子目录
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun listDirs(
|
||||
startDirPath: String,
|
||||
excludeDirs: Array<String>? = null, @SortType sortType: Int = BY_NAME_ASC
|
||||
): Array<File> {
|
||||
var excludeDirs1 = excludeDirs
|
||||
val dirList = ArrayList<File>()
|
||||
val startDir = File(startDirPath)
|
||||
if (!startDir.isDirectory) {
|
||||
return arrayOf()
|
||||
}
|
||||
val dirs = startDir.listFiles(FileFilter { f ->
|
||||
if (f == null) {
|
||||
return@FileFilter false
|
||||
}
|
||||
f.isDirectory
|
||||
}) ?: return arrayOf()
|
||||
if (excludeDirs1 == null) {
|
||||
excludeDirs1 = arrayOf()
|
||||
}
|
||||
for (dir in dirs) {
|
||||
val file = dir.absoluteFile
|
||||
if (!excludeDirs1.contentDeepToString().contains(file.name)) {
|
||||
dirList.add(file)
|
||||
}
|
||||
}
|
||||
when (sortType) {
|
||||
BY_NAME_ASC -> Collections.sort(dirList, SortByName())
|
||||
BY_NAME_DESC -> {
|
||||
Collections.sort(dirList, SortByName())
|
||||
dirList.reverse()
|
||||
}
|
||||
BY_TIME_ASC -> Collections.sort(dirList, SortByTime())
|
||||
BY_TIME_DESC -> {
|
||||
Collections.sort(dirList, SortByTime())
|
||||
dirList.reverse()
|
||||
}
|
||||
BY_SIZE_ASC -> Collections.sort(dirList, SortBySize())
|
||||
BY_SIZE_DESC -> {
|
||||
Collections.sort(dirList, SortBySize())
|
||||
dirList.reverse()
|
||||
}
|
||||
BY_EXTENSION_ASC -> Collections.sort(dirList, SortByExtension())
|
||||
BY_EXTENSION_DESC -> {
|
||||
Collections.sort(dirList, SortByExtension())
|
||||
dirList.reverse()
|
||||
}
|
||||
}
|
||||
return dirList.toTypedArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出指定目录下的所有子目录及所有文件
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun listDirsAndFiles(
|
||||
startDirPath: String,
|
||||
allowExtensions: Array<String>? = null
|
||||
): Array<File>? {
|
||||
val dirs: Array<File>?
|
||||
val files: Array<File>? = if (allowExtensions == null) {
|
||||
listFiles(startDirPath)
|
||||
} else {
|
||||
listFiles(startDirPath, allowExtensions)
|
||||
}
|
||||
dirs = listDirs(startDirPath)
|
||||
if (files == null) {
|
||||
return null
|
||||
}
|
||||
return dirs + files
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出指定目录下的所有文件
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun listFiles(
|
||||
startDirPath: String,
|
||||
filterPattern: Pattern? = null, @SortType sortType: Int = BY_NAME_ASC
|
||||
): Array<File> {
|
||||
val fileList = ArrayList<File>()
|
||||
val f = File(startDirPath)
|
||||
if (!f.isDirectory) {
|
||||
return arrayOf()
|
||||
}
|
||||
val files = f.listFiles(FileFilter { file ->
|
||||
if (file == null) {
|
||||
return@FileFilter false
|
||||
}
|
||||
if (file.isDirectory) {
|
||||
return@FileFilter false
|
||||
}
|
||||
|
||||
filterPattern?.matcher(file.name)?.find() ?: true
|
||||
})
|
||||
?: return arrayOf()
|
||||
for (file in files) {
|
||||
fileList.add(file.absoluteFile)
|
||||
}
|
||||
when (sortType) {
|
||||
BY_NAME_ASC -> Collections.sort(fileList, SortByName())
|
||||
BY_NAME_DESC -> {
|
||||
Collections.sort(fileList, SortByName())
|
||||
fileList.reverse()
|
||||
}
|
||||
BY_TIME_ASC -> Collections.sort(fileList, SortByTime())
|
||||
BY_TIME_DESC -> {
|
||||
Collections.sort(fileList, SortByTime())
|
||||
fileList.reverse()
|
||||
}
|
||||
BY_SIZE_ASC -> Collections.sort(fileList, SortBySize())
|
||||
BY_SIZE_DESC -> {
|
||||
Collections.sort(fileList, SortBySize())
|
||||
fileList.reverse()
|
||||
}
|
||||
BY_EXTENSION_ASC -> Collections.sort(fileList, SortByExtension())
|
||||
BY_EXTENSION_DESC -> {
|
||||
Collections.sort(fileList, SortByExtension())
|
||||
fileList.reverse()
|
||||
}
|
||||
}
|
||||
return fileList.toTypedArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出指定目录下的所有文件
|
||||
*/
|
||||
fun listFiles(startDirPath: String, allowExtensions: Array<String>?): Array<File>? {
|
||||
val file = File(startDirPath)
|
||||
return file.listFiles { _, name ->
|
||||
//返回当前目录所有以某些扩展名结尾的文件
|
||||
val extension = getExtension(name)
|
||||
allowExtensions?.contentDeepToString()?.contains(extension) == true
|
||||
|| allowExtensions == null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出指定目录下的所有文件
|
||||
*/
|
||||
fun listFiles(startDirPath: String, allowExtension: String?): Array<File>? {
|
||||
return if (allowExtension == null)
|
||||
listFiles(startDirPath, allowExtension = null)
|
||||
else
|
||||
listFiles(startDirPath, arrayOf(allowExtension))
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断文件或目录是否存在
|
||||
*/
|
||||
fun exist(path: String): Boolean {
|
||||
val file = File(path)
|
||||
return file.exists()
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件或目录
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun delete(file: File, deleteRootDir: Boolean = false): Boolean {
|
||||
var result = false
|
||||
if (file.isFile) {
|
||||
//是文件
|
||||
result = deleteResolveEBUSY(file)
|
||||
} else {
|
||||
//是目录
|
||||
val files = file.listFiles() ?: return false
|
||||
if (files.isEmpty()) {
|
||||
result = deleteRootDir && deleteResolveEBUSY(file)
|
||||
} else {
|
||||
for (f in files) {
|
||||
delete(f, deleteRootDir)
|
||||
result = deleteResolveEBUSY(f)
|
||||
}
|
||||
}
|
||||
if (deleteRootDir) {
|
||||
result = deleteResolveEBUSY(file)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* bug: open failed: EBUSY (Device or resource busy)
|
||||
* fix: http://stackoverflow.com/questions/11539657/open-failed-ebusy-device-or-resource-busy
|
||||
*/
|
||||
private fun deleteResolveEBUSY(file: File): Boolean {
|
||||
// Before you delete a Directory or File: rename it!
|
||||
val to = File(file.absolutePath + System.currentTimeMillis())
|
||||
|
||||
file.renameTo(to)
|
||||
return to.delete()
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件或目录
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun delete(path: String, deleteRootDir: Boolean = true): Boolean {
|
||||
val file = File(path)
|
||||
|
||||
return if (file.exists()) {
|
||||
delete(file, deleteRootDir)
|
||||
} else false
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制文件为另一个文件,或复制某目录下的所有文件及目录到另一个目录下
|
||||
*/
|
||||
fun copy(src: String, tar: String): Boolean {
|
||||
val srcFile = File(src)
|
||||
return srcFile.exists() && copy(srcFile, File(tar))
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制文件或目录
|
||||
*/
|
||||
fun copy(src: File, tar: File): Boolean {
|
||||
try {
|
||||
if (src.isFile) {
|
||||
val inputStream = FileInputStream(src)
|
||||
val outputStream = FileOutputStream(tar)
|
||||
inputStream.use {
|
||||
outputStream.use {
|
||||
inputStream.copyTo(outputStream)
|
||||
outputStream.flush()
|
||||
}
|
||||
}
|
||||
} else if (src.isDirectory) {
|
||||
tar.mkdirs()
|
||||
src.listFiles()?.forEach { file ->
|
||||
copy(file.absoluteFile, File(tar.absoluteFile, file.name))
|
||||
}
|
||||
}
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动文件或目录
|
||||
*/
|
||||
fun move(src: String, tar: String): Boolean {
|
||||
return move(File(src), File(tar))
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动文件或目录
|
||||
*/
|
||||
fun move(src: File, tar: File): Boolean {
|
||||
return rename(src, tar)
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件重命名
|
||||
*/
|
||||
fun rename(oldPath: String, newPath: String): Boolean {
|
||||
return rename(File(oldPath), File(newPath))
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件重命名
|
||||
*/
|
||||
fun rename(src: File, tar: File): Boolean {
|
||||
return src.renameTo(tar)
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取文本文件, 失败将返回空串
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun readText(filepath: String, charset: String = "utf-8"): String {
|
||||
try {
|
||||
val data = readBytes(filepath)
|
||||
if (data != null) {
|
||||
return String(data, Charset.forName(charset)).trim { it <= ' ' }
|
||||
}
|
||||
} catch (ignored: UnsupportedEncodingException) {
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取文件内容, 失败将返回空串
|
||||
*/
|
||||
fun readBytes(filepath: String): ByteArray? {
|
||||
var fis: FileInputStream? = null
|
||||
try {
|
||||
fis = FileInputStream(filepath)
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
val buffer = ByteArray(1024)
|
||||
while (true) {
|
||||
val len = fis.read(buffer, 0, buffer.size)
|
||||
if (len == -1) {
|
||||
break
|
||||
} else {
|
||||
outputStream.write(buffer, 0, len)
|
||||
}
|
||||
}
|
||||
val data = outputStream.toByteArray()
|
||||
outputStream.close()
|
||||
return data
|
||||
} catch (e: IOException) {
|
||||
return null
|
||||
} finally {
|
||||
closeSilently(fis)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存文本内容
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun writeText(filepath: String, content: String, charset: String = "utf-8"): Boolean {
|
||||
return try {
|
||||
writeBytes(filepath, content.toByteArray(charset(charset)))
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存文件内容
|
||||
*/
|
||||
fun writeBytes(filepath: String, data: ByteArray): Boolean {
|
||||
val file = File(filepath)
|
||||
var fos: FileOutputStream? = null
|
||||
return try {
|
||||
if (!file.exists()) {
|
||||
file.parentFile?.mkdirs()
|
||||
file.createNewFile()
|
||||
}
|
||||
fos = FileOutputStream(filepath)
|
||||
fos.write(data)
|
||||
true
|
||||
} catch (e: IOException) {
|
||||
false
|
||||
} finally {
|
||||
closeSilently(fos)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存文件内容
|
||||
*/
|
||||
fun writeInputStream(filepath: String, data: InputStream): Boolean {
|
||||
val file = File(filepath)
|
||||
return writeInputStream(file, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存文件内容
|
||||
*/
|
||||
fun writeInputStream(file: File, data: InputStream): Boolean {
|
||||
return try {
|
||||
if (!file.exists()) {
|
||||
file.parentFile?.mkdirs()
|
||||
file.createNewFile()
|
||||
}
|
||||
data.use {
|
||||
FileOutputStream(file).use { fos ->
|
||||
data.copyTo(fos)
|
||||
fos.flush()
|
||||
}
|
||||
}
|
||||
true
|
||||
} catch (e: IOException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加文本内容
|
||||
*/
|
||||
fun appendText(path: String, content: String): Boolean {
|
||||
val file = File(path)
|
||||
var writer: FileWriter? = null
|
||||
return try {
|
||||
if (!file.exists()) {
|
||||
file.createNewFile()
|
||||
}
|
||||
writer = FileWriter(file, true)
|
||||
writer.write(content)
|
||||
true
|
||||
} catch (e: IOException) {
|
||||
false
|
||||
} finally {
|
||||
closeSilently(writer)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件大小
|
||||
*/
|
||||
fun getLength(path: String): Long {
|
||||
val file = File(path)
|
||||
return if (!file.isFile || !file.exists()) {
|
||||
0
|
||||
} else file.length()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件或网址的名称(包括后缀)
|
||||
*/
|
||||
fun getName(path: String?): String {
|
||||
if (path == null) {
|
||||
return ""
|
||||
}
|
||||
val pos = path.lastIndexOf(File.separator)
|
||||
return if (0 <= pos) {
|
||||
path.substring(pos + 1)
|
||||
} else {
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件名(不包括扩展名)
|
||||
*/
|
||||
fun getNameExcludeExtension(path: String): String {
|
||||
return try {
|
||||
var fileName = File(path).name
|
||||
val lastIndexOf = fileName.lastIndexOf(".")
|
||||
if (lastIndexOf != -1) {
|
||||
fileName = fileName.substring(0, lastIndexOf)
|
||||
}
|
||||
fileName
|
||||
} catch (e: Exception) {
|
||||
""
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取格式化后的文件大小
|
||||
*/
|
||||
fun getSize(path: String): String {
|
||||
val fileSize = getLength(path)
|
||||
return ConvertUtils.formatFileSize(fileSize)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件后缀,不包括“.”
|
||||
*/
|
||||
fun getExtension(pathOrUrl: String): String {
|
||||
val dotPos = pathOrUrl.lastIndexOf('.')
|
||||
return if (0 <= dotPos) {
|
||||
pathOrUrl.substring(dotPos + 1)
|
||||
} else {
|
||||
"ext"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件的MIME类型
|
||||
*/
|
||||
fun getMimeType(pathOrUrl: String): String {
|
||||
val ext = getExtension(pathOrUrl)
|
||||
val map = MimeTypeMap.getSingleton()
|
||||
return map.getMimeTypeFromExtension(ext) ?: "*/*"
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取格式化后的文件/目录创建或最后修改时间
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun getDateTime(path: String, format: String = "yyyy年MM月dd日HH:mm"): String {
|
||||
val file = File(path)
|
||||
return getDateTime(file, format)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取格式化后的文件/目录创建或最后修改时间
|
||||
*/
|
||||
fun getDateTime(file: File, format: String): String {
|
||||
val cal = Calendar.getInstance()
|
||||
cal.timeInMillis = file.lastModified()
|
||||
return SimpleDateFormat(format, Locale.PRC).format(cal.time)
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较两个文件的最后修改时间
|
||||
*/
|
||||
fun compareLastModified(path1: String, path2: String): Int {
|
||||
val stamp1 = File(path1).lastModified()
|
||||
val stamp2 = File(path2).lastModified()
|
||||
return when {
|
||||
stamp1 > stamp2 -> 1
|
||||
stamp1 < stamp2 -> -1
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建多级别的目录
|
||||
*/
|
||||
fun makeDirs(path: String): Boolean {
|
||||
return makeDirs(File(path))
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建多级别的目录
|
||||
*/
|
||||
fun makeDirs(file: File): Boolean {
|
||||
return file.mkdirs()
|
||||
}
|
||||
|
||||
class SortByExtension : Comparator<File> {
|
||||
|
||||
override fun compare(f1: File?, f2: File?): Int {
|
||||
return if (f1 == null || f2 == null) {
|
||||
if (f1 == null) -1 else 1
|
||||
} else {
|
||||
if (f1.isDirectory && f2.isFile) {
|
||||
-1
|
||||
} else if (f1.isFile && f2.isDirectory) {
|
||||
1
|
||||
} else {
|
||||
f1.name.compareTo(f2.name, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class SortByName : Comparator<File> {
|
||||
private var caseSensitive: Boolean = false
|
||||
|
||||
constructor(caseSensitive: Boolean) {
|
||||
this.caseSensitive = caseSensitive
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.caseSensitive = false
|
||||
}
|
||||
|
||||
override fun compare(f1: File?, f2: File?): Int {
|
||||
if (f1 == null || f2 == null) {
|
||||
return if (f1 == null) {
|
||||
-1
|
||||
} else {
|
||||
1
|
||||
}
|
||||
} else {
|
||||
return if (f1.isDirectory && f2.isFile) {
|
||||
-1
|
||||
} else if (f1.isFile && f2.isDirectory) {
|
||||
1
|
||||
} else {
|
||||
val s1 = f1.name
|
||||
val s2 = f2.name
|
||||
if (caseSensitive) {
|
||||
s1.cnCompare(s2)
|
||||
} else {
|
||||
s1.compareTo(s2, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class SortBySize : Comparator<File> {
|
||||
|
||||
override fun compare(f1: File?, f2: File?): Int {
|
||||
return if (f1 == null || f2 == null) {
|
||||
if (f1 == null) {
|
||||
-1
|
||||
} else {
|
||||
1
|
||||
}
|
||||
} else {
|
||||
if (f1.isDirectory && f2.isFile) {
|
||||
-1
|
||||
} else if (f1.isFile && f2.isDirectory) {
|
||||
1
|
||||
} else {
|
||||
if (f1.length() < f2.length()) {
|
||||
-1
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class SortByTime : Comparator<File> {
|
||||
|
||||
override fun compare(f1: File?, f2: File?): Int {
|
||||
return if (f1 == null || f2 == null) {
|
||||
if (f1 == null) {
|
||||
-1
|
||||
} else {
|
||||
1
|
||||
}
|
||||
} else {
|
||||
if (f1.isDirectory && f2.isFile) {
|
||||
-1
|
||||
} else if (f1.isFile && f2.isDirectory) {
|
||||
1
|
||||
} else {
|
||||
if (f1.lastModified() > f2.lastModified()) {
|
||||
-1
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import io.legato.kazusa.data.appDb
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapMerge
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.produceIn
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
inline fun <T> Flow<T>.onEachParallel(
|
||||
concurrency: Int,
|
||||
crossinline action: suspend (T) -> Unit
|
||||
): Flow<T> = flatMapMerge(concurrency) { value ->
|
||||
flow {
|
||||
action(value)
|
||||
emit(value)
|
||||
}
|
||||
}.buffer(0)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
inline fun <T> Flow<T>.onEachParallelSafe(
|
||||
concurrency: Int,
|
||||
crossinline action: suspend (T) -> Unit
|
||||
): Flow<T> = flatMapMerge(concurrency) { value ->
|
||||
flow {
|
||||
try {
|
||||
action(value)
|
||||
} catch (e: Throwable) {
|
||||
coroutineContext.ensureActive()
|
||||
}
|
||||
emit(value)
|
||||
}
|
||||
}.buffer(0)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
inline fun <T, R> Flow<T>.mapParallel(
|
||||
concurrency: Int,
|
||||
crossinline transform: suspend (T) -> R,
|
||||
): Flow<R> = flatMapMerge(concurrency) { value -> flow { emit(transform(value)) } }.buffer(0)
|
||||
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
inline fun <T, R> Flow<T>.mapParallelSafe(
|
||||
concurrency: Int,
|
||||
crossinline transform: suspend (T) -> R,
|
||||
): Flow<R> = flatMapMerge(concurrency) { value ->
|
||||
flow {
|
||||
try {
|
||||
emit(transform(value))
|
||||
} catch (_: Throwable) {
|
||||
coroutineContext.ensureActive()
|
||||
}
|
||||
}
|
||||
}.buffer(0)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
inline fun <T, R> Flow<T>.transformParallelSafe(
|
||||
concurrency: Int,
|
||||
crossinline transform: suspend FlowCollector<R>.(T) -> R,
|
||||
): Flow<R> = flatMapMerge(concurrency) { value ->
|
||||
flow {
|
||||
try {
|
||||
transform(value)
|
||||
} catch (e: Throwable) {
|
||||
coroutineContext.ensureActive()
|
||||
}
|
||||
}
|
||||
}.buffer(0)
|
||||
|
||||
inline fun <T, R> Flow<T>.mapNotNullParallel(
|
||||
concurrency: Int,
|
||||
crossinline transform: suspend (T) -> R?,
|
||||
): Flow<R> = mapParallel(concurrency, transform).filterNotNull()
|
||||
|
||||
inline fun <T> Flow<T>.onEachIndexed(
|
||||
crossinline action: suspend (index: Int, T) -> Unit,
|
||||
): Flow<T> = flow {
|
||||
var index = 0
|
||||
collect { value ->
|
||||
action(index++, value)
|
||||
emit(value)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T, R> Flow<T>.mapIndexed(
|
||||
crossinline action: suspend (index: Int, T) -> R,
|
||||
): Flow<R> = flow {
|
||||
var index = 0
|
||||
collect { value ->
|
||||
emit(action(index++, value))
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T, R> Flow<T>.mapAsync(
|
||||
concurrency: Int,
|
||||
crossinline transform: suspend (T) -> R
|
||||
): Flow<R> = if (concurrency == 1) {
|
||||
map { transform(it) }
|
||||
} else {
|
||||
Semaphore(concurrency).let { semaphore ->
|
||||
channelFlow {
|
||||
collect {
|
||||
semaphore.acquire()
|
||||
send(async { transform(it) })
|
||||
}
|
||||
}.map {
|
||||
it.await()
|
||||
}.onEach { semaphore.release() }
|
||||
}.buffer(0)
|
||||
}
|
||||
|
||||
inline fun <T, R> Flow<T>.mapAsyncIndexed(
|
||||
concurrency: Int,
|
||||
crossinline transform: suspend (index: Int, T) -> R
|
||||
): Flow<R> = if (concurrency == 1) {
|
||||
mapIndexed { index, value ->
|
||||
transform(index, value)
|
||||
}
|
||||
} else {
|
||||
Semaphore(concurrency).let { semaphore ->
|
||||
channelFlow {
|
||||
var index = 0
|
||||
collect {
|
||||
semaphore.acquire()
|
||||
val i = index++
|
||||
send(async { transform(i, it) })
|
||||
}
|
||||
}.map {
|
||||
it.await()
|
||||
}.onEach { semaphore.release() }
|
||||
}.buffer(0)
|
||||
}
|
||||
|
||||
inline fun <T> Flow<T>.onEachAsync(
|
||||
concurrency: Int,
|
||||
crossinline action: suspend (T) -> Unit
|
||||
): Flow<T> = if (concurrency == 1) {
|
||||
onEach { action(it) }
|
||||
} else {
|
||||
Semaphore(concurrency).let { semaphore ->
|
||||
channelFlow {
|
||||
collect {
|
||||
semaphore.acquire()
|
||||
send(async {
|
||||
action(it)
|
||||
it
|
||||
})
|
||||
}
|
||||
}.map {
|
||||
it.await()
|
||||
}.onEach { semaphore.release() }
|
||||
}.buffer(0)
|
||||
}
|
||||
|
||||
inline fun <T> Flow<T>.onEachAsyncIndexed(
|
||||
concurrency: Int,
|
||||
crossinline action: suspend (index: Int, T) -> Unit
|
||||
): Flow<T> = if (concurrency == 1) {
|
||||
onEachIndexed { index, value ->
|
||||
action(index, value)
|
||||
}
|
||||
} else {
|
||||
Semaphore(concurrency).let { semaphore ->
|
||||
channelFlow {
|
||||
var index = 0
|
||||
collect {
|
||||
semaphore.acquire()
|
||||
val i = index++
|
||||
send(async {
|
||||
action(i, it)
|
||||
it
|
||||
})
|
||||
}
|
||||
}.map {
|
||||
it.await()
|
||||
}.onEach { semaphore.release() }
|
||||
}.buffer(0)
|
||||
}
|
||||
|
||||
fun <T> Flow<T>.flowWithLifecycleFirst(
|
||||
lifecycle: Lifecycle,
|
||||
minActiveState: Lifecycle.State = Lifecycle.State.STARTED
|
||||
): Flow<T> = callbackFlow {
|
||||
if (!lifecycle.currentState.isAtLeast(minActiveState)) {
|
||||
send(first())
|
||||
}
|
||||
lifecycle.repeatOnLifecycle(minActiveState) {
|
||||
this@flowWithLifecycleFirst.collect {
|
||||
send(it)
|
||||
}
|
||||
}
|
||||
close()
|
||||
}
|
||||
|
||||
fun <T> Flow<T>.flowWithLifecycleAndDatabaseChange(
|
||||
lifecycle: Lifecycle,
|
||||
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
|
||||
table: String
|
||||
): Flow<T> = callbackFlow {
|
||||
var update = 0
|
||||
val channel = appDb.invalidationTracker
|
||||
.createFlow(table)
|
||||
.conflate()
|
||||
.onEach { update++ }
|
||||
.produceIn(this)
|
||||
lifecycle.repeatOnLifecycle(minActiveState) {
|
||||
if (update == 0) {
|
||||
channel.receive()
|
||||
}
|
||||
this@flowWithLifecycleAndDatabaseChange.collect {
|
||||
update = 0
|
||||
send(it)
|
||||
}
|
||||
}
|
||||
close()
|
||||
}
|
||||
|
||||
fun <T> Flow<T>.flowWithLifecycleAndDatabaseChangeFirst(
|
||||
lifecycle: Lifecycle,
|
||||
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
|
||||
table: String
|
||||
): Flow<T> = callbackFlow {
|
||||
var update = 0
|
||||
val isActive = lifecycle.currentState.isAtLeast(minActiveState)
|
||||
val channel = appDb.invalidationTracker
|
||||
.createFlow(table, emitInitialState = isActive)
|
||||
.conflate()
|
||||
.onEach { update++ }
|
||||
.produceIn(this)
|
||||
if (!isActive) {
|
||||
send(first())
|
||||
}
|
||||
lifecycle.repeatOnLifecycle(minActiveState) {
|
||||
if (update == 0) {
|
||||
channel.receive()
|
||||
}
|
||||
this@flowWithLifecycleAndDatabaseChangeFirst.collect {
|
||||
update = 0
|
||||
send(it)
|
||||
}
|
||||
}
|
||||
close()
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.core.content.edit
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.data.entities.Book
|
||||
import io.legato.kazusa.help.book.isAudio
|
||||
import io.legato.kazusa.help.book.isImage
|
||||
import io.legato.kazusa.help.config.AppConfig
|
||||
import io.legato.kazusa.ui.book.audio.AudioPlayActivity
|
||||
import io.legato.kazusa.ui.book.manga.ReadMangaActivity
|
||||
import io.legato.kazusa.ui.book.read.ReadBookActivity
|
||||
import io.legato.kazusa.ui.widget.dialog.TextDialog
|
||||
|
||||
inline fun <reified T : DialogFragment> Fragment.showDialogFragment(
|
||||
arguments: Bundle.() -> Unit = {}
|
||||
) {
|
||||
val dialog = T::class.java.newInstance()
|
||||
val bundle = Bundle()
|
||||
bundle.apply(arguments)
|
||||
dialog.arguments = bundle
|
||||
dialog.show(childFragmentManager, T::class.simpleName)
|
||||
}
|
||||
|
||||
fun Fragment.showDialogFragment(dialogFragment: DialogFragment) {
|
||||
dialogFragment.show(childFragmentManager, dialogFragment::class.simpleName)
|
||||
}
|
||||
|
||||
fun Fragment.getPrefBoolean(key: String, defValue: Boolean = false) =
|
||||
requireContext().defaultSharedPreferences.getBoolean(key, defValue)
|
||||
|
||||
fun Fragment.putPrefBoolean(key: String, value: Boolean = false) =
|
||||
requireContext().defaultSharedPreferences.edit { putBoolean(key, value) }
|
||||
|
||||
fun Fragment.getPrefInt(key: String, defValue: Int = 0) =
|
||||
requireContext().defaultSharedPreferences.getInt(key, defValue)
|
||||
|
||||
fun Fragment.putPrefInt(key: String, value: Int) =
|
||||
requireContext().defaultSharedPreferences.edit { putInt(key, value) }
|
||||
|
||||
fun Fragment.getPrefLong(key: String, defValue: Long = 0L) =
|
||||
requireContext().defaultSharedPreferences.getLong(key, defValue)
|
||||
|
||||
fun Fragment.putPrefLong(key: String, value: Long) =
|
||||
requireContext().defaultSharedPreferences.edit { putLong(key, value) }
|
||||
|
||||
fun Fragment.getPrefString(key: String, defValue: String? = null) =
|
||||
requireContext().defaultSharedPreferences.getString(key, defValue)
|
||||
|
||||
fun Fragment.putPrefString(key: String, value: String) =
|
||||
requireContext().defaultSharedPreferences.edit { putString(key, value) }
|
||||
|
||||
fun Fragment.getPrefStringSet(
|
||||
key: String,
|
||||
defValue: MutableSet<String>? = null
|
||||
): MutableSet<String>? =
|
||||
requireContext().defaultSharedPreferences.getStringSet(key, defValue)
|
||||
|
||||
fun Fragment.putPrefStringSet(key: String, value: MutableSet<String>) =
|
||||
requireContext().defaultSharedPreferences.edit { putStringSet(key, value) }
|
||||
|
||||
fun Fragment.removePref(key: String) =
|
||||
requireContext().defaultSharedPreferences.edit { remove(key) }
|
||||
|
||||
fun Fragment.getCompatColor(@ColorRes id: Int): Int = requireContext().getCompatColor(id)
|
||||
|
||||
fun Fragment.getCompatDrawable(@DrawableRes id: Int): Drawable? =
|
||||
requireContext().getCompatDrawable(id)
|
||||
|
||||
fun Fragment.getCompatColorStateList(@ColorRes id: Int): ColorStateList? =
|
||||
requireContext().getCompatColorStateList(id)
|
||||
|
||||
inline fun <reified T : Activity> Fragment.startActivity(
|
||||
configIntent: Intent.() -> Unit = {}
|
||||
) {
|
||||
startActivity(Intent(requireContext(), T::class.java).apply(configIntent))
|
||||
}
|
||||
|
||||
fun Fragment.startActivityForBook(
|
||||
book: Book,
|
||||
configIntent: Intent.() -> Unit = {},
|
||||
) {
|
||||
val cls = when {
|
||||
book.isAudio -> AudioPlayActivity::class.java
|
||||
book.isImage && AppConfig.showMangaUi -> ReadMangaActivity::class.java
|
||||
else -> ReadBookActivity::class.java
|
||||
}
|
||||
val intent = Intent(requireActivity(), cls)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
intent.putExtra("bookUrl", book.bookUrl)
|
||||
intent.apply(configIntent)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
fun Fragment.showHelp(fileName: String) {
|
||||
val mdText = String(requireContext().assets.open("web/help/md/${fileName}.md").readBytes())
|
||||
showDialogFragment(TextDialog(getString(R.string.help), mdText, TextDialog.Mode.MD))
|
||||
}
|
||||
|
||||
val Fragment.isCreated
|
||||
get() = lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)
|
||||
@@ -0,0 +1,244 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonParseException
|
||||
import com.google.gson.JsonSyntaxException
|
||||
import com.google.gson.Strictness
|
||||
import com.google.gson.ToNumberPolicy
|
||||
import com.google.gson.internal.LinkedTreeMap
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.google.gson.stream.JsonWriter
|
||||
import io.legato.kazusa.data.entities.rule.BookInfoRule
|
||||
import io.legato.kazusa.data.entities.rule.ContentRule
|
||||
import io.legato.kazusa.data.entities.rule.ExploreRule
|
||||
import io.legato.kazusa.data.entities.rule.ReviewRule
|
||||
import io.legato.kazusa.data.entities.rule.SearchRule
|
||||
import io.legato.kazusa.data.entities.rule.TocRule
|
||||
import java.io.InputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.io.OutputStream
|
||||
import java.io.OutputStreamWriter
|
||||
import java.lang.reflect.Type
|
||||
import kotlin.math.ceil
|
||||
|
||||
val INITIAL_GSON: Gson by lazy {
|
||||
GsonBuilder()
|
||||
.registerTypeAdapter(
|
||||
object : TypeToken<Map<String?, Any?>?>() {}.type,
|
||||
MapDeserializerDoubleAsIntFix()
|
||||
)
|
||||
.registerTypeAdapter(Int::class.java, IntJsonDeserializer())
|
||||
.registerTypeAdapter(String::class.java, StringJsonDeserializer())
|
||||
.setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE)
|
||||
.disableHtmlEscaping()
|
||||
.setPrettyPrinting()
|
||||
.create()
|
||||
}
|
||||
|
||||
val GSON: Gson by lazy {
|
||||
INITIAL_GSON.newBuilder()
|
||||
.registerTypeAdapter(ExploreRule::class.java, ExploreRule.jsonDeserializer)
|
||||
.registerTypeAdapter(SearchRule::class.java, SearchRule.jsonDeserializer)
|
||||
.registerTypeAdapter(BookInfoRule::class.java, BookInfoRule.jsonDeserializer)
|
||||
.registerTypeAdapter(TocRule::class.java, TocRule.jsonDeserializer)
|
||||
.registerTypeAdapter(ContentRule::class.java, ContentRule.jsonDeserializer)
|
||||
.registerTypeAdapter(ReviewRule::class.java, ReviewRule.jsonDeserializer)
|
||||
.create()
|
||||
}
|
||||
|
||||
val GSONStrict: Gson by lazy {
|
||||
GSON.newBuilder()
|
||||
.setStrictness(Strictness.STRICT)
|
||||
.create()
|
||||
}
|
||||
|
||||
inline fun <reified T> genericType(): Type = object : TypeToken<T>() {}.type
|
||||
|
||||
inline fun <reified T> Gson.fromJsonObject(json: String?): Result<T> {
|
||||
return kotlin.runCatching {
|
||||
if (json == null) {
|
||||
throw JsonSyntaxException("解析字符串为空")
|
||||
}
|
||||
fromJson(json, genericType<T>()) as T
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified T> Gson.fromJsonArray(json: String?): Result<List<T>> {
|
||||
return kotlin.runCatching {
|
||||
if (json == null) {
|
||||
throw JsonSyntaxException("解析字符串为空")
|
||||
}
|
||||
val type = TypeToken.getParameterized(List::class.java, T::class.java).type
|
||||
val list = fromJson(json, type) as List<T?>
|
||||
if (list.contains(null)) {
|
||||
throw JsonSyntaxException(
|
||||
"列表不能存在null元素,可能是json格式错误,通常为列表存在多余的逗号所致"
|
||||
)
|
||||
}
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
list as List<T>
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified T> Gson.fromJsonObject(inputStream: InputStream?): Result<T> {
|
||||
return kotlin.runCatching {
|
||||
if (inputStream == null) {
|
||||
throw JsonSyntaxException("解析流为空")
|
||||
}
|
||||
val reader = InputStreamReader(inputStream)
|
||||
fromJson(reader, genericType<T>()) as T
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified T> Gson.fromJsonArray(inputStream: InputStream?): Result<List<T>> {
|
||||
return kotlin.runCatching {
|
||||
if (inputStream == null) {
|
||||
throw JsonSyntaxException("解析流为空")
|
||||
}
|
||||
val reader = InputStreamReader(inputStream)
|
||||
val type = TypeToken.getParameterized(List::class.java, T::class.java).type
|
||||
val list = fromJson(reader, type) as List<T?>
|
||||
if (list.contains(null)) {
|
||||
throw JsonSyntaxException(
|
||||
"列表不能存在null元素,可能是json格式错误,通常为列表存在多余的逗号所致"
|
||||
)
|
||||
}
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
list as List<T>
|
||||
}
|
||||
}
|
||||
|
||||
fun Gson.writeToOutputStream(out: OutputStream, any: Any) {
|
||||
val writer = JsonWriter(OutputStreamWriter(out, "UTF-8"))
|
||||
writer.setIndent(" ")
|
||||
if (any is List<*>) {
|
||||
writer.beginArray()
|
||||
any.forEach {
|
||||
it?.let {
|
||||
toJson(it, it::class.java, writer)
|
||||
}
|
||||
}
|
||||
writer.endArray()
|
||||
} else {
|
||||
toJson(any, any::class.java, writer)
|
||||
}
|
||||
writer.close()
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class StringJsonDeserializer : JsonDeserializer<String?> {
|
||||
|
||||
override fun deserialize(
|
||||
json: JsonElement,
|
||||
typeOfT: Type,
|
||||
context: JsonDeserializationContext?
|
||||
): String? {
|
||||
return when {
|
||||
json.isJsonPrimitive -> json.asString
|
||||
json.isJsonNull -> null
|
||||
else -> json.toString()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* int类型转化失败时跳过
|
||||
*/
|
||||
class IntJsonDeserializer : JsonDeserializer<Int?> {
|
||||
|
||||
override fun deserialize(
|
||||
json: JsonElement,
|
||||
typeOfT: Type?,
|
||||
context: JsonDeserializationContext?
|
||||
): Int? {
|
||||
return when {
|
||||
json.isJsonPrimitive -> {
|
||||
val prim = json.asJsonPrimitive
|
||||
if (prim.isNumber) {
|
||||
prim.asNumber.toInt()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 修复Int变为Double的问题
|
||||
*/
|
||||
class MapDeserializerDoubleAsIntFix :
|
||||
JsonDeserializer<Map<String, Any?>?> {
|
||||
|
||||
@Throws(JsonParseException::class)
|
||||
override fun deserialize(
|
||||
jsonElement: JsonElement,
|
||||
type: Type,
|
||||
jsonDeserializationContext: JsonDeserializationContext
|
||||
): Map<String, Any?>? {
|
||||
@Suppress("unchecked_cast")
|
||||
return read(jsonElement) as? Map<String, Any?>
|
||||
}
|
||||
|
||||
fun read(json: JsonElement): Any? {
|
||||
when {
|
||||
json.isJsonArray -> {
|
||||
val list: MutableList<Any?> = ArrayList()
|
||||
val arr = json.asJsonArray
|
||||
for (anArr in arr) {
|
||||
list.add(read(anArr))
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
json.isJsonObject -> {
|
||||
val map: MutableMap<String, Any?> =
|
||||
LinkedTreeMap()
|
||||
val obj = json.asJsonObject
|
||||
val entitySet =
|
||||
obj.entrySet()
|
||||
for ((key, value) in entitySet) {
|
||||
map[key] = read(value)
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
json.isJsonPrimitive -> {
|
||||
val prim = json.asJsonPrimitive
|
||||
when {
|
||||
prim.isBoolean -> {
|
||||
return prim.asBoolean
|
||||
}
|
||||
|
||||
prim.isString -> {
|
||||
return prim.asString
|
||||
}
|
||||
|
||||
prim.isNumber -> {
|
||||
val num: Number = prim.asNumber
|
||||
// here you can handle double int/long values
|
||||
// and return any type you want
|
||||
// this solution will transform 3.0 float to long values
|
||||
return if (ceil(num.toDouble()) == num.toLong().toDouble()) {
|
||||
num.toLong()
|
||||
} else {
|
||||
num.toDouble()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/** This main looper cache avoids synchronization overhead when accessed repeatedly. */
|
||||
private val mainLooper: Looper = Looper.getMainLooper()
|
||||
|
||||
private val mainThread: Thread = mainLooper.thread
|
||||
|
||||
val isMainThread: Boolean get() = mainThread === Thread.currentThread()
|
||||
|
||||
fun buildMainHandler(): Handler {
|
||||
return if (SDK_INT >= 28) Handler.createAsync(mainLooper) else try {
|
||||
Handler::class.java.getDeclaredConstructor(
|
||||
Looper::class.java,
|
||||
Handler.Callback::class.java,
|
||||
Boolean::class.javaPrimitiveType // async
|
||||
).newInstance(mainLooper, null, true)
|
||||
} catch (ignored: NoSuchMethodException) {
|
||||
// Hidden constructor absent. Fall back to non-async constructor.
|
||||
Handler(mainLooper)
|
||||
}
|
||||
}
|
||||
|
||||
private val mainHandler by lazy { buildMainHandler() }
|
||||
|
||||
fun runOnUI(function: () -> Unit) {
|
||||
if (isMainThread) {
|
||||
function()
|
||||
} else {
|
||||
mainHandler.post(function)
|
||||
}
|
||||
}
|
||||
|
||||
fun CoroutineScope.runOnIO(function: () -> Unit) {
|
||||
if (isMainThread) {
|
||||
launch(IO) {
|
||||
function()
|
||||
}
|
||||
} else {
|
||||
function()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import io.legato.kazusa.model.analyzeRule.AnalyzeUrl
|
||||
import java.net.URL
|
||||
import java.util.regex.Pattern
|
||||
|
||||
@Suppress("RegExpRedundantEscape")
|
||||
object HtmlFormatter {
|
||||
private val nbspRegex = "( )+".toRegex()
|
||||
private val espRegex = "( | )".toRegex()
|
||||
private val noPrintRegex = "( |‌|‍|\u2009|\u200C|\u200D)".toRegex()
|
||||
private val wrapHtmlRegex = "</?(?:div|p|br|hr|h\\d|article|dd|dl)[^>]*>".toRegex()
|
||||
private val commentRegex = "<!--[^>]*-->".toRegex() //注释
|
||||
private val notImgHtmlRegex = "</?(?!img)[a-zA-Z]+(?=[ >])[^<>]*>".toRegex()
|
||||
private val otherHtmlRegex = "</?[a-zA-Z]+(?=[ >])[^<>]*>".toRegex()
|
||||
private val formatImagePattern = Pattern.compile(
|
||||
"<img[^>]*\\ssrc\\s*=\\s*\"([^\"{>]*\\{(?:[^{}]|\\{[^}>]+\\})+\\})\"[^>]*>|<img[^>]*\\sdata-[^=>]*=\\s*\"([^\">]*)\"[^>]*>|<img[^>]*\\ssrc\\s*=\\s*\"([^\">]*)\"[^>]*>",
|
||||
Pattern.CASE_INSENSITIVE
|
||||
)
|
||||
private val indent1Regex = "\\s*\\n+\\s*".toRegex()
|
||||
private val indent2Regex = "^[\\n\\s]+".toRegex()
|
||||
private val lastRegex = "[\\n\\s]+$".toRegex()
|
||||
|
||||
fun format(html: String?, otherRegex: Regex = otherHtmlRegex): String {
|
||||
html ?: return ""
|
||||
return html.replace(nbspRegex, " ")
|
||||
.replace(espRegex, " ")
|
||||
.replace(noPrintRegex, "")
|
||||
.replace(wrapHtmlRegex, "\n")
|
||||
.replace(commentRegex, "")
|
||||
.replace(otherRegex, "")
|
||||
.replace(indent1Regex, "\n ")
|
||||
.replace(indent2Regex, " ")
|
||||
.replace(lastRegex, "")
|
||||
}
|
||||
|
||||
fun formatKeepImg(html: String?, redirectUrl: URL? = null): String {
|
||||
html ?: return ""
|
||||
val keepImgHtml = format(html, notImgHtmlRegex)
|
||||
|
||||
//正则的“|”处于顶端而不处于()中时,具有类似||的熔断效果,故以此机制简化原来的代码
|
||||
val matcher = formatImagePattern.matcher(keepImgHtml)
|
||||
var appendPos = 0
|
||||
val sb = StringBuilder()
|
||||
while (matcher.find()) {
|
||||
var param = ""
|
||||
sb.append(
|
||||
keepImgHtml.substring(appendPos, matcher.start()), "<img src=\"${
|
||||
NetworkUtils.getAbsoluteURL(
|
||||
redirectUrl,
|
||||
matcher.group(1)?.let {
|
||||
val urlMatcher = AnalyzeUrl.paramPattern.matcher(it)
|
||||
if (urlMatcher.find()) {
|
||||
param = ',' + it.substring(urlMatcher.end())
|
||||
it.substring(0, urlMatcher.start())
|
||||
} else it
|
||||
} ?: matcher.group(2) ?: matcher.group(3)!!
|
||||
) + param
|
||||
}\">"
|
||||
)
|
||||
appendPos = matcher.end()
|
||||
}
|
||||
if (appendPos < keepImgHtml.length) sb.append(
|
||||
keepImgHtml.substring(
|
||||
appendPos,
|
||||
keepImgHtml.length
|
||||
)
|
||||
)
|
||||
return sb.toString()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import io.legato.kazusa.constant.AppLog
|
||||
import io.legato.kazusa.data.entities.BaseSource
|
||||
import io.legato.kazusa.data.entities.Book
|
||||
import io.legato.kazusa.data.entities.BookSource
|
||||
import io.legato.kazusa.data.entities.RssSource
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* 加密图片解密工具
|
||||
*/
|
||||
object ImageUtils {
|
||||
|
||||
/**
|
||||
* @param isCover 根据这个执行书源中不同的解密规则
|
||||
* @return 解密失败返回Null 解密规则为空不处理
|
||||
*/
|
||||
fun decode(
|
||||
src: String, bytes: ByteArray, isCover: Boolean,
|
||||
source: BaseSource?, book: Book? = null
|
||||
): ByteArray? {
|
||||
val ruleJs = getRuleJs(source, isCover)
|
||||
if (ruleJs.isNullOrBlank()) return bytes
|
||||
//解密库hutool.crypto ByteArray|InputStream -> ByteArray
|
||||
return kotlin.runCatching {
|
||||
source?.evalJS(ruleJs) {
|
||||
put("book", book)
|
||||
put("result", bytes)
|
||||
put("src", src)
|
||||
} as ByteArray
|
||||
}.onFailure {
|
||||
AppLog.putDebug("${src}解密错误", it)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
fun decode(
|
||||
src: String, inputStream: InputStream, isCover: Boolean,
|
||||
source: BaseSource?, book: Book? = null
|
||||
): InputStream? {
|
||||
val ruleJs = getRuleJs(source, isCover)
|
||||
if (ruleJs.isNullOrBlank()) return inputStream
|
||||
//解密库hutool.crypto ByteArray|InputStream -> ByteArray
|
||||
return kotlin.runCatching {
|
||||
val bytes = source?.evalJS(ruleJs) {
|
||||
put("book", book)
|
||||
put("result", inputStream)
|
||||
put("src", src)
|
||||
} as ByteArray
|
||||
ByteArrayInputStream(bytes)
|
||||
}.onFailure {
|
||||
AppLog.putDebug("${src}解密错误", it)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
fun skipDecode(source: BaseSource?, isCover: Boolean): Boolean {
|
||||
return getRuleJs(source, isCover).isNullOrBlank()
|
||||
}
|
||||
|
||||
private fun getRuleJs(
|
||||
source: BaseSource?, isCover: Boolean
|
||||
): String? {
|
||||
return when (source) {
|
||||
is BookSource ->
|
||||
if (isCover) source.coverDecodeJs
|
||||
else source.getContentRule().imageDecode
|
||||
|
||||
is RssSource -> source.coverDecodeJs
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import java.io.InputStream
|
||||
import java.util.*
|
||||
|
||||
fun InputStream?.isJson(): Boolean {
|
||||
this ?: return false
|
||||
this.use {
|
||||
val byteArray = ByteArray(128)
|
||||
it.read(byteArray)
|
||||
val a = String(byteArray).trim()
|
||||
it.skip(it.available() - 128L)
|
||||
it.read(byteArray)
|
||||
val b = String(byteArray).trim()
|
||||
return (a + b).isJson()
|
||||
}
|
||||
}
|
||||
|
||||
fun InputStream?.contains(str: String): Boolean {
|
||||
this ?: return false
|
||||
this.use {
|
||||
val scanner = Scanner(it)
|
||||
return scanner.findWithinHorizon(str, 0) != null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.content.Intent
|
||||
|
||||
fun Intent.putJson(key: String, any: Any?) {
|
||||
any?.let {
|
||||
putExtra(key, GSON.toJson(any))
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified T> Intent.getJsonObject(key: String): T? {
|
||||
val value = getStringExtra(key)
|
||||
return GSON.fromJsonObject<T>(value).getOrNull()
|
||||
}
|
||||
|
||||
inline fun <reified T> Intent.getJsonArray(key: String): List<T>? {
|
||||
val value = getStringExtra(key)
|
||||
return GSON.fromJsonArray<T>(value).getOrNull()
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.net.Uri
|
||||
import java.io.File
|
||||
|
||||
object IntentType {
|
||||
|
||||
fun from(uri: Uri): String {
|
||||
return from(uri.toString())
|
||||
}
|
||||
|
||||
fun from(file: File): String {
|
||||
return from(file.absolutePath)
|
||||
}
|
||||
|
||||
fun from(path: String?): String {
|
||||
val suffix = path
|
||||
?.substringAfterLast(File.separator)
|
||||
?.substringAfterLast(".", "")
|
||||
?.lowercase()
|
||||
return when (suffix) {
|
||||
"m4a", "mp3", "mid", "xmf", "ogg", "wav" -> "video/*"
|
||||
"3gp", "mp4" -> "audio/*"
|
||||
"jpg", "gif", "png", "jpeg", "bmp" -> "image/*"
|
||||
"", "txt", "json", "log" -> "text/plain"
|
||||
"apk" -> "application/vnd.android.package-archive"
|
||||
else -> "*/*"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import java.net.URL
|
||||
import java.net.URLDecoder
|
||||
|
||||
@Keep
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
class JsURL(url: String, baseUrl: String? = null) {
|
||||
|
||||
val searchParams: Map<String, String>?
|
||||
val host: String
|
||||
val origin: String
|
||||
val pathname: String
|
||||
|
||||
init {
|
||||
val mUrl = if (!baseUrl.isNullOrEmpty()) {
|
||||
val base = URL(baseUrl)
|
||||
URL(base, url)
|
||||
} else {
|
||||
URL(url)
|
||||
}
|
||||
host = mUrl.host
|
||||
origin = if (mUrl.port > 0) {
|
||||
"${mUrl.protocol}://$host:${mUrl}:${mUrl.port}"
|
||||
} else {
|
||||
"${mUrl.protocol}://$host:${mUrl}"
|
||||
}
|
||||
pathname = mUrl.path
|
||||
val query = mUrl.query
|
||||
searchParams = query?.let { _ ->
|
||||
val map = hashMapOf<String, String>()
|
||||
query.split("&").forEach {
|
||||
val x = it.split("=", limit = 2)
|
||||
map[x[0]] = URLDecoder.decode(x[1], "utf-8")
|
||||
}
|
||||
map
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import com.jayway.jsonpath.*
|
||||
|
||||
val jsonPath: ParseContext by lazy {
|
||||
JsonPath.using(
|
||||
Configuration.builder()
|
||||
.options(Option.SUPPRESS_EXCEPTIONS)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
fun ReadContext.readString(path: String): String? = this.read(path, String::class.java)
|
||||
|
||||
fun ReadContext.readBool(path: String): Boolean? = this.read(path, Boolean::class.java)
|
||||
|
||||
fun ReadContext.readInt(path: String): Int? = this.read(path, Int::class.java)
|
||||
|
||||
fun ReadContext.readLong(path: String): Long? = this.read(path, Long::class.java)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import org.jsoup.internal.StringUtil
|
||||
import org.jsoup.nodes.CDataNode
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.nodes.Node
|
||||
import org.jsoup.nodes.TextNode
|
||||
import org.jsoup.select.Elements
|
||||
import org.jsoup.select.NodeTraversor
|
||||
import org.jsoup.select.NodeVisitor
|
||||
|
||||
|
||||
fun Element.textArray(): Array<String> {
|
||||
val sb = StringUtil.borrowBuilder()
|
||||
NodeTraversor.traverse(object : NodeVisitor {
|
||||
override fun head(node: Node, depth: Int) {
|
||||
if (node is TextNode) {
|
||||
appendNormalisedText(sb, node)
|
||||
} else if (node is Element) {
|
||||
if (sb.isNotEmpty() &&
|
||||
(node.isBlock || node.tag().name == "br") &&
|
||||
!lastCharIsWhitespace(sb)
|
||||
) sb.append("\n")
|
||||
}
|
||||
}
|
||||
|
||||
override fun tail(node: Node, depth: Int) {
|
||||
if (node is Element) {
|
||||
if (node.isBlock && node.nextSibling() is TextNode
|
||||
&& !lastCharIsWhitespace(sb)
|
||||
) {
|
||||
sb.append("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}, this)
|
||||
val text = StringUtil.releaseBuilder(sb).trim { it <= ' ' }
|
||||
return text.splitNotBlank("\n")
|
||||
}
|
||||
|
||||
fun Element.findNS(tag: String, namespace: HashSet<String>): Elements {
|
||||
return select("*|$tag").filter { el ->
|
||||
namespace.contains(el.tagName().substringBefore(":"))
|
||||
}.toElements()
|
||||
}
|
||||
|
||||
fun Element.findNSPrefix(namespaceURI: String): HashSet<String> {
|
||||
return select("[^xmlns:]").map { element ->
|
||||
element.attributes().filter { it.value == namespaceURI }.map { it.key.substring(6) }
|
||||
}.flatten().toHashSet()
|
||||
}
|
||||
|
||||
fun List<Element>.toElements() = Elements(this)
|
||||
|
||||
private fun appendNormalisedText(sb: StringBuilder, textNode: TextNode) {
|
||||
val text = textNode.wholeText
|
||||
if (preserveWhitespace(textNode.parentNode()) || textNode is CDataNode)
|
||||
sb.append(text)
|
||||
else StringUtil.appendNormalisedWhitespace(sb, text, lastCharIsWhitespace(sb))
|
||||
}
|
||||
|
||||
private fun preserveWhitespace(node: Node?): Boolean {
|
||||
if (node is Element) {
|
||||
var el = node as Element?
|
||||
var i = 0
|
||||
do {
|
||||
if (el!!.tag().preserveWhitespace()) return true
|
||||
el = el.parent()
|
||||
i++
|
||||
} while (i < 6 && el != null)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun lastCharIsWhitespace(sb: java.lang.StringBuilder): Boolean {
|
||||
return sb.isNotEmpty() && sb[sb.length - 1] == ' '
|
||||
}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.webkit.WebSettings
|
||||
import io.legato.kazusa.BuildConfig
|
||||
import io.legato.kazusa.constant.AppConst
|
||||
import io.legato.kazusa.constant.AppLog
|
||||
import io.legato.kazusa.help.config.AppConfig
|
||||
import io.legato.kazusa.help.globalExecutor
|
||||
import splitties.init.appCtx
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.logging.FileHandler
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.LogRecord
|
||||
import java.util.logging.Logger
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
@Suppress("unused")
|
||||
object LogUtils {
|
||||
const val TIME_PATTERN = "yy-MM-dd HH:mm:ss.SSS"
|
||||
val logTimeFormat by lazy { SimpleDateFormat(TIME_PATTERN) }
|
||||
|
||||
fun init(context: Context) {
|
||||
fileHandler = createFileHandler(context)?.also {
|
||||
logger.addHandler(it)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun d(tag: String, msg: String) {
|
||||
logger.log(Level.INFO, "$tag $msg")
|
||||
}
|
||||
|
||||
inline fun d(tag: String, lazyMsg: () -> String) {
|
||||
if (logger.isLoggable(Level.INFO)) {
|
||||
logger.log(Level.INFO, "$tag ${lazyMsg()}")
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun e(tag: String, msg: String) {
|
||||
logger.log(Level.WARNING, "$tag $msg")
|
||||
}
|
||||
|
||||
val logger: Logger by lazy {
|
||||
Logger.getLogger("Legado")
|
||||
}
|
||||
|
||||
private var fileHandler: FileHandler? = null
|
||||
|
||||
private fun createFileHandler(context: Context): FileHandler? {
|
||||
try {
|
||||
val root = context.externalCacheDir ?: return null
|
||||
val logFolder = FileUtils.createFolderIfNotExist(root, "logs")
|
||||
globalExecutor.execute {
|
||||
val expiredTime = System.currentTimeMillis() - 7.days.inWholeMilliseconds
|
||||
logFolder.listFiles()?.forEach {
|
||||
if (it.lastModified() < expiredTime || it.name.endsWith(".lck")) {
|
||||
it.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
val date = getCurrentDateStr(TIME_PATTERN)
|
||||
val logPath = FileUtils.getPath(root = logFolder, "appLog-$date.txt")
|
||||
return AsyncFileHandler(logPath).apply {
|
||||
formatter = object : java.util.logging.Formatter() {
|
||||
override fun format(record: LogRecord): String {
|
||||
// 设置文件输出格式
|
||||
return getCurrentDateStr(TIME_PATTERN) + ": " + record.message + "\n"
|
||||
}
|
||||
}
|
||||
level = if (AppConfig.recordLog) {
|
||||
Level.INFO
|
||||
} else {
|
||||
Level.OFF
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
AppLog.putNotSave("创建fileHandler出错\n$e", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun upLevel() {
|
||||
val level = if (AppConfig.recordLog) {
|
||||
Level.INFO
|
||||
} else {
|
||||
Level.OFF
|
||||
}
|
||||
fileHandler?.level = level
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前时间
|
||||
*/
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
fun getCurrentDateStr(pattern: String): String {
|
||||
val date = Date()
|
||||
val sdf = SimpleDateFormat(pattern)
|
||||
return sdf.format(date)
|
||||
}
|
||||
|
||||
fun logDeviceInfo() {
|
||||
d("DeviceInfo") {
|
||||
buildString {
|
||||
kotlin.runCatching {
|
||||
//获取系统信息
|
||||
append("MANUFACTURER=").append(Build.MANUFACTURER).append("\n")
|
||||
append("BRAND=").append(Build.BRAND).append("\n")
|
||||
append("MODEL=").append(Build.MODEL).append("\n")
|
||||
append("SDK_INT=").append(Build.VERSION.SDK_INT).append("\n")
|
||||
append("RELEASE=").append(Build.VERSION.RELEASE).append("\n")
|
||||
val userAgent = try {
|
||||
WebSettings.getDefaultUserAgent(appCtx)
|
||||
} catch (e: Throwable) {
|
||||
e.toString()
|
||||
}
|
||||
append("WebViewUserAgent=").append(userAgent).append("\n")
|
||||
append("packageName=").append(appCtx.packageName).append("\n")
|
||||
append("heapSize=").append(Runtime.getRuntime().maxMemory()).append("\n")
|
||||
//获取app版本信息
|
||||
AppConst.appInfo.let {
|
||||
append("versionName=").append(it.versionName).append("\n")
|
||||
append("versionCode=").append(it.versionCode).append("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun Throwable.printOnDebug() {
|
||||
if (BuildConfig.DEBUG) {
|
||||
printStackTrace()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import cn.hutool.crypto.digest.DigestUtil
|
||||
import cn.hutool.crypto.digest.Digester
|
||||
import java.io.InputStream
|
||||
import kotlin.concurrent.getOrSet
|
||||
|
||||
/**
|
||||
* 将字符串转化为MD5
|
||||
*/
|
||||
@Suppress("unused")
|
||||
object MD5Utils {
|
||||
|
||||
private val threadLocal = ThreadLocal<Digester>()
|
||||
|
||||
private val MD5Digester
|
||||
get() = threadLocal.getOrSet {
|
||||
DigestUtil.digester("MD5")
|
||||
}
|
||||
|
||||
fun md5Encode(str: String?): String {
|
||||
return MD5Digester.digestHex(str)
|
||||
}
|
||||
|
||||
fun md5Encode(inputStream: InputStream): String {
|
||||
return MD5Digester.digestHex(inputStream)
|
||||
}
|
||||
|
||||
fun md5Encode16(str: String): String {
|
||||
var reStr = md5Encode(str)
|
||||
reStr = reStr.substring(8, 24)
|
||||
return reStr
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
fun HashMap<String, *>.has(key: String, ignoreCase: Boolean = false): Boolean {
|
||||
for (item in this) {
|
||||
if (key.equals(item.key, ignoreCase)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun <T> HashMap<String, T>.get(key: String, ignoreCase: Boolean = false): T? {
|
||||
for (item in this) {
|
||||
if (key.equals(item.key, ignoreCase)) {
|
||||
return item.value
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
inline fun <K, V> MutableMap<K, V>.getOrPutLimit(key: K, maxSize: Int, defaultValue: () -> V): V {
|
||||
var value = get(key)
|
||||
if (containsKey(key)) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return value as V
|
||||
}
|
||||
value = defaultValue()
|
||||
if (size < maxSize) {
|
||||
put(key, value)
|
||||
}
|
||||
return value
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.Menu
|
||||
import android.view.View
|
||||
import android.widget.ImageButton
|
||||
import androidx.appcompat.view.menu.MenuBuilder
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.constant.Theme
|
||||
//import io.legado.app.lib.theme.primaryTextColor
|
||||
import java.lang.reflect.Method
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
fun Menu.applyTint(context: Context, theme: Theme = Theme.Auto): Menu = this.let { menu ->
|
||||
if (menu is MenuBuilder) {
|
||||
menu.setOptionalIconsVisible(true)
|
||||
}
|
||||
// val defaultTextColor = context.getCompatColor(R.color.primaryText)
|
||||
// val tintColor = MenuExtensions.getMenuColor(context, theme)
|
||||
// menu.forEach { item ->
|
||||
// (item as MenuItemImpl).let { impl ->
|
||||
// //overflow:展开的item
|
||||
// impl.icon?.setTintMutate(
|
||||
// if (impl.requiresOverflow()) defaultTextColor else tintColor
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
return menu
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
fun Menu.applyOpenTint(context: Context) {
|
||||
//展开菜单显示图标
|
||||
if (this.javaClass.simpleName.equals("MenuBuilder", ignoreCase = true)) {
|
||||
// val defaultTextColor = context.getCompatColor(R.color.primaryText)
|
||||
kotlin.runCatching {
|
||||
var method: Method =
|
||||
this.javaClass.getDeclaredMethod("setOptionalIconsVisible", java.lang.Boolean.TYPE)
|
||||
method.isAccessible = true
|
||||
method.invoke(this, true)
|
||||
method = this.javaClass.getDeclaredMethod("getNonActionItems")
|
||||
val menuItems = method.invoke(this)
|
||||
if (menuItems is ArrayList<*>) {
|
||||
for (menuItem in menuItems) {
|
||||
// if (menuItem is MenuItem) {
|
||||
// menuItem.icon?.setTintMutate(defaultTextColor)
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (this.javaClass.simpleName.equals("SubMenuBuilder", ignoreCase = true)) {
|
||||
// val defaultTextColor = context.getCompatColor(R.color.primaryText)
|
||||
// (this as? SubMenuBuilder)?.forEach { item: MenuItem ->
|
||||
// item.icon?.setTintMutate(defaultTextColor)
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
fun Menu.iconItemOnLongClick(id: Int, function: (view: View) -> Unit) {
|
||||
findItem(id)?.let { item ->
|
||||
item.setActionView(R.layout.view_action_button)
|
||||
item.actionView?.run {
|
||||
contentDescription = item.title
|
||||
findViewById<ImageButton>(R.id.item).setImageDrawable(item.icon)
|
||||
setOnLongClickListener {
|
||||
function.invoke(this)
|
||||
true
|
||||
}
|
||||
setOnClickListener {
|
||||
performIdentifierAction(id, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
inline fun Menu.transaction(block: (Menu) -> Unit) {
|
||||
val menuBuilder = this as? MenuBuilder
|
||||
menuBuilder?.stopDispatchingItemsChanged()
|
||||
try {
|
||||
block(this)
|
||||
} finally {
|
||||
menuBuilder?.startDispatchingItemsChanged()
|
||||
}
|
||||
}
|
||||
|
||||
object MenuExtensions {
|
||||
|
||||
// fun getMenuColor(
|
||||
// context: Context,
|
||||
// theme: Theme = Theme.Auto,
|
||||
// requiresOverflow: Boolean = false
|
||||
// ): Int {
|
||||
// val defaultTextColor = context.getCompatColor(R.color.primaryText)
|
||||
// if (requiresOverflow)
|
||||
// return defaultTextColor
|
||||
// //val primaryTextColor = context.primaryTextColor
|
||||
// return when (theme) {
|
||||
// Theme.Dark -> context.getCompatColor(R.color.md_white_1000)
|
||||
// Theme.Light -> context.getCompatColor(R.color.md_black_1000)
|
||||
// else -> context.getCompatColor(R.color.md_black_1000)
|
||||
// }
|
||||
// }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
|
||||
private val mainHandler by lazy { Handler(Looper.getMainLooper()) }
|
||||
|
||||
fun <T> MutableLiveData<T>.sendValue(value: T) {
|
||||
mainHandler.post {
|
||||
this@sendValue.value = value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.content.res.ColorStateList
|
||||
import androidx.annotation.ColorInt
|
||||
import com.google.android.material.internal.NavigationMenuView
|
||||
import com.google.android.material.navigation.NavigationView
|
||||
|
||||
fun NavigationView.setItemIconColors(
|
||||
@ColorInt normalColor: Int,
|
||||
@ColorInt selectedColor: Int
|
||||
) {
|
||||
val iconSl = ColorStateList(
|
||||
arrayOf(
|
||||
intArrayOf(-android.R.attr.state_checked),
|
||||
intArrayOf(android.R.attr.state_checked)
|
||||
),
|
||||
intArrayOf(normalColor, selectedColor)
|
||||
)
|
||||
itemIconTintList = iconSl
|
||||
}
|
||||
|
||||
fun NavigationView.setItemTextColors(
|
||||
@ColorInt normalColor: Int,
|
||||
@ColorInt selectedColor: Int
|
||||
) {
|
||||
val textSl = ColorStateList(
|
||||
arrayOf(
|
||||
intArrayOf(-android.R.attr.state_checked),
|
||||
intArrayOf(android.R.attr.state_checked)
|
||||
),
|
||||
intArrayOf(normalColor, selectedColor)
|
||||
)
|
||||
itemTextColor = textSl
|
||||
}
|
||||
|
||||
fun NavigationView.disableScrollbar() {
|
||||
val navigationMenuView = getChildAt(0) as? NavigationMenuView
|
||||
navigationMenuView?.isVerticalScrollBarEnabled = false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.Build
|
||||
import cn.hutool.core.lang.Validator
|
||||
import io.legato.kazusa.constant.AppLog
|
||||
import okhttp3.internal.publicsuffix.PublicSuffixDatabase
|
||||
import splitties.systemservices.connectivityManager
|
||||
import java.net.InetAddress
|
||||
import java.net.NetworkInterface
|
||||
import java.net.SocketException
|
||||
import java.net.URL
|
||||
import java.util.BitSet
|
||||
import java.util.Enumeration
|
||||
|
||||
@Suppress("unused", "MemberVisibilityCanBePrivate")
|
||||
object NetworkUtils {
|
||||
|
||||
/**
|
||||
* 判断是否联网
|
||||
*/
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
@Suppress("DEPRECATION")
|
||||
fun isAvailable(): Boolean {
|
||||
if (Build.VERSION.SDK_INT < 23) {
|
||||
val mWiFiNetworkInfo = connectivityManager.activeNetworkInfo
|
||||
if (mWiFiNetworkInfo != null) {
|
||||
// WIFI
|
||||
return mWiFiNetworkInfo.type == ConnectivityManager.TYPE_WIFI ||
|
||||
// 移动数据
|
||||
mWiFiNetworkInfo.type == ConnectivityManager.TYPE_MOBILE ||
|
||||
// 以太网
|
||||
mWiFiNetworkInfo.type == ConnectivityManager.TYPE_ETHERNET ||
|
||||
// VPN
|
||||
mWiFiNetworkInfo.type == ConnectivityManager.TYPE_VPN
|
||||
}
|
||||
} else {
|
||||
val network = connectivityManager.activeNetwork
|
||||
if (network != null) {
|
||||
val nc = connectivityManager.getNetworkCapabilities(network)
|
||||
if (nc != null) {
|
||||
// WIFI
|
||||
return nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
|
||||
// 移动数据
|
||||
nc.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
|
||||
// 以太网
|
||||
nc.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) ||
|
||||
// VPN
|
||||
nc.hasTransport(NetworkCapabilities.TRANSPORT_VPN)
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private val notNeedEncodingQuery: BitSet by lazy {
|
||||
val bitSet = BitSet(256)
|
||||
for (i in 'a'.code..'z'.code) {
|
||||
bitSet.set(i)
|
||||
}
|
||||
for (i in 'A'.code..'Z'.code) {
|
||||
bitSet.set(i)
|
||||
}
|
||||
for (i in '0'.code..'9'.code) {
|
||||
bitSet.set(i)
|
||||
}
|
||||
for (char in "!$&()*+,-./:;=?@[\\]^_`{|}~") {
|
||||
bitSet.set(char.code)
|
||||
}
|
||||
return@lazy bitSet
|
||||
}
|
||||
|
||||
private val notNeedEncodingForm: BitSet by lazy {
|
||||
val bitSet = BitSet(256)
|
||||
for (i in 'a'.code..'z'.code) {
|
||||
bitSet.set(i)
|
||||
}
|
||||
for (i in 'A'.code..'Z'.code) {
|
||||
bitSet.set(i)
|
||||
}
|
||||
for (i in '0'.code..'9'.code) {
|
||||
bitSet.set(i)
|
||||
}
|
||||
for (char in "*-._") {
|
||||
bitSet.set(char.code)
|
||||
}
|
||||
return@lazy bitSet
|
||||
}
|
||||
|
||||
/**
|
||||
* 支持JAVA的URLEncoder.encode出来的string做判断。 即: 将' '转成'+'
|
||||
* 0-9a-zA-Z保留 <br></br>
|
||||
* ! * ' ( ) ; : @ & = + $ , / ? # [ ] 保留
|
||||
* 其他字符转成%XX的格式,X是16进制的大写字符,范围是[0-9A-F]
|
||||
*/
|
||||
fun encodedQuery(str: String): Boolean {
|
||||
var needEncode = false
|
||||
var i = 0
|
||||
while (i < str.length) {
|
||||
val c = str[i]
|
||||
if (notNeedEncodingQuery.get(c.code)) {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if (c == '%' && i + 2 < str.length) {
|
||||
// 判断是否符合urlEncode规范
|
||||
val c1 = str[++i]
|
||||
val c2 = str[++i]
|
||||
if (isDigit16Char(c1) && isDigit16Char(c2)) {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
}
|
||||
// 其他字符,肯定需要urlEncode
|
||||
needEncode = true
|
||||
break
|
||||
}
|
||||
|
||||
return !needEncode
|
||||
}
|
||||
|
||||
fun encodedForm(str: String): Boolean {
|
||||
var needEncode = false
|
||||
var i = 0
|
||||
while (i < str.length) {
|
||||
val c = str[i]
|
||||
if (notNeedEncodingForm.get(c.code)) {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if (c == '%' && i + 2 < str.length) {
|
||||
// 判断是否符合urlEncode规范
|
||||
val c1 = str[++i]
|
||||
val c2 = str[++i]
|
||||
if (isDigit16Char(c1) && isDigit16Char(c2)) {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
}
|
||||
// 其他字符,肯定需要urlEncode
|
||||
needEncode = true
|
||||
break
|
||||
}
|
||||
|
||||
return !needEncode
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断c是否是16进制的字符
|
||||
*/
|
||||
private fun isDigit16Char(c: Char): Boolean {
|
||||
return c in '0'..'9' || c in 'A'..'F' || c in 'a'..'f'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取绝对地址
|
||||
*/
|
||||
fun getAbsoluteURL(baseURL: String?, relativePath: String): String {
|
||||
if (baseURL.isNullOrEmpty()) return relativePath.trim()
|
||||
var absoluteUrl: URL? = null
|
||||
try {
|
||||
absoluteUrl = URL(baseURL.substringBefore(","))
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
}
|
||||
return getAbsoluteURL(absoluteUrl, relativePath)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取绝对地址
|
||||
*/
|
||||
fun getAbsoluteURL(baseURL: URL?, relativePath: String): String {
|
||||
val relativePathTrim = relativePath.trim()
|
||||
if (baseURL == null) return relativePathTrim
|
||||
if (relativePathTrim.isAbsUrl()) return relativePathTrim
|
||||
if (relativePathTrim.isDataUrl()) return relativePathTrim
|
||||
if (relativePathTrim.startsWith("javascript")) return ""
|
||||
var relativeUrl = relativePathTrim
|
||||
try {
|
||||
val parseUrl = URL(baseURL, relativePath)
|
||||
relativeUrl = parseUrl.toString()
|
||||
return relativeUrl
|
||||
} catch (e: Exception) {
|
||||
AppLog.put("网址拼接出错\n${e.localizedMessage}", e)
|
||||
}
|
||||
return relativeUrl
|
||||
}
|
||||
|
||||
fun getBaseUrl(url: String?): String? {
|
||||
url ?: return null
|
||||
if (url.startsWith("http://", true)
|
||||
|| url.startsWith("https://", true)
|
||||
) {
|
||||
val index = url.indexOf("/", 9)
|
||||
return if (index == -1) {
|
||||
url
|
||||
} else url.substring(0, index)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取域名,供cookie保存和读取,处理失败返回传入的url
|
||||
* http://1.2.3.4 => 1.2.3.4
|
||||
* https://www.example.com => example.com
|
||||
* http://www.biquge.com.cn => biquge.com.cn
|
||||
* http://www.content.example.com => example.com
|
||||
*/
|
||||
fun getSubDomain(url: String): String {
|
||||
val baseUrl = getBaseUrl(url) ?: return url
|
||||
return kotlin.runCatching {
|
||||
val mURL = URL(baseUrl)
|
||||
val host: String = mURL.host
|
||||
//mURL.scheme https/http
|
||||
//判断是否为ip
|
||||
if (isIPAddress(host)) return host
|
||||
//PublicSuffixDatabase处理域名
|
||||
PublicSuffixDatabase.get().getEffectiveTldPlusOne(host) ?: host
|
||||
}.getOrDefault(baseUrl)
|
||||
}
|
||||
|
||||
fun getSubDomainOrNull(url: String): String? {
|
||||
val baseUrl = getBaseUrl(url) ?: return null
|
||||
return kotlin.runCatching {
|
||||
val mURL = URL(baseUrl)
|
||||
val host: String = mURL.host
|
||||
//mURL.scheme https/http
|
||||
//判断是否为ip
|
||||
if (isIPAddress(host)) return host
|
||||
//PublicSuffixDatabase处理域名
|
||||
PublicSuffixDatabase.get().getEffectiveTldPlusOne(host) ?: host
|
||||
}.getOrDefault(null)
|
||||
}
|
||||
|
||||
fun getDomain(url: String): String {
|
||||
val baseUrl = getBaseUrl(url) ?: return url
|
||||
return kotlin.runCatching {
|
||||
URL(baseUrl).host
|
||||
}.getOrDefault(baseUrl)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get local Ip address.
|
||||
*/
|
||||
fun getLocalIPAddress(): List<InetAddress> {
|
||||
val enumeration: Enumeration<NetworkInterface>
|
||||
try {
|
||||
enumeration = NetworkInterface.getNetworkInterfaces()
|
||||
} catch (e: SocketException) {
|
||||
e.printOnDebug()
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val addressList = mutableListOf<InetAddress>()
|
||||
|
||||
while (enumeration.hasMoreElements()) {
|
||||
val nif = enumeration.nextElement()
|
||||
val addresses = nif.inetAddresses ?: continue
|
||||
while (addresses.hasMoreElements()) {
|
||||
val address = addresses.nextElement()
|
||||
if (!address.isLoopbackAddress && isIPv4Address(address.hostAddress)) {
|
||||
addressList.add(address)
|
||||
}
|
||||
}
|
||||
}
|
||||
return addressList
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if valid IPV4 address.
|
||||
*
|
||||
* @param input the address string to check for validity.
|
||||
* @return True if the input parameter is a valid IPv4 address.
|
||||
*/
|
||||
fun isIPv4Address(input: String?): Boolean {
|
||||
return input != null && input.isNotEmpty()
|
||||
&& input[0] in '1'..'9'
|
||||
&& input.count { it == '.' } == 3
|
||||
&& Validator.isIpv4(input)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if valid IPV6 address.
|
||||
*/
|
||||
fun isIPv6Address(input: String?): Boolean {
|
||||
return input != null && input.contains(":") && Validator.isIpv6(input)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if valid IP address.
|
||||
*/
|
||||
fun isIPAddress(input: String?): Boolean {
|
||||
return isIPv4Address(input) || isIPv6Address(input)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.os.Build
|
||||
import android.text.TextPaint
|
||||
|
||||
val TextPaint.textHeight: Float
|
||||
get() = fontMetrics.run { descent - ascent + leading }
|
||||
|
||||
fun TextPaint.getTextWidthsCompat(text: String, widths: FloatArray) {
|
||||
getTextWidths(text, widths)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||
val letterSpacing = letterSpacing * textSize
|
||||
val letterSpacingHalf = letterSpacing * 0.5f
|
||||
for (i in widths.indices) {
|
||||
if (widths[i] > 0) {
|
||||
widths[i] += letterSpacingHalf
|
||||
break
|
||||
}
|
||||
}
|
||||
for (i in text.lastIndex downTo 0) {
|
||||
if (widths[i] > 0) {
|
||||
widths[i] += letterSpacingHalf
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.system.ErrnoException
|
||||
import android.system.Os
|
||||
import android.system.OsConstants
|
||||
import java.io.IOException
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.SeekableByteChannel
|
||||
|
||||
@Suppress("unused")
|
||||
@SuppressLint("NewApi")
|
||||
class ParcelFileDescriptorChannel(private val pfd: ParcelFileDescriptor) : SeekableByteChannel {
|
||||
@Throws(IOException::class)
|
||||
override fun read(dst: ByteBuffer): Int {
|
||||
return try {
|
||||
Os.read(pfd.fileDescriptor, dst)
|
||||
} catch (e: ErrnoException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun write(src: ByteBuffer): Int {
|
||||
return try {
|
||||
Os.write(pfd.fileDescriptor, src)
|
||||
} catch (e: ErrnoException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun position(): Long {
|
||||
return try {
|
||||
Os.lseek(pfd.fileDescriptor, 0, OsConstants.SEEK_CUR)
|
||||
} catch (e: ErrnoException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun position(newPosition: Long): SeekableByteChannel {
|
||||
try {
|
||||
Os.lseek(pfd.fileDescriptor, newPosition, OsConstants.SEEK_SET)
|
||||
} catch (e: ErrnoException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun size(): Long {
|
||||
return try {
|
||||
Os.fstat(pfd.fileDescriptor).st_size
|
||||
} catch (e: ErrnoException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun truncate(newSize: Long): SeekableByteChannel {
|
||||
require(!(newSize < 0L || newSize > Int.MAX_VALUE)) { "Size has to be in range 0.. " + Int.MAX_VALUE }
|
||||
try {
|
||||
Os.ftruncate(pfd.fileDescriptor, newSize)
|
||||
} catch (e: ErrnoException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
override fun isOpen(): Boolean {
|
||||
return pfd.fileDescriptor.valid()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun close() {
|
||||
pfd.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import splitties.init.appCtx
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* 获取自定义路径的SharedPreferences, 用反射生成 SharedPreferences
|
||||
* @param dir 目录路径
|
||||
* @param fileName 文件名,不需要 '.xml' 后缀
|
||||
* @return SharedPreferences
|
||||
*/
|
||||
@SuppressLint("DiscouragedPrivateApi")
|
||||
fun Context.getSharedPreferences(
|
||||
dir: String,
|
||||
fileName: String
|
||||
): SharedPreferences? {
|
||||
try {
|
||||
// 获取 ContextWrapper对象中的mBase变量。该变量保存了 ContextImpl 对象
|
||||
val fieldMBase = ContextWrapper::class.java.getDeclaredField("mBase")
|
||||
fieldMBase.isAccessible = true
|
||||
// 获取 mBase变量
|
||||
val objMBase = fieldMBase.get(this)
|
||||
// 获取 ContextImpl。mPreferencesDir变量,该变量保存了数据文件的保存路径
|
||||
val fieldMPreferencesDir = objMBase.javaClass.getDeclaredField("mPreferencesDir")
|
||||
fieldMPreferencesDir.isAccessible = true
|
||||
// 创建自定义路径
|
||||
val file = File(dir)
|
||||
// 修改mPreferencesDir变量的值
|
||||
fieldMPreferencesDir.set(objMBase, file)
|
||||
// 返回修改路径以后的 SharedPreferences :%FILE_PATH%/%fileName%.xml
|
||||
return getSharedPreferences(fileName, Activity.MODE_PRIVATE)
|
||||
} catch (e: NoSuchFieldException) {
|
||||
e.printOnDebug()
|
||||
} catch (e: IllegalArgumentException) {
|
||||
e.printOnDebug()
|
||||
} catch (e: IllegalAccessException) {
|
||||
e.printOnDebug()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun SharedPreferences.getString(key: String): String? {
|
||||
return getString(key, null)
|
||||
}
|
||||
|
||||
fun SharedPreferences.putString(key: String, value: String) {
|
||||
edit {
|
||||
putString(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
fun SharedPreferences.getBoolean(key: String): Boolean {
|
||||
return getBoolean(key, false)
|
||||
}
|
||||
|
||||
fun SharedPreferences.putBoolean(key: String, value: Boolean) {
|
||||
edit {
|
||||
putBoolean(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
fun SharedPreferences.getInt(key: String): Int {
|
||||
return getInt(key, 0)
|
||||
}
|
||||
|
||||
fun SharedPreferences.putInt(key: String, value: Int) {
|
||||
edit {
|
||||
putInt(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
fun SharedPreferences.getLong(key: String): Long {
|
||||
return getLong(key, 0)
|
||||
}
|
||||
|
||||
fun SharedPreferences.putLong(key: String, value: Long) {
|
||||
edit {
|
||||
putLong(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
fun SharedPreferences.getFloat(key: String): Float {
|
||||
return getFloat(key, 0f)
|
||||
}
|
||||
|
||||
fun SharedPreferences.putFloat(key: String, value: Float) {
|
||||
edit {
|
||||
putFloat(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
fun SharedPreferences.remove(key: String) {
|
||||
edit {
|
||||
remove(key)
|
||||
}
|
||||
}
|
||||
|
||||
fun LifecycleOwner.observeSharedPreferences(
|
||||
prefs: SharedPreferences = appCtx.defaultSharedPreferences,
|
||||
l: SharedPreferences.OnSharedPreferenceChangeListener
|
||||
) {
|
||||
val observer = object : DefaultLifecycleObserver {
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
prefs.registerOnSharedPreferenceChangeListener(l)
|
||||
}
|
||||
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
prefs.unregisterOnSharedPreferenceChangeListener(l)
|
||||
lifecycle.removeObserver(this)
|
||||
}
|
||||
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
prefs.unregisterOnSharedPreferenceChangeListener(l)
|
||||
}
|
||||
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
prefs.registerOnSharedPreferenceChangeListener(l)
|
||||
}
|
||||
}
|
||||
lifecycle.addObserver(observer)
|
||||
}
|
||||
@@ -0,0 +1,479 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.text.TextPaint
|
||||
import android.text.TextUtils
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.FloatRange
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.BinaryBitmap
|
||||
import com.google.zxing.DecodeHintType
|
||||
import com.google.zxing.EncodeHintType
|
||||
import com.google.zxing.LuminanceSource
|
||||
import com.google.zxing.MultiFormatReader
|
||||
import com.google.zxing.MultiFormatWriter
|
||||
import com.google.zxing.RGBLuminanceSource
|
||||
import com.google.zxing.Result
|
||||
import com.google.zxing.WriterException
|
||||
import com.google.zxing.common.GlobalHistogramBinarizer
|
||||
import com.google.zxing.common.HybridBinarizer
|
||||
import com.google.zxing.qrcode.QRCodeWriter
|
||||
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
|
||||
import com.king.zxing.DecodeFormatManager
|
||||
import java.util.EnumMap
|
||||
import kotlin.math.max
|
||||
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate", "unused")
|
||||
object QRCodeUtils {
|
||||
|
||||
const val DEFAULT_REQ_WIDTH = 480
|
||||
const val DEFAULT_REQ_HEIGHT = 640
|
||||
|
||||
/**
|
||||
* 生成二维码
|
||||
* @param content 二维码的内容
|
||||
* @param heightPix 二维码的高
|
||||
* @param logo 二维码中间的logo
|
||||
* @param ratio logo所占比例 因为二维码的最大容错率为30%,所以建议ratio的范围小于0.3
|
||||
* @param errorCorrectionLevel
|
||||
*/
|
||||
fun createQRCode(
|
||||
content: String,
|
||||
heightPix: Int = DEFAULT_REQ_HEIGHT,
|
||||
logo: Bitmap? = null,
|
||||
@FloatRange(from = 0.0, to = 1.0) ratio: Float = 0.2f,
|
||||
errorCorrectionLevel: ErrorCorrectionLevel = ErrorCorrectionLevel.H
|
||||
): Bitmap? {
|
||||
//配置参数
|
||||
val hints: MutableMap<EncodeHintType, Any> = EnumMap(EncodeHintType::class.java)
|
||||
hints[EncodeHintType.CHARACTER_SET] = "utf-8"
|
||||
//容错级别
|
||||
hints[EncodeHintType.ERROR_CORRECTION] = errorCorrectionLevel
|
||||
//设置空白边距的宽度
|
||||
hints[EncodeHintType.MARGIN] = 1 //default is 4
|
||||
return createQRCode(content, heightPix, logo, ratio, hints)
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成二维码
|
||||
* @param content 二维码的内容
|
||||
* @param heightPix 二维码的高
|
||||
* @param logo 二维码中间的logo
|
||||
* @param ratio logo所占比例 因为二维码的最大容错率为30%,所以建议ratio的范围小于0.3
|
||||
* @param hints
|
||||
* @param codeColor 二维码的颜色
|
||||
* @return
|
||||
*/
|
||||
fun createQRCode(
|
||||
content: String?,
|
||||
heightPix: Int,
|
||||
logo: Bitmap?,
|
||||
@FloatRange(from = 0.0, to = 1.0) ratio: Float = 0.2f,
|
||||
hints: Map<EncodeHintType, *>,
|
||||
codeColor: Int = Color.BLACK
|
||||
): Bitmap? {
|
||||
try {
|
||||
// 图像数据转换,使用了矩阵转换
|
||||
val bitMatrix =
|
||||
QRCodeWriter().encode(content, BarcodeFormat.QR_CODE, heightPix, heightPix, hints)
|
||||
val pixels = IntArray(heightPix * heightPix)
|
||||
// 下面这里按照二维码的算法,逐个生成二维码的图片,
|
||||
// 两个for循环是图片横列扫描的结果
|
||||
for (y in 0 until heightPix) {
|
||||
for (x in 0 until heightPix) {
|
||||
if (bitMatrix[x, y]) {
|
||||
pixels[y * heightPix + x] = codeColor
|
||||
} else {
|
||||
pixels[y * heightPix + x] = Color.WHITE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 生成二维码图片的格式
|
||||
var bitmap: Bitmap? = Bitmap.createBitmap(heightPix, heightPix, Bitmap.Config.ARGB_8888)
|
||||
bitmap!!.setPixels(pixels, 0, heightPix, 0, 0, heightPix, heightPix)
|
||||
if (logo != null) {
|
||||
bitmap = addLogo(bitmap, logo, ratio)
|
||||
}
|
||||
return bitmap
|
||||
} catch (e: WriterException) {
|
||||
e.printOnDebug()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 在二维码中间添加Logo图案
|
||||
* @param src
|
||||
* @param logo
|
||||
* @param ratio logo所占比例 因为二维码的最大容错率为30%,所以建议ratio的范围小于0.3
|
||||
* @return
|
||||
*/
|
||||
private fun addLogo(
|
||||
src: Bitmap?,
|
||||
logo: Bitmap?,
|
||||
@FloatRange(from = 0.0, to = 1.0) ratio: Float
|
||||
): Bitmap? {
|
||||
if (src == null) {
|
||||
return null
|
||||
}
|
||||
if (logo == null) {
|
||||
return src
|
||||
}
|
||||
|
||||
//获取图片的宽高
|
||||
val srcWidth = src.width
|
||||
val srcHeight = src.height
|
||||
val logoWidth = logo.width
|
||||
val logoHeight = logo.height
|
||||
if (srcWidth == 0 || srcHeight == 0) {
|
||||
return null
|
||||
}
|
||||
if (logoWidth == 0 || logoHeight == 0) {
|
||||
return src
|
||||
}
|
||||
|
||||
//logo大小为二维码整体大小
|
||||
val scaleFactor = srcWidth * ratio / logoWidth
|
||||
var bitmap: Bitmap? = Bitmap.createBitmap(srcWidth, srcHeight, Bitmap.Config.ARGB_8888)
|
||||
try {
|
||||
val canvas = Canvas(bitmap!!)
|
||||
canvas.drawBitmap(src, 0f, 0f, null)
|
||||
canvas.scale(
|
||||
scaleFactor,
|
||||
scaleFactor,
|
||||
(srcWidth / 2).toFloat(),
|
||||
(srcHeight / 2).toFloat()
|
||||
)
|
||||
canvas.drawBitmap(
|
||||
logo,
|
||||
((srcWidth - logoWidth) / 2).toFloat(),
|
||||
((srcHeight - logoHeight) / 2).toFloat(),
|
||||
null
|
||||
)
|
||||
canvas.save()
|
||||
canvas.restore()
|
||||
} catch (e: Exception) {
|
||||
bitmap = null
|
||||
e.printOnDebug()
|
||||
}
|
||||
return bitmap
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析一维码/二维码图片
|
||||
* @param bitmap 解析的图片
|
||||
* @param hints 解析编码类型
|
||||
* @return
|
||||
*/
|
||||
fun parseCode(
|
||||
bitmap: Bitmap,
|
||||
reqWidth: Int = DEFAULT_REQ_WIDTH,
|
||||
reqHeight: Int = DEFAULT_REQ_HEIGHT,
|
||||
hints: Map<DecodeHintType?, Any?> = DecodeFormatManager.ALL_HINTS
|
||||
): String? {
|
||||
val result = parseCodeResult(bitmap, reqWidth, reqHeight, hints)
|
||||
return result?.text
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析一维码/二维码图片
|
||||
* @param bitmap 解析的图片
|
||||
* @param hints 解析编码类型
|
||||
* @return
|
||||
*/
|
||||
fun parseCodeResult(
|
||||
bitmap: Bitmap,
|
||||
reqWidth: Int = DEFAULT_REQ_WIDTH,
|
||||
reqHeight: Int = DEFAULT_REQ_HEIGHT,
|
||||
hints: Map<DecodeHintType?, Any?> = DecodeFormatManager.ALL_HINTS
|
||||
): Result? {
|
||||
if (bitmap.width > reqWidth || bitmap.height > reqHeight) {
|
||||
val bm = bitmap.resizeAndRecycle(reqWidth, reqHeight)
|
||||
return parseCodeResult(getRGBLuminanceSource(bm), hints)
|
||||
}
|
||||
return parseCodeResult(getRGBLuminanceSource(bitmap), hints)
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析一维码/二维码图片
|
||||
* @param source
|
||||
* @param hints
|
||||
* @return
|
||||
*/
|
||||
fun parseCodeResult(source: LuminanceSource?, hints: Map<DecodeHintType?, Any?>?): Result? {
|
||||
var result: Result? = null
|
||||
val reader = MultiFormatReader()
|
||||
try {
|
||||
reader.setHints(hints)
|
||||
if (source != null) {
|
||||
result = decodeInternal(reader, source)
|
||||
if (result == null) {
|
||||
result = decodeInternal(reader, source.invert())
|
||||
}
|
||||
if (result == null && source.isRotateSupported) {
|
||||
result = decodeInternal(reader, source.rotateCounterClockwise())
|
||||
}
|
||||
}
|
||||
} catch (e: java.lang.Exception) {
|
||||
e.printOnDebug()
|
||||
} finally {
|
||||
reader.reset()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析二维码图片
|
||||
* @param bitmapPath 需要解析的图片路径
|
||||
* @return
|
||||
*/
|
||||
fun parseQRCode(bitmapPath: String?): String? {
|
||||
val result = parseQRCodeResult(bitmapPath)
|
||||
return result?.text
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析二维码图片
|
||||
* @param bitmapPath 需要解析的图片路径
|
||||
* @param reqWidth 请求目标宽度,如果实际图片宽度大于此值,会自动进行压缩处理,当 reqWidth 和 reqHeight都小于或等于0时,则不进行压缩处理
|
||||
* @param reqHeight 请求目标高度,如果实际图片高度大于此值,会自动进行压缩处理,当 reqWidth 和 reqHeight都小于或等于0时,则不进行压缩处理
|
||||
* @return
|
||||
*/
|
||||
fun parseQRCodeResult(
|
||||
bitmapPath: String?,
|
||||
reqWidth: Int = DEFAULT_REQ_WIDTH,
|
||||
reqHeight: Int = DEFAULT_REQ_HEIGHT
|
||||
): Result? {
|
||||
return parseCodeResult(bitmapPath, reqWidth, reqHeight, DecodeFormatManager.QR_CODE_HINTS)
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析一维码/二维码图片
|
||||
* @param bitmapPath 需要解析的图片路径
|
||||
* @return
|
||||
*/
|
||||
fun parseCode(
|
||||
bitmapPath: String?,
|
||||
reqWidth: Int = DEFAULT_REQ_WIDTH,
|
||||
reqHeight: Int = DEFAULT_REQ_HEIGHT,
|
||||
hints: Map<DecodeHintType?, Any?> = DecodeFormatManager.ALL_HINTS
|
||||
): String? {
|
||||
return parseCodeResult(bitmapPath, reqWidth, reqHeight, hints)?.text
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析一维码/二维码图片
|
||||
* @param bitmapPath 需要解析的图片路径
|
||||
* @param reqWidth 请求目标宽度,如果实际图片宽度大于此值,会自动进行压缩处理,当 reqWidth 和 reqHeight都小于或等于0时,则不进行压缩处理
|
||||
* @param reqHeight 请求目标高度,如果实际图片高度大于此值,会自动进行压缩处理,当 reqWidth 和 reqHeight都小于或等于0时,则不进行压缩处理
|
||||
* @param hints 解析编码类型
|
||||
* @return
|
||||
*/
|
||||
fun parseCodeResult(
|
||||
bitmapPath: String?,
|
||||
reqWidth: Int = DEFAULT_REQ_WIDTH,
|
||||
reqHeight: Int = DEFAULT_REQ_HEIGHT,
|
||||
hints: Map<DecodeHintType?, Any?> = DecodeFormatManager.ALL_HINTS
|
||||
): Result? {
|
||||
var result: Result? = null
|
||||
val reader = MultiFormatReader()
|
||||
try {
|
||||
reader.setHints(hints)
|
||||
val source = getRGBLuminanceSource(compressBitmap(bitmapPath, reqWidth, reqHeight))
|
||||
result = decodeInternal(reader, source)
|
||||
if (result == null) {
|
||||
result = decodeInternal(reader, source.invert())
|
||||
}
|
||||
if (result == null && source.isRotateSupported) {
|
||||
result = decodeInternal(reader, source.rotateCounterClockwise())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
} finally {
|
||||
reader.reset()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun decodeInternal(reader: MultiFormatReader, source: LuminanceSource): Result? {
|
||||
var result: Result? = null
|
||||
try {
|
||||
try {
|
||||
//采用HybridBinarizer解析
|
||||
result = reader.decodeWithState(BinaryBitmap(HybridBinarizer(source)))
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
if (result == null) {
|
||||
//如果没有解析成功,再采用GlobalHistogramBinarizer解析一次
|
||||
result = reader.decodeWithState(BinaryBitmap(GlobalHistogramBinarizer(source)))
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 压缩图片
|
||||
* @param path
|
||||
* @return
|
||||
*/
|
||||
private fun compressBitmap(path: String?, reqWidth: Int, reqHeight: Int): Bitmap {
|
||||
if (reqWidth > 0 && reqHeight > 0) { //都大于进行判断是否压缩
|
||||
val newOpts = BitmapFactory.Options()
|
||||
// 开始读入图片,此时把options.inJustDecodeBounds 设回true了
|
||||
newOpts.inJustDecodeBounds = true //获取原始图片大小
|
||||
BitmapFactory.decodeFile(path, newOpts) // 此时返回bm为空
|
||||
val width = newOpts.outWidth.toFloat()
|
||||
val height = newOpts.outHeight.toFloat()
|
||||
// 缩放比,由于是固定比例缩放,只用高或者宽其中一个数据进行计算即可
|
||||
var wSize = 1 // wSize=1表示不缩放
|
||||
if (width > reqWidth) { // 如果宽度大的话根据宽度固定大小缩放
|
||||
wSize = (width / reqWidth).toInt()
|
||||
}
|
||||
var hSize = 1 // wSize=1表示不缩放
|
||||
if (height > reqHeight) { // 如果高度高的话根据宽度固定大小缩放
|
||||
hSize = (height / reqHeight).toInt()
|
||||
}
|
||||
var size = max(wSize, hSize)
|
||||
if (size <= 0) size = 1
|
||||
newOpts.inSampleSize = size // 设置缩放比例
|
||||
// 重新读入图片,注意此时已经把options.inJustDecodeBounds 设回false了
|
||||
newOpts.inJustDecodeBounds = false
|
||||
return BitmapFactory.decodeFile(path, newOpts)
|
||||
}
|
||||
return BitmapFactory.decodeFile(path)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取RGBLuminanceSource
|
||||
* @param bitmap
|
||||
* @return
|
||||
*/
|
||||
private fun getRGBLuminanceSource(bitmap: Bitmap): RGBLuminanceSource {
|
||||
val width = bitmap.width
|
||||
val height = bitmap.height
|
||||
val pixels = IntArray(width * height)
|
||||
bitmap.getPixels(pixels, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height)
|
||||
return RGBLuminanceSource(width, height, pixels)
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成条形码
|
||||
* @param content
|
||||
* @param format
|
||||
* @param desiredWidth
|
||||
* @param desiredHeight
|
||||
* @param hints
|
||||
* @param isShowText
|
||||
* @param textSize
|
||||
* @param codeColor
|
||||
* @return
|
||||
*/
|
||||
fun createBarCode(
|
||||
content: String?,
|
||||
desiredWidth: Int,
|
||||
desiredHeight: Int,
|
||||
format: BarcodeFormat = BarcodeFormat.CODE_128,
|
||||
hints: Map<EncodeHintType?, *>? = null,
|
||||
isShowText: Boolean = true,
|
||||
textSize: Int = 40,
|
||||
@ColorInt codeColor: Int = Color.BLACK
|
||||
): Bitmap? {
|
||||
if (TextUtils.isEmpty(content)) {
|
||||
return null
|
||||
}
|
||||
val writer = MultiFormatWriter()
|
||||
try {
|
||||
val result = writer.encode(
|
||||
content, format, desiredWidth,
|
||||
desiredHeight, hints
|
||||
)
|
||||
val width = result.width
|
||||
val height = result.height
|
||||
val pixels = IntArray(width * height)
|
||||
// All are 0, or black, by default
|
||||
for (y in 0 until height) {
|
||||
val offset = y * width
|
||||
for (x in 0 until width) {
|
||||
pixels[offset + x] = if (result[x, y]) codeColor else Color.WHITE
|
||||
}
|
||||
}
|
||||
val bitmap = Bitmap.createBitmap(
|
||||
width, height,
|
||||
Bitmap.Config.ARGB_8888
|
||||
)
|
||||
bitmap.setPixels(pixels, 0, width, 0, 0, width, height)
|
||||
return if (isShowText) {
|
||||
addCode(bitmap, content, textSize, codeColor, textSize / 2)
|
||||
} else bitmap
|
||||
} catch (e: WriterException) {
|
||||
e.printOnDebug()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 条形码下面添加文本信息
|
||||
* @param src
|
||||
* @param code
|
||||
* @param textSize
|
||||
* @param textColor
|
||||
* @return
|
||||
*/
|
||||
private fun addCode(
|
||||
src: Bitmap?,
|
||||
code: String?,
|
||||
textSize: Int,
|
||||
@ColorInt textColor: Int,
|
||||
offset: Int
|
||||
): Bitmap? {
|
||||
if (src == null) {
|
||||
return null
|
||||
}
|
||||
if (TextUtils.isEmpty(code)) {
|
||||
return src
|
||||
}
|
||||
|
||||
//获取图片的宽高
|
||||
val srcWidth = src.width
|
||||
val srcHeight = src.height
|
||||
if (srcWidth <= 0 || srcHeight <= 0) {
|
||||
return null
|
||||
}
|
||||
var bitmap: Bitmap? = Bitmap.createBitmap(
|
||||
srcWidth,
|
||||
srcHeight + textSize + offset * 2,
|
||||
Bitmap.Config.ARGB_8888
|
||||
)
|
||||
try {
|
||||
val canvas = Canvas(bitmap!!)
|
||||
canvas.drawBitmap(src, 0f, 0f, null)
|
||||
val paint = TextPaint()
|
||||
paint.textSize = textSize.toFloat()
|
||||
paint.color = textColor
|
||||
paint.textAlign = Paint.Align.CENTER
|
||||
canvas.drawText(
|
||||
code!!,
|
||||
(srcWidth / 2).toFloat(),
|
||||
(srcHeight + textSize / 2 + offset).toFloat(),
|
||||
paint
|
||||
)
|
||||
canvas.save()
|
||||
canvas.restore()
|
||||
} catch (e: Exception) {
|
||||
bitmap = null
|
||||
e.printOnDebug()
|
||||
}
|
||||
return bitmap
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.graphics.Color
|
||||
import java.util.*
|
||||
|
||||
@Suppress("unused")
|
||||
class RandomColor(alpha: Int, lower: Int, upper: Int) {
|
||||
|
||||
constructor() : this(255, 80, 200)
|
||||
|
||||
private var alpha: Int = 0
|
||||
private var lower: Int = 0
|
||||
private var upper: Int = 0
|
||||
|
||||
init {
|
||||
require(upper > lower) { "must be lower < upper" }
|
||||
setAlpha(alpha)
|
||||
setLower(lower)
|
||||
setUpper(upper)
|
||||
}
|
||||
|
||||
//随机数是前闭 后开
|
||||
fun build(): Int {
|
||||
val red = getLower() + Random().nextInt(getUpper() - getLower() + 1)
|
||||
val green = getLower() + Random().nextInt(getUpper() - getLower() + 1)
|
||||
val blue = getLower() + Random().nextInt(getUpper() - getLower() + 1)
|
||||
return Color.argb(getAlpha(), red, green, blue)
|
||||
}
|
||||
|
||||
private fun getAlpha(): Int {
|
||||
return alpha
|
||||
}
|
||||
|
||||
private fun setAlpha(alpha: Int) {
|
||||
var alpha1 = alpha
|
||||
if (alpha1 > 255) alpha1 = 255
|
||||
if (alpha1 < 0) alpha1 = 0
|
||||
this.alpha = alpha1
|
||||
}
|
||||
|
||||
private fun getLower(): Int {
|
||||
return lower
|
||||
}
|
||||
|
||||
private fun setLower(lower: Int) {
|
||||
var lower1 = lower
|
||||
if (lower1 < 0) lower1 = 0
|
||||
this.lower = lower1
|
||||
}
|
||||
|
||||
private fun getUpper(): Int {
|
||||
return upper
|
||||
}
|
||||
|
||||
private fun setUpper(upper: Int) {
|
||||
var upper1 = upper
|
||||
if (upper1 > 255) upper1 = 255
|
||||
this.upper = upper1
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.DocumentsContract
|
||||
import android.provider.MediaStore
|
||||
import androidx.core.provider.DocumentsContractCompat
|
||||
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
|
||||
@Suppress("unused")
|
||||
object RealPathUtil {
|
||||
/**
|
||||
* Method for return file path of Gallery image
|
||||
* @return path of the selected image file from gallery
|
||||
*/
|
||||
private var filePathUri: Uri? = null
|
||||
|
||||
fun getPath(context: Context, uri: Uri): String? {
|
||||
//check here to KITKAT or new version
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
val isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
|
||||
filePathUri = uri
|
||||
// DocumentProvider
|
||||
if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { // ExternalStorageProvider
|
||||
if (isExternalStorageDocument(uri)) {
|
||||
val docId = DocumentsContract.getDocumentId(uri)
|
||||
val split = docId.split(":")
|
||||
val type = split[0]
|
||||
if ("primary".equals(type, ignoreCase = true)) {
|
||||
return Environment.getExternalStorageDirectory().toString() + "/" + split[1]
|
||||
}
|
||||
} else if (isDownloadsDocument(uri)) {
|
||||
val id = DocumentsContract.getDocumentId(uri)
|
||||
val contentUri = ContentUris.withAppendedId(
|
||||
Uri.parse("content://downloads/public_downloads"),
|
||||
java.lang.Long.valueOf(id)
|
||||
)
|
||||
//return getDataColumn(context, uri, null, null);
|
||||
return getDataColumn(context, contentUri, null, null)
|
||||
} else if (isMediaDocument(uri)) {
|
||||
val docId = DocumentsContract.getDocumentId(uri)
|
||||
val split = docId.split(":").toTypedArray()
|
||||
val type = split[0]
|
||||
var contentUri: Uri? = null
|
||||
when (type) {
|
||||
"image" -> {
|
||||
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
||||
}
|
||||
|
||||
"video" -> {
|
||||
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
|
||||
}
|
||||
|
||||
"audio" -> {
|
||||
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
|
||||
}
|
||||
}
|
||||
val selection = "_id=?"
|
||||
val selectionArgs = arrayOf(split[1])
|
||||
return getDataColumn(context, contentUri, selection, selectionArgs)
|
||||
}
|
||||
} else if (DocumentsContractCompat.isTreeUri(uri)) {
|
||||
if (isExternalStorageDocument(uri)) {
|
||||
val docId = DocumentsContract.getTreeDocumentId(uri)
|
||||
val split = docId.split(":")
|
||||
val type = split[0]
|
||||
if ("primary".equals(type, ignoreCase = true)) {
|
||||
return Environment.getExternalStorageDirectory().toString() + "/" + split[1]
|
||||
}
|
||||
}
|
||||
} else if ("content".equals(uri.scheme, ignoreCase = true)) { // Return the remote address
|
||||
return if (isGooglePhotosUri(uri)) uri.lastPathSegment
|
||||
else getDataColumn(context, uri, null, null)
|
||||
} else if ("file".equals(uri.scheme, ignoreCase = true)) {
|
||||
return uri.path
|
||||
}
|
||||
return uri.path
|
||||
}
|
||||
|
||||
fun getTreePath(uri: Uri): String? {
|
||||
if (!DocumentsContractCompat.isTreeUri(uri) || !isExternalStorageDocument(uri)) {
|
||||
return null
|
||||
}
|
||||
val docId = DocumentsContract.getTreeDocumentId(uri)
|
||||
val split = docId.split(":")
|
||||
if (split.size < 2) {
|
||||
return null
|
||||
}
|
||||
val type = split[0]
|
||||
if ("primary".equals(type, ignoreCase = true)) {
|
||||
return Environment.getExternalStorageDirectory().toString() + "/" + split[1]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the data column for this Uri. This is useful for
|
||||
* MediaStore Uris, and other file-based ContentProviders.
|
||||
*
|
||||
* @param context The context.
|
||||
* @param uri The Uri to query.
|
||||
* @param selection (Optional) Filter used in the query.
|
||||
* @param selectionArgs (Optional) Selection arguments used in the query.
|
||||
* @return The value of the _data column, which is typically a file path.
|
||||
*/
|
||||
private fun getDataColumn(
|
||||
context: Context, uri: Uri?, selection: String?,
|
||||
selectionArgs: Array<String>?
|
||||
): String? {
|
||||
var cursor: Cursor? = null
|
||||
val column = "_data"
|
||||
val projection = arrayOf(
|
||||
column
|
||||
)
|
||||
try {
|
||||
cursor =
|
||||
context.contentResolver.query(uri!!, projection, selection, selectionArgs, null)
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
val index = cursor.getColumnIndexOrThrow(column)
|
||||
return cursor.getString(index)
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
e.printOnDebug()
|
||||
val file = File(context.cacheDir, "tmp")
|
||||
val filePath = file.absolutePath
|
||||
try {
|
||||
return context.contentResolver.openFileDescriptor(filePathUri!!, "r")?.use {
|
||||
val fd = it.fileDescriptor
|
||||
FileInputStream(fd).use { fis ->
|
||||
FileOutputStream(filePath).use { fos ->
|
||||
fis.copyTo(fos)
|
||||
}
|
||||
}
|
||||
File(filePath).absolutePath
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
} finally {
|
||||
cursor?.close()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uri The Uri to check.
|
||||
* @return Whether the Uri authority is ExternalStorageProvider.
|
||||
*/
|
||||
private fun isExternalStorageDocument(uri: Uri): Boolean {
|
||||
return "com.android.externalstorage.documents" == uri.authority
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uri The Uri to check.
|
||||
* @return Whether the Uri authority is DownloadsProvider.
|
||||
*/
|
||||
private fun isDownloadsDocument(uri: Uri): Boolean {
|
||||
return "com.android.providers.downloads.documents" == uri.authority
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uri The Uri to check.
|
||||
* @return Whether the Uri authority is MediaProvider.
|
||||
*/
|
||||
private fun isMediaDocument(uri: Uri): Boolean {
|
||||
return "com.android.providers.media.documents" == uri.authority
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uri The Uri to check.
|
||||
* @return Whether the Uri authority is Google Photos.
|
||||
*/
|
||||
private fun isGooglePhotosUri(uri: Uri): Boolean {
|
||||
return "com.google.android.apps.photos.content" == uri.authority
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
fun RecyclerView.findCenterViewPosition(): Int {
|
||||
return getChildAdapterPosition(
|
||||
findChildViewUnder(width / 2f, height / 2f) ?: return RecyclerView.NO_POSITION
|
||||
)
|
||||
}
|
||||
|
||||
fun RecyclerView.findViewPosition(x: Float, y: Float): Int {
|
||||
return getChildAdapterPosition(findChildViewUnder(x, y) ?: return RecyclerView.NO_POSITION)
|
||||
}
|
||||
|
||||
fun RecyclerView.findFirstVisibleViewPosition(): Int {
|
||||
var pos = -1
|
||||
if (layoutManager is LinearLayoutManager) {
|
||||
pos = (layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
|
||||
}
|
||||
return pos
|
||||
}
|
||||
|
||||
fun RecyclerView.findLastVisibleViewPosition(): Int {
|
||||
var pos = -1
|
||||
if (layoutManager is LinearLayoutManager) {
|
||||
pos = (layoutManager as LinearLayoutManager).findLastVisibleItemPosition()
|
||||
}
|
||||
return pos
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import androidx.core.os.postDelayed
|
||||
import com.script.ScriptBindings
|
||||
import com.script.rhino.RhinoScriptEngine
|
||||
import io.legato.kazusa.exception.RegexTimeoutException
|
||||
import io.legato.kazusa.help.CrashHandler
|
||||
import io.legato.kazusa.help.coroutine.Coroutine
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import splitties.init.appCtx
|
||||
import java.util.regex.Matcher
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
private val handler by lazy { buildMainHandler() }
|
||||
|
||||
/**
|
||||
* 带有超时检测的正则替换
|
||||
*/
|
||||
fun CharSequence.replace(regex: Regex, replacement: String, timeout: Long): String {
|
||||
val charSequence = this@replace
|
||||
val isJs = replacement.startsWith("@js:")
|
||||
val replacement1 = if (isJs) replacement.substring(4) else replacement
|
||||
return runBlocking {
|
||||
suspendCancellableCoroutine { block ->
|
||||
val coroutine = Coroutine.async(executeContext = IO) {
|
||||
try {
|
||||
val pattern = regex.toPattern()
|
||||
val matcher = pattern.matcher(charSequence)
|
||||
val stringBuffer = StringBuffer()
|
||||
while (matcher.find()) {
|
||||
if (isJs) {
|
||||
val jsResult = RhinoScriptEngine.run {
|
||||
val bindings = ScriptBindings()
|
||||
bindings["result"] = matcher.group()
|
||||
eval(replacement1, bindings)
|
||||
}.toString()
|
||||
val quotedResult = Matcher.quoteReplacement(jsResult)
|
||||
matcher.appendReplacement(stringBuffer, quotedResult)
|
||||
} else {
|
||||
matcher.appendReplacement(stringBuffer, replacement1)
|
||||
}
|
||||
}
|
||||
matcher.appendTail(stringBuffer)
|
||||
block.resume(stringBuffer.toString())
|
||||
} catch (e: Exception) {
|
||||
block.resumeWithException(e)
|
||||
}
|
||||
}
|
||||
handler.postDelayed(timeout) {
|
||||
if (coroutine.isActive) {
|
||||
val timeoutMsg =
|
||||
"替换超时,3秒后还未结束将重启应用\n替换规则$regex\n替换内容:$charSequence"
|
||||
val exception = RegexTimeoutException(timeoutMsg)
|
||||
block.cancel(exception)
|
||||
appCtx.longToastOnUi(timeoutMsg)
|
||||
CrashHandler.saveCrashInfo2File(exception)
|
||||
handler.postDelayed(3000) {
|
||||
if (coroutine.isActive) {
|
||||
appCtx.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.RequestManager
|
||||
import splitties.init.appCtx
|
||||
|
||||
private val applicationRequestManager by lazy {
|
||||
Glide.with(appCtx)
|
||||
}
|
||||
|
||||
fun RequestManager.lifecycle(lifecycle: Lifecycle): RequestManager {
|
||||
if (this === applicationRequestManager) {
|
||||
return this
|
||||
}
|
||||
|
||||
val observer = object : DefaultLifecycleObserver {
|
||||
override fun onResume(owner: LifecycleOwner) = onStart()
|
||||
override fun onPause(owner: LifecycleOwner) = onStop()
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
owner.lifecycle.removeObserver(this)
|
||||
}
|
||||
}
|
||||
|
||||
lifecycle.addObserver(observer)
|
||||
|
||||
return this
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.os.Build
|
||||
|
||||
val isVivoDevice by lazy {
|
||||
Build.MANUFACTURER.equals("vivo", ignoreCase = true)
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.view.View
|
||||
import androidx.annotation.StringRes
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
|
||||
/**
|
||||
* Display the Snackbar with the [Snackbar.LENGTH_SHORT] duration.
|
||||
*
|
||||
* @param message the message text resource.
|
||||
*/
|
||||
@JvmName("snackbar2")
|
||||
fun View.snackbar(
|
||||
@StringRes message: Int
|
||||
) = Snackbar
|
||||
.make(this, message, Snackbar.LENGTH_SHORT)
|
||||
.apply { show() }
|
||||
|
||||
/**
|
||||
* Display Snackbar with the [Snackbar.LENGTH_LONG] duration.
|
||||
*
|
||||
* @param message the message text resource.
|
||||
*/
|
||||
@JvmName("longSnackbar2")
|
||||
fun View.longSnackbar(
|
||||
@StringRes message: Int
|
||||
) = Snackbar
|
||||
.make(this, message, Snackbar.LENGTH_LONG)
|
||||
.apply { show() }
|
||||
|
||||
/**
|
||||
* Display Snackbar with the [Snackbar.LENGTH_INDEFINITE] duration.
|
||||
*
|
||||
* @param message the message text resource.
|
||||
*/
|
||||
@JvmName("indefiniteSnackbar2")
|
||||
fun View.indefiniteSnackbar(
|
||||
@StringRes message: Int
|
||||
) = Snackbar
|
||||
.make(this, message, Snackbar.LENGTH_INDEFINITE)
|
||||
.apply { show() }
|
||||
|
||||
/**
|
||||
* Display the Snackbar with the [Snackbar.LENGTH_SHORT] duration.
|
||||
*
|
||||
* @param message the message text.
|
||||
*/
|
||||
@JvmName("snackbar2")
|
||||
fun View.snackbar(
|
||||
message: CharSequence
|
||||
) = Snackbar
|
||||
.make(this, message, Snackbar.LENGTH_SHORT)
|
||||
.apply { show() }
|
||||
|
||||
/**
|
||||
* Display Snackbar with the [Snackbar.LENGTH_LONG] duration.
|
||||
*
|
||||
* @param message the message text.
|
||||
*/
|
||||
@JvmName("longSnackbar2")
|
||||
fun View.longSnackbar(
|
||||
message: CharSequence
|
||||
) = Snackbar
|
||||
.make(this, message, Snackbar.LENGTH_LONG)
|
||||
.apply { show() }
|
||||
|
||||
/**
|
||||
* Display Snackbar with the [Snackbar.LENGTH_INDEFINITE] duration.
|
||||
*
|
||||
* @param message the message text.
|
||||
*/
|
||||
@JvmName("indefiniteSnackbar2")
|
||||
fun View.indefiniteSnackbar(
|
||||
message: CharSequence
|
||||
) = Snackbar
|
||||
.make(this, message, Snackbar.LENGTH_INDEFINITE)
|
||||
.apply { show() }
|
||||
|
||||
/**
|
||||
* Display the Snackbar with the [Snackbar.LENGTH_SHORT] duration.
|
||||
*
|
||||
* @param message the message text resource.
|
||||
*/
|
||||
@JvmName("snackbar2")
|
||||
fun View.snackbar(
|
||||
message: Int,
|
||||
@StringRes actionText:
|
||||
Int, action: (View) -> Unit
|
||||
) = Snackbar
|
||||
.make(this, message, Snackbar.LENGTH_SHORT)
|
||||
.setAction(actionText, action)
|
||||
.apply { show() }
|
||||
|
||||
/**
|
||||
* Display Snackbar with the [Snackbar.LENGTH_LONG] duration.
|
||||
*
|
||||
* @param message the message text resource.
|
||||
*/
|
||||
@JvmName("longSnackbar2")
|
||||
fun View.longSnackbar(
|
||||
@StringRes message: Int,
|
||||
@StringRes actionText: Int,
|
||||
action: (View) -> Unit
|
||||
) = Snackbar
|
||||
.make(this, message, Snackbar.LENGTH_LONG)
|
||||
.setAction(actionText, action)
|
||||
.apply { show() }
|
||||
|
||||
/**
|
||||
* Display Snackbar with the [Snackbar.LENGTH_INDEFINITE] duration.
|
||||
*
|
||||
* @param message the message text resource.
|
||||
*/
|
||||
@JvmName("indefiniteSnackbar2")
|
||||
fun View.indefiniteSnackbar(
|
||||
@StringRes message: Int,
|
||||
@StringRes actionText: Int,
|
||||
action: (View) -> Unit
|
||||
) = Snackbar
|
||||
.make(this, message, Snackbar.LENGTH_INDEFINITE)
|
||||
.setAction(actionText, action)
|
||||
.apply { show() }
|
||||
|
||||
/**
|
||||
* Display the Snackbar with the [Snackbar.LENGTH_SHORT] duration.
|
||||
*
|
||||
* @param message the message text.
|
||||
*/
|
||||
@JvmName("snackbar2")
|
||||
fun View.snackbar(
|
||||
message: CharSequence,
|
||||
actionText: CharSequence,
|
||||
action: (View) -> Unit
|
||||
) = Snackbar
|
||||
.make(this, message, Snackbar.LENGTH_SHORT)
|
||||
.setAction(actionText, action)
|
||||
.apply { show() }
|
||||
|
||||
/**
|
||||
* Display Snackbar with the [Snackbar.LENGTH_LONG] duration.
|
||||
*
|
||||
* @param message the message text.
|
||||
*/
|
||||
@JvmName("longSnackbar2")
|
||||
fun View.longSnackbar(
|
||||
message: CharSequence,
|
||||
actionText: CharSequence,
|
||||
action: (View) -> Unit
|
||||
) = Snackbar
|
||||
.make(this, message, Snackbar.LENGTH_LONG)
|
||||
.setAction(actionText, action)
|
||||
.apply { show() }
|
||||
|
||||
/**
|
||||
* Display Snackbar with the [Snackbar.LENGTH_INDEFINITE] duration.
|
||||
*
|
||||
* @param message the message text.
|
||||
*/
|
||||
@JvmName("indefiniteSnackbar2")
|
||||
fun View.indefiniteSnackbar(
|
||||
message: CharSequence,
|
||||
actionText: CharSequence,
|
||||
action: (View) -> Unit
|
||||
) = Snackbar
|
||||
.make(this, message, Snackbar.LENGTH_INDEFINITE)
|
||||
.setAction(actionText, action)
|
||||
.apply { show() }
|
||||
@@ -0,0 +1,142 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.icu.text.Collator
|
||||
import android.icu.util.ULocale
|
||||
import android.net.Uri
|
||||
import android.text.Editable
|
||||
import cn.hutool.core.net.URLEncodeUtil
|
||||
import io.legato.kazusa.constant.AppPattern
|
||||
import io.legato.kazusa.constant.AppPattern.dataUriRegex
|
||||
import java.io.File
|
||||
import java.lang.Character.codePointCount
|
||||
import java.lang.Character.offsetByCodePoints
|
||||
import java.util.Locale
|
||||
import java.util.regex.Pattern
|
||||
|
||||
fun String?.safeTrim() = if (this.isNullOrBlank()) null else this.trim()
|
||||
|
||||
fun String?.isContentScheme(): Boolean = this?.startsWith("content://") == true
|
||||
|
||||
fun String.toEditable(): Editable = Editable.Factory.getInstance().newEditable(this)
|
||||
|
||||
fun String.parseToUri(): Uri {
|
||||
return if (isUri()) Uri.parse(this) else {
|
||||
Uri.fromFile(File(this))
|
||||
}
|
||||
}
|
||||
|
||||
fun String?.isUri(): Boolean {
|
||||
this ?: return false
|
||||
return this.startsWith("file://", true) || isContentScheme()
|
||||
}
|
||||
|
||||
fun String?.isAbsUrl() =
|
||||
this?.let {
|
||||
it.startsWith("http://", true) || it.startsWith("https://", true)
|
||||
} ?: false
|
||||
|
||||
fun String?.isDataUrl() =
|
||||
this?.let {
|
||||
dataUriRegex.matches(it)
|
||||
} ?: false
|
||||
|
||||
fun String?.isJson(): Boolean =
|
||||
this?.run {
|
||||
val str = this.trim()
|
||||
when {
|
||||
str.startsWith("{") && str.endsWith("}") -> true
|
||||
str.startsWith("[") && str.endsWith("]") -> true
|
||||
else -> false
|
||||
}
|
||||
} ?: false
|
||||
|
||||
fun String?.isJsonObject(): Boolean =
|
||||
this?.run {
|
||||
val str = this.trim()
|
||||
str.startsWith("{") && str.endsWith("}")
|
||||
} ?: false
|
||||
|
||||
fun String?.isJsonArray(): Boolean =
|
||||
this?.run {
|
||||
val str = this.trim()
|
||||
str.startsWith("[") && str.endsWith("]")
|
||||
} ?: false
|
||||
|
||||
fun String?.isXml(): Boolean =
|
||||
this?.run {
|
||||
val str = this.trim()
|
||||
str.startsWith("<") && str.endsWith(">")
|
||||
} ?: false
|
||||
|
||||
fun String?.isTrue(nullIsTrue: Boolean = false): Boolean {
|
||||
if (this.isNullOrBlank() || this == "null") {
|
||||
return nullIsTrue
|
||||
}
|
||||
return !this.trim().matches("(?i)^(false|no|not|0)$".toRegex())
|
||||
}
|
||||
|
||||
fun String.isHex(): Boolean {
|
||||
return all {c ->
|
||||
c in '0'..'9' || c in 'A'..'F' || c in 'a'..'f'
|
||||
}
|
||||
}
|
||||
|
||||
fun String.splitNotBlank(vararg delimiter: String, limit: Int = 0): Array<String> = run {
|
||||
this.split(*delimiter, limit = limit).map { it.trim() }.filterNot { it.isBlank() }
|
||||
.toTypedArray()
|
||||
}
|
||||
|
||||
fun String.splitNotBlank(regex: Regex, limit: Int = 0): Array<String> = run {
|
||||
this.split(regex, limit).map { it.trim() }.filterNot { it.isBlank() }.toTypedArray()
|
||||
}
|
||||
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
fun String.cnCompare(other: String): Int {
|
||||
return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
|
||||
Collator.getInstance(ULocale.SIMPLIFIED_CHINESE).compare(this, other)
|
||||
} else {
|
||||
java.text.Collator.getInstance(Locale.CHINA).compare(this, other)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 字符串所占内存大小
|
||||
*/
|
||||
fun String?.memorySize(): Int {
|
||||
this ?: return 0
|
||||
return 40 + 2 * length
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否中文
|
||||
*/
|
||||
fun String.isChinese(): Boolean {
|
||||
val p = Pattern.compile("[\u4e00-\u9fa5]")
|
||||
val m = p.matcher(this)
|
||||
return m.find()
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字符串拆分为单个字符,包含emoji
|
||||
*/
|
||||
fun CharSequence.toStringArray(): Array<String> {
|
||||
var codePointIndex = 0
|
||||
return try {
|
||||
Array(codePointCount(this, 0, length)) {
|
||||
val start = codePointIndex
|
||||
codePointIndex = offsetByCodePoints(this, start, 1)
|
||||
substring(start, codePointIndex)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
split("").toTypedArray()
|
||||
}
|
||||
}
|
||||
|
||||
fun String.escapeRegex(): String {
|
||||
return replace(AppPattern.regexCharRegex, "\\\\$0")
|
||||
}
|
||||
|
||||
fun String.encodeURI(): String = URLEncodeUtil.encodeQuery(this)
|
||||
@@ -0,0 +1,370 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.text.TextUtils.isEmpty
|
||||
import android.util.Base64
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.text.DecimalFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import java.util.regex.Matcher
|
||||
import java.util.regex.Pattern
|
||||
import java.util.zip.GZIPInputStream
|
||||
import java.util.zip.GZIPOutputStream
|
||||
import kotlin.math.abs
|
||||
|
||||
|
||||
@Suppress("unused", "MemberVisibilityCanBePrivate")
|
||||
object StringUtils {
|
||||
private const val HOUR_OF_DAY = 24
|
||||
private const val DAY_OF_YESTERDAY = 2
|
||||
private const val TIME_UNIT = 60
|
||||
private val ChnMap = chnMap
|
||||
private val wordCountFormatter by lazy {
|
||||
DecimalFormat("#.#")
|
||||
}
|
||||
|
||||
private val chnMap: HashMap<Char, Int>
|
||||
get() {
|
||||
val map = HashMap<Char, Int>()
|
||||
var cnStr = "零一二三四五六七八九十"
|
||||
var c = cnStr.toCharArray()
|
||||
for (i in 0..10) {
|
||||
map[c[i]] = i
|
||||
}
|
||||
cnStr = "〇壹贰叁肆伍陆柒捌玖拾"
|
||||
c = cnStr.toCharArray()
|
||||
for (i in 0..10) {
|
||||
map[c[i]] = i
|
||||
}
|
||||
map['两'] = 2
|
||||
map['百'] = 100
|
||||
map['佰'] = 100
|
||||
map['千'] = 1000
|
||||
map['仟'] = 1000
|
||||
map['万'] = 10000
|
||||
map['亿'] = 100000000
|
||||
return map
|
||||
}
|
||||
|
||||
/**
|
||||
* 将日期转换成昨天、今天、明天
|
||||
*/
|
||||
fun dateConvert(source: String, pattern: String): String {
|
||||
val format = SimpleDateFormat(pattern, Locale.getDefault())
|
||||
val calendar = Calendar.getInstance()
|
||||
kotlin.runCatching {
|
||||
val date = format.parse(source) ?: return ""
|
||||
val curTime = calendar.timeInMillis
|
||||
calendar.time = date
|
||||
//将MISC 转换成 sec
|
||||
val difSec = abs((curTime - date.time) / 1000)
|
||||
val difMin = difSec / 60
|
||||
val difHour = difMin / 60
|
||||
val difDate = difHour / 60
|
||||
val oldHour = calendar.get(Calendar.HOUR)
|
||||
//如果没有时间
|
||||
if (oldHour == 0) {
|
||||
//比日期:昨天今天和明天
|
||||
return when {
|
||||
difDate == 0L -> "今天"
|
||||
difDate < DAY_OF_YESTERDAY -> "昨天"
|
||||
else -> {
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
val convertFormat = SimpleDateFormat("yyyy-MM-dd")
|
||||
convertFormat.format(date)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return when {
|
||||
difSec < TIME_UNIT -> difSec.toString() + "秒前"
|
||||
difMin < TIME_UNIT -> difMin.toString() + "分钟前"
|
||||
difHour < HOUR_OF_DAY -> difHour.toString() + "小时前"
|
||||
difDate < DAY_OF_YESTERDAY -> "昨天"
|
||||
else -> {
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
val convertFormat = SimpleDateFormat("yyyy-MM-dd")
|
||||
convertFormat.format(date)
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
it.printOnDebug()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
/**
|
||||
* 首字母大写
|
||||
*/
|
||||
@SuppressLint("DefaultLocale")
|
||||
fun toFirstCapital(str: String): String {
|
||||
return str.substring(0, 1).uppercase(Locale.getDefault()) + str.substring(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* 将文本中的半角字符,转换成全角字符
|
||||
*/
|
||||
fun halfToFull(input: String): String {
|
||||
val c = input.toCharArray()
|
||||
for (i in c.indices) {
|
||||
if (c[i].code == 32)
|
||||
//半角空格
|
||||
{
|
||||
c[i] = 12288.toChar()
|
||||
continue
|
||||
}
|
||||
//根据实际情况,过滤不需要转换的符号
|
||||
//if (c[i] == 46) //半角点号,不转换
|
||||
// continue;
|
||||
|
||||
if (c[i].code in 33..126)
|
||||
//其他符号都转换为全角
|
||||
c[i] = (c[i].code + 65248).toChar()
|
||||
}
|
||||
return String(c)
|
||||
}
|
||||
|
||||
/**
|
||||
* 字符串全角转换为半角
|
||||
*/
|
||||
fun fullToHalf(input: String): String {
|
||||
val c = input.toCharArray()
|
||||
for (i in c.indices) {
|
||||
if (c[i].code == 12288)
|
||||
//全角空格
|
||||
{
|
||||
c[i] = 32.toChar()
|
||||
continue
|
||||
}
|
||||
|
||||
if (c[i].code in 65281..65374)
|
||||
c[i] = (c[i].code - 65248).toChar()
|
||||
}
|
||||
return String(c)
|
||||
}
|
||||
|
||||
/**
|
||||
* 中文大写数字转数字
|
||||
*/
|
||||
fun chineseNumToInt(chNum: String): Int {
|
||||
var result = 0
|
||||
var tmp = 0
|
||||
var billion = 0
|
||||
val cn = chNum.toCharArray()
|
||||
|
||||
// "一零二五" 形式
|
||||
if (cn.size > 1 && chNum.matches("^[〇零一二三四五六七八九壹贰叁肆伍陆柒捌玖]$".toRegex())) {
|
||||
for (i in cn.indices) {
|
||||
cn[i] = (48 + ChnMap[cn[i]]!!).toChar()
|
||||
}
|
||||
return Integer.parseInt(String(cn))
|
||||
}
|
||||
|
||||
// "一千零二十五", "一千二" 形式
|
||||
return kotlin.runCatching {
|
||||
for (i in cn.indices) {
|
||||
val tmpNum = ChnMap[cn[i]]!!
|
||||
when {
|
||||
tmpNum == 100000000 -> {
|
||||
result += tmp
|
||||
result *= tmpNum
|
||||
billion = billion * 100000000 + result
|
||||
result = 0
|
||||
tmp = 0
|
||||
}
|
||||
|
||||
tmpNum == 10000 -> {
|
||||
result += tmp
|
||||
result *= tmpNum
|
||||
tmp = 0
|
||||
}
|
||||
|
||||
tmpNum >= 10 -> {
|
||||
if (tmp == 0)
|
||||
tmp = 1
|
||||
result += tmpNum * tmp
|
||||
tmp = 0
|
||||
}
|
||||
|
||||
else -> {
|
||||
tmp = if (i >= 2 && i == cn.size - 1 && ChnMap[cn[i - 1]]!! > 10)
|
||||
tmpNum * ChnMap[cn[i - 1]]!! / 10
|
||||
else
|
||||
tmp * 10 + tmpNum
|
||||
}
|
||||
}
|
||||
}
|
||||
result += tmp + billion
|
||||
result
|
||||
}.getOrDefault(-1)
|
||||
}
|
||||
|
||||
/**
|
||||
* 字符串转数字
|
||||
*/
|
||||
fun stringToInt(str: String?): Int {
|
||||
if (str != null) {
|
||||
val num = fullToHalf(str).replace("\\s+".toRegex(), "")
|
||||
return kotlin.runCatching {
|
||||
Integer.parseInt(num)
|
||||
}.getOrElse {
|
||||
chineseNumToInt(num)
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否包含数字
|
||||
*/
|
||||
fun isContainNumber(company: String): Boolean {
|
||||
val p = Pattern.compile("[0-9]+")
|
||||
val m = p.matcher(company)
|
||||
return m.find()
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否数字
|
||||
*/
|
||||
fun isNumeric(str: String): Boolean {
|
||||
val pattern = Pattern.compile("-?[0-9]+")
|
||||
val isNum = pattern.matcher(str)
|
||||
return isNum.matches()
|
||||
}
|
||||
|
||||
fun wordCountFormat(words: Int): String {
|
||||
var wordsS = ""
|
||||
if (words > 0) {
|
||||
if (words > 10000) {
|
||||
val df = wordCountFormatter
|
||||
wordsS = df.format(words * 1.0f / 10000f.toDouble()) + "万字"
|
||||
} else {
|
||||
wordsS = words.toString() + "字"
|
||||
}
|
||||
}
|
||||
return wordsS
|
||||
}
|
||||
|
||||
fun wordCountFormat(wc: String?): String {
|
||||
if (wc == null) return ""
|
||||
var wordsS = ""
|
||||
if (isNumeric(wc)) {
|
||||
val words: Int = wc.toInt()
|
||||
if (words > 0) {
|
||||
if (words > 10000) {
|
||||
val df = wordCountFormatter
|
||||
wordsS = df.format(words * 1.0f / 10000f.toDouble()) + "万字"
|
||||
} else {
|
||||
wordsS = words.toString() + "字"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
wordsS = wc
|
||||
}
|
||||
return wordsS
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除字符串首尾空字符的高效方法(利用ASCII值判断,包括全角空格)
|
||||
*/
|
||||
fun trim(s: String): String {
|
||||
if (isEmpty(s)) return ""
|
||||
var start = 0
|
||||
val len = s.length
|
||||
var end = len - 1
|
||||
while (start < end && (s[start].code <= 0x20 || s[start] == ' ')) {
|
||||
++start
|
||||
}
|
||||
while (start < end && (s[end].code <= 0x20 || s[end] == ' ')) {
|
||||
--end
|
||||
}
|
||||
++end
|
||||
return if (start > 0 || end < len) s.substring(start, end) else s
|
||||
}
|
||||
|
||||
/**
|
||||
* 重复字符串
|
||||
*/
|
||||
fun repeat(str: String, n: Int): String {
|
||||
val stringBuilder = StringBuilder()
|
||||
for (i in 0 until n) {
|
||||
stringBuilder.append(str)
|
||||
}
|
||||
return stringBuilder.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除UTF头
|
||||
*/
|
||||
fun removeUTFCharacters(data: String?): String? {
|
||||
if (data == null) return null
|
||||
val p = Pattern.compile("\\\\u(\\p{XDigit}{4})")
|
||||
val m = p.matcher(data)
|
||||
val buf = StringBuffer(data.length)
|
||||
while (m.find()) {
|
||||
val ch = Integer.parseInt(m.group(1)!!, 16).toChar().toString()
|
||||
m.appendReplacement(buf, Matcher.quoteReplacement(ch))
|
||||
}
|
||||
m.appendTail(buf)
|
||||
return buf.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩字符串
|
||||
*/
|
||||
fun compress(str: String): Result<String> {
|
||||
return kotlin.runCatching {
|
||||
if (str.isEmpty()) {
|
||||
return@runCatching str
|
||||
}
|
||||
val out = ByteArrayOutputStream()
|
||||
var gzip: GZIPOutputStream? = null
|
||||
return@runCatching try {
|
||||
gzip = GZIPOutputStream(out)
|
||||
gzip.write(str.toByteArray())
|
||||
Base64.encodeToString(out.toByteArray(), Base64.NO_WRAP)
|
||||
} finally {
|
||||
gzip?.runCatching {
|
||||
close()
|
||||
}
|
||||
out.runCatching {
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解压字符串
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun unCompress(str: String): Result<String> {
|
||||
return kotlin.runCatching {
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
var inputStream: ByteArrayInputStream? = null
|
||||
var ginZip: GZIPInputStream? = null
|
||||
return@runCatching try {
|
||||
val compressed = Base64.decode(str, Base64.NO_WRAP)
|
||||
inputStream = ByteArrayInputStream(compressed)
|
||||
ginZip = GZIPInputStream(inputStream)
|
||||
ginZip.copyTo(outputStream)
|
||||
outputStream.toString()
|
||||
} finally {
|
||||
ginZip?.runCatching {
|
||||
close()
|
||||
}
|
||||
inputStream?.runCatching {
|
||||
close()
|
||||
}
|
||||
outputStream.runCatching {
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.RectF
|
||||
import android.util.Size
|
||||
import java.io.FileInputStream
|
||||
import java.io.InputStream
|
||||
import com.caverock.androidsvg.SVG
|
||||
import kotlin.math.max
|
||||
|
||||
@Suppress("WeakerAccess", "MemberVisibilityCanBePrivate")
|
||||
object SvgUtils {
|
||||
|
||||
/**
|
||||
* 从Svg中解码bitmap
|
||||
*/
|
||||
|
||||
fun createBitmap(filePath: String, width: Int, height: Int? = null): Bitmap? {
|
||||
return kotlin.runCatching {
|
||||
val inputStream = FileInputStream(filePath)
|
||||
createBitmap(inputStream, width, height)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
fun createBitmap(inputStream: InputStream, width: Int, height: Int? = null): Bitmap? {
|
||||
return kotlin.runCatching {
|
||||
val svg = SVG.getFromInputStream(inputStream)
|
||||
createBitmap(svg, width, height)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
//获取svg图片大小
|
||||
fun getSize(filePath: String): Size? {
|
||||
return kotlin.runCatching {
|
||||
val inputStream = FileInputStream(filePath)
|
||||
getSize(inputStream)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
fun getSize(inputStream: InputStream): Size? {
|
||||
return kotlin.runCatching {
|
||||
val svg = SVG.getFromInputStream(inputStream)
|
||||
getSize(svg)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
/////// private method
|
||||
private fun createBitmap(svg: SVG, width: Int? = null, height: Int? = null): Bitmap {
|
||||
val size = getSize(svg)
|
||||
val wRatio = width?.let { size.width / it } ?: -1
|
||||
val hRatio = height?.let { size.height / it } ?: -1
|
||||
//如果超出指定大小,则缩小相应的比例
|
||||
val ratio = when {
|
||||
wRatio > 1 && hRatio > 1 -> max(wRatio, hRatio)
|
||||
wRatio > 1 -> wRatio
|
||||
hRatio > 1 -> hRatio
|
||||
else -> 1
|
||||
}
|
||||
|
||||
val viewBox: RectF? = svg.documentViewBox
|
||||
if (viewBox == null && size.width > 0 && size.height > 0) {
|
||||
svg.setDocumentViewBox(0f, 0f, svg.documentWidth, svg.documentHeight)
|
||||
}
|
||||
|
||||
svg.setDocumentWidth("100%")
|
||||
svg.setDocumentHeight("100%")
|
||||
|
||||
val bitmapWidth = size.width / ratio
|
||||
val bitmapHeight = size.height / ratio
|
||||
val bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888)
|
||||
|
||||
svg.renderToCanvas(Canvas(bitmap))
|
||||
return bitmap
|
||||
}
|
||||
|
||||
private fun getSize(svg: SVG): Size {
|
||||
val width = svg.documentWidth.toInt().takeIf { it > 0 }
|
||||
?: (svg.documentViewBox.right - svg.documentViewBox.left).toInt()
|
||||
val height = svg.documentHeight.toInt().takeIf { it > 0 }
|
||||
?: (svg.documentViewBox.bottom - svg.documentViewBox.top).toInt()
|
||||
return Size(width, height)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.view.Choreographer
|
||||
|
||||
class SyncedRenderer(val doFrame: (frameTime: Double) -> Unit) {
|
||||
|
||||
private var callback: (Long) -> Unit = {}
|
||||
|
||||
fun start() {
|
||||
var currTime = System.nanoTime() / 1000000.0
|
||||
callback = {
|
||||
val currTimeMs = it / 1000000.0
|
||||
val frameTime = currTimeMs - currTime
|
||||
currTime = currTimeMs
|
||||
doFrame(frameTime)
|
||||
Choreographer.getInstance().postFrameCallback(callback)
|
||||
}
|
||||
Choreographer.getInstance().postFrameCallback(callback)
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
Choreographer.getInstance().removeFrameCallback(callback)
|
||||
callback = {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.Settings
|
||||
import android.view.Display
|
||||
import splitties.init.appCtx
|
||||
import splitties.systemservices.displayManager
|
||||
import splitties.systemservices.powerManager
|
||||
|
||||
|
||||
@Suppress("unused")
|
||||
object SystemUtils {
|
||||
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
fun ignoreBatteryOptimization(activity: Activity) {
|
||||
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) return
|
||||
|
||||
val hasIgnored = powerManager.isIgnoringBatteryOptimizations(activity.packageName)
|
||||
// 判断当前APP是否有加入电池优化的白名单,如果没有,弹出加入电池优化的白名单的设置对话框。
|
||||
if (!hasIgnored) {
|
||||
try {
|
||||
@SuppressLint("BatteryLife")
|
||||
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
|
||||
intent.data = Uri.parse("package:" + activity.packageName)
|
||||
activity.startActivity(intent)
|
||||
} catch (ignored: Throwable) {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun isScreenOn(): Boolean {
|
||||
return displayManager.displays.filterNotNull().any {
|
||||
it.state != Display.STATE_OFF
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 屏幕像素宽度
|
||||
*/
|
||||
val screenWidthPx by lazy {
|
||||
appCtx.resources.displayMetrics.widthPixels
|
||||
}
|
||||
|
||||
/**
|
||||
* 屏幕像素高度
|
||||
*/
|
||||
val screenHeightPx by lazy {
|
||||
appCtx.resources.displayMetrics.heightPixels
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
@Suppress("unused")
|
||||
class Throttle<T>(
|
||||
wait: Long = 0L,
|
||||
leading: Boolean = true,
|
||||
trailing: Boolean = true,
|
||||
func: () -> T
|
||||
) : Debounce<T>(wait, wait, leading, trailing, func)
|
||||
|
||||
fun <T> throttle(
|
||||
wait: Long = 0L,
|
||||
leading: Boolean = true,
|
||||
trailing: Boolean = true,
|
||||
func: () -> T
|
||||
) = Throttle(wait, leading, trailing, func)
|
||||
@@ -0,0 +1,19 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import java.io.IOException
|
||||
|
||||
val Throwable.stackTraceStr: String
|
||||
get() {
|
||||
val stackTrace = stackTraceToString()
|
||||
val lMsg = this.localizedMessage ?: "noErrorMsg"
|
||||
return when {
|
||||
stackTrace.isNotEmpty() -> stackTrace
|
||||
else -> lMsg
|
||||
}
|
||||
}
|
||||
|
||||
fun Throwable.asIOException(): IOException {
|
||||
val newException = IOException(this.message)
|
||||
newException.initCause(this)
|
||||
return newException
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import kotlin.math.abs
|
||||
|
||||
fun Long.toTimeAgo(): String {
|
||||
val curTime = System.currentTimeMillis()
|
||||
val time = this
|
||||
val seconds = abs(System.currentTimeMillis() - time) / 1000f
|
||||
val end = if (time < curTime) "前" else "后"
|
||||
|
||||
val start = when {
|
||||
seconds < 60 -> "${seconds.toInt()}秒"
|
||||
seconds < 3600 -> {
|
||||
val minutes = seconds / 60f
|
||||
"${minutes.toInt()}分钟"
|
||||
}
|
||||
seconds < 86400 -> {
|
||||
val hours = seconds / 3600f
|
||||
"${hours.toInt()}小时"
|
||||
}
|
||||
seconds < 604800 -> {
|
||||
val days = seconds / 86400f
|
||||
"${days.toInt()}天"
|
||||
}
|
||||
seconds < 2_628_000 -> {
|
||||
val weeks = seconds / 604800f
|
||||
"${weeks.toInt()}周"
|
||||
}
|
||||
seconds < 31_536_000 -> {
|
||||
val months = seconds / 2_628_000f
|
||||
"${months.toInt()}月"
|
||||
}
|
||||
else -> {
|
||||
val years = seconds / 31_536_000f
|
||||
"${years.toInt()}年"
|
||||
}
|
||||
}
|
||||
return start + end
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import io.legato.kazusa.BuildConfig
|
||||
import io.legato.kazusa.help.config.AppConfig
|
||||
|
||||
private var toast: Toast? = null
|
||||
private var toastLegacy: Toast? = null
|
||||
|
||||
fun Context.toastOnUi(message: Int, duration: Int = Toast.LENGTH_SHORT) {
|
||||
toastOnUi(getString(message), duration)
|
||||
}
|
||||
|
||||
fun Context.toastOnUi(message: CharSequence?, duration: Int = Toast.LENGTH_SHORT) {
|
||||
runOnUI {
|
||||
kotlin.runCatching {
|
||||
toast?.cancel()
|
||||
toast = Toast.makeText(this, message, duration)
|
||||
toast?.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.toastOnUiLegacy(message: CharSequence) {
|
||||
runOnUI {
|
||||
kotlin.runCatching {
|
||||
if (toastLegacy == null || BuildConfig.DEBUG || AppConfig.recordLog) {
|
||||
toastLegacy = Toast.makeText(this, message, Toast.LENGTH_SHORT)
|
||||
} else {
|
||||
toastLegacy?.setText(message)
|
||||
toastLegacy?.duration = Toast.LENGTH_SHORT
|
||||
}
|
||||
toastLegacy?.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.longToastOnUi(message: Int) {
|
||||
toastOnUi(message, Toast.LENGTH_LONG)
|
||||
}
|
||||
|
||||
fun Context.longToastOnUi(message: CharSequence?) {
|
||||
toastOnUi(message, Toast.LENGTH_LONG)
|
||||
}
|
||||
|
||||
fun Context.longToastOnUiLegacy(message: CharSequence) {
|
||||
runOnUI {
|
||||
kotlin.runCatching {
|
||||
if (toastLegacy == null || BuildConfig.DEBUG || AppConfig.recordLog) {
|
||||
toastLegacy = Toast.makeText(this, message, Toast.LENGTH_LONG)
|
||||
} else {
|
||||
toastLegacy?.setText(message)
|
||||
toastLegacy?.duration = Toast.LENGTH_LONG
|
||||
}
|
||||
toastLegacy?.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Fragment.toastOnUi(message: Int) = requireActivity().toastOnUi(message)
|
||||
|
||||
fun Fragment.toastOnUi(message: CharSequence) = requireActivity().toastOnUi(message)
|
||||
|
||||
fun Fragment.longToast(message: Int) = requireContext().longToastOnUi(message)
|
||||
|
||||
fun Fragment.longToast(message: CharSequence) = requireContext().longToastOnUi(message)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.os.Build
|
||||
import android.widget.Toolbar
|
||||
import androidx.core.content.ContextCompat
|
||||
import io.legato.kazusa.R
|
||||
|
||||
/**
|
||||
* 设置toolBar更多图标颜色
|
||||
*/
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
fun Toolbar.setMoreIconColor(color: Int) {
|
||||
val moreIcon = ContextCompat.getDrawable(context, R.drawable.ic_more)
|
||||
if (moreIcon != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
moreIcon.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)
|
||||
overflowIcon = moreIcon
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.ParcelFileDescriptor
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.fragment.app.Fragment
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.constant.AppLog
|
||||
import io.legato.kazusa.exception.NoStackTraceException
|
||||
import io.legato.kazusa.lib.permission.Permissions
|
||||
import io.legato.kazusa.lib.permission.PermissionsCompat
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.RequestBody
|
||||
import okio.BufferedSink
|
||||
import okio.source
|
||||
import splitties.init.appCtx
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.charset.Charset
|
||||
|
||||
fun Uri.isContentScheme() = this.scheme == "content"
|
||||
|
||||
fun Uri.isFileScheme() = this.scheme == "file"
|
||||
|
||||
/**
|
||||
* 读取URI
|
||||
*/
|
||||
fun AppCompatActivity.readUri(
|
||||
uri: Uri?,
|
||||
success: (fileDoc: FileDoc, inputStream: InputStream) -> Unit
|
||||
) {
|
||||
uri ?: return
|
||||
try {
|
||||
if (uri.isContentScheme()) {
|
||||
val doc = DocumentFile.fromSingleUri(this, uri)
|
||||
doc ?: throw NoStackTraceException("未获取到文件")
|
||||
val fileDoc = FileDoc.fromDocumentFile(doc)
|
||||
contentResolver.openInputStream(uri)!!.use { inputStream ->
|
||||
success.invoke(fileDoc, inputStream)
|
||||
}
|
||||
} else {
|
||||
PermissionsCompat.Builder()
|
||||
.addPermissions(*Permissions.Group.STORAGE)
|
||||
.rationale(R.string.get_storage_per)
|
||||
.onGranted {
|
||||
RealPathUtil.getPath(this, uri)?.let { path ->
|
||||
val file = File(path)
|
||||
val fileDoc = FileDoc.fromFile(file)
|
||||
FileInputStream(file).use { inputStream ->
|
||||
success.invoke(fileDoc, inputStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
.request()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
AppLog.put("读取Uri出错\n$e", e, true)
|
||||
if (e is SecurityException) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取URI
|
||||
*/
|
||||
fun Fragment.readUri(uri: Uri?, success: (fileDoc: FileDoc, inputStream: InputStream) -> Unit) {
|
||||
uri ?: return
|
||||
try {
|
||||
if (uri.isContentScheme()) {
|
||||
val doc = DocumentFile.fromSingleUri(requireContext(), uri)
|
||||
doc ?: throw NoStackTraceException("未获取到文件")
|
||||
val fileDoc = FileDoc.fromDocumentFile(doc)
|
||||
requireContext().contentResolver.openInputStream(uri)!!.use { inputStream ->
|
||||
success.invoke(fileDoc, inputStream)
|
||||
}
|
||||
} else {
|
||||
PermissionsCompat.Builder()
|
||||
.addPermissions(*Permissions.Group.STORAGE)
|
||||
.rationale(R.string.get_storage_per)
|
||||
.onGranted {
|
||||
RealPathUtil.getPath(requireContext(), uri)?.let { path ->
|
||||
val file = File(path)
|
||||
val fileDoc = FileDoc.fromFile(file)
|
||||
FileInputStream(file).use { inputStream ->
|
||||
success.invoke(fileDoc, inputStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
.request()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
AppLog.put("读取Uri出错\n$e", e, true)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun Uri.readBytes(context: Context): ByteArray {
|
||||
return if (this.isContentScheme()) {
|
||||
context.contentResolver.openInputStream(this)?.let {
|
||||
val len: Int = it.available()
|
||||
val buffer = ByteArray(len)
|
||||
it.read(buffer)
|
||||
it.close()
|
||||
return buffer
|
||||
} ?: throw NoStackTraceException("打开文件失败\n${this}")
|
||||
} else {
|
||||
val path = RealPathUtil.getPath(context, this)
|
||||
if (path?.isNotEmpty() == true) {
|
||||
File(path).readBytes()
|
||||
} else {
|
||||
throw NoStackTraceException("获取文件真实地址失败\n${this.path}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun Uri.readText(context: Context): String {
|
||||
readBytes(context).let {
|
||||
return String(it)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun Uri.writeBytes(
|
||||
context: Context,
|
||||
byteArray: ByteArray
|
||||
): Boolean {
|
||||
if (this.isContentScheme()) {
|
||||
context.contentResolver.openOutputStream(this)?.let {
|
||||
it.write(byteArray)
|
||||
it.close()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
} else {
|
||||
val path = RealPathUtil.getPath(context, this)
|
||||
if (path?.isNotEmpty() == true) {
|
||||
File(path).writeBytes(byteArray)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun Uri.writeText(context: Context, text: String, charset: Charset = Charsets.UTF_8): Boolean {
|
||||
return writeBytes(context, text.toByteArray(charset))
|
||||
}
|
||||
|
||||
fun Uri.writeBytes(
|
||||
context: Context,
|
||||
fileName: String,
|
||||
byteArray: ByteArray
|
||||
): Boolean {
|
||||
if (this.isContentScheme()) {
|
||||
DocumentFile.fromTreeUri(context, this)?.let { pDoc ->
|
||||
DocumentUtils.createFileIfNotExist(pDoc, fileName)?.let {
|
||||
return it.uri.writeBytes(context, byteArray)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
FileUtils.createFileWithReplace(path + File.separatorChar + fileName)
|
||||
.writeBytes(byteArray)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun Uri.inputStream(context: Context): Result<InputStream> {
|
||||
val uri = this
|
||||
return kotlin.runCatching {
|
||||
try {
|
||||
if (isContentScheme()) {
|
||||
DocumentFile.fromSingleUri(context, uri)
|
||||
?: throw NoStackTraceException("未获取到文件")
|
||||
return@runCatching context.contentResolver.openInputStream(uri)!!
|
||||
} else {
|
||||
val path = RealPathUtil.getPath(context, uri)
|
||||
?: throw NoStackTraceException("未获取到文件")
|
||||
val file = File(path)
|
||||
if (file.exists()) {
|
||||
return@runCatching FileInputStream(file)
|
||||
} else {
|
||||
throw NoStackTraceException("文件不存在")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
AppLog.put("读取inputStream失败:${e.localizedMessage}", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Uri.outputStream(context: Context): Result<OutputStream> {
|
||||
val uri = this
|
||||
return kotlin.runCatching {
|
||||
try {
|
||||
if (isContentScheme()) {
|
||||
DocumentFile.fromSingleUri(context, uri)
|
||||
?: throw NoStackTraceException("未获取到文件")
|
||||
return@runCatching context.contentResolver.openOutputStream(uri)!!
|
||||
} else {
|
||||
val path = RealPathUtil.getPath(context, uri)
|
||||
?: throw NoStackTraceException("未获取到文件")
|
||||
val file = File(path)
|
||||
if (file.exists()) {
|
||||
return@runCatching FileOutputStream(file)
|
||||
} else {
|
||||
throw NoStackTraceException("文件不存在")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
AppLog.put("读取inputStream失败:${e.localizedMessage}", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Uri.toReadPfd(context: Context): Result<ParcelFileDescriptor> {
|
||||
val uri = this
|
||||
return kotlin.runCatching {
|
||||
try {
|
||||
if (isContentScheme()) {
|
||||
DocumentFile.fromSingleUri(context, uri)
|
||||
?: throw NoStackTraceException("未获取到文件")
|
||||
return@runCatching context.contentResolver.openFileDescriptor(uri, "r")!!
|
||||
} else {
|
||||
val path = RealPathUtil.getPath(context, uri)
|
||||
?: throw NoStackTraceException("未获取到文件")
|
||||
val file = File(path)
|
||||
if (file.exists()) {
|
||||
return@runCatching ParcelFileDescriptor.open(
|
||||
file,
|
||||
ParcelFileDescriptor.MODE_READ_ONLY
|
||||
)
|
||||
} else {
|
||||
throw NoStackTraceException("文件不存在")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
AppLog.put("读取inputStream失败:${e.localizedMessage}", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Uri.toWritePfd(context: Context): Result<ParcelFileDescriptor> {
|
||||
val uri = this
|
||||
return kotlin.runCatching {
|
||||
try {
|
||||
if (isContentScheme()) {
|
||||
DocumentFile.fromSingleUri(context, uri)
|
||||
?: throw NoStackTraceException("未获取到文件")
|
||||
return@runCatching context.contentResolver.openFileDescriptor(uri, "w")!!
|
||||
} else {
|
||||
val path = RealPathUtil.getPath(context, uri)
|
||||
?: throw NoStackTraceException("未获取到文件")
|
||||
val file = File(path)
|
||||
if (file.exists()) {
|
||||
return@runCatching ParcelFileDescriptor.open(
|
||||
file,
|
||||
ParcelFileDescriptor.MODE_WRITE_ONLY
|
||||
)
|
||||
} else {
|
||||
throw NoStackTraceException("文件不存在")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
AppLog.put("读取inputStream失败:${e.localizedMessage}", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Uri.toRequestBody(contentType: MediaType? = null): RequestBody {
|
||||
val uri = this
|
||||
return object : RequestBody() {
|
||||
override fun contentType() = contentType
|
||||
|
||||
override fun contentLength(): Long {
|
||||
val length = uri.inputStream(appCtx).getOrThrow().available().toLong()
|
||||
return if (length > 0) length else -1
|
||||
}
|
||||
|
||||
override fun writeTo(sink: BufferedSink) {
|
||||
uri.inputStream(appCtx).getOrThrow().source().use { source ->
|
||||
sink.writeAll(source)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Uri.canRead(): Boolean {
|
||||
return appCtx.checkSelfUriPermission(
|
||||
this,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import io.legato.kazusa.BuildConfig
|
||||
import io.legato.kazusa.constant.AppLog
|
||||
import io.legato.kazusa.constant.AppPattern.semicolonRegex
|
||||
import io.legato.kazusa.help.config.AppConfig
|
||||
import io.legato.kazusa.model.analyzeRule.AnalyzeUrl
|
||||
import io.legato.kazusa.model.analyzeRule.CustomUrl
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.net.URLDecoder
|
||||
|
||||
object UrlUtil {
|
||||
|
||||
// 有时候文件名在query里,截取path会截到其他内容
|
||||
// https://www.example.com/download.php?filename=文件.txt
|
||||
// https://www.example.com/txt/文件.txt?token=123456
|
||||
private val unExpectFileSuffixs = arrayOf(
|
||||
"php", "html"
|
||||
)
|
||||
|
||||
fun replaceReservedChar(text: String): String {
|
||||
return text.replace("%", "%25")
|
||||
.replace(" ", "%20")
|
||||
.replace("\"", "%22")
|
||||
.replace("#", "%23")
|
||||
.replace("&", "%26")
|
||||
.replace("(", "%28")
|
||||
.replace(")", "%29")
|
||||
.replace("+", "%2B")
|
||||
.replace(",", "%2C")
|
||||
.replace("/", "%2F")
|
||||
.replace(":", "%3A")
|
||||
.replace(";", "%3B")
|
||||
.replace("<", "%3C")
|
||||
.replace("=", "%3D")
|
||||
.replace(">", "%3E")
|
||||
.replace("?", "%3F")
|
||||
.replace("@", "%40")
|
||||
.replace("\\", "%5C")
|
||||
.replace("|", "%7C")
|
||||
}
|
||||
|
||||
|
||||
/* 阅读定义的url,{urlOption} */
|
||||
fun getFileName(analyzeUrl: AnalyzeUrl): String? {
|
||||
return getFileName(analyzeUrl.url, analyzeUrl.headerMap)
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据网络url获取文件信息 文件名
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
fun getFileName(fileUrl: String, headerMap: Map<String, String>? = null): String? {
|
||||
return kotlin.runCatching {
|
||||
val url = URL(fileUrl)
|
||||
var fileName: String? = getFileNameFromPath(url)
|
||||
if (fileName == null) {
|
||||
fileName = getFileNameFromResponseHeader(url, headerMap)
|
||||
}
|
||||
fileName
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
private fun getFileNameFromResponseHeader(
|
||||
url: URL,
|
||||
headerMap: Map<String, String>? = null
|
||||
): String? {
|
||||
// HEAD方式获取链接响应头信息
|
||||
val conn: HttpURLConnection = url.openConnection() as HttpURLConnection
|
||||
conn.requestMethod = "HEAD"
|
||||
// 下载链接可能还需要header才能成功访问
|
||||
headerMap?.forEach { (key, value) ->
|
||||
conn.setRequestProperty(key, value)
|
||||
}
|
||||
// 禁止重定向 否则获取不到响应头返回的Location
|
||||
conn.instanceFollowRedirects = false
|
||||
conn.connect()
|
||||
|
||||
if (AppConfig.recordLog || BuildConfig.DEBUG) {
|
||||
val headers = conn.headerFields
|
||||
val headersString = buildString {
|
||||
headers.forEach { (key, value) ->
|
||||
value.forEach {
|
||||
append(key)
|
||||
append(": ")
|
||||
append(it)
|
||||
append("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
AppLog.put("$url response header:\n$headersString")
|
||||
}
|
||||
|
||||
// val fileSize = conn.getContentLengthLong() / 1024
|
||||
/** Content-Disposition 存在三种情况 文件名应该用引号 有些用空格
|
||||
* filename="filename"
|
||||
* filename*="charset''filename"
|
||||
*/
|
||||
val raw: String? = conn.getHeaderField("Content-Disposition")
|
||||
// Location跳转到实际链接
|
||||
val redirectUrl: String? = conn.getHeaderField("Location")
|
||||
|
||||
return if (raw != null) {
|
||||
val fileNames = raw.split(semicolonRegex).filter { it.contains("filename") }
|
||||
val names = hashSetOf<String>()
|
||||
fileNames.forEach {
|
||||
val fileName = it.substringAfter("=")
|
||||
.trim()
|
||||
.replace("^\"".toRegex(), "")
|
||||
.replace("\"$".toRegex(), "")
|
||||
if (it.contains("filename*")) {
|
||||
val data = fileName.split("''")
|
||||
names.add(URLDecoder.decode(data[1], data[0]))
|
||||
} else {
|
||||
names.add(fileName)
|
||||
/* 好像不用这样
|
||||
names.add(
|
||||
String(
|
||||
fileName.toByteArray(StandardCharsets.ISO_8859_1),
|
||||
StandardCharsets.UTF_8
|
||||
)
|
||||
)
|
||||
*/
|
||||
}
|
||||
}
|
||||
names.firstOrNull()
|
||||
} else if (redirectUrl != null) {
|
||||
val newUrl= URL(URLDecoder.decode(redirectUrl, "UTF-8"))
|
||||
getFileNameFromPath(newUrl)
|
||||
} else {
|
||||
AppLog.put("Cannot obtain URL file name, enable recordLog for response header")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFileNameFromPath(fileUrl: URL): String? {
|
||||
val path = fileUrl.path ?: return null
|
||||
val suffix = getSuffix(path, "")
|
||||
return if (
|
||||
suffix != "" && !unExpectFileSuffixs.contains(suffix)
|
||||
) {
|
||||
path.substringAfterLast("/")
|
||||
} else {
|
||||
AppLog.put("getFileNameFromPath: Unexpected file suffix: $suffix")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private val fileSuffixRegex = Regex("^[a-z\\d]+$", RegexOption.IGNORE_CASE)
|
||||
|
||||
/* 获取合法的文件后缀 */
|
||||
fun getSuffix(str: String, default: String? = null): String {
|
||||
val suffix = CustomUrl(str).getUrl()
|
||||
.substringAfterLast("/")
|
||||
.substringBefore("?")
|
||||
.substringBefore("#")
|
||||
.substringAfterLast(".", "")
|
||||
//检查截取的后缀字符是否合法 [a-zA-Z0-9]
|
||||
return if (suffix.length > 5 || !suffix.matches(fileSuffixRegex)) {
|
||||
if (default == null) {
|
||||
AppLog.put("Cannot find legal suffix:\n target: $str\n suffix: $suffix")
|
||||
}
|
||||
default ?: "ext"
|
||||
} else {
|
||||
suffix
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
@Suppress("unused")
|
||||
object Utf8BomUtils {
|
||||
private val UTF8_BOM_BYTES = byteArrayOf(0xEF.toByte(), 0xBB.toByte(), 0xBF.toByte())
|
||||
|
||||
fun removeUTF8BOM(xmlText: String): String {
|
||||
val bytes = xmlText.toByteArray()
|
||||
val containsBOM = (bytes.size > 3
|
||||
&& bytes[0] == UTF8_BOM_BYTES[0]
|
||||
&& bytes[1] == UTF8_BOM_BYTES[1]
|
||||
&& bytes[2] == UTF8_BOM_BYTES[2])
|
||||
if (containsBOM) {
|
||||
return String(bytes, 3, bytes.size - 3)
|
||||
}
|
||||
return xmlText
|
||||
}
|
||||
|
||||
fun removeUTF8BOM(bytes: ByteArray): ByteArray {
|
||||
val containsBOM = (bytes.size > 3
|
||||
&& bytes[0] == UTF8_BOM_BYTES[0]
|
||||
&& bytes[1] == UTF8_BOM_BYTES[1]
|
||||
&& bytes[2] == UTF8_BOM_BYTES[2])
|
||||
if (containsBOM) {
|
||||
val copy = ByteArray(bytes.size - 3)
|
||||
System.arraycopy(bytes, 3, copy, 0, bytes.size - 3)
|
||||
return copy
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
fun hasBom(bytes: ByteArray): Boolean {
|
||||
return (bytes.size > 3
|
||||
&& bytes[0] == UTF8_BOM_BYTES[0]
|
||||
&& bytes[1] == UTF8_BOM_BYTES[1]
|
||||
&& bytes[2] == UTF8_BOM_BYTES[2])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Picture
|
||||
import android.os.Build
|
||||
import android.text.Html
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.View.GONE
|
||||
import android.view.View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS
|
||||
import android.view.View.INVISIBLE
|
||||
import android.view.View.VISIBLE
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.EdgeEffect
|
||||
import android.widget.EditText
|
||||
import android.widget.RadioGroup
|
||||
import android.widget.SeekBar
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.menu.MenuPopupHelper
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.graphics.record
|
||||
import androidx.core.graphics.withTranslation
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.get
|
||||
import androidx.core.view.marginBottom
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager.widget.ViewPager
|
||||
import io.legato.kazusa.help.config.AppConfig
|
||||
import io.legato.kazusa.lib.theme.TintHelper
|
||||
import io.legato.kazusa.utils.canvasrecorder.CanvasRecorder
|
||||
import io.legato.kazusa.utils.canvasrecorder.record
|
||||
import splitties.systemservices.inputMethodManager
|
||||
import splitties.views.bottomPadding
|
||||
import splitties.views.topPadding
|
||||
import java.lang.reflect.Field
|
||||
|
||||
|
||||
private tailrec fun getCompatActivity(context: Context?): AppCompatActivity? {
|
||||
return when (context) {
|
||||
is AppCompatActivity -> context
|
||||
is androidx.appcompat.view.ContextThemeWrapper -> getCompatActivity(context.baseContext)
|
||||
is android.view.ContextThemeWrapper -> getCompatActivity(context.baseContext)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
val View.activity: AppCompatActivity?
|
||||
get() = getCompatActivity(context)
|
||||
|
||||
fun View.hideSoftInput() = run {
|
||||
inputMethodManager.hideSoftInputFromWindow(this.windowToken, 0)
|
||||
}
|
||||
|
||||
fun EditText.showSoftInput() = run {
|
||||
requestFocus()
|
||||
inputMethodManager.showSoftInput(this, InputMethodManager.RESULT_SHOWN)
|
||||
}
|
||||
|
||||
fun View.disableAutoFill() = run {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
this.importantForAutofill = IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS
|
||||
}
|
||||
}
|
||||
|
||||
fun View.applyTint(
|
||||
@ColorInt color: Int,
|
||||
isDark: Boolean = AppConfig.isNightTheme
|
||||
) {
|
||||
TintHelper.setTintAuto(this, color, false, isDark)
|
||||
}
|
||||
|
||||
fun View.applyBackgroundTint(
|
||||
@ColorInt color: Int,
|
||||
isDark: Boolean = AppConfig.isNightTheme
|
||||
) {
|
||||
if (background == null) {
|
||||
setBackgroundColor(color)
|
||||
} else {
|
||||
TintHelper.setTintAuto(this, color, true, isDark)
|
||||
}
|
||||
}
|
||||
|
||||
fun RecyclerView.setEdgeEffectColor(@ColorInt color: Int) {
|
||||
edgeEffectFactory = object : RecyclerView.EdgeEffectFactory() {
|
||||
override fun createEdgeEffect(view: RecyclerView, direction: Int): EdgeEffect {
|
||||
val edgeEffect = super.createEdgeEffect(view, direction)
|
||||
edgeEffect.color = color
|
||||
return edgeEffect
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ViewPager.setEdgeEffectColor(@ColorInt color: Int) {
|
||||
try {
|
||||
val clazz = ViewPager::class.java
|
||||
for (name in arrayOf("mLeftEdge", "mRightEdge")) {
|
||||
val field = clazz.getDeclaredField(name)
|
||||
field.isAccessible = true
|
||||
val edge = field.get(this)
|
||||
(edge as EdgeEffect).color = color
|
||||
}
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
fun EditText.disableEdit() {
|
||||
keyListener = null
|
||||
}
|
||||
|
||||
fun View.gone() {
|
||||
if (visibility != GONE) {
|
||||
visibility = GONE
|
||||
}
|
||||
}
|
||||
|
||||
fun View.gone(gone: Boolean) {
|
||||
if (gone) {
|
||||
gone()
|
||||
} else {
|
||||
visibility = VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
fun View.invisible() {
|
||||
if (visibility != INVISIBLE) {
|
||||
visibility = INVISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
fun View.visible() {
|
||||
if (visibility != VISIBLE) {
|
||||
visibility = VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
fun View.visible(visible: Boolean) {
|
||||
if (visible && visibility != VISIBLE) {
|
||||
visibility = VISIBLE
|
||||
} else if (!visible && visibility == VISIBLE) {
|
||||
visibility = INVISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
fun View.screenshot(bitmap: Bitmap? = null, canvas: Canvas? = null): Bitmap? {
|
||||
return if (width > 0 && height > 0) {
|
||||
val screenshot = if (bitmap != null && bitmap.width == width && bitmap.height == height) {
|
||||
bitmap.eraseColor(Color.TRANSPARENT)
|
||||
bitmap
|
||||
} else {
|
||||
bitmap?.recycle()
|
||||
Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
}
|
||||
val c = canvas ?: Canvas()
|
||||
c.setBitmap(screenshot)
|
||||
c.save()
|
||||
c.translate(-scrollX.toFloat(), -scrollY.toFloat())
|
||||
this.draw(c)
|
||||
c.restore()
|
||||
c.setBitmap(null)
|
||||
screenshot.prepareToDraw()
|
||||
screenshot
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun View.screenshot(picture: Picture) {
|
||||
if (width > 0 && height > 0) {
|
||||
picture.record(width, height) {
|
||||
withTranslation(-scrollX.toFloat(), -scrollY.toFloat()) {
|
||||
draw(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun View.screenshot(canvasRecorder: CanvasRecorder) {
|
||||
if (width > 0 && height > 0) {
|
||||
canvasRecorder.record(width, height) {
|
||||
draw(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun View.setPaddingBottom(bottom: Int) {
|
||||
setPadding(paddingLeft, paddingTop, paddingRight, bottom)
|
||||
}
|
||||
|
||||
fun SeekBar.progressAdd(int: Int) {
|
||||
progress += int
|
||||
}
|
||||
|
||||
fun RadioGroup.getIndexById(id: Int): Int {
|
||||
for (i in 0 until this.childCount) {
|
||||
if (id == get(i).id) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
fun RadioGroup.getCheckedIndex(): Int {
|
||||
for (i in 0 until this.childCount) {
|
||||
if (checkedRadioButtonId == get(i).id) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
fun RadioGroup.checkByIndex(index: Int) {
|
||||
check(get(index).id)
|
||||
}
|
||||
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
fun TextView.setHtml(html: String) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
text = Html.fromHtml(html, Html.FROM_HTML_MODE_COMPACT)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
text = Html.fromHtml(html)
|
||||
}
|
||||
}
|
||||
|
||||
fun TextView.setTextIfNotEqual(charSequence: CharSequence?) {
|
||||
if (text != charSequence) {
|
||||
text = charSequence
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
fun PopupMenu.show(x: Int, y: Int) {
|
||||
kotlin.runCatching {
|
||||
val field: Field = this.javaClass.getDeclaredField("mPopup")
|
||||
field.isAccessible = true
|
||||
(field.get(this) as MenuPopupHelper).show(x, y)
|
||||
}.onFailure {
|
||||
it.printOnDebug()
|
||||
}
|
||||
}
|
||||
|
||||
fun View.shouldHideSoftInput(event: MotionEvent): Boolean {
|
||||
if (this is EditText) {
|
||||
val l = intArrayOf(0, 0)
|
||||
getLocationInWindow(l)
|
||||
val left = l[0]
|
||||
val top = l[1]
|
||||
val bottom = top + getHeight()
|
||||
val right = left + getWidth()
|
||||
return !(event.x > left && event.x < right && event.y > top && event.y < bottom)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun View.applyStatusBarPadding(withInitialPadding: Boolean = false) {
|
||||
val initialPadding = if (withInitialPadding) topPadding else 0
|
||||
setOnApplyWindowInsetsListenerCompat { _, windowInsets ->
|
||||
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars())
|
||||
topPadding = initialPadding + insets.top
|
||||
windowInsets
|
||||
}
|
||||
}
|
||||
|
||||
fun View.applyNavigationBarPadding(withInitialPadding: Boolean = false) {
|
||||
val initialPadding = if (withInitialPadding) bottomPadding else 0
|
||||
setOnApplyWindowInsetsListenerCompat { _, windowInsets ->
|
||||
bottomPadding = initialPadding + windowInsets.navigationBarHeight
|
||||
windowInsets
|
||||
}
|
||||
}
|
||||
|
||||
fun View.applyNavigationBarMargin(withInitialMargin: Boolean = false) {
|
||||
val initialMargin = if (withInitialMargin) marginBottom else 0
|
||||
setOnApplyWindowInsetsListenerCompat { _, windowInsets ->
|
||||
updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = initialMargin + windowInsets.navigationBarHeight
|
||||
}
|
||||
windowInsets
|
||||
}
|
||||
}
|
||||
|
||||
fun View.setBackgroundKeepPadding(@DrawableRes backgroundResId: Int) {
|
||||
val paddingLeft = paddingLeft
|
||||
val paddingTop = paddingTop
|
||||
val paddingRight = paddingRight
|
||||
val paddingBottom = paddingBottom
|
||||
setBackgroundResource(backgroundResId)
|
||||
setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom)
|
||||
}
|
||||
|
||||
fun View.canScroll(direction: Int): Boolean {
|
||||
return canScrollVertically(direction) || canScrollHorizontally(direction)
|
||||
}
|
||||
|
||||
private val requestLayoutBroken = Build.VERSION.SDK_INT <= Build.VERSION_CODES.M
|
||||
|| Build.VERSION.SDK_INT in Build.VERSION_CODES.O..Build.VERSION_CODES.Q
|
||||
|
||||
fun View.setOnApplyWindowInsetsListenerCompat(listener: (View, WindowInsetsCompat) -> WindowInsetsCompat) {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets ->
|
||||
val windowInsets = listener(view, insets)
|
||||
if (requestLayoutBroken && isLayoutRequested) {
|
||||
post {
|
||||
requestLayout()
|
||||
}
|
||||
}
|
||||
windowInsets
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import android.webkit.WebSettings
|
||||
import androidx.webkit.WebSettingsCompat
|
||||
import androidx.webkit.WebViewFeature
|
||||
import io.legato.kazusa.help.config.AppConfig
|
||||
|
||||
/**
|
||||
* 设置是否夜间模式
|
||||
*/
|
||||
@SuppressLint("RequiresFeature")
|
||||
fun WebSettings.setDarkeningAllowed(allow: Boolean) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
kotlin.runCatching {
|
||||
WebSettingsCompat.setAlgorithmicDarkeningAllowed(this, allow)
|
||||
return
|
||||
}.onFailure {
|
||||
it.printOnDebug()
|
||||
}
|
||||
}
|
||||
if (AppConfig.isNightTheme) {
|
||||
if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY)) {
|
||||
@Suppress("DEPRECATION")
|
||||
WebSettingsCompat.setForceDarkStrategy(
|
||||
this,
|
||||
WebSettingsCompat.DARK_STRATEGY_PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING
|
||||
)
|
||||
}
|
||||
if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
|
||||
@Suppress("DEPRECATION")
|
||||
WebSettingsCompat.setForceDark(
|
||||
this,
|
||||
WebSettingsCompat.FORCE_DARK_ON
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package io.legato.kazusa.utils
|
||||
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
|
||||
val WindowInsetsCompat.navigationBarHeight
|
||||
get() = (getInsets(WindowInsetsCompat.Type.systemBars()).bottom - imeHeight).coerceAtLeast(0)
|
||||
|
||||
val WindowInsetsCompat.imeHeight
|
||||
get() = getInsets(WindowInsetsCompat.Type.ime()).bottom
|
||||
@@ -0,0 +1,36 @@
|
||||
package io.legato.kazusa.utils.canvasrecorder
|
||||
|
||||
import androidx.annotation.CallSuper
|
||||
|
||||
abstract class BaseCanvasRecorder : CanvasRecorder {
|
||||
|
||||
@JvmField
|
||||
protected var isDirty = true
|
||||
|
||||
override fun invalidate() {
|
||||
isDirty = true
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun recycle() {
|
||||
isDirty = true
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun endRecording() {
|
||||
isDirty = false
|
||||
}
|
||||
|
||||
override fun isDirty(): Boolean {
|
||||
return isDirty
|
||||
}
|
||||
|
||||
override fun isLocked(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun needRecord(): Boolean {
|
||||
return isDirty() && !isLocked()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package io.legato.kazusa.utils.canvasrecorder
|
||||
|
||||
import android.graphics.Canvas
|
||||
|
||||
interface CanvasRecorder {
|
||||
|
||||
val width: Int
|
||||
|
||||
val height: Int
|
||||
|
||||
fun beginRecording(width: Int, height: Int): Canvas
|
||||
|
||||
fun endRecording()
|
||||
|
||||
fun draw(canvas: Canvas)
|
||||
|
||||
fun invalidate()
|
||||
|
||||
fun recycle()
|
||||
|
||||
fun isDirty(): Boolean
|
||||
|
||||
fun isLocked(): Boolean
|
||||
|
||||
fun needRecord(): Boolean
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package io.legato.kazusa.utils.canvasrecorder
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Picture
|
||||
import io.legato.kazusa.utils.canvasrecorder.pools.PicturePool
|
||||
import io.legato.kazusa.utils.objectpool.synchronized
|
||||
|
||||
class CanvasRecorderApi23Impl : BaseCanvasRecorder() {
|
||||
|
||||
private var picture: Picture? = null
|
||||
|
||||
override val width get() = picture?.width ?: -1
|
||||
override val height get() = picture?.height ?: -1
|
||||
|
||||
private fun initPicture() {
|
||||
if (picture == null) {
|
||||
picture = picturePool.obtain()
|
||||
}
|
||||
}
|
||||
|
||||
override fun beginRecording(width: Int, height: Int): Canvas {
|
||||
initPicture()
|
||||
return picture!!.beginRecording(width, height)
|
||||
}
|
||||
|
||||
override fun endRecording() {
|
||||
picture!!.endRecording()
|
||||
super.endRecording()
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
if (picture == null) return
|
||||
canvas.drawPicture(picture!!)
|
||||
}
|
||||
|
||||
override fun recycle() {
|
||||
super.recycle()
|
||||
if (picture == null) return
|
||||
picturePool.recycle(picture!!)
|
||||
picture = null
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val picturePool = PicturePool().synchronized()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package io.legato.kazusa.utils.canvasrecorder
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Picture
|
||||
import android.graphics.RenderNode
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import io.legato.kazusa.utils.objectpool.synchronized
|
||||
import io.legato.kazusa.utils.canvasrecorder.pools.PicturePool
|
||||
import io.legato.kazusa.utils.canvasrecorder.pools.RenderNodePool
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
class CanvasRecorderApi29Impl : BaseCanvasRecorder() {
|
||||
|
||||
private var renderNode: RenderNode? = null
|
||||
private var picture: Picture? = null
|
||||
|
||||
override val width get() = renderNode?.width ?: -1
|
||||
override val height get() = renderNode?.height ?: -1
|
||||
|
||||
private fun init() {
|
||||
if (renderNode == null) {
|
||||
renderNode = renderNodePool.obtain()
|
||||
}
|
||||
if (picture == null) {
|
||||
picture = picturePool.obtain()
|
||||
}
|
||||
}
|
||||
|
||||
private fun flushRenderNode() {
|
||||
val rc = renderNode!!.beginRecording()
|
||||
rc.drawPicture(picture!!)
|
||||
renderNode!!.endRecording()
|
||||
}
|
||||
|
||||
override fun beginRecording(width: Int, height: Int): Canvas {
|
||||
init()
|
||||
renderNode!!.setPosition(0, 0, width, height)
|
||||
return picture!!.beginRecording(width, height)
|
||||
}
|
||||
|
||||
override fun endRecording() {
|
||||
picture!!.endRecording()
|
||||
flushRenderNode()
|
||||
super.endRecording()
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
if (renderNode == null || picture == null) {
|
||||
return
|
||||
}
|
||||
if (canvas.isHardwareAccelerated) {
|
||||
if (!renderNode!!.hasDisplayList()) {
|
||||
flushRenderNode()
|
||||
}
|
||||
canvas.drawRenderNode(renderNode!!)
|
||||
} else {
|
||||
canvas.drawPicture(picture!!)
|
||||
}
|
||||
}
|
||||
|
||||
override fun recycle() {
|
||||
super.recycle()
|
||||
if (renderNode == null || picture == null) return
|
||||
renderNodePool.recycle(renderNode!!)
|
||||
renderNode = null
|
||||
picturePool.recycle(picture!!)
|
||||
picture = null
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val picturePool = PicturePool().synchronized()
|
||||
private val renderNodePool = RenderNodePool().synchronized()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package io.legato.kazusa.utils.canvasrecorder
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.view.View
|
||||
import androidx.core.graphics.withSave
|
||||
|
||||
inline fun CanvasRecorder.recordIfNeeded(
|
||||
width: Int,
|
||||
height: Int,
|
||||
block: Canvas.() -> Unit
|
||||
): Boolean {
|
||||
if (!needRecord()) return false
|
||||
record(width, height, block)
|
||||
return true
|
||||
}
|
||||
|
||||
fun CanvasRecorder.recordIfNeeded(view: View): Boolean {
|
||||
if (!needRecord()) return false
|
||||
record(view.width, view.height) {
|
||||
view.draw(this)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
inline fun CanvasRecorder.record(width: Int, height: Int, block: Canvas.() -> Unit) {
|
||||
val canvas = beginRecording(width, height)
|
||||
try {
|
||||
canvas.withSave {
|
||||
block()
|
||||
}
|
||||
} finally {
|
||||
endRecording()
|
||||
}
|
||||
}
|
||||
|
||||
inline fun CanvasRecorder.recordIfNeededThenDraw(
|
||||
canvas: Canvas,
|
||||
width: Int,
|
||||
height: Int,
|
||||
block: Canvas.() -> Unit
|
||||
) {
|
||||
recordIfNeeded(width, height, block)
|
||||
draw(canvas)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package io.legato.kazusa.utils.canvasrecorder
|
||||
|
||||
import android.os.Build
|
||||
import io.legato.kazusa.help.config.AppConfig
|
||||
|
||||
object CanvasRecorderFactory {
|
||||
|
||||
private val atLeastApi24 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
|
||||
private val atLeastApi29 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
||||
val isSupport = atLeastApi24
|
||||
|
||||
// issue 3868
|
||||
fun create(locked: Boolean = false): CanvasRecorder {
|
||||
val impl = when {
|
||||
!AppConfig.optimizeRender -> CanvasRecorderImpl()
|
||||
atLeastApi29 -> CanvasRecorderApi29Impl()
|
||||
atLeastApi24 -> CanvasRecorderApi23Impl()
|
||||
else -> CanvasRecorderImpl()
|
||||
}
|
||||
return if (locked) {
|
||||
CanvasRecorderLocked(impl)
|
||||
} else {
|
||||
impl
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package io.legato.kazusa.utils.canvasrecorder
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import com.bumptech.glide.Glide
|
||||
import io.legato.kazusa.utils.canvasrecorder.pools.CanvasPool
|
||||
import splitties.init.appCtx
|
||||
|
||||
class CanvasRecorderImpl : BaseCanvasRecorder() {
|
||||
|
||||
var bitmap: Bitmap? = null
|
||||
var canvas: Canvas? = null
|
||||
|
||||
override val width get() = bitmap?.width ?: -1
|
||||
override val height get() = bitmap?.height ?: -1
|
||||
|
||||
private fun init(width: Int, height: Int) {
|
||||
if (width <= 0 || height <= 0) {
|
||||
return
|
||||
}
|
||||
if (bitmap == null) {
|
||||
bitmap = bitmapPool.get(width, height, Bitmap.Config.ARGB_8888)
|
||||
}
|
||||
if (bitmap!!.width != width || bitmap!!.height != height) {
|
||||
if (bitmap!!.isMutable && canReconfigure(width, height)) {
|
||||
bitmap!!.reconfigure(width, height, Bitmap.Config.ARGB_8888)
|
||||
} else {
|
||||
bitmapPool.put(bitmap!!)
|
||||
bitmap = bitmapPool.get(width, height, Bitmap.Config.ARGB_8888)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun canReconfigure(width: Int, height: Int): Boolean {
|
||||
return bitmap!!.allocationByteCount >= width * height * 4
|
||||
}
|
||||
|
||||
override fun beginRecording(width: Int, height: Int): Canvas {
|
||||
init(width, height)
|
||||
bitmap?.eraseColor(Color.TRANSPARENT)
|
||||
canvas = canvasPool.obtain().apply { setBitmap(bitmap) }
|
||||
return canvas!!
|
||||
}
|
||||
|
||||
override fun endRecording() {
|
||||
bitmap?.prepareToDraw()
|
||||
super.endRecording()
|
||||
canvasPool.recycle(canvas!!)
|
||||
canvas = null
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
if (bitmap == null) return
|
||||
canvas.drawBitmap(bitmap!!, 0f, 0f, null)
|
||||
}
|
||||
|
||||
override fun recycle() {
|
||||
super.recycle()
|
||||
val bitmap = bitmap ?: return
|
||||
bitmapPool.put(bitmap)
|
||||
this.bitmap = null
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val canvasPool = CanvasPool(2)
|
||||
private val bitmapPool = Glide.get(appCtx).bitmapPool
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package io.legato.kazusa.utils.canvasrecorder
|
||||
|
||||
import android.graphics.Canvas
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
|
||||
class CanvasRecorderLocked(private val delegate: CanvasRecorder) :
|
||||
CanvasRecorder by delegate {
|
||||
|
||||
var lock: ReentrantLock? = ReentrantLock()
|
||||
|
||||
private fun initLock() {
|
||||
if (lock == null) {
|
||||
synchronized(this) {
|
||||
if (lock == null) {
|
||||
lock = ReentrantLock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun beginRecording(width: Int, height: Int): Canvas {
|
||||
initLock()
|
||||
lock!!.lock()
|
||||
return delegate.beginRecording(width, height)
|
||||
}
|
||||
|
||||
override fun endRecording() {
|
||||
delegate.endRecording()
|
||||
lock!!.unlock()
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
if (lock == null) {
|
||||
return
|
||||
}
|
||||
lock!!.lock()
|
||||
try {
|
||||
delegate.draw(canvas)
|
||||
} finally {
|
||||
lock!!.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
override fun isLocked(): Boolean {
|
||||
if (lock == null) {
|
||||
return false
|
||||
}
|
||||
return lock!!.isLocked
|
||||
}
|
||||
|
||||
override fun recycle() {
|
||||
if (lock == null) {
|
||||
return
|
||||
}
|
||||
lock!!.lock()
|
||||
try {
|
||||
delegate.recycle()
|
||||
} finally {
|
||||
lock!!.unlock()
|
||||
}
|
||||
lock = null
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package io.legato.kazusa.utils.canvasrecorder.pools
|
||||
|
||||
import android.graphics.Canvas
|
||||
import androidx.core.util.Pools
|
||||
|
||||
class CanvasPool(size: Int) {
|
||||
|
||||
private val pool = Pools.SynchronizedPool<Canvas>(size)
|
||||
|
||||
fun obtain(): Canvas {
|
||||
val canvas = pool.acquire() ?: Canvas()
|
||||
return canvas
|
||||
}
|
||||
|
||||
fun recycle(canvas: Canvas) {
|
||||
canvas.setBitmap(null)
|
||||
canvas.restoreToCount(1)
|
||||
pool.release(canvas)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package io.legato.kazusa.utils.canvasrecorder.pools
|
||||
|
||||
import android.graphics.Picture
|
||||
import io.legato.kazusa.utils.objectpool.BaseObjectPool
|
||||
|
||||
class PicturePool : BaseObjectPool<Picture>(64) {
|
||||
|
||||
override fun create(): Picture = Picture()
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package io.legato.kazusa.utils.canvasrecorder.pools
|
||||
|
||||
import android.graphics.RenderNode
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import io.legato.kazusa.utils.objectpool.BaseObjectPool
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
class RenderNodePool : BaseObjectPool<RenderNode>(64) {
|
||||
|
||||
override fun recycle(target: RenderNode) {
|
||||
target.discardDisplayList()
|
||||
super.recycle(target)
|
||||
}
|
||||
|
||||
override fun create(): RenderNode = RenderNode("CanvasRecorder")
|
||||
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
package io.legato.kazusa.utils.compress
|
||||
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.system.ErrnoException
|
||||
import android.system.Os
|
||||
import android.system.OsConstants
|
||||
import android.system.OsConstants.S_ISDIR
|
||||
import io.legato.kazusa.lib.icu4j.CharsetDetector
|
||||
import me.zhanghai.android.libarchive.Archive
|
||||
import me.zhanghai.android.libarchive.ArchiveEntry
|
||||
import me.zhanghai.android.libarchive.ArchiveException
|
||||
import okio.Buffer
|
||||
import java.io.File
|
||||
import java.io.FileDescriptor
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.InterruptedIOException
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.SeekableByteChannel
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
|
||||
object LibArchiveUtils {
|
||||
|
||||
@Throws(ArchiveException::class)
|
||||
fun openArchive(
|
||||
inputStream: InputStream,
|
||||
): Long {
|
||||
val archive: Long = Archive.readNew()
|
||||
var successful = false
|
||||
try {
|
||||
Archive.setCharset(archive, StandardCharsets.UTF_8.name().toByteArray())
|
||||
Archive.readSupportFilterAll(archive)
|
||||
Archive.readSupportFormatAll(archive)
|
||||
Archive.readSetCallbackData(archive, null)
|
||||
val buffer = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE)
|
||||
Archive.readSetReadCallback<Any?>(archive) { _, _ ->
|
||||
buffer.clear()
|
||||
val bytesRead = try {
|
||||
inputStream.read(buffer.array())
|
||||
} catch (e: IOException) {
|
||||
throw ArchiveException(Archive.ERRNO_FATAL, "InputStream.read", e)
|
||||
}
|
||||
if (bytesRead != -1) {
|
||||
buffer.limit(bytesRead)
|
||||
buffer
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
Archive.readSetSkipCallback<Any?>(archive) { _, _, request ->
|
||||
try {
|
||||
inputStream.skip(request)
|
||||
} catch (e: IOException) {
|
||||
throw ArchiveException(Archive.ERRNO_FATAL, "InputStream.skip", e)
|
||||
}
|
||||
}
|
||||
Archive.readOpen1(archive)
|
||||
successful = true
|
||||
return archive
|
||||
} finally {
|
||||
if (!successful) {
|
||||
Archive.free(archive)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Throws(ArchiveException::class)
|
||||
private fun openArchive(
|
||||
channel: SeekableByteChannel,
|
||||
): Long {
|
||||
val archive: Long = Archive.readNew()
|
||||
var successful = false
|
||||
try {
|
||||
Archive.setCharset(archive, StandardCharsets.UTF_8.name().toByteArray())
|
||||
Archive.readSupportFilterAll(archive)
|
||||
Archive.readSupportFormatAll(archive)
|
||||
Archive.readSetCallbackData(archive, null)
|
||||
val buffer = ByteBuffer.allocateDirect(DEFAULT_BUFFER_SIZE)
|
||||
Archive.readSetReadCallback<Any?>(archive) { _, _ ->
|
||||
buffer.clear()
|
||||
val bytesRead = try {
|
||||
channel.read(buffer)
|
||||
} catch (e: IOException) {
|
||||
throw ArchiveException(Archive.ERRNO_FATAL, "SeekableByteChannel.read", e)
|
||||
}
|
||||
if (bytesRead != -1) {
|
||||
buffer.flip()
|
||||
buffer
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
Archive.readSetSkipCallback<Any?>(archive) { _, _, request ->
|
||||
try {
|
||||
channel.position(channel.position() + request)
|
||||
} catch (e: IOException) {
|
||||
throw ArchiveException(Archive.ERRNO_FATAL, "SeekableByteChannel.position", e)
|
||||
}
|
||||
request
|
||||
}
|
||||
Archive.readSetSeekCallback<Any?>(archive) { _, _, offset, whence ->
|
||||
val newPosition: Long
|
||||
try {
|
||||
newPosition = when (whence) {
|
||||
OsConstants.SEEK_SET -> offset
|
||||
OsConstants.SEEK_CUR -> channel.position() + offset
|
||||
OsConstants.SEEK_END -> channel.size() + offset
|
||||
else -> throw ArchiveException(
|
||||
Archive.ERRNO_FATAL,
|
||||
"Unknown whence $whence"
|
||||
)
|
||||
}
|
||||
channel.position(newPosition)
|
||||
} catch (e: IOException) {
|
||||
throw ArchiveException(Archive.ERRNO_FATAL, "SeekableByteChannel.position", e)
|
||||
}
|
||||
newPosition
|
||||
}
|
||||
Archive.readOpen1(archive)
|
||||
successful = true
|
||||
return archive
|
||||
|
||||
|
||||
} finally {
|
||||
if (!successful) {
|
||||
Archive.free(archive)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Throws(ArchiveException::class)
|
||||
private fun openArchive(
|
||||
pfd: ParcelFileDescriptor,
|
||||
useCb: Boolean = true
|
||||
): Long {
|
||||
val archive: Long = Archive.readNew()
|
||||
var successful = false
|
||||
try {
|
||||
Archive.setCharset(archive, StandardCharsets.UTF_8.name().toByteArray())
|
||||
Archive.readSupportFilterAll(archive)
|
||||
Archive.readSupportFormatAll(archive)
|
||||
if (useCb) {
|
||||
Archive.readSetCallbackData(archive, pfd.fileDescriptor)
|
||||
val buffer = ByteBuffer.allocateDirect(DEFAULT_BUFFER_SIZE)
|
||||
Archive.readSetReadCallback<Any>(
|
||||
archive
|
||||
) { _: Long, fd: Any? ->
|
||||
buffer.clear()
|
||||
try {
|
||||
Os.read(fd as FileDescriptor?, buffer)
|
||||
} catch (e: ErrnoException) {
|
||||
throw ArchiveException(Archive.ERRNO_FATAL, "Os.read", e)
|
||||
} catch (e: InterruptedIOException) {
|
||||
throw ArchiveException(Archive.ERRNO_FATAL, "Os.read", e)
|
||||
}
|
||||
buffer.flip()
|
||||
buffer
|
||||
}
|
||||
Archive.readSetSkipCallback<Any>(
|
||||
archive
|
||||
) { _: Long, fd: Any?, request: Long ->
|
||||
try {
|
||||
Os.lseek(
|
||||
fd as FileDescriptor?, request, OsConstants.SEEK_CUR
|
||||
)
|
||||
} catch (e: ErrnoException) {
|
||||
throw ArchiveException(Archive.ERRNO_FATAL, "Os.lseek", e)
|
||||
}
|
||||
request
|
||||
}
|
||||
Archive.readSetSeekCallback<Any>(
|
||||
archive
|
||||
) { _: Long, fd: Any?, offset: Long, whence: Int ->
|
||||
try {
|
||||
return@readSetSeekCallback Os.lseek(
|
||||
fd as FileDescriptor?, offset, whence
|
||||
)
|
||||
} catch (e: ErrnoException) {
|
||||
throw ArchiveException(Archive.ERRNO_FATAL, "Os.lseek", e)
|
||||
}
|
||||
}
|
||||
Archive.readOpen1(archive)
|
||||
|
||||
} else {
|
||||
Archive.readOpenFd(archive, pfd.fd, DEFAULT_BUFFER_SIZE.toLong())
|
||||
}
|
||||
|
||||
successful = true
|
||||
return archive
|
||||
|
||||
|
||||
} finally {
|
||||
if (!successful) {
|
||||
Archive.free(archive)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 解压文件
|
||||
*/
|
||||
@Throws(NullPointerException::class, SecurityException::class)
|
||||
fun unArchive(
|
||||
pfd: ParcelFileDescriptor,
|
||||
destDir: File,
|
||||
filter: ((String) -> Boolean)? = null
|
||||
): List<File> {
|
||||
return unArchive(openArchive(pfd), destDir, filter)
|
||||
}
|
||||
|
||||
/**
|
||||
* 解压
|
||||
*/
|
||||
@Throws(NullPointerException::class, SecurityException::class)
|
||||
private fun unArchive(
|
||||
archive: Long,
|
||||
destDir: File?,
|
||||
filter: ((String) -> Boolean)? = null
|
||||
): List<File> {
|
||||
destDir ?: throw NullPointerException("解压路径不能为空")
|
||||
val files = arrayListOf<File>()
|
||||
|
||||
|
||||
try {
|
||||
var entry: Long
|
||||
|
||||
while (Archive.readNextHeader(archive).also { entry = it } != 0L) {
|
||||
val entryName =
|
||||
getEntryString(ArchiveEntry.pathnameUtf8(entry), ArchiveEntry.pathname(entry))
|
||||
?: continue
|
||||
val entryFile = File(destDir, entryName)
|
||||
if (!entryFile.canonicalPath.startsWith(destDir.canonicalPath)) {
|
||||
throw SecurityException("压缩文件只能解压到指定路径")
|
||||
}
|
||||
val entryStat = ArchiveEntry.stat(entry)
|
||||
|
||||
//判断是否是文件夹
|
||||
if (entryStat.isDir()) {
|
||||
if (!entryFile.exists()) {
|
||||
entryFile.mkdirs()
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (entryFile.parentFile?.exists() != true) {
|
||||
entryFile.parentFile?.mkdirs()
|
||||
}
|
||||
if (filter != null && !filter.invoke(entryName)) continue
|
||||
if (!entryFile.exists()) {
|
||||
entryFile.createNewFile()
|
||||
entryFile.setReadable(true)
|
||||
entryFile.setExecutable(true)
|
||||
}
|
||||
|
||||
ParcelFileDescriptor.open(entryFile, ParcelFileDescriptor.MODE_WRITE_ONLY).use {
|
||||
Archive.readDataIntoFd(archive, it.fd)
|
||||
files.add(entryFile)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
} finally {
|
||||
Archive.free(archive)
|
||||
}
|
||||
|
||||
return files
|
||||
|
||||
}
|
||||
|
||||
fun getFilesName(pfd: ParcelFileDescriptor, filter: ((String) -> Boolean)?): List<String> {
|
||||
return getFilesName(openArchive(pfd), filter)
|
||||
}
|
||||
|
||||
fun getByteArrayContent(inputStream: InputStream, path: String): ByteArray? {
|
||||
val archive = openArchive(inputStream)
|
||||
try {
|
||||
var entry: Long
|
||||
while (Archive.readNextHeader(archive).also { entry = it } != 0L) {
|
||||
val entryName =
|
||||
getEntryString(ArchiveEntry.pathnameUtf8(entry), ArchiveEntry.pathname(entry))
|
||||
?: continue
|
||||
|
||||
val entryStat = ArchiveEntry.stat(entry)
|
||||
|
||||
//判断是否是文件夹
|
||||
if (entryStat.isDir()) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (entryName == path) {
|
||||
val byteBuffer = ByteBuffer.allocateDirect(DEFAULT_BUFFER_SIZE)
|
||||
val buffer = Buffer()
|
||||
while (true) {
|
||||
Archive.readData(archive, byteBuffer)
|
||||
byteBuffer.flip()
|
||||
if (!byteBuffer.hasRemaining()) {
|
||||
return buffer.readByteArray()
|
||||
}
|
||||
buffer.write(byteBuffer)
|
||||
byteBuffer.clear()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
} finally {
|
||||
Archive.free(archive)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@Throws(SecurityException::class)
|
||||
private fun getFilesName(
|
||||
archive: Long,
|
||||
filter: ((String) -> Boolean)? = null
|
||||
): List<String> {
|
||||
val fileNames = mutableListOf<String>()
|
||||
try {
|
||||
|
||||
|
||||
var entry: Long
|
||||
|
||||
while (Archive.readNextHeader(archive).also { entry = it } != 0L) {
|
||||
val fileName =
|
||||
getEntryString(ArchiveEntry.pathnameUtf8(entry), ArchiveEntry.pathname(entry))
|
||||
?: continue
|
||||
|
||||
|
||||
val entryStat = ArchiveEntry.stat(entry)
|
||||
|
||||
if (entryStat.isDir()) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (filter != null && filter.invoke(fileName))
|
||||
fileNames.add(fileName)
|
||||
|
||||
|
||||
}
|
||||
} finally {
|
||||
Archive.free(archive)
|
||||
}
|
||||
|
||||
return fileNames
|
||||
}
|
||||
|
||||
private fun ArchiveEntry.StructStat.isDir() = S_ISDIR(this.stMode)
|
||||
|
||||
private fun getEntryString(utf8: String?, bytes: ByteArray?): String? {
|
||||
return utf8 ?: newStringFromBytes(bytes)
|
||||
}
|
||||
|
||||
private fun newStringFromBytes(bytes: ByteArray?): String? {
|
||||
bytes ?: return null
|
||||
val cd = CharsetDetector()
|
||||
cd.setText(bytes)
|
||||
val c = cd.detectAll().first().name
|
||||
return String(bytes, Charset.forName(c))
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
package io.legato.kazusa.utils.compress
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import io.legato.kazusa.utils.DebugLog
|
||||
import io.legato.kazusa.utils.printOnDebug
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.*
|
||||
import java.util.zip.*
|
||||
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
@Suppress("unused", "MemberVisibilityCanBePrivate")
|
||||
object ZipUtils {
|
||||
|
||||
fun gzipByteArray(byteArray: ByteArray): ByteArray {
|
||||
val byteOut = ByteArrayOutputStream()
|
||||
val zip = GZIPOutputStream(byteOut)
|
||||
return zip.use {
|
||||
it.write(byteArray)
|
||||
byteOut.use {
|
||||
byteOut.toByteArray()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun zipByteArray(byteArray: ByteArray, fileName: String): ByteArray {
|
||||
val byteOut = ByteArrayOutputStream()
|
||||
val zipOutputStream = ZipOutputStream(byteOut)
|
||||
zipOutputStream.putNextEntry(ZipEntry(fileName))
|
||||
zipOutputStream.write(byteArray)
|
||||
zipOutputStream.closeEntry()
|
||||
zipOutputStream.finish()
|
||||
return zipOutputStream.use {
|
||||
byteOut.use {
|
||||
byteOut.toByteArray()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zip the files.
|
||||
*
|
||||
* @param srcFiles The source of files.
|
||||
* @param zipFilePath The path of ZIP file.
|
||||
* @return `true`: success<br></br>`false`: fail
|
||||
* @throws IOException if an I/O error has occurred
|
||||
*/
|
||||
suspend fun zipFiles(
|
||||
srcFiles: Collection<String>,
|
||||
zipFilePath: String
|
||||
): Boolean {
|
||||
return zipFiles(srcFiles, zipFilePath, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Zip the files.
|
||||
*
|
||||
* @param srcFilePaths The paths of source files.
|
||||
* @param zipFilePath The path of ZIP file.
|
||||
* @param comment The comment.
|
||||
* @return `true`: success<br></br>`false`: fail
|
||||
* @throws IOException if an I/O error has occurred
|
||||
*/
|
||||
suspend fun zipFiles(
|
||||
srcFilePaths: Collection<String>?,
|
||||
zipFilePath: String?,
|
||||
comment: String?
|
||||
): Boolean = withContext(IO) {
|
||||
if (srcFilePaths == null || zipFilePath == null) return@withContext false
|
||||
ZipOutputStream(FileOutputStream(zipFilePath)).use {
|
||||
for (srcFile in srcFilePaths) {
|
||||
if (!zipFile(getFileByPath(srcFile)!!, "", it, comment))
|
||||
return@withContext false
|
||||
}
|
||||
return@withContext true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zip the files.
|
||||
*
|
||||
* @param srcFiles The source of files.
|
||||
* @param zipFile The ZIP file.
|
||||
* @param comment The comment.
|
||||
* @return `true`: success<br></br>`false`: fail
|
||||
* @throws IOException if an I/O error has occurred
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
@JvmOverloads
|
||||
fun zipFiles(
|
||||
srcFiles: Collection<File>?,
|
||||
zipFile: File?,
|
||||
comment: String? = null
|
||||
): Boolean {
|
||||
if (srcFiles == null || zipFile == null) return false
|
||||
ZipOutputStream(FileOutputStream(zipFile)).use {
|
||||
for (srcFile in srcFiles) {
|
||||
if (!zipFile(srcFile, "", it, comment)) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zip the file.
|
||||
*
|
||||
* @param srcFilePath The path of source file.
|
||||
* @param zipFilePath The path of ZIP file.
|
||||
* @return `true`: success<br></br>`false`: fail
|
||||
* @throws IOException if an I/O error has occurred
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun zipFile(
|
||||
srcFilePath: String,
|
||||
zipFilePath: String
|
||||
): Boolean {
|
||||
return zipFile(getFileByPath(srcFilePath), getFileByPath(zipFilePath), null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Zip the file.
|
||||
*
|
||||
* @param srcFilePath The path of source file.
|
||||
* @param zipFilePath The path of ZIP file.
|
||||
* @param comment The comment.
|
||||
* @return `true`: success<br></br>`false`: fail
|
||||
* @throws IOException if an I/O error has occurred
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun zipFile(
|
||||
srcFilePath: String,
|
||||
zipFilePath: String,
|
||||
comment: String
|
||||
): Boolean {
|
||||
return zipFile(getFileByPath(srcFilePath), getFileByPath(zipFilePath), comment)
|
||||
}
|
||||
|
||||
/**
|
||||
* Zip the file.
|
||||
*
|
||||
* @param srcFile The source of file.
|
||||
* @param zipFile The ZIP file.
|
||||
* @param comment The comment.
|
||||
* @return `true`: success<br></br>`false`: fail
|
||||
* @throws IOException if an I/O error has occurred
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
@JvmOverloads
|
||||
fun zipFile(
|
||||
srcFile: File?,
|
||||
zipFile: File?,
|
||||
comment: String? = null
|
||||
): Boolean {
|
||||
if (srcFile == null || zipFile == null) return false
|
||||
ZipOutputStream(FileOutputStream(zipFile)).use { zos ->
|
||||
return zipFile(srcFile, "", zos, comment)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun zipFile(
|
||||
srcFile: File,
|
||||
rootPath: String,
|
||||
zos: ZipOutputStream,
|
||||
comment: String?
|
||||
): Boolean {
|
||||
var rootPath1 = rootPath
|
||||
if (!srcFile.exists()) return true
|
||||
rootPath1 = rootPath1 + (if (isSpace(rootPath1)) "" else File.separator) + srcFile.name
|
||||
if (srcFile.isDirectory) {
|
||||
val fileList = srcFile.listFiles()
|
||||
if (fileList == null || fileList.isEmpty()) {
|
||||
val entry = ZipEntry("$rootPath1/")
|
||||
entry.comment = comment
|
||||
zos.putNextEntry(entry)
|
||||
zos.closeEntry()
|
||||
} else {
|
||||
for (file in fileList) {
|
||||
if (!zipFile(file, rootPath1, zos, comment)) return false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
BufferedInputStream(FileInputStream(srcFile)).use {
|
||||
val entry = ZipEntry(rootPath1)
|
||||
entry.comment = comment
|
||||
zos.putNextEntry(entry)
|
||||
it.copyTo(zos)
|
||||
zos.closeEntry()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@Throws(SecurityException::class)
|
||||
fun unZipToPath(file: File, path: String, filter: ((String) -> Boolean)? = null): List<File> {
|
||||
return FileInputStream(file).use {
|
||||
unZipToPath(it, path, filter)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(SecurityException::class)
|
||||
fun unZipToPath(file: File, dir: File, filter: ((String) -> Boolean)? = null): List<File> {
|
||||
return FileInputStream(file).use {
|
||||
unZipToPath(it, dir, filter)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(SecurityException::class)
|
||||
fun unZipToPath(
|
||||
inputStream: InputStream,
|
||||
path: String,
|
||||
filter: ((String) -> Boolean)? = null
|
||||
): List<File> {
|
||||
return ZipInputStream(inputStream).use {
|
||||
unZipToPath(it, File(path), filter)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(SecurityException::class)
|
||||
fun unZipToPath(
|
||||
inputStream: InputStream,
|
||||
dir: File,
|
||||
filter: ((String) -> Boolean)? = null
|
||||
): List<File> {
|
||||
return ZipInputStream(inputStream).use {
|
||||
unZipToPath(it, dir, filter)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(SecurityException::class)
|
||||
private fun unZipToPath(
|
||||
zipInputStream: ZipInputStream,
|
||||
dir: File,
|
||||
filter: ((String) -> Boolean)? = null
|
||||
): List<File> {
|
||||
val files = arrayListOf<File>()
|
||||
var entry: ZipEntry?
|
||||
while (zipInputStream.nextEntry.also { entry = it } != null) {
|
||||
val entryName = entry!!.name
|
||||
val entryFile = File(dir, entryName)
|
||||
if (!entryFile.canonicalPath.startsWith(dir.canonicalPath)) {
|
||||
throw SecurityException("压缩文件只能解压到指定路径")
|
||||
}
|
||||
if (entry!!.isDirectory) {
|
||||
if (!entryFile.exists()) {
|
||||
entryFile.mkdirs()
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (entryFile.parentFile?.exists() != true) {
|
||||
entryFile.parentFile?.mkdirs()
|
||||
}
|
||||
if (filter != null && !filter.invoke(entryName)) continue
|
||||
if (!entryFile.exists()) {
|
||||
entryFile.createNewFile()
|
||||
entryFile.setReadable(true)
|
||||
entryFile.setExecutable(true)
|
||||
}
|
||||
FileOutputStream(entryFile).use {
|
||||
zipInputStream.copyTo(it)
|
||||
files.add(entryFile)
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
/* 遍历目录获取所有文件名 */
|
||||
@Throws(SecurityException::class)
|
||||
fun getFilesName(
|
||||
inputStream: InputStream,
|
||||
filter: ((String) -> Boolean)? = null
|
||||
): List<String> {
|
||||
return ZipInputStream(inputStream).use {
|
||||
getFilesName(it, filter)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(SecurityException::class)
|
||||
private fun getFilesName(
|
||||
zipInputStream: ZipInputStream,
|
||||
filter: ((String) -> Boolean)? = null
|
||||
): List<String> {
|
||||
val fileNames = mutableListOf<String>()
|
||||
var entry: ZipEntry?
|
||||
while (zipInputStream.nextEntry.also { entry = it } != null) {
|
||||
if (entry!!.isDirectory) {
|
||||
continue
|
||||
}
|
||||
val fileName = entry!!.name
|
||||
if (filter != null && filter.invoke(fileName))
|
||||
fileNames.add(fileName)
|
||||
}
|
||||
return fileNames
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the files' path in ZIP file.
|
||||
*
|
||||
* @param zipFilePath The path of ZIP file.
|
||||
* @return the files' path in ZIP file
|
||||
* @throws IOException if an I/O error has occurred
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun getFilesPath(zipFilePath: String): List<String>? {
|
||||
return getFilesPath(getFileByPath(zipFilePath))
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the files' path in ZIP file.
|
||||
*
|
||||
* @param zipFile The ZIP file.
|
||||
* @return the files' path in ZIP file
|
||||
* @throws IOException if an I/O error has occurred
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun getFilesPath(zipFile: File?): List<String>? {
|
||||
if (zipFile == null) return null
|
||||
val paths = ArrayList<String>()
|
||||
val zip = ZipFile(zipFile)
|
||||
val entries = zip.entries()
|
||||
while (entries.hasMoreElements()) {
|
||||
val entryName = (entries.nextElement() as ZipEntry).name
|
||||
if (entryName.contains("../")) {
|
||||
DebugLog.e(javaClass.name, "entryName: $entryName is dangerous!")
|
||||
paths.add(entryName)
|
||||
} else {
|
||||
paths.add(entryName)
|
||||
}
|
||||
}
|
||||
zip.close()
|
||||
return paths
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the files' comment in ZIP file.
|
||||
*
|
||||
* @param zipFilePath The path of ZIP file.
|
||||
* @return the files' comment in ZIP file
|
||||
* @throws IOException if an I/O error has occurred
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun getComments(zipFilePath: String): List<String>? {
|
||||
return getComments(getFileByPath(zipFilePath))
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the files' comment in ZIP file.
|
||||
*
|
||||
* @param zipFile The ZIP file.
|
||||
* @return the files' comment in ZIP file
|
||||
* @throws IOException if an I/O error has occurred
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun getComments(zipFile: File?): List<String>? {
|
||||
if (zipFile == null) return null
|
||||
val comments = ArrayList<String>()
|
||||
val zip = ZipFile(zipFile)
|
||||
val entries = zip.entries()
|
||||
while (entries.hasMoreElements()) {
|
||||
val entry = entries.nextElement() as ZipEntry
|
||||
comments.add(entry.comment)
|
||||
}
|
||||
zip.close()
|
||||
return comments
|
||||
}
|
||||
|
||||
private fun createOrExistsDir(file: File?): Boolean {
|
||||
return file != null && if (file.exists()) file.isDirectory else file.mkdirs()
|
||||
}
|
||||
|
||||
private fun createOrExistsFile(file: File?): Boolean {
|
||||
if (file == null) return false
|
||||
if (file.exists()) return file.isFile
|
||||
if (!createOrExistsDir(file.parentFile)) return false
|
||||
return try {
|
||||
file.createNewFile()
|
||||
} catch (e: IOException) {
|
||||
e.printOnDebug()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFileByPath(filePath: String): File? {
|
||||
return if (isSpace(filePath)) null else File(filePath)
|
||||
}
|
||||
|
||||
private fun isSpace(s: String?): Boolean {
|
||||
if (s == null) return true
|
||||
var i = 0
|
||||
val len = s.length
|
||||
while (i < len) {
|
||||
if (!Character.isWhitespace(s[i])) {
|
||||
return false
|
||||
}
|
||||
++i
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package io.legato.kazusa.utils.objectpool
|
||||
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.core.util.Pools
|
||||
|
||||
abstract class BaseObjectPool<T : Any>(size: Int) : ObjectPool<T> {
|
||||
|
||||
open val pool = Pools.SimplePool<T>(size)
|
||||
|
||||
override fun obtain(): T {
|
||||
return pool.acquire() ?: create()
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun recycle(target: T) {
|
||||
pool.release(target)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package io.legato.kazusa.utils.objectpool
|
||||
|
||||
import androidx.core.util.Pools
|
||||
|
||||
abstract class BaseSafeObjectPool<T : Any>(size: Int): BaseObjectPool<T>(size) {
|
||||
|
||||
override val pool = Pools.SynchronizedPool<T>(size)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package io.legato.kazusa.utils.objectpool
|
||||
|
||||
interface ObjectPool<T> {
|
||||
|
||||
fun obtain(): T
|
||||
|
||||
fun recycle(target: T)
|
||||
|
||||
fun create(): T
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package io.legato.kazusa.utils.objectpool
|
||||
|
||||
fun <T> ObjectPool<T>.synchronized(): ObjectPool<T> = ObjectPoolLocked(this)
|
||||
@@ -0,0 +1,15 @@
|
||||
package io.legato.kazusa.utils.objectpool
|
||||
|
||||
class ObjectPoolLocked<T>(private val delegate: ObjectPool<T>) : ObjectPool<T> by delegate {
|
||||
|
||||
@Synchronized
|
||||
override fun obtain(): T {
|
||||
return delegate.obtain()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun recycle(target: T) {
|
||||
return delegate.recycle(target)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
@file:Suppress("RedundantVisibilityModifier", "unused")
|
||||
|
||||
package io.legato.kazusa.utils.viewbindingdelegate
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import androidx.core.app.ComponentActivity
|
||||
import androidx.viewbinding.ViewBinding
|
||||
|
||||
/**
|
||||
* Create new [ViewBinding] associated with the [ComponentActivity]
|
||||
*/
|
||||
@JvmName("viewBindingActivity")
|
||||
inline fun <T : ViewBinding> ComponentActivity.viewBinding(
|
||||
crossinline bindingInflater: (LayoutInflater) -> T,
|
||||
setContentView: Boolean = false
|
||||
) = lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
|
||||
val binding = bindingInflater.invoke(layoutInflater)
|
||||
if (setContentView) {
|
||||
setContentView(binding.root)
|
||||
}
|
||||
binding
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
@file:Suppress("RedundantVisibilityModifier", "unused", "UnusedReceiverParameter")
|
||||
@file:JvmName("ReflectionFragmentViewBindings")
|
||||
|
||||
package io.legato.kazusa.utils.viewbindingdelegate
|
||||
|
||||
import android.view.View
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.viewbinding.ViewBinding
|
||||
|
||||
private class FragmentViewBindingProperty<F : Fragment, T : ViewBinding>(
|
||||
viewBinder: (F) -> T
|
||||
) : ViewBindingProperty<F, T>(viewBinder) {
|
||||
|
||||
override fun getLifecycleOwner(thisRef: F) = thisRef.viewLifecycleOwner
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new [ViewBinding] associated with the [Fragment]
|
||||
*/
|
||||
@JvmName("viewBindingFragment")
|
||||
public fun <F : Fragment, T : ViewBinding> Fragment.viewBinding(viewBinder: (F) -> T): ViewBindingProperty<F, T> {
|
||||
return FragmentViewBindingProperty(viewBinder)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new [ViewBinding] associated with the [Fragment]
|
||||
*
|
||||
* @param vbFactory Function that create new instance of [ViewBinding]. `MyViewBinding::bind` can be used
|
||||
* @param viewProvider Provide a [View] from the Fragment. By default call [Fragment.requireView]
|
||||
*/
|
||||
@JvmName("viewBindingFragment")
|
||||
public inline fun <F : Fragment, T : ViewBinding> Fragment.viewBinding(
|
||||
crossinline vbFactory: (View) -> T,
|
||||
crossinline viewProvider: (F) -> View = Fragment::requireView
|
||||
): ViewBindingProperty<F, T> {
|
||||
return viewBinding { fragment: F -> vbFactory(viewProvider(fragment)) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new [ViewBinding] associated with the [Fragment]
|
||||
*
|
||||
* @param vbFactory Function that create new instance of [ViewBinding]. `MyViewBinding::bind` can be used
|
||||
* @param viewBindingRootId Root view's id that will be used as root for the view binding
|
||||
*/
|
||||
@JvmName("viewBindingFragment")
|
||||
public inline fun <T : ViewBinding> Fragment.viewBinding(
|
||||
crossinline vbFactory: (View) -> T,
|
||||
@IdRes viewBindingRootId: Int
|
||||
): ViewBindingProperty<Fragment, T> {
|
||||
return viewBinding(vbFactory) { fragment: Fragment ->
|
||||
fragment.requireView().findViewById(viewBindingRootId)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
@file:Suppress("RedundantVisibilityModifier")
|
||||
|
||||
package io.legato.kazusa.utils.viewbindingdelegate
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
public abstract class ViewBindingProperty<in R : Any, T : ViewBinding>(
|
||||
private val viewBinder: (R) -> T
|
||||
) : ReadOnlyProperty<R, T> {
|
||||
|
||||
private var viewBinding: T? = null
|
||||
private val lifecycleObserver = ClearOnDestroyLifecycleObserver()
|
||||
private var thisRef: R? = null
|
||||
|
||||
protected abstract fun getLifecycleOwner(thisRef: R): LifecycleOwner
|
||||
|
||||
@MainThread
|
||||
public override fun getValue(thisRef: R, property: KProperty<*>): T {
|
||||
viewBinding?.let { return it }
|
||||
|
||||
this.thisRef = thisRef
|
||||
val lifecycle = getLifecycleOwner(thisRef).lifecycle
|
||||
if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
|
||||
mainHandler.post { viewBinding = null }
|
||||
} else {
|
||||
lifecycle.addObserver(lifecycleObserver)
|
||||
}
|
||||
return viewBinder(thisRef).also { viewBinding = it }
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public fun clear() {
|
||||
val thisRef = thisRef ?: return
|
||||
this.thisRef = null
|
||||
getLifecycleOwner(thisRef).lifecycle.removeObserver(lifecycleObserver)
|
||||
mainHandler.post { viewBinding = null }
|
||||
}
|
||||
|
||||
private inner class ClearOnDestroyLifecycleObserver : DefaultLifecycleObserver {
|
||||
|
||||
@MainThread
|
||||
override fun onDestroy(owner: LifecycleOwner): Unit = clear()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user