This commit is contained in:
阿猫
2026-09-03 12:38:16 +08:00
parent 09669fe4a2
commit e96e92d7ab
68 changed files with 2810 additions and 0 deletions

74
app/build.gradle.kts Normal file
View File

@@ -0,0 +1,74 @@
// app 业务壳导航、主题、权限、配置注入M1M6 页面经 FeatureMount 挂载
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.ksp)
alias(libs.plugins.hilt)
}
android {
namespace = "com.stec.cmd"
compileSdk = 34
defaultConfig {
applicationId = "com.stec.cmd"
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "0.1.0-S0"
}
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
}
}
buildFeatures {
viewBinding = true
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
}
dependencies {
// core 基础层 + 协议解析(业务模块只允许经 core 层访问能力)
implementation(project(":core:common"))
implementation(project(":core:network"))
implementation(project(":core:database"))
implementation(project(":core:ble"))
implementation(project(":ble-protocol"))
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.activity)
implementation(libs.androidx.fragment.ktx)
implementation(libs.androidx.splashscreen)
implementation(libs.google.material)
implementation(libs.androidx.lifecycle.runtime)
implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.androidx.lifecycle.livedata)
implementation(libs.androidx.datastore.preferences)
implementation(libs.androidx.security.crypto)
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
}

12
app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,12 @@
# app 壳混淆规则release
# S0 骨架期保持最小规则集S5 发布阶段随业务实体补全
# kotlinx.serialization保留序列化器生成逻辑S1 业务 DTO 接入后按类型名细化)
-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.AnnotationsKt
# OkHttp / Retrofit
-dontwarn okhttp3.**
-dontwarn okio.**
-dontwarn javax.annotation.**
-keepattributes Signature, Exceptions

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- 蓝牙Android 12L+ -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation"
tools:targetApi="s" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- 蓝牙Android 12L 以下(随 12L+ 权限一同声明,按系统版本生效) -->
<uses-permission android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<!-- 12L 以下 BLE 扫描依赖定位权限 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="30" />
<!-- 通知33+,上传进度/采集会话通知预留) -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- 网络core:network -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- 蓝牙为可选硬件:无蓝牙设备仍可安装 -->
<uses-feature android:name="android.hardware.bluetooth_le"
android:required="false" />
<application
android:name=".SteCmdApplication"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true"
android:theme="@style/Theme.SteCmd"
tools:targetApi="34">
<activity
android:name=".MainActivity"
android:exported="true"
android:screenOrientation="portrait"
android:theme="@style/Theme.SteCmd.Splash">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,7 @@
package com.stec.cmd
import dagger.hilt.android.AndroidEntryPoint
/** 采集占位M4+M5 数据采集S2/S3 挂载扫描与采集页)。 */
@AndroidEntryPoint
class CollectFragment : PlaceholderFragment()

View File

@@ -0,0 +1,41 @@
package com.stec.cmd
/**
* 底部导航 5 Tab 与业务模块(功能明细表编号)的挂载点注册中心。
*
* S1+ 挂载业务页面的方式:把对应 [MountItem.fragmentClass] 从占位 Fragment
* 换成业务 Fragment 即可导航壳MainActivity零改动新增 Tab 时扩展 [Tab]。
* 挂载映射依据《安卓客户端功能明细表》:
* 首页=M2、任务=M3、采集=M4+M5、统计=M6、我的=M1。
*/
object FeatureMount {
/** 底部导航 TabitemId 与 res/menu/bottom_nav_menu.xml 一一对应)。 */
enum class Tab(val itemId: Int, val labelRes: Int) {
HOME(R.id.nav_home, R.string.tab_home),
TASK(R.id.nav_task, R.string.tab_task),
COLLECT(R.id.nav_collect, R.string.tab_collect),
STATS(R.id.nav_stats, R.string.tab_stats),
MINE(R.id.nav_mine, R.string.tab_mine),
}
/** 单个 Tab 的挂载项。 */
data class MountItem(
val tab: Tab,
val fragmentClass: Class<out androidx.fragment.app.Fragment>,
val moduleTag: String,
)
/** 挂载注册表S1+ 替换 fragmentClass 即完成业务页挂载。 */
val mounts: List<MountItem> = listOf(
MountItem(Tab.HOME, HomeFragment::class.java, "M2 项目工作台"),
MountItem(Tab.TASK, TaskFragment::class.java, "M3 任务管理"),
MountItem(Tab.COLLECT, CollectFragment::class.java, "M4+M5 数据采集"),
MountItem(Tab.STATS, StatsFragment::class.java, "M6 统计与上传"),
MountItem(Tab.MINE, MineFragment::class.java, "M1 我的"),
)
/** 按 menu itemId 取挂载项;未知 id 返回 null容错。 */
fun byItemId(itemId: Int): MountItem? =
mounts.firstOrNull { it.tab.itemId == itemId }
}

