太好了!下面给你一套完整的 可扩展安卓 APP 设计 + 关键代码骨架,一次解决你提到的三大需求:
- 可扩展的硬件架构(EEG/HRV 等蓝牙设备)
- 可扩展的 HTML/JS 插件系统(社区审核后动态加载)
- 正念带课的音频播放 + 可编辑日志(本地存储、按日期查询、可同步到服务器)
- 应用更新检测与更新机制
我把它拆成模块说明 + 关键代码片段,便于你直接落地。若你用 Kotlin + AndroidX,复制这些骨架即可快速起步。
总体架构(模块化)
app/
├─ core/ # 基础:DI、配置、网络、更新、权限
├─ hardware/ # 硬件抽象层:BLE、设备模块(EEG/HRV…)
├─ plugins/ # HTML/JS 插件管理与沙箱 WebView
├─ features/
│ ├─ audio/ # 正念带课音频(ExoPlayer + 前台Service)
│ └─ logbook/ # 日志(Room + Repository + Sync)
└─ sync/ # WorkManager 增量同步、冲突策略
- UI 层:Jetpack Compose(或传统 View 任你选)
- 依赖注入:Hilt(或 Koin)
- 后台任务:WorkManager
- 播放:ExoPlayer(前台服务 + 媒体通知)
- 存储:Room(离线优先)
- 硬件:统一
HardwareModule接口 + BLE 抽象 - 插件:Zip 包(
index.html、main.js、manifest.json),WebView 沙箱 + JS Bridge - 更新:Play In-App Updates 或 自建 JSON Manifest 检测(都支持)
一、硬件可扩展(HAL 思路)
1) 统一接口
interface HardwareModule {
val name: String
val version: String
val capabilities: Set
<String> // e.g. setOf("eeg-stream", "hrv-stream")
suspend fun connect(context: Context, deviceId: String): Boolean
suspend fun disconnect()
fun isConnected(): Boolean
fun startStreaming()
fun stopStreaming()
fun setListener(listener: Listener?)
interface Listener {
fun onConnected()
fun onDisconnected()
fun onData(sample: HardwareSample)
fun onError(error: Throwable)
}
}
data class HardwareSample(
val deviceType: String, // "EEG" / "HRV"
val timestamp: Long,
val metrics: Map<String, Any> // { "alpha": 0.62, "hrvRMSSD": 45, ... }
)
2) 设备注册与发现
class HardwareRegistry @Inject constructor() {
private val modules = mutableMapOf<String, HardwareModule>() // key = deviceType
fun register(type: String, module: HardwareModule) { modules[type] = module }
fun get(type: String): HardwareModule? = modules[type]
fun all(): List
<HardwareModule> = modules.values.toList()
}
每新增硬件(EEG/HRV…)只需新增一个实现类并在
App启动时注册。
3) BLE 抽象(示意)
interface BleClient {
suspend fun scan(filter: UUID? = null): List
<BluetoothDevice>
suspend fun connect(device: BluetoothDevice): Boolean
fun read(uuid: UUID): ByteArray
fun write(uuid: UUID, data: ByteArray): Boolean
fun notify(uuid: UUID, onBytes: (ByteArray) -> Unit)
fun disconnect()
}
设备模块只关心 特征值 UUID 和 数据解析,其余由 BleClient 处理。
二、HTML/JS 插件系统(动态扩展,无需发版)
1) 插件包结构(Zip)
plugin-name-1.0.0.zip
├─ index.html
├─ main.js
├─ style.css
├─ assets/*
└─ manifest.json
manifest.json 示例:
{
"id": "com.mind.app.hrvanalytics",
"name": "HRV 可视化",
"version": "1.0.0",
"entry": "index.html",
"minAppVersionCode": 1,
"permissions": ["hardware:hrv", "log:write", "audio:control", "network"],
"signature": "BASE64_OF_SIGNATURE"
}
2) 插件管理器
- 从社区“商店”列表拉取可用插件(已审核)
- 下载 Zip → 校验签名/Hash → 解压到私有目录 → 记录到本地数据库(可启用/禁用)
- 通过 受限 WebView 加载
entry,只开放白名单 JS 接口
class PluginManager @Inject constructor(
private val context: Context,
private val verifier: PluginVerifier,
private val repo: PluginRepository
) {
suspend fun installFromUrl(url: String): PluginMeta {
val file = download(url) // 省略实现
val meta = unzipAndReadManifest(file)
verifier.verify(meta, file) // 校验签名/Hash、版本兼容
val installDir = persist(meta, file)
return repo.save(meta.copy(localPath = installDir.absolutePath))
}
fun launch(activity: Activity, pluginId: String) {
val meta = repo.get(pluginId) ?: error("Plugin not installed")
PluginWebActivity.start(activity, meta)
}
}
3) WebView 沙箱 + JS Bridge
class PluginWebActivity : AppCompatActivity() {
private lateinit var webView: WebView
private lateinit var bridge: JsBridge
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
webView = WebView(this).apply {
settings.javaScriptEnabled = true
settings.allowFileAccess = false
settings.domStorageEnabled = true
settings.setSupportMultipleWindows(false)
WebView.setWebContentsDebuggingEnabled(false)
}
bridge = JsBridge(this)
webView.addJavascriptInterface(bridge, "HostAPI")
setContentView(webView)
val entryUrl = "file://${intent.getStringExtra("entry")}"
webView.loadUrl(entryUrl)
}
}
class JsBridge(
private val ctx: Context,
@Inject private val logApi: LogApi,
@Inject private val hardwareRegistry: HardwareRegistry,
@Inject private val audioController: AudioController,
) {
@JavascriptInterface fun appendLog(json: String) {
runBlocking { logApi.appendFromPlugin(json) }
}
@JavascriptInterface fun playAudio(trackId: String) {
audioController.play(trackId)
}
@JavascriptInterface fun subscribeDevice(type: String) {
hardwareRegistry.get(type)?.setListener(object: HardwareModule.Listener {
override fun onData(sample: HardwareSample) {
val js = "window.onDeviceData && window.onDeviceData(${sample.toJson()});"
(ctx as Activity).runOnUiThread {
(ctx as PluginWebActivity).webView.evaluateJavascript(js, null)
}
}
// 其他回调略
})
}
}
安全:禁用 file 访问、启用 SafeBrowsing、严格白名单 API、校验签名、每个插件独立存储空间、可在
manifest.permissions上做细粒度授权控制(宿主端拦截)。
三、音频播放(正念带课)
- 使用 ExoPlayer + 前台 Service + 媒体通知
- 支持导入本地音频(SAF 文件选择器)作为“带课口令文件”
- 支持循环/定时(可选)
@AndroidEntryPoint
class AudioService : LifecycleService() {
@Inject lateinit var player: ExoPlayer
override fun onCreate() {
super.onCreate()
startForeground(NOTI_ID, buildNotification())
}
fun play(uri: Uri) { player.setMediaItem(MediaItem.fromUri(uri)); player.prepare(); player.play() }
fun pause() = player.pause()
fun stop() = player.stop()
}
AudioController(提供给 UI 和 JS Bridge 调用):
class AudioController @Inject constructor(private val ctx: Context) {
private val conn = ServiceConnectionImpl
<AudioService>()
fun play(trackId: String) { /* 解析 trackId -> Uri;bindService 后调用 service.play(uri) */ }
fun playCustom(uri: Uri) { /* 用户导入的带课文件 */ }
}
四、日志系统(可编辑、按日期查询、设备可写、可同步)
1) 数据模型(Room)
@Entity(tableName = "log_entries")
data class LogEntry(
@PrimaryKey val id: String = UUID.randomUUID().toString(),
val startTime: Long,
val endTime: Long?,
val title: String = "正念练习",
val note: String? = null, // 用户可编辑
val tags: String? = null, // 逗号分隔或 JSON
val source: String = "app", // app/hardware/plugin
val updatedAt: Long = System.currentTimeMillis()
)
@Entity(
tableName = "log_metrics",
primaryKeys = ["entryId", "timestamp", "key"]
)
data class LogMetric(
val entryId: String,
val timestamp: Long,
val key: String, // e.g. "eeg.alpha", "hrv.rmssd"
val value: Double
)
DAO 及查询:
@Dao
interface LogDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun upsert(entry: LogEntry)
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun upsertMetrics(metrics: List
<LogMetric>)
@Query("SELECT * FROM log_entries WHERE date(startTime/1000,'unixepoch','localtime') = :yyyymmdd")
suspend fun listByDate(yyyymmdd: String): List
<LogEntry>
@Query("UPDATE log_entries SET note=:note, updatedAt=:ts WHERE id=:id")
suspend fun updateNote(id: String, note: String?, ts: Long = System.currentTimeMillis())
}
硬件模块与插件都通过
LogApi追加数据:
interface LogApi {
suspend fun start(title: String = "正念练习"): String // returns entryId
suspend fun end(entryId: String)
suspend fun appendMetrics(entryId: String, metrics: List
<LogMetric>)
suspend fun appendFromPlugin(json: String) // 解析并写入
}
练习开始/结束时机:
- 开始播放带课 →
LogApi.start()(记录startTime) - 结束播放或用户手动结束 →
LogApi.end()(记录endTime) - 过程中硬件/插件不断
appendMetrics
2) 同步(服务器)
- 离线优先:本地 Room 为主
- 增量同步:记录
updatedAt;按时间窗口/游标向服务器上传 - 冲突策略:
updatedAt后写覆盖(或改为三路合并) - WorkManager:网络可用时后台触发;用户也可手动“立即同步”
class LogSyncWorker(
ctx: Context,
params: WorkerParameters,
) : CoroutineWorker(ctx, params) {
override suspend fun doWork(): Result {
// 1. 读取自上次游标以来变化的 LogEntry/LogMetric
// 2. 调用 API 上传
// 3. 拉取服务器更新并合并
return Result.success()
}
}
对外可选 接口:你可以暴露一个简易
ContentProvider或bound Service,供新蓝牙模块/插件调用日志写入(但推荐直接通过 app 内的LogApi)。
五、更新机制(两种都支持)
方案 A:Google Play In-App Updates(推荐上架应用)
- 依赖
com.google.android.play:app-update-ktx - 支持 Flexible(后台下载、完成后提示重启)和 Immediate(强制更新)
不上具体代码以节省篇幅,Play 文档示例接入很快。
方案 B:自建 JSON Manifest(私发/企业内)
服务器放一个 app-manifest.json:
{
"versionCode": 10012,
"versionName": "1.12.0",
"minSupportedCode": 10000,
"downloadUrl": "https://cdn.example.com/app-release-1.12.0.apk",
"changelog": "修复若干问题,新增HRV曲线。",
"signature": "BASE64_OF_APK_SIGNATURE"
}
启动与前台页面定期检查:
data class AppManifest(val versionCode: Int, val versionName: String,
val minSupportedCode: Int, val downloadUrl: String,
val changelog: String, val signature: String)
class UpdateChecker @Inject constructor(
private val api: UpdateApi, // Retrofit: GET /app-manifest.json
private val installer: ApkInstaller
) {
suspend fun checkAndPrompt(activity: Activity) {
val m = api.getManifest()
val current = BuildConfig.VERSION_CODE
when {
m.versionCode > current -> installer.promptDownload(activity, m)
m.minSupportedCode > current -> installer.forceUpdate(activity, m)
}
}
}
ApkInstaller.promptDownload():下载 APK(DownloadManager),校验签名 → 调用 ACTION_VIEW 安装(要求“未知来源应用”权限,企业/内测常见)。
六、权限清单(精简示例)
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
Android 13+ 媒体权限拆分;BLE 需要
BLUETOOTH_*新权限;前台播放要FOREGROUND_SERVICE_MEDIA_PLAYBACKserviceType。
七、关键用户流程
-
练习开始
- 选择带课音频 → 播放 →
LogApi.start() - 设备连接(可选)→ 数据流入 →
LogApi.appendMetrics(...)
- 选择带课音频 → 播放 →
-
练习结束
- 停止播放 →
LogApi.end(entryId) - 用户可编辑标题/备注/标签
- 停止播放 →
-
查看与编辑日志
- 日历页/列表页按日期筛选 → 进入详情可编辑
-
同步
- 自动(WorkManager)+ 手动同步;断网缓存
-
插件
- 插件商店 → 下载并安装 → 启动 WebView 容器
- 插件通过 JS Bridge 调用
appendLog、playAudio、订阅设备数据
-
更新
- 启动时及后台定时检查 → 弹窗提示或强制更新
八、Gradle 依赖(示例)
dependencies {
implementation "androidx.core:core-ktx:1.13.1"
implementation "androidx.appcompat:appcompat:1.7.0"
implementation "com.google.android.material:material:1.12.0"
// ExoPlayer
implementation "androidx.media3:media3-exoplayer:1.3.1"
implementation "androidx.media3:media3-ui:1.3.1"
// Room
implementation "androidx.room:room-ktx:2.6.1"
kapt "androidx.room:room-compiler:2.6.1"
// Hilt
implementation "com.google.dagger:hilt-android:2.52"
kapt "com.google.dagger:hilt-compiler:2.52"
// WorkManager
implementation "androidx.work:work-runtime-ktx:2.9.1"
// Retrofit/OkHttp
implementation "com.squareup.retrofit2:retrofit:2.11.0"
implementation "com.squareup.retrofit2:converter-moshi:2.11.0"
implementation "com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.14"
// In-App Updates(若使用Play机制)
implementation "com.google.android.play:app-update-ktx:2.1.0"
}
九、扩展与安全要点
- 插件权限模型:
manifest.permissions与宿主白名单匹配,不匹配的 API 直接拒绝。 - CSP:
index.html使用严格Content-Security-Policy,禁止内联脚本、外域资源(除非白名单)。 - 签名校验:插件 Zip 与 APK 都做签名/Hash 校验,避免被篡改。
- 隔离:每个插件独立数据目录;WebView 禁止
file://读取其他路径。 - 崩溃隔离:插件异常不影响宿主(捕获 JS 异常并日志化)。
- 可观测性:埋点/日志把插件版本与设备类型上报,定位问题更快。
十、你可以直接落地的“最小闭环”
- 先把 音频播放 + 日志 start/end 做通(ExoPlayer + Room)。
- 接入 UpdateChecker(自建 JSON),完成更新闭环。
- 接上 PluginManager,加载一个最简单的插件(按钮调用
HostAPI.appendLog(...))。 - 最后接入 一个硬件模块(比如 HRV),把指标写到
log_metrics。
如果你愿意,我可以把这些骨架按 Android Studio 项目结构整理成一个 起始模板包(含基本 Activity、Service、DAO、Bridge、示例插件和清单),你直接导入即可开发下一步。
发表回复