Skip to content

Fix reboot-time authorization reset by hardening config persistence and load failure handling#28

Merged
ItosEO merged 4 commits intomainfrom
copilot/fix-config-loss-on-reboot
Mar 3, 2026
Merged

Fix reboot-time authorization reset by hardening config persistence and load failure handling#28
ItosEO merged 4 commits intomainfrom
copilot/fix-config-loss-on-reboot

Conversation

Copy link
Contributor

Copilot AI commented Mar 3, 2026

On some Root startup paths (notably Android 15), authorization state was read as default (ASK) after reboot, causing repeated permission prompts.

This was caused by two related startup risks:

  1. Direct-boot storage boundary: config DB could be unavailable during early boot if stored in CE.
  2. Transient provider load failure path: loadFromManagerWithStatus() treated callProvider(...) == null as a successful empty config load, which could trigger writes that overwrite persisted permission flags back to defaults.
  • Changes (minimal and scoped)

    • Updated AppDatabase.get(...) to open Room DB from createDeviceProtectedStorageContext().
    • Added migration attempt from CE -> DPS using moveDatabaseFrom(...) for existing installs.
    • Wrapped migration in runCatching to avoid startup crash if source DB is absent/already migrated.
    • Updated server config loading logic so reply == null is treated as load failure (status=false) instead of successful empty config load.
  • Effect on behavior

    • Config (including per-app permission flags) remains readable during locked-boot startup.
    • Transient manager/provider unavailability during reboot no longer causes authorization config to be overwritten to default ASK.
    • Reboot no longer resets authorized apps to “ask again” due to these startup races.
val deviceContext = context.applicationContext.createDeviceProtectedStorageContext()
runCatching { deviceContext.moveDatabaseFrom(context.applicationContext, DATABASE_NAME) }

instance ?: Room.databaseBuilder(deviceContext, AppDatabase::class.java, DATABASE_NAME)
    .fallbackToDestructiveMigration()
    .build()
val reply = callProvider("loadConfig", null)
if (reply == null) {
    return Pair(StellarConfig(), false)
}
Original prompt

This section details on the original issue you should resolve

<issue_title>最新1.0.13版还是丢配置每次重启都得重新授权</issue_title>
<issue_description>### 问题描述

最新1.0.13版还是丢配置每次重启都得重新授权

复现步骤

最新1.0.13版还是丢配置每次重启都得重新授权

期望行为

No response

启动方式

Root

Stellar 版本

1.0.13

Android 版本

安卓15

设备型号

Redmi Note 12 Turbo

日志信息

补充信息

No response</issue_description>

<agent_instructions>请分析什么原因可能导致的开机丢配置</agent_instructions>

Comments on the Issue (you are @copilot in this section)

@roro2239 收到,有root权限是吧 @roro2239 降权关掉试试 @roro2239 @ItosEO @ItosEO 丢配置是指? @ItosEO > > 丢配置是指? > > 授权应用里,每次重启。都会变成询问。

好</comment_new>


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

Copilot AI and others added 2 commits March 3, 2026 14:20
…rtup

Co-authored-by: ItosEO <83892149+ItosEO@users.noreply.github.com>
Co-authored-by: ItosEO <83892149+ItosEO@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix configuration loss on reboot in version 1.0.13 Fix boot-time authorization reset by moving manager config DB to device-protected storage Mar 3, 2026
@ItosEO
Copy link
Collaborator

ItosEO commented Mar 3, 2026

但是使用标准boot-completed也不行,这个时间点CE已经解密

@ItosEO
Copy link
Collaborator

ItosEO commented Mar 3, 2026

@copilot 但是使用标准boot-completed也不行,这个时间点CE已经解密

Co-authored-by: ItosEO <83892149+ItosEO@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 3, 2026

但是使用标准boot-completed也不行,这个时间点CE已经解密

你这个判断对,BOOT_COMPLETED 时 CE 通常已可用。根因不只存储位置:loadFromManagerWithStatus() 之前把 reply == null 当成“成功读取空配置”,会在启动阶段把已有授权覆盖成默认 ASK。我已修复为 null 视为加载失败并跳过初始化写回,避免重启时误清空授权。提交:7765555。

@copilot 但是使用标准boot-completed也不行,这个时间点CE已经解密

已按你说的场景补修:即使是标准 BOOT_COMPLETED,也可能出现 provider 短暂不可用,之前代码会把这种失败当成空配置并覆盖授权。现在改成 reply == null 直接判定加载失败(不做初始化写回),避免每次重启回到“询问”。提交:7765555。

Copilot AI changed the title Fix boot-time authorization reset by moving manager config DB to device-protected storage Fix reboot-time authorization reset by hardening config persistence and load failure handling Mar 3, 2026
@ItosEO ItosEO marked this pull request as ready for review March 3, 2026 15:56
Copilot AI review requested due to automatic review settings March 3, 2026 15:56
@ItosEO ItosEO merged commit d04d9ad into main Mar 3, 2026
3 checks passed
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a reboot-time authorization reset bug on Android 15 (Root startup path), where per-app permission flags reverted to ASK after every reboot. Two root causes are addressed: (1) the config database being stored in Credential Encrypted (CE) storage, making it unavailable during early (direct) boot, and (2) a transient provider load failure being silently misclassified as an empty successful load, causing permission state to be overwritten with defaults.

Changes:

  • AppDatabase.kt: Database is now opened from Device Protected Storage (DPS) via createDeviceProtectedStorageContext(), with a one-time migration from CE → DPS using moveDatabaseFrom() wrapped in runCatching for safety.
  • ConfigManager.kt: loadFromManagerWithStatus() now treats a null reply from callProvider() as a load failure (status = false), preventing default-config writes when the provider is transiently unavailable during boot.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
manager/src/main/kotlin/roro/stellar/manager/db/AppDatabase.kt Opens DB in DPS with CE→DPS migration for direct-boot accessibility
server/src/main/kotlin/roro/stellar/server/ConfigManager.kt Treats null provider reply as load failure to prevent config reset


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +19 to 22
runCatching { deviceContext.moveDatabaseFrom(context.applicationContext, DATABASE_NAME) }
instance ?: Room.databaseBuilder(deviceContext, AppDatabase::class.java, DATABASE_NAME)
.fallbackToDestructiveMigration()
.build().also { instance = it }
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The moveDatabaseFrom migration call is placed outside the inner instance ?: check but inside the synchronized block. In the double-checked locking pattern, a second thread that was waiting at the synchronized boundary (with instance == null at the outer check) will re-evaluate instance in the inner check and skip building the DB — but it will still execute the moveDatabaseFrom call unnecessarily.

While this is safe in practice because runCatching absorbs the resulting error (the source file no longer exists after the first successful migration), it is cleaner and more efficient to place the migration inside the inner instance ?: check alongside the Room builder call, so that it is only ever attempted once during DB initialization.

Suggested change
runCatching { deviceContext.moveDatabaseFrom(context.applicationContext, DATABASE_NAME) }
instance ?: Room.databaseBuilder(deviceContext, AppDatabase::class.java, DATABASE_NAME)
.fallbackToDestructiveMigration()
.build().also { instance = it }
instance ?: run {
runCatching { deviceContext.moveDatabaseFrom(context.applicationContext, DATABASE_NAME) }
Room.databaseBuilder(deviceContext, AppDatabase::class.java, DATABASE_NAME)
.fallbackToDestructiveMigration()
.build().also { instance = it }
}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

最新1.0.13版还是丢配置每次重启都得重新授权

3 participants