Skip to content

Commit 133d60a

Browse files
nqmgamingclaude
andcommitted
fix(settings): initialise the default-installer flow before init reads it
Opening Settings on an emulator killed the process: FATAL EXCEPTION: DefaultDispatcher-worker-1 java.lang.NullPointerException: Attempt to invoke interface method 'void kotlinx.coroutines.flow.MutableStateFlow.setValue(java.lang.Object)' on a null object reference at SettingViewModel$updateDefaultInstallerStatus$1.invokeSuspend(SettingViewModel.kt:927) _isDefaultInstaller was declared 480 lines below the init block that starts the coroutine assigning it. Property initialisers run in declaration order, so until the constructor reached line 850 the field was genuinely null, and whichever of the two got there first decided whether the app lived. Physical devices hid it: resolveActivity is an IPC, slow enough that the constructor always finished first. An emulator answers fast enough to lose the race — which is why this only ever showed up there. Moved next to _shizukuState and _rootState, the other state init touches, with a note about the ordering so it does not drift back down. Swept the rest of the app for the same shape: one other flow is declared after its init block (_selectedLanguage), but init never reaches it. Verified on the emulator: Settings opens, process survives, zero uncaught exceptions in logcat. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent ebacf39 commit 133d60a

1 file changed

Lines changed: 12 additions & 2 deletions

File tree

app/src/main/java/app/pwhs/universalinstaller/presentation/setting/SettingViewModel.kt

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,18 @@ class SettingViewModel(
333333
if (backendFactory.rootSupportCompiledIn) RootState.UNKNOWN else RootState.UNAVAILABLE,
334334
)
335335

336+
/**
337+
* Kept up here, with the rest of the state [init] touches, because it has to exist before the
338+
* constructor reaches that block.
339+
*
340+
* It used to be declared 480 lines below, and `updateDefaultInstallerStatus()` — launched from
341+
* init — assigned it from a coroutine. Property initializers run in declaration order, so the
342+
* flow was still null whenever that coroutine won the race, and the process died with an NPE
343+
* on a background thread. Physical devices hid it: resolveActivity's IPC took long enough for
344+
* the constructor to finish first. An emulator returns fast enough to lose the race.
345+
*/
346+
private val _isDefaultInstaller = MutableStateFlow(false)
347+
336348
private val binderReceivedListener = Shizuku.OnBinderReceivedListener {
337349
Timber.d("Shizuku binder received")
338350
updateShizukuState()
@@ -847,8 +859,6 @@ class SettingViewModel(
847859
}
848860
}
849861

850-
private val _isDefaultInstaller = MutableStateFlow(false)
851-
852862
/**
853863
* Toggles Universal Installer's DialogInstallActivity as the preferred handler for APK
854864
* install intents — the same mechanism Android uses when the user taps "Always" in the

0 commit comments

Comments
 (0)