first commit

This commit is contained in:
fengjignqi
2026-09-17 08:59:24 +08:00
parent ea41ddbe74
commit 6e9b97e42e
149 changed files with 5948 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
package com.juhabi.birds.util
import android.content.Context
import java.io.File
import java.util.Locale
import top.zibin.luban.Luban
import top.zibin.luban.OnCompressListener
/** 使用 Luban 对图片文件进行异步压缩。 */
object LubanCompressUtil {
private const val DEFAULT_IGNORE_SIZE_KB = 100
interface CompressCallback {
/** 图片压缩成功。 */
fun success(file: File)
/** 图片压缩失败。 */
fun fail(failMsg: String)
}
/** 兼容 WaterProject 中的单例调用方式。 */
fun getInstance(): LubanCompressUtil {
return this
}
/**
* 压缩指定图片。
*
* 小于 100KB 的图片保持原样,GIF 图片不参与压缩。
*/
fun getCompressFile(
context: Context,
oldFile: File,
callback: CompressCallback?
) {
if (!oldFile.exists() || !oldFile.isFile) {
callback?.fail("图片文件不存在")
return
}
Luban.with(context.applicationContext)
.load(oldFile)
.ignoreBy(DEFAULT_IGNORE_SIZE_KB)
.filter { path ->
path.isNotBlank() && !path.lowercase(Locale.ROOT).endsWith(".gif")
}
.setCompressListener(object : OnCompressListener {
/** 压缩开始。 */
override fun onStart() {
}
/** 返回压缩后的图片文件。 */
override fun onSuccess(file: File) {
callback?.success(file)
}
/** 将压缩异常转换为业务错误消息。 */
override fun onError(error: Throwable) {
callback?.fail(error.message ?: "图片压缩失败")
}
})
.launch()
}
}

View File

@@ -0,0 +1,112 @@
package com.juhabi.birds.util
import android.content.Context
import android.content.SharedPreferences
import com.google.gson.Gson
import com.google.gson.JsonSyntaxException
/**
* 使用 SharedPreferences 存储轻量级本地数据。
*
* 复杂对象会序列化为 JSON,仅适合存储用户信息等体积较小的数据。
*/
object SharedPreferencesUtil {
const val DEFAULT_PREFERENCES_NAME = "kot_mvp_user"
private val gson = Gson()
private fun preferences(
context: Context,
preferencesName: String
): SharedPreferences = context.applicationContext.getSharedPreferences(
preferencesName,
Context.MODE_PRIVATE
)
fun putString(
context: Context,
key: String,
value: String,
preferencesName: String = DEFAULT_PREFERENCES_NAME
) = preferences(context, preferencesName).edit().putString(key, value).apply()
fun getString(
context: Context,
key: String,
defaultValue: String = "",
preferencesName: String = DEFAULT_PREFERENCES_NAME
): String = preferences(context, preferencesName).getString(key, defaultValue) ?: defaultValue
fun putInt(context: Context, key: String, value: Int, preferencesName: String = DEFAULT_PREFERENCES_NAME) =
preferences(context, preferencesName).edit().putInt(key, value).apply()
fun getInt(context: Context, key: String, defaultValue: Int = 0, preferencesName: String = DEFAULT_PREFERENCES_NAME) =
preferences(context, preferencesName).getInt(key, defaultValue)
fun putLong(context: Context, key: String, value: Long, preferencesName: String = DEFAULT_PREFERENCES_NAME) =
preferences(context, preferencesName).edit().putLong(key, value).apply()
fun getLong(context: Context, key: String, defaultValue: Long = 0L, preferencesName: String = DEFAULT_PREFERENCES_NAME) =
preferences(context, preferencesName).getLong(key, defaultValue)
fun putFloat(context: Context, key: String, value: Float, preferencesName: String = DEFAULT_PREFERENCES_NAME) =
preferences(context, preferencesName).edit().putFloat(key, value).apply()
fun getFloat(context: Context, key: String, defaultValue: Float = 0f, preferencesName: String = DEFAULT_PREFERENCES_NAME) =
preferences(context, preferencesName).getFloat(key, defaultValue)
fun putBoolean(context: Context, key: String, value: Boolean, preferencesName: String = DEFAULT_PREFERENCES_NAME) =
preferences(context, preferencesName).edit().putBoolean(key, value).apply()
fun getBoolean(context: Context, key: String, defaultValue: Boolean = false, preferencesName: String = DEFAULT_PREFERENCES_NAME) =
preferences(context, preferencesName).getBoolean(key, defaultValue)
fun putStringSet(
context: Context,
key: String,
value: Set<String>,
preferencesName: String = DEFAULT_PREFERENCES_NAME
) = preferences(context, preferencesName).edit().putStringSet(key, value).apply()
fun getStringSet(
context: Context,
key: String,
defaultValue: Set<String> = emptySet(),
preferencesName: String = DEFAULT_PREFERENCES_NAME
): Set<String> = preferences(context, preferencesName)
.getStringSet(key, defaultValue)
?.toSet()
?: defaultValue
fun putObject(
context: Context,
key: String,
value: Any,
preferencesName: String = DEFAULT_PREFERENCES_NAME
) = putString(context, key, gson.toJson(value), preferencesName)
fun <T> getObject(
context: Context,
key: String,
clazz: Class<T>,
preferencesName: String = DEFAULT_PREFERENCES_NAME
): T? {
val json = getString(context, key, preferencesName = preferencesName)
if (json.isBlank()) return null
return try {
gson.fromJson(json, clazz)
} catch (_: JsonSyntaxException) {
null
}
}
fun contains(context: Context, key: String, preferencesName: String = DEFAULT_PREFERENCES_NAME) =
preferences(context, preferencesName).contains(key)
fun remove(context: Context, key: String, preferencesName: String = DEFAULT_PREFERENCES_NAME) =
preferences(context, preferencesName).edit().remove(key).apply()
fun clear(context: Context, preferencesName: String = DEFAULT_PREFERENCES_NAME) =
preferences(context, preferencesName).edit().clear().apply()
}

