From 664419b3c44eef211bdb3f9b3eb40b2db8528024 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Sun, 4 Jan 2026 00:58:35 +0100 Subject: [PATCH] [WIP] Add experimental feed support --- app/app/build.gradle.kts | 1 + .../de/mm20/launcher2/LauncherApplication.kt | 2 + app/ui/build.gradle.kts | 1 + .../ui/launcher/SharedLauncherActivity.kt | 17 +- .../ui/launcher/scaffold/FeedComponent.kt | 145 ++++++++++++ .../ui/launcher/scaffold/LauncherScaffold.kt | 31 ++- .../launcher2/ui/settings/SettingsActivity.kt | 5 + .../feed/FeedIntegrationSettingsScreen.kt | 74 ++++++ .../feed/FeedIntegrationSettingsScreenVM.kt | 33 +++ .../gestures/GestureSettingsScreen.kt | 7 +- .../IntegrationsSettingsScreen.kt | 8 + core/base/src/main/res/drawable/news_24px.xml | 11 + core/base/src/main/res/drawable/news_48px.xml | 11 + core/i18n/src/main/res/values/strings.xml | 3 + .../preferences/LauncherSettingsData.kt | 6 + .../de/mm20/launcher2/preferences/Module.kt | 2 + .../preferences/feed/FeedSettings.kt | 18 ++ docs/docs/user-guide/integrations/feed.md | 38 ++++ gradle/libs.versions.toml | 2 + services/feed/.gitignore | 1 + services/feed/build.gradle.kts | 58 +++++ services/feed/consumer-rules.pro | 0 services/feed/proguard-rules.pro | 21 ++ services/feed/src/main/AndroidManifest.xml | 4 + .../main/aidl/amirz/aidlbridge/IBridge.aidl | 7 + .../amirz/aidlbridge/IBridgeCallback.aidl | 7 + .../launcherclient/ILauncherOverlay.aidl | 42 ++++ .../ILauncherOverlayCallback.aidl | 9 + .../de/mm20/launcher2/feed/FeedCallback.kt | 5 + .../de/mm20/launcher2/feed/FeedConnection.kt | 211 ++++++++++++++++++ .../de/mm20/launcher2/feed/FeedProvider.kt | 6 + .../de/mm20/launcher2/feed/FeedService.kt | 78 +++++++ .../java/de/mm20/launcher2/feed/Module.kt | 8 + .../mm20/launcher2/feed/ServiceConnection.kt | 18 ++ settings.gradle.kts | 1 + 35 files changed, 876 insertions(+), 15 deletions(-) create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/FeedComponent.kt create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/settings/feed/FeedIntegrationSettingsScreen.kt create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/settings/feed/FeedIntegrationSettingsScreenVM.kt create mode 100644 core/base/src/main/res/drawable/news_24px.xml create mode 100644 core/base/src/main/res/drawable/news_48px.xml create mode 100644 core/preferences/src/main/java/de/mm20/launcher2/preferences/feed/FeedSettings.kt create mode 100644 docs/docs/user-guide/integrations/feed.md create mode 100644 services/feed/.gitignore create mode 100644 services/feed/build.gradle.kts create mode 100644 services/feed/consumer-rules.pro create mode 100644 services/feed/proguard-rules.pro create mode 100644 services/feed/src/main/AndroidManifest.xml create mode 100644 services/feed/src/main/aidl/amirz/aidlbridge/IBridge.aidl create mode 100644 services/feed/src/main/aidl/amirz/aidlbridge/IBridgeCallback.aidl create mode 100644 services/feed/src/main/aidl/com/google/android/libraries/launcherclient/ILauncherOverlay.aidl create mode 100644 services/feed/src/main/aidl/com/google/android/libraries/launcherclient/ILauncherOverlayCallback.aidl create mode 100644 services/feed/src/main/java/de/mm20/launcher2/feed/FeedCallback.kt create mode 100644 services/feed/src/main/java/de/mm20/launcher2/feed/FeedConnection.kt create mode 100644 services/feed/src/main/java/de/mm20/launcher2/feed/FeedProvider.kt create mode 100644 services/feed/src/main/java/de/mm20/launcher2/feed/FeedService.kt create mode 100644 services/feed/src/main/java/de/mm20/launcher2/feed/Module.kt create mode 100644 services/feed/src/main/java/de/mm20/launcher2/feed/ServiceConnection.kt diff --git a/app/app/build.gradle.kts b/app/app/build.gradle.kts index 5a451204e..88c3ce5f0 100644 --- a/app/app/build.gradle.kts +++ b/app/app/build.gradle.kts @@ -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) diff --git a/app/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt b/app/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt index 2a7f86f67..aad22947a 100644 --- a/app/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt +++ b/app/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt @@ -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, ) ) } diff --git a/app/ui/build.gradle.kts b/app/ui/build.gradle.kts index f9cc916d7..e6ce7e2d1 100644 --- a/app/ui/build.gradle.kts +++ b/app/ui/build.gradle.kts @@ -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")) } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/SharedLauncherActivity.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/SharedLauncherActivity.kt index d5fffd75d..afde17d07 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/SharedLauncherActivity.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/SharedLauncherActivity.kt @@ -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, 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 new file mode 100644 index 000000000..4936048ba --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/FeedComponent.kt @@ -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(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) + } + +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/LauncherScaffold.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/LauncherScaffold.kt index 63aba5f64..161b2fb3e 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/LauncherScaffold.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/LauncherScaffold.kt @@ -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 diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt index d5ee23ade..ad8295558 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt @@ -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 { SmartspacerSettingsScreen() } + entry { + FeedIntegrationSettingsScreen() + } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/feed/FeedIntegrationSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/feed/FeedIntegrationSettingsScreen.kt new file mode 100644 index 000000000..223b4ac31 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/feed/FeedIntegrationSettingsScreen.kt @@ -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) + } + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/feed/FeedIntegrationSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/feed/FeedIntegrationSettingsScreenVM.kt new file mode 100644 index 000000000..6b132e20f --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/feed/FeedIntegrationSettingsScreenVM.kt @@ -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 { + return feedService.getAvailableFeedProviders() + } +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/gestures/GestureSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/gestures/GestureSettingsScreen.kt index 58467eae3..f4df35539 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/gestures/GestureSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/gestures/GestureSettingsScreen.kt @@ -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) } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/integrations/IntegrationsSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/integrations/IntegrationsSettingsScreen.kt index 42074c26c..dd9f7ac07 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/integrations/IntegrationsSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/integrations/IntegrationsSettingsScreen.kt @@ -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 { diff --git a/core/base/src/main/res/drawable/news_24px.xml b/core/base/src/main/res/drawable/news_24px.xml new file mode 100644 index 000000000..6ded41fae --- /dev/null +++ b/core/base/src/main/res/drawable/news_24px.xml @@ -0,0 +1,11 @@ + + + diff --git a/core/base/src/main/res/drawable/news_48px.xml b/core/base/src/main/res/drawable/news_48px.xml new file mode 100644 index 000000000..fbe7ab9a6 --- /dev/null +++ b/core/base/src/main/res/drawable/news_48px.xml @@ -0,0 +1,11 @@ + + + diff --git a/core/i18n/src/main/res/values/strings.xml b/core/i18n/src/main/res/values/strings.xml index 588f0078d..c9f6cec57 100644 --- a/core/i18n/src/main/res/values/strings.xml +++ b/core/i18n/src/main/res/values/strings.xml @@ -700,6 +700,8 @@ 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 Enable Smartspacer integration Open Smartspacer + Feed + No feed providers installed Tap to call Start a call without confirmation when tapping a phone number @@ -804,6 +806,7 @@ Open quick settings Show power menu Open recent apps + Feed 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: The launcher\'s accessibility service needs to be enabled to perform this action. This action requires the launcher\'s accessibility service to be enabled. diff --git a/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt b/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt index d90939dc2..342945f61 100644 --- a/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt +++ b/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt @@ -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 } diff --git a/core/preferences/src/main/java/de/mm20/launcher2/preferences/Module.kt b/core/preferences/src/main/java/de/mm20/launcher2/preferences/Module.kt index ab0d808c6..4089dd0a7 100644 --- a/core/preferences/src/main/java/de/mm20/launcher2/preferences/Module.kt +++ b/core/preferences/src/main/java/de/mm20/launcher2/preferences/Module.kt @@ -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()) } } \ No newline at end of file diff --git a/core/preferences/src/main/java/de/mm20/launcher2/preferences/feed/FeedSettings.kt b/core/preferences/src/main/java/de/mm20/launcher2/preferences/feed/FeedSettings.kt new file mode 100644 index 000000000..67084e118 --- /dev/null +++ b/core/preferences/src/main/java/de/mm20/launcher2/preferences/feed/FeedSettings.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/docs/docs/user-guide/integrations/feed.md b/docs/docs/user-guide/integrations/feed.md new file mode 100644 index 000000000..6f1a36097 --- /dev/null +++ b/docs/docs/user-guide/integrations/feed.md @@ -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 \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bdefb5138..af34affad 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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"] diff --git a/services/feed/.gitignore b/services/feed/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/services/feed/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/services/feed/build.gradle.kts b/services/feed/build.gradle.kts new file mode 100644 index 000000000..0804efac6 --- /dev/null +++ b/services/feed/build.gradle.kts @@ -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")) +} \ No newline at end of file diff --git a/services/feed/consumer-rules.pro b/services/feed/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/services/feed/proguard-rules.pro b/services/feed/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/services/feed/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/services/feed/src/main/AndroidManifest.xml b/services/feed/src/main/AndroidManifest.xml new file mode 100644 index 000000000..a5918e68a --- /dev/null +++ b/services/feed/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/services/feed/src/main/aidl/amirz/aidlbridge/IBridge.aidl b/services/feed/src/main/aidl/amirz/aidlbridge/IBridge.aidl new file mode 100644 index 000000000..441d699e9 --- /dev/null +++ b/services/feed/src/main/aidl/amirz/aidlbridge/IBridge.aidl @@ -0,0 +1,7 @@ +package amirz.aidlbridge; + +import amirz.aidlbridge.IBridgeCallback; + +interface IBridge { + oneway void bindService(in IBridgeCallback cb, in int flags); +} \ No newline at end of file diff --git a/services/feed/src/main/aidl/amirz/aidlbridge/IBridgeCallback.aidl b/services/feed/src/main/aidl/amirz/aidlbridge/IBridgeCallback.aidl new file mode 100644 index 000000000..de81718fd --- /dev/null +++ b/services/feed/src/main/aidl/amirz/aidlbridge/IBridgeCallback.aidl @@ -0,0 +1,7 @@ +package amirz.aidlbridge; + +interface IBridgeCallback { + oneway void onServiceConnected(in ComponentName name, in IBinder service); + + oneway void onServiceDisconnected(in ComponentName name); +} \ No newline at end of file diff --git a/services/feed/src/main/aidl/com/google/android/libraries/launcherclient/ILauncherOverlay.aidl b/services/feed/src/main/aidl/com/google/android/libraries/launcherclient/ILauncherOverlay.aidl new file mode 100644 index 000000000..48aae475e --- /dev/null +++ b/services/feed/src/main/aidl/com/google/android/libraries/launcherclient/ILauncherOverlay.aidl @@ -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); + +} \ No newline at end of file diff --git a/services/feed/src/main/aidl/com/google/android/libraries/launcherclient/ILauncherOverlayCallback.aidl b/services/feed/src/main/aidl/com/google/android/libraries/launcherclient/ILauncherOverlayCallback.aidl new file mode 100644 index 000000000..9c7202870 --- /dev/null +++ b/services/feed/src/main/aidl/com/google/android/libraries/launcherclient/ILauncherOverlayCallback.aidl @@ -0,0 +1,9 @@ +package com.google.android.libraries.launcherclient; + +interface ILauncherOverlayCallback { + + oneway void overlayScrollChanged(float progress); + + oneway void overlayStatusChanged(int status); + +} \ No newline at end of file diff --git a/services/feed/src/main/java/de/mm20/launcher2/feed/FeedCallback.kt b/services/feed/src/main/java/de/mm20/launcher2/feed/FeedCallback.kt new file mode 100644 index 000000000..e5b391eb8 --- /dev/null +++ b/services/feed/src/main/java/de/mm20/launcher2/feed/FeedCallback.kt @@ -0,0 +1,5 @@ +package de.mm20.launcher2.feed + +fun interface FeedCallback { + fun onOverlayScrollChanged(progress: Float) +} \ No newline at end of file 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 new file mode 100644 index 000000000..f4f23d0f9 --- /dev/null +++ b/services/feed/src/main/java/de/mm20/launcher2/feed/FeedConnection.kt @@ -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 + val ready: StateFlow + + 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 + } + +} \ No newline at end of file diff --git a/services/feed/src/main/java/de/mm20/launcher2/feed/FeedProvider.kt b/services/feed/src/main/java/de/mm20/launcher2/feed/FeedProvider.kt new file mode 100644 index 000000000..f429891bf --- /dev/null +++ b/services/feed/src/main/java/de/mm20/launcher2/feed/FeedProvider.kt @@ -0,0 +1,6 @@ +package de.mm20.launcher2.feed + +data class FeedProvider( + val label: String, + val packageName: String, +) \ 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 new file mode 100644 index 000000000..e498858d1 --- /dev/null +++ b/services/feed/src/main/java/de/mm20/launcher2/feed/FeedService.kt @@ -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 { + 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", + ) + } + } +} \ No newline at end of file diff --git a/services/feed/src/main/java/de/mm20/launcher2/feed/Module.kt b/services/feed/src/main/java/de/mm20/launcher2/feed/Module.kt new file mode 100644 index 000000000..b76f82462 --- /dev/null +++ b/services/feed/src/main/java/de/mm20/launcher2/feed/Module.kt @@ -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()) } +} \ No newline at end of file diff --git a/services/feed/src/main/java/de/mm20/launcher2/feed/ServiceConnection.kt b/services/feed/src/main/java/de/mm20/launcher2/feed/ServiceConnection.kt new file mode 100644 index 000000000..9733edbca --- /dev/null +++ b/services/feed/src/main/java/de/mm20/launcher2/feed/ServiceConnection.kt @@ -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?) { + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 951967c10..66e5ec4fa 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -68,3 +68,4 @@ include(":services:plugins") include(":core:devicepose") include(":core:profiles") include(":data:i18n") +include(":services:feed")