feat: 小趣手表APP Android原生重构 - 基础框架搭建

已完成的模块:
1. 项目脚手架 - Gradle配置、28个包目录、核心基类
2. 权限管理 - 确认定制ROM已预授权所有权限
3. 工具类 - DateUtil/DeviceUtil/NetworkUtil/Md5Util
4. 设备信息 - DevicePrefs/UserPrefs (SharedPreferences)
5. 网络层 - OkHttp+Retrofit+MD5签名拦截器+解绑拦截器
6. 基础UI组件 - NavBarView/QuTipDialog/QuConfirmDialog/ActionButton/iconfont

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
dongliang
2026-04-27 11:26:50 +09:30
commit a397985954
89 changed files with 3211 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
package com.xiaoqu.watch.app
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import timber.log.Timber
/**
* 开机自启广播接收器
* 收到 BOOT_COMPLETED 后启动 MainActivity
*/
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
Timber.d("Boot completed, launching MainActivity")
val launchIntent = Intent(context, MainActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(launchIntent)
}
}
}

View File

@@ -0,0 +1,53 @@
package com.xiaoqu.watch.app
import android.content.pm.ActivityInfo
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import com.xiaoqu.watch.databinding.ActivityMainBinding
import dagger.hilt.android.AndroidEntryPoint
import timber.log.Timber
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 固定竖屏
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
// 全屏(定制系统无状态栏,此为防御性设置)
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// 拦截物理返回键
setupBackButton()
Timber.d("MainActivity created")
}
/**
* 物理返回键拦截:
* - 已绑定用户 → 开启 NFC 打卡模式(后续模块实现)
* - 未绑定 → 无操作
* - 所有情况阻止默认页面回退
*/
private fun setupBackButton() {
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// TODO: 后续模块实现 NFC 打卡触发
Timber.d("Back button pressed - intercepted")
}
})
}
}

View File

@@ -0,0 +1,27 @@
package com.xiaoqu.watch.app
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber
@HiltAndroidApp
class WatchApplication : Application() {
override fun onCreate() {
super.onCreate()
// 1. Timber 初始化
Timber.plant(Timber.DebugTree())
// 2-9 其他初始化在后续模块中逐步添加
// LogManager.init()
// CrashHandler.init()
// DeviceRepository.collectDeviceInfo()
// DeviceMonitor.start()
// ClockManager.start()
// WakeLockHelper.acquire()
// OtaManager.checkOnStartup()
Timber.d("WatchApplication initialized")
}
}

View File

@@ -0,0 +1,89 @@
package com.xiaoqu.watch.data.prefs
import android.content.Context
import android.content.SharedPreferences
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import javax.inject.Singleton
/**
* 设备信息存储(解绑时不清除)
* 对应旧版 uni.setStorage('watchInfo', {...})
*/
@Singleton
class DevicePrefs @Inject constructor(
@ApplicationContext context: Context
) {
private val sp: SharedPreferences =
context.getSharedPreferences("device_prefs", Context.MODE_PRIVATE)
var imei: String
get() = sp.getString(KEY_IMEI, "") ?: ""
set(value) = sp.edit().putString(KEY_IMEI, value).apply()
var serial: String
get() = sp.getString(KEY_SERIAL, "") ?: ""
set(value) = sp.edit().putString(KEY_SERIAL, value).apply()
var bluetoothName: String
get() = sp.getString(KEY_BT_NAME, "") ?: ""
set(value) = sp.edit().putString(KEY_BT_NAME, value).apply()
var bluetoothMac: String
get() = sp.getString(KEY_BT_MAC, "") ?: ""
set(value) = sp.edit().putString(KEY_BT_MAC, value).apply()
var brand: String
get() = sp.getString(KEY_BRAND, "") ?: ""
set(value) = sp.edit().putString(KEY_BRAND, value).apply()
var model: String
get() = sp.getString(KEY_MODEL, "") ?: ""
set(value) = sp.edit().putString(KEY_MODEL, value).apply()
var osVersion: String
get() = sp.getString(KEY_OS_VERSION, "") ?: ""
set(value) = sp.edit().putString(KEY_OS_VERSION, value).apply()
var totalMemory: Long
get() = sp.getLong(KEY_TOTAL_MEMORY, 0)
set(value) = sp.edit().putLong(KEY_TOTAL_MEMORY, value).apply()
/** 是否已初始化过设备信息 */
val isInitialized: Boolean
get() = imei.isNotEmpty()
/** 从 DeviceUtil 批量写入设备信息 */
fun saveDeviceInfo(
imei: String,
serial: String,
bluetoothName: String,
bluetoothMac: String,
brand: String,
model: String,
osVersion: String,
totalMemory: Long
) {
sp.edit()
.putString(KEY_IMEI, imei)
.putString(KEY_SERIAL, serial)
.putString(KEY_BT_NAME, bluetoothName)
.putString(KEY_BT_MAC, bluetoothMac)
.putString(KEY_BRAND, brand)
.putString(KEY_MODEL, model)
.putString(KEY_OS_VERSION, osVersion)
.putLong(KEY_TOTAL_MEMORY, totalMemory)
.apply()
}
companion object {
private const val KEY_IMEI = "imei"
private const val KEY_SERIAL = "serial"
private const val KEY_BT_NAME = "bluetooth_name"
private const val KEY_BT_MAC = "bluetooth_mac"
private const val KEY_BRAND = "brand"
private const val KEY_MODEL = "model"
private const val KEY_OS_VERSION = "os_version"
private const val KEY_TOTAL_MEMORY = "total_memory"
}
}

View File

@@ -0,0 +1,61 @@
package com.xiaoqu.watch.data.prefs
import android.content.Context
import android.content.SharedPreferences
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import javax.inject.Singleton
/**
* 用户绑定信息存储(解绑时清除)
* 对应旧版 uni.setStorage('bondUser', {...})
*/
@Singleton
class UserPrefs @Inject constructor(
@ApplicationContext context: Context
) {
private val sp: SharedPreferences =
context.getSharedPreferences("user_prefs", Context.MODE_PRIVATE)
var userId: Long
get() = sp.getLong(KEY_USER_ID, 0)
set(value) = sp.edit().putLong(KEY_USER_ID, value).apply()
var mobile: String
get() = sp.getString(KEY_MOBILE, "") ?: ""
set(value) = sp.edit().putString(KEY_MOBILE, value).apply()
var userName: String
get() = sp.getString(KEY_USER_NAME, "") ?: ""
set(value) = sp.edit().putString(KEY_USER_NAME, value).apply()
var headUrl: String
get() = sp.getString(KEY_HEAD_URL, "") ?: ""
set(value) = sp.edit().putString(KEY_HEAD_URL, value).apply()
/** 是否已绑定用户 */
val isBound: Boolean
get() = userId > 0
/** 保存用户绑定信息 */
fun saveUser(userId: Long, mobile: String, userName: String, headUrl: String) {
sp.edit()
.putLong(KEY_USER_ID, userId)
.putString(KEY_MOBILE, mobile)
.putString(KEY_USER_NAME, userName)
.putString(KEY_HEAD_URL, headUrl)
.apply()
}
/** 解绑时清除所有用户数据 */
fun clear() {
sp.edit().clear().apply()
}
companion object {
private const val KEY_USER_ID = "user_id"
private const val KEY_MOBILE = "mobile"
private const val KEY_USER_NAME = "user_name"
private const val KEY_HEAD_URL = "head_url"
}
}

View File

@@ -0,0 +1,65 @@
package com.xiaoqu.watch.di
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.xiaoqu.watch.network.EnvConfig
import com.xiaoqu.watch.network.SignatureInterceptor
import com.xiaoqu.watch.network.UnbindInterceptor
import com.xiaoqu.watch.network.api.CommonApi
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideGson(): Gson {
return GsonBuilder().create()
}
@Provides
@Singleton
fun provideOkHttpClient(
signatureInterceptor: SignatureInterceptor,
unbindInterceptor: UnbindInterceptor
): OkHttpClient {
val logging = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
return OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.addInterceptor(signatureInterceptor)
.addInterceptor(unbindInterceptor)
.addInterceptor(logging)
.build()
}
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient, gson: Gson): Retrofit {
return Retrofit.Builder()
.baseUrl(EnvConfig.serviceUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
}
@Provides
@Singleton
fun provideCommonApi(retrofit: Retrofit): CommonApi {
return retrofit.create(CommonApi::class.java)
}
}

View File

@@ -0,0 +1,20 @@
package com.xiaoqu.watch.event
/**
* 全局事件定义(替代旧版 uni.$emit/$on 事件总线)
*/
sealed class AppEvent {
// 任务相关
data class TaskUpdated(val taskId: Long) : AppEvent()
data object TaskListRefresh : AppEvent()
data object HomeRefresh : AppEvent()
data object NewMessage : AppEvent()
data object PunchTaskListRefresh : AppEvent()
data class TabChanged(val index: Int) : AppEvent()
data class NfcCardRead(val nfcId: String) : AppEvent()
// 系统相关
data object DeviceUnbound : AppEvent()
data object BindSuccess : AppEvent()
data class WorkStateChanged(val isWorking: Boolean) : AppEvent()
}

View File

@@ -0,0 +1,22 @@
package com.xiaoqu.watch.event
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import javax.inject.Inject
import javax.inject.Singleton
/**
* 全局事件总线SharedFlow 实现)
* 替代旧版 uni.$emit/$on
*/
@Singleton
class EventBus @Inject constructor() {
private val _events = MutableSharedFlow<AppEvent>(replay = 0)
val events: SharedFlow<AppEvent> = _events.asSharedFlow()
suspend fun emit(event: AppEvent) {
_events.emit(event)
}
}

View File

@@ -0,0 +1,21 @@
package com.xiaoqu.watch.network
import com.google.gson.annotations.SerializedName
/**
* 服务端统一响应格式
* { "code": 0, "data": {...}, "err": "错误信息" }
*/
data class ApiResponse<T>(
@SerializedName("code") val code: Int = 0,
@SerializedName("data") val data: T? = null,
@SerializedName("err") val err: String? = null
) {
val isSuccess: Boolean get() = code == 0
val isUnbound: Boolean get() = code == CODE_UNBOUND
companion object {
/** 解绑状态码 */
const val CODE_UNBOUND = 104
}
}

View File

@@ -0,0 +1,11 @@
package com.xiaoqu.watch.network
/**
* 统一的 API 返回结果封装
* 调用方用 when 表达式处理,编译器强制覆盖所有情况
*/
sealed class ApiResult<out T> {
data class Success<T>(val data: T) : ApiResult<T>()
data class Error(val code: Int, val message: String) : ApiResult<Nothing>()
data class NetworkError(val exception: Throwable) : ApiResult<Nothing>()
}

View File

@@ -0,0 +1,13 @@
package com.xiaoqu.watch.network
import com.xiaoqu.watch.BuildConfig
/**
* 环境配置
* debug 构建 → develop 环境
* release 构建 → production 环境
*/
object EnvConfig {
val serviceUrl: String = BuildConfig.SERVICE_URL
val mqttUrl: String = BuildConfig.MQTT_URL
}

View File

@@ -0,0 +1,22 @@
package com.xiaoqu.watch.network
import timber.log.Timber
/**
* 安全 API 调用封装
* 将 Retrofit suspend 调用转换为 ApiResult
*/
suspend fun <T> safeApiCall(call: suspend () -> ApiResponse<T>): ApiResult<T> {
return try {
val response = call()
if (response.isSuccess) {
@Suppress("UNCHECKED_CAST")
ApiResult.Success(response.data as T)
} else {
ApiResult.Error(response.code, response.err ?: "未知错误")
}
} catch (e: Exception) {
Timber.w(e, "网络请求异常")
ApiResult.NetworkError(e)
}
}