View File

@@ -0,0 +1,43 @@
package com.juhabi.birds.util
import android.content.Context
import android.widget.Toast
/** Toast 统一管理工具。 */
object ToastUtil {
var isShow = true
private var toast: Toast? = null
fun showShort(context: Context, message: CharSequence) {
show(context, message, Toast.LENGTH_SHORT)
}
fun showShort(context: Context, message: Int) {
show(context, message.toString(), Toast.LENGTH_SHORT)
}
fun showLong(context: Context, message: CharSequence) {
show(context, message, Toast.LENGTH_LONG)
}
fun showLong(context: Context, message: Int) {
show(context, message.toString(), Toast.LENGTH_LONG)
}
fun show(context: Context, message: CharSequence, duration: Int) {
val currentToast = toast
if (currentToast == null) {
toast = Toast.makeText(context, message, duration)
} else {
currentToast.setText(message)
currentToast.duration = duration
}
toast?.show()
}
fun show(context: Context, message: Int, duration: Int) {
show(context, message.toString(), duration)
}
}

View File

@@ -0,0 +1,20 @@
package com.juhabi.birds.util.click
import android.graphics.Rect
import android.view.TouchDelegate
import android.view.View
object ClickUtil {
/** 将 [view] 的触摸区域向四周分别扩大 [size] 像素。 */
@JvmStatic
fun addClickPart(view: View, size: Int) {
val parent = view.parent as? View ?: return
val expandSize = size.coerceAtLeast(0)
parent.post {
val delegateArea = Rect()
view.getHitRect(delegateArea)
delegateArea.inset(-expandSize, -expandSize)
parent.touchDelegate = TouchDelegate(delegateArea, view)
}
}
}

View File

