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>
19
.gitignore
vendored
Normal file
@@ -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
|
||||
3
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
6
.idea/AndroidProjectSystem.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AndroidProjectSystem">
|
||||
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/compiler.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="21" />
|
||||
</component>
|
||||
</project>
|
||||
10
.idea/deploymentTargetSelector.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetSelector">
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
</project>
|
||||
19
.idea/gradle.xml
generated
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
10
.idea/migrations.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectMigrations">
|
||||
<option name="MigrateToGradleLocalJavaHome">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
10
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
||||
17
.idea/runConfigurations.xml
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
|
||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
|
||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
1
app/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
109
app/build.gradle.kts
Normal file
@@ -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)
|
||||
}
|
||||
21
app/proguard-rules.pro
vendored
Normal file
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
74
app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!-- 网络 -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
|
||||
<!-- 蓝牙 -->
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
||||
|
||||
<!-- NFC -->
|
||||
<uses-permission android:name="android.permission.NFC" />
|
||||
|
||||
<!-- 定位(蓝牙扫描需要) -->
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
|
||||
<!-- 振动 -->
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<!-- 开机自启 -->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
<!-- WakeLock -->
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<!-- 手机状态(获取 IMEI) -->
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
|
||||
<!-- 相机(二维码扫描备用) -->
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
<application
|
||||
android:name=".app.WatchApplication"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.XqWatch">
|
||||
|
||||
<!-- 主 Activity(Launcher 模式) -->
|
||||
<activity
|
||||
android:name=".app.MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:screenOrientation="portrait"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.HOME" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- 开机自启广播接收器 -->
|
||||
<receiver
|
||||
android:name=".app.BootReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
BIN
app/src/main/assets/font/iconfont.ttf
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 -> "其他"
|
||||
}
|
||||
}
|
||||
}
|
||||
170
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
30
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
@@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
BIN
app/src/main/res/font/iconfont.ttf
Normal file
29
app/src/main/res/layout/activity_main.xml
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- MainActivity 布局:NavBar 已移入各 Fragment 自行管理
|
||||
只保留 NavHostFragment(主内容)和 dialog_container(全局弹窗层) -->
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/background"
|
||||
tools:context=".app.MainActivity">
|
||||
|
||||
<!-- Layer 1: 主内容区(NavHostFragment) -->
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/nav_host_fragment"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:defaultNavHost="true"
|
||||
app:navGraph="@navigation/nav_main" />
|
||||
|
||||
<!-- Layer 2: 全局弹窗层(QuTipDialog / QuConfirmDialog 动态添加,默认隐藏) -->
|
||||
<FrameLayout
|
||||
android:id="@+id/dialog_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
57
app/src/main/res/layout/dialog_confirm.xml
Normal file
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- QuConfirmDialog 布局:确认弹窗
|
||||
结构:半透明遮罩 + 居中内容区 + 底部取消(×)/确认(✓)图标按钮 -->
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/confirmOverlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/overlay">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/spacing_lg">
|
||||
|
||||
<!-- 内容区域(由调用方动态设置) -->
|
||||
<FrameLayout
|
||||
android:id="@+id/confirmContent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/spacing_xl" />
|
||||
|
||||
<!-- 底部按钮组(取消 × / 确认 ✓) -->
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<!-- 取消按钮(×) -->
|
||||
<TextView
|
||||
android:id="@+id/btnCancel"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="60dp"
|
||||
android:gravity="center"
|
||||
android:text="\ue623"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="36sp"
|
||||
android:layout_marginEnd="@dimen/spacing_xl" />
|
||||
|
||||
<!-- 确认按钮(✓) -->
|
||||
<TextView
|
||||
android:id="@+id/btnConfirm"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="60dp"
|
||||
android:gravity="center"
|
||||
android:text="\ue600"
|
||||
android:textColor="@color/success"
|
||||
android:textSize="36sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
57
app/src/main/res/layout/dialog_tip.xml
Normal file
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- QuTipDialog 布局:提示弹窗(成功/警告/错误)
|
||||
结构:全屏遮罩 + 居中内容(图标 + 标题 + 描述 + 倒计时返回按钮) -->
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/background">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/spacing_lg">
|
||||
|
||||
<!-- 状态图标(成功✓ / 警告! / 错误×) -->
|
||||
<TextView
|
||||
android:id="@+id/tipIcon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="48sp"
|
||||
android:layout_marginBottom="@dimen/spacing_md" />
|
||||
|
||||
<!-- 标题文字 -->
|
||||
<TextView
|
||||
android:id="@+id/tipTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="@dimen/text_title"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="@dimen/spacing_sm" />
|
||||
|
||||
<!-- 描述文字(可选) -->
|
||||
<TextView
|
||||
android:id="@+id/tipDesc"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="@dimen/text_caption"
|
||||
android:gravity="center"
|
||||
android:visibility="gone"
|
||||
android:layout_marginBottom="@dimen/spacing_lg" />
|
||||
|
||||
<!-- 倒计时返回按钮(显示"返回 3s") -->
|
||||
<TextView
|
||||
android:id="@+id/tipBackBtn"
|
||||
android:layout_width="@dimen/touch_min_size"
|
||||
android:layout_height="@dimen/touch_min_size"
|
||||
android:gravity="center"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="@dimen/text_caption"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
9
app/src/main/res/layout/fragment_bind.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/background">
|
||||
|
||||
<!-- TODO: 二维码 + 设备信息 -->
|
||||
|
||||
</FrameLayout>
|
||||
94
app/src/main/res/layout/fragment_home.xml
Normal file
@@ -0,0 +1,94 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 首页布局:NavBar + 内容区 -->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/background"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- 顶部导航栏(首页模式:状态图标 + 时间 + 电量) -->
|
||||
<include layout="@layout/layout_nav_bar" />
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="@dimen/safe_area_left"
|
||||
android:paddingEnd="@dimen/safe_area_right"
|
||||
android:paddingBottom="@dimen/safe_area_bottom">
|
||||
|
||||
<!-- demo 信息展示 -->
|
||||
<TextView
|
||||
android:id="@+id/tvDemoInfo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="@dimen/text_small"
|
||||
android:lineSpacingExtra="3dp"
|
||||
android:layout_marginBottom="@dimen/spacing_md" />
|
||||
|
||||
<!-- 按钮 demo:触发 QuTipDialog -->
|
||||
<TextView
|
||||
android:id="@+id/btnShowTip"
|
||||
style="@style/ActionButton.Primary"
|
||||
android:text="显示提示弹窗"
|
||||
android:layout_marginBottom="@dimen/spacing_sm" />
|
||||
|
||||
<!-- 按钮 demo:触发 QuConfirmDialog -->
|
||||
<TextView
|
||||
android:id="@+id/btnShowConfirm"
|
||||
style="@style/ActionButton.Success"
|
||||
android:text="显示确认弹窗"
|
||||
android:layout_marginBottom="@dimen/spacing_sm" />
|
||||
|
||||
<!-- 按钮 demo:其他样式展示 -->
|
||||
<TextView
|
||||
android:id="@+id/btnDanger"
|
||||
style="@style/ActionButton.Danger"
|
||||
android:text="危险按钮"
|
||||
android:layout_marginBottom="@dimen/spacing_sm" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/btnWarning"
|
||||
style="@style/ActionButton.Warning"
|
||||
android:text="警告按钮"
|
||||
android:layout_marginBottom="@dimen/spacing_sm" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/btnGrey"
|
||||
style="@style/ActionButton.Grey"
|
||||
android:text="灰色按钮"
|
||||
android:layout_marginBottom="@dimen/spacing_sm" />
|
||||
|
||||
<!-- 半宽按钮并排 -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
style="@style/ActionButton.Grey"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:text="取消"
|
||||
android:layout_marginEnd="@dimen/spacing_sm" />
|
||||
|
||||
<TextView
|
||||
style="@style/ActionButton.Primary"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:text="确定" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
9
app/src/main/res/layout/fragment_info.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/background">
|
||||
|
||||
<!-- TODO: 设备信息展示 -->
|
||||
|
||||
</FrameLayout>
|
||||
9
app/src/main/res/layout/fragment_punch.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/background">
|
||||
|
||||
<!-- TODO: 打卡操作界面 -->
|
||||
|
||||
</FrameLayout>
|
||||
9
app/src/main/res/layout/fragment_task_detail.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/background">
|
||||
|
||||
<!-- TODO: 任务详情 + 操作按钮 -->
|
||||
|
||||
</FrameLayout>
|
||||
9
app/src/main/res/layout/fragment_task_list.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/background">
|
||||
|
||||
<!-- TODO: RecyclerView 任务列表 -->
|
||||
|
||||
</FrameLayout>
|
||||
108
app/src/main/res/layout/layout_nav_bar.xml
Normal file
@@ -0,0 +1,108 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- NavBarView:顶部导航栏,每个 Fragment 通过 <include> 引入
|
||||
左/中/右三栏布局:
|
||||
- 首页:左=状态图标(蓝牙/4G/NFC),中=时间,右=电量
|
||||
- 子页面:左=返回按钮,中=标题,右=电量 -->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/navBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/nav_bar_height"
|
||||
android:background="@color/background"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="@dimen/safe_area_left"
|
||||
android:paddingEnd="@dimen/safe_area_right">
|
||||
|
||||
<!-- 左侧区域(状态图标 或 返回按钮) -->
|
||||
<FrameLayout
|
||||
android:id="@+id/navLeft"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<!-- 返回按钮(子页面用,默认隐藏) -->
|
||||
<TextView
|
||||
android:id="@+id/btnBack"
|
||||
android:layout_width="@dimen/touch_min_size"
|
||||
android:layout_height="@dimen/touch_min_size"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:gravity="center"
|
||||
android:text="\ue6bd"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="18sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- 状态图标组(首页用,默认显示) -->
|
||||
<LinearLayout
|
||||
android:id="@+id/statusIcons"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<!-- 蓝牙状态 -->
|
||||
<TextView
|
||||
android:id="@+id/iconBluetooth"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="\ue6da"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="14sp"
|
||||
android:paddingEnd="6dp" />
|
||||
|
||||
<!-- 4G 状态 -->
|
||||
<TextView
|
||||
android:id="@+id/icon4G"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="\ue6dc"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="14sp"
|
||||
android:paddingEnd="6dp" />
|
||||
|
||||
<!-- NFC 状态 -->
|
||||
<TextView
|
||||
android:id="@+id/iconNfc"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="\ue6d8"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="14sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<!-- 中间区域(时间 或 标题) -->
|
||||
<TextView
|
||||
android:id="@+id/navTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="2"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="@dimen/text_body" />
|
||||
|
||||
<!-- 右侧区域(电量图标) -->
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical|end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<!-- 电量图标 -->
|
||||
<TextView
|
||||
android:id="@+id/iconBattery"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="\ue6ce"
|
||||
android:textColor="@color/success"
|
||||
android:textSize="16sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
6
app/src/main/res/mipmap-anydpi/ic_launcher.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
6
app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 982 B |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
43
app/src/main/res/navigation/nav_main.xml
Normal file
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/nav_main"
|
||||
app:startDestination="@id/homeFragment">
|
||||
|
||||
<!-- 首页(含 ViewPager2 左右滑动:设置页 / 主页) -->
|
||||
<fragment
|
||||
android:id="@+id/homeFragment"
|
||||
android:name="com.xiaoqu.watch.ui.home.HomeFragment"
|
||||
android:label="首页" />
|
||||
|
||||
<!-- 设备绑定页(全屏二维码) -->
|
||||
<fragment
|
||||
android:id="@+id/bindFragment"
|
||||
android:name="com.xiaoqu.watch.ui.bind.BindFragment"
|
||||
android:label="设备绑定" />
|
||||
|
||||
<!-- 任务列表 -->
|
||||
<fragment
|
||||
android:id="@+id/taskListFragment"
|
||||
android:name="com.xiaoqu.watch.ui.task.TaskListFragment"
|
||||
android:label="任务列表" />
|
||||
|
||||
<!-- 任务详情 -->
|
||||
<fragment
|
||||
android:id="@+id/taskDetailFragment"
|
||||
android:name="com.xiaoqu.watch.ui.task.TaskDetailFragment"
|
||||
android:label="任务详情" />
|
||||
|
||||
<!-- 打卡页 -->
|
||||
<fragment
|
||||
android:id="@+id/punchFragment"
|
||||
android:name="com.xiaoqu.watch.ui.punch.PunchFragment"
|
||||
android:label="打卡" />
|
||||
|
||||
<!-- 设备信息 -->
|
||||
<fragment
|
||||
android:id="@+id/infoFragment"
|
||||
android:name="com.xiaoqu.watch.ui.info.InfoFragment"
|
||||
android:label="设备信息" />
|
||||
|
||||
</navigation>
|
||||
7
app/src/main/res/values-night/themes.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Base.Theme.XqWatch" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<!-- Customize your dark theme here. -->
|
||||
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
|
||||
</style>
|
||||
</resources>
|
||||
36
app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
|
||||
<!-- 全局背景 -->
|
||||
<color name="background">#FF000000</color>
|
||||
|
||||
<!-- 主题色 -->
|
||||
<color name="primary">#FF007AFF</color>
|
||||
<color name="action_primary">#FF339AFB</color>
|
||||
|
||||
<!-- 状态色 -->
|
||||
<color name="success">#FF1CC46B</color>
|
||||
<color name="warning">#FFEB9A26</color>
|
||||
<color name="error">#FFDA5050</color>
|
||||
|
||||
<!-- 按钮色 -->
|
||||
<color name="grey_button">#FF666666</color>
|
||||
|
||||
<!-- 文字色 -->
|
||||
<color name="text_primary">#FFFFFFFF</color>
|
||||
<color name="text_secondary">#FF999999</color>
|
||||
<color name="text_placeholder">#FF808080</color>
|
||||
<color name="text_disabled">#FFC0C0C0</color>
|
||||
|
||||
<!-- 界面元素 -->
|
||||
<color name="overlay">#66000000</color>
|
||||
<color name="border">#FFC8C7CC</color>
|
||||
<color name="card_background">#FF1A1A1A</color>
|
||||
<color name="pressed">#FFF1F1F1</color>
|
||||
|
||||
<!-- 状态图标 -->
|
||||
<color name="charge_green">#FF00FF00</color>
|
||||
<color name="disconnect_red">#FF8B0000</color>
|
||||
</resources>
|
||||
44
app/src/main/res/values/dimens.xml
Normal file
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- SafeArea 内边距(圆角屏防裁切) -->
|
||||
<dimen name="safe_area_left">12dp</dimen>
|
||||
<dimen name="safe_area_top">16dp</dimen>
|
||||
<dimen name="safe_area_right">12dp</dimen>
|
||||
<dimen name="safe_area_bottom">16dp</dimen>
|
||||
|
||||
<!-- NavBar -->
|
||||
<dimen name="nav_bar_height">60dp</dimen>
|
||||
|
||||
<!-- 字体大小 -->
|
||||
<dimen name="text_title">18sp</dimen>
|
||||
<dimen name="text_body">15sp</dimen>
|
||||
<dimen name="text_caption">13sp</dimen>
|
||||
<dimen name="text_small">11sp</dimen>
|
||||
<dimen name="text_button">15sp</dimen>
|
||||
<dimen name="text_button_small">13sp</dimen>
|
||||
|
||||
<!-- 间距 -->
|
||||
<dimen name="spacing_xs">4dp</dimen>
|
||||
<dimen name="spacing_sm">8dp</dimen>
|
||||
<dimen name="spacing_md">12dp</dimen>
|
||||
<dimen name="spacing_lg">16dp</dimen>
|
||||
<dimen name="spacing_xl">24dp</dimen>
|
||||
|
||||
<!-- 圆角 -->
|
||||
<dimen name="corner_radius_sm">4dp</dimen>
|
||||
<dimen name="corner_radius_md">8dp</dimen>
|
||||
<dimen name="corner_radius_lg">16dp</dimen>
|
||||
<dimen name="corner_radius_button">8dp</dimen>
|
||||
|
||||
<!-- 按钮 -->
|
||||
<dimen name="button_height">40dp</dimen>
|
||||
<dimen name="button_half_width">120dp</dimen>
|
||||
|
||||
<!-- 触摸区域最小尺寸 -->
|
||||
<dimen name="touch_min_size">40dp</dimen>
|
||||
|
||||
<!-- 图标 -->
|
||||
<dimen name="icon_sm">16dp</dimen>
|
||||
<dimen name="icon_md">24dp</dimen>
|
||||
<dimen name="icon_lg">32dp</dimen>
|
||||
</resources>
|
||||
3
app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">XqWatch</string>
|
||||
</resources>
|
||||
42
app/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- ===== ActionButton 基础样式 ===== -->
|
||||
<style name="ActionButton">
|
||||
<item name="android:layout_width">match_parent</item>
|
||||
<item name="android:layout_height">@dimen/button_height</item>
|
||||
<item name="android:gravity">center</item>
|
||||
<item name="android:textColor">@color/white</item>
|
||||
<item name="android:textSize">@dimen/text_button</item>
|
||||
<item name="android:textStyle">bold</item>
|
||||
</style>
|
||||
|
||||
<!-- 主色按钮(默认) -->
|
||||
<style name="ActionButton.Primary">
|
||||
<item name="android:background">@color/action_primary</item>
|
||||
</style>
|
||||
|
||||
<!-- 成功按钮(确认/完成) -->
|
||||
<style name="ActionButton.Success">
|
||||
<item name="android:background">@color/success</item>
|
||||
</style>
|
||||
|
||||
<!-- 危险按钮(取消/撤销) -->
|
||||
<style name="ActionButton.Danger">
|
||||
<item name="android:background">@color/error</item>
|
||||
</style>
|
||||
|
||||
<!-- 警告按钮(抢单) -->
|
||||
<style name="ActionButton.Warning">
|
||||
<item name="android:background">@color/warning</item>
|
||||
</style>
|
||||
|
||||
<!-- 灰色按钮(返回/次要操作) -->
|
||||
<style name="ActionButton.Grey">
|
||||
<item name="android:background">@color/grey_button</item>
|
||||
</style>
|
||||
|
||||
<!-- 半宽按钮(两个并排时使用) -->
|
||||
<style name="ActionButton.Half">
|
||||
<item name="android:layout_width">@dimen/button_half_width</item>
|
||||
</style>
|
||||
</resources>
|
||||
29
app/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,29 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- 应用主题:黑底全屏、无 ActionBar、无窗口动画 -->
|
||||
<style name="Base.Theme.XqWatch" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<!-- 背景色 -->
|
||||
<item name="android:windowBackground">@color/background</item>
|
||||
<item name="android:colorBackground">@color/background</item>
|
||||
|
||||
<!-- 主题色 -->
|
||||
<item name="colorPrimary">@color/primary</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
|
||||
<!-- 全屏(隐藏状态栏/导航栏) -->
|
||||
<item name="android:windowFullscreen">true</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
|
||||
<!-- 禁用窗口动画(手表性能优先) -->
|
||||
<item name="android:windowAnimationStyle">@null</item>
|
||||
|
||||
<!-- 文字颜色 -->
|
||||
<item name="android:textColorPrimary">@color/text_primary</item>
|
||||
<item name="android:textColorSecondary">@color/text_secondary</item>
|
||||
<item name="android:textColorHint">@color/text_placeholder</item>
|
||||
|
||||
<!-- 固定竖屏 -->
|
||||
<item name="android:screenOrientation">portrait</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.XqWatch" parent="Base.Theme.XqWatch" />
|
||||
</resources>
|
||||
13
app/src/main/res/xml/backup_rules.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample backup rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/guide/topics/data/autobackup
|
||||
for details.
|
||||
Note: This file is ignored for devices older than API 31
|
||||
See https://developer.android.com/about/versions/12/backup-restore
|
||||
-->
|
||||
<full-backup-content>
|
||||
<!--
|
||||
<include domain="sharedpref" path="."/>
|
||||
<exclude domain="sharedpref" path="device.xml"/>
|
||||
-->
|
||||
</full-backup-content>
|
||||
19
app/src/main/res/xml/data_extraction_rules.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample data extraction rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||
for details.
|
||||
-->
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
-->
|
||||
</cloud-backup>
|
||||
<!--
|
||||
<device-transfer>
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
</device-transfer>
|
||||
-->
|
||||
</data-extraction-rules>
|
||||
17
app/src/test/java/com/xiaoqu/watch/ExampleUnitTest.kt
Normal file
@@ -0,0 +1,17 @@
|
||||
package com.xiaoqu.watch
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||
7
build.gradle.kts
Normal file
@@ -0,0 +1,7 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.kotlin.android) apply false
|
||||
alias(libs.plugins.ksp) apply false
|
||||
alias(libs.plugins.hilt) apply false
|
||||
}
|
||||
23
gradle.properties
Normal file
@@ -0,0 +1,23 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. For more details, visit
|
||||
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
90
gradle/libs.versions.toml
Normal file
@@ -0,0 +1,90 @@
|
||||
[versions]
|
||||
agp = "8.7.3"
|
||||
kotlin = "2.0.21"
|
||||
ksp = "2.0.21-1.0.27"
|
||||
hilt = "2.51.1"
|
||||
coroutines = "1.8.1"
|
||||
lifecycle = "2.7.0"
|
||||
navigation = "2.7.7"
|
||||
room = "2.6.1"
|
||||
retrofit = "2.9.0"
|
||||
okhttp = "4.12.0"
|
||||
gson = "2.10.1"
|
||||
paho = "1.2.5"
|
||||
timber = "5.0.1"
|
||||
zxing = "3.5.3"
|
||||
swiperefresh = "1.1.0"
|
||||
viewpager2 = "1.0.0"
|
||||
coreKtx = "1.12.0"
|
||||
appcompat = "1.6.1"
|
||||
material = "1.11.0"
|
||||
activity = "1.8.2"
|
||||
fragment = "1.6.2"
|
||||
constraintlayout = "2.1.4"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.1.5"
|
||||
espressoCore = "3.5.1"
|
||||
mockk = "1.13.10"
|
||||
coroutinesTest = "1.8.1"
|
||||
|
||||
[libraries]
|
||||
# Core
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
||||
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||
androidx-activity = { group = "androidx.activity", name = "activity-ktx", version.ref = "activity" }
|
||||
androidx-fragment = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragment" }
|
||||
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
|
||||
androidx-viewpager2 = { group = "androidx.viewpager2", name = "viewpager2", version.ref = "viewpager2" }
|
||||
androidx-swiperefreshlayout = { group = "androidx.swiperefreshlayout", name = "swiperefreshlayout", version.ref = "swiperefresh" }
|
||||
|
||||
# Lifecycle
|
||||
androidx-lifecycle-viewmodel = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
|
||||
androidx-lifecycle-livedata = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycle" }
|
||||
androidx-lifecycle-runtime = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
|
||||
|
||||
# Navigation
|
||||
androidx-navigation-fragment = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigation" }
|
||||
androidx-navigation-ui = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigation" }
|
||||
|
||||
# Hilt
|
||||
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
|
||||
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
|
||||
|
||||
# Coroutines
|
||||
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" }
|
||||
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" }
|
||||
|
||||
# Room
|
||||
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
|
||||
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
|
||||
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
|
||||
|
||||
# Network
|
||||
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
|
||||
retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
|
||||
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
|
||||
okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
|
||||
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
|
||||
|
||||
# MQTT
|
||||
paho-mqtt = { group = "org.eclipse.paho", name = "org.eclipse.paho.client.mqttv3", version.ref = "paho" }
|
||||
|
||||
# Logging
|
||||
timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" }
|
||||
|
||||
# QR Code
|
||||
zxing-core = { group = "com.google.zxing", name = "core", version.ref = "zxing" }
|
||||
|
||||
# Test
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||
mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" }
|
||||
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutinesTest" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
#Fri Apr 24 17:29:26 CST 2026
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
185
gradlew
vendored
Executable file
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
89
gradlew.bat
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
24
settings.gradle.kts
Normal file
@@ -0,0 +1,24 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google {
|
||||
content {
|
||||
includeGroupByRegex("com\\.android.*")
|
||||
includeGroupByRegex("com\\.google.*")
|
||||
includeGroupByRegex("androidx.*")
|
||||
}
|
||||
}
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "XqWatch"
|
||||
include(":app")
|
||||
|
||||