View File

@@ -0,0 +1,7 @@
package com.stec.cmd
import dagger.hilt.android.AndroidEntryPoint
/** 首页占位M2 项目工作台S1 挂载项目列表)。 */
@AndroidEntryPoint
class HomeFragment : PlaceholderFragment()

View File

@@ -0,0 +1,59 @@
package com.stec.cmd
import android.os.Bundle
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
import com.stec.cmd.databinding.ActivityMainBinding
import dagger.hilt.android.AndroidEntryPoint
/**
* 单 Activity 导航壳BottomNavigationView 5 Tab 切换 Fragment。
* show/hide 复用实例以保留各 Tab 视图状态S1 可迁移 Navigation 组件。
*/
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.bottomNav.setOnItemSelectedListener(::selectTab)
if (savedInstanceState == null) {
binding.bottomNav.selectedItemId = FeatureMount.Tab.HOME.itemId
}
}
private fun selectTab(item: MenuItem): Boolean {
val mount = FeatureMount.byItemId(item.itemId) ?: return false
switchTo(mount)
return true
}
private fun switchTo(mount: FeatureMount.MountItem) {
val tag = mount.tab.name
val existing = supportFragmentManager.findFragmentByTag(tag)
val target: Fragment = existing ?: createFragment(mount)
supportFragmentManager.commit {
setReorderingAllowed(true)
supportFragmentManager.fragments.forEach { if (it !== target) hide(it) }
if (existing == null) add(R.id.fragment_container, target, tag)
show(target)
}
supportActionBar?.title = getString(mount.tab.labelRes)
}
/** 实例化挂载项声明的 Fragment占位 Fragment 支持模块名参数。 */
private fun createFragment(mount: FeatureMount.MountItem): Fragment {
val fragment = mount.fragmentClass.getDeclaredConstructor().newInstance()
if (fragment is PlaceholderFragment) {
fragment.arguments = PlaceholderFragment.args(mount.moduleTag)
}
return fragment
}
}

View File

@@ -0,0 +1,7 @@
package com.stec.cmd
import dagger.hilt.android.AndroidEntryPoint
/** 我的占位M1 登录与配置S1 挂载登录/配置页)。 */
@AndroidEntryPoint
class MineFragment : PlaceholderFragment()

View File

@@ -0,0 +1,45 @@
package com.stec.cmd
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.stec.cmd.databinding.FragmentPlaceholderBinding
/**
* S0 占位页基类显示模块编号与挂载说明S1+ 由各业务 Fragment 替换。
*/
open class PlaceholderFragment : Fragment() {
private var _binding: FragmentPlaceholderBinding? = null
private val binding get() = checkNotNull(_binding)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
_binding = FragmentPlaceholderBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val moduleTag = arguments?.getString(ARG_MODULE_TAG).orEmpty()
binding.moduleTag.text = moduleTag
binding.hint.text = getString(R.string.placeholder_hint)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
const val ARG_MODULE_TAG = "module_tag"
fun args(moduleTag: String): Bundle = Bundle().apply {
putString(ARG_MODULE_TAG, moduleTag)
}
}
}

View File

@@ -0,0 +1,7 @@
package com.stec.cmd
import dagger.hilt.android.AndroidEntryPoint
/** 统计占位M6 统计与上传S4 挂载统计表与文件上传)。 */
@AndroidEntryPoint
class StatsFragment : PlaceholderFragment()

View File

@@ -0,0 +1,31 @@
package com.stec.cmd
import android.app.Application
import android.util.Log
import com.stec.cmd.core.common.AppLog
import dagger.hilt.android.HiltAndroidApp
/**
* 应用入口:装配 Hilt 图并安装日志实现。
* core:common 的 AppLog 门面不依赖 android.util.LogLogCat sink 在壳层注入。
*/
@HiltAndroidApp
class SteCmdApplication : Application() {
override fun onCreate() {
super.onCreate()
AppLog.install { level, tag, message, throwable ->
when (level) {
AppLog.Level.DEBUG -> Log.d(tag, message, throwable)
AppLog.Level.INFO -> Log.i(tag, message, throwable)
AppLog.Level.WARN -> Log.w(tag, message, throwable)
AppLog.Level.ERROR -> Log.e(tag, message, throwable)
}
}
AppLog.i(TAG, "监测数据不落地平台客户端启动S0 骨架)")
}
private companion object {
const val TAG = "SteCmd"
}
}

View File

@@ -0,0 +1,7 @@
package com.stec.cmd
import dagger.hilt.android.AndroidEntryPoint
/** 任务占位M3 任务管理S2 挂载任务列表)。 */
@AndroidEntryPoint
class TaskFragment : PlaceholderFragment()

View File

@@ -0,0 +1,49 @@
package com.stec.cmd.config
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
import com.stec.cmd.core.network.NetworkConfig
import com.stec.cmd.core.network.TokenProvider
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
private val Context.configDataStore: DataStore<Preferences> by preferencesDataStore(
name = "stec_cmd_config",
)
/**
* app 壳 Hilt 装配:
* - DataStore 实例;
* - [ConfigRepository] 绑定为 core:network 的 [NetworkConfig] / [TokenProvider]
* 补全 core:network NetworkModule 的依赖图(密钥值由用户配置注入,不入库)。
*/
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideConfigDataStore(
@ApplicationContext context: Context,
): DataStore<Preferences> = context.configDataStore
}
@Module
@InstallIn(SingletonComponent::class)
abstract class ConfigBindModule {
@Binds
@Singleton
abstract fun bindNetworkConfig(impl: ConfigRepository): NetworkConfig
@Binds
@Singleton
abstract fun bindTokenProvider(impl: ConfigRepository): TokenProvider
}

View File

