[WIP] Add experimental feed support
This commit is contained in:
@@ -158,6 +158,7 @@ dependencies {
|
||||
implementation(project(":data:locations"))
|
||||
implementation(project(":services:plugins"))
|
||||
implementation(project(":core:devicepose"))
|
||||
implementation(project(":services:feed"))
|
||||
|
||||
// Uncomment this if you want annoying notifications in your debug builds
|
||||
//debugImplementation(libs.leakcanary)
|
||||
|
||||
@@ -31,6 +31,7 @@ import de.mm20.launcher2.locations.locationsModule
|
||||
import de.mm20.launcher2.permissions.permissionsModule
|
||||
import de.mm20.launcher2.data.plugins.dataPluginsModule
|
||||
import de.mm20.launcher2.devicepose.devicePoseModule
|
||||
import de.mm20.launcher2.feed.feedModule
|
||||
import de.mm20.launcher2.plugins.servicesPluginsModule
|
||||
import de.mm20.launcher2.preferences.preferencesModule
|
||||
import de.mm20.launcher2.profiles.profilesModule
|
||||
@@ -98,6 +99,7 @@ class LauncherApplication : Application(), CoroutineScope, ImageLoaderFactory {
|
||||
devicePoseModule,
|
||||
profilesModule,
|
||||
i18nDataModule,
|
||||
feedModule,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -161,5 +161,6 @@ dependencies {
|
||||
implementation(project(":services:global-actions"))
|
||||
implementation(project(":services:widgets"))
|
||||
implementation(project(":services:favorites"))
|
||||
implementation(project(":services:feed"))
|
||||
implementation(project(":core:devicepose"))
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import android.view.WindowManager
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
@@ -27,6 +28,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.TransformOrigin
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
@@ -48,6 +50,7 @@ import de.mm20.launcher2.ui.ktx.animateTo
|
||||
import de.mm20.launcher2.ui.launcher.scaffold.ClockAndWidgetsHomeComponent
|
||||
import de.mm20.launcher2.ui.launcher.scaffold.ClockHomeComponent
|
||||
import de.mm20.launcher2.ui.launcher.scaffold.DismissComponent
|
||||
import de.mm20.launcher2.ui.launcher.scaffold.FeedComponent
|
||||
import de.mm20.launcher2.ui.launcher.scaffold.Gesture
|
||||
import de.mm20.launcher2.ui.launcher.scaffold.LaunchComponent
|
||||
import de.mm20.launcher2.ui.launcher.scaffold.LauncherScaffold
|
||||
@@ -168,7 +171,7 @@ abstract class SharedLauncherActivity(
|
||||
val darkSearchBar = LocalPreferDarkContentOverWallpaper.current
|
||||
&& searchBarColor == SearchBarColors.Auto || searchBarColor == SearchBarColors.Dark
|
||||
|
||||
LaunchedEffect(dimBackground && darkTheme) {
|
||||
/*LaunchedEffect(dimBackground && darkTheme) {
|
||||
if (dimBackground && darkTheme) {
|
||||
val windowAttributes = window.attributes
|
||||
windowAttributes.flags =
|
||||
@@ -182,7 +185,7 @@ abstract class SharedLauncherActivity(
|
||||
window.attributes = windowAttributes
|
||||
window.setDimAmount(0f)
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
val enterTransitionProgress = remember { mutableStateOf(100f) }
|
||||
var enterTransition by remember {
|
||||
@@ -211,6 +214,10 @@ abstract class SharedLauncherActivity(
|
||||
|
||||
OverlayHost(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
if (dimBackground && darkTheme) Color( 0f, 0f, 0f, 0.3f)
|
||||
else Color.Transparent
|
||||
)
|
||||
.fillMaxSize(),
|
||||
contentAlignment = Alignment.BottomCenter
|
||||
) {
|
||||
@@ -266,6 +273,7 @@ abstract class SharedLauncherActivity(
|
||||
openKeyboard = searchBarAutofocus,
|
||||
)
|
||||
val widgetComponent by lazy { WidgetsComponent }
|
||||
val feedComponent by lazy { FeedComponent(this@SharedLauncherActivity) }
|
||||
|
||||
fun getScaffoldGesture(
|
||||
action: GestureAction?,
|
||||
@@ -318,6 +326,11 @@ abstract class SharedLauncherActivity(
|
||||
animation = if (gesture.orientation == null) ScaffoldAnimation.ZoomIn else ScaffoldAnimation.Push,
|
||||
)
|
||||
|
||||
is GestureAction.Feed -> ScaffoldGesture(
|
||||
component = FeedComponent(this@SharedLauncherActivity),
|
||||
animation = if (gesture.orientation == null) ScaffoldAnimation.ZoomIn else ScaffoldAnimation.Push,
|
||||
)
|
||||
|
||||
is GestureAction.Launch if (searchable != null) -> ScaffoldGesture(
|
||||
component = LaunchComponent(
|
||||
this@SharedLauncherActivity,
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
package de.mm20.launcher2.ui.launcher.scaffold
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
internal class FeedComponent(
|
||||
private val context: Context,
|
||||
) : ScaffoldComponent(), KoinComponent {
|
||||
|
||||
private val feedSettings: FeedSettings by inject()
|
||||
private val feedService: FeedService by inject()
|
||||
|
||||
override val permanent: Boolean = true
|
||||
|
||||
@Composable
|
||||
override fun Component(
|
||||
modifier: Modifier,
|
||||
insets: PaddingValues,
|
||||
state: LauncherScaffoldState
|
||||
) {
|
||||
val activity = LocalActivity.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
|
||||
val progress = state.currentProgress
|
||||
val feedProgress = remember { mutableFloatStateOf(0f) }
|
||||
|
||||
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)
|
||||
|
||||
|
||||
DisposableEffect(feedProviderPackage) {
|
||||
val conn = feedProviderPackage?.let {
|
||||
feedService.createFeedInstance(activity as AppCompatActivity, it) { p ->
|
||||
feedProgress.floatValue = p
|
||||
}
|
||||
}
|
||||
feedConnection = conn
|
||||
|
||||
onDispose {
|
||||
conn?.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
val enableScroll = state.currentComponent == this && progress > 0f && progress < 1f
|
||||
|
||||
LaunchedEffect(enableScroll) {
|
||||
if (enableScroll) {
|
||||
feedConnection?.startScroll()
|
||||
} else {
|
||||
feedConnection?.endScroll()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(progress) {
|
||||
feedConnection?.onScroll(progress)
|
||||
}
|
||||
|
||||
LaunchedEffect(feedProgress.floatValue) {
|
||||
if (isActive) {
|
||||
state.setProgress(feedProgress.floatValue)
|
||||
if (feedProgress.floatValue <= 0f) {
|
||||
state.onPredictiveBackEnd()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
if (feedAvailable?.value == false) {
|
||||
Icon(
|
||||
painterResource(R.drawable.error_48px),
|
||||
null,
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(48.dp)
|
||||
)
|
||||
Text(
|
||||
"Feed could not be loaded.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ModifierFactoryExtensionFunction")
|
||||
override fun homePageModifier(
|
||||
state: LauncherScaffoldState,
|
||||
defaultModifier: Modifier
|
||||
): Modifier {
|
||||
return defaultModifier.alpha(1f - state.currentProgress)
|
||||
}
|
||||
|
||||
@SuppressLint("ModifierFactoryExtensionFunction")
|
||||
override fun searchBarModifier(
|
||||
state: LauncherScaffoldState,
|
||||
defaultModifier: Modifier
|
||||
): Modifier {
|
||||
return defaultModifier
|
||||
.offset { state.currentOffset.toIntOffset() }
|
||||
.alpha(1f - state.currentProgress)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -791,17 +791,13 @@ internal class LauncherScaffoldState(
|
||||
}
|
||||
}
|
||||
|
||||
private val backInterpolation = PathInterpolator(0f, 0f, 0f, 1f)
|
||||
fun onPredictiveBack(progress: Float) {
|
||||
if (!isSettledOnSecondaryPage) return
|
||||
fun setProgress(progress: Float) {
|
||||
val gesture = currentGesture ?: return
|
||||
val anim = currentAnimation ?: return
|
||||
|
||||
val progress = backInterpolation.getInterpolation(progress)
|
||||
|
||||
when (gesture) {
|
||||
Gesture.TapSearchBar, Gesture.DoubleTap, Gesture.LongPress -> {
|
||||
currentZOffset = 1f - progress
|
||||
currentZOffset = progress
|
||||
}
|
||||
|
||||
else -> {
|
||||
@@ -818,13 +814,13 @@ internal class LauncherScaffoldState(
|
||||
|
||||
currentOffset = if (anim == ScaffoldAnimation.Push) {
|
||||
Offset(
|
||||
x * size.width * (1f - progress * 0.1f),
|
||||
y * size.height * (1f - progress * 0.1f)
|
||||
x * size.width * progress,
|
||||
y * size.height * progress
|
||||
)
|
||||
} else {
|
||||
Offset(
|
||||
x * rubberbandThreshold * progress,
|
||||
y * rubberbandThreshold * progress
|
||||
x * rubberbandThreshold * (1f - progress),
|
||||
y * rubberbandThreshold * (1f - progress)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -832,6 +828,21 @@ internal class LauncherScaffoldState(
|
||||
}
|
||||
}
|
||||
|
||||
private val backInterpolation = PathInterpolator(0f, 0f, 0f, 1f)
|
||||
fun onPredictiveBack(progress: Float) {
|
||||
if (!isSettledOnSecondaryPage) return
|
||||
val anim = currentAnimation ?: return
|
||||
|
||||
val progress = backInterpolation.getInterpolation(progress)
|
||||
|
||||
val p = when(anim) {
|
||||
ScaffoldAnimation.Push -> 1f - progress * 0.1f
|
||||
else -> 1f - progress
|
||||
}
|
||||
|
||||
setProgress(p)
|
||||
}
|
||||
|
||||
suspend fun onPredictiveBackCancel() {
|
||||
val gesture = currentGesture ?: return
|
||||
val anim = currentAnimation ?: return
|
||||
|
||||
@@ -70,6 +70,8 @@ import de.mm20.launcher2.ui.settings.easteregg.EasterEggSettingsRoute
|
||||
import de.mm20.launcher2.ui.settings.easteregg.EasterEggSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.favorites.FavoritesSettingsRoute
|
||||
import de.mm20.launcher2.ui.settings.favorites.FavoritesSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.feed.FeedIntegrationSettingsRoute
|
||||
import de.mm20.launcher2.ui.settings.feed.FeedIntegrationSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.filesearch.FileSearchSettingsRoute
|
||||
import de.mm20.launcher2.ui.settings.filesearch.FileSearchSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.filterbar.FilterBarSettingsRoute
|
||||
@@ -305,6 +307,9 @@ class SettingsActivity : BaseActivity() {
|
||||
entry<SmartspacerSettingsRoute> {
|
||||
SmartspacerSettingsScreen()
|
||||
}
|
||||
entry<FeedIntegrationSettingsRoute> {
|
||||
FeedIntegrationSettingsScreen()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package de.mm20.launcher2.ui.settings.feed
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.LargeMessage
|
||||
import de.mm20.launcher2.ui.component.preferences.Preference
|
||||
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
|
||||
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data object FeedIntegrationSettingsRoute : NavKey
|
||||
|
||||
@Composable
|
||||
fun FeedIntegrationSettingsScreen() {
|
||||
val context = LocalContext.current
|
||||
|
||||
val viewModel: FeedIntegrationSettingsScreenVM = viewModel()
|
||||
val selectedProvider by viewModel.providerPackage.collectAsState(null)
|
||||
|
||||
val providers = remember { viewModel.getFeedProviders(context) }
|
||||
PreferenceScreen(
|
||||
title = stringResource(R.string.preference_feed_integration),
|
||||
helpUrl = "https://kvaesitso.mm20.de/docs/user-guide/integrations/feed"
|
||||
) {
|
||||
if (providers.isEmpty()) {
|
||||
item {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillParentMaxHeight()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
LargeMessage(
|
||||
icon = R.drawable.news_48px,
|
||||
text = stringResource(R.string.no_feed_providers),
|
||||
color = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
item {
|
||||
PreferenceCategory {
|
||||
for (prov in providers) {
|
||||
Preference(
|
||||
title = prov.label,
|
||||
icon = if (prov.packageName == selectedProvider) R.drawable.radio_button_checked_24px else R.drawable.radio_button_unchecked_24px,
|
||||
onClick = {
|
||||
viewModel.setProviderPackage(prov.packageName)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package de.mm20.launcher2.ui.settings.feed
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Process
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.mm20.launcher2.feed.FeedProvider
|
||||
import de.mm20.launcher2.feed.FeedService
|
||||
import de.mm20.launcher2.preferences.feed.FeedSettings
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import java.text.Collator
|
||||
|
||||
class FeedIntegrationSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
|
||||
private val feedService: FeedService by inject()
|
||||
private val feedSettings: FeedSettings by inject()
|
||||
|
||||
val providerPackage = feedSettings.providerPackage
|
||||
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(0), 1)
|
||||
|
||||
fun setProviderPackage(providerPackage: String?) {
|
||||
feedSettings.setProviderPackage(providerPackage)
|
||||
}
|
||||
|
||||
fun getFeedProviders(context: Context): List<FeedProvider> {
|
||||
return feedService.getAvailableFeedProviders()
|
||||
}
|
||||
}
|
||||
@@ -61,13 +61,14 @@ fun GestureSettingsScreen() {
|
||||
add(stringResource(R.string.gesture_action_launch_app) to GestureAction.Launch(null))
|
||||
}
|
||||
|
||||
val optionsWithFeed = options + (stringResource(R.string.gesture_action_feed) to GestureAction.Feed)
|
||||
|
||||
|
||||
val context = LocalContext.current
|
||||
PreferenceScreen(title = stringResource(R.string.preference_screen_gestures)) {
|
||||
item {
|
||||
val appIconSize = 32.dp.toPixels()
|
||||
PreferenceCategory {
|
||||
|
||||
|
||||
val swipeDown by viewModel.swipeDown.collectAsStateWithLifecycle(null)
|
||||
val swipeDownApp by viewModel.swipeDownApp.collectAsState(null)
|
||||
val swipeDownAppIcon by remember(swipeDownApp?.key) {
|
||||
@@ -128,7 +129,7 @@ fun GestureSettingsScreen() {
|
||||
icon = R.drawable.swipe_right_alt_24px,
|
||||
value = swipeRight,
|
||||
onValueChanged = { viewModel.setSwipeRight(it) },
|
||||
options = options,
|
||||
options = optionsWithFeed,
|
||||
app = swipeRightApp,
|
||||
appIcon = swipeRightAppIcon,
|
||||
onAppChanged = { viewModel.setSwipeRightApp(it) }
|
||||
|
||||
@@ -11,6 +11,7 @@ import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
|
||||
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
|
||||
import de.mm20.launcher2.ui.locals.LocalBackStack
|
||||
import de.mm20.launcher2.ui.settings.breezyweather.BreezyWeatherSettingsRoute
|
||||
import de.mm20.launcher2.ui.settings.feed.FeedIntegrationSettingsRoute
|
||||
import de.mm20.launcher2.ui.settings.media.MediaIntegrationSettingsRoute
|
||||
import de.mm20.launcher2.ui.settings.nextcloud.NextcloudSettingsRoute
|
||||
import de.mm20.launcher2.ui.settings.owncloud.OwncloudSettingsRoute
|
||||
@@ -45,6 +46,13 @@ fun IntegrationsSettingsScreen() {
|
||||
backStack.add(MediaIntegrationSettingsRoute)
|
||||
}
|
||||
)
|
||||
Preference(
|
||||
title = stringResource(R.string.preference_feed_integration),
|
||||
icon = R.drawable.news_24px,
|
||||
onClick = {
|
||||
backStack.add(FeedIntegrationSettingsRoute)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
item {
|
||||
|
||||
11
core/base/src/main/res/drawable/news_24px.xml
Normal file
11
core/base/src/main/res/drawable/news_24px.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:autoMirrored="true">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L607,120Q623,120 637.5,126Q652,132 663,143L817,297Q828,308 834,322.5Q840,337 840,353L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM200,760L760,760Q760,760 760,760Q760,760 760,760L760,360L640,360Q623,360 611.5,348.5Q600,337 600,320L600,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760ZM640,680Q657,680 668.5,668.5Q680,657 680,640Q680,623 668.5,611.5Q657,600 640,600L320,600Q303,600 291.5,611.5Q280,623 280,640Q280,657 291.5,668.5Q303,680 320,680L640,680ZM440,360Q457,360 468.5,348.5Q480,337 480,320Q480,303 468.5,291.5Q457,280 440,280L320,280Q303,280 291.5,291.5Q280,303 280,320Q280,337 291.5,348.5Q303,360 320,360L440,360ZM640,520Q657,520 668.5,508.5Q680,497 680,480Q680,463 668.5,451.5Q657,440 640,440L320,440Q303,440 291.5,451.5Q280,463 280,480Q280,497 291.5,508.5Q303,520 320,520L640,520ZM200,200L200,200L200,360L200,360L200,200L200,360L200,360L200,760Q200,760 200,760Q200,760 200,760L200,760Q200,760 200,760Q200,760 200,760L200,200Q200,200 200,200Q200,200 200,200Z"/>
|
||||
</vector>
|
||||
11
core/base/src/main/res/drawable/news_48px.xml
Normal file
11
core/base/src/main/res/drawable/news_48px.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:autoMirrored="true">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M180,840Q156,840 138,822Q120,804 120,780L120,180Q120,156 138,138Q156,120 180,120L617,120Q629.44,120 640.72,125Q652,130 660,138L822,300Q830,308 835,319.28Q840,330.56 840,343L840,780Q840,804 822,822Q804,840 780,840L180,840ZM180,780L780,780Q780,780 780,780Q780,780 780,780L780,351L639,351Q626.25,351 617.63,342.37Q609,333.75 609,321L609,180L180,180Q180,180 180,180Q180,180 180,180L180,780Q180,780 180,780Q180,780 180,780ZM651,669Q663.75,669 672.38,660.32Q681,651.65 681,638.82Q681,626 672.38,617.5Q663.75,609 651,609L309,609Q296.25,609 287.63,617.68Q279,626.35 279,639.18Q279,652 287.63,660.5Q296.25,669 309,669L651,669ZM450,351Q462.75,351 471.38,342.32Q480,333.65 480,320.82Q480,308 471.38,299.5Q462.75,291 450,291L309,291Q296.25,291 287.63,299.68Q279,308.35 279,321.18Q279,334 287.63,342.5Q296.25,351 309,351L450,351ZM651,510Q663.75,510 672.38,501.32Q681,492.65 681,479.82Q681,467 672.38,458.5Q663.75,450 651,450L309,450Q296.25,450 287.63,458.68Q279,467.35 279,480.18Q279,493 287.63,501.5Q296.25,510 309,510L651,510ZM180,180L180,180L180,351.43L180,351.43L180,180L180,351.43L180,351.43L180,780Q180,780 180,780Q180,780 180,780L180,780Q180,780 180,780Q180,780 180,780L180,180Q180,180 180,180Q180,180 180,180Z"/>
|
||||
</vector>
|
||||
@@ -700,6 +700,8 @@
|
||||
<string name="preference_smartspacer_integration_description">Smartspacer is a free and open source At a Glance widget. If installed, it can be integrated into the dynamic zone of the clock widget</string>
|
||||
<string name="preference_smartspacer_enable">Enable Smartspacer integration</string>
|
||||
<string name="preference_launch_smartspacer_app">Open Smartspacer</string>
|
||||
<string name="preference_feed_integration">Feed</string>
|
||||
<string name="no_feed_providers">No feed providers installed</string>
|
||||
<string name="preference_contacts_call_on_tap">Tap to call</string>
|
||||
<string name="preference_contacts_call_on_tap_summary">Start a call without confirmation when tapping a phone number</string>
|
||||
<!-- Used in an info banner if a specific feature requires a Nextcloud account -->
|
||||
@@ -804,6 +806,7 @@
|
||||
<string name="gesture_action_quick_settings">Open quick settings</string>
|
||||
<string name="gesture_action_power_menu">Show power menu</string>
|
||||
<string name="gesture_action_recents">Open recent apps</string>
|
||||
<string name="gesture_action_feed">Feed</string>
|
||||
<string name="gesture_failed_message">You have performed a \"%1$s\" gesture. This gesture is currently set to trigger a \"%2$s\" action. However, the action could not be performed for the following reason:</string>
|
||||
<string name="missing_permission_accessibility_gesture_failed">The launcher\'s accessibility service needs to be enabled to perform this action.</string>
|
||||
<string name="missing_permission_accessibility_gesture_settings">This action requires the launcher\'s accessibility service to be enabled.</string>
|
||||
|
||||
@@ -202,6 +202,8 @@ data class LauncherSettingsData internal constructor(
|
||||
*/
|
||||
val localeTransliterator: String? = "",
|
||||
|
||||
val feedProviderPackage: String? = null
|
||||
|
||||
|
||||
) {
|
||||
constructor(
|
||||
@@ -394,6 +396,10 @@ sealed interface GestureAction {
|
||||
@Serializable
|
||||
@SerialName("launch_searchable")
|
||||
data class Launch(val key: String?) : GestureAction
|
||||
|
||||
@Serializable
|
||||
@SerialName("feed")
|
||||
data object Feed : GestureAction
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package de.mm20.launcher2.preferences
|
||||
|
||||
import de.mm20.launcher2.backup.Backupable
|
||||
import de.mm20.launcher2.preferences.feed.FeedSettings
|
||||
import de.mm20.launcher2.preferences.search.ContactSearchSettings
|
||||
import de.mm20.launcher2.preferences.media.MediaSettings
|
||||
import de.mm20.launcher2.preferences.search.CalculatorSearchSettings
|
||||
@@ -52,4 +53,5 @@ val preferencesModule = module {
|
||||
factory { LocationSearchSettings(get()) }
|
||||
factory { SearchFilterSettings(get()) }
|
||||
factory { LocaleSettings(get()) }
|
||||
factory { FeedSettings(get()) }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package de.mm20.launcher2.preferences.feed
|
||||
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class FeedSettings internal constructor(
|
||||
private val launcherDataStore: LauncherDataStore,
|
||||
) {
|
||||
|
||||
val providerPackage
|
||||
get() = launcherDataStore.data.map { it.feedProviderPackage }
|
||||
|
||||
fun setProviderPackage(providerPackage: String?) {
|
||||
launcherDataStore.update {
|
||||
it.copy(feedProviderPackage = providerPackage)
|
||||
}
|
||||
}
|
||||
}
|
||||
38
docs/docs/user-guide/integrations/feed.md
Normal file
38
docs/docs/user-guide/integrations/feed.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Feed
|
||||
|
||||
The feed is a personalized content page that lives to the left of your home screen, that can be
|
||||
opened without leaving the launcher. Traditionally, launchers show the Google Discover feed here,
|
||||
but there are other alternatives.
|
||||
|
||||
## Feed providers
|
||||
|
||||
The feed content is provided by a third party app, which needs to be installed on the device.
|
||||
|
||||
### Google Discover
|
||||
|
||||
On most devices, the Google app is preinstalled as a feed provider. Unfortunately, the Google app
|
||||
can't be embedded as a feed directly, because it only allows to be embedded by system apps and
|
||||
development builds. If you want to use Google Discover as your feed provider, you can
|
||||
install [AIDL Bridge](https://github.com/amirzaidi/AIDLBridge/releases)
|
||||
as a workaround. AIDL Bridge acts as a bridge to allow third party apps to connect to the
|
||||
Google Discover feed.
|
||||
|
||||
> [!WARNING]
|
||||
> AIDL Bridge uses an unofficial workaround to expose the Google Discover feed to third party apps.
|
||||
> This workaround might stop working without further notice. Use at your own risk.
|
||||
|
||||
### Other providers
|
||||
|
||||
The following feed providers have been tested and are known to be working:
|
||||
|
||||
- **[Neo-Feed](https://github.com/NeoApplications/Neo-Feed)**: A simple RSS feed reader
|
||||
- **[Smartspacer](https://github.com/KieronQuinn/Smartspacer)**: A customizable widgets page
|
||||
|
||||
## Enable the feed
|
||||
|
||||
After you have installed at least one feed provider, you can enable it under Settings >
|
||||
Integrations > Feed. Then you can assign the feed action to the swipe right gesture under Settings >
|
||||
Gestures
|
||||
|
||||
> [!NOTE]
|
||||
> The feed can only be assigned to the swipe right gesture
|
||||
@@ -47,6 +47,7 @@ junitVersion = "1.2.1"
|
||||
espressoCore = "3.6.1"
|
||||
|
||||
osmOpeningHours = "0.4.0"
|
||||
material = "1.13.0"
|
||||
|
||||
[libraries]
|
||||
mustache-compiler = { module = "com.github.spullara.mustache.java:compiler", version.ref = "mustache" }
|
||||
@@ -135,6 +136,7 @@ androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-co
|
||||
osmopeninghours = { group = "de.westnordost", name = "osm-opening-hours", version.ref = "osmOpeningHours" }
|
||||
|
||||
smartspacer = { group = "com.kieronquinn.smartspacer", name = "sdk-client", version = "1.1.2" }
|
||||
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||
|
||||
[bundles]
|
||||
kotlin = ["kotlin-stdlib", "kotlinx-coroutines-core", "kotlinx-coroutines-android", "kotlinx-collections-immutable", "kotlinx-serialization-json"]
|
||||
|
||||
1
services/feed/.gitignore
vendored
Normal file
1
services/feed/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
58
services/feed/build.gradle.kts
Normal file
58
services/feed/build.gradle.kts
Normal file
@@ -0,0 +1,58 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.plugin.serialization)
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = libs.versions.compileSdk.get().toInt()
|
||||
|
||||
defaultConfig {
|
||||
minSdk = libs.versions.minSdk.get().toInt()
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_1_8)
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
aidl = true
|
||||
}
|
||||
|
||||
namespace = "de.mm20.launcher2.feed"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.bundles.kotlin)
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.bundles.androidx.lifecycle)
|
||||
|
||||
implementation(libs.koin.android)
|
||||
|
||||
implementation(project(":core:ktx"))
|
||||
implementation(project(":core:preferences"))
|
||||
implementation(project(":core:crashreporter"))
|
||||
implementation(project(":core:base"))
|
||||
}
|
||||
0
services/feed/consumer-rules.pro
Normal file
0
services/feed/consumer-rules.pro
Normal file
21
services/feed/proguard-rules.pro
vendored
Normal file
21
services/feed/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
4
services/feed/src/main/AndroidManifest.xml
Normal file
4
services/feed/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,7 @@
|
||||
package amirz.aidlbridge;
|
||||
|
||||
import amirz.aidlbridge.IBridgeCallback;
|
||||
|
||||
interface IBridge {
|
||||
oneway void bindService(in IBridgeCallback cb, in int flags);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package amirz.aidlbridge;
|
||||
|
||||
interface IBridgeCallback {
|
||||
oneway void onServiceConnected(in ComponentName name, in IBinder service);
|
||||
|
||||
oneway void onServiceDisconnected(in ComponentName name);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.google.android.libraries.launcherclient;
|
||||
|
||||
import android.view.WindowManager.LayoutParams;
|
||||
import com.google.android.libraries.launcherclient.ILauncherOverlayCallback;
|
||||
|
||||
interface ILauncherOverlay {
|
||||
|
||||
oneway void startScroll();
|
||||
|
||||
oneway void onScroll(in float progress);
|
||||
|
||||
oneway void endScroll();
|
||||
|
||||
oneway void windowAttached(in LayoutParams lp, in ILauncherOverlayCallback cb, in int flags);
|
||||
|
||||
oneway void windowDetached(in boolean isChangingConfigurations);
|
||||
|
||||
oneway void closeOverlay(in int flags);
|
||||
|
||||
oneway void onPause();
|
||||
|
||||
oneway void onResume();
|
||||
|
||||
oneway void openOverlay(in int flags);
|
||||
|
||||
oneway void requestVoiceDetection(in boolean start);
|
||||
|
||||
String getVoiceSearchLanguage();
|
||||
|
||||
boolean isVoiceDetectionRunning();
|
||||
|
||||
boolean hasOverlayContent();
|
||||
|
||||
oneway void windowAttached2(in Bundle bundle, in ILauncherOverlayCallback cb);
|
||||
|
||||
oneway void unusedMethod();
|
||||
|
||||
oneway void setActivityState(in int flags);
|
||||
|
||||
boolean startSearch(in byte[] data, in Bundle bundle);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.google.android.libraries.launcherclient;
|
||||
|
||||
interface ILauncherOverlayCallback {
|
||||
|
||||
oneway void overlayScrollChanged(float progress);
|
||||
|
||||
oneway void overlayStatusChanged(int status);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package de.mm20.launcher2.feed
|
||||
|
||||
fun interface FeedCallback {
|
||||
fun onOverlayScrollChanged(progress: Float)
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
package de.mm20.launcher2.feed
|
||||
|
||||
import amirz.aidlbridge.IBridge
|
||||
import amirz.aidlbridge.IBridgeCallback
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.os.RemoteException
|
||||
import android.util.Log
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.google.android.libraries.launcherclient.ILauncherOverlay
|
||||
import com.google.android.libraries.launcherclient.ILauncherOverlayCallback
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface FeedConnection {
|
||||
val available: StateFlow<Boolean>
|
||||
val ready: StateFlow<Boolean>
|
||||
|
||||
fun restart()
|
||||
fun destroy()
|
||||
|
||||
fun startScroll()
|
||||
fun endScroll()
|
||||
fun onScroll(progress: Float)
|
||||
}
|
||||
|
||||
internal class FeedConnectionImpl(
|
||||
private val activity: AppCompatActivity,
|
||||
private val serviceIntent: Intent,
|
||||
private val callback: FeedCallback,
|
||||
) : IBridgeCallback.Stub(), FeedConnection, ServiceConnection, DefaultLifecycleObserver {
|
||||
|
||||
private var isActivityStarted = false
|
||||
private var isActivityResumed = false
|
||||
private val activityState: Int
|
||||
get() {
|
||||
return (if (isActivityResumed) 2 else 0) + (if (isActivityStarted) 1 else 0)
|
||||
}
|
||||
|
||||
override val available = MutableStateFlow(true)
|
||||
override val ready = MutableStateFlow(false)
|
||||
|
||||
private var overlay: ILauncherOverlay? = null
|
||||
|
||||
init {
|
||||
start()
|
||||
}
|
||||
|
||||
override fun startScroll() {
|
||||
try {
|
||||
overlay?.startScroll()
|
||||
} catch (e: RemoteException) {
|
||||
CrashReporter.logException(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun endScroll() {
|
||||
try {
|
||||
overlay?.endScroll()
|
||||
} catch (e: RemoteException) {
|
||||
CrashReporter.logException(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onScroll(progress: Float) {
|
||||
try {
|
||||
overlay?.onScroll(progress)
|
||||
} catch (e: RemoteException) {
|
||||
CrashReporter.logException(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun start() {
|
||||
activity.lifecycle.addObserver(this)
|
||||
val service = activity.packageManager.resolveService(serviceIntent, 0)
|
||||
if (service?.serviceInfo == null) {
|
||||
available.value = false
|
||||
ready.value = false
|
||||
return
|
||||
} else {
|
||||
available.value = true
|
||||
}
|
||||
activity.bindService(serviceIntent, this, Flags)
|
||||
}
|
||||
|
||||
override fun restart() {
|
||||
destroy()
|
||||
start()
|
||||
}
|
||||
|
||||
override fun destroy() {
|
||||
activity.lifecycle.removeObserver(this)
|
||||
try {
|
||||
activity.unbindService(this)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// Service was not bound
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceConnected(
|
||||
name: ComponentName?,
|
||||
service: IBinder?
|
||||
) {
|
||||
if (service == null) {
|
||||
ready.value = false
|
||||
available.value = false
|
||||
overlay = null
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (service.interfaceDescriptor == IBridge.DESCRIPTOR) {
|
||||
val bridge = IBridge.Stub.asInterface(service)
|
||||
bridge.bindService(this, Flags)
|
||||
} else if (service.interfaceDescriptor == ILauncherOverlay.DESCRIPTOR) {
|
||||
overlay = ILauncherOverlay.Stub.asInterface(service)
|
||||
sendConfig()
|
||||
ready.value = true
|
||||
} else {
|
||||
Log.e(
|
||||
"FeedConnection",
|
||||
"Unknown service descriptor \"${service.interfaceDescriptor}\" for intent $serviceIntent"
|
||||
)
|
||||
available.value = false
|
||||
ready.value = false
|
||||
destroy()
|
||||
}
|
||||
} catch (e: RemoteException) {
|
||||
CrashReporter.logException(e)
|
||||
ready.value = false
|
||||
available.value = false
|
||||
destroy()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
Log.w("FeedConnection", "service has been disconnected")
|
||||
ready.value = false
|
||||
overlay = null
|
||||
}
|
||||
|
||||
override fun onBindingDied(name: ComponentName?) {
|
||||
super.onBindingDied(name)
|
||||
Log.w("FeedConnection", "binding has died :(")
|
||||
restart()
|
||||
}
|
||||
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
super.onStart(owner)
|
||||
isActivityStarted = true
|
||||
overlay?.setActivityState(activityState)
|
||||
}
|
||||
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
super.onResume(owner)
|
||||
isActivityResumed = true
|
||||
overlay?.setActivityState(activityState)
|
||||
}
|
||||
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
super.onPause(owner)
|
||||
isActivityResumed = false
|
||||
overlay?.setActivityState(activityState)
|
||||
}
|
||||
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
super.onStop(owner)
|
||||
isActivityStarted = false
|
||||
overlay?.setActivityState(activityState)
|
||||
|
||||
}
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
private fun sendConfig() {
|
||||
val layoutParams = activity.window.attributes
|
||||
|
||||
val callback = object : ILauncherOverlayCallback.Stub() {
|
||||
override fun overlayScrollChanged(progress: Float) {
|
||||
callback.onOverlayScrollChanged(progress)
|
||||
}
|
||||
|
||||
override fun overlayStatusChanged(status: Int) {
|
||||
Log.d("FeedConnection", "overlayStatusChanged: $status")
|
||||
}
|
||||
|
||||
}
|
||||
overlay?.windowAttached2(
|
||||
Bundle().also {
|
||||
it.putParcelable("layout_params", layoutParams)
|
||||
it.putParcelable("configuration", activity.resources.configuration);
|
||||
it.putInt("client_options", Flags)
|
||||
},
|
||||
callback,
|
||||
)
|
||||
|
||||
overlay?.setActivityState(activityState)
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val Flags = Context.BIND_AUTO_CREATE or Context.BIND_IMPORTANT
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package de.mm20.launcher2.feed
|
||||
|
||||
data class FeedProvider(
|
||||
val label: String,
|
||||
val packageName: String,
|
||||
)
|
||||
@@ -0,0 +1,78 @@
|
||||
package de.mm20.launcher2.feed
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Process
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.net.toUri
|
||||
import java.text.Collator
|
||||
|
||||
class FeedService(
|
||||
private val context: Context,
|
||||
) {
|
||||
fun createFeedInstance(
|
||||
activity: AppCompatActivity,
|
||||
feedProvider: String,
|
||||
callback: FeedCallback,
|
||||
): FeedConnection {
|
||||
val intent = Intent(
|
||||
"com.android.launcher3.WINDOW_OVERLAY",
|
||||
).also {
|
||||
it.`package` = feedProvider
|
||||
it.data = "app://${context.packageName}:${Process.myUid()}".toUri()
|
||||
.buildUpon()
|
||||
.appendQueryParameter("v", "7")
|
||||
.appendQueryParameter("cv", "9")
|
||||
.build()
|
||||
}
|
||||
return FeedConnectionImpl(
|
||||
activity,
|
||||
intent.setPackage(feedProvider),
|
||||
callback,
|
||||
)
|
||||
}
|
||||
|
||||
fun getAvailableFeedProviders(): List<FeedProvider> {
|
||||
val services = context.packageManager.queryIntentServices(
|
||||
Intent(
|
||||
"com.android.launcher3.WINDOW_OVERLAY",
|
||||
).also {
|
||||
it.data = "app://${context.packageName}:${Process.myUid()}".toUri()
|
||||
.buildUpon()
|
||||
.appendQueryParameter("v", "7")
|
||||
.appendQueryParameter("cv", "9")
|
||||
.build()
|
||||
}, 0
|
||||
)
|
||||
|
||||
val collator = Collator.getInstance().apply { strength = Collator.SECONDARY }
|
||||
return services.map {
|
||||
FeedProvider(
|
||||
it.loadLabel(context.packageManager).toString(),
|
||||
it.serviceInfo.packageName,
|
||||
)
|
||||
}
|
||||
.filter {
|
||||
it.packageName !in BlocklistedPackages
|
||||
}
|
||||
.sortedWith { el1, el2 ->
|
||||
collator.compare(el1.label, el2.label)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* These overlay providers are known to not work with third party apps; block them to avoid
|
||||
* confusion.
|
||||
*/
|
||||
private val BlocklistedPackages = if (BuildConfig.DEBUG) {
|
||||
emptySet()
|
||||
} else {
|
||||
setOf(
|
||||
"com.google.android.googlequicksearchbox",
|
||||
"app.lawnchair.lawnfeed",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package de.mm20.launcher2.feed
|
||||
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.dsl.module
|
||||
|
||||
val feedModule = module {
|
||||
single { FeedService(androidContext()) }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package de.mm20.launcher2.feed
|
||||
|
||||
import amirz.aidlbridge.IBridgeCallback
|
||||
import android.content.ComponentName
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
|
||||
class ServiceConnection: IBridgeCallback.Stub(), ServiceConnection {
|
||||
override fun onServiceConnected(
|
||||
name: ComponentName?,
|
||||
service: IBinder?
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
}
|
||||
}
|
||||
@@ -68,3 +68,4 @@ include(":services:plugins")
|
||||
include(":core:devicepose")
|
||||
include(":core:profiles")
|
||||
include(":data:i18n")
|
||||
include(":services:feed")
|
||||
|
||||
Reference in New Issue
Block a user