Skip to content

Commit ef13c51

Browse files
authored
Merge pull request #10 from zerofrip/prD/batch-update-all
feat: Sequential awaited update-all for silent installers
2 parents ae1f25a + c7026c8 commit ef13c51

11 files changed

Lines changed: 600 additions & 79 deletions

File tree

app/src/main/kotlin/com/looker/droidify/compose/settings/SettingsViewModel.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ import com.looker.droidify.installer.installers.isMagiskGranted
2828
import com.looker.droidify.installer.installers.isShizukuAlive
2929
import com.looker.droidify.installer.installers.isShizukuGranted
3030
import com.looker.droidify.installer.installers.isShizukuInstalled
31+
import com.looker.droidify.installer.installers.dhizuku.isDhizukuAlive
32+
import com.looker.droidify.installer.installers.dhizuku.isDhizukuGranted
33+
import com.looker.droidify.installer.installers.dhizuku.isDhizukuInstalled
34+
import com.looker.droidify.installer.installers.dhizuku.requestDhizukuPermission
3135
import com.looker.droidify.installer.installers.requestPermissionListener
3236
import com.looker.droidify.utility.common.extension.asStateFlow
3337
import com.looker.droidify.utility.common.extension.updateAsMutable
@@ -150,6 +154,7 @@ class SettingsViewModel @Inject constructor(
150154
viewModelScope.launch {
151155
when (installerType) {
152156
InstallerType.SHIZUKU -> handleShizukuInstaller(context, installerType)
157+
InstallerType.DHIZUKU -> handleDhizukuInstaller(context, installerType)
153158
InstallerType.ROOT -> handleRootInstaller(installerType)
154159
InstallerType.LEGACY -> {
155160
settingsRepository.setDeleteApkOnInstall(false)
@@ -176,6 +181,23 @@ class SettingsViewModel @Inject constructor(
176181
}
177182
}
178183

184+
185+
private suspend fun handleDhizukuInstaller(context: Context, installerType: InstallerType) {
186+
if (isDhizukuInstalled(context)) {
187+
when {
188+
!isDhizukuAlive(context) -> showSnackbar(R.string.dhizuku_not_alive)
189+
isDhizukuGranted() -> settingsRepository.setInstallerType(installerType)
190+
else -> {
191+
if (requestDhizukuPermission(context)) {
192+
settingsRepository.setInstallerType(installerType)
193+
}
194+
}
195+
}
196+
} else {
197+
showSnackbar(R.string.dhizuku_not_installed)
198+
}
199+
}
200+
179201
private suspend fun handleRootInstaller(installerType: InstallerType) {
180202
if (isMagiskGranted()) {
181203
settingsRepository.setInstallerType(installerType)

app/src/main/kotlin/com/looker/droidify/service/DownloadService.kt

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,10 @@ import com.looker.droidify.utility.common.log
4343
import com.looker.droidify.utility.notifications.createInstallNotification
4444
import com.looker.droidify.utility.notifications.installNotification
4545
import dagger.hilt.android.AndroidEntryPoint
46+
import kotlinx.coroutines.CompletableDeferred
4647
import kotlinx.coroutines.CoroutineScope
4748
import kotlinx.coroutines.Job
49+
import kotlinx.coroutines.NonCancellable
4850
import kotlinx.coroutines.flow.MutableStateFlow
4951
import kotlinx.coroutines.flow.asStateFlow
5052
import kotlinx.coroutines.flow.collectLatest
@@ -55,14 +57,17 @@ import kotlinx.coroutines.flow.update
5557
import kotlinx.coroutines.launch
5658
import kotlinx.coroutines.sync.Mutex
5759
import kotlinx.coroutines.sync.withLock
60+
import kotlinx.coroutines.withContext
5861
import kotlinx.coroutines.yield
5962
import java.io.File
63+
import java.util.concurrent.ConcurrentHashMap
6064
import javax.inject.Inject
6165
import com.looker.droidify.R.string as stringRes
6266

6367
@AndroidEntryPoint
6468
class DownloadService : ConnectionService<DownloadService.Binder>() {
6569
companion object {
70+
private const val TAG = "DroidifyUpdateAll"
6671
private const val ACTION_CANCEL = "${BuildConfig.APPLICATION_ID}.intent.action.CANCEL"
6772
}
6873

@@ -130,9 +135,46 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
130135
private var currentTask: CurrentTask? = null
131136

132137
private val lock = Mutex()
138+
private val updateCompletions = ConcurrentHashMap<String, CompletableDeferred<Boolean>>()
133139

134140
inner class Binder : android.os.Binder() {
135141
val downloadState = _downloadState.asStateFlow()
142+
143+
suspend fun enqueueAndAwait(
144+
packageName: String,
145+
name: String,
146+
repository: Repository,
147+
release: Release,
148+
isUpdate: Boolean = false,
149+
): Boolean {
150+
val task = Task(
151+
packageName = packageName,
152+
name = name,
153+
release = release,
154+
url = release.getDownloadUrl(repository),
155+
authentication = repository.authentication,
156+
isUpdate = isUpdate,
157+
)
158+
val deferred = CompletableDeferred<Boolean>()
159+
updateCompletions[packageName] = deferred
160+
log("enqueueAndAwait: $packageName", TAG)
161+
return try {
162+
if (Cache.getReleaseFile(this@DownloadService, release.cacheFileName).exists()) {
163+
log("Using cached APK for $packageName (await)", TAG)
164+
publishSuccess(task)
165+
} else {
166+
enqueueDownload(task)
167+
}
168+
withContext(NonCancellable) { deferred.await() }
169+
} catch (e: Exception) {
170+
log("enqueueAndAwait failed: $packageName${e.message}", TAG, Log.ERROR)
171+
signalUpdateComplete(packageName, false)
172+
false
173+
} finally {
174+
updateCompletions.remove(packageName)
175+
}
176+
}
177+
136178
fun enqueue(
137179
packageName: String,
138180
name: String,
@@ -149,11 +191,17 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
149191
isUpdate = isUpdate,
150192
)
151193
if (Cache.getReleaseFile(this@DownloadService, release.cacheFileName).exists()) {
194+
log("Using cached APK for $packageName", TAG)
152195
lifecycleScope.launch { publishSuccess(task) }
153196
return
154197
}
155-
cancelTasks(packageName)
156-
cancelCurrentTask(packageName)
198+
enqueueDownload(task)
199+
}
200+
201+
private fun enqueueDownload(task: Task) {
202+
log("Queued download for ${task.packageName} (pending=${tasks.size})", TAG)
203+
cancelTasks(task.packageName)
204+
cancelCurrentTask(task.packageName)
157205
notificationManager?.cancel(
158206
task.notificationTag,
159207
Constants.NOTIFICATION_ID_DOWNLOADING,
@@ -162,7 +210,7 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
162210
if (currentTask == null) {
163211
handleDownload()
164212
} else {
165-
updateCurrentQueue { add(packageName) }
213+
updateCurrentQueue { add(task.packageName) }
166214
}
167215
}
168216

@@ -298,18 +346,42 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
298346
val currentInstaller = installerType.first()
299347
updateCurrentQueue { add("") }
300348
updateCurrentState(State.Success(task.packageName, task.release))
349+
log(
350+
"Download complete: ${task.packageName}, installer=$currentInstaller, queue=${tasks.size}",
351+
TAG,
352+
)
301353
val autoInstallWithSessionInstaller =
302354
SdkCheck.canAutoInstall(task.release.targetSdkVersion) &&
303355
currentInstaller == InstallerType.SESSION &&
304356
task.isUpdate
305357

306358
showNotificationInstall(task)
307-
if (currentInstaller == InstallerType.ROOT ||
359+
val success = if (currentInstaller == InstallerType.ROOT ||
308360
currentInstaller == InstallerType.SHIZUKU ||
361+
currentInstaller == InstallerType.DHIZUKU ||
309362
autoInstallWithSessionInstaller
310363
) {
311364
val installItem = task.packageName installFrom task.release.cacheFileName
312-
installer install installItem
365+
val result = withContext(NonCancellable) {
366+
installer.installAndAwait(installItem)
367+
}
368+
log(
369+
"Auto-install finished: ${task.packageName} -> $result",
370+
TAG,
371+
if (result == InstallState.Installed) Log.DEBUG else Log.WARN,
372+
)
373+
result == InstallState.Installed
374+
} else {
375+
true
376+
}
377+
signalUpdateComplete(task.packageName, success)
378+
}
379+
380+
private fun signalUpdateComplete(packageName: String, success: Boolean) {
381+
val pending = updateCompletions.remove(packageName)
382+
if (pending != null) {
383+
log("Update step complete: $packageName success=$success", TAG)
384+
pending.complete(success)
313385
}
314386
}
315387

@@ -436,6 +508,7 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
436508
task: Task,
437509
target: File,
438510
) = launch {
511+
var publishCalled = false
439512
try {
440513
val response = downloader.downloadToFile(
441514
url = task.url,
@@ -458,13 +531,15 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
458531
target.delete()
459532
updateCurrentState(State.Error(task.packageName))
460533
showErrorNotification(task, could_not_validate_FORMAT, result.message)
534+
signalUpdateComplete(task.packageName, false)
461535
return@launch
462536
}
463537
val releaseFile = Cache.getReleaseFile(
464538
this@DownloadService,
465539
task.release.cacheFileName,
466540
)
467541
target.renameTo(releaseFile)
542+
publishCalled = true
468543
publishSuccess(task)
469544
}
470545

@@ -478,8 +553,14 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
478553
is NetworkResponse.Error.Unknown -> unknown_error_DESC
479554
}
480555
showErrorNotification(task, could_not_download_FORMAT, getString(description))
556+
signalUpdateComplete(task.packageName, false)
481557
}
482558
}
559+
} catch (e: Exception) {
560+
log("Download failed: ${task.packageName}${e.message}", TAG, Log.ERROR)
561+
if (!publishCalled) {
562+
signalUpdateComplete(task.packageName, false)
563+
}
483564
} finally {
484565
lock.withLock { currentTask = null }
485566
handleDownload()

0 commit comments

Comments
 (0)