You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android 13+下Kotlin闹钟App的Room数据库备份恢复权限配置

Android 13+ 下Room数据库备份/恢复的权限配置与代码优化

一、权限适配方案

1. Android 12及以下版本

在AndroidManifest.xml中声明存储权限,并在运行时动态申请:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

动态申请代码示例:

private val requestPermissionsLauncher = registerForActivityResult(
    ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
    val allGranted = permissions.values.all { it }
    if (allGranted) {
        // 权限已授予,执行备份/恢复
    } else {
        // 权限被拒绝,提示用户开启
    }
}

// 触发权限申请
fun requestStoragePermissions() {
    requestPermissionsLauncher.launch(
        arrayOf(
            Manifest.permission.READ_EXTERNAL_STORAGE,
            Manifest.permission.WRITE_EXTERNAL_STORAGE
        )
    )
}

2. Android 13及以上版本

Android 13拆分了存储权限,且直接访问公共目录的旧方式被限制,推荐以下两种无权限方案:

  • 写入备份文件:使用MediaStore直接写入Download目录,无需申请权限
  • 读取备份文件:使用**存储访问框架(SAF)**让用户选择文件,无需申请权限

二、备份功能(Export)代码优化

替换原直接操作File的逻辑,改用MediaStore写入公共Download目录:

fun exportDataToJson(context: Context) {
    CoroutineScope(Dispatchers.IO).launch {
        val db = AppDatabase.getDatabase(context)

        val remedio = db.remedioDao().getRemedio()
        val alarmeRemedio = db.alarmeDao().getAllAlarme()
        val itemAlarmeRemedio = db.itemAlarmeDao().getAllItemAlarme()

        val dataToExport = mapOf(
            "remedio" to remedio,
            "alarmeRemedio" to alarmeRemedio,
            "itemAlarmeRemedio" to itemAlarmeRemedio
        )
        val json = Gson().toJson(dataToExport)

        // 配置文件信息,自动创建room_backups子目录
        val contentValues = ContentValues().apply {
            put(MediaStore.MediaColumns.DISPLAY_NAME, "my_remedio.json")
            put(MediaStore.MediaColumns.MIME_TYPE, "application/json")
            put(MediaStore.MediaColumns.RELATIVE_PATH, "${Environment.DIRECTORY_DOWNLOADS}/room_backups")
        }

        // 插入文件到MediaStore并写入内容
        val uri = context.contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
        uri?.let {
            context.contentResolver.openOutputStream(it)?.use { outputStream ->
                outputStream.write(json.toByteArray())
                withContext(Dispatchers.Main) {
                    Toast.makeText(context, "备份成功", Toast.LENGTH_SHORT).show()
                }
            }
        } ?: run {
            withContext(Dispatchers.Main) {
                Toast.makeText(context, "创建备份文件失败", Toast.LENGTH_SHORT).show()
            }
        }
    }
}

三、恢复功能(Restore)代码优化

改用SAF让用户选择备份文件,避免权限问题:

1. 启动文件选择器

在Activity/Fragment中注册文件选择回调:

private val pickBackupLauncher = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
    uri?.let {
        CoroutineScope(Dispatchers.IO).launch {
            // 读取选中文件的内容
            val jsonContent = context.contentResolver.openInputStream(it)?.bufferedReader()?.use { reader ->
                reader.readText()
            } ?: ""
            // 执行恢复逻辑
            restoreFromJson(context, jsonContent)
        }
    }
}

// 触发文件选择
fun pickBackupFile() {
    pickBackupLauncher.launch(arrayOf("application/json"))
}

2. 优化恢复逻辑

修改原方法直接接收JSON字符串,同时修复闹钟调度的协程异步问题:

fun restoreFromJson(context: Context, jsonContent: String) {
    if (jsonContent.isEmpty()) {
        withContext(Dispatchers.Main) {
            Toast.makeText(context, "备份文件为空", Toast.LENGTH_SHORT).show()
        }
        return
    }

    val gson = Gson()
    val jsonObject = JsonParser.parseString(jsonContent).asJsonObject
    val remedios: List<RemedioVo> = gson.fromJson(
        jsonObject.getAsJsonArray("remedio"),
        object : TypeToken<List<RemedioVo>>() {}.type
    )
    val alarmes: List<AlarmeRemedioVo> = gson.fromJson(
        jsonObject.getAsJsonArray("alarmeRemedio"),
        object : TypeToken<List<AlarmeRemedioVo>>() {}.type
    )
    val itensAlarme: List<ItemAlarmeRemedioVo> = gson.fromJson(
        jsonObject.getAsJsonArray("itemAlarmeRemedio"),
        object : TypeToken<List<ItemAlarmeRemedioVo>>() {}.type
    )

    val database = AppDatabase.getDatabase(context)
    val remedioDao = database.remedioDao()
    val alarmeDao = database.alarmeDao()
    val itemAlarmeDao = database.itemAlarmeDao()

    CoroutineScope(Dispatchers.IO).launch {
        try {
            // 事务保证数据一致性
            database.runInTransaction {
                remedioDao.deleteAllRemedios()
                remedioDao.insertAllRemedios(remedios)

                alarmeDao.deleteAllAlarmes()
                alarmeDao.insertAllAlarmes(alarmes)

                itemAlarmeDao.deleteAllItensAlarme()
                itemAlarmeDao.insertAllItensAlarme(itensAlarme)
            }

            val currentTime = System.currentTimeMillis()
            val activeItems = itemAlarmeDao.getAllItemAlarmeMais(currentTime)

            withContext(Dispatchers.Main) {
                val alarmManager = context.getSystemService(AlarmManager::class.java)
                // 取消现有闹钟
                activeItems.forEach { item ->
                    val intent = Intent(context, AlarmReceiverNovo::class.java).apply {
                        putExtra("id", item.id)
                    }
                    val pendingIntent = PendingIntent.getBroadcast(
                        context,
                        item.id!!,
                        intent,
                        PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
                    )
                    alarmManager.cancel(pendingIntent)
                }

                // 重新调度有效闹钟
                itensAlarme.filter { it.dataAlarme > currentTime }.forEach { item ->
                    val alarme = alarmes.find { it.id == item.fk_alertaRemedio }
                    val intent = Intent(context, AlarmReceiverNovo::class.java).apply {
                        putExtra("id", item.id)
                        putExtra("remedio", alarme)
                    }
                    val pendingIntent = PendingIntent.getBroadcast(
                        context,
                        item.id!!,
                        intent,
                        PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
                    )
                    alarmManager.setExactAndAllowWhileIdle(
                        AlarmManager.RTC_WAKEUP,
                        item.dataAlarme,
                        pendingIntent
                    )
                }

                Toast.makeText(context, "恢复成功", Toast.LENGTH_SHORT).show()
            }
        } catch (e: Exception) {
            withContext(Dispatchers.Main) {
                Toast.makeText(context, "恢复失败", Toast.LENGTH_SHORT).show()
            }
        }
    }
}

四、关键注意事项

  • 分区存储适配:Android 10及以上禁止直接访问/storage/emulated/0路径,必须使用MediaStore或SAF
  • PendingIntent标记:Android 12及以上必须添加FLAG_IMMUTABLE(广播场景推荐)或FLAG_MUTABLE
  • 事务处理:恢复数据时使用Room事务,避免部分数据插入失败导致的不一致
  • 权限简化:使用MediaStore写入和SAF读取无需申请存储权限,减少用户交互

内容的提问来源于stack exchange,提问作者Eliel Moreno

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 09:53:16