feat(demo): 对齐 iOS demo 布局 + Info+Ver 并发按钮
主要改动: - DemoSecrets 默认值对齐 iOS:owner=xmly, scanNamePrefix=Duooomi- - userId 默认值对齐:Cjgex9kTTTJq9u9RMOnc5hiIZTvrOOq3 - Config 区可在运行时编辑 owner/apiKey/scanPrefix,SharedPreferences 持久化;Reload SDK 按钮重建实例(旧 SDK 自动 disconnect) - PlayMode 行:显示当前模式标签(循环 / 单播)+ Single/Loop 按钮 - Firmware OTA:3 个独立输入(sn/currentVersion/userId) +「回填当前 状态」+「拉取安装包」+「安装」三按钮,对齐 iOS WrapperTestView - bind 完成 / version 更新自动回填 OTA 入参 - UpgradeRecords 4 状态展示:待上报 / ✅ 正确上报 / ❌ 错误上报 (版本不匹配) / 过期放弃 - 保留先前加的 Info+Ver 并发按钮(async 同时发 getDeviceInfo + getVersion,验证不同 commandType 通道并发不冲突) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,9 +13,9 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.duooomi.ble.DuooomiBleSDK
|
import com.duooomi.ble.DuooomiBleSDK
|
||||||
import com.duooomi.ble.core.ConnectionState
|
import com.duooomi.ble.core.ConnectionState
|
||||||
import com.duooomi.ble.models.FirmwareUpgradeCheckResult
|
|
||||||
import com.duooomi.ble.models.PlayMode
|
import com.duooomi.ble.models.PlayMode
|
||||||
import com.duooomi.ble.protocol.CommandType
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
// MARK: - CDN Helper
|
// MARK: - CDN Helper
|
||||||
@@ -44,7 +44,11 @@ private enum class LogLevel { INFO, SUCCESS, ERROR }
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun DemoScreen(sdk: DuooomiBleSDK) {
|
fun DemoScreen(
|
||||||
|
sdk: DuooomiBleSDK,
|
||||||
|
config: DemoConfig,
|
||||||
|
onReload: (DemoConfig) -> Unit
|
||||||
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
// SDK state
|
// SDK state
|
||||||
@@ -56,13 +60,18 @@ fun DemoScreen(sdk: DuooomiBleSDK) {
|
|||||||
val isActivated by sdk.isActivated.collectAsState()
|
val isActivated by sdk.isActivated.collectAsState()
|
||||||
val transferProgress by sdk.transferProgress.collectAsState()
|
val transferProgress by sdk.transferProgress.collectAsState()
|
||||||
val sdkError by sdk.error.collectAsState()
|
val sdkError by sdk.error.collectAsState()
|
||||||
|
|
||||||
// Local state
|
|
||||||
var userId by remember { mutableStateOf("test-user-001") }
|
|
||||||
var fileUrl by remember { mutableStateOf("https://cdn.bowong.cc/material/569f48a8e29f47859b3a9808be37f94c.mp4") }
|
|
||||||
val sn by sdk.sn.collectAsState()
|
val sn by sdk.sn.collectAsState()
|
||||||
val firmwareUpgrade by sdk.firmwareUpgrade.collectAsState()
|
val firmwareUpgrade by sdk.firmwareUpgrade.collectAsState()
|
||||||
val upgradeRecords by sdk.upgradeRecords.collectAsState()
|
val upgradeRecords by sdk.upgradeRecords.collectAsState()
|
||||||
|
|
||||||
|
// Editable config (rekey on sdk change so new SDK 不会带着旧编辑残留)
|
||||||
|
var configOwner by remember(sdk) { mutableStateOf(config.owner) }
|
||||||
|
var configApiKey by remember(sdk) { mutableStateOf(config.apiKey) }
|
||||||
|
var configScanPrefix by remember(sdk) { mutableStateOf(config.scanPrefix) }
|
||||||
|
|
||||||
|
// Local state
|
||||||
|
var userId by remember { mutableStateOf("Cjgex9kTTTJq9u9RMOnc5hiIZTvrOOq3") }
|
||||||
|
var fileUrl by remember { mutableStateOf("https://cdn.bowong.cc/material/569f48a8e29f47859b3a9808be37f94c.mp4") }
|
||||||
var firmwareLoading by remember { mutableStateOf(false) }
|
var firmwareLoading by remember { mutableStateOf(false) }
|
||||||
var isBusy by remember { mutableStateOf(false) }
|
var isBusy by remember { mutableStateOf(false) }
|
||||||
var connectingDeviceId by remember { mutableStateOf<String?>(null) }
|
var connectingDeviceId by remember { mutableStateOf<String?>(null) }
|
||||||
@@ -70,6 +79,23 @@ fun DemoScreen(sdk: DuooomiBleSDK) {
|
|||||||
var logs by remember { mutableStateOf<List<LogLine>>(emptyList()) }
|
var logs by remember { mutableStateOf<List<LogLine>>(emptyList()) }
|
||||||
var logCounter by remember { mutableLongStateOf(0L) }
|
var logCounter by remember { mutableLongStateOf(0L) }
|
||||||
|
|
||||||
|
// OTA fetch 入参(可手动填,bind/getVersion 完成时自动回填)
|
||||||
|
var otaFetchSn by remember(sdk) { mutableStateOf("") }
|
||||||
|
var otaFetchVersion by remember(sdk) { mutableStateOf("") }
|
||||||
|
var otaFetchUserId by remember(sdk) { mutableStateOf("") }
|
||||||
|
|
||||||
|
// bind 完成 → 回填 OTA 入参
|
||||||
|
LaunchedEffect(isActivated, sn) {
|
||||||
|
if (isActivated && sn.isNotEmpty()) {
|
||||||
|
otaFetchSn = sn
|
||||||
|
if (version.isNotEmpty()) otaFetchVersion = version
|
||||||
|
if (otaFetchUserId.isEmpty()) otaFetchUserId = userId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(version) {
|
||||||
|
if (version.isNotEmpty()) otaFetchVersion = version
|
||||||
|
}
|
||||||
|
|
||||||
fun log(message: String, level: LogLevel = LogLevel.INFO) {
|
fun log(message: String, level: LogLevel = LogLevel.INFO) {
|
||||||
logCounter++
|
logCounter++
|
||||||
logs = listOf(LogLine(logCounter, level, message)) + logs.take(199)
|
logs = listOf(LogLine(logCounter, level, message)) + logs.take(199)
|
||||||
@@ -91,6 +117,24 @@ fun DemoScreen(sdk: DuooomiBleSDK) {
|
|||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
|
// === Config Section ===
|
||||||
|
item {
|
||||||
|
SectionHeader("SDK Config(修改后点 Reload 生效)")
|
||||||
|
ConfigField("x-owner", configOwner) { configOwner = it }
|
||||||
|
ConfigField("apiKey", configApiKey) { configApiKey = it }
|
||||||
|
ConfigField("scanPrefix", configScanPrefix) { configScanPrefix = it }
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
onReload(DemoConfig(configOwner, configApiKey, configScanPrefix))
|
||||||
|
log(
|
||||||
|
"SDK reloaded with owner=$configOwner, prefix=$configScanPrefix",
|
||||||
|
LogLevel.SUCCESS
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) { Text("Reload SDK") }
|
||||||
|
}
|
||||||
|
|
||||||
// === Status Section ===
|
// === Status Section ===
|
||||||
item {
|
item {
|
||||||
SectionHeader("Status")
|
SectionHeader("Status")
|
||||||
@@ -214,6 +258,27 @@ fun DemoScreen(sdk: DuooomiBleSDK) {
|
|||||||
} finally { isBusy = false }
|
} finally { isBusy = false }
|
||||||
}
|
}
|
||||||
}) { Text("getVersion") }
|
}) { Text("getVersion") }
|
||||||
|
|
||||||
|
OutlinedButton(onClick = {
|
||||||
|
scope.launch {
|
||||||
|
isBusy = true
|
||||||
|
log("-> getDeviceInfo + getVersion (并发)")
|
||||||
|
try {
|
||||||
|
coroutineScope {
|
||||||
|
val infoJob = async { sdk.getDeviceInfo() }
|
||||||
|
val verJob = async { sdk.getVersion() }
|
||||||
|
val info = infoJob.await()
|
||||||
|
val ver = verJob.await()
|
||||||
|
log(
|
||||||
|
"info+ver OK: brand=${info.brand} power=${info.powerlevel}% v=${ver.version}",
|
||||||
|
LogLevel.SUCCESS
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log("info+ver FAIL: ${e.message}", LogLevel.ERROR)
|
||||||
|
} finally { isBusy = false }
|
||||||
|
}
|
||||||
|
}) { Text("Info+Ver") }
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(Modifier.height(4.dp))
|
Spacer(Modifier.height(4.dp))
|
||||||
@@ -257,7 +322,18 @@ fun DemoScreen(sdk: DuooomiBleSDK) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Play mode toggle —— gated by isActivated
|
// Play mode toggle —— gated by isActivated
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text("PlayMode", fontSize = 12.sp)
|
||||||
|
val modeLabel = when (deviceInfo?.loop) {
|
||||||
|
PlayMode.LOOP -> "循环"
|
||||||
|
PlayMode.SINGLE -> "单播"
|
||||||
|
null -> ""
|
||||||
|
}
|
||||||
|
Text(modeLabel, fontSize = 12.sp, color = Color.Gray, modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -319,64 +395,59 @@ fun DemoScreen(sdk: DuooomiBleSDK) {
|
|||||||
) { Text("Transfer") }
|
) { Text("Transfer") }
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Firmware Section ===
|
// === Firmware OTA Section ===
|
||||||
item {
|
item {
|
||||||
SectionHeader("Firmware Update")
|
SectionHeader("Firmware OTA")
|
||||||
StatusRow("SN", sn.ifEmpty { "--" })
|
|
||||||
StatusRow("Current Version", version.ifEmpty { "--" })
|
StatusRow("Current Version", version.ifEmpty { "--" })
|
||||||
|
|
||||||
firmwareUpgrade?.targetFirmware?.let { target ->
|
ConfigField("sn", otaFetchSn) { otaFetchSn = it }
|
||||||
Card(modifier = Modifier.fillMaxWidth()) {
|
ConfigField("currentVersion", otaFetchVersion) { otaFetchVersion = it }
|
||||||
Column(modifier = Modifier.padding(12.dp)) {
|
ConfigField("userId", otaFetchUserId) { otaFetchUserId = it }
|
||||||
StatusRow("Latest", target.version)
|
|
||||||
target.fileSize?.let { StatusRow("Size", "$it bytes") }
|
|
||||||
target.fileMd5?.let { Text("MD5: $it", fontSize = 10.sp, color = Color.Gray) }
|
|
||||||
target.forceUpgrade?.let { Text("forceUpgrade=$it", fontSize = 11.sp, color = Color.Red) }
|
|
||||||
target.fileUrl?.let { Text(it, fontSize = 10.sp, color = Color.Gray, maxLines = 2) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: firmwareUpgrade?.let {
|
|
||||||
Text("Already up-to-date", fontSize = 12.sp, color = Color.Gray)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (firmwareLoading) {
|
Row(
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
CircularProgressIndicator(modifier = Modifier.size(16.dp), strokeWidth = 2.dp)
|
verticalAlignment = Alignment.CenterVertically
|
||||||
Spacer(Modifier.width(8.dp))
|
) {
|
||||||
Text("Fetching...", fontSize = 12.sp, color = Color.Gray)
|
OutlinedButton(onClick = {
|
||||||
}
|
otaFetchSn = sn
|
||||||
}
|
otaFetchVersion = version
|
||||||
|
otaFetchUserId = userId
|
||||||
|
}) { Text("回填当前状态") }
|
||||||
|
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
firmwareLoading = true
|
firmwareLoading = true
|
||||||
log("-> fetchLatestFirmware(sn=$sn, version=$version, $userId)")
|
log("-> fetchLatestFirmware(sn=$otaFetchSn, version=$otaFetchVersion, userId=$otaFetchUserId) [report→softBind→check]")
|
||||||
try {
|
try {
|
||||||
val r = sdk.fetchLatestFirmware(sn = sn, currentVersion = version, userId = userId)
|
val r = sdk.fetchLatestFirmware(
|
||||||
val target = r.targetFirmware
|
sn = otaFetchSn,
|
||||||
if (target != null) {
|
currentVersion = otaFetchVersion,
|
||||||
log("firmware latest: ${target.version}", LogLevel.SUCCESS)
|
userId = otaFetchUserId
|
||||||
} else {
|
)
|
||||||
log("already up-to-date")
|
val tv = r.targetFirmware?.version ?: "—"
|
||||||
}
|
log("fetchLatestFirmware OK target=$tv", LogLevel.SUCCESS)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
log("firmware info FAIL: ${e.message}", LogLevel.ERROR)
|
log("fetchLatestFirmware FAIL: ${e.message}", LogLevel.ERROR)
|
||||||
} finally { firmwareLoading = false }
|
} finally { firmwareLoading = false }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enabled = !firmwareLoading && sn.isNotEmpty()
|
enabled = !firmwareLoading && otaFetchSn.isNotEmpty() && otaFetchUserId.isNotEmpty()
|
||||||
) { Text("Get Update Info") }
|
) { Text("拉取安装包") }
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val target: FirmwareUpgradeCheckResult.TargetFirmware =
|
val target = firmwareUpgrade?.targetFirmware
|
||||||
firmwareUpgrade?.targetFirmware ?: return@launch
|
val targetFileUrl = target?.fileUrl
|
||||||
val targetFileUrl = target.fileUrl ?: return@launch
|
if (target == null || targetFileUrl.isNullOrEmpty()) {
|
||||||
|
log("install FAIL: 无可用固件包(请先点拉取安装包)", LogLevel.ERROR)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
isBusy = true
|
isBusy = true
|
||||||
log("-> upgradeFirmware ${target.version}")
|
log("-> upgradeFirmware sn=$sn $version→${target.version} (fw=${target.id})")
|
||||||
try {
|
try {
|
||||||
sdk.upgradeFirmware(
|
sdk.upgradeFirmware(
|
||||||
sn = sn,
|
sn = sn,
|
||||||
@@ -385,33 +456,69 @@ fun DemoScreen(sdk: DuooomiBleSDK) {
|
|||||||
fromVersion = version,
|
fromVersion = version,
|
||||||
targetVersion = target.version
|
targetVersion = target.version
|
||||||
)
|
)
|
||||||
log("OTA OK", LogLevel.SUCCESS)
|
log("install OK(已落 UpgradeRecord,下次 fetchLatestFirmware 自动对账上报)", LogLevel.SUCCESS)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
log("OTA FAIL: ${e.message}", LogLevel.ERROR)
|
log("install FAIL: ${e.message}", LogLevel.ERROR)
|
||||||
} finally { isBusy = false }
|
} finally { isBusy = false }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enabled = firmwareUpgrade?.targetFirmware?.fileUrl?.isNotEmpty() == true
|
enabled = firmwareUpgrade?.targetFirmware?.fileUrl?.isNotEmpty() == true && !isBusy
|
||||||
&& connectedDevice != null && !isBusy
|
) { Text("安装") }
|
||||||
) { Text("Upgrade") }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (upgradeRecords.isNotEmpty()) {
|
if (firmwareLoading) {
|
||||||
Spacer(Modifier.height(8.dp))
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text("Upgrade Records (${upgradeRecords.size})", fontSize = 12.sp, fontWeight = FontWeight.Bold)
|
CircularProgressIndicator(modifier = Modifier.size(14.dp), strokeWidth = 2.dp)
|
||||||
upgradeRecords.sortedByDescending { it.triggeredAt }.take(5).forEach { r ->
|
Spacer(Modifier.width(8.dp))
|
||||||
val outcome = r.outcome
|
Text("Fetching...", fontSize = 12.sp, color = Color.Gray)
|
||||||
val outcomeText = when {
|
|
||||||
!r.reported -> "pending"
|
|
||||||
outcome == null -> "expired"
|
|
||||||
else -> outcome.raw
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
firmwareUpgrade?.let { upgrade ->
|
||||||
|
val target = upgrade.targetFirmware
|
||||||
|
if (target != null) {
|
||||||
|
StatusRow("target version", target.version)
|
||||||
|
Text("firmwareId: ${target.id}", fontSize = 10.sp, color = Color.Gray)
|
||||||
|
target.forceUpgrade?.let {
|
||||||
Text(
|
Text(
|
||||||
"[$outcomeText] fw=${r.firmwareId} ${r.fromVersion}→${r.targetVersion}",
|
"forceUpgrade: ${if (it) "是" else "否"}",
|
||||||
fontSize = 10.sp,
|
fontSize = 10.sp,
|
||||||
color = Color.Gray
|
color = if (it) Color.Red else Color.Gray
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
target.fileSize?.let { Text("fileSize: $it", fontSize = 10.sp, color = Color.Gray) }
|
||||||
|
target.fileMd5?.let { Text("md5: $it", fontSize = 10.sp, color = Color.Gray) }
|
||||||
|
target.fileUrl?.let { Text(it, fontSize = 10.sp, color = Color.Gray, maxLines = 2) }
|
||||||
|
} else {
|
||||||
|
Text("无可用固件(targetFirmware 为空)", fontSize = 12.sp, color = Color.Gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Upgrade Records ===
|
||||||
|
if (upgradeRecords.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
"Upgrade Records (${upgradeRecords.size})",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color.Gray,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
modifier = Modifier.padding(top = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items(upgradeRecords.sortedByDescending { it.triggeredAt }, key = { it.firmwareId + it.sn }) { r ->
|
||||||
|
Column(modifier = Modifier.padding(vertical = 2.dp)) {
|
||||||
|
Text("${r.sn} · ${r.firmwareId}", fontSize = 11.sp)
|
||||||
|
Text("${r.fromVersion} → ${r.targetVersion}", fontSize = 10.sp, color = Color.Gray)
|
||||||
|
val outcome = r.outcome
|
||||||
|
val (statusText, statusColor) = when {
|
||||||
|
!r.reported -> "待上报" to Color(0xFFFF9800)
|
||||||
|
outcome?.raw == "success" -> "✅ 正确上报" to Color(0xFF2E7D32)
|
||||||
|
outcome?.raw == "version_mismatch" -> "❌ 错误上报(版本不匹配)" to Color.Red
|
||||||
|
else -> "过期放弃" to Color.Gray
|
||||||
|
}
|
||||||
|
Text(statusText, fontSize = 10.sp, color = statusColor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,6 +593,23 @@ private fun SectionHeader(title: String) {
|
|||||||
Divider()
|
Divider()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ConfigField(label: String, value: String, onChange: (String) -> Unit) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(label, fontSize = 12.sp, modifier = Modifier.width(90.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = value,
|
||||||
|
onValueChange = onChange,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
singleLine = true,
|
||||||
|
textStyle = androidx.compose.ui.text.TextStyle(fontSize = 12.sp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun StatusRow(label: String, value: String) {
|
private fun StatusRow(label: String, value: String) {
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.duooomi.ble.demo
|
package com.duooomi.ble.demo
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -10,42 +12,76 @@ import androidx.activity.result.contract.ActivityResultContracts
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.duooomi.ble.DuooomiBleConfig
|
import com.duooomi.ble.DuooomiBleConfig
|
||||||
import com.duooomi.ble.DuooomiBleSDK
|
import com.duooomi.ble.DuooomiBleSDK
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
private lateinit var sdk: DuooomiBleSDK
|
|
||||||
|
|
||||||
private val permissionLauncher = registerForActivityResult(
|
private val permissionLauncher = registerForActivityResult(
|
||||||
ActivityResultContracts.RequestMultiplePermissions()
|
ActivityResultContracts.RequestMultiplePermissions()
|
||||||
) { /* permissions granted or denied — SDK handles errors */ }
|
) { /* permissions granted or denied — SDK handles errors */ }
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
sdk = DuooomiBleSDK(
|
|
||||||
context = this,
|
|
||||||
config = DuooomiBleConfig(
|
|
||||||
apiKey = DemoSecrets.API_KEY,
|
|
||||||
owner = DemoSecrets.OWNER,
|
|
||||||
scanNamePrefix = DemoSecrets.SCAN_NAME_PREFIX
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
requestBlePermissions()
|
requestBlePermissions()
|
||||||
|
|
||||||
|
val prefs = getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
val initialConfig = DemoConfig(
|
||||||
|
owner = prefs.getString(KEY_OWNER, DemoSecrets.OWNER) ?: DemoSecrets.OWNER,
|
||||||
|
apiKey = prefs.getString(KEY_API_KEY, DemoSecrets.API_KEY) ?: DemoSecrets.API_KEY,
|
||||||
|
scanPrefix = prefs.getString(KEY_SCAN_PREFIX, DemoSecrets.SCAN_NAME_PREFIX)
|
||||||
|
?: DemoSecrets.SCAN_NAME_PREFIX
|
||||||
|
)
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
|
var config by remember { mutableStateOf(initialConfig) }
|
||||||
|
var sdk by remember { mutableStateOf(buildSdk(config)) }
|
||||||
|
|
||||||
MaterialTheme {
|
MaterialTheme {
|
||||||
Surface(modifier = Modifier.fillMaxSize()) {
|
Surface(modifier = Modifier.fillMaxSize()) {
|
||||||
DemoScreen(sdk = sdk)
|
DemoScreen(
|
||||||
|
sdk = sdk,
|
||||||
|
config = config,
|
||||||
|
onReload = { newConfig ->
|
||||||
|
persist(prefs, newConfig)
|
||||||
|
// 旧 SDK:fire-and-forget disconnect 释放 BLE 资源
|
||||||
|
val old = sdk
|
||||||
|
@Suppress("OPT_IN_USAGE")
|
||||||
|
GlobalScope.launch { runCatching { old.disconnect() } }
|
||||||
|
config = newConfig
|
||||||
|
sdk = buildSdk(newConfig)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun buildSdk(c: DemoConfig) = DuooomiBleSDK(
|
||||||
|
context = this,
|
||||||
|
config = DuooomiBleConfig(
|
||||||
|
apiKey = c.apiKey,
|
||||||
|
owner = c.owner,
|
||||||
|
scanNamePrefix = c.scanPrefix
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun persist(prefs: SharedPreferences, c: DemoConfig) {
|
||||||
|
prefs.edit()
|
||||||
|
.putString(KEY_OWNER, c.owner)
|
||||||
|
.putString(KEY_API_KEY, c.apiKey)
|
||||||
|
.putString(KEY_SCAN_PREFIX, c.scanPrefix)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
private fun requestBlePermissions() {
|
private fun requestBlePermissions() {
|
||||||
val permissions = mutableListOf<String>()
|
val permissions = mutableListOf<String>()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
@@ -62,4 +98,17 @@ class MainActivity : ComponentActivity() {
|
|||||||
permissionLauncher.launch(needed.toTypedArray())
|
permissionLauncher.launch(needed.toTypedArray())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val PREFS = "demo_config"
|
||||||
|
const val KEY_OWNER = "demo.config.owner"
|
||||||
|
const val KEY_API_KEY = "demo.config.apiKey"
|
||||||
|
const val KEY_SCAN_PREFIX = "demo.config.scanPrefix"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DemoConfig(
|
||||||
|
val owner: String,
|
||||||
|
val apiKey: String,
|
||||||
|
val scanPrefix: String
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
package com.duooomi.ble.demo
|
package com.duooomi.ble.demo
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copy this file to `DemoSecrets.kt` (gitignored) and fill in real credentials.
|
* Copy this file to `demo/src/main/kotlin/com/duooomi/ble/demo/DemoSecrets.kt`
|
||||||
|
* (already gitignored) and fill in real credentials.
|
||||||
*/
|
*/
|
||||||
object DemoSecrets {
|
object DemoSecrets {
|
||||||
const val API_KEY: String = "<paste-api-key>"
|
const val API_KEY: String = "<paste-api-key>"
|
||||||
const val OWNER: String = "<paste-owner>"
|
const val OWNER: String = "xmly"
|
||||||
const val SCAN_NAME_PREFIX: String = "Duooomi-"
|
const val SCAN_NAME_PREFIX: String = "Duooomi-"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user