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:
23
app/src/main/java/com/xiaoqu/watch/app/BootReceiver.kt
Normal file
23
app/src/main/java/com/xiaoqu/watch/app/BootReceiver.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
53
app/src/main/java/com/xiaoqu/watch/app/MainActivity.kt
Normal file
53
app/src/main/java/com/xiaoqu/watch/app/MainActivity.kt
Normal 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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
27
app/src/main/java/com/xiaoqu/watch/app/WatchApplication.kt
Normal file
27
app/src/main/java/com/xiaoqu/watch/app/WatchApplication.kt
Normal 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")
|
||||
}
|
||||
}
|
||||
89
app/src/main/java/com/xiaoqu/watch/data/prefs/DevicePrefs.kt
Normal file
89
app/src/main/java/com/xiaoqu/watch/data/prefs/DevicePrefs.kt
Normal 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"
|
||||
}
|
||||
}
|
||||
61
app/src/main/java/com/xiaoqu/watch/data/prefs/UserPrefs.kt
Normal file
61
app/src/main/java/com/xiaoqu/watch/data/prefs/UserPrefs.kt
Normal 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"
|
||||
}
|
||||
}
|
||||
65
app/src/main/java/com/xiaoqu/watch/di/NetworkModule.kt
Normal file
65
app/src/main/java/com/xiaoqu/watch/di/NetworkModule.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
20
app/src/main/java/com/xiaoqu/watch/event/AppEvent.kt
Normal file
20
app/src/main/java/com/xiaoqu/watch/event/AppEvent.kt
Normal 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()
|
||||
}
|
||||
22
app/src/main/java/com/xiaoqu/watch/event/EventBus.kt
Normal file
22
app/src/main/java/com/xiaoqu/watch/event/EventBus.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
21
app/src/main/java/com/xiaoqu/watch/network/ApiResponse.kt
Normal file
21
app/src/main/java/com/xiaoqu/watch/network/ApiResponse.kt
Normal 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
|
||||
}
|
||||
}
|
||||
11
app/src/main/java/com/xiaoqu/watch/network/ApiResult.kt
Normal file
11
app/src/main/java/com/xiaoqu/watch/network/ApiResult.kt
Normal 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>()
|
||||
}
|
||||
13
app/src/main/java/com/xiaoqu/watch/network/EnvConfig.kt
Normal file
13
app/src/main/java/com/xiaoqu/watch/network/EnvConfig.kt
Normal 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
|
||||
}
|
||||
22
app/src/main/java/com/xiaoqu/watch/network/SafeApiCall.kt
Normal file
22
app/src/main/java/com/xiaoqu/watch/network/SafeApiCall.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
34
app/src/main/java/com/xiaoqu/watch/network/api/CommonApi.kt
Normal file
34
app/src/main/java/com/xiaoqu/watch/network/api/CommonApi.kt
Normal 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>
|
||||
}
|
||||
15
app/src/main/java/com/xiaoqu/watch/ui/bind/BindFragment.kt
Normal file
15
app/src/main/java/com/xiaoqu/watch/ui/bind/BindFragment.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
66
app/src/main/java/com/xiaoqu/watch/ui/common/BaseFragment.kt
Normal file
66
app/src/main/java/com/xiaoqu/watch/ui/common/BaseFragment.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
121
app/src/main/java/com/xiaoqu/watch/ui/home/HomeFragment.kt
Normal file
121
app/src/main/java/com/xiaoqu/watch/ui/home/HomeFragment.kt
Normal 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
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
15
app/src/main/java/com/xiaoqu/watch/ui/info/InfoFragment.kt
Normal file
15
app/src/main/java/com/xiaoqu/watch/ui/info/InfoFragment.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
15
app/src/main/java/com/xiaoqu/watch/ui/punch/PunchFragment.kt
Normal file
15
app/src/main/java/com/xiaoqu/watch/ui/punch/PunchFragment.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
103
app/src/main/java/com/xiaoqu/watch/ui/widget/IconFont.kt
Normal file
103
app/src/main/java/com/xiaoqu/watch/ui/widget/IconFont.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
110
app/src/main/java/com/xiaoqu/watch/ui/widget/NavBarHelper.kt
Normal file
110
app/src/main/java/com/xiaoqu/watch/ui/widget/NavBarHelper.kt
Normal 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
|
||||
}
|
||||
}
|
||||
109
app/src/main/java/com/xiaoqu/watch/ui/widget/QuConfirmDialog.kt
Normal file
109
app/src/main/java/com/xiaoqu/watch/ui/widget/QuConfirmDialog.kt
Normal 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
|
||||
}
|
||||
148
app/src/main/java/com/xiaoqu/watch/ui/widget/QuTipDialog.kt
Normal file
148
app/src/main/java/com/xiaoqu/watch/ui/widget/QuTipDialog.kt
Normal 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
|
||||
}
|
||||
78
app/src/main/java/com/xiaoqu/watch/util/DateUtil.kt
Normal file
78
app/src/main/java/com/xiaoqu/watch/util/DateUtil.kt
Normal 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
|
||||
)
|
||||
}
|
||||
116
app/src/main/java/com/xiaoqu/watch/util/DeviceUtil.kt
Normal file
116
app/src/main/java/com/xiaoqu/watch/util/DeviceUtil.kt
Normal 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
|
||||
)
|
||||
}
|
||||
22
app/src/main/java/com/xiaoqu/watch/util/Md5Util.kt
Normal file
22
app/src/main/java/com/xiaoqu/watch/util/Md5Util.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
49
app/src/main/java/com/xiaoqu/watch/util/NetworkUtil.kt
Normal file
49
app/src/main/java/com/xiaoqu/watch/util/NetworkUtil.kt
Normal 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 -> "其他"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user