From a816e18e146def26485f72bb7461dd4944cae5e9 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Sun, 11 Jan 2026 14:30:23 +0100 Subject: [PATCH] yet another attempt to improve feed interaction --- .../ui/launcher/scaffold/FeedComponent.kt | 111 +++++++++++------- .../de/mm20/launcher2/feed/FeedConnection.kt | 40 ++++++- .../de/mm20/launcher2/feed/FeedService.kt | 2 - 3 files changed, 106 insertions(+), 47 deletions(-) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/FeedComponent.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/FeedComponent.kt index 6f81c1420..90b3ea1bf 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/FeedComponent.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/FeedComponent.kt @@ -15,9 +15,9 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -28,12 +28,12 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.LocalLifecycleOwner import de.mm20.launcher2.feed.FeedConnection import de.mm20.launcher2.feed.FeedService import de.mm20.launcher2.preferences.feed.FeedSettings import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.ktx.toIntOffset +import kotlinx.coroutines.flow.flowOf import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -55,23 +55,33 @@ internal class FeedComponent( state: LauncherScaffoldState ) { val activity = LocalActivity.current - val lifecycleOwner = LocalLifecycleOwner.current - - val progress = state.currentProgress - val feedProgress = remember { mutableFloatStateOf(-1f) } val feedProviderPackage by remember { feedSettings.providerPackage }.collectAsState(null) var feedConnection by remember { mutableStateOf(null) } - val feedReady = feedConnection?.ready?.collectAsState(false) - val feedAvailable = feedConnection?.available?.collectAsState(null) + val feedReady by remember(feedConnection) { + feedConnection?.ready ?: flowOf(false) + }.collectAsState(false) + + val feedAvailable by remember(feedConnection) { + feedConnection?.available ?: flowOf(false) + }.collectAsState(false) + + + val feedHasContent by remember(feedConnection) { + feedConnection?.hasContent ?: flowOf(false) + }.collectAsState(false) + + val feedProgress by remember(feedConnection) { + feedConnection?.scrollProgress ?: flowOf(0f) + }.collectAsState(0f) + + val progress = state.currentProgress DisposableEffect(feedProviderPackage) { val conn = feedProviderPackage?.let { - feedService.createFeedInstance(activity as AppCompatActivity, it) { p -> - feedProgress.floatValue = p - } + feedService.createFeedInstance(activity as AppCompatActivity, it) } feedConnection = conn @@ -80,38 +90,45 @@ internal class FeedComponent( } } - - if (state.currentComponent == this) { - LaunchedEffect(Unit) { - feedConnection?.startScroll() - } - LaunchedEffect(state.isSettledOnSecondaryPage, progress > 0.8f) { - if (state.isSettledOnSecondaryPage && progress > 0.8f) { - feedConnection?.endScroll() + if (feedHasContent && feedReady && feedAvailable) { + if (state.currentComponent == this) { + LaunchedEffect(Unit) { + feedConnection?.startScroll() } - } - LaunchedEffect(progress) { - if (feedProgress.floatValue != progress) { - feedConnection?.onScroll(progress) - } - } - LaunchedEffect( - isActive, - feedProgress.floatValue - ) { - if (isActive) { - state.setProgress(feedProgress.floatValue) - if (feedProgress.floatValue <= 0f) { - feedConnection?.closeFeed() - state.onPredictiveBackEnd() + DisposableEffect(Unit) { + onDispose { + feedConnection?.onScroll(0f) + feedConnection?.endScroll() } } - } - } else { - LaunchedEffect(Unit) { - feedConnection?.closeFeed() + + if (isActive) { + SideEffect { + state.setProgress(feedProgress) + } + + if (feedProgress <= 0) { + LaunchedEffect(Unit) { + feedConnection?.endScroll() + state.onPredictiveBackEnd() + } + } + } else { + SideEffect { + if (feedProgress != progress) { + feedConnection?.onScroll(progress) + } + } + } + + if (state.isSettledOnSecondaryPage && feedProgress >= 0.8f) { + LaunchedEffect(Unit) { + feedConnection?.endScroll() + } + } + } } @@ -119,8 +136,9 @@ internal class FeedComponent( modifier = modifier, verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically), horizontalAlignment = Alignment.CenterHorizontally, - ) { - if (feedAvailable?.value == false) { + ) + { + if (!feedAvailable) { Icon( painterResource(R.drawable.error_48px), null, @@ -133,6 +151,19 @@ internal class FeedComponent( color = MaterialTheme.colorScheme.error, textAlign = TextAlign.Center, ) + } else if (!feedHasContent) { + Icon( + painterResource(R.drawable.news_48px), + null, + tint = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.size(48.dp) + ) + Text( + "Feed has no content.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + ) } } } diff --git a/services/feed/src/main/java/de/mm20/launcher2/feed/FeedConnection.kt b/services/feed/src/main/java/de/mm20/launcher2/feed/FeedConnection.kt index bfc80297a..5b583bc0f 100644 --- a/services/feed/src/main/java/de/mm20/launcher2/feed/FeedConnection.kt +++ b/services/feed/src/main/java/de/mm20/launcher2/feed/FeedConnection.kt @@ -22,6 +22,9 @@ import kotlinx.coroutines.flow.StateFlow interface FeedConnection { val available: StateFlow val ready: StateFlow + val hasContent: StateFlow + + val scrollProgress: StateFlow fun restart() fun destroy() @@ -29,7 +32,7 @@ interface FeedConnection { fun startScroll() fun endScroll() fun onScroll(progress: Float) - + fun openFeed() fun closeFeed() } @@ -37,7 +40,6 @@ interface FeedConnection { internal class FeedConnectionImpl( private val activity: AppCompatActivity, private val serviceIntent: Intent, - private val callback: FeedCallback, ) : IBridgeCallback.Stub(), FeedConnection, ServiceConnection, DefaultLifecycleObserver { private var isActivityStarted = false @@ -49,9 +51,14 @@ internal class FeedConnectionImpl( override val available = MutableStateFlow(true) override val ready = MutableStateFlow(false) + override val hasContent = MutableStateFlow(false) + + override val scrollProgress = MutableStateFlow(0f) private var overlay: ILauncherOverlay? = null + private var retries = MaxRetries + init { start() } @@ -131,6 +138,10 @@ internal class FeedConnectionImpl( ready.value = false available.value = false overlay = null + Log.e( + "FeedConnection", + "onServiceConnected received a null binder" + ) return } @@ -142,6 +153,11 @@ internal class FeedConnectionImpl( overlay = ILauncherOverlay.Stub.asInterface(service) sendConfig() ready.value = true + retries = MaxRetries + Log.d( + "FeedConnection", + "Feed service connected with interface descriptor \"${service.interfaceDescriptor}\"" + ) } else { Log.e( "FeedConnection", @@ -149,7 +165,11 @@ internal class FeedConnectionImpl( ) available.value = false ready.value = false - destroy() + if (retries > 0) { + Log.e("FeedConnection", "$retries remaining, trying again") + retries-- + restart() + } } } catch (e: RemoteException) { CrashReporter.logException(e) @@ -168,7 +188,10 @@ internal class FeedConnectionImpl( override fun onBindingDied(name: ComponentName?) { super.onBindingDied(name) Log.w("FeedConnection", "binding has died :(") - restart() + if (retries > 0) { + retries-- + restart() + } } override fun onStart(owner: LifecycleOwner) { @@ -202,11 +225,17 @@ internal class FeedConnectionImpl( val callback = object : ILauncherOverlayCallback.Stub() { override fun overlayScrollChanged(progress: Float) { - callback.onOverlayScrollChanged(progress) + scrollProgress.value = progress } override fun overlayStatusChanged(status: Int) { Log.d("FeedConnection", "overlayStatusChanged: $status") + hasContent.value = try { + overlay?.hasOverlayContent() ?: false + } catch (e: RemoteException) { + CrashReporter.logException(e) + false + } } } @@ -225,6 +254,7 @@ internal class FeedConnectionImpl( companion object { private const val Flags = Context.BIND_AUTO_CREATE or Context.BIND_IMPORTANT + private const val MaxRetries = 5 } } \ No newline at end of file diff --git a/services/feed/src/main/java/de/mm20/launcher2/feed/FeedService.kt b/services/feed/src/main/java/de/mm20/launcher2/feed/FeedService.kt index e498858d1..72aad80e7 100644 --- a/services/feed/src/main/java/de/mm20/launcher2/feed/FeedService.kt +++ b/services/feed/src/main/java/de/mm20/launcher2/feed/FeedService.kt @@ -14,7 +14,6 @@ class FeedService( fun createFeedInstance( activity: AppCompatActivity, feedProvider: String, - callback: FeedCallback, ): FeedConnection { val intent = Intent( "com.android.launcher3.WINDOW_OVERLAY", @@ -29,7 +28,6 @@ class FeedService( return FeedConnectionImpl( activity, intent.setPackage(feedProvider), - callback, ) }