This commit is contained in:
HapeLee committed 2025-06-04 13:33:51 +08:00
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 = "(&nbsp;)+".toRegex()
private val espRegex = "(&ensp;|&emsp;)".toRegex()
private val noPrintRegex = "(&thinsp;|&zwnj;|&zwj;|\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())
}
}