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.core.annotation.ExperimentalVoyagerApi
|
||||||
import cafe.adriel.voyager.navigator.*
|
import cafe.adriel.voyager.navigator.*
|
||||||
import cafe.adriel.voyager.transitions.SlideTransition
|
import cafe.adriel.voyager.transitions.SlideTransition
|
||||||
|
import com.aliucord.manager.manager.OverlayManager
|
||||||
import com.aliucord.manager.manager.PreferencesManager
|
import com.aliucord.manager.manager.PreferencesManager
|
||||||
import com.aliucord.manager.ui.components.ManagerTheme
|
import com.aliucord.manager.ui.components.ManagerTheme
|
||||||
import com.aliucord.manager.ui.components.Theme
|
import com.aliucord.manager.ui.components.Theme
|
||||||
|
|
@ -30,6 +31,7 @@ import org.koin.android.ext.android.inject
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
private val preferences: PreferencesManager by inject()
|
private val preferences: PreferencesManager by inject()
|
||||||
|
private val overlays: OverlayManager by inject()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
|
@ -70,6 +72,8 @@ class MainActivity : ComponentActivity() {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
overlays.Overlays()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,8 @@
|
||||||
package com.aliucord.manager.manager
|
package com.aliucord.manager.manager
|
||||||
|
|
||||||
import android.app.Activity
|
import androidx.compose.runtime.*
|
||||||
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 kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
typealias ResultComposable<R> = @Composable (onResult: (R) -> Unit) -> Unit
|
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.
|
* 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 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.
|
* and then displaying them in the UI, which is too much boilerplate.
|
||||||
*/
|
*/
|
||||||
class OverlayManager(
|
@Stable
|
||||||
private val prefs: PreferencesManager,
|
class OverlayManager {
|
||||||
private val activityProvider: ActivityProvider,
|
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.
|
* Display all the currently queued overlays.
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
suspend fun <R> startComposableForResult(newDialog: ResultComposable<R>): R {
|
@Composable
|
||||||
return suspendCancellableCoroutine { continuation ->
|
fun Overlays() {
|
||||||
val activity = activityProvider.get<Activity>()
|
for (composable in overlays) {
|
||||||
var view: View? = null
|
key(System.identityHashCode(composable)) {
|
||||||
|
val composable by rememberUpdatedState(composable)
|
||||||
|
|
||||||
val callback = callback@{ value: R ->
|
composable { result ->
|
||||||
val v = view ?: return@callback
|
if (!overlayResults.tryEmit(composable to result))
|
||||||
|
error("overlayResults flow full!")
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
overlays -= composable
|
||||||
(v.parent as ViewGroup).removeView(v)
|
|
||||||
view = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
continuation.invokeOnCancellation {
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
coroutineScope.launch {
|
||||||
val v = view ?: return@launch
|
overlays -= composable
|
||||||
|
job.cancel()
|
||||||
(v.parent as ViewGroup).removeView(v)
|
|
||||||
view = null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
view = ComposeView(activity).apply {
|
coroutineScope.launch {
|
||||||
setContent {
|
overlays += composable
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,11 +44,11 @@ class DownloadInjectorStep : DownloadStep(), IDexProvider, KoinComponent {
|
||||||
// Prompt to select or manage custom versions instead of downloading
|
// Prompt to select or manage custom versions instead of downloading
|
||||||
if (customVersions.isNotEmpty()) {
|
if (customVersions.isNotEmpty()) {
|
||||||
container.log("Custom versions present, waiting for selection: ${customVersions.joinToString()}")
|
container.log("Custom versions present, waiting for selection: ${customVersions.joinToString()}")
|
||||||
val selectedVersion = overlays.startComposableForResult { callback ->
|
val selectedVersion = overlays.startComposableForResult { onResult ->
|
||||||
CustomComponentVersionPicker(
|
CustomComponentVersionPicker(
|
||||||
componentTitle = "Injector",
|
componentTitle = "Injector",
|
||||||
versions = customVersions,
|
versions = customVersions,
|
||||||
onConfirm = { version -> callback(version) },
|
onConfirm = { version -> onResult(version) },
|
||||||
onDelete = { version ->
|
onDelete = { version ->
|
||||||
try {
|
try {
|
||||||
paths.cachedInjectorDex(version, custom = true).delete()
|
paths.cachedInjectorDex(version, custom = true).delete()
|
||||||
|
|
@ -56,12 +56,12 @@ class DownloadInjectorStep : DownloadStep(), IDexProvider, KoinComponent {
|
||||||
|
|
||||||
// Dismiss if no custom versions left
|
// Dismiss if no custom versions left
|
||||||
if (customVersions.isEmpty())
|
if (customVersions.isEmpty())
|
||||||
callback(null)
|
onResult(null)
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
Log.e(BuildConfig.TAG, "Failed to delete custom component", t)
|
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
|
// Prompt to select or manage custom versions instead of downloading
|
||||||
if (customVersions.isNotEmpty()) {
|
if (customVersions.isNotEmpty()) {
|
||||||
container.log("Custom versions present, waiting for selection: ${customVersions.joinToString()}")
|
container.log("Custom versions present, waiting for selection: ${customVersions.joinToString()}")
|
||||||
val selectedVersion = overlays.startComposableForResult { callback ->
|
val selectedVersion = overlays.startComposableForResult { onResult ->
|
||||||
CustomComponentVersionPicker(
|
CustomComponentVersionPicker(
|
||||||
componentTitle = "Smali Patches",
|
componentTitle = "Smali Patches",
|
||||||
versions = customVersions,
|
versions = customVersions,
|
||||||
onConfirm = { version -> callback(version) },
|
onConfirm = { version -> onResult(version) },
|
||||||
onDelete = { version ->
|
onDelete = { version ->
|
||||||
try {
|
try {
|
||||||
paths.cachedSmaliPatches(version, custom = true).delete()
|
paths.cachedSmaliPatches(version, custom = true).delete()
|
||||||
|
|
@ -54,12 +54,12 @@ class DownloadPatchesStep : DownloadStep(), KoinComponent {
|
||||||
|
|
||||||
// Dismiss if no custom versions left
|
// Dismiss if no custom versions left
|
||||||
if (customVersions.isEmpty())
|
if (customVersions.isEmpty())
|
||||||
callback(null)
|
onResult(null)
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
Log.e(BuildConfig.TAG, "Failed to delete custom component", t)
|
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
|
&& context.isPlayProtectEnabled() == true
|
||||||
) {
|
) {
|
||||||
container.log("Showing play protect warning dialog")
|
container.log("Showing play protect warning dialog")
|
||||||
val neverShowAgain = overlays.startComposableForResult { callback ->
|
val neverShowAgain = overlays.startComposableForResult { onResult ->
|
||||||
PlayProtectDialog(onDismiss = callback)
|
PlayProtectDialog(onDismiss = onResult)
|
||||||
}
|
}
|
||||||
prefs.showPlayProtectWarning = !neverShowAgain
|
prefs.showPlayProtectWarning = !neverShowAgain
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ fun PlayProtectDialog(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
properties = DialogProperties(
|
properties = DialogProperties(
|
||||||
dismissOnBackPress = false,
|
dismissOnClickOutside = false,
|
||||||
usePlatformDefaultWidth = false,
|
usePlatformDefaultWidth = false,
|
||||||
),
|
),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue