yet another attempt to improve feed interaction
This commit is contained in:
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user