commit a397985954932341a8103fc02ae82f1c27ef8a4a Author: dongliang Date: Mon Apr 27 11:26:50 2026 +0930 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) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..460f8c6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties + +# JVM crash logs +hs_err_*.log +replay_*.log diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..eaf91e2 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..d58d49b --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..8b3f102 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..0d46093 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..22d9498 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..48052b2 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..ae3bf2d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..5bd6771 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..cdb0a89 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,109 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.ksp) + alias(libs.plugins.hilt) +} + +android { + namespace = "com.xiaoqu.watch" + compileSdk = 36 + + defaultConfig { + applicationId = "com.xiaoqu.watch" + minSdk = 27 + targetSdk = 27 + versionCode = 1 + versionName = "2.0.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + debug { + // TODO: 内网测试时改回 http://192.168.1.181:8091/ + buildConfigField("String", "SERVICE_URL", "\"https://app.updatexiaoqu.com:9443/\"") + buildConfigField("String", "MQTT_URL", "\"mqtt.ququbranch.com:8085/mqtt\"") + } + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + buildConfigField("String", "SERVICE_URL", "\"https://app.updatexiaoqu.com:9443/\"") + buildConfigField("String", "MQTT_URL", "\"mqtt.ququbranch.com:8085/mqtt\"") + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + } + + buildFeatures { + viewBinding = true + buildConfig = true + } +} + +dependencies { + // Core + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.material) + implementation(libs.androidx.activity) + implementation(libs.androidx.fragment) + implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.viewpager2) + implementation(libs.androidx.swiperefreshlayout) + + // Lifecycle + implementation(libs.androidx.lifecycle.viewmodel) + implementation(libs.androidx.lifecycle.livedata) + implementation(libs.androidx.lifecycle.runtime) + + // Navigation + implementation(libs.androidx.navigation.fragment) + implementation(libs.androidx.navigation.ui) + + // Hilt + implementation(libs.hilt.android) + ksp(libs.hilt.compiler) + + // Coroutines + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.coroutines.android) + + // Room + implementation(libs.androidx.room.runtime) + implementation(libs.androidx.room.ktx) + ksp(libs.androidx.room.compiler) + + // Network + implementation(libs.retrofit) + implementation(libs.retrofit.converter.gson) + implementation(libs.okhttp) + implementation(libs.okhttp.logging) + implementation(libs.gson) + + // MQTT + implementation(libs.paho.mqtt) + + // Logging + implementation(libs.timber) + + // QR Code + implementation(libs.zxing.core) + + // Test + testImplementation(libs.junit) + testImplementation(libs.mockk) + testImplementation(libs.kotlinx.coroutines.test) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/xiaoqu/watch/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/xiaoqu/watch/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..1f0ecb0 --- /dev/null +++ b/app/src/androidTest/java/com/xiaoqu/watch/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.xiaoqu.watch + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.xiaoqu.watch", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..520ccb4 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/assets/font/iconfont.ttf b/app/src/main/assets/font/iconfont.ttf new file mode 100644 index 0000000..ea62e25 Binary files /dev/null and b/app/src/main/assets/font/iconfont.ttf differ diff --git a/app/src/main/java/com/xiaoqu/watch/app/BootReceiver.kt b/app/src/main/java/com/xiaoqu/watch/app/BootReceiver.kt new file mode 100644 index 0000000..8836802 --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/app/BootReceiver.kt @@ -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) + } + } +} diff --git a/app/src/main/java/com/xiaoqu/watch/app/MainActivity.kt b/app/src/main/java/com/xiaoqu/watch/app/MainActivity.kt new file mode 100644 index 0000000..982b368 --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/app/MainActivity.kt @@ -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") + } + }) + } +} diff --git a/app/src/main/java/com/xiaoqu/watch/app/WatchApplication.kt b/app/src/main/java/com/xiaoqu/watch/app/WatchApplication.kt new file mode 100644 index 0000000..5be2342 --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/app/WatchApplication.kt @@ -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") + } +} diff --git a/app/src/main/java/com/xiaoqu/watch/data/prefs/DevicePrefs.kt b/app/src/main/java/com/xiaoqu/watch/data/prefs/DevicePrefs.kt new file mode 100644 index 0000000..536e733 --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/data/prefs/DevicePrefs.kt @@ -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" + } +} diff --git a/app/src/main/java/com/xiaoqu/watch/data/prefs/UserPrefs.kt b/app/src/main/java/com/xiaoqu/watch/data/prefs/UserPrefs.kt new file mode 100644 index 0000000..e3b6167 --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/data/prefs/UserPrefs.kt @@ -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" + } +} diff --git a/app/src/main/java/com/xiaoqu/watch/di/NetworkModule.kt b/app/src/main/java/com/xiaoqu/watch/di/NetworkModule.kt new file mode 100644 index 0000000..60642a4 --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/di/NetworkModule.kt @@ -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) + } +} diff --git a/app/src/main/java/com/xiaoqu/watch/event/AppEvent.kt b/app/src/main/java/com/xiaoqu/watch/event/AppEvent.kt new file mode 100644 index 0000000..3b5d817 --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/event/AppEvent.kt @@ -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() +} diff --git a/app/src/main/java/com/xiaoqu/watch/event/EventBus.kt b/app/src/main/java/com/xiaoqu/watch/event/EventBus.kt new file mode 100644 index 0000000..b7cb16b --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/event/EventBus.kt @@ -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(replay = 0) + val events: SharedFlow = _events.asSharedFlow() + + suspend fun emit(event: AppEvent) { + _events.emit(event) + } +} diff --git a/app/src/main/java/com/xiaoqu/watch/network/ApiResponse.kt b/app/src/main/java/com/xiaoqu/watch/network/ApiResponse.kt new file mode 100644 index 0000000..8102224 --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/network/ApiResponse.kt @@ -0,0 +1,21 @@ +package com.xiaoqu.watch.network + +import com.google.gson.annotations.SerializedName + +/** + * 服务端统一响应格式 + * { "code": 0, "data": {...}, "err": "错误信息" } + */ +data class ApiResponse( + @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 + } +} diff --git a/app/src/main/java/com/xiaoqu/watch/network/ApiResult.kt b/app/src/main/java/com/xiaoqu/watch/network/ApiResult.kt new file mode 100644 index 0000000..2a6e89a --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/network/ApiResult.kt @@ -0,0 +1,11 @@ +package com.xiaoqu.watch.network + +/** + * 统一的 API 返回结果封装 + * 调用方用 when 表达式处理,编译器强制覆盖所有情况 + */ +sealed class ApiResult { + data class Success(val data: T) : ApiResult() + data class Error(val code: Int, val message: String) : ApiResult() + data class NetworkError(val exception: Throwable) : ApiResult() +} diff --git a/app/src/main/java/com/xiaoqu/watch/network/EnvConfig.kt b/app/src/main/java/com/xiaoqu/watch/network/EnvConfig.kt new file mode 100644 index 0000000..a238432 --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/network/EnvConfig.kt @@ -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 +} diff --git a/app/src/main/java/com/xiaoqu/watch/network/SafeApiCall.kt b/app/src/main/java/com/xiaoqu/watch/network/SafeApiCall.kt new file mode 100644 index 0000000..33eaaec --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/network/SafeApiCall.kt @@ -0,0 +1,22 @@ +package com.xiaoqu.watch.network + +import timber.log.Timber + +/** + * 安全 API 调用封装 + * 将 Retrofit suspend 调用转换为 ApiResult + */ +suspend fun safeApiCall(call: suspend () -> ApiResponse): ApiResult { + 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) + } +} diff --git a/app/src/main/java/com/xiaoqu/watch/network/SignatureInterceptor.kt b/app/src/main/java/com/xiaoqu/watch/network/SignatureInterceptor.kt new file mode 100644 index 0000000..caf68aa --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/network/SignatureInterceptor.kt @@ -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) + } +} diff --git a/app/src/main/java/com/xiaoqu/watch/network/UnbindInterceptor.kt b/app/src/main/java/com/xiaoqu/watch/network/UnbindInterceptor.kt new file mode 100644 index 0000000..c2743da --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/network/UnbindInterceptor.kt @@ -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 + } +} diff --git a/app/src/main/java/com/xiaoqu/watch/network/api/CommonApi.kt b/app/src/main/java/com/xiaoqu/watch/network/api/CommonApi.kt new file mode 100644 index 0000000..cb4087b --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/network/api/CommonApi.kt @@ -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): ApiResponse + + /** 根据 IMEI 查询手表信息 */ + @GET("watch/getWatchByImei") + suspend fun getWatchByImei(@Query("imei") imei: String): ApiResponse + + /** 检查版本更新 */ + @GET("newAppVersion/queryWatch") + suspend fun checkVersion(@Query("imei") imei: String): ApiResponse + + /** 上报设备状态(电量、蓝牙、NFC等) */ + @POST("watch/setWatchStatusByImeiFormWatch") + suspend fun reportDeviceStatus(@Body params: Map): ApiResponse + + /** 上报日志 */ + @POST("watchTestLog") + suspend fun reportLog(@Body params: Map): ApiResponse +} diff --git a/app/src/main/java/com/xiaoqu/watch/ui/bind/BindFragment.kt b/app/src/main/java/com/xiaoqu/watch/ui/bind/BindFragment.kt new file mode 100644 index 0000000..729b2f6 --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/ui/bind/BindFragment.kt @@ -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() { + + override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentBindBinding { + return FragmentBindBinding.inflate(inflater, container, false) + } +} diff --git a/app/src/main/java/com/xiaoqu/watch/ui/common/BaseFragment.kt b/app/src/main/java/com/xiaoqu/watch/ui/common/BaseFragment.kt new file mode 100644 index 0000000..4385437 --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/ui/common/BaseFragment.kt @@ -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 : 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() + } + } +} diff --git a/app/src/main/java/com/xiaoqu/watch/ui/common/BaseViewModel.kt b/app/src/main/java/com/xiaoqu/watch/ui/common/BaseViewModel.kt new file mode 100644 index 0000000..2d26ac8 --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/ui/common/BaseViewModel.kt @@ -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 = _loading + + private val _error = MutableLiveData() + val error: LiveData = _error + + /** + * 安全调用:自动管理 loading 和 error 状态 + */ + protected fun launch( + block: suspend () -> ApiResult, + 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() {} +} diff --git a/app/src/main/java/com/xiaoqu/watch/ui/home/HomeFragment.kt b/app/src/main/java/com/xiaoqu/watch/ui/home/HomeFragment.kt new file mode 100644 index 0000000..414e4ef --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/ui/home/HomeFragment.kt @@ -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() { + + @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(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 + ) + } + ) + } + } +} diff --git a/app/src/main/java/com/xiaoqu/watch/ui/info/InfoFragment.kt b/app/src/main/java/com/xiaoqu/watch/ui/info/InfoFragment.kt new file mode 100644 index 0000000..3fa1cf8 --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/ui/info/InfoFragment.kt @@ -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() { + + override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentInfoBinding { + return FragmentInfoBinding.inflate(inflater, container, false) + } +} diff --git a/app/src/main/java/com/xiaoqu/watch/ui/punch/PunchFragment.kt b/app/src/main/java/com/xiaoqu/watch/ui/punch/PunchFragment.kt new file mode 100644 index 0000000..2cc15bc --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/ui/punch/PunchFragment.kt @@ -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() { + + override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentPunchBinding { + return FragmentPunchBinding.inflate(inflater, container, false) + } +} diff --git a/app/src/main/java/com/xiaoqu/watch/ui/task/TaskDetailFragment.kt b/app/src/main/java/com/xiaoqu/watch/ui/task/TaskDetailFragment.kt new file mode 100644 index 0000000..61ff6bc --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/ui/task/TaskDetailFragment.kt @@ -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() { + + override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentTaskDetailBinding { + return FragmentTaskDetailBinding.inflate(inflater, container, false) + } +} diff --git a/app/src/main/java/com/xiaoqu/watch/ui/task/TaskListFragment.kt b/app/src/main/java/com/xiaoqu/watch/ui/task/TaskListFragment.kt new file mode 100644 index 0000000..15fc9d7 --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/ui/task/TaskListFragment.kt @@ -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() { + + override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentTaskListBinding { + return FragmentTaskListBinding.inflate(inflater, container, false) + } +} diff --git a/app/src/main/java/com/xiaoqu/watch/ui/widget/IconFont.kt b/app/src/main/java/com/xiaoqu/watch/ui/widget/IconFont.kt new file mode 100644 index 0000000..0876aec --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/ui/widget/IconFont.kt @@ -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 + } + } +} diff --git a/app/src/main/java/com/xiaoqu/watch/ui/widget/NavBarHelper.kt b/app/src/main/java/com/xiaoqu/watch/ui/widget/NavBarHelper.kt new file mode 100644 index 0000000..5ad2835 --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/ui/widget/NavBarHelper.kt @@ -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(R.id.statusIcons)?.visibility = View.VISIBLE + rootView.findViewById(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(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(R.id.statusIcons)?.visibility = View.GONE + val btnBack = rootView.findViewById(R.id.btnBack) + btnBack?.visibility = View.VISIBLE + btnBack?.typeface = typeface + btnBack?.setOnClickListener { onBackClick() } + + // 中间:显示标题 + rootView.findViewById(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(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(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(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(viewId)?.typeface = typeface + } +} diff --git a/app/src/main/java/com/xiaoqu/watch/ui/widget/QuConfirmDialog.kt b/app/src/main/java/com/xiaoqu/watch/ui/widget/QuConfirmDialog.kt new file mode 100644 index 0000000..9d48435 --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/ui/widget/QuConfirmDialog.kt @@ -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(R.id.confirmContent) + contentContainer.addView(contentView) + } + + // 取消按钮 + val btnCancel = view.findViewById(R.id.btnCancel) + btnCancel.typeface = typeface + btnCancel.setOnClickListener { + dismiss() + onCancel?.invoke() + } + + // 确认按钮 + val btnConfirm = view.findViewById(R.id.btnConfirm) + btnConfirm.typeface = typeface + btnConfirm.setOnClickListener { + dismiss() + onConfirm() + } + + // 点击遮罩不关闭(手表误触概率高) + view.findViewById(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 +} diff --git a/app/src/main/java/com/xiaoqu/watch/ui/widget/QuTipDialog.kt b/app/src/main/java/com/xiaoqu/watch/ui/widget/QuTipDialog.kt new file mode 100644 index 0000000..2aeda05 --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/ui/widget/QuTipDialog.kt @@ -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(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(R.id.tipTitle).text = title + + // 设置描述(可选) + val descView = view.findViewById(R.id.tipDesc) + if (desc != null) { + descView.text = desc + descView.visibility = View.VISIBLE + } + + // 倒计时返回按钮 + val backBtn = view.findViewById(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 +} diff --git a/app/src/main/java/com/xiaoqu/watch/util/DateUtil.kt b/app/src/main/java/com/xiaoqu/watch/util/DateUtil.kt new file mode 100644 index 0000000..f78319f --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/util/DateUtil.kt @@ -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 + ) +} diff --git a/app/src/main/java/com/xiaoqu/watch/util/DeviceUtil.kt b/app/src/main/java/com/xiaoqu/watch/util/DeviceUtil.kt new file mode 100644 index 0000000..82ee89f --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/util/DeviceUtil.kt @@ -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 + ) +} diff --git a/app/src/main/java/com/xiaoqu/watch/util/Md5Util.kt b/app/src/main/java/com/xiaoqu/watch/util/Md5Util.kt new file mode 100644 index 0000000..082c48f --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/util/Md5Util.kt @@ -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() + } +} diff --git a/app/src/main/java/com/xiaoqu/watch/util/NetworkUtil.kt b/app/src/main/java/com/xiaoqu/watch/util/NetworkUtil.kt new file mode 100644 index 0000000..c1390ba --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/util/NetworkUtil.kt @@ -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 -> "其他" + } + } +} diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/font/iconfont.ttf b/app/src/main/res/font/iconfont.ttf new file mode 100644 index 0000000..ea62e25 Binary files /dev/null and b/app/src/main/res/font/iconfont.ttf differ diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..aa906a9 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_confirm.xml b/app/src/main/res/layout/dialog_confirm.xml new file mode 100644 index 0000000..8a1a21a --- /dev/null +++ b/app/src/main/res/layout/dialog_confirm.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_tip.xml b/app/src/main/res/layout/dialog_tip.xml new file mode 100644 index 0000000..d7c955b --- /dev/null +++ b/app/src/main/res/layout/dialog_tip.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_bind.xml b/app/src/main/res/layout/fragment_bind.xml new file mode 100644 index 0000000..e41e04c --- /dev/null +++ b/app/src/main/res/layout/fragment_bind.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml new file mode 100644 index 0000000..1a3b2c4 --- /dev/null +++ b/app/src/main/res/layout/fragment_home.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_info.xml b/app/src/main/res/layout/fragment_info.xml new file mode 100644 index 0000000..b87962d --- /dev/null +++ b/app/src/main/res/layout/fragment_info.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/layout/fragment_punch.xml b/app/src/main/res/layout/fragment_punch.xml new file mode 100644 index 0000000..14c4a2e --- /dev/null +++ b/app/src/main/res/layout/fragment_punch.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/layout/fragment_task_detail.xml b/app/src/main/res/layout/fragment_task_detail.xml new file mode 100644 index 0000000..992b72f --- /dev/null +++ b/app/src/main/res/layout/fragment_task_detail.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/layout/fragment_task_list.xml b/app/src/main/res/layout/fragment_task_list.xml new file mode 100644 index 0000000..7efdcc5 --- /dev/null +++ b/app/src/main/res/layout/fragment_task_list.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/layout/layout_nav_bar.xml b/app/src/main/res/layout/layout_nav_bar.xml new file mode 100644 index 0000000..33f5655 --- /dev/null +++ b/app/src/main/res/layout/layout_nav_bar.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/navigation/nav_main.xml b/app/src/main/res/navigation/nav_main.xml new file mode 100644 index 0000000..4028bb9 --- /dev/null +++ b/app/src/main/res/navigation/nav_main.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..e1416ef --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..7cf48d4 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,36 @@ + + + #FF000000 + #FFFFFFFF + + + #FF000000 + + + #FF007AFF + #FF339AFB + + + #FF1CC46B + #FFEB9A26 + #FFDA5050 + + + #FF666666 + + + #FFFFFFFF + #FF999999 + #FF808080 + #FFC0C0C0 + + + #66000000 + #FFC8C7CC + #FF1A1A1A + #FFF1F1F1 + + + #FF00FF00 + #FF8B0000 + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..fe9f4c8 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,44 @@ + + + + 12dp + 16dp + 12dp + 16dp + + + 60dp + + + 18sp + 15sp + 13sp + 11sp + 15sp + 13sp + + + 4dp + 8dp + 12dp + 16dp + 24dp + + + 4dp + 8dp + 16dp + 8dp + + + 40dp + 120dp + + + 40dp + + + 16dp + 24dp + 32dp + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..8ccd01c --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + XqWatch + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..d8468df --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..172754b --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,29 @@ + + + + +