Offer the board as a launcher's feed page

80ad3c2 · Gabriel Morell · 2026-09-02 21:05

28 files +559 -14
Message
{commit_body(@commit)}

Files changed

modified clients/android/README.md
+28 −0
@@ -68,6 +68,34 @@ The connection is a foreground service with an ongoing notification. That is
68 68 what an always-open socket costs on Android; the alternative is polling, which
69 69 this deliberately does not do.
70 70
71 +## As a launcher's feed page
72 +
73 +Launchers that implement Google's "minus one" gesture -- the page to the left
74 +of the home screen -- bind a service exposing `ILauncherOverlay` and hand it
75 +their own window token. `feed/FeedOverlayService` is that service, so the board
76 +is available as the feed page with nothing else installed.
77 +
78 +The page is read-only by construction: it draws the same board the app does and
79 +offers no way into the settings. A page you can swipe onto by accident should
80 +not be able to change anything.
81 +
82 +**Launchers check the provider's signature.** Lawnchair keeps a list in
83 +`FeedBridge.initializeWhitelist`, keyed by package name and by
84 +`Signature.hashCode()` -- an unlisted package is filtered out of the picker
85 +before you ever see it. The Connection screen reports this build's hash for
86 +that reason; it differs per keystore, so a debug build's is not a release
87 +build's.
88 +
89 +Three ways past it, in the order they are worth trying:
90 +
91 +1. **A debug build of the launcher.** Lawnchair's `BridgeInfo.isSigned()`
92 + returns true unconditionally when `BuildConfig.DEBUG`, so a locally built
93 + Lawnchair accepts any provider. This is the way to test.
94 +2. **Get listed.** One line in Lawnchair's whitelist, package plus hash.
95 +3. **`pref_ignoreFeedWhitelist`.** Real, but it lives in Lawnchair's debug
96 + menu, whose own enable switch lives inside the debug menu -- so there is no
97 + way to reach it from a release build.
98 +
71 99 ## Building
72 100
73 101 ```sh
modified clients/android/app/build.gradle.kts
+3 −0
@@ -35,6 +35,9 @@ android {
35 35 }
36 36 buildFeatures {
37 37 compose = true
38 + // The launcher overlay interface is AIDL, and AGP leaves that off by
39 + // default now.
40 + aidl = true
38 41 }
39 42 testOptions {
40 43 unitTests.isIncludeAndroidResources = true
modified clients/android/app/src/main/AndroidManifest.xml
+17 −0
@@ -50,6 +50,23 @@
50 50 android:exported="false"
51 51 android:foregroundServiceType="dataSync" />
52 52
53 + <!-- The board as a launcher's page to the left of the home screen.
54 + Launchers implementing Google's "minus one" gesture look for this
55 + action and this scheme; the api version tells the client which
56 + attach call to use. -->
57 + <service
58 + android:name=".feed.FeedOverlayService"
59 + android:exported="true"
60 + android:enabled="true">
61 + <intent-filter>
62 + <action android:name="com.android.launcher3.WINDOW_OVERLAY" />
63 + <data android:scheme="app" />
64 + </intent-filter>
65 + <meta-data
66 + android:name="service.api.version"
67 + android:value="7" />
68 + </service>
69 +
53 70 <receiver
54 71 android:name=".mqtt.BootReceiver"
55 72 android:exported="true">
added clients/android/app/src/main/aidl/com/google/android/libraries/launcherclient/ILauncherOverlay.aidl
+48 −0
@@ -0,0 +1,48 @@
1 +// The launcher overlay interface, as launchers implementing the Google
2 +// "minus one" page expect to find it. The package and method order are the
3 +// contract -- both are fixed by the callers, not by us.
4 +//
5 +// Copied from Lawnchair, which carries the same declaration:
6 +// lawnchair/aidl/com/google/android/libraries/launcherclient/ILauncherOverlay.aidl
7 +package com.google.android.libraries.launcherclient;
8 +
9 +import android.view.WindowManager.LayoutParams;
10 +import com.google.android.libraries.launcherclient.ILauncherOverlayCallback;
11 +
12 +interface ILauncherOverlay {
13 +
14 + oneway void startScroll();
15 +
16 + oneway void onScroll(in float progress);
17 +
18 + oneway void endScroll();
19 +
20 + oneway void windowAttached(in LayoutParams lp, in ILauncherOverlayCallback cb, in int flags);
21 +
22 + oneway void windowDetached(in boolean isChangingConfigurations);
23 +
24 + oneway void closeOverlay(in int flags);
25 +
26 + oneway void onPause();
27 +
28 + oneway void onResume();
29 +
30 + oneway void openOverlay(in int flags);
31 +
32 + oneway void requestVoiceDetection(in boolean start);
33 +
34 + String getVoiceSearchLanguage();
35 +
36 + boolean isVoiceDetectionRunning();
37 +
38 + boolean hasOverlayContent();
39 +
40 + oneway void windowAttached2(in Bundle bundle, in ILauncherOverlayCallback cb);
41 +
42 + oneway void unusedMethod();
43 +
44 + oneway void setActivityState(in int flags);
45 +
46 + boolean startSearch(in byte[] data, in Bundle bundle);
47 +
48 +}
added clients/android/app/src/main/aidl/com/google/android/libraries/launcherclient/ILauncherOverlayCallback.aidl
+9 −0
@@ -0,0 +1,9 @@
1 +package com.google.android.libraries.launcherclient;
2 +
3 +interface ILauncherOverlayCallback {
4 +
5 + oneway void overlayScrollChanged(float progress);
6 +
7 + oneway void overlayStatusChanged(int status);
8 +
9 +}
added clients/android/app/src/main/java/io/neiam/apolloscrib/feed/FeedOverlayService.kt
+136 −0

Click to load diff…

added clients/android/app/src/main/java/io/neiam/apolloscrib/feed/FeedProvider.kt
+50 −0

Click to load diff…

added clients/android/app/src/main/java/io/neiam/apolloscrib/feed/OverlayHost.kt
+53 −0

Click to load diff…

added clients/android/app/src/main/java/io/neiam/apolloscrib/feed/OverlayPanel.kt
+185 −0

Click to load diff…

modified clients/android/app/src/main/java/io/neiam/apolloscrib/ui/BoardScreen.kt
+19 −14

Click to load diff…

modified clients/android/app/src/main/java/io/neiam/apolloscrib/ui/MainActivity.kt
+11 −0

Click to load diff…

Parents: c6f2de8