View File

@@ -0,0 +1,41 @@
package com.xiaoqu.watch.network
import com.xiaoqu.watch.data.prefs.DevicePrefs
import com.xiaoqu.watch.data.prefs.UserPrefs
import com.xiaoqu.watch.util.Md5Util
import okhttp3.Interceptor
import okhttp3.Response
import javax.inject.Inject
import javax.inject.Singleton
/**
* MD5 签名拦截器
* 自动为每个请求添加 signature、imei、timeStamp 请求头
*
* 签名算法(与旧版一致):
* signature = MD5(MD5(imei).upper() + MD5(timeStamp + phone).upper()).upper()
*/
@Singleton
class SignatureInterceptor @Inject constructor(
private val devicePrefs: DevicePrefs,
private val userPrefs: UserPrefs
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val imei = devicePrefs.imei
val phone = userPrefs.mobile
val timeStamp = System.currentTimeMillis().toString()
val signature = Md5Util.md5Upper(
Md5Util.md5Upper(imei) + Md5Util.md5Upper(timeStamp + phone)
)
val request = chain.request().newBuilder()
.addHeader("signature", signature)
.addHeader("imei", imei)
.addHeader("timeStamp", timeStamp)
.build()
return chain.proceed(request)
}
}

View File

@@ -0,0 +1,51 @@
package com.xiaoqu.watch.network
import com.google.gson.Gson
import com.xiaoqu.watch.data.prefs.UserPrefs
import com.xiaoqu.watch.event.AppEvent
import com.xiaoqu.watch.event.EventBus
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
import okhttp3.Response
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
/**
* 解绑拦截器
* 检测 API 返回 code=104 时,自动清除用户数据并发送解绑事件
*/
@Singleton
class UnbindInterceptor @Inject constructor(
private val userPrefs: UserPrefs,
private val eventBus: EventBus,
private val gson: Gson
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
if (response.isSuccessful) {
try {
// 窥视响应体(不消耗)
val source = response.body?.source() ?: return response
source.request(Long.MAX_VALUE)
val buffer = source.buffer.clone()
val json = buffer.readUtf8()
val apiResponse = gson.fromJson(json, ApiResponse::class.java)
if (apiResponse?.isUnbound == true) {
Timber.w("收到 code=104执行自动解绑")
userPrefs.clear()
runBlocking {
eventBus.emit(AppEvent.DeviceUnbound)
}
}
} catch (e: Exception) {
Timber.w(e, "解绑检测异常")
}
}
return response
}
}

View File

@@ -0,0 +1,34 @@
package com.xiaoqu.watch.network.api
import com.xiaoqu.watch.network.ApiResponse
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Query
/**
* 通用 API 接口(绑定、版本检查、设备状态)
* 其他模块的 API 在各自模块开发时添加
*/
interface CommonApi {
/** 绑定确认 */
@POST("watch/bindWatchConfirm")
suspend fun bindWatchConfirm(@Body params: Map<String, Any>): ApiResponse<Any>
/** 根据 IMEI 查询手表信息 */
@GET("watch/getWatchByImei")
suspend fun getWatchByImei(@Query("imei") imei: String): ApiResponse<Any>
/** 检查版本更新 */
@GET("newAppVersion/queryWatch")
suspend fun checkVersion(@Query("imei") imei: String): ApiResponse<Any>
/** 上报设备状态电量、蓝牙、NFC等 */
@POST("watch/setWatchStatusByImeiFormWatch")
suspend fun reportDeviceStatus(@Body params: Map<String, Any>): ApiResponse<Any>
/** 上报日志 */
@POST("watchTestLog")
suspend fun reportLog(@Body params: Map<String, Any>): ApiResponse<Any>
}

View File

@@ -0,0 +1,15 @@
package com.xiaoqu.watch.ui.bind
import android.view.LayoutInflater
import android.view.ViewGroup
import com.xiaoqu.watch.databinding.FragmentBindBinding
import com.xiaoqu.watch.ui.common.BaseFragment
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class BindFragment : BaseFragment<FragmentBindBinding>() {
override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentBindBinding {
return FragmentBindBinding.inflate(inflater, container, false)
}
}

View File

@@ -0,0 +1,66 @@
package com.xiaoqu.watch.ui.common
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.navigation.NavController
import androidx.viewbinding.ViewBinding
/**
* 所有 Fragment 的基类
* 提供 ViewBinding 管理、圆角 SafeArea padding、导航工具方法
*/
abstract class BaseFragment<VB : ViewBinding> : Fragment() {
private var _binding: VB? = null
protected val binding get() = _binding!!
/**
* 子类实现:创建 ViewBinding
*/
abstract fun createBinding(inflater: LayoutInflater, container: ViewGroup?): VB
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = createBinding(inflater, container)
// 圆角屏 SafeArea padding顶部由 NavBar 自行处理,底部和左右由各布局自行设置)
// NavBar 已移入各 Fragment 布局root 不再统一加 padding
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null // 防止内存泄漏
}
/**
* 获取主 NavController支持 ViewPager2 内的 Fragment 向上查找)
*/
protected fun findMainNavController(): NavController {
var fragment: Fragment? = this
while (fragment != null) {
try {
return androidx.navigation.fragment.NavHostFragment.findNavController(fragment)
} catch (e: IllegalStateException) {
fragment = fragment.parentFragment
}
}
throw IllegalStateException("No NavController found")
}
/**
* 设置标题栏返回按钮(物理返回键被拦截,页面返回靠标题栏按钮)
*/
protected fun setupBackButton(backButton: View) {
backButton.setOnClickListener {
findMainNavController().popBackStack()
}
}
}

View File

