Skip to content

Commit fac84ff

Browse files
committed
refactor: Migrate boot-time Shizuku waiting and configuration application from BootReceiver to a new BootWorker using WorkManager.
1 parent dcc033c commit fac84ff

3 files changed

Lines changed: 133 additions & 149 deletions

File tree

app/src/main/java/io/github/vvb2060/ims/BootReceiver.kt

Lines changed: 9 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -3,171 +3,31 @@ package io.github.vvb2060.ims
33
import android.content.BroadcastReceiver
44
import android.content.Context
55
import android.content.Intent
6-
import android.content.pm.PackageManager
7-
import android.os.Handler
8-
import android.os.Looper
96
import android.util.Log
10-
import android.widget.Toast
11-
import kotlinx.coroutines.CoroutineScope
12-
import kotlinx.coroutines.Dispatchers
13-
import kotlinx.coroutines.Job
14-
import kotlinx.coroutines.delay
15-
import kotlinx.coroutines.launch
16-
import kotlinx.coroutines.withContext
17-
import rikka.shizuku.Shizuku
7+
import androidx.work.OneTimeWorkRequest
8+
import androidx.work.WorkManager
189

1910
/**
20-
* BootReceiver listens for device boot and Shizuku binder availability.
21-
* Once Shizuku is ready, it automatically applies saved configurations to all SIM cards.
22-
*
23-
* Note: This receiver is triggered infrequently (only on boot), and the coroutine scope
24-
* is designed to outlive the receiver's onReceive() context. The static job reference
25-
* ensures we can cancel any previous attempts if multiple boot events occur.
11+
* BootReceiver listens for device boot and schedules a BootWorker to wait for Shizuku and apply
12+
* saved configurations.
2613
*/
2714
class BootReceiver : BroadcastReceiver() {
2815
companion object {
2916
private const val TAG = "BootReceiver"
30-
private const val MAX_SHIZUKU_WAIT_TIME_MS = 60000L // 60 seconds
31-
private const val SHIZUKU_CHECK_INTERVAL_MS = 1000L // 1 second
32-
33-
// Static job to track ongoing configuration application
34-
// Synchronized to prevent race conditions from multiple boot events
35-
@Volatile
36-
private var applyJob: Job? = null
3717
}
3818

3919
override fun onReceive(context: Context, intent: Intent) {
4020
when (intent.action) {
4121
Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_LOCKED_BOOT_COMPLETED -> {
42-
Log.i(TAG, "Boot completed, waiting for Shizuku to start")
43-
waitForShizukuAndApplyConfigs(context)
22+
Log.i(TAG, "Boot completed, enqueuing BootWorker")
23+
enqueueBootWork(context)
4424
}
4525
}
4626
}
4727

48-
/**
49-
* Waits for Shizuku to become available and then applies saved configurations.
50-
* Uses a coroutine that outlives the receiver's onReceive() lifecycle.
51-
*/
52-
private fun waitForShizukuAndApplyConfigs(context: Context) {
53-
// Use application context to ensure validity beyond receiver lifecycle
54-
val appContext = context.applicationContext
55-
56-
// Synchronized cancellation and creation to prevent race conditions
57-
synchronized(BootReceiver::class.java) {
58-
applyJob?.cancel()
59-
60-
applyJob = CoroutineScope(Dispatchers.IO).launch {
61-
var elapsedTime = 0L
62-
63-
// Wait for Shizuku to be ready
64-
while (elapsedTime < MAX_SHIZUKU_WAIT_TIME_MS) {
65-
if (Shizuku.pingBinder() &&
66-
Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) {
67-
Log.i(TAG, "Shizuku is ready after ${elapsedTime}ms")
68-
applyAllSavedConfigurations(appContext)
69-
return@launch
70-
}
71-
72-
delay(SHIZUKU_CHECK_INTERVAL_MS)
73-
elapsedTime += SHIZUKU_CHECK_INTERVAL_MS
74-
}
75-
76-
Log.w(TAG, "Timeout waiting for Shizuku to start after ${MAX_SHIZUKU_WAIT_TIME_MS}ms")
77-
}
78-
}
79-
}
80-
81-
/**
82-
* Applies saved configurations to all SIM cards that have saved preferences.
83-
*/
84-
private suspend fun applyAllSavedConfigurations(context: Context) {
85-
var successCount = 0
86-
var failureCount = 0
87-
88-
try {
89-
// Read all available SIM cards
90-
val simList = ShizukuProvider.readSimInfoList(context)
91-
Log.i(TAG, "Found ${simList.size} SIM cards")
92-
93-
if (simList.isEmpty()) {
94-
Log.w(TAG, "No SIM cards found, skipping configuration application")
95-
return
96-
}
97-
98-
// Apply saved configuration for each SIM card that has one
99-
for (sim in simList) {
100-
val savedConfig = SimConfigManager.loadConfiguration(context, sim.subId)
101-
if (savedConfig != null) {
102-
Log.i(TAG, "Applying saved configuration for SIM ${sim.subId} (${sim.displayName})")
103-
val resultMsg = SimConfigManager.applyConfiguration(context, sim.subId, savedConfig)
104-
if (resultMsg == null) {
105-
Log.i(TAG, "Successfully applied configuration for SIM ${sim.subId}")
106-
successCount++
107-
} else {
108-
Log.e(TAG, "Failed to apply configuration for SIM ${sim.subId}: $resultMsg")
109-
failureCount++
110-
}
111-
} else {
112-
Log.d(TAG, "No saved configuration found for SIM ${sim.subId} (${sim.displayName})")
113-
}
114-
}
115-
116-
Log.i(TAG, "Finished applying saved configurations on boot")
117-
118-
// Show toast notification on main thread
119-
if (successCount > 0 || failureCount > 0) {
120-
withContext(Dispatchers.Main) {
121-
showToast(context, successCount, failureCount)
122-
}
123-
}
124-
} catch (e: Exception) {
125-
Log.e(TAG, "Error applying saved configurations on boot", e)
126-
withContext(Dispatchers.Main) {
127-
showErrorToast(context, e.message ?: "Unknown error")
128-
}
129-
}
130-
}
131-
132-
/**
133-
* Shows a toast message on the main thread with configuration results.
134-
*/
135-
private fun showToast(context: Context, successCount: Int, failureCount: Int) {
136-
val mainHandler = Handler(Looper.getMainLooper())
137-
var runnable: Runnable
138-
139-
runnable = object : Runnable {
140-
override fun run() {
141-
val message = when {
142-
failureCount == 0 && successCount > 0 ->
143-
context.getString(R.string.config_success_message)
144-
failureCount > 0 && successCount == 0 ->
145-
context.getString(R.string.config_all_failed)
146-
else ->
147-
context.getString(R.string.config_mixed_result, successCount, failureCount)
148-
}
149-
150-
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
151-
}
152-
}
153-
154-
mainHandler.post(runnable)
155-
}
156-
157-
/**
158-
* Shows an error toast on the main thread.
159-
*/
160-
private fun showErrorToast(context: Context, error: String) {
161-
val mainHandler = Handler(Looper.getMainLooper())
162-
var runnable: Runnable
163-
164-
runnable = object : Runnable {
165-
override fun run() {
166-
val message = context.getString(R.string.config_failed, error)
167-
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
168-
}
169-
}
28+
private fun enqueueBootWork(context: Context) {
29+
val workRequest = OneTimeWorkRequest.Builder(BootWorker::class.java).build()
17030

171-
mainHandler.post(runnable)
31+
WorkManager.getInstance(context).enqueue(workRequest)
17232
}
17333
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package io.github.vvb2060.ims
2+
3+
import android.content.Context
4+
import android.content.pm.PackageManager
5+
import android.util.Log
6+
import android.widget.Toast
7+
import androidx.work.CoroutineWorker
8+
import androidx.work.WorkerParameters
9+
import kotlinx.coroutines.Dispatchers
10+
import kotlinx.coroutines.delay
11+
import kotlinx.coroutines.withContext
12+
import rikka.shizuku.Shizuku
13+
14+
class BootWorker(appContext: Context, workerParams: WorkerParameters) :
15+
CoroutineWorker(appContext, workerParams) {
16+
17+
companion object {
18+
private const val TAG = "BootWorker"
19+
private const val MAX_SHIZUKU_WAIT_TIME_MS = 60000L // 60 seconds
20+
private const val SHIZUKU_CHECK_INTERVAL_MS = 1000L // 1 second
21+
}
22+
23+
override suspend fun doWork(): Result {
24+
Log.i(TAG, "BootWorker started, waiting for Shizuku")
25+
26+
var elapsedTime = 0L
27+
28+
// Wait for Shizuku to be ready
29+
while (elapsedTime < MAX_SHIZUKU_WAIT_TIME_MS) {
30+
if (Shizuku.pingBinder() &&
31+
Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED
32+
) {
33+
Log.i(TAG, "Shizuku is ready after ${elapsedTime}ms")
34+
applyAllSavedConfigurations(applicationContext)
35+
return Result.success()
36+
}
37+
38+
delay(SHIZUKU_CHECK_INTERVAL_MS)
39+
elapsedTime += SHIZUKU_CHECK_INTERVAL_MS
40+
}
41+
42+
Log.w(TAG, "Timeout waiting for Shizuku to start after ${MAX_SHIZUKU_WAIT_TIME_MS}ms")
43+
return Result.failure()
44+
}
45+
46+
/** Applies saved configurations to all SIM cards that have saved preferences. */
47+
private suspend fun applyAllSavedConfigurations(context: Context) {
48+
var successCount = 0
49+
var failureCount = 0
50+
51+
try {
52+
// Read all available SIM cards
53+
val simList = ShizukuProvider.readSimInfoList(context)
54+
Log.i(TAG, "Found ${simList.size} SIM cards")
55+
56+
if (simList.isEmpty()) {
57+
Log.w(TAG, "No SIM cards found, skipping configuration application")
58+
return
59+
}
60+
61+
// Apply saved configuration for each SIM card that has one
62+
for (sim in simList) {
63+
val savedConfig = SimConfigManager.loadConfiguration(context, sim.subId)
64+
if (savedConfig != null) {
65+
Log.i(
66+
TAG,
67+
"Applying saved configuration for SIM ${sim.subId} (${sim.displayName})"
68+
)
69+
val resultMsg =
70+
SimConfigManager.applyConfiguration(context, sim.subId, savedConfig)
71+
if (resultMsg == null) {
72+
Log.i(TAG, "Successfully applied configuration for SIM ${sim.subId}")
73+
successCount++
74+
} else {
75+
Log.e(TAG, "Failed to apply configuration for SIM ${sim.subId}: $resultMsg")
76+
failureCount++
77+
}
78+
} else {
79+
Log.d(
80+
TAG,
81+
"No saved configuration found for SIM ${sim.subId} (${sim.displayName})"
82+
)
83+
}
84+
}
85+
86+
Log.i(TAG, "Finished applying saved configurations on boot")
87+
88+
// Show toast notification on main thread
89+
if (successCount > 0 || failureCount > 0) {
90+
withContext(Dispatchers.Main) { showToast(context, successCount, failureCount) }
91+
}
92+
} catch (e: Exception) {
93+
Log.e(TAG, "Error applying saved configurations on boot", e)
94+
withContext(Dispatchers.Main) { showErrorToast(context, e.message ?: "Unknown error") }
95+
}
96+
}
97+
98+
/** Shows a toast message on the main thread with configuration results. */
99+
private fun showToast(context: Context, successCount: Int, failureCount: Int) {
100+
val message =
101+
when {
102+
failureCount == 0 && successCount > 0 ->
103+
context.getString(R.string.config_success_message)
104+
failureCount > 0 && successCount == 0 ->
105+
context.getString(R.string.config_all_failed)
106+
else ->
107+
context.getString(
108+
R.string.config_mixed_result,
109+
successCount,
110+
failureCount
111+
)
112+
}
113+
114+
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
115+
}
116+
117+
/** Shows an error toast on the main thread. */
118+
private fun showErrorToast(context: Context, error: String) {
119+
val message = context.getString(R.string.config_failed, error)
120+
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
121+
}
122+
}

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ material3 = "1.5.0-alpha11"
1818
androidx-splashscreen = "1.2.0"
1919
lifecycle = "2.10.0"
2020
material-icons = "1.7.8"
21+
work = "2.10.0"
2122

2223
[libraries]
2324
shizuku-api = { module = "dev.rikka.shizuku:api", version.ref = "shizuku" }
@@ -35,6 +36,7 @@ androidx-splashscreen = { module = "androidx.core:core-splashscreen", version.re
3536
lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
3637
material-icons-core = { group = "androidx.compose.material", name = "material-icons-core", version.ref = "material-icons" }
3738
material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "material-icons" }
39+
androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work" }
3840

3941
[plugins]
4042
android-application = { id = "com.android.application", version.ref = "agp" }

0 commit comments

Comments
 (0)