first commit
This commit is contained in:
11
app/src/main/java/com/juhabi/birds/AppConfig.kt
Normal file
11
app/src/main/java/com/juhabi/birds/AppConfig.kt
Normal file
@@ -0,0 +1,11 @@
|
||||
package com.juhabi.birds
|
||||
|
||||
object AppConfig {
|
||||
const val BASE_URL = "http://47.97.217.123:8081/"
|
||||
const val APP_CLIENT_ID = "428a8310cd442757ae699df5d894f051"
|
||||
const val APP_USER_TYPE = "app_user"
|
||||
const val CONTENT_LANGUAGE = "zh_CN"
|
||||
|
||||
@Volatile
|
||||
var userToken: String = ""
|
||||
}
|
||||
92
app/src/main/java/com/juhabi/birds/MyApplication.kt
Normal file
92
app/src/main/java/com/juhabi/birds/MyApplication.kt
Normal file
@@ -0,0 +1,92 @@
|
||||
package com.juhabi.birds
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.ActivityManager
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import com.juhabi.birds.util.manager.UserInfoManager
|
||||
import me.jessyan.autosize.AutoSize
|
||||
import me.jessyan.autosize.AutoSizeConfig
|
||||
|
||||
class MyApplication : Application(), Application.ActivityLifecycleCallbacks {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
// 初始化 AutoSize
|
||||
AutoSize.initCompatMultiProcess(this) // 处理多进程:cite[1]
|
||||
|
||||
// 可选:进行高级配置
|
||||
AutoSizeConfig.getInstance()
|
||||
.setLog(false) // 设置是否打印日志,默认 true
|
||||
.setBaseOnWidth(true) // 默认 true,表示以宽度为基准适配。false 则以高度为基准:cite[2]
|
||||
.setUseDeviceSize(false) // 是否使用设备的实际尺寸做适配,默认为 false:cite[1]
|
||||
.setExcludeFontScale(false) // 是否屏蔽系统字体大小对 AndroidAutoSize 的影响, 默认为 false:cite[1]
|
||||
|
||||
instance = this
|
||||
|
||||
if (!isMainProcess()) return
|
||||
|
||||
registerActivityLifecycleCallbacks(this)
|
||||
initialize()
|
||||
}
|
||||
|
||||
/** Add application-wide, privacy-safe SDK initialization here. */
|
||||
private fun initialize() {
|
||||
UserInfoManager.initialize(this)
|
||||
// NetworkManager is created lazily by ApiClient, so it needs no eager initialization.
|
||||
}
|
||||
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
currentActivity = activity
|
||||
}
|
||||
|
||||
override fun onActivityPaused(activity: Activity) {
|
||||
if (currentActivity === activity) {
|
||||
currentActivity = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityDestroyed(activity: Activity) {
|
||||
if (currentActivity === activity) {
|
||||
currentActivity = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||
}
|
||||
|
||||
override fun onActivityStarted(activity: Activity) {
|
||||
}
|
||||
|
||||
override fun onActivityStopped(activity: Activity) {
|
||||
}
|
||||
|
||||
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
|
||||
}
|
||||
|
||||
private fun isMainProcess(): Boolean {
|
||||
val processName = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
getProcessName()
|
||||
} else {
|
||||
val activityManager = getSystemService(ACTIVITY_SERVICE) as ActivityManager
|
||||
activityManager.runningAppProcesses
|
||||
?.firstOrNull { it.pid == android.os.Process.myPid() }
|
||||
?.processName
|
||||
}
|
||||
return processName == applicationInfo.processName
|
||||
}
|
||||
|
||||
companion object {
|
||||
lateinit var instance: MyApplication
|
||||
private set
|
||||
|
||||
val appContext: Context
|
||||
get() = instance.applicationContext
|
||||
|
||||
var currentActivity: Activity? = null
|
||||
private set
|
||||
}
|
||||
}
|
||||
383
app/src/main/java/com/juhabi/birds/base/BaseActivity.kt
Normal file
383
app/src/main/java/com/juhabi/birds/base/BaseActivity.kt
Normal file
@@ -0,0 +1,383 @@
|
||||
package com.juhabi.birds.base
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.WindowManager
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.juhabi.birds.R
|
||||
import com.juhabi.birds.dialog.LoadingDialog
|
||||
import com.gyf.immersionbar.ImmersionBar
|
||||
|
||||
/**
|
||||
* Activity 基类:统一管理 ViewBinding、Presenter 生命周期、弹窗和动态权限。
|
||||
*/
|
||||
abstract class BaseActivity<VB : ViewBinding, P : IPresenter<*>> :
|
||||
AppCompatActivity(), IView {
|
||||
|
||||
protected val mContext: BaseActivity<VB, P>
|
||||
get() = this
|
||||
|
||||
private var _binding: VB? = null
|
||||
protected val binding: VB
|
||||
get() = requireNotNull(_binding) { "Binding is unavailable outside the Activity lifecycle" }
|
||||
|
||||
protected var presenter: P? = null
|
||||
private set
|
||||
private var loadingDialog: LoadingDialog? = null
|
||||
private var permissionDialog: AlertDialog? = null
|
||||
private val permissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestMultiplePermissions()
|
||||
) { result ->
|
||||
hidePermissionDialog()
|
||||
val denied = result.filterValues { !it }.keys.toTypedArray()
|
||||
if (denied.isEmpty()) {
|
||||
onPermissionsGranted(result.keys.toTypedArray())
|
||||
} else {
|
||||
val permanentlyDenied = denied.all { !shouldShowRequestPermissionRationale(it) }
|
||||
onPermissionsDenied(denied, permanentlyDenied)
|
||||
}
|
||||
}
|
||||
|
||||
/** 创建当前页面的 ViewBinding。 */
|
||||
protected abstract fun createBinding(inflater: LayoutInflater): VB
|
||||
/** 创建 Presenter;不需要 Presenter 时可以返回 null。 */
|
||||
protected abstract fun createPresenter(): P?
|
||||
/** ViewBinding 初始化完成后,在这里设置控件状态和监听事件。 */
|
||||
protected abstract fun initView(savedInstanceState: Bundle?)
|
||||
/** 初始化页面数据,子类可按需重写。 */
|
||||
protected open fun initData(savedInstanceState: Bundle?) {
|
||||
}
|
||||
|
||||
/** 创建页面并依次完成绑定、Presenter、视图和数据初始化。 */
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
_binding = createBinding(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
initWindows()
|
||||
presenter = createPresenter().also { it?.attachBaseView(this) }
|
||||
initView(savedInstanceState)
|
||||
initData(savedInstanceState)
|
||||
}
|
||||
|
||||
/** 初始化系统标题栏、状态栏和导航栏,子页面可以按需重写。 */
|
||||
protected open fun initWindows() {
|
||||
supportActionBar?.hide()
|
||||
ImmersionBar.with(this)
|
||||
.fitsSystemWindows(true)
|
||||
.statusBarColor(R.color.color_zhu)
|
||||
.statusBarDarkFont(true)
|
||||
.navigationBarColor(R.color.color_zhu)
|
||||
.navigationBarDarkIcon(true)
|
||||
.init()
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun P.attachBaseView(view: IView) {
|
||||
(this as IPresenter<IView>).attachView(view)
|
||||
}
|
||||
|
||||
/** 实现 IView,显示默认加载弹窗。 */
|
||||
override fun showLoading() {
|
||||
showLoadingDialog()
|
||||
}
|
||||
|
||||
/** 实现 IView,隐藏默认加载弹窗。 */
|
||||
override fun hideLoading() {
|
||||
hideLoadingDialog()
|
||||
}
|
||||
|
||||
/** 显示加载弹窗;按返回键取消时关闭当前 Activity。 */
|
||||
fun showLoadingDialog(message: String = "加载中...") {
|
||||
if (isFinishing || isDestroyed || loadingDialog?.isShowing == true) return
|
||||
val dialog = LoadingDialog(this, message).apply {
|
||||
setCanceledOnTouchOutside(false)
|
||||
setCancelable(true)
|
||||
setOnCancelListener { finish() }
|
||||
}
|
||||
try {
|
||||
dialog.show()
|
||||
loadingDialog = dialog
|
||||
} catch (_: Exception) {
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
/** 关闭并释放加载弹窗。 */
|
||||
fun hideLoadingDialog() {
|
||||
loadingDialog?.dismiss()
|
||||
loadingDialog = null
|
||||
}
|
||||
|
||||
/** 使用 Toast 显示短消息。 */
|
||||
override fun showMessage(message: String) {
|
||||
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
/** 跳转到指定 Activity,可携带 Bundle 参数。 */
|
||||
protected fun goTo(to: Class<out Activity>, data: Bundle? = null) {
|
||||
startActivity(createActivityIntent(to, data))
|
||||
}
|
||||
|
||||
/** 兼容 Water 项目命名,跳转到不继承当前 BaseActivity 的普通 Activity。 */
|
||||
protected fun goToWihoutNoBase(to: Class<out Activity>, data: Bundle? = null) {
|
||||
goTo(to, data)
|
||||
}
|
||||
|
||||
/** 兼容 Water 项目命名,跳转到不使用 Presenter 的 Activity。 */
|
||||
protected fun goToWihoutNoPresent(to: Class<out Activity>, data: Bundle? = null) {
|
||||
goTo(to, data)
|
||||
}
|
||||
|
||||
/** 跳转到目标页面并关闭当前 Activity。 */
|
||||
protected fun goToAndFinish(to: Class<out Activity>, data: Bundle? = null) {
|
||||
goTo(to, data)
|
||||
finish()
|
||||
}
|
||||
|
||||
/** 清空现有任务栈并以目标 Activity 作为新的根页面。 */
|
||||
protected fun goToClearTask(to: Class<out Activity>, data: Bundle? = null) {
|
||||
val intent = createActivityIntent(to, data).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
/** 使用 Activity Result API 跳转,并由传入的 Launcher 接收返回结果。 */
|
||||
protected fun goToForResult(
|
||||
to: Class<out Activity>,
|
||||
launcher: ActivityResultLauncher<Intent>,
|
||||
data: Bundle? = null
|
||||
) {
|
||||
launcher.launch(createActivityIntent(to, data))
|
||||
}
|
||||
|
||||
/** 创建统一的 Activity 跳转 Intent,并按需写入 Bundle 参数。 */
|
||||
private fun createActivityIntent(to: Class<out Activity>, data: Bundle?): Intent {
|
||||
return Intent(this, to).apply {
|
||||
data?.let(::putExtras)
|
||||
}
|
||||
}
|
||||
|
||||
/** 检查单个权限是否已授权。 */
|
||||
fun hasRecordPermission(permission: String): Boolean {
|
||||
return hasRecordPermission(arrayOf(permission))
|
||||
}
|
||||
|
||||
/** 检查一组权限是否已全部授权。 */
|
||||
fun hasRecordPermission(permissions: Array<String>): Boolean {
|
||||
return permissions.all { ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED }
|
||||
}
|
||||
|
||||
/** 显示说明弹窗并申请单个权限。 */
|
||||
fun getSinglePermissionRequst(permission: String, title: String, content: String, requestCode: Int) {
|
||||
getMorePermissionRequst(arrayOf(permission), title, content, requestCode)
|
||||
}
|
||||
|
||||
/** 显示说明弹窗并一次申请多个权限。 */
|
||||
fun getMorePermissionRequst(permissions: Array<String>, title: String, content: String, requestCode: Int) {
|
||||
if (hasRecordPermission(permissions)) {
|
||||
onPermissionsGranted(permissions)
|
||||
return
|
||||
}
|
||||
showPermissionConfirmDialog(
|
||||
title,
|
||||
content,
|
||||
onCancel = { onPermissionsDenied(permissions, false) },
|
||||
onAllow = {
|
||||
showPermissionDialog(title, content)
|
||||
permissionLauncher.launch(permissions)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/** 使用默认说明文案申请一个或多个权限。 */
|
||||
protected open fun requestPermission(vararg permissions: String) {
|
||||
getMorePermissionRequst(permissions.toList().toTypedArray(), "需要权限", "请允许应用使用相关权限", 0)
|
||||
}
|
||||
|
||||
/** 权限被拒绝后的兼容回调,子类可重写。 */
|
||||
protected open fun noAllowPermission() {
|
||||
}
|
||||
/** 权限全部通过后的回调,默认继续执行下一步。 */
|
||||
protected open fun onPermissionsGranted(permissions: Array<String>) {
|
||||
nextStep()
|
||||
}
|
||||
/** 权限被拒绝后的回调,永久拒绝时默认引导用户进入设置页。 */
|
||||
protected open fun onPermissionsDenied(permissions: Array<String>, permanentlyDenied: Boolean) {
|
||||
noAllowPermission()
|
||||
if (permanentlyDenied && !isFinishing) {
|
||||
AlertDialog.Builder(this).setTitle("权限被拒绝")
|
||||
.setMessage("请在应用设置中开启所需权限")
|
||||
.setNegativeButton("取消", null)
|
||||
.setPositiveButton("去设置") { _, _ -> openAppPermissionSettings() }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
/** 权限通过后需要继续执行的业务步骤。 */
|
||||
protected open fun nextStep() {
|
||||
}
|
||||
|
||||
/** 普通确认弹窗和输入弹窗共用的点击回调。 */
|
||||
interface OnDialogClick {
|
||||
/** 点击取消按钮。 */
|
||||
fun onCancelClick() {
|
||||
}
|
||||
/** 点击普通弹窗的确定按钮。 */
|
||||
fun onConfirmClick() {
|
||||
}
|
||||
/** 点击输入弹窗的确定按钮,并返回输入内容。 */
|
||||
fun onConfirmClick(content: String) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 显示带取消和确定按钮的普通弹窗。 */
|
||||
fun showDialogtext(
|
||||
title: String,
|
||||
content: String,
|
||||
okText: String = "确定",
|
||||
cancelText: String = "取消",
|
||||
click: OnDialogClick? = null,
|
||||
touchOutsideCancel: Boolean = true
|
||||
) {
|
||||
val view = layoutInflater.inflate(R.layout.permission_confirm_dialog, null)
|
||||
view.findViewById<TextView>(R.id.permissionTitle).text = title
|
||||
view.findViewById<TextView>(R.id.permissionContent).text = content
|
||||
val dialog = AlertDialog.Builder(this).setView(view).create()
|
||||
view.findViewById<TextView>(R.id.permissionCancel).apply {
|
||||
text = cancelText
|
||||
setOnClickListener {
|
||||
dialog.dismiss()
|
||||
click?.onCancelClick()
|
||||
}
|
||||
}
|
||||
view.findViewById<TextView>(R.id.permissionAllow).apply {
|
||||
text = okText
|
||||
setOnClickListener {
|
||||
dialog.dismiss()
|
||||
click?.onConfirmClick()
|
||||
}
|
||||
}
|
||||
dialog.setCanceledOnTouchOutside(touchOutsideCancel)
|
||||
dialog.setOnShowListener { dialog.window?.setBackgroundDrawable(ColorDrawable(android.graphics.Color.TRANSPARENT)) }
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
/** 显示带输入框的弹窗,输入为空时保留弹窗并提示。 */
|
||||
fun showEditDialog(
|
||||
title: String,
|
||||
hint: String,
|
||||
content: String = "",
|
||||
touchOutsideCancel: Boolean = true,
|
||||
cancelText: String = "取消",
|
||||
confirmText: String = "确定",
|
||||
click: OnDialogClick? = null
|
||||
) {
|
||||
val view = layoutInflater.inflate(R.layout.dialog_input_text, null)
|
||||
view.findViewById<TextView>(R.id.dialogTitle).text = title
|
||||
val input = view.findViewById<EditText>(R.id.dialogInput).apply {
|
||||
this.hint = hint
|
||||
setText(content)
|
||||
requestFocus()
|
||||
}
|
||||
val dialog = AlertDialog.Builder(this).setView(view).create()
|
||||
view.findViewById<TextView>(R.id.dialogCancel).apply {
|
||||
text = cancelText
|
||||
setOnClickListener {
|
||||
dialog.dismiss()
|
||||
click?.onCancelClick()
|
||||
}
|
||||
}
|
||||
view.findViewById<TextView>(R.id.dialogConfirm).apply {
|
||||
text = confirmText
|
||||
setOnClickListener {
|
||||
val value = input.text.toString().trim()
|
||||
if (value.isEmpty()) {
|
||||
input.error = hint
|
||||
return@setOnClickListener
|
||||
}
|
||||
dialog.dismiss()
|
||||
click?.onConfirmClick(value)
|
||||
}
|
||||
}
|
||||
dialog.setCanceledOnTouchOutside(touchOutsideCancel)
|
||||
dialog.setOnShowListener { dialog.window?.setBackgroundDrawable(ColorDrawable(android.graphics.Color.TRANSPARENT)) }
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
/** 在屏幕顶部显示权限用途提示弹窗。 */
|
||||
fun showPermissionDialog(title: String, content: String) {
|
||||
permissionDialog?.dismiss()
|
||||
val view = layoutInflater.inflate(R.layout.permission_top_dialog, null)
|
||||
view.findViewById<TextView>(R.id.permissionTopTitle).text = title
|
||||
view.findViewById<TextView>(R.id.permissionTopContent).text = content
|
||||
permissionDialog = AlertDialog.Builder(this).setView(view).create().also { dialog ->
|
||||
dialog.setCanceledOnTouchOutside(false)
|
||||
dialog.show()
|
||||
dialog.window?.apply {
|
||||
setBackgroundDrawable(ColorDrawable(android.graphics.Color.TRANSPARENT))
|
||||
setGravity(Gravity.TOP)
|
||||
setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 在顶部显示可配置图标的权限用途提示弹窗。 */
|
||||
fun showPermissionDialog(icon: Drawable?, showIcon: Boolean, title: String, content: String) {
|
||||
showPermissionDialog(title, content)
|
||||
permissionDialog?.findViewById<ImageView>(R.id.permissionIcon)?.apply {
|
||||
visibility = if (showIcon) android.view.View.VISIBLE else android.view.View.GONE
|
||||
if (icon != null) setImageDrawable(icon)
|
||||
}
|
||||
}
|
||||
|
||||
/** 在系统权限申请框弹出前,显示自定义权限说明弹窗。 */
|
||||
private fun showPermissionConfirmDialog(title: String, content: String, onCancel: () -> Unit, onAllow: () -> Unit) {
|
||||
val view = layoutInflater.inflate(R.layout.permission_confirm_dialog, null)
|
||||
view.findViewById<TextView>(R.id.permissionTitle).text = title
|
||||
view.findViewById<TextView>(R.id.permissionContent).text = content
|
||||
val dialog = AlertDialog.Builder(this).setView(view).create()
|
||||
view.findViewById<TextView>(R.id.permissionCancel).setOnClickListener { dialog.dismiss(); onCancel() }
|
||||
view.findViewById<TextView>(R.id.permissionAllow).setOnClickListener { dialog.dismiss(); onAllow() }
|
||||
dialog.setOnShowListener { dialog.window?.setBackgroundDrawable(ColorDrawable(android.graphics.Color.TRANSPARENT)) }
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
/** 关闭并释放顶部权限提示弹窗。 */
|
||||
fun hidePermissionDialog() {
|
||||
permissionDialog?.dismiss()
|
||||
permissionDialog = null
|
||||
}
|
||||
|
||||
/** 打开当前应用的系统设置详情页。 */
|
||||
protected fun openAppPermissionSettings() {
|
||||
startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:$packageName")))
|
||||
}
|
||||
|
||||
|
||||
/** 页面销毁时释放 Presenter、弹窗和 ViewBinding。 */
|
||||
override fun onDestroy() {
|
||||
presenter?.onDestroy()
|
||||
presenter = null
|
||||
hideLoading()
|
||||
hidePermissionDialog()
|
||||
_binding = null
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.juhabi.birds.base
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.juhabi.birds.R
|
||||
import com.gyf.immersionbar.ImmersionBar
|
||||
|
||||
abstract class BaseActivityNoPresenter<VB : ViewBinding> : AppCompatActivity() {
|
||||
protected val mContext: BaseActivityNoPresenter<VB>
|
||||
get() = this
|
||||
|
||||
private var _binding: VB? = null
|
||||
protected val binding: VB get() = requireNotNull(_binding)
|
||||
|
||||
protected abstract fun createBinding(inflater: LayoutInflater): VB
|
||||
protected abstract fun initView(savedInstanceState: Bundle?)
|
||||
protected open fun initData(savedInstanceState: Bundle?) {
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
_binding = createBinding(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
initWindows()
|
||||
initView(savedInstanceState)
|
||||
initData(savedInstanceState)
|
||||
}
|
||||
|
||||
/** 初始化系统标题栏、状态栏和导航栏,子页面可以按需重写。 */
|
||||
protected open fun initWindows() {
|
||||
supportActionBar?.hide()
|
||||
ImmersionBar.with(this)
|
||||
.fitsSystemWindows(true)
|
||||
.statusBarColor(R.color.color_white)
|
||||
.statusBarDarkFont(true)
|
||||
.navigationBarColor(R.color.color_white)
|
||||
.navigationBarDarkIcon(true)
|
||||
.init()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
_binding = null
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
379
app/src/main/java/com/juhabi/birds/base/BaseFragment.kt
Normal file
379
app/src/main/java/com/juhabi/birds/base/BaseFragment.kt
Normal file
@@ -0,0 +1,379 @@
|
||||
package com.juhabi.birds.base
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.juhabi.birds.R
|
||||
import com.juhabi.birds.dialog.LoadingDialog
|
||||
|
||||
/**
|
||||
* Fragment 基类:统一管理 ViewBinding、Presenter 生命周期、弹窗和动态权限。
|
||||
*/
|
||||
abstract class BaseFragment<VB : ViewBinding, P : IPresenter<*>> : Fragment(), IView {
|
||||
protected val mContext: FragmentActivity
|
||||
get() = requireActivity()
|
||||
|
||||
private var _binding: VB? = null
|
||||
protected val binding: VB
|
||||
get() = requireNotNull(_binding) { "Binding is unavailable outside the Fragment view lifecycle" }
|
||||
|
||||
protected var presenter: P? = null
|
||||
private set
|
||||
private var loadingDialog: LoadingDialog? = null
|
||||
private var permissionDialog: AlertDialog? = null
|
||||
private val permissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestMultiplePermissions()
|
||||
) { result ->
|
||||
hidePermissionDialog()
|
||||
val denied = result.filterValues { !it }.keys.toTypedArray()
|
||||
if (denied.isEmpty()) {
|
||||
onPermissionsGranted(result.keys.toTypedArray())
|
||||
} else {
|
||||
val permanentlyDenied = denied.all { !shouldShowRequestPermissionRationale(it) }
|
||||
onPermissionsDenied(denied, permanentlyDenied)
|
||||
}
|
||||
}
|
||||
|
||||
/** 创建当前页面的 ViewBinding。 */
|
||||
protected abstract fun createBinding(inflater: LayoutInflater, container: ViewGroup?): VB
|
||||
/** 创建 Presenter;不需要 Presenter 时可以返回 null。 */
|
||||
protected abstract fun createPresenter(): P?
|
||||
/** Fragment View 创建完成后,在这里设置控件状态和监听事件。 */
|
||||
protected abstract fun initView(savedInstanceState: Bundle?)
|
||||
/** 初始化页面数据,子类可按需重写。 */
|
||||
protected open fun initData(savedInstanceState: Bundle?) {
|
||||
}
|
||||
|
||||
/** 创建 Fragment 视图并初始化 ViewBinding。 */
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, state: Bundle?): View {
|
||||
_binding = createBinding(inflater, container)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
/** View 创建完成后依次初始化 Presenter、控件和页面数据。 */
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
initWindows()
|
||||
presenter = createPresenter().also { it?.attachBaseView(this) }
|
||||
initView(savedInstanceState)
|
||||
initData(savedInstanceState)
|
||||
}
|
||||
|
||||
/** Fragment 显示时配置宿主 Activity 的状态栏和导航栏,子页面可以按需重写。 */
|
||||
protected open fun initWindows() {
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun P.attachBaseView(view: IView) {
|
||||
(this as IPresenter<IView>).attachView(view)
|
||||
}
|
||||
|
||||
/** 实现 IView,显示默认加载弹窗。 */
|
||||
override fun showLoading() {
|
||||
showLoadingDialog()
|
||||
}
|
||||
|
||||
/** 实现 IView,隐藏默认加载弹窗。 */
|
||||
override fun hideLoading() {
|
||||
hideLoadingDialog()
|
||||
}
|
||||
|
||||
/** 在 Fragment 已附加时显示加载弹窗。 */
|
||||
fun showLoadingDialog(message: String = "加载中...") {
|
||||
val currentContext = context ?: return
|
||||
if (!isAdded || loadingDialog?.isShowing == true) return
|
||||
val dialog = LoadingDialog(currentContext, message).apply {
|
||||
setCanceledOnTouchOutside(false)
|
||||
setCancelable(true)
|
||||
}
|
||||
try {
|
||||
dialog.show()
|
||||
loadingDialog = dialog
|
||||
} catch (_: Exception) {
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
/** 关闭并释放加载弹窗。 */
|
||||
fun hideLoadingDialog() {
|
||||
loadingDialog?.dismiss()
|
||||
loadingDialog = null
|
||||
}
|
||||
|
||||
/** 使用 Toast 显示短消息。 */
|
||||
override fun showMessage(message: String) {
|
||||
context?.let { Toast.makeText(it, message, Toast.LENGTH_SHORT).show() }
|
||||
}
|
||||
|
||||
/** 使用普通消息样式显示错误信息。 */
|
||||
override fun showError(message: String) {
|
||||
showMessage(message)
|
||||
}
|
||||
|
||||
/** 从当前 Fragment 跳转到指定 Activity,可携带 Bundle 参数。 */
|
||||
protected fun goTo(to: Class<out Activity>, data: Bundle? = null) {
|
||||
startActivity(createActivityIntent(to, data))
|
||||
}
|
||||
|
||||
/** 兼容 Water 项目命名,跳转到不继承当前 BaseActivity 的普通 Activity。 */
|
||||
protected fun goToWihoutNoBase(to: Class<out Activity>, data: Bundle? = null) {
|
||||
goTo(to, data)
|
||||
}
|
||||
|
||||
/** 兼容 Water 项目命名,跳转到不使用 Presenter 的 Activity。 */
|
||||
protected fun goToWihoutNoPresent(to: Class<out Activity>, data: Bundle? = null) {
|
||||
goTo(to, data)
|
||||
}
|
||||
|
||||
/** 跳转到目标页面并关闭 Fragment 所在的 Activity。 */
|
||||
protected fun goToAndFinish(to: Class<out Activity>, data: Bundle? = null) {
|
||||
goTo(to, data)
|
||||
requireActivity().finish()
|
||||
}
|
||||
|
||||
/** 清空现有任务栈并以目标 Activity 作为新的根页面。 */
|
||||
protected fun goToClearTask(to: Class<out Activity>, data: Bundle? = null) {
|
||||
val intent = createActivityIntent(to, data).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
/** 使用 Activity Result API 跳转,并由传入的 Launcher 接收返回结果。 */
|
||||
protected fun goToForResult(
|
||||
to: Class<out Activity>,
|
||||
launcher: ActivityResultLauncher<Intent>,
|
||||
data: Bundle? = null
|
||||
) {
|
||||
launcher.launch(createActivityIntent(to, data))
|
||||
}
|
||||
|
||||
/** 创建统一的 Activity 跳转 Intent,并按需写入 Bundle 参数。 */
|
||||
private fun createActivityIntent(to: Class<out Activity>, data: Bundle?): Intent {
|
||||
return Intent(requireContext(), to).apply {
|
||||
data?.let(::putExtras)
|
||||
}
|
||||
}
|
||||
|
||||
/** 检查单个权限是否已授权。 */
|
||||
fun hasRecordPermission(permission: String): Boolean {
|
||||
return hasRecordPermission(arrayOf(permission))
|
||||
}
|
||||
/** 检查一组权限是否已全部授权。 */
|
||||
fun hasRecordPermission(permissions: Array<String>): Boolean {
|
||||
return permissions.all {
|
||||
ContextCompat.checkSelfPermission(requireContext(), it) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
}
|
||||
|
||||
/** 显示说明弹窗并申请单个权限。 */
|
||||
fun getSinglePermissionRequst(permission: String, title: String, content: String, requestCode: Int) {
|
||||
getMorePermissionRequst(arrayOf(permission), title, content, requestCode)
|
||||
}
|
||||
|
||||
/** 显示说明弹窗并一次申请多个权限。 */
|
||||
fun getMorePermissionRequst(permissions: Array<String>, title: String, content: String, requestCode: Int) {
|
||||
if (hasRecordPermission(permissions)) {
|
||||
onPermissionsGranted(permissions)
|
||||
return
|
||||
}
|
||||
showPermissionConfirmDialog(
|
||||
title,
|
||||
content,
|
||||
onCancel = { onPermissionsDenied(permissions, false) },
|
||||
onAllow = {
|
||||
showPermissionDialog(title, content)
|
||||
permissionLauncher.launch(permissions)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/** 使用默认说明文案申请一个或多个权限。 */
|
||||
protected open fun requestPermission(vararg permissions: String) {
|
||||
getMorePermissionRequst(permissions.toList().toTypedArray(), "需要权限", "请允许应用使用相关权限", 0)
|
||||
}
|
||||
|
||||
/** 权限被拒绝后的兼容回调,子类可重写。 */
|
||||
protected open fun noAllowPermission() {
|
||||
}
|
||||
/** 权限全部通过后的回调,默认继续执行下一步。 */
|
||||
protected open fun onPermissionsGranted(permissions: Array<String>) {
|
||||
nextStep()
|
||||
}
|
||||
/** 权限被拒绝后的回调,永久拒绝时默认引导用户进入设置页。 */
|
||||
protected open fun onPermissionsDenied(permissions: Array<String>, permanentlyDenied: Boolean) {
|
||||
noAllowPermission()
|
||||
if (permanentlyDenied && isAdded) {
|
||||
AlertDialog.Builder(requireContext()).setTitle("权限被拒绝")
|
||||
.setMessage("请在应用设置中开启所需权限")
|
||||
.setNegativeButton("取消", null)
|
||||
.setPositiveButton("去设置") { _, _ -> openAppPermissionSettings() }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
/** 权限通过后需要继续执行的业务步骤。 */
|
||||
protected open fun nextStep() {
|
||||
}
|
||||
|
||||
/** 普通确认弹窗和输入弹窗共用的点击回调。 */
|
||||
interface OnDialogClick {
|
||||
/** 点击取消按钮。 */
|
||||
fun onCancelClick() {
|
||||
}
|
||||
/** 点击普通弹窗的确定按钮。 */
|
||||
fun onConfirmClick() {
|
||||
}
|
||||
/** 点击输入弹窗的确定按钮,并返回输入内容。 */
|
||||
fun onConfirmClick(content: String) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 显示带取消和确定按钮的普通弹窗。 */
|
||||
fun showDialogtext(
|
||||
title: String,
|
||||
content: String,
|
||||
okText: String = "确定",
|
||||
cancelText: String = "取消",
|
||||
click: OnDialogClick? = null,
|
||||
touchOutsideCancel: Boolean = true
|
||||
) {
|
||||
val view = layoutInflater.inflate(R.layout.permission_confirm_dialog, null)
|
||||
view.findViewById<TextView>(R.id.permissionTitle).text = title
|
||||
view.findViewById<TextView>(R.id.permissionContent).text = content
|
||||
val dialog = AlertDialog.Builder(requireContext()).setView(view).create()
|
||||
view.findViewById<TextView>(R.id.permissionCancel).apply {
|
||||
text = cancelText
|
||||
setOnClickListener {
|
||||
dialog.dismiss()
|
||||
click?.onCancelClick()
|
||||
}
|
||||
}
|
||||
view.findViewById<TextView>(R.id.permissionAllow).apply {
|
||||
text = okText
|
||||
setOnClickListener {
|
||||
dialog.dismiss()
|
||||
click?.onConfirmClick()
|
||||
}
|
||||
}
|
||||
dialog.setCanceledOnTouchOutside(touchOutsideCancel)
|
||||
dialog.setOnShowListener { dialog.window?.setBackgroundDrawable(ColorDrawable(android.graphics.Color.TRANSPARENT)) }
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
/** 显示带输入框的弹窗,输入为空时保留弹窗并提示。 */
|
||||
fun showEditDialog(
|
||||
title: String,
|
||||
hint: String,
|
||||
content: String = "",
|
||||
touchOutsideCancel: Boolean = true,
|
||||
cancelText: String = "取消",
|
||||
confirmText: String = "确定",
|
||||
click: OnDialogClick? = null
|
||||
) {
|
||||
val view = layoutInflater.inflate(R.layout.dialog_input_text, null)
|
||||
view.findViewById<TextView>(R.id.dialogTitle).text = title
|
||||
val input = view.findViewById<EditText>(R.id.dialogInput).apply {
|
||||
this.hint = hint
|
||||
setText(content)
|
||||
requestFocus()
|
||||
}
|
||||
val dialog = AlertDialog.Builder(requireContext()).setView(view).create()
|
||||
view.findViewById<TextView>(R.id.dialogCancel).apply {
|
||||
text = cancelText
|
||||
setOnClickListener {
|
||||
dialog.dismiss()
|
||||
click?.onCancelClick()
|
||||
}
|
||||
}
|
||||
view.findViewById<TextView>(R.id.dialogConfirm).apply {
|
||||
text = confirmText
|
||||
setOnClickListener {
|
||||
val value = input.text.toString().trim()
|
||||
if (value.isEmpty()) {
|
||||
input.error = hint
|
||||
return@setOnClickListener
|
||||
}
|
||||
dialog.dismiss()
|
||||
click?.onConfirmClick(value)
|
||||
}
|
||||
}
|
||||
dialog.setCanceledOnTouchOutside(touchOutsideCancel)
|
||||
dialog.setOnShowListener { dialog.window?.setBackgroundDrawable(ColorDrawable(android.graphics.Color.TRANSPARENT)) }
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
/** 在屏幕顶部显示权限用途提示弹窗。 */
|
||||
fun showPermissionDialog(title: String, content: String) {
|
||||
permissionDialog?.dismiss()
|
||||
val view = layoutInflater.inflate(R.layout.permission_top_dialog, null)
|
||||
view.findViewById<TextView>(R.id.permissionTopTitle).text = title
|
||||
view.findViewById<TextView>(R.id.permissionTopContent).text = content
|
||||
permissionDialog = AlertDialog.Builder(requireContext()).setView(view).create().also {
|
||||
it.setCanceledOnTouchOutside(false)
|
||||
it.show()
|
||||
it.window?.apply {
|
||||
setBackgroundDrawable(ColorDrawable(android.graphics.Color.TRANSPARENT))
|
||||
setGravity(android.view.Gravity.TOP)
|
||||
setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
}
|
||||
}
|
||||
/** 在顶部显示可配置图标的权限用途提示弹窗。 */
|
||||
fun showPermissionDialog(icon: Drawable?, showIcon: Boolean, title: String, content: String) {
|
||||
showPermissionDialog(title, content)
|
||||
permissionDialog?.findViewById<ImageView>(R.id.permissionIcon)?.apply {
|
||||
visibility = if (showIcon) View.VISIBLE else View.GONE
|
||||
if (icon != null) setImageDrawable(icon)
|
||||
}
|
||||
}
|
||||
/** 在系统权限申请框弹出前,显示自定义权限说明弹窗。 */
|
||||
private fun showPermissionConfirmDialog(title: String, content: String, onCancel: () -> Unit, onAllow: () -> Unit) {
|
||||
val view = layoutInflater.inflate(R.layout.permission_confirm_dialog, null)
|
||||
view.findViewById<TextView>(R.id.permissionTitle).text = title
|
||||
view.findViewById<TextView>(R.id.permissionContent).text = content
|
||||
val dialog = AlertDialog.Builder(requireContext()).setView(view).create()
|
||||
view.findViewById<TextView>(R.id.permissionCancel).setOnClickListener { dialog.dismiss(); onCancel() }
|
||||
view.findViewById<TextView>(R.id.permissionAllow).setOnClickListener { dialog.dismiss(); onAllow() }
|
||||
dialog.setOnShowListener { dialog.window?.setBackgroundDrawable(ColorDrawable(android.graphics.Color.TRANSPARENT)) }
|
||||
dialog.show()
|
||||
}
|
||||
/** 关闭并释放顶部权限提示弹窗。 */
|
||||
fun hidePermissionDialog() {
|
||||
permissionDialog?.dismiss()
|
||||
permissionDialog = null
|
||||
}
|
||||
/** 打开当前应用的系统设置详情页。 */
|
||||
protected fun openAppPermissionSettings() {
|
||||
startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:${requireContext().packageName}")))
|
||||
}
|
||||
|
||||
/** Fragment View 销毁时释放 Presenter、弹窗和 ViewBinding。 */
|
||||
override fun onDestroyView() {
|
||||
presenter?.onDestroy()
|
||||
presenter = null
|
||||
hideLoading()
|
||||
hidePermissionDialog()
|
||||
_binding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.juhabi.birds.base
|
||||
|
||||
import androidx.viewbinding.ViewBinding
|
||||
|
||||
/**
|
||||
* 不需要 Presenter 的 Fragment 基类。
|
||||
* 子类仍可复用 BaseFragment 提供的 ViewBinding、加载弹窗、权限和页面跳转能力。
|
||||
*/
|
||||
abstract class BaseFragmentNoPresenter<VB : ViewBinding> :
|
||||
BaseFragment<VB, BasePresenter<IView>>() {
|
||||
|
||||
final override fun createPresenter(): BasePresenter<IView>? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun showError(message: String) {
|
||||
showMessage(message)
|
||||
}
|
||||
}
|
||||
18
app/src/main/java/com/juhabi/birds/base/BasePresenter.kt
Normal file
18
app/src/main/java/com/juhabi/birds/base/BasePresenter.kt
Normal file
@@ -0,0 +1,18 @@
|
||||
package com.juhabi.birds.base
|
||||
|
||||
abstract class BasePresenter<V : IView> : IPresenter<V> {
|
||||
final override var view: V? = null
|
||||
private set
|
||||
|
||||
final override fun attachView(view: V) {
|
||||
this.view = view
|
||||
onStart()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
view = null
|
||||
}
|
||||
}
|
||||
8
app/src/main/java/com/juhabi/birds/base/IPresenter.kt
Normal file
8
app/src/main/java/com/juhabi/birds/base/IPresenter.kt
Normal file
@@ -0,0 +1,8 @@
|
||||
package com.juhabi.birds.base
|
||||
|
||||
interface IPresenter<V : IView> {
|
||||
val view: V?
|
||||
fun attachView(view: V)
|
||||
fun onStart()
|
||||
fun onDestroy()
|
||||
}
|
||||
11
app/src/main/java/com/juhabi/birds/base/IView.kt
Normal file
11
app/src/main/java/com/juhabi/birds/base/IView.kt
Normal file
@@ -0,0 +1,11 @@
|
||||
package com.juhabi.birds.base
|
||||
|
||||
interface IView {
|
||||
fun showLoading()
|
||||
fun hideLoading()
|
||||
fun showMessage(message: String)
|
||||
fun showError(message: String)
|
||||
fun handleError(error: Throwable) {
|
||||
showError(error.message ?: "Unknown error")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package com.juhabi.birds.component.picker
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.bigkoo.pickerview.builder.TimePickerBuilder
|
||||
import com.juhabi.birds.R
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* 日期时间选择器统一入口。
|
||||
*
|
||||
* 组件基于 Android-PickerView 封装,支持日期、时间、日期时间三种模式。
|
||||
* 页面只需要配置模式和监听器,不需要重复设置控件颜色、单位及格式化规则。
|
||||
*/
|
||||
class DateTimePicker private constructor(
|
||||
private val context: Context,
|
||||
private val mode: Mode,
|
||||
private val selectedTime: Calendar,
|
||||
private val startTime: Calendar?,
|
||||
private val endTime: Calendar?,
|
||||
private val onSelected: (Result) -> Unit
|
||||
) {
|
||||
|
||||
/** 显示选择器。 */
|
||||
fun show() {
|
||||
val pickerBuilder = TimePickerBuilder(context) { date, _ ->
|
||||
onSelected(
|
||||
Result(
|
||||
date = date,
|
||||
timestamp = date.time,
|
||||
formattedText = mode.format(date)
|
||||
)
|
||||
)
|
||||
}
|
||||
.setType(mode.visibleItems)
|
||||
.setDate(selectedTime)
|
||||
.setTitleText(mode.title)
|
||||
.setSubmitText("确定")
|
||||
.setCancelText("取消")
|
||||
.setLabel("年", "月", "日", "时", "分", "秒")
|
||||
.setSubmitColor(ContextCompat.getColor(context, R.color.bottom_nav_selected))
|
||||
.setCancelColor(ContextCompat.getColor(context, R.color.bottom_nav_unselected))
|
||||
.setTextColorCenter(ContextCompat.getColor(context, R.color.bottom_nav_selected))
|
||||
.isCenterLabel(false)
|
||||
|
||||
if (startTime != null && endTime != null) {
|
||||
pickerBuilder.setRangDate(startTime, endTime)
|
||||
}
|
||||
|
||||
pickerBuilder.build().show()
|
||||
}
|
||||
|
||||
/** 选择器展示模式,以及各模式对应的显示列和返回格式。 */
|
||||
enum class Mode(
|
||||
internal val visibleItems: BooleanArray,
|
||||
internal val pattern: String,
|
||||
internal val title: String
|
||||
) {
|
||||
DATE(
|
||||
booleanArrayOf(true, true, true, false, false, false),
|
||||
"yyyy-MM-dd",
|
||||
"选择日期"
|
||||
),
|
||||
TIME(
|
||||
booleanArrayOf(false, false, false, true, true, true),
|
||||
"HH:mm:ss",
|
||||
"选择时间"
|
||||
),
|
||||
DATE_TIME(
|
||||
booleanArrayOf(true, true, true, true, true, true),
|
||||
"yyyy-MM-dd HH:mm:ss",
|
||||
"选择日期时间"
|
||||
);
|
||||
|
||||
internal fun format(date: Date): String {
|
||||
return SimpleDateFormat(pattern, Locale.getDefault()).format(date)
|
||||
}
|
||||
}
|
||||
|
||||
/** 页面接收到的统一选择结果。 */
|
||||
data class Result(
|
||||
val date: Date,
|
||||
val timestamp: Long,
|
||||
val formattedText: String
|
||||
)
|
||||
|
||||
/**
|
||||
* 选择器构建器。
|
||||
*
|
||||
* 默认选中当前时间;只有开始和结束时间都设置时,时间范围才会生效。
|
||||
*/
|
||||
class Builder(private val context: Context) {
|
||||
private var mode: Mode = Mode.DATE_TIME
|
||||
private var selectedTime: Calendar = Calendar.getInstance()
|
||||
private var startTime: Calendar? = null
|
||||
private var endTime: Calendar? = null
|
||||
private var onSelected: ((Result) -> Unit)? = null
|
||||
|
||||
fun setMode(mode: Mode): Builder {
|
||||
return apply {
|
||||
this.mode = mode
|
||||
}
|
||||
}
|
||||
|
||||
fun setSelectedTime(timestamp: Long): Builder {
|
||||
return apply {
|
||||
selectedTime = Calendar.getInstance().apply { timeInMillis = timestamp }
|
||||
}
|
||||
}
|
||||
|
||||
fun setTimeRange(startTimeInMillis: Long, endTimeInMillis: Long): Builder {
|
||||
return apply {
|
||||
require(startTimeInMillis <= endTimeInMillis) { "开始时间不能晚于结束时间" }
|
||||
startTime = Calendar.getInstance().apply { timeInMillis = startTimeInMillis }
|
||||
endTime = Calendar.getInstance().apply { timeInMillis = endTimeInMillis }
|
||||
}
|
||||
}
|
||||
|
||||
fun setOnSelectedListener(listener: (Result) -> Unit): Builder {
|
||||
return apply {
|
||||
onSelected = listener
|
||||
}
|
||||
}
|
||||
|
||||
fun build(): DateTimePicker {
|
||||
val listener = requireNotNull(onSelected) { "必须设置选择结果监听器" }
|
||||
val effectiveSelectedTime = selectedTime.clone() as Calendar
|
||||
startTime?.let { start ->
|
||||
if (effectiveSelectedTime.before(start)) effectiveSelectedTime.timeInMillis = start.timeInMillis
|
||||
}
|
||||
endTime?.let { end ->
|
||||
if (effectiveSelectedTime.after(end)) effectiveSelectedTime.timeInMillis = end.timeInMillis
|
||||
}
|
||||
return DateTimePicker(
|
||||
context = context,
|
||||
mode = mode,
|
||||
selectedTime = effectiveSelectedTime,
|
||||
startTime = startTime,
|
||||
endTime = endTime,
|
||||
onSelected = listener
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.juhabi.birds.demo.constraint
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import com.juhabi.birds.base.BaseActivityNoPresenter
|
||||
import com.juhabi.birds.databinding.ActivityConstraintLayoutDemoBinding
|
||||
|
||||
/**
|
||||
* ConstraintLayout 综合示例页。
|
||||
* 布局知识点集中写在 activity_constraint_layout_demo.xml 中,方便边看页面边读代码。
|
||||
*/
|
||||
class ConstraintLayoutDemoActivity : BaseActivityNoPresenter<ActivityConstraintLayoutDemoBinding>() {
|
||||
|
||||
private var detailsVisible = true
|
||||
|
||||
override fun createBinding(inflater: LayoutInflater): ActivityConstraintLayoutDemoBinding {
|
||||
return ActivityConstraintLayoutDemoBinding.inflate(inflater)
|
||||
}
|
||||
|
||||
override fun initView(savedInstanceState: Bundle?) {
|
||||
binding.backButton.setOnClickListener { finish() }
|
||||
binding.toggleDetailsButton.setOnClickListener {
|
||||
detailsVisible = !detailsVisible
|
||||
binding.detailGroup.visibility = if (detailsVisible) View.VISIBLE else View.GONE
|
||||
binding.toggleDetailsButton.text = if (detailsVisible) "隐藏详情" else "显示详情"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.juhabi.birds.demo.recycler
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.juhabi.birds.R
|
||||
import com.juhabi.birds.base.BaseActivityNoPresenter
|
||||
import com.juhabi.birds.databinding.ActivityRecyclerViewDemoBinding
|
||||
import com.juhabi.birds.demo.recycler.adapter.RecyclerDemoAdapter
|
||||
import com.juhabi.birds.demo.recycler.model.RecyclerDemoItem
|
||||
|
||||
/** RecyclerView 与 BaseQuickAdapter 的基础调用示例页。 */
|
||||
class RecyclerViewDemoActivity : BaseActivityNoPresenter<ActivityRecyclerViewDemoBinding>() {
|
||||
|
||||
override fun createBinding(inflater: LayoutInflater): ActivityRecyclerViewDemoBinding {
|
||||
return ActivityRecyclerViewDemoBinding.inflate(inflater)
|
||||
}
|
||||
|
||||
override fun initView(savedInstanceState: Bundle?) {
|
||||
title = getString(R.string.recycler_demo_title)
|
||||
|
||||
val adapter = RecyclerDemoAdapter(this, R.layout.item_recycler_demo, createDemoData())
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
binding.recyclerView.adapter = adapter
|
||||
|
||||
// 列表点击事件由页面处理,适配器只负责展示数据。
|
||||
adapter.setOnItemClickListener { _, _, position ->
|
||||
showSelectedItem(adapter.data[position])
|
||||
}
|
||||
|
||||
adapter.setOnItemChildClickListener { _, view, position ->
|
||||
val item = adapter.data[position]
|
||||
val message = when (view.id) {
|
||||
R.id.titleTextView -> "点击了标题:${item.title}"
|
||||
R.id.idTextView -> "点击了编号:${item.id}"
|
||||
R.id.statusTextView -> "点击了状态:${item.status}"
|
||||
R.id.timeTextView -> "点击了更新时间:${item.updateTime}"
|
||||
else -> return@setOnItemChildClickListener
|
||||
}
|
||||
android.widget.Toast.makeText(this, message, android.widget.Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSelectedItem(item: RecyclerDemoItem) {
|
||||
android.widget.Toast.makeText(this, "点击了:${item.title}", android.widget.Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
/** 创建固定演示数据,接入接口后可直接替换为服务端返回的列表。 */
|
||||
private fun createDemoData(): List<RecyclerDemoItem> {
|
||||
return listOf(
|
||||
RecyclerDemoItem(1001, "设备巡检记录", "已完成", "2026-07-23 09:30:00"),
|
||||
RecyclerDemoItem(1002, "自动浇水计划", "执行中", "2026-07-23 10:15:20"),
|
||||
RecyclerDemoItem(1003, "传感器数据同步", "待处理", "2026-07-23 11:08:45"),
|
||||
RecyclerDemoItem(1004, "系统配置备份", "已完成", "2026-07-22 18:42:10"),
|
||||
RecyclerDemoItem(1005, "固件升级任务", "失败", "2026-07-22 16:20:35"),
|
||||
RecyclerDemoItem(1006, "用户操作日志", "已完成", "2026-07-22 14:05:00")
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.juhabi.birds.demo.recycler.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.chad.library.adapter.base.BaseQuickAdapter;
|
||||
import com.chad.library.adapter.base.viewholder.BaseViewHolder;
|
||||
import com.juhabi.birds.R;
|
||||
import com.juhabi.birds.demo.recycler.model.RecyclerDemoItem;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* BaseQuickAdapter 使用示例。
|
||||
* convert() 只负责把一条数据绑定到对应的 item 控件。
|
||||
*/
|
||||
public class RecyclerDemoAdapter extends BaseQuickAdapter<RecyclerDemoItem, BaseViewHolder> {
|
||||
private final Context mContext;
|
||||
|
||||
public RecyclerDemoAdapter(Context context, int layoutResId, @Nullable List<RecyclerDemoItem> data) {
|
||||
super(layoutResId, data);
|
||||
this.mContext = context;
|
||||
addChildClickViewIds(
|
||||
R.id.titleTextView,
|
||||
R.id.idTextView,
|
||||
R.id.statusTextView,
|
||||
R.id.timeTextView
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void convert(@NonNull BaseViewHolder holder, RecyclerDemoItem item) {
|
||||
holder.setText(R.id.titleTextView, item.getTitle());
|
||||
holder.setText(R.id.idTextView, mContext.getString(R.string.recycler_item_id, item.getId()));
|
||||
holder.setText(R.id.statusTextView, mContext.getString(R.string.recycler_item_status, item.getStatus()));
|
||||
holder.setText(
|
||||
R.id.timeTextView,
|
||||
mContext.getString(R.string.recycler_item_update_time, item.getUpdateTime())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.juhabi.birds.demo.recycler.model
|
||||
|
||||
/** RecyclerView 示例页使用的简单数据模型。 */
|
||||
data class RecyclerDemoItem(
|
||||
val id: Long,
|
||||
val title: String,
|
||||
val status: String,
|
||||
val updateTime: String
|
||||
)
|
||||
41
app/src/main/java/com/juhabi/birds/dialog/LoadingDialog.kt
Normal file
41
app/src/main/java/com/juhabi/birds/dialog/LoadingDialog.kt
Normal file
@@ -0,0 +1,41 @@
|
||||
package com.juhabi.birds.dialog
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import android.widget.TextView
|
||||
import com.juhabi.birds.R
|
||||
|
||||
class LoadingDialog(
|
||||
context: Context,
|
||||
private val message: String = "加载中..."
|
||||
) : Dialog(context, R.style.LoadingDialogTheme) {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
setContentView(R.layout.dialog_loading)
|
||||
findViewById<TextView>(R.id.loadingText).text = message
|
||||
window?.apply {
|
||||
// 参考 Water:窗口全屏、内容居中,并为页面增加轻量半透明遮罩。
|
||||
setBackgroundDrawable(ColorDrawable(Color.argb(13, 0, 0, 0)))
|
||||
setGravity(Gravity.CENTER)
|
||||
setDimAmount(0.2f)
|
||||
addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
||||
setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT)
|
||||
}
|
||||
}
|
||||
|
||||
override fun show() {
|
||||
super.show()
|
||||
window?.apply {
|
||||
setGravity(Gravity.CENTER)
|
||||
setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT)
|
||||
}
|
||||
}
|
||||
}
|
||||
71
app/src/main/java/com/juhabi/birds/login/LoginActivity.kt
Normal file
71
app/src/main/java/com/juhabi/birds/login/LoginActivity.kt
Normal file
@@ -0,0 +1,71 @@
|
||||
package com.juhabi.birds.login
|
||||
|
||||
import android.os.Bundle
|
||||
import android.content.Intent
|
||||
import android.view.LayoutInflater
|
||||
import com.juhabi.birds.base.BaseActivity
|
||||
import com.juhabi.birds.databinding.ActivityLoginBinding
|
||||
import com.juhabi.birds.login.model.LoginUser
|
||||
import com.juhabi.birds.login.mvp.LoginContract
|
||||
import com.juhabi.birds.login.mvp.LoginImpl
|
||||
import com.juhabi.birds.login.mvp.LoginPresenter
|
||||
import com.juhabi.birds.network.TokenManager
|
||||
import com.juhabi.birds.mine.MineCenterActivity
|
||||
import com.juhabi.birds.util.ToastUtil
|
||||
|
||||
class LoginActivity : BaseActivity<ActivityLoginBinding, LoginPresenter>(),
|
||||
LoginContract.LoginView {
|
||||
|
||||
override fun createBinding(inflater: LayoutInflater): ActivityLoginBinding {
|
||||
return ActivityLoginBinding.inflate(inflater)
|
||||
}
|
||||
|
||||
override fun createPresenter(): LoginPresenter {
|
||||
return LoginPresenter(LoginImpl())
|
||||
}
|
||||
|
||||
override fun initView(savedInstanceState: Bundle?) {
|
||||
// if (TokenManager.get(this).isNotBlank()) {
|
||||
// startActivity(Intent(this, MineCenterActivity::class.java))
|
||||
// finish()
|
||||
// return
|
||||
// }
|
||||
if(binding.usernameEditText.text.toString().isNotBlank()){
|
||||
ToastUtil.showShort(mContext,"");
|
||||
return
|
||||
}
|
||||
binding.loginButton.setOnClickListener {
|
||||
presenter?.login(
|
||||
binding.usernameEditText.text?.toString().orEmpty(),
|
||||
binding.passwordEditText.text?.toString().orEmpty()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun showUsernameError(message: String) {
|
||||
binding.usernameLayout.error = message
|
||||
binding.usernameEditText.requestFocus()
|
||||
}
|
||||
|
||||
override fun showPasswordError(message: String) {
|
||||
binding.passwordLayout.error = message
|
||||
binding.passwordEditText.requestFocus()
|
||||
}
|
||||
|
||||
override fun clearInputErrors() {
|
||||
binding.usernameLayout.error = null
|
||||
binding.passwordLayout.error = null
|
||||
}
|
||||
|
||||
override fun onLoginSuccess(user: LoginUser) {
|
||||
TokenManager.save(this, user.accessToken)
|
||||
binding.resultTextView.text = "登录成功,Token 已保存"
|
||||
showMessage("登录成功")
|
||||
startActivity(Intent(this, MineCenterActivity::class.java))
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun showError(message: String) {
|
||||
ToastUtil.showShort(mContext,message)
|
||||
}
|
||||
}
|
||||
18
app/src/main/java/com/juhabi/birds/login/model/LoginUser.kt
Normal file
18
app/src/main/java/com/juhabi/birds/login/model/LoginUser.kt
Normal file
@@ -0,0 +1,18 @@
|
||||
package com.juhabi.birds.login.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class LoginUser(
|
||||
val scope: Any? = null,
|
||||
val openid: Any? = null,
|
||||
@SerializedName("access_token")
|
||||
val accessToken: String = "",
|
||||
@SerializedName("refresh_token")
|
||||
val refreshToken: Any? = null,
|
||||
@SerializedName("expire_in")
|
||||
val expireIn: Long = 0,
|
||||
@SerializedName("refresh_expire_in")
|
||||
val refreshExpireIn: Any? = null,
|
||||
@SerializedName("client_id")
|
||||
val clientId: String = ""
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.juhabi.birds.login.mvp
|
||||
|
||||
import com.juhabi.birds.base.IView
|
||||
import com.juhabi.birds.login.model.LoginUser
|
||||
import com.juhabi.birds.network.BaseBean
|
||||
import org.json.JSONObject
|
||||
|
||||
interface LoginContract {
|
||||
interface Callback<T> {
|
||||
fun success(data: T)
|
||||
fun fail(message: String)
|
||||
}
|
||||
|
||||
interface DataModel {
|
||||
fun login(json: JSONObject, callback: Callback<BaseBean<LoginUser>>)
|
||||
fun cancel()
|
||||
}
|
||||
|
||||
interface LoginView : IView {
|
||||
fun showUsernameError(message: String)
|
||||
fun showPasswordError(message: String)
|
||||
fun clearInputErrors()
|
||||
fun onLoginSuccess(user: LoginUser)
|
||||
}
|
||||
|
||||
}
|
||||
57
app/src/main/java/com/juhabi/birds/login/mvp/LoginImpl.kt
Normal file
57
app/src/main/java/com/juhabi/birds/login/mvp/LoginImpl.kt
Normal file
@@ -0,0 +1,57 @@
|
||||
package com.juhabi.birds.login.mvp
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import com.juhabi.birds.login.model.LoginUser
|
||||
import com.juhabi.birds.network.ApiClient
|
||||
import com.juhabi.birds.network.BaseBean
|
||||
import com.juhabi.birds.network.NetworkManager
|
||||
import org.json.JSONObject
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
|
||||
class LoginImpl : LoginContract.DataModel {
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
private var loginCall: Call<BaseBean<LoginUser>>? = null
|
||||
|
||||
override fun login(
|
||||
json: JSONObject,
|
||||
callback: LoginContract.Callback<BaseBean<LoginUser>>
|
||||
) {
|
||||
loginCall?.cancel()
|
||||
loginCall = ApiClient.apiService
|
||||
.login(NetworkManager.getToken(), NetworkManager.toRequestBody(json))
|
||||
.also { call ->
|
||||
call.enqueue(object : Callback<BaseBean<LoginUser>> {
|
||||
override fun onResponse(
|
||||
call: Call<BaseBean<LoginUser>>,
|
||||
response: Response<BaseBean<LoginUser>>
|
||||
) {
|
||||
mainHandler.post {
|
||||
val bean = response.body()
|
||||
if (response.isSuccessful && bean != null) {
|
||||
callback.success(bean)
|
||||
} else {
|
||||
callback.fail("接口响应异常:${response.code()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<BaseBean<LoginUser>>, error: Throwable) {
|
||||
if (!call.isCanceled) {
|
||||
mainHandler.post {
|
||||
callback.fail(error.message ?: "网络请求失败")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
loginCall?.cancel()
|
||||
loginCall = null
|
||||
mainHandler.removeCallbacksAndMessages(null)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.juhabi.birds.login.mvp
|
||||
|
||||
import com.juhabi.birds.base.BasePresenter
|
||||
import com.juhabi.birds.AppConfig
|
||||
import com.juhabi.birds.login.model.LoginUser
|
||||
import com.juhabi.birds.network.BaseBean
|
||||
import org.json.JSONObject
|
||||
|
||||
class LoginPresenter(private val loginImpl: LoginImpl) : BasePresenter<LoginContract.LoginView>() {
|
||||
|
||||
fun login(username: String, password: String) {
|
||||
val currentView = view ?: return
|
||||
currentView.clearInputErrors()
|
||||
|
||||
if (username.isBlank()) {
|
||||
currentView.showUsernameError("请输入账号")
|
||||
return
|
||||
}
|
||||
if (password.isBlank()) {
|
||||
currentView.showPasswordError("请输入密码")
|
||||
return
|
||||
}
|
||||
|
||||
currentView.showLoading()
|
||||
val json = JSONObject();
|
||||
json.put("grantType", "password")
|
||||
json.put("username", username.trim())
|
||||
json.put("password", password)
|
||||
json.put("clientId", AppConfig.APP_CLIENT_ID)
|
||||
json.put("userType", AppConfig.APP_USER_TYPE)
|
||||
loginImpl.login(
|
||||
json,
|
||||
object : LoginContract.Callback<BaseBean<LoginUser>> {
|
||||
override fun success(data: BaseBean<LoginUser>) {
|
||||
val attachedView = view ?: return
|
||||
attachedView.hideLoading()
|
||||
val user = data.data
|
||||
if ((data.code == 0 || data.code == 200) && user != null) {
|
||||
attachedView.onLoginSuccess(user)
|
||||
} else {
|
||||
attachedView.showError(data.message.ifBlank { "登录失败" })
|
||||
}
|
||||
}
|
||||
|
||||
override fun fail(message: String) {
|
||||
view?.apply {
|
||||
hideLoading()
|
||||
showError(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
loginImpl.cancel()
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
138
app/src/main/java/com/juhabi/birds/main/MainActivity.kt
Normal file
138
app/src/main/java/com/juhabi/birds/main/MainActivity.kt
Normal file
@@ -0,0 +1,138 @@
|
||||
package com.juhabi.birds.main
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.juhabi.birds.R
|
||||
import com.juhabi.birds.base.BaseActivity
|
||||
import com.juhabi.birds.databinding.ActivityMainBinding
|
||||
import com.juhabi.birds.main.adapter.MainPagerAdapter
|
||||
import com.juhabi.birds.main.fragment.HistoryFragment
|
||||
import com.juhabi.birds.main.fragment.HomeFragment
|
||||
import com.juhabi.birds.main.fragment.MineFragment
|
||||
import com.juhabi.birds.main.fragment.PlanFragment
|
||||
import com.juhabi.birds.main.model.AppVersionInfo
|
||||
import com.juhabi.birds.main.mvp.MainContract
|
||||
import com.juhabi.birds.main.mvp.MainImpl
|
||||
import com.juhabi.birds.main.mvp.MainPresenter
|
||||
import com.gyf.immersionbar.ImmersionBar
|
||||
|
||||
class MainActivity : BaseActivity<ActivityMainBinding, MainPresenter>(),
|
||||
MainContract.MainView {
|
||||
|
||||
override fun createBinding(inflater: LayoutInflater): ActivityMainBinding {
|
||||
return ActivityMainBinding.inflate(inflater)
|
||||
}
|
||||
|
||||
override fun createPresenter(): MainPresenter {
|
||||
return MainPresenter(MainImpl(applicationContext))
|
||||
}
|
||||
|
||||
override fun initWindows() {
|
||||
ImmersionBar.with(this)
|
||||
.statusBarView(binding.statusBarView)
|
||||
.statusBarDarkFont(true)
|
||||
.navigationBarColor(R.color.color_zhu)
|
||||
.navigationBarDarkIcon(true)
|
||||
.init()
|
||||
}
|
||||
|
||||
override fun initView(savedInstanceState: Bundle?) {
|
||||
binding.viewPager.adapter = MainPagerAdapter(
|
||||
this,
|
||||
listOf(HomeFragment(), PlanFragment(), HistoryFragment(), MineFragment())
|
||||
)
|
||||
binding.viewPager.offscreenPageLimit = 1
|
||||
binding.viewPager.registerOnPageChangeCallback(object : androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
binding.bottomNavigation.menu.getItem(position).isChecked = true
|
||||
updateTopBarColor(position)
|
||||
}
|
||||
})
|
||||
binding.bottomNavigation.setOnItemSelectedListener { item ->
|
||||
onNavigationItemSelected(item.itemId)
|
||||
}
|
||||
binding.bottomNavigation.selectedItemId = R.id.navigation_home
|
||||
}
|
||||
|
||||
private fun onNavigationItemSelected(itemId: Int): Boolean {
|
||||
return when (itemId) {
|
||||
R.id.navigation_home -> onHomeSelected()
|
||||
R.id.navigation_plan -> onPlanSelected()
|
||||
R.id.navigation_history -> onHistorySelected()
|
||||
R.id.navigation_mine -> onMineSelected()
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun onHomeSelected(): Boolean {
|
||||
switchPage(PAGE_HOME)
|
||||
// Add Home-specific status bar, refresh, or Fragment communication here.
|
||||
return true
|
||||
}
|
||||
|
||||
private fun onPlanSelected(): Boolean {
|
||||
switchPage(PAGE_PLAN)
|
||||
// Add Plan-specific status bar, refresh, or Fragment communication here.
|
||||
return true
|
||||
}
|
||||
|
||||
private fun onHistorySelected(): Boolean {
|
||||
switchPage(PAGE_HISTORY)
|
||||
// Add History-specific status bar, refresh, or Fragment communication here.
|
||||
return true
|
||||
}
|
||||
|
||||
private fun onMineSelected(): Boolean {
|
||||
switchPage(PAGE_MINE)
|
||||
// Add Mine-specific status bar, refresh, or Fragment communication here.
|
||||
return true
|
||||
}
|
||||
|
||||
private fun switchPage(position: Int) {
|
||||
updateTopBarColor(position)
|
||||
if (binding.viewPager.currentItem != position) {
|
||||
binding.viewPager.setCurrentItem(position, false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTopBarColor(position: Int) {
|
||||
if (position == PAGE_PLAN) {
|
||||
binding.immersiveTopImageView.visibility = View.VISIBLE
|
||||
binding.statusBarView.setBackgroundColor(
|
||||
ContextCompat.getColor(this, android.R.color.transparent)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
binding.immersiveTopImageView.visibility = View.GONE
|
||||
@ColorRes val colorRes = when (position) {
|
||||
PAGE_HOME -> R.color.color_zhu
|
||||
PAGE_HISTORY -> R.color.color_E9A01C
|
||||
PAGE_MINE -> R.color.color_B85CE6
|
||||
else -> R.color.color_zhu
|
||||
}
|
||||
binding.statusBarView.setBackgroundColor(ContextCompat.getColor(this, colorRes))
|
||||
}
|
||||
|
||||
override fun initData(savedInstanceState: Bundle?) {
|
||||
presenter?.loadAppVersion()
|
||||
}
|
||||
|
||||
override fun onAppVersionLoaded(versionInfo: AppVersionInfo) {
|
||||
// The version is available for a future update-check API or dialog.
|
||||
}
|
||||
|
||||
override fun showError(message: String) {
|
||||
showMessage(message)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val PAGE_HOME = 0
|
||||
const val PAGE_PLAN = 1
|
||||
const val PAGE_HISTORY = 2
|
||||
const val PAGE_MINE = 3
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.juhabi.birds.main.adapter
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
|
||||
class MainPagerAdapter(
|
||||
activity: FragmentActivity,
|
||||
private val fragments: List<Fragment>
|
||||
) : FragmentStateAdapter(activity) {
|
||||
override fun getItemCount(): Int {
|
||||
return fragments.size
|
||||
}
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return fragments[position]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.juhabi.birds.main.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import com.juhabi.birds.base.BaseFragment
|
||||
import com.juhabi.birds.databinding.FragmentHistoryBinding
|
||||
import com.juhabi.birds.main.fragment.history.mvp.HistoryContract
|
||||
import com.juhabi.birds.main.fragment.history.mvp.HistoryImpl
|
||||
import com.juhabi.birds.main.fragment.history.mvp.HistoryPresenter
|
||||
import me.jessyan.autosize.internal.CustomAdapt
|
||||
|
||||
class HistoryFragment : BaseFragment<FragmentHistoryBinding, HistoryPresenter>(),
|
||||
HistoryContract.HistoryView{
|
||||
override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentHistoryBinding {
|
||||
return FragmentHistoryBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun createPresenter(): HistoryPresenter {
|
||||
return HistoryPresenter(HistoryImpl())
|
||||
}
|
||||
|
||||
override fun initView(savedInstanceState: Bundle?) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.juhabi.birds.main.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import com.juhabi.birds.base.BaseFragment
|
||||
import com.juhabi.birds.databinding.FragmentHomeBinding
|
||||
import com.juhabi.birds.main.fragment.home.mvp.HomeContract
|
||||
import com.juhabi.birds.main.fragment.home.mvp.HomeImpl
|
||||
import com.juhabi.birds.main.fragment.home.mvp.HomePresenter
|
||||
|
||||
class HomeFragment : BaseFragment<FragmentHomeBinding, HomePresenter>(),
|
||||
HomeContract.HomeView {
|
||||
|
||||
override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentHomeBinding {
|
||||
return FragmentHomeBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun createPresenter(): HomePresenter {
|
||||
return HomePresenter(HomeImpl())
|
||||
}
|
||||
|
||||
override fun initView(savedInstanceState: Bundle?) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.juhabi.birds.main.fragment
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import com.juhabi.birds.base.BaseFragment
|
||||
import com.juhabi.birds.databinding.FragmentMineBinding
|
||||
import com.juhabi.birds.main.fragment.mine.mvp.MineFragmentContract
|
||||
import com.juhabi.birds.main.fragment.mine.mvp.MineFragmentImpl
|
||||
import com.juhabi.birds.main.fragment.mine.mvp.MineFragmentPresenter
|
||||
import com.juhabi.birds.mine.MineCenterActivity
|
||||
|
||||
class MineFragment : BaseFragment<FragmentMineBinding, MineFragmentPresenter>(),
|
||||
MineFragmentContract.MineView {
|
||||
|
||||
override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentMineBinding {
|
||||
return FragmentMineBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun createPresenter(): MineFragmentPresenter {
|
||||
return MineFragmentPresenter(MineFragmentImpl())
|
||||
}
|
||||
|
||||
override fun initView(savedInstanceState: Bundle?) {
|
||||
binding.openComponentCenterButton.setOnClickListener {
|
||||
startActivity(Intent(requireContext(), MineCenterActivity::class.java))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.juhabi.birds.main.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import com.juhabi.birds.base.BaseFragment
|
||||
import com.juhabi.birds.databinding.FragmentPlanBinding
|
||||
import com.juhabi.birds.main.fragment.plan.mvp.PlanContract
|
||||
import com.juhabi.birds.main.fragment.plan.mvp.PlanImpl
|
||||
import com.juhabi.birds.main.fragment.plan.mvp.PlanPresenter
|
||||
|
||||
class PlanFragment : BaseFragment<FragmentPlanBinding, PlanPresenter>(),
|
||||
PlanContract.PlanView {
|
||||
|
||||
override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentPlanBinding {
|
||||
return FragmentPlanBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun createPresenter(): PlanPresenter {
|
||||
return PlanPresenter(PlanImpl())
|
||||
}
|
||||
|
||||
override fun initView(savedInstanceState: Bundle?) {
|
||||
binding.planBackButton.setOnClickListener {
|
||||
requireActivity().onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.juhabi.birds.main.fragment.history.mvp
|
||||
|
||||
import com.juhabi.birds.base.IView
|
||||
|
||||
interface HistoryContract {
|
||||
interface DataModel
|
||||
|
||||
interface HistoryView : IView
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.juhabi.birds.main.fragment.history.mvp
|
||||
|
||||
class HistoryImpl : HistoryContract.DataModel
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.juhabi.birds.main.fragment.history.mvp
|
||||
|
||||
import com.juhabi.birds.base.BasePresenter
|
||||
|
||||
class HistoryPresenter(
|
||||
private val model: HistoryContract.DataModel
|
||||
) : BasePresenter<HistoryContract.HistoryView>()
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.juhabi.birds.main.fragment.home.mvp
|
||||
|
||||
import com.juhabi.birds.base.IView
|
||||
|
||||
interface HomeContract {
|
||||
interface DataModel
|
||||
|
||||
interface HomeView : IView
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.juhabi.birds.main.fragment.home.mvp
|
||||
|
||||
class HomeImpl : HomeContract.DataModel
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.juhabi.birds.main.fragment.home.mvp
|
||||
|
||||
import com.juhabi.birds.base.BasePresenter
|
||||
|
||||
class HomePresenter(
|
||||
private val model: HomeContract.DataModel
|
||||
) : BasePresenter<HomeContract.HomeView>()
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.juhabi.birds.main.fragment.mine.mvp
|
||||
|
||||
import com.juhabi.birds.base.IView
|
||||
|
||||
interface MineFragmentContract {
|
||||
interface DataModel
|
||||
|
||||
interface MineView : IView
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.juhabi.birds.main.fragment.mine.mvp
|
||||
|
||||
class MineFragmentImpl : MineFragmentContract.DataModel
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.juhabi.birds.main.fragment.mine.mvp
|
||||
|
||||
import com.juhabi.birds.base.BasePresenter
|
||||
|
||||
class MineFragmentPresenter(
|
||||
private val model: MineFragmentContract.DataModel
|
||||
) : BasePresenter<MineFragmentContract.MineView>()
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.juhabi.birds.main.fragment.plan.mvp
|
||||
|
||||
import com.juhabi.birds.base.IView
|
||||
|
||||
interface PlanContract {
|
||||
interface DataModel
|
||||
|
||||
interface PlanView : IView
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.juhabi.birds.main.fragment.plan.mvp
|
||||
|
||||
class PlanImpl : PlanContract.DataModel
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.juhabi.birds.main.fragment.plan.mvp
|
||||
|
||||
import com.juhabi.birds.base.BasePresenter
|
||||
|
||||
class PlanPresenter(
|
||||
private val model: PlanContract.DataModel
|
||||
) : BasePresenter<PlanContract.PlanView>()
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.juhabi.birds.main.model
|
||||
|
||||
data class AppVersionInfo(
|
||||
val versionName: String,
|
||||
var platform: String = "",
|
||||
var currentVersion: String = "",
|
||||
var latestVersion: String = "",
|
||||
var versionCode: Int = 0,
|
||||
var updateAvailable: Boolean = false,
|
||||
var forceUpdate: Boolean = false,
|
||||
var downloadUrl: String = "",
|
||||
var releaseNotes: String = ""
|
||||
)
|
||||
19
app/src/main/java/com/juhabi/birds/main/mvp/MainContract.kt
Normal file
19
app/src/main/java/com/juhabi/birds/main/mvp/MainContract.kt
Normal file
@@ -0,0 +1,19 @@
|
||||
package com.juhabi.birds.main.mvp
|
||||
|
||||
import com.juhabi.birds.base.IView
|
||||
import com.juhabi.birds.main.model.AppVersionInfo
|
||||
|
||||
interface MainContract {
|
||||
interface DataModel {
|
||||
fun getAppVersion(callback: Callback<AppVersionInfo>)
|
||||
}
|
||||
|
||||
interface Callback<T> {
|
||||
fun onSuccess(data: T)
|
||||
fun onFailure(message: String)
|
||||
}
|
||||
|
||||
interface MainView : IView {
|
||||
fun onAppVersionLoaded(versionInfo: AppVersionInfo)
|
||||
}
|
||||
}
|
||||
27
app/src/main/java/com/juhabi/birds/main/mvp/MainImpl.kt
Normal file
27
app/src/main/java/com/juhabi/birds/main/mvp/MainImpl.kt
Normal file
@@ -0,0 +1,27 @@
|
||||
package com.juhabi.birds.main.mvp
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.juhabi.birds.main.model.AppVersionInfo
|
||||
|
||||
class MainImpl(context: Context) : MainContract.DataModel {
|
||||
private val appContext = context.applicationContext
|
||||
|
||||
override fun getAppVersion(callback: MainContract.Callback<AppVersionInfo>) {
|
||||
// runCatching {
|
||||
// val packageInfo = appContext.packageManager.getPackageInfo(appContext.packageName, 0)
|
||||
// AppVersionInfo(
|
||||
// versionName = packageInfo.versionName.orEmpty(),
|
||||
// versionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
// packageInfo.longVersionCode
|
||||
// } else {
|
||||
// @Suppress("DEPRECATION")
|
||||
// packageInfo.versionCode.toLong()
|
||||
// }
|
||||
// )
|
||||
// }.onSuccess(callback::onSuccess)
|
||||
// .onFailure { error ->
|
||||
// callback.onFailure(error.message ?: "Unable to read app version")
|
||||
// }
|
||||
}
|
||||
}
|
||||
21
app/src/main/java/com/juhabi/birds/main/mvp/MainPresenter.kt
Normal file
21
app/src/main/java/com/juhabi/birds/main/mvp/MainPresenter.kt
Normal file
@@ -0,0 +1,21 @@
|
||||
package com.juhabi.birds.main.mvp
|
||||
|
||||
import com.juhabi.birds.base.BasePresenter
|
||||
import com.juhabi.birds.main.model.AppVersionInfo
|
||||
|
||||
class MainPresenter(
|
||||
private val model: MainContract.DataModel
|
||||
) : BasePresenter<MainContract.MainView>() {
|
||||
|
||||
fun loadAppVersion() {
|
||||
model.getAppVersion(object : MainContract.Callback<AppVersionInfo> {
|
||||
override fun onSuccess(data: AppVersionInfo) {
|
||||
view?.onAppVersionLoaded(data)
|
||||
}
|
||||
|
||||
override fun onFailure(message: String) {
|
||||
view?.showError(message)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
245
app/src/main/java/com/juhabi/birds/mine/MineCenterActivity.kt
Normal file
245
app/src/main/java/com/juhabi/birds/mine/MineCenterActivity.kt
Normal file
@@ -0,0 +1,245 @@
|
||||
package com.juhabi.birds.mine
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.juhabi.birds.R
|
||||
import com.juhabi.birds.base.BaseActivity
|
||||
import com.juhabi.birds.component.picker.DateTimePicker
|
||||
import com.juhabi.birds.databinding.ActivityMineCenterBinding
|
||||
import com.juhabi.birds.demo.constraint.ConstraintLayoutDemoActivity
|
||||
import com.juhabi.birds.demo.recycler.RecyclerViewDemoActivity
|
||||
import com.juhabi.birds.login.LoginActivity
|
||||
import com.juhabi.birds.mine.model.MineUserInfo
|
||||
import com.juhabi.birds.mine.mvp.MineContract
|
||||
import com.juhabi.birds.mine.mvp.MineImpl
|
||||
import com.juhabi.birds.mine.mvp.MinePresenter
|
||||
import com.juhabi.birds.main.model.AppVersionInfo
|
||||
import com.juhabi.birds.network.TokenManager
|
||||
import com.juhabi.birds.utils.AppUpdateManager
|
||||
import com.gyf.immersionbar.ImmersionBar
|
||||
import java.util.Calendar
|
||||
|
||||
class MineCenterActivity : BaseActivity<ActivityMineCenterBinding, MinePresenter>(),
|
||||
MineContract.MineView, View.OnClickListener {
|
||||
|
||||
private val appUpdateManager by lazy {
|
||||
AppUpdateManager(this, ::showMessage)
|
||||
}
|
||||
|
||||
override fun createBinding(inflater: LayoutInflater): ActivityMineCenterBinding {
|
||||
return ActivityMineCenterBinding.inflate(inflater)
|
||||
}
|
||||
|
||||
override fun createPresenter(): MinePresenter {
|
||||
return MinePresenter(MineImpl())
|
||||
}
|
||||
|
||||
override fun initView(savedInstanceState: Bundle?) {
|
||||
TokenManager.get(this)
|
||||
listOf(
|
||||
binding.reloadButton,
|
||||
binding.checkVersionButton,
|
||||
binding.openLoginButton,
|
||||
binding.multiplePermissionButton,
|
||||
binding.locationPermissionButton,
|
||||
binding.normalDialogButton,
|
||||
binding.inputDialogButton,
|
||||
binding.datePickerButton,
|
||||
binding.timePickerButton,
|
||||
binding.dateTimePickerButton,
|
||||
binding.recyclerViewDemoButton,
|
||||
binding.constraintLayoutDemoButton
|
||||
).forEach { button ->
|
||||
button.setOnClickListener(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun initWindows() {
|
||||
ImmersionBar.with(this)
|
||||
.statusBarView(binding.statusBarView)
|
||||
.statusBarDarkFont(true)
|
||||
.navigationBarColor(R.color.color_zhu)
|
||||
.navigationBarDarkIcon(true)
|
||||
.init()
|
||||
}
|
||||
|
||||
override fun onClick(view: View) {
|
||||
when (view.id) {
|
||||
R.id.reloadButton -> {
|
||||
presenter?.getUserInfo()
|
||||
}
|
||||
R.id.checkVersionButton -> checkVersion()
|
||||
|
||||
R.id.openLoginButton -> {
|
||||
goTo(LoginActivity::class.java, null);
|
||||
}
|
||||
|
||||
R.id.multiplePermissionButton -> {
|
||||
getMorePermissionRequst(
|
||||
arrayOf(
|
||||
Manifest.permission.CAMERA,
|
||||
Manifest.permission.RECORD_AUDIO
|
||||
),
|
||||
"相机和麦克风权限",
|
||||
"拍照和录音功能需要使用相机及麦克风权限",
|
||||
REQUEST_CAMERA_AND_AUDIO
|
||||
)
|
||||
}
|
||||
|
||||
R.id.locationPermissionButton -> {
|
||||
getSinglePermissionRequst(
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
"定位权限",
|
||||
"定位功能需要获取您的位置信息",
|
||||
REQUEST_LOCATION
|
||||
)
|
||||
}
|
||||
|
||||
R.id.normalDialogButton -> {
|
||||
showDialogtext(
|
||||
"操作确认",
|
||||
"确定要执行这项操作吗?",
|
||||
"确定",
|
||||
"取消",
|
||||
object : OnDialogClick {
|
||||
override fun onCancelClick() {
|
||||
showMessage("已取消")
|
||||
}
|
||||
|
||||
override fun onConfirmClick() {
|
||||
showMessage("已点击确定")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
R.id.inputDialogButton -> {
|
||||
showEditDialog(
|
||||
"请输入内容",
|
||||
"请输入测试内容",
|
||||
click = object : OnDialogClick {
|
||||
override fun onCancelClick() {
|
||||
showMessage("已取消输入")
|
||||
}
|
||||
override fun onConfirmClick(content: String) {
|
||||
showMessage("输入内容:$content")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
R.id.datePickerButton -> showDateTimePicker(DateTimePicker.Mode.DATE)
|
||||
|
||||
R.id.timePickerButton -> showDateTimePicker(DateTimePicker.Mode.TIME)
|
||||
|
||||
R.id.dateTimePickerButton -> showDateTimePicker(DateTimePicker.Mode.DATE_TIME)
|
||||
|
||||
R.id.recyclerViewDemoButton -> {
|
||||
startActivity(Intent(this, RecyclerViewDemoActivity::class.java))
|
||||
}
|
||||
|
||||
R.id.constraintLayoutDemoButton -> {
|
||||
startActivity(Intent(this, ConstraintLayoutDemoActivity::class.java))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private fun checkVersion() {
|
||||
val currentVersion = packageManager
|
||||
.getPackageInfo(packageName, 0)
|
||||
.versionName
|
||||
.orEmpty()
|
||||
presenter?.checkVersion(currentVersion)
|
||||
}
|
||||
|
||||
override fun onVersionChecked(versionInfo: AppVersionInfo) {
|
||||
val currentVersion = packageManager.getPackageInfo(packageName, 0).versionName.orEmpty()
|
||||
appUpdateManager.checkAndUpdate(currentVersion, versionInfo)
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
appUpdateManager.onRequestPermissionsResult(requestCode, grantResults)
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期、时间和日期时间共用同一个组件调用入口。
|
||||
* 后续页面需要限制可选范围时,可以继续调用 setTimeRange()。
|
||||
*/
|
||||
private fun showDateTimePicker(mode: DateTimePicker.Mode) {
|
||||
DateTimePicker.Builder(this)
|
||||
.setMode(mode)
|
||||
.setSelectedTime(selectedDateTime)
|
||||
.setOnSelectedListener { result ->
|
||||
selectedDateTime = result.timestamp
|
||||
binding.dateTimeResultTextView.text = result.formattedText
|
||||
showMessage("已选择:${result.formattedText}")
|
||||
}
|
||||
.build()
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun initData(savedInstanceState: Bundle?) {
|
||||
presenter?.getUserInfo()
|
||||
}
|
||||
|
||||
override fun getUserInfoSuccess(userInfo: MineUserInfo) {
|
||||
val user = userInfo.user ?: return showError("个人信息为空")
|
||||
binding.nicknameTextView.text = user.nickName.ifBlank { "未设置昵称" }
|
||||
binding.phoneTextView.text = "手机号:${user.phonenumber.ifBlank { "未绑定" }}"
|
||||
binding.emailTextView.text = "邮箱:${user.email.ifBlank { "未绑定" }}"
|
||||
binding.userIdTextView.text = "用户 ID:${user.userId}"
|
||||
binding.userTypeTextView.text = "用户类型:${user.userType}"
|
||||
binding.loginDateTextView.text = "最近登录:${user.loginDate.ifBlank { "暂无" }}"
|
||||
}
|
||||
|
||||
override fun showError(message: String) {
|
||||
showMessage(message)
|
||||
}
|
||||
|
||||
override fun onPermissionsGranted(permissions: Array<String>) {
|
||||
showMessage("权限获取成功:${permissions.joinToString { it.substringAfterLast('.') }}")
|
||||
}
|
||||
|
||||
override fun onPermissionsDenied(permissions: Array<String>, permanentlyDenied: Boolean) {
|
||||
showMessage("权限被拒绝:${permissions.joinToString { it.substringAfterLast('.') }}")
|
||||
super.onPermissionsDenied(permissions, permanentlyDenied)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val REQUEST_CAMERA_AND_AUDIO = 1001
|
||||
private const val REQUEST_LOCATION = 1002
|
||||
}
|
||||
|
||||
/** 保存最近一次选择结果,再次打开选择器时用于回显。 */
|
||||
private var selectedDateTime: Long = Calendar.getInstance().timeInMillis
|
||||
|
||||
}
|
||||
//@Preview(showBackground = true, name = "Test")
|
||||
//@Composable
|
||||
//fun UserInfo() {
|
||||
// Column {
|
||||
// Text("张三")
|
||||
// Text("手机号:13800138000")
|
||||
//
|
||||
// Button(
|
||||
// onClick = {}
|
||||
// ) {
|
||||
// Text("修改")
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.juhabi.birds.mine.model
|
||||
|
||||
data class MineUserInfo(
|
||||
val user: User? = null,
|
||||
val permissions: List<Any> = emptyList(),
|
||||
val roles: List<Any> = emptyList(),
|
||||
val url: String? = null
|
||||
) {
|
||||
data class User(
|
||||
val userId: String = "",
|
||||
val tenantId: String = "",
|
||||
val userName: String? = null,
|
||||
val nickName: String = "",
|
||||
val userType: String = "",
|
||||
val email: String = "",
|
||||
val phonenumber: String = "",
|
||||
val sex: String = "",
|
||||
val avatar: String? = null,
|
||||
val status: String = "",
|
||||
val loginIp: String = "",
|
||||
val loginDate: String = "",
|
||||
val createTime: String = ""
|
||||
)
|
||||
}
|
||||
26
app/src/main/java/com/juhabi/birds/mine/mvp/MineContract.kt
Normal file
26
app/src/main/java/com/juhabi/birds/mine/mvp/MineContract.kt
Normal file
@@ -0,0 +1,26 @@
|
||||
package com.juhabi.birds.mine.mvp
|
||||
|
||||
import com.juhabi.birds.base.IView
|
||||
import com.juhabi.birds.main.model.AppVersionInfo
|
||||
import com.juhabi.birds.mine.model.MineUserInfo
|
||||
|
||||
interface MineContract {
|
||||
interface Callback<T> {
|
||||
fun success(data: T)
|
||||
fun fail(message: String)
|
||||
}
|
||||
|
||||
interface DataModel {
|
||||
fun getUserInfo(params: Map<String, String>, callback: Callback<MineUserInfo>)
|
||||
fun checkVersion(
|
||||
params: Map<String, String>,
|
||||
callback: Callback<AppVersionInfo>
|
||||
)
|
||||
fun cancel()
|
||||
}
|
||||
|
||||
interface MineView : IView {
|
||||
fun getUserInfoSuccess(userInfo: MineUserInfo)
|
||||
fun onVersionChecked(versionInfo: AppVersionInfo)
|
||||
}
|
||||
}
|
||||
106
app/src/main/java/com/juhabi/birds/mine/mvp/MineImpl.kt
Normal file
106
app/src/main/java/com/juhabi/birds/mine/mvp/MineImpl.kt
Normal file
@@ -0,0 +1,106 @@
|
||||
package com.juhabi.birds.mine.mvp
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import com.juhabi.birds.mine.model.MineUserInfo
|
||||
import com.juhabi.birds.main.model.AppVersionInfo
|
||||
import com.juhabi.birds.network.ApiClient
|
||||
import com.juhabi.birds.network.BaseBean
|
||||
import com.juhabi.birds.network.NetworkManager
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
|
||||
class MineImpl : MineContract.DataModel {
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
private var userInfoCall: Call<BaseBean<MineUserInfo>>? = null
|
||||
private var versionCall: Call<BaseBean<AppVersionInfo>>? = null
|
||||
|
||||
override fun getUserInfo(
|
||||
params: Map<String, String>,
|
||||
callback: MineContract.Callback<MineUserInfo>
|
||||
) {
|
||||
userInfoCall?.cancel()
|
||||
userInfoCall = ApiClient.apiService
|
||||
.getMineUserInfo(NetworkManager.getToken(), params)
|
||||
.also { call ->
|
||||
call.enqueue(object : Callback<BaseBean<MineUserInfo>> {
|
||||
override fun onResponse(
|
||||
call: Call<BaseBean<MineUserInfo>>,
|
||||
response: Response<BaseBean<MineUserInfo>>
|
||||
) {
|
||||
mainHandler.post {
|
||||
val bean = response.body()
|
||||
val data = bean?.data
|
||||
if (response.isSuccessful && bean?.code == 200 && data != null) {
|
||||
callback.success(data)
|
||||
} else {
|
||||
callback.fail(bean?.message?.ifBlank { null } ?: "获取个人信息失败")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<BaseBean<MineUserInfo>>, error: Throwable) {
|
||||
if (!call.isCanceled) {
|
||||
mainHandler.post {
|
||||
callback.fail(error.message ?: "网络请求失败")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override fun checkVersion(
|
||||
params: Map<String, String>,
|
||||
callback: MineContract.Callback<AppVersionInfo>) {
|
||||
versionCall?.cancel()
|
||||
versionCall = ApiClient.apiService
|
||||
.checkVersionCode(NetworkManager.getToken(), params)
|
||||
.also { call ->
|
||||
call.enqueue(object : Callback<BaseBean<AppVersionInfo>> {
|
||||
override fun onResponse(
|
||||
call: Call<BaseBean<AppVersionInfo>>,
|
||||
response: Response<BaseBean<AppVersionInfo>>
|
||||
) {
|
||||
mainHandler.post {
|
||||
val bean = response.body()
|
||||
val data = bean?.data
|
||||
if (response.isSuccessful &&
|
||||
bean?.code == 200 &&
|
||||
data != null
|
||||
) {
|
||||
callback.success(data)
|
||||
} else {
|
||||
callback.fail(
|
||||
bean?.message?.ifBlank { null }
|
||||
?: "版本检测失败"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(
|
||||
call: Call<BaseBean<AppVersionInfo>>,
|
||||
error: Throwable
|
||||
) {
|
||||
if (!call.isCanceled) {
|
||||
mainHandler.post {
|
||||
callback.fail(
|
||||
error.message ?: "版本检测失败"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
userInfoCall?.cancel()
|
||||
userInfoCall = null
|
||||
versionCall?.cancel()
|
||||
versionCall = null
|
||||
mainHandler.removeCallbacksAndMessages(null)
|
||||
}
|
||||
}
|
||||
53
app/src/main/java/com/juhabi/birds/mine/mvp/MinePresenter.kt
Normal file
53
app/src/main/java/com/juhabi/birds/mine/mvp/MinePresenter.kt
Normal file
@@ -0,0 +1,53 @@
|
||||
package com.juhabi.birds.mine.mvp
|
||||
|
||||
import com.juhabi.birds.base.BasePresenter
|
||||
import com.juhabi.birds.main.model.AppVersionInfo
|
||||
import com.juhabi.birds.mine.model.MineUserInfo
|
||||
|
||||
class MinePresenter(private val mineImpl: MineImpl) : BasePresenter<MineContract.MineView>() {
|
||||
|
||||
fun getUserInfo() {
|
||||
val currentView = view ?: return
|
||||
currentView.showLoading()
|
||||
mineImpl.getUserInfo(emptyMap(), object : MineContract.Callback<MineUserInfo> {
|
||||
override fun success(data: MineUserInfo) {
|
||||
view?.apply {
|
||||
hideLoading()
|
||||
getUserInfoSuccess(data)
|
||||
}
|
||||
}
|
||||
|
||||
override fun fail(message: String) {
|
||||
view?.apply {
|
||||
hideLoading()
|
||||
showError(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun checkVersion(currentVersion: String) {
|
||||
val params = mapOf(
|
||||
"currentVersion" to currentVersion,
|
||||
"platform" to "android"
|
||||
)
|
||||
mineImpl.checkVersion(
|
||||
params,
|
||||
object : MineContract.Callback<AppVersionInfo> {
|
||||
override fun success(data: AppVersionInfo) {
|
||||
view?.onVersionChecked(data)
|
||||
}
|
||||
|
||||
override fun fail(message: String) {
|
||||
view?.showError(message)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
mineImpl.cancel()
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
20
app/src/main/java/com/juhabi/birds/network/ApiClient.kt
Normal file
20
app/src/main/java/com/juhabi/birds/network/ApiClient.kt
Normal file
@@ -0,0 +1,20 @@
|
||||
package com.juhabi.birds.network
|
||||
|
||||
import com.juhabi.birds.AppConfig
|
||||
|
||||
/**
|
||||
* 全局网络入口。
|
||||
*/
|
||||
object ApiClient {
|
||||
private val networkManager: NetworkManager by lazy {
|
||||
NetworkManager.create(
|
||||
NetworkConfig(
|
||||
baseUrl = AppConfig.BASE_URL,
|
||||
enableLogging = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val apiService: ApiService
|
||||
get() = networkManager.apiService
|
||||
}
|
||||
14
app/src/main/java/com/juhabi/birds/network/ApiResult.kt
Normal file
14
app/src/main/java/com/juhabi/birds/network/ApiResult.kt
Normal file
@@ -0,0 +1,14 @@
|
||||
package com.juhabi.birds.network
|
||||
|
||||
sealed interface ApiResult<out T> {
|
||||
data class Success<T>(val data: T) : ApiResult<T>
|
||||
data class Failure(val error: Throwable) : ApiResult<Nothing>
|
||||
}
|
||||
|
||||
suspend inline fun <T> safeApiCall(crossinline request: suspend () -> T): ApiResult<T> {
|
||||
return try {
|
||||
ApiResult.Success(request())
|
||||
} catch (error: Exception) {
|
||||
ApiResult.Failure(error)
|
||||
}
|
||||
}
|
||||
27
app/src/main/java/com/juhabi/birds/network/ApiService.kt
Normal file
27
app/src/main/java/com/juhabi/birds/network/ApiService.kt
Normal file
@@ -0,0 +1,27 @@
|
||||
package com.juhabi.birds.network
|
||||
|
||||
import com.juhabi.birds.login.model.LoginUser
|
||||
import com.juhabi.birds.main.model.AppVersionInfo
|
||||
import com.juhabi.birds.mine.model.MineUserInfo
|
||||
import okhttp3.RequestBody
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.HeaderMap
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.QueryMap
|
||||
import retrofit2.http.POST
|
||||
|
||||
/**
|
||||
* 项目统一接口定义。所有业务接口地址集中维护在这里。
|
||||
*/
|
||||
interface ApiService {
|
||||
@POST("auth/login")
|
||||
fun login(@HeaderMap headers: Map<String, String>, @Body requestBody: RequestBody): Call<BaseBean<LoginUser>>
|
||||
|
||||
@GET("app/v1/getUserInfo")
|
||||
fun getMineUserInfo(@HeaderMap headers: Map<String, String>, @QueryMap params: Map<String, String>): Call<BaseBean<MineUserInfo>>
|
||||
|
||||
|
||||
@GET("app/v1/checkVersion")
|
||||
fun checkVersionCode(@HeaderMap headers: Map<String, String>, @QueryMap params: Map<String, String>): Call<BaseBean<AppVersionInfo>>
|
||||
}
|
||||
25
app/src/main/java/com/juhabi/birds/network/BaseBean.kt
Normal file
25
app/src/main/java/com/juhabi/birds/network/BaseBean.kt
Normal file
@@ -0,0 +1,25 @@
|
||||
package com.juhabi.birds.network
|
||||
|
||||
import com.google.gson.Gson
|
||||
import java.io.Serializable
|
||||
|
||||
data class BaseBean<T>(
|
||||
val code: Int = -1,
|
||||
val msg: String = "",
|
||||
val data: T? = null,
|
||||
val rows: T? = null,
|
||||
val pages: Int = 0,
|
||||
val total: Int = 0,
|
||||
val size: Int = 0,
|
||||
val currentPage: Int = 0
|
||||
) : Serializable {
|
||||
val isSuccess: Boolean get() = code == SUCCESS_CODE
|
||||
val message: String get() = msg
|
||||
fun dataString(): String {
|
||||
return data?.let { Gson().toJson(it) }.orEmpty()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SUCCESS_CODE = 0
|
||||
}
|
||||
}
|
||||
18
app/src/main/java/com/juhabi/birds/network/NetworkConfig.kt
Normal file
18
app/src/main/java/com/juhabi/birds/network/NetworkConfig.kt
Normal file
@@ -0,0 +1,18 @@
|
||||
package com.juhabi.birds.network
|
||||
|
||||
import okhttp3.Interceptor
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
data class NetworkConfig(
|
||||
val baseUrl: String,
|
||||
val connectTimeout: Long = 30,
|
||||
val readTimeout: Long = 30,
|
||||
val writeTimeout: Long = 30,
|
||||
val timeoutUnit: TimeUnit = TimeUnit.SECONDS,
|
||||
val enableLogging: Boolean = false,
|
||||
val interceptors: List<Interceptor> = emptyList()
|
||||
) {
|
||||
init {
|
||||
require(baseUrl.endsWith('/')) { "baseUrl must end with '/'" }
|
||||
}
|
||||
}
|
||||
82
app/src/main/java/com/juhabi/birds/network/NetworkManager.kt
Normal file
82
app/src/main/java/com/juhabi/birds/network/NetworkManager.kt
Normal file
@@ -0,0 +1,82 @@
|
||||
package com.juhabi.birds.network
|
||||
|
||||
import com.juhabi.birds.AppConfig
|
||||
import com.google.gson.GsonBuilder
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import org.json.JSONObject
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
|
||||
class NetworkManager private constructor(config: NetworkConfig) {
|
||||
private val retrofit: Retrofit
|
||||
|
||||
val apiService: ApiService by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
|
||||
retrofit.create(ApiService::class.java)
|
||||
}
|
||||
|
||||
init {
|
||||
val clientBuilder = OkHttpClient.Builder()
|
||||
.connectTimeout(config.connectTimeout, config.timeoutUnit)
|
||||
.readTimeout(config.readTimeout, config.timeoutUnit)
|
||||
.writeTimeout(config.writeTimeout, config.timeoutUnit)
|
||||
.retryOnConnectionFailure(true)
|
||||
|
||||
config.interceptors.forEach(clientBuilder::addInterceptor)
|
||||
if (config.enableLogging) {
|
||||
clientBuilder.addInterceptor(
|
||||
HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)
|
||||
)
|
||||
}
|
||||
|
||||
val gson = GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create()
|
||||
retrofit = Retrofit.Builder()
|
||||
.baseUrl(config.baseUrl)
|
||||
.client(clientBuilder.build())
|
||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||
.build()
|
||||
}
|
||||
|
||||
fun <T : Any> create(service: Class<T>): T {
|
||||
return retrofit.create(service)
|
||||
}
|
||||
|
||||
inline fun <reified T : Any> create(): T {
|
||||
return create(T::class.java)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val JSON = "application/json; charset=utf-8".toMediaType()
|
||||
private val FORM_DATA = "multipart/form-data".toMediaType()
|
||||
|
||||
fun create(config: NetworkConfig): NetworkManager {
|
||||
return NetworkManager(config)
|
||||
}
|
||||
|
||||
/** 与 WaterProject 的 NetWorkManager.getToken() 保持一致,统一生成公共请求头。 */
|
||||
fun getToken(): Map<String, String> {
|
||||
return buildMap {
|
||||
put("clientId", AppConfig.APP_CLIENT_ID)
|
||||
put("content-language", AppConfig.CONTENT_LANGUAGE)
|
||||
AppConfig.userToken.takeIf(String::isNotBlank)?.let { token ->
|
||||
put("Authorization", "Bearer $token")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toRequestBody(json: JSONObject): RequestBody {
|
||||
return toRequestBody(json.toString())
|
||||
}
|
||||
|
||||
fun toRequestBody(json: String): RequestBody {
|
||||
return json.toRequestBody(JSON)
|
||||
}
|
||||
|
||||
fun formBodies(values: Map<String, String?>): Map<String, RequestBody> {
|
||||
return values.mapValues { (_, value) -> value.orEmpty().toRequestBody(FORM_DATA) }
|
||||
}
|
||||
}
|
||||
}
|
||||
25
app/src/main/java/com/juhabi/birds/network/TokenManager.kt
Normal file
25
app/src/main/java/com/juhabi/birds/network/TokenManager.kt
Normal file
@@ -0,0 +1,25 @@
|
||||
package com.juhabi.birds.network
|
||||
|
||||
import android.content.Context
|
||||
import com.juhabi.birds.AppConfig
|
||||
import com.juhabi.birds.util.SharedPreferencesUtil
|
||||
|
||||
object TokenManager {
|
||||
private const val KEY_USER_TOKEN = "user_token"
|
||||
|
||||
fun save(context: Context, token: String) {
|
||||
SharedPreferencesUtil.putString(context, KEY_USER_TOKEN, token)
|
||||
AppConfig.userToken = token
|
||||
}
|
||||
|
||||
fun get(context: Context): String {
|
||||
val token = SharedPreferencesUtil.getString(context, KEY_USER_TOKEN)
|
||||
AppConfig.userToken = token
|
||||
return token
|
||||
}
|
||||
|
||||
fun clear(context: Context) {
|
||||
SharedPreferencesUtil.remove(context, KEY_USER_TOKEN)
|
||||
AppConfig.userToken = ""
|
||||
}
|
||||
}
|
||||
15
app/src/main/java/com/juhabi/birds/user/model/UserEntity.kt
Normal file
15
app/src/main/java/com/juhabi/birds/user/model/UserEntity.kt
Normal file
@@ -0,0 +1,15 @@
|
||||
package com.juhabi.birds.user.model
|
||||
|
||||
/**
|
||||
* 临时用户实体,用户接口确定后可根据实际业务替换或扩展字段。
|
||||
*/
|
||||
data class UserEntity(
|
||||
val userId: String = "",
|
||||
val userName: String = "",
|
||||
val nickName: String = "",
|
||||
val phoneNumber: String = "",
|
||||
val email: String = "",
|
||||
val avatar: String = "",
|
||||
val sex: String = "",
|
||||
val tenantId: String = ""
|
||||
)
|
||||
65
app/src/main/java/com/juhabi/birds/util/LubanCompressUtil.kt
Normal file
65
app/src/main/java/com/juhabi/birds/util/LubanCompressUtil.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
112
app/src/main/java/com/juhabi/birds/util/SharedPreferencesUtil.kt
Normal file
112
app/src/main/java/com/juhabi/birds/util/SharedPreferencesUtil.kt
Normal 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()
|
||||
}
|
||||
43
app/src/main/java/com/juhabi/birds/util/ToastUtil.kt
Normal file
43
app/src/main/java/com/juhabi/birds/util/ToastUtil.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
20
app/src/main/java/com/juhabi/birds/util/click/ClickUtil.kt
Normal file
20
app/src/main/java/com/juhabi/birds/util/click/ClickUtil.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 = "*/*"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
250
app/src/main/java/com/juhabi/birds/utils/AppUpdateManager.kt
Normal file
250
app/src/main/java/com/juhabi/birds/utils/AppUpdateManager.kt
Normal file
@@ -0,0 +1,250 @@
|
||||
package com.juhabi.birds.utils
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import com.juhabi.birds.main.model.AppVersionInfo
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class AppUpdateManager(
|
||||
private val activity: Activity,
|
||||
private val messageCallback: (String) -> Unit
|
||||
) {
|
||||
private var pendingDownloadUrl: String? = null
|
||||
|
||||
fun checkAndUpdate(
|
||||
currentVersion: String,
|
||||
versionInfo: AppVersionInfo
|
||||
) {
|
||||
val latestVersion = versionInfo.latestVersion.ifBlank {
|
||||
versionInfo.versionName
|
||||
}
|
||||
if (!isNewerVersion(currentVersion, latestVersion)) {
|
||||
messageCallback("当前已是最新版本")
|
||||
return
|
||||
}
|
||||
showUpdateDialog(latestVersion, versionInfo)
|
||||
}
|
||||
|
||||
fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
grantResults: IntArray
|
||||
): Boolean {
|
||||
if (requestCode != REQUEST_NOTIFICATIONS) return false
|
||||
|
||||
val downloadUrl = pendingDownloadUrl
|
||||
pendingDownloadUrl = null
|
||||
val permissionGranted = grantResults.firstOrNull() ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
if (permissionGranted && !downloadUrl.isNullOrBlank()) {
|
||||
downloadApk(downloadUrl)
|
||||
} else {
|
||||
messageCallback("需要通知权限才能显示下载进度")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun showUpdateDialog(
|
||||
latestVersion: String,
|
||||
versionInfo: AppVersionInfo
|
||||
) {
|
||||
AlertDialog.Builder(activity)
|
||||
.setTitle("发现新版本 $latestVersion")
|
||||
.setMessage(
|
||||
versionInfo.releaseNotes.ifBlank {
|
||||
"是否立即下载更新?"
|
||||
}
|
||||
)
|
||||
.setNegativeButton("取消", null)
|
||||
.setPositiveButton("下载") { _, _ ->
|
||||
downloadApk(versionInfo.downloadUrl)
|
||||
}
|
||||
.setCancelable(!versionInfo.forceUpdate)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun isNewerVersion(
|
||||
currentVersion: String,
|
||||
latestVersion: String
|
||||
): Boolean {
|
||||
val currentParts = parseVersion(currentVersion)
|
||||
val latestParts = parseVersion(latestVersion)
|
||||
val partCount = maxOf(currentParts.size, latestParts.size)
|
||||
|
||||
for (index in 0 until partCount) {
|
||||
val currentPart = currentParts.getOrElse(index) { 0 }
|
||||
val latestPart = latestParts.getOrElse(index) { 0 }
|
||||
if (currentPart != latestPart) {
|
||||
return latestPart > currentPart
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun parseVersion(version: String): List<Int> {
|
||||
return version
|
||||
.split('.', '-', '_')
|
||||
.map { part -> part.toIntOrNull() ?: 0 }
|
||||
}
|
||||
|
||||
private fun downloadApk(url: String) {
|
||||
if (url.isBlank()) {
|
||||
messageCallback("下载地址为空")
|
||||
return
|
||||
}
|
||||
if (!hasNotificationPermission()) {
|
||||
pendingDownloadUrl = url
|
||||
activity.requestPermissions(
|
||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
REQUEST_NOTIFICATIONS
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val notificationManager = activity.getSystemService(
|
||||
NotificationManager::class.java
|
||||
)
|
||||
createDownloadChannel(notificationManager)
|
||||
Executors.newSingleThreadExecutor().execute {
|
||||
runCatching {
|
||||
downloadFile(url, notificationManager)
|
||||
}.onSuccess { apkFile ->
|
||||
activity.runOnUiThread {
|
||||
showDownloadComplete(notificationManager)
|
||||
installApk(apkFile)
|
||||
}
|
||||
}.onFailure { error ->
|
||||
activity.runOnUiThread {
|
||||
messageCallback("下载失败:${error.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasNotificationPermission(): Boolean {
|
||||
return Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||
|
||||
activity.checkSelfPermission(
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
private fun createDownloadChannel(manager: NotificationManager) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
|
||||
manager.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
DOWNLOAD_CHANNEL_ID,
|
||||
"应用更新",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun downloadFile(
|
||||
url: String,
|
||||
notificationManager: NotificationManager
|
||||
): File {
|
||||
val response = OkHttpClient().newCall(
|
||||
Request.Builder().url(url).build()
|
||||
).execute()
|
||||
if (!response.isSuccessful) error("HTTP ${response.code}")
|
||||
|
||||
val body = response.body ?: error("下载响应为空")
|
||||
val apkFile = File(
|
||||
activity.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
UPDATE_APK_NAME
|
||||
)
|
||||
body.byteStream().use { input ->
|
||||
FileOutputStream(apkFile).use { output ->
|
||||
copyDownload(
|
||||
input,
|
||||
output,
|
||||
body.contentLength(),
|
||||
notificationManager
|
||||
)
|
||||
}
|
||||
}
|
||||
return apkFile
|
||||
}
|
||||
|
||||
private fun copyDownload(
|
||||
input: InputStream,
|
||||
output: FileOutputStream,
|
||||
totalBytes: Long,
|
||||
notificationManager: NotificationManager
|
||||
) {
|
||||
val buffer = ByteArray(DOWNLOAD_BUFFER_SIZE)
|
||||
var downloadedBytes = 0L
|
||||
var readCount: Int
|
||||
|
||||
while (input.read(buffer).also { readCount = it } != -1) {
|
||||
output.write(buffer, 0, readCount)
|
||||
downloadedBytes += readCount
|
||||
if (totalBytes > 0) {
|
||||
val progress = (downloadedBytes * 100 / totalBytes).toInt()
|
||||
showDownloadProgress(notificationManager, progress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showDownloadProgress(
|
||||
manager: NotificationManager,
|
||||
progress: Int
|
||||
) {
|
||||
val notification = NotificationCompat.Builder(
|
||||
activity,
|
||||
DOWNLOAD_CHANNEL_ID
|
||||
)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
.setContentTitle("正在下载更新")
|
||||
.setOnlyAlertOnce(true)
|
||||
.setProgress(100, progress, false)
|
||||
.build()
|
||||
manager.notify(DOWNLOAD_NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
private fun showDownloadComplete(manager: NotificationManager) {
|
||||
val notification = NotificationCompat.Builder(
|
||||
activity,
|
||||
DOWNLOAD_CHANNEL_ID
|
||||
)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
.setContentTitle("下载完成")
|
||||
.build()
|
||||
manager.notify(DOWNLOAD_NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
private fun installApk(file: File) {
|
||||
val uri = FileProvider.getUriForFile(
|
||||
activity,
|
||||
"${activity.packageName}.fileprovider",
|
||||
file
|
||||
)
|
||||
val installIntent = Intent(Intent.ACTION_VIEW)
|
||||
.setDataAndType(uri, APK_MIME_TYPE)
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
activity.startActivity(installIntent)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val REQUEST_NOTIFICATIONS = 1003
|
||||
const val DOWNLOAD_NOTIFICATION_ID = 10086
|
||||
const val DOWNLOAD_CHANNEL_ID = "app_update"
|
||||
const val DOWNLOAD_BUFFER_SIZE = 8 * 1024
|
||||
const val UPDATE_APK_NAME = "kot-update.apk"
|
||||
const val APK_MIME_TYPE = "application/vnd.android.package-archive"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user