@@ -0,0 +1,53 @@
package com.xiaoqu.watch.ui.common
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.xiaoqu.watch.network.ApiResult
import kotlinx.coroutines.launch
/**
* 所有 ViewModel 的基类
* 提供通用的 Loading/Error 状态管理和安全调用方法
*/
open class BaseViewModel : ViewModel() {
private val _loading = MutableLiveData(false)
val loading: LiveData<Boolean> = _loading
private val _error = MutableLiveData<String?>()
val error: LiveData<String?> = _error
/**
* 安全调用:自动管理 loading 和 error 状态
*/
protected fun <T> launch(
block: suspend () -> ApiResult<T>,
onSuccess: (T) -> Unit,
onError: ((String) -> Unit)? = null
) {
viewModelScope.launch {
_loading.value = true
_error.value = null
when (val result = block()) {
is ApiResult.Success -> onSuccess(result.data)
is ApiResult.Error -> {
_error.value = result.message
onError?.invoke(result.message)
}
is ApiResult.NetworkError -> {
val msg = "网络连接异常"
_error.value = msg
onError?.invoke(msg)
}
}
_loading.value = false
}
}
/**
* 子类覆盖,实现重新加载(配合 StateLayout 重试按钮)
*/
open fun retry() {}
}

View File

@@ -0,0 +1,121 @@
package com.xiaoqu.watch.ui.home
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import com.xiaoqu.watch.R
import com.xiaoqu.watch.data.prefs.DevicePrefs
import com.xiaoqu.watch.data.prefs.UserPrefs
import com.xiaoqu.watch.databinding.FragmentHomeBinding
import com.xiaoqu.watch.ui.common.BaseFragment
import com.xiaoqu.watch.ui.widget.NavBarHelper
import com.xiaoqu.watch.ui.widget.QuConfirmDialog
import com.xiaoqu.watch.ui.widget.QuTipDialog
import com.xiaoqu.watch.util.DateUtil
import com.xiaoqu.watch.util.DeviceUtil
import com.xiaoqu.watch.util.NetworkUtil
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
/**
* 首页 Fragment
* 当前为 UI 组件 demo 页面,展示 NavBar、按钮样式、弹窗组件
*/
@AndroidEntryPoint
class HomeFragment : BaseFragment<FragmentHomeBinding>() {
@Inject lateinit var devicePrefs: DevicePrefs
@Inject lateinit var userPrefs: UserPrefs
/** 提示弹窗(挂载到 Activity 的 dialog_container */
private lateinit var tipDialog: QuTipDialog
/** 确认弹窗 */
private lateinit var confirmDialog: QuConfirmDialog
override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentHomeBinding {
return FragmentHomeBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 初始化设备信息
initDevicePrefs()
// 设置 NavBar 为首页模式(状态图标 + 时间 + 电量)
NavBarHelper.setupHomePage(binding.root)
// 初始化弹窗(使用 Activity 的全局弹窗容器)
val dialogContainer = requireActivity().findViewById<FrameLayout>(R.id.dialog_container)
tipDialog = QuTipDialog(dialogContainer)
confirmDialog = QuConfirmDialog(dialogContainer)
// 显示 demo 信息
showDemoInfo()
// 绑定按钮事件
setupButtons()
}
/** 首次启动时初始化设备信息到 device_prefs */
private fun initDevicePrefs() {
if (!devicePrefs.isInitialized) {
val info = DeviceUtil.getDeviceInfo(requireContext())
devicePrefs.saveDeviceInfo(
imei = info.imei,
serial = info.serial,
bluetoothName = info.bluetoothName,
bluetoothMac = info.bluetoothMac,
brand = info.brand,
model = info.model,
osVersion = info.osVersion,
totalMemory = info.totalMemory
)
}
}
/** 显示设备和网络基本信息 */
private fun showDemoInfo() {
val dateInfo = DateUtil.getDateInfo()
val sb = StringBuilder()
sb.appendLine("${dateInfo.date} ${dateInfo.week}")
sb.appendLine("设备: ${devicePrefs.brand} ${devicePrefs.model}")
sb.appendLine("网络: ${NetworkUtil.getNetworkTypeName(requireContext())}")
sb.appendLine("绑定: ${if (userPrefs.isBound) "是" else "否"}")
binding.tvDemoInfo.text = sb.toString()
}
/** 绑定按钮点击事件 */
private fun setupButtons() {
// 显示提示弹窗成功状态3 秒倒计时后自动关闭)
binding.btnShowTip.setOnClickListener {
tipDialog.show(
status = QuTipDialog.Status.SUCCESS,
title = "操作成功",
desc = "这是一个提示弹窗 demo",
back = true,
step = 0, // 只关闭,不返回
countdown = 3
)
}
// 显示确认弹窗
binding.btnShowConfirm.setOnClickListener {
confirmDialog.showText(
text = "确认执行此操作?",
onConfirm = {
// 确认后显示成功提示
tipDialog.show(
status = QuTipDialog.Status.SUCCESS,
title = "已确认",
back = true,
step = 0,
countdown = 2
)
}
)
}
}
}

View File

@@ -0,0 +1,15 @@
package com.xiaoqu.watch.ui.info
import android.view.LayoutInflater
import android.view.ViewGroup
import com.xiaoqu.watch.databinding.FragmentInfoBinding
import com.xiaoqu.watch.ui.common.BaseFragment
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class InfoFragment : BaseFragment<FragmentInfoBinding>() {
override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentInfoBinding {
return FragmentInfoBinding.inflate(inflater, container, false)
}
}

View File

@@ -0,0 +1,15 @@
package com.xiaoqu.watch.ui.punch
import android.view.LayoutInflater
import android.view.ViewGroup
import com.xiaoqu.watch.databinding.FragmentPunchBinding
import com.xiaoqu.watch.ui.common.BaseFragment
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class PunchFragment : BaseFragment<FragmentPunchBinding>() {
override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentPunchBinding {
return FragmentPunchBinding.inflate(inflater, container, false)
}
}

View File

@@ -0,0 +1,15 @@
package com.xiaoqu.watch.ui.task
import android.view.LayoutInflater
import android.view.ViewGroup
import com.xiaoqu.watch.databinding.FragmentTaskDetailBinding
import com.xiaoqu.watch.ui.common.BaseFragment
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class TaskDetailFragment : BaseFragment<FragmentTaskDetailBinding>() {
override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentTaskDetailBinding {
return FragmentTaskDetailBinding.inflate(inflater, container, false)
}
}

View File

@@ -0,0 +1,15 @@
package com.xiaoqu.watch.ui.task
import android.view.LayoutInflater
import android.view.ViewGroup
import com.xiaoqu.watch.databinding.FragmentTaskListBinding
import com.xiaoqu.watch.ui.common.BaseFragment
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class TaskListFragment : BaseFragment<FragmentTaskListBinding>() {
override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentTaskListBinding {
return FragmentTaskListBinding.inflate(inflater, container, false)
}
}

View File

@@ -0,0 +1,103 @@
package com.xiaoqu.watch.ui.widget
/**
* iconfont 图标 Unicode 映射
* 对应旧版 static/style/iconfont.css 中的图标编码
* 使用方式TextView.text = IconFont.BLUETOOTH并设置 typeface 为 iconfont.ttf
*/
object IconFont {
// ===== 导航栏状态图标 =====
/** 蓝牙已连接 */
const val BLUETOOTH = "\ue6da"
/** 蓝牙未连接 */
const val BLUETOOTH_OFF = "\ue6db"
/** 4G 已连接 */
const val SIGNAL_4G = "\ue6dc"
/** 4G 未连接 */
const val SIGNAL_4G_OFF = "\ue6d9"
/** NFC */
const val NFC = "\ue6d8"
// ===== 电量图标6 级) =====
/** 电量 0-10% */
const val BATTERY_10 = "\ue6d0"
/** 电量 11-20% */
const val BATTERY_20 = "\ue6cd"
/** 电量 21-40% */
const val BATTERY_40 = "\ue6d1"
/** 电量 41-60% */
const val BATTERY_60 = "\ue6cc"
/** 电量 61-99% */
const val BATTERY_80 = "\ue6cf"
/** 电量 100% */
const val BATTERY_100 = "\ue6ce"
/** 充电中 */
const val CHARGING = "\ue6ca"
/** 电量极低 */
const val BATTERY_LOW = "\ue7a4"
// ===== 提示弹窗图标 =====
/** 成功(✓) */
const val SUCCESS = "\ue600"
/** 错误(×) */
const val ERROR = "\ue623"
/** 警告(! */
const val WARNING = "\ue685"
/** 定位打卡 */
const val LOCATION = "\ue6c0"
// ===== 导航图标 =====
/** 返回 */
const val BACK = "\ue6bd"
// ===== 功能图标 =====
/** 未读消息 */
const val UNREAD_MESSAGE = "\ue6e3"
/** 邮件 */
const val MAIL = "\uebca"
/** 设置 */
const val SETTINGS = "\ue8b8"
/** 硬件信息 */
const val HARDWARE_INFO = "\ue6c9"
/** 查看 */
const val VIEW = "\ue6d2"
/** 日历/选择时间 */
const val CALENDAR = "\ue6c4"
/** 调试模式 */
const val DEBUG = "\ue6c1"
/** 蓝牙定位数据 */
const val BLE_DATA = "\ue6c3"
/** 蓝牙连接记录 */
const val BLE_LOG = "\ue6c5"
/** 蓝牙通信日志 */
const val BLE_COMM_LOG = "\ue6c7"
/** 计步数据 */
const val STEP_DATA = "\ue6c8"
/** 存储空间 */
const val STORAGE = "\ue6bf"
/** 喇叭/声音 */
const val SOUND = "\ue601"
/** WiFi */
const val WIFI = "\ue604"
/** 账号 */
const val ACCOUNT = "\ue60d"
/** 日志 */
const val LOG = "\ue69a"
/**
* 根据电量百分比返回对应的电量图标
* @param level 电量百分比 0-100
* @param isCharging 是否充电中
*/
fun getBatteryIcon(level: Int, isCharging: Boolean): String {
if (isCharging) return CHARGING
return when {
level <= 10 -> BATTERY_10
level <= 20 -> BATTERY_20
level <= 40 -> BATTERY_40
level <= 60 -> BATTERY_60
level <= 99 -> BATTERY_80
else -> BATTERY_100
}
}
}

View File

@@ -0,0 +1,110 @@
package com.xiaoqu.watch.ui.widget
import android.graphics.Typeface
import android.view.View
import android.widget.TextView
import com.xiaoqu.watch.R
import com.xiaoqu.watch.util.DateUtil
/**
* NavBar 辅助工具
* 帮助 Fragment 配置导航栏的显示模式(首页模式 / 子页面模式)
*
* 使用方式:
* - 首页NavBarHelper.setupHomePage(binding.root)
* - 子页面NavBarHelper.setupSubPage(binding.root, "标题") { navBack() }
*/
object NavBarHelper {
/**
* 设置为首页模式:左=状态图标,中=时间,右=电量
* @param rootView 包含 layout_nav_bar 的根视图
*/
fun setupHomePage(rootView: View) {
// 加载 iconfont 字体
val typeface = Typeface.createFromAsset(rootView.context.assets, "font/iconfont.ttf")
// 左侧:显示状态图标,隐藏返回按钮
rootView.findViewById<View>(R.id.statusIcons)?.visibility = View.VISIBLE
rootView.findViewById<View>(R.id.btnBack)?.visibility = View.GONE
// 状态图标设置字体
applyIconFont(rootView, R.id.iconBluetooth, typeface)
applyIconFont(rootView, R.id.icon4G, typeface)
applyIconFont(rootView, R.id.iconNfc, typeface)
// 中间:显示时间
rootView.findViewById<TextView>(R.id.navTitle)?.text = DateUtil.formatTimeShort()
// 右侧:电量图标设置字体
applyIconFont(rootView, R.id.iconBattery, typeface)
}
/**
* 设置为子页面模式:左=返回按钮,中=标题,右=电量
* @param rootView 包含 layout_nav_bar 的根视图
* @param title 页面标题
* @param onBackClick 返回按钮点击回调
*/
fun setupSubPage(rootView: View, title: String, onBackClick: () -> Unit) {
val typeface = Typeface.createFromAsset(rootView.context.assets, "font/iconfont.ttf")
// 左侧:隐藏状态图标,显示返回按钮
rootView.findViewById<View>(R.id.statusIcons)?.visibility = View.GONE
val btnBack = rootView.findViewById<TextView>(R.id.btnBack)
btnBack?.visibility = View.VISIBLE
btnBack?.typeface = typeface
btnBack?.setOnClickListener { onBackClick() }
// 中间:显示标题
rootView.findViewById<TextView>(R.id.navTitle)?.text = title
// 右侧:电量图标设置字体
applyIconFont(rootView, R.id.iconBattery, typeface)
}
/**
* 更新电量图标
* @param rootView 包含 layout_nav_bar 的根视图
* @param level 电量百分比 0-100
* @param isCharging 是否充电中
*/
fun updateBattery(rootView: View, level: Int, isCharging: Boolean) {
val iconBattery = rootView.findViewById<TextView>(R.id.iconBattery) ?: return
iconBattery.text = IconFont.getBatteryIcon(level, isCharging)
// 充电中显示绿色,低电量显示红色,其他白色
val colorRes = when {
isCharging -> R.color.charge_green
level <= 10 -> R.color.disconnect_red
else -> R.color.text_primary
}
iconBattery.setTextColor(iconBattery.context.getColor(colorRes))
}
/**
* 更新蓝牙状态图标
*/
fun updateBluetooth(rootView: View, isConnected: Boolean) {
val icon = rootView.findViewById<TextView>(R.id.iconBluetooth) ?: return
icon.text = if (isConnected) IconFont.BLUETOOTH else IconFont.BLUETOOTH_OFF
icon.setTextColor(icon.context.getColor(
if (isConnected) R.color.text_primary else R.color.text_secondary
))
}
/**
* 更新 4G 状态图标
*/
fun update4G(rootView: View, isConnected: Boolean) {
val icon = rootView.findViewById<TextView>(R.id.icon4G) ?: return
icon.text = if (isConnected) IconFont.SIGNAL_4G else IconFont.SIGNAL_4G_OFF
icon.setTextColor(icon.context.getColor(
if (isConnected) R.color.text_primary else R.color.text_secondary
))
}
/** 为 TextView 设置 iconfont 字体 */
private fun applyIconFont(rootView: View, viewId: Int, typeface: Typeface) {
rootView.findViewById<TextView>(viewId)?.typeface = typeface
}
}

View File

@@ -0,0 +1,109 @@
package com.xiaoqu.watch.ui.widget
import android.graphics.Typeface
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.TextView
import com.xiaoqu.watch.R
/**
* 确认弹窗(对应旧版 qu-confirm.vue
* 半透明遮罩 + 内容区 + 底部取消(×)/确认(✓)图标按钮
*
* 使用方式:
* ```
* val confirm = QuConfirmDialog(binding.dialogContainer)
* confirm.show(
* contentView = myContentView, // 或传 contentText
* onConfirm = { /* 确认操作 */ },
* onCancel = { /* 取消操作 */ }
* )
* ```
*/
class QuConfirmDialog(
private val container: FrameLayout
) {
private var confirmView: View? = null
/**
* 显示确认弹窗(自定义内容 View
* @param contentView 内容区域的自定义 View
* @param onConfirm 确认按钮回调
* @param onCancel 取消按钮回调(默认关闭弹窗)
*/
fun show(
contentView: View? = null,
onConfirm: () -> Unit,
onCancel: (() -> Unit)? = null
) {
dismiss()
val view = LayoutInflater.from(container.context)
.inflate(R.layout.dialog_confirm, container, false)
confirmView = view
// 加载 iconfont 字体
val typeface = Typeface.createFromAsset(container.context.assets, "font/iconfont.ttf")
// 设置内容区域
if (contentView != null) {
val contentContainer = view.findViewById<FrameLayout>(R.id.confirmContent)
contentContainer.addView(contentView)
}
// 取消按钮
val btnCancel = view.findViewById<TextView>(R.id.btnCancel)
btnCancel.typeface = typeface
btnCancel.setOnClickListener {
dismiss()
onCancel?.invoke()
}
// 确认按钮
val btnConfirm = view.findViewById<TextView>(R.id.btnConfirm)
btnConfirm.typeface = typeface
btnConfirm.setOnClickListener {
dismiss()
onConfirm()
}
// 点击遮罩不关闭(手表误触概率高)
view.findViewById<View>(R.id.confirmOverlay).setOnClickListener { /* 拦截点击 */ }
// 显示弹窗
container.addView(view)
container.visibility = View.VISIBLE
}
/**
* 显示确认弹窗(纯文字内容)
* @param text 提示文字
* @param onConfirm 确认按钮回调
* @param onCancel 取消按钮回调
*/
fun showText(
text: String,
onConfirm: () -> Unit,
onCancel: (() -> Unit)? = null
) {
val textView = TextView(container.context).apply {
this.text = text
setTextColor(context.getColor(R.color.text_primary))
textSize = 15f
gravity = android.view.Gravity.CENTER
}
show(contentView = textView, onConfirm = onConfirm, onCancel = onCancel)
}
/** 关闭弹窗 */
fun dismiss() {
confirmView?.let { container.removeView(it) }
confirmView = null
container.visibility = View.GONE
}
/** 弹窗是否正在显示 */
val isShowing: Boolean get() = confirmView != null
}

View File

@@ -0,0 +1,148 @@
package com.xiaoqu.watch.ui.widget
import android.graphics.Typeface
import android.os.CountDownTimer
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.TextView
import com.xiaoqu.watch.R
/**
* 提示弹窗(对应旧版 qu-tip.vue
* 显示状态图标(成功/警告/错误)+ 标题 + 可选描述 + 倒计时自动关闭/返回
*
* 使用方式:
* ```
* val tip = QuTipDialog(binding.dialogContainer)
* tip.show(
* status = QuTipDialog.Status.SUCCESS,
* title = "打卡成功",
* desc = "已记录考勤",
* back = true,
* step = 1, // 0=只关闭1=返回上一页
* countdown = 3,
* onBack = { findMainNavController().popBackStack() }
* )
* ```
*/
class QuTipDialog(
private val container: FrameLayout
) {
/** 提示状态类型 */
enum class Status { SUCCESS, WARNING, ERROR, LOCATION }
private var tipView: View? = null
private var timer: CountDownTimer? = null
/**
* 显示提示弹窗
* @param status 状态类型(成功/警告/错误/定位)
* @param title 标题文字
* @param desc 描述文字(可选)
* @param back 是否显示倒计时返回按钮
* @param step 倒计时结束后行为0=只关闭弹窗,>0=触发 onBack 回调
* @param countdown 倒计时秒数(默认 3 秒)
* @param onBack 返回回调step > 0 时触发)
*/
fun show(
status: Status,
title: String,
desc: String? = null,
back: Boolean = true,
step: Int = 1,
countdown: Int = 3,
onBack: (() -> Unit)? = null
) {
// 先移除旧弹窗
dismiss()
// 加载布局
val view = LayoutInflater.from(container.context)
.inflate(R.layout.dialog_tip, container, false)
tipView = view
// 加载 iconfont 字体
val typeface = Typeface.createFromAsset(container.context.assets, "font/iconfont.ttf")
// 设置状态图标和颜色
val iconView = view.findViewById<TextView>(R.id.tipIcon)
iconView.typeface = typeface
when (status) {
Status.SUCCESS -> {
iconView.text = IconFont.SUCCESS
iconView.setTextColor(container.context.getColor(R.color.success))
}
Status.WARNING -> {
iconView.text = IconFont.WARNING
iconView.setTextColor(container.context.getColor(R.color.warning))
}
Status.ERROR -> {
iconView.text = IconFont.ERROR
iconView.setTextColor(container.context.getColor(R.color.error))
}
Status.LOCATION -> {
iconView.text = IconFont.LOCATION
iconView.setTextColor(container.context.getColor(R.color.success))
}
}
// 设置标题
view.findViewById<TextView>(R.id.tipTitle).text = title
// 设置描述(可选)
val descView = view.findViewById<TextView>(R.id.tipDesc)
if (desc != null) {
descView.text = desc
descView.visibility = View.VISIBLE
}
// 倒计时返回按钮
val backBtn = view.findViewById<TextView>(R.id.tipBackBtn)
if (back) {
backBtn.visibility = View.VISIBLE
backBtn.typeface = typeface
// 启动倒计时
timer = object : CountDownTimer(countdown * 1000L, 1000L) {
override fun onTick(millisUntilFinished: Long) {
val seconds = (millisUntilFinished / 1000) + 1
backBtn.text = "${IconFont.BACK} ${seconds}s"
}
override fun onFinish() {
dismiss()
// step > 0 时触发返回回调
if (step > 0) {
onBack?.invoke()
}
}
}.start()
// 点击立即返回(不等倒计时)
backBtn.setOnClickListener {
dismiss()
if (step > 0) {
onBack?.invoke()
}
}
}
// 显示弹窗
container.addView(view)
container.visibility = View.VISIBLE
}
/** 关闭弹窗 */
fun dismiss() {
timer?.cancel()
timer = null
tipView?.let { container.removeView(it) }
tipView = null
container.visibility = View.GONE
}
/** 弹窗是否正在显示 */
val isShowing: Boolean get() = tipView != null
}

View File

@@ -0,0 +1,78 @@
package com.xiaoqu.watch.util
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
/**
* 日期格式化工具
* 对应旧版 commonUtil.js 的 getDateTime()
*/
object DateUtil {
private val weekNames = arrayOf("周日", "周一", "周二", "周三", "周四", "周五", "周六")
private val formatDateTime = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA)
private val formatDate = SimpleDateFormat("MM月dd日", Locale.CHINA)
private val formatTime = SimpleDateFormat("HH:mm:ss", Locale.CHINA)
private val formatTimeShort = SimpleDateFormat("HH:mm", Locale.CHINA)
/** 完整日期时间2026-04-24 20:15:30 */
fun formatDateTime(timestamp: Long = System.currentTimeMillis()): String {
return formatDateTime.format(Date(timestamp))
}
/** 月日04月24日 */
fun formatDate(timestamp: Long = System.currentTimeMillis()): String {
return formatDate.format(Date(timestamp))
}
/** 时分秒20:15:30 */
fun formatTime(timestamp: Long = System.currentTimeMillis()): String {
return formatTime.format(Date(timestamp))
}
/** 时分20:15 */
fun formatTimeShort(timestamp: Long = System.currentTimeMillis()): String {
return formatTimeShort.format(Date(timestamp))
}
/** 星期:周四 */
fun getWeekDay(timestamp: Long = System.currentTimeMillis()): String {
val calendar = Calendar.getInstance()
calendar.timeInMillis = timestamp
return weekNames[calendar.get(Calendar.DAY_OF_WEEK) - 1]
}
/**
* 获取完整日期信息(对应旧版 getDateTime() 返回对象)
*/
fun getDateInfo(timestamp: Long = System.currentTimeMillis()): DateInfo {
val calendar = Calendar.getInstance()
calendar.timeInMillis = timestamp
return DateInfo(
date = formatDate(timestamp),
time = formatTime(timestamp),
week = weekNames[calendar.get(Calendar.DAY_OF_WEEK) - 1],
year = calendar.get(Calendar.YEAR),
month = calendar.get(Calendar.MONTH) + 1,
day = calendar.get(Calendar.DAY_OF_MONTH),
hour = calendar.get(Calendar.HOUR_OF_DAY),
minute = calendar.get(Calendar.MINUTE),
second = calendar.get(Calendar.SECOND)
)
}
data class DateInfo(
val date: String,
val time: String,
val week: String,
val year: Int,
val month: Int,
val day: Int,
val hour: Int,
val minute: Int,
val second: Int
)
}

View File

@@ -0,0 +1,116 @@
package com.xiaoqu.watch.util
import android.annotation.SuppressLint
import android.app.ActivityManager
import android.bluetooth.BluetoothAdapter
import android.content.Context
import android.os.Build
import android.telephony.TelephonyManager
import timber.log.Timber
/**
* 设备信息工具
* 对应旧版 deviceInfoUtil.js 的 getDevice()
*/
object DeviceUtil {
/** 设备品牌 */
fun getBrand(): String = Build.BRAND
/** 设备型号 */
fun getModel(): String = Build.MODEL
/** 系统版本 */
fun getOsVersion(): String = Build.VERSION.RELEASE
/** 设备序列号 */
@SuppressLint("HardwareIds")
fun getSerial(): String {
return try {
Build.SERIAL ?: ""
} catch (e: Exception) {
Timber.w(e, "获取序列号失败")
""
}
}
/** 设备 IMEI需要 READ_PHONE_STATE 权限) */
@SuppressLint("HardwareIds", "MissingPermission")
fun getImei(context: Context): String {
return try {
val tm = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
@Suppress("DEPRECATION")
tm.deviceId ?: ""
} catch (e: Exception) {
Timber.w(e, "获取IMEI失败")
""
}
}
/** 蓝牙适配器名称 */
@SuppressLint("HardwareIds")
fun getBluetoothName(): String {
return try {
BluetoothAdapter.getDefaultAdapter()?.name ?: ""
} catch (e: Exception) {
Timber.w(e, "获取蓝牙名称失败")
""
}
}
/** 蓝牙 MAC 地址 */
@SuppressLint("HardwareIds")
fun getBluetoothMac(): String {
return try {
BluetoothAdapter.getDefaultAdapter()?.address ?: ""
} catch (e: Exception) {
Timber.w(e, "获取蓝牙MAC失败")
""
}
}
/** 总内存MB */
fun getTotalMemory(context: Context): Long {
val memInfo = ActivityManager.MemoryInfo()
val am = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
am.getMemoryInfo(memInfo)
return memInfo.totalMem / (1024 * 1024)
}
/** 可用内存MB */
fun getAvailableMemory(context: Context): Long {
val memInfo = ActivityManager.MemoryInfo()
val am = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
am.getMemoryInfo(memInfo)
return memInfo.availMem / (1024 * 1024)
}
/**
* 获取完整设备信息(对应旧版 watchInfo 对象)
*/
fun getDeviceInfo(context: Context): DeviceInfo {
return DeviceInfo(
brand = getBrand(),
model = getModel(),
osVersion = getOsVersion(),
serial = getSerial(),
imei = getImei(context),
bluetoothName = getBluetoothName(),
bluetoothMac = getBluetoothMac(),
totalMemory = getTotalMemory(context),
availableMemory = getAvailableMemory(context)
)
}
data class DeviceInfo(
val brand: String,
val model: String,
val osVersion: String,
val serial: String,
val imei: String,
val bluetoothName: String,
val bluetoothMac: String,
val totalMemory: Long,
val availableMemory: Long
)
}

View File

@@ -0,0 +1,22 @@
package com.xiaoqu.watch.util
import java.security.MessageDigest
/**
* MD5 哈希工具
* 对应旧版 md5.js 的 hex_md5(),用于 API 请求签名
*/
object Md5Util {
/** 计算字符串的 MD5 哈希值(小写 hex */
fun md5(input: String): String {
val digest = MessageDigest.getInstance("MD5")
val bytes = digest.digest(input.toByteArray())
return bytes.joinToString("") { "%02x".format(it) }
}
/** 计算字符串的 MD5 哈希值(大写 hex */
fun md5Upper(input: String): String {
return md5(input).uppercase()
}
}

View File

@@ -0,0 +1,49 @@
package com.xiaoqu.watch.util
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkInfo
/**
* 网络状态检测工具
* 对应旧版 systemUtil.js 的 getNetWorkAvailable()
*/
object NetworkUtil {
/** 网络是否可用 */
@Suppress("DEPRECATION")
fun isNetworkAvailable(context: Context): Boolean {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val activeNetwork: NetworkInfo? = cm.activeNetworkInfo
return activeNetwork?.isConnected == true
}
/** 是否连接 WiFi */
@Suppress("DEPRECATION")
fun isWifiConnected(context: Context): Boolean {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val wifiInfo = cm.getNetworkInfo(ConnectivityManager.TYPE_WIFI)
return wifiInfo?.isConnected == true
}
/** 是否连接移动数据4G */
@Suppress("DEPRECATION")
fun isMobileConnected(context: Context): Boolean {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val mobileInfo = cm.getNetworkInfo(ConnectivityManager.TYPE_MOBILE)
return mobileInfo?.isConnected == true
}
/** 获取当前网络类型描述 */
@Suppress("DEPRECATION")
fun getNetworkTypeName(context: Context): String {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val activeNetwork: NetworkInfo? = cm.activeNetworkInfo
return when {
activeNetwork == null || !activeNetwork.isConnected -> "无网络"
activeNetwork.type == ConnectivityManager.TYPE_WIFI -> "WiFi"
activeNetwork.type == ConnectivityManager.TYPE_MOBILE -> "移动数据"
else -> "其他"
}
}
}