@@ -0,0 +1,127 @@
package com.stec.cmd.config
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.core.stringPreferencesKey
import com.stec.cmd.core.common.AppLog
import com.stec.cmd.core.network.NetworkConfig
import com.stec.cmd.core.network.TokenProvider
import java.io.IOException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import javax.inject.Inject
import javax.inject.Singleton
/**
* 平台配置仓储M1-06 前置、M7-04 落地):
* baseUrl / SecretKey / SystemCode / Token 经 DataStore 持久化 + 内存快照。
*
* **默认值全空,密钥不进代码库**S1 在「我的」页面提供配置 UI
* 未配置时 [NetworkConfig] 各字段为空串AuthInterceptor 将带空三要素,
* 业务请求由 S1 的配置检查(未配置 → 跳转配置页)拦截。
*
* 拦截器在 OkHttp 后台线程同步读快照runBlocking 只在冷启动首读时短暂阻塞,
* 之后 [snapshotStateFlow] 已被预热,直接返回缓存值)。
*/
@Singleton
class ConfigRepository @Inject constructor(
private val dataStore: DataStore<Preferences>,
) : NetworkConfig, TokenProvider {
data class Snapshot(
val baseUrl: String = "",
val secretKey: String = "",
val systemCode: String = "",
val token: String = "",
) {
/** 三要素 + 地址是否已配置完整。 */
val isReady: Boolean
get() = baseUrl.isNotBlank() && secretKey.isNotBlank() && systemCode.isNotBlank()
}
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val snapshotFlow = MutableStateFlow(Snapshot())
/** 配置快照流S1 配置页/业务层订阅。 */
val snapshotStateFlow: StateFlow<Snapshot> = snapshotFlow.asStateFlow()
init {
scope.launch {
dataStore.data
.catch { e ->
if (e is IOException) emit(emptyPreferences()) else throw e
}
.collect { prefs ->
snapshotFlow.value = Snapshot(
baseUrl = prefs[KEY_BASE_URL].orEmpty(),
secretKey = prefs[KEY_SECRET_KEY].orEmpty(),
systemCode = prefs[KEY_SYSTEM_CODE].orEmpty(),
token = prefs[KEY_TOKEN].orEmpty(),
)
}
}
}
// ---- NetworkConfig ----
override val baseUrl: String get() = snapshot().baseUrl
override val secretKey: String get() = snapshot().secretKey
override val systemCode: String get() = snapshot().systemCode
override val tokenProvider: TokenProvider get() = this
// ---- TokenProvider ----
override fun currentToken(): String = snapshot().token
// ---- 写入S1 配置页 / 登录流程调用)----
/** 保存三要素与服务器地址。 */
suspend fun updateConnection(baseUrl: String, secretKey: String, systemCode: String) {
dataStore.edit { prefs ->
prefs[KEY_BASE_URL] = baseUrl.trim().trimEnd('/')
prefs[KEY_SECRET_KEY] = secretKey.trim()
prefs[KEY_SYSTEM_CODE] = systemCode.trim()
}
AppLog.i(TAG, "连接配置已更新")
}
/** 登录成功保存 Token登出传空串。 */
suspend fun updateToken(token: String) {
dataStore.edit { it[KEY_TOKEN] = token }
}
/** 阻塞读取当前快照(拦截器线程安全:值来自内存缓存)。 */
private fun snapshot(): Snapshot =
if (snapshotFlow.value != Snapshot()) {
snapshotFlow.value
} else {
// 冷启动首读:预热缓存
runBlocking { snapshotFlow.first() }
}
private companion object {
const val TAG = "ConfigRepository"
val KEY_BASE_URL = stringPreferencesKey("base_url")
val KEY_SECRET_KEY = stringPreferencesKey("secret_key")
val KEY_SYSTEM_CODE = stringPreferencesKey("system_code")
val KEY_TOKEN = stringPreferencesKey("token")
}
}
/** 只读 Token 快照接口的窄化视图(给不需要完整 NetworkConfig 的组件用)。 */
val ConfigRepository.tokenFlow: Flow<Snapshot> get() = snapshotStateFlow

View File

@@ -0,0 +1,7 @@
<?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="@color/launcher_background"
android:pathData="M0,0 L108,0 L108,108 L0,108 Z" />
</vector>

View File