@@ -0,0 +1,81 @@
package com.juhabi.birds.util.data
import java.time.DateTimeException
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.time.format.ResolverStyle
/** 校验中国大陆地区的 15 位和 18 位居民身份证号码。 */
object IDCardValidate {
private val weights = intArrayOf(7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2)
private val verifyCodes = charArrayOf('1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2')
private val areaCodes = setOf(
"11", "12", "13", "14", "15",
"21", "22", "23",
"31", "32", "33", "34", "35", "36", "37",
"41", "42", "43", "44", "45", "46",
"50", "51", "52", "53", "54",
"61", "62", "63", "64", "65",
"71", "81", "82", "91"
)
private val birthDateFormatter = DateTimeFormatter
.ofPattern("uuuuMMdd")
.withResolverStyle(ResolverStyle.STRICT)
private val earliestBirthDate = LocalDate.of(1900, 1, 1)
@JvmStatic
fun validate_effective(idcardNumber: String?): Boolean = validateEffective(idcardNumber)
/**
* 为兼容 Water 项目而保留,第二个参数不会影响校验结果。
*/
@JvmStatic
@Suppress("UNUSED_PARAMETER")
fun validate_effective(idcardNumber: String?, isreturn_AutoCard: Boolean): Boolean =
validateEffective(idcardNumber)
@JvmStatic
fun validateEffective(idCardNumber: String?): Boolean {
val source = idCardNumber?.trim()?.uppercase() ?: return false
val cardNumber = when (source.length) {
OLD_CARD_NUMBER_LENGTH -> convertToNewCardNumber(source) ?: return false
NEW_CARD_NUMBER_LENGTH -> source
else -> return false
}
if (!cardNumber.take(17).all(Char::isDigit)) return false
if (cardNumber.last() != 'X' && !cardNumber.last().isDigit()) return false
if (cardNumber.substring(0, 2) !in areaCodes) return false
if (!hasValidBirthDate(cardNumber.substring(6, 14))) return false
return cardNumber.last() == calculateVerifyCode(cardNumber)
}
private fun convertToNewCardNumber(oldCardNumber: String): String? {
if (!oldCardNumber.all(Char::isDigit)) return null
val firstSeventeen = buildString(17) {
append(oldCardNumber, 0, 6)
append("19")
append(oldCardNumber, 6, oldCardNumber.length)
}
return firstSeventeen + calculateVerifyCode(firstSeventeen)
}
private fun hasValidBirthDate(value: String): Boolean {
val birthDate = try {
LocalDate.parse(value, birthDateFormatter)
} catch (_: DateTimeException) {
return false
}
return birthDate.isAfter(earliestBirthDate) && birthDate.isBefore(LocalDate.now())
}
private fun calculateVerifyCode(cardNumber: CharSequence): Char {
val sum = weights.indices.sumOf { index ->
(cardNumber[index] - '0') * weights[index]
}
return verifyCodes[sum % 11]
}
private const val NEW_CARD_NUMBER_LENGTH = 18
private const val OLD_CARD_NUMBER_LENGTH = 15
}

View File

@@ -0,0 +1,13 @@
package com.juhabi.birds.util.data
object PhonePatternUtil {
private val phonePattern = Regex("^1[3-9]\\d{9}$")
@JvmStatic
fun phoneReg(phoneNumber: String?): Boolean = isValidPhoneNumber(phoneNumber)
/** 校验中国大陆地区的 11 位手机号码。 */
@JvmStatic
fun isValidPhoneNumber(phoneNumber: String?): Boolean =
phoneNumber != null && phonePattern.matches(phoneNumber)
}

View File

@@ -0,0 +1,67 @@
package com.juhabi.birds.util.file
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.webkit.MimeTypeMap
import androidx.core.content.FileProvider
import java.io.File
object PreviewFileUtil {
@JvmStatic
fun previewFiles(context: Context, suffix: String?, fileUrl: String?): Boolean {
if (fileUrl.isNullOrBlank()) return false
val uri = when {
fileUrl.startsWith("content://", ignoreCase = true) -> Uri.parse(fileUrl)
fileUrl.startsWith("http://", ignoreCase = true) ||
fileUrl.startsWith("https://", ignoreCase = true) -> Uri.parse(fileUrl)
else -> fileToContentUri(context, File(fileUrl)) ?: return false
}
return preview(context, uri, resolveMimeType(suffix, uri))
}
@JvmStatic
fun preview(context: Context, uri: Uri, mimeType: String? = null): Boolean {
val resolvedMimeType = mimeType
?.takeIf { it.isNotBlank() }
?: context.contentResolver.getType(uri)
?: resolveMimeType(null, uri)
val intent = Intent(Intent.ACTION_VIEW)
.setDataAndType(uri, resolvedMimeType)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
if (context !is Activity) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
return try {
context.startActivity(Intent.createChooser(intent, null).apply {
if (context !is Activity) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
})
true
} catch (_: ActivityNotFoundException) {
false
}
}
private fun fileToContentUri(context: Context, file: File): Uri? {
if (!file.exists() || !file.isFile) return null
return try {
FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file)
} catch (_: IllegalArgumentException) {
null
}
}
private fun resolveMimeType(suffix: String?, uri: Uri): String {
val extension = suffix
?.substringAfterLast('.')
?.trim()
?.lowercase()
?.takeIf { it.isNotBlank() }
?: MimeTypeMap.getFileExtensionFromUrl(uri.toString()).lowercase()
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
?: DEFAULT_MIME_TYPE
}
private const val DEFAULT_MIME_TYPE = "*/*"
}

View File

@@ -0,0 +1,60 @@
package com.juhabi.birds.util.manager
import android.app.Activity
import java.util.Collections
import java.util.LinkedHashMap
object ActivityManager {
private val activities = Collections.synchronizedMap(LinkedHashMap<String, Activity>())
/** 返回 Activity 集合的快照,防止调用方直接修改内部集合。 */
@JvmStatic
fun getActivitys(): Map<String, Activity> = synchronized(activities) {
LinkedHashMap(activities)
}
@JvmStatic
fun getActivity(key: String): Activity? = activities[key]
@JvmStatic
fun addActivity(value: Activity, key: String) {
activities[key] = value
}
@JvmStatic
fun removeActivity(key: String) {
activities.remove(key)
}
@JvmStatic
fun removeAllActivity() {
finishAndRemove { _, _ -> true }
}
@JvmStatic
fun removeAllActivityExceptMain() {
finishAndRemove { key, activity ->
!key.contains(MAIN_ACTIVITY_NAME) && activity.javaClass.simpleName != MAIN_ACTIVITY_NAME
}
}
@JvmStatic
fun removeActivityByTag(tag: String) {
if (tag.isBlank()) return
finishAndRemove { key, _ -> key.contains(tag) }
}
private fun finishAndRemove(predicate: (String, Activity) -> Boolean) {
val targets = synchronized(activities) {
activities.filter { (key, activity) -> predicate(key, activity) }
}
targets.forEach { (key, activity) ->
if (!activity.isFinishing && !activity.isDestroyed) {
activity.runOnUiThread { activity.finish() }
}
activities.remove(key, activity)
}
}
private const val MAIN_ACTIVITY_NAME = "MainActivity"
}

View File

@@ -0,0 +1,85 @@
package com.juhabi.birds.util.manager
import android.content.Context
import com.juhabi.birds.MyApplication
import com.juhabi.birds.network.TokenManager
import com.juhabi.birds.user.model.UserEntity
import com.juhabi.birds.util.SharedPreferencesUtil
/** 管理当前用户的内存缓存和轻量级本地持久化数据。 */
object UserInfoManager {
@Volatile
private var cachedUser: UserEntity? = null
@JvmStatic
fun getInstance(): UserInfoManager = this
/** 应用启动时恢复本地保存的 token 和用户信息。 */
@JvmStatic
fun initialize(context: Context) {
TokenManager.get(context)
cachedUser = SharedPreferencesUtil.getObject(
context,
KEY_USER_INFO,
UserEntity::class.java
)
}
@JvmStatic
fun getUserInfo(): UserEntity? = getUserInfo(MyApplication.appContext)
@JvmStatic
fun getUserInfo(context: Context): UserEntity? {
cachedUser?.let { return it }
return SharedPreferencesUtil.getObject(
context,
KEY_USER_INFO,
UserEntity::class.java
)?.also { cachedUser = it }
}
@JvmStatic
fun setUserInfo(userInfo: UserEntity) {
setUserInfo(MyApplication.appContext, userInfo)
}
@JvmStatic
fun setUserInfo(context: Context, userInfo: UserEntity) {
cachedUser = userInfo
SharedPreferencesUtil.putObject(context, KEY_USER_INFO, userInfo)
}
/** 登录成功后统一保存用户信息和 token。 */
@JvmStatic
fun saveLogin(userInfo: UserEntity, token: String) {
saveLogin(MyApplication.appContext, userInfo, token)
}
@JvmStatic
fun saveLogin(context: Context, userInfo: UserEntity, token: String) {
setUserInfo(context, userInfo)
TokenManager.save(context, token)
SharedPreferencesUtil.putBoolean(context, KEY_USER_LOGIN, token.isNotBlank())
}
@JvmStatic
fun isLoggedIn(): Boolean = isLoggedIn(MyApplication.appContext)
@JvmStatic
fun isLoggedIn(context: Context): Boolean {
val hasToken = TokenManager.get(context).isNotBlank()
return hasToken && SharedPreferencesUtil.getBoolean(context, KEY_USER_LOGIN, hasToken)
}
/** 仅清除账号相关数据,不影响应用中的其他本地配置。 */
@JvmStatic
fun clearUser(context: Context) {
cachedUser = null
TokenManager.clear(context)
SharedPreferencesUtil.remove(context, KEY_USER_INFO)
SharedPreferencesUtil.remove(context, KEY_USER_LOGIN)
}
private const val KEY_USER_INFO = "user_info"
private const val KEY_USER_LOGIN = "user_login"
}