架构代码 mindflow app

太好了!下面给你一套完整的 可扩展安卓 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.htmlmain.jsmanifest.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()
    }
}

对外可选 接口:你可以暴露一个简易 ContentProviderbound 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_PLAYBACK serviceType。


七、关键用户流程

  1. 练习开始

    • 选择带课音频 → 播放 → LogApi.start()
    • 设备连接(可选)→ 数据流入 → LogApi.appendMetrics(...)
  2. 练习结束

    • 停止播放 → LogApi.end(entryId)
    • 用户可编辑标题/备注/标签
  3. 查看与编辑日志

    • 日历页/列表页按日期筛选 → 进入详情可编辑
  4. 同步

    • 自动(WorkManager)+ 手动同步;断网缓存
  5. 插件

    • 插件商店 → 下载并安装 → 启动 WebView 容器
    • 插件通过 JS Bridge 调用 appendLogplayAudio、订阅设备数据
  6. 更新

    • 启动时及后台定时检查 → 弹窗提示或强制更新

八、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 直接拒绝。
  • CSPindex.html 使用严格 Content-Security-Policy,禁止内联脚本、外域资源(除非白名单)。
  • 签名校验:插件 Zip 与 APK 都做签名/Hash 校验,避免被篡改。
  • 隔离:每个插件独立数据目录;WebView 禁止 file:// 读取其他路径。
  • 崩溃隔离:插件异常不影响宿主(捕获 JS 异常并日志化)。
  • 可观测性:埋点/日志把插件版本与设备类型上报,定位问题更快。

十、你可以直接落地的“最小闭环”

  1. 先把 音频播放 + 日志 start/end 做通(ExoPlayer + Room)。
  2. 接入 UpdateChecker(自建 JSON),完成更新闭环。
  3. 接上 PluginManager,加载一个最简单的插件(按钮调用 HostAPI.appendLog(...))。
  4. 最后接入 一个硬件模块(比如 HRV),把指标写到 log_metrics

如果你愿意,我可以把这些骨架按 Android Studio 项目结构整理成一个 起始模板包(含基本 Activity、Service、DAO、Bridge、示例插件和清单),你直接导入即可开发下一步。


评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注