@@ -0,0 +1,13 @@
<?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">
<!-- 蓝牙风格前景:安全区中央(内容控制在 66dp 内圆内) -->
<path
android:strokeColor="#FFFFFFFF"
android:strokeWidth="4"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000"
android:pathData="M54,32 L54,76 M54,32 L64,41.5 L44,66.5 M54,76 L64,66.5 L44,41.5" />
</vector>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportHeight="24">
<!-- 蓝牙符号:采集 Tab -->
<path android:strokeColor="#FF000000"
android:strokeWidth="1.8"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:fillColor="#00000000"
android:pathData="M12,2.5 L12,21.5 M12,2.5 L16.5,6.8 L7.5,17.2 M12,21.5 L16.5,17.2 L7.5,6.8" />
</vector>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FF000000"
android:pathData="M12,3 L4,10 L4,21 L10,21 L10,15 L14,15 L14,21 L20,21 L20,10 Z" />
</vector>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FF000000"
android:pathData="M12,12 C14.2,12 16,10.2 16,8 C16,5.8 14.2,4 12,4 C9.8,4 8,5.8 8,8 C8,10.2 9.8,12 12,12 Z M4,20 C4,16.7 7.6,14 12,14 C16.4,14 20,16.7 20,20 Z" />
</vector>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FF000000"
android:pathData="M5,20 L5,12 L8,12 L8,20 Z M11,20 L11,6 L14,6 L14,20 Z M17,20 L17,9 L20,9 L20,20 Z" />
</vector>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FF000000"
android:pathData="M4,5 L20,5 L20,7 L4,7 Z M4,11 L20,11 L20,13 L4,13 Z M4,17 L14,17 L14,19 L4,19 Z" />
</vector>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_nav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
app:labelVisibilityMode="labeled"
app:menu="@menu/bottom_nav_menu" />
</LinearLayout>

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="24dp">
<TextView
android:id="@+id/module_tag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="22sp"
android:textStyle="bold"
android:textColor="?attr/colorPrimary"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/hint"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/hint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:textSize="14sp"
android:alpha="0.7"
app:layout_constraintTop_toBottomOf="@id/module_tag"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/nav_home"
android:icon="@drawable/ic_nav_home"
android:title="@string/tab_home" />
<item
android:id="@+id/nav_task"
android:icon="@drawable/ic_nav_task"
android:title="@string/tab_task" />
<item
android:id="@+id/nav_collect"
android:icon="@drawable/ic_nav_collect"
android:title="@string/tab_collect" />
<item
android:id="@+id/nav_stats"
android:icon="@drawable/ic_nav_stats"
android:title="@string/tab_stats" />
<item
android:id="@+id/nav_mine"
android:icon="@drawable/ic_nav_mine"
android:title="@string/tab_mine" />
</menu>

View File

@@ -0,0 +1,5 @@
<?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" />
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?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" />
</adaptive-icon>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- 品牌色占位S1 视觉规范化时校准) -->
<color name="brand_primary">#1E6FD9</color>
<color name="brand_on_primary">#FFFFFF</color>
<color name="brand_background">#F7F9FC</color>
<color name="brand_surface">#FFFFFF</color>
<color name="launcher_background">#1E6FD9</color>
</resources>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">监测数据不落地</string>
<string name="tab_home">首页</string>
<string name="tab_task">任务</string>
<string name="tab_collect">采集</string>
<string name="tab_stats">统计</string>
<string name="tab_mine">我的</string>
<string name="placeholder_hint">占位页 —— 业务功能按 S1S4 计划经 FeatureMount 挂载</string>
</resources>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- 应用主主题Material 3 DayNight无标题栏自管理标题 -->
<style name="Theme.SteCmd" parent="Theme.Material3.DayNight.NoActionBar">
<item name="colorPrimary">@color/brand_primary</item>
<item name="colorOnPrimary">@color/brand_on_primary</item>
<item name="android:colorBackground">@color/brand_background</item>
<item name="colorSurface">@color/brand_surface</item>
<item name="android:statusBarColor">@color/brand_background</item>
<item name="android:windowLightStatusBar" tools:targetApi="m">true</item>
</style>
<!-- 启动闪屏core-splashscreen 兼容 12 以下) -->
<style name="Theme.SteCmd.Splash" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/launcher_background</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_launcher_foreground</item>
<item name="postSplashScreenTheme">@style/Theme.SteCmd</item>
</style>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
网络安全白名单:
- 默认全域强制 HTTPSusesCleartextTraffic=false 于 Manifest
- 仅对接口文档 1.2.4 测试环境 IP 开放明文 HTTP外场联调发布前如生产全 HTTPS 可移除;
- 测试域名 jcd.stec.p-q.co 为 HTTPS无需额外配置。
-->
<network-security-config>
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">106.15.183.20</domain>
</domain-config>
</network-security-config>