yet another attempt to improve feed interaction

This commit is contained in:
MM20
2026-01-11 14:30:23 +01:00
parent 86867ead37
commit a816e18e14
3 changed files with 106 additions and 47 deletions

View File

@@ -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<FeedConnection?>(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,
)
}
}
}

View File

@@ -22,6 +22,9 @@ import kotlinx.coroutines.flow.StateFlow
interface FeedConnection {
val available: StateFlow<Boolean>
val ready: StateFlow<Boolean>
val hasContent: StateFlow<Boolean>
val scrollProgress: StateFlow<Float>
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
}
}

View File

@@ -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,
)
}