refactor(OverlayManager): better composable management
Previously this created a separate `ComposeView` and attached it to the activity root, which in essence made it detached from the main composition context and introduced a lot of complexity. This changes it store the composable lambdas instead and manually display each one directly from the activity's `setContent`, which is a lot less cursed.
This commit is contained in:
parent
7ec6c16b9b
commit
f5da0eea2f
6 changed files with 60 additions and 65 deletions
|
|
@ -18,6 +18,7 @@ import androidx.core.view.WindowCompat
|
|||
import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi
|
||||
import cafe.adriel.voyager.navigator.*
|
||||
import cafe.adriel.voyager.transitions.SlideTransition
|
||||
import com.aliucord.manager.manager.OverlayManager
|
||||
import com.aliucord.manager.manager.PreferencesManager
|
||||
import com.aliucord.manager.ui.components.ManagerTheme
|
||||
import com.aliucord.manager.ui.components.Theme
|
||||
|
|
@ -30,6 +31,7 @@ import org.koin.android.ext.android.inject
|
|||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private val preferences: PreferencesManager by inject()
|
||||
private val overlays: OverlayManager by inject()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
|
@ -70,6 +72,8 @@ class MainActivity : ComponentActivity() {
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
overlays.Overlays()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,8 @@
|
|||
package com.aliucord.manager.manager
|
||||
|
||||
import android.app.Activity
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import com.aliucord.manager.di.ActivityProvider
|
||||
import com.aliucord.manager.ui.components.ManagerTheme
|
||||
import com.aliucord.manager.ui.components.Theme
|
||||
import androidx.compose.runtime.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
typealias ResultComposable<R> = @Composable (onResult: (R) -> Unit) -> Unit
|
||||
|
|
@ -17,64 +10,62 @@ typealias ResultComposable<R> = @Composable (onResult: (R) -> Unit) -> Unit
|
|||
/**
|
||||
* This is used to display dialogs on top of the current activity at any point in the code.
|
||||
* The main use case for this is dialogs for which a result is needed during patching steps.
|
||||
* The only other alternative is binding `Flow`s from the steps back to the patching screen model
|
||||
*
|
||||
* The only other alternative to this setup is binding `Flow`s from the steps back to the patching screen model
|
||||
* and then displaying them in the UI, which is too much boilerplate.
|
||||
*/
|
||||
class OverlayManager(
|
||||
private val prefs: PreferencesManager,
|
||||
private val activityProvider: ActivityProvider,
|
||||
) {
|
||||
@Stable
|
||||
class OverlayManager {
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.Main) + SupervisorJob()
|
||||
private var overlays = mutableStateListOf<ResultComposable<Any?>>()
|
||||
private var overlayResults = MutableSharedFlow<Pair<ResultComposable<Any?>, Any?>>(extraBufferCapacity = 5)
|
||||
|
||||
/**
|
||||
* Attaches a new [ComposeView] content view to the root of the activity on top of everything else.
|
||||
* This view will be displayed until the `onResult` callback is called,
|
||||
* after which this method will finish suspending with the result from the callback invoke.
|
||||
* Display all the currently queued overlays.
|
||||
*/
|
||||
suspend fun <R> startComposableForResult(newDialog: ResultComposable<R>): R {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
val activity = activityProvider.get<Activity>()
|
||||
var view: View? = null
|
||||
@Composable
|
||||
fun Overlays() {
|
||||
for (composable in overlays) {
|
||||
key(System.identityHashCode(composable)) {
|
||||
val composable by rememberUpdatedState(composable)
|
||||
|
||||
val callback = callback@{ value: R ->
|
||||
val v = view ?: return@callback
|
||||
composable { result ->
|
||||
if (!overlayResults.tryEmit(composable to result))
|
||||
error("overlayResults flow full!")
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
(v.parent as ViewGroup).removeView(v)
|
||||
view = null
|
||||
overlays -= composable
|
||||
}
|
||||
|
||||
continuation.resume(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a composable to the overlay stack which will be displayed over the top of any content.
|
||||
*
|
||||
* This content will be displayed until the `onResult` callback is called,
|
||||
* after which this method will finish suspending with the result from the invoked callback.
|
||||
*
|
||||
* If the coroutine scope this method was called in gets cancelled, then the overlay will be
|
||||
* removed and no result will be returned (cancelled).
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
suspend fun <R> startComposableForResult(composable: ResultComposable<R>): R {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
val job = overlayResults
|
||||
.filter { (c, _) -> c === composable }
|
||||
.onEach { (_, result) -> continuation.resume(result as R) }
|
||||
.cancellable()
|
||||
.launchIn(coroutineScope)
|
||||
|
||||
continuation.invokeOnCancellation {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
val v = view ?: return@launch
|
||||
|
||||
(v.parent as ViewGroup).removeView(v)
|
||||
view = null
|
||||
coroutineScope.launch {
|
||||
overlays -= composable
|
||||
job.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
view = ComposeView(activity).apply {
|
||||
setContent {
|
||||
ManagerTheme(
|
||||
isDarkTheme = prefs.theme == Theme.DARK || prefs.theme == Theme.SYSTEM && isSystemInDarkTheme(),
|
||||
isDynamicColor = prefs.dynamicColor
|
||||
) {
|
||||
newDialog(callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
activity.addContentView(
|
||||
view!!,
|
||||
ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
)
|
||||
)
|
||||
|
||||
view?.bringToFront()
|
||||
coroutineScope.launch {
|
||||
overlays += composable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,11 +44,11 @@ class DownloadInjectorStep : DownloadStep(), IDexProvider, KoinComponent {
|
|||
// Prompt to select or manage custom versions instead of downloading
|
||||
if (customVersions.isNotEmpty()) {
|
||||
container.log("Custom versions present, waiting for selection: ${customVersions.joinToString()}")
|
||||
val selectedVersion = overlays.startComposableForResult { callback ->
|
||||
val selectedVersion = overlays.startComposableForResult { onResult ->
|
||||
CustomComponentVersionPicker(
|
||||
componentTitle = "Injector",
|
||||
versions = customVersions,
|
||||
onConfirm = { version -> callback(version) },
|
||||
onConfirm = { version -> onResult(version) },
|
||||
onDelete = { version ->
|
||||
try {
|
||||
paths.cachedInjectorDex(version, custom = true).delete()
|
||||
|
|
@ -56,12 +56,12 @@ class DownloadInjectorStep : DownloadStep(), IDexProvider, KoinComponent {
|
|||
|
||||
// Dismiss if no custom versions left
|
||||
if (customVersions.isEmpty())
|
||||
callback(null)
|
||||
onResult(null)
|
||||
} catch (t: Throwable) {
|
||||
Log.e(BuildConfig.TAG, "Failed to delete custom component", t)
|
||||
}
|
||||
},
|
||||
onCancel = { callback(null) },
|
||||
onCancel = { onResult(null) },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,11 +42,11 @@ class DownloadPatchesStep : DownloadStep(), KoinComponent {
|
|||
// Prompt to select or manage custom versions instead of downloading
|
||||
if (customVersions.isNotEmpty()) {
|
||||
container.log("Custom versions present, waiting for selection: ${customVersions.joinToString()}")
|
||||
val selectedVersion = overlays.startComposableForResult { callback ->
|
||||
val selectedVersion = overlays.startComposableForResult { onResult ->
|
||||
CustomComponentVersionPicker(
|
||||
componentTitle = "Smali Patches",
|
||||
versions = customVersions,
|
||||
onConfirm = { version -> callback(version) },
|
||||
onConfirm = { version -> onResult(version) },
|
||||
onDelete = { version ->
|
||||
try {
|
||||
paths.cachedSmaliPatches(version, custom = true).delete()
|
||||
|
|
@ -54,12 +54,12 @@ class DownloadPatchesStep : DownloadStep(), KoinComponent {
|
|||
|
||||
// Dismiss if no custom versions left
|
||||
if (customVersions.isEmpty())
|
||||
callback(null)
|
||||
onResult(null)
|
||||
} catch (t: Throwable) {
|
||||
Log.e(BuildConfig.TAG, "Failed to delete custom component", t)
|
||||
}
|
||||
},
|
||||
onCancel = { callback(null) },
|
||||
onCancel = { onResult(null) },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -60,8 +60,8 @@ class InstallStep(private val options: PatchOptions) : Step(), KoinComponent {
|
|||
&& context.isPlayProtectEnabled() == true
|
||||
) {
|
||||
container.log("Showing play protect warning dialog")
|
||||
val neverShowAgain = overlays.startComposableForResult { callback ->
|
||||
PlayProtectDialog(onDismiss = callback)
|
||||
val neverShowAgain = overlays.startComposableForResult { onResult ->
|
||||
PlayProtectDialog(onDismiss = onResult)
|
||||
}
|
||||
prefs.showPlayProtectWarning = !neverShowAgain
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ fun PlayProtectDialog(
|
|||
}
|
||||
},
|
||||
properties = DialogProperties(
|
||||
dismissOnBackPress = false,
|
||||
dismissOnClickOutside = false,
|
||||
usePlatformDefaultWidth = false,
|
||||
),
|
||||
modifier = Modifier
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue