Files
EasyLauncher/WIDGET_HOSTING_TASK.md
jonas a42f913875
Some checks failed
Android Main Branch CI / Build, Sign & Upload (push) Has been cancelled
Update CHANGELOG.md / changelog (push) Has been cancelled
Validate Gradle Wrapper / Validation (push) Has been cancelled
Android Release CI / Build, Sign & Release (push) Has been cancelled
refactor: use shared met-weather library for home-screen MET weather (v0.3.7)
- Replace internal MetApiService/MetForecastResponse + Retrofit MET fetch in
  AppHelper with dk.haugesenspil:met-weather (Gitea maven).
- fetchMetWeather now delegates to MetWeatherClient; 15-min cache kept.
- Drop MetApiService.kt and MetForecastResponse.kt.
release build v0.3.7
2026-08-20 12:58:52 +02:00

215 lines
11 KiB
Markdown

# Task: Add home-screen app-widget hosting to EasyLauncher
You are extending the EasyLauncher fork at `~/sources/EasyLauncher` (branch `main`).
Goal: let the user place **real Android app widgets** (in particular K-9 Mail's
"K-9 Unread" widget) on the launcher home screen, rendered live and persisted
across reboots.
Read `CLAUDE.md` in the repo root first — it is the fork's operating manual
(build, signing, on-device testing, quirks). Follow it.
## Why (background)
The launcher shows a red dot on app icons for active notifications
(`NotificationBadgeService`). K-9 Mail cancels its new-mail notification when
the app is opened, so the dot disappears even when unread mail remains. There
is no Android API to read another app's unread count. The clean, official
workaround is K-9's own **"K-9 Unread" widget**, which shows a persistent
unread count that only goes to zero when the mail is read. EasyLauncher
currently cannot host third-party app widgets (its "Widgets" page,
`WidgetFragment.kt`, is only self-drawn battery/clock widgets). This task adds
real app-widget hosting.
Scope guard: do NOT implement root-based database reading, IMAP polling, or
changes to K-9. Widget hosting only.
## Environment
- Repo: `~/sources/EasyLauncher`, branch `main`, origin = Gitea `jonas/EasyLauncher`.
No `upstream` remote. Commit locally with conventional messages (see git
history: `feat: ...`, `fix: ...`, `docs: ...`). Do not push, tag, or create
a Gitea release unless the user asks.
- Stack: Kotlin, minSdk 24, compileSdk/targetSdk 36, Java 17, Hilt (kapt),
Room, ViewBinding (+ DataBinding enabled), two product flavors
(`withInternet` / `withoutInternet`), R8 enabled in release.
- Build (offline, JDK 21):
```bash
export JAVA_HOME=~/jdk21
./gradlew :app:compileWithInternetReleaseKotlin :app:compileWithoutInternetReleaseKotlin --offline
./gradlew :app:assembleWithInternetRelease :app:assembleWithoutInternetRelease --offline
```
Outputs: `app/build/outputs/apk/{withInternet,withoutInternet}/release/`.
- Signing (for installing on the phone):
```bash
PASS=$(cat ~/android-keystores/easylauncher-release.pass)
~/android-sdk/build-tools/36.0.0/apksigner sign --v4-signing-enabled true \
--ks ~/android-keystores/easylauncher-release.jks --ks-key-alias easylauncher \
--ks-pass "pass:$PASS" --key-pass "pass:$PASS" \
--out /tmp/EasyLauncher-widgethost.apk \
app/build/outputs/apk/withInternet/release/app.easy.launcher_v0.3.6-Release.apk
```
Do NOT bump versionCode/versionName (current 36 / 0.3.6) and do NOT write
into `dist/` — this is a feature build, not a release. Installing with the
same versionCode works: `adb install -r /tmp/EasyLauncher-widgethost.apk`.
- Phone: Unihertz Titan 2 Elite, adb serial `TITAN20000043119`, rooted
(Magisk). `app.easy.launcher` (signed release) is installed and is the
default home app. K-9 Mail (`com.fsck.k9`) is installed with 3 accounts.
Device facts: after a reboot the phone is CE-locked until the user enters
the PIN — if something "doesn't work" right after a reboot, ask the user to
unlock before debugging.
- Screenshots: `adb exec-out screencap -p > /tmp/s.png`; UI dump:
`adb shell uiautomator dump /sdcard/ui.xml && adb pull /sdcard/ui.xml`.
Logs: `adb logcat | grep -i widgethost`.
## What to build
### 1. Widget host manager (the core plumbing)
Create a Hilt `@Singleton` manager (e.g.
`app/src/main/java/com/github/droidworksstudio/launcher/service/WidgetHostManager.kt`,
or `helper/` if that fits better — follow existing package conventions) that
wraps:
- `AppWidgetHost(context, hostId)` with a unique constant hostId.
- `AppWidgetManager.getInstance(context)`.
Responsibilities:
- **`startListening()` / `stopListening()`** — delegate to `AppWidgetHost`.
- **Restore placed widgets** — re-create `AppWidgetHostView`s from persisted
widget ids after process death / reboot:
- For each stored `(appWidgetId, component)`:
- `appWidgetManager.getAppWidgetInfo(id)`; if null (unbound/removed),
drop the entry (`host.deleteAppWidget(id)` is not needed for missing
info; just clear the record).
- Else `val view = host.createView(context, id, info)` and add to the
home widget container. `createView` binds the widget; the view updates
automatically while the host is listening.
- **`requestAddWidget(component, callback)`** — the add flow:
1. `val id = host.allocateAppWidgetId()`
2. `if (!appWidgetManager.bindAppWidgetIdIfAllowed(id, component))` →
the app is not the active launcher; notify the user
("Set Easy Launcher as the default home app") and abort.
3. If the provider has a configure activity
(`providerInfo.configure != null`), launch it with
`appWidgetManager.startAppWidgetConfigureActivityForResult(activity, id,
REQUEST_ADD_WIDGET, Bundle())` (4-arg version, API 17+, fine for
minSdk 24; verify the exact signature against the local SDK — compileSdk
36 sources are available). Resume on `onActivityResult`: `RESULT_OK` →
continue; otherwise `host.deleteAppWidget(id)` and abort.
4. Create and return the bound `AppWidgetHostView`
(`host.createView(context, id, info)`).
- **`removeWidget(view, id)`** — remove view from container,
`host.deleteAppWidget(id)`, clear the persisted record.
- **Persistence** — store placed widgets in SharedPreferences (follow
`PreferenceHelper` patterns; JSON string of `id|component` pairs, key e.g.
`HOSTED_WIDGETS`). Room is acceptable but prefs match the fork's style.
- Keep it dependency-light and testable. Log with tag `WidgetHost`.
### 2. Home screen widget area
`fragment_home.xml` has a vertical `mainView` LinearLayout:
`blockView` (battery/clock/date/weather) on top, `appListTouchArea`
(RecyclerView grid) below.
- Insert a full-width widget container between `blockView` and
`appListTouchArea`, e.g. a `FrameLayout` (or vertical `LinearLayout` for
stacking) `android:id="@+id/widgetHostArea"`, `visibility="gone"` when no
widgets are placed. Keep margins consistent with the rest of the screen
(20dp horizontal, mirroring `blockView`).
- Wire in `HomeFragment.kt`:
- `onStart`: `widgetHostManager.startListening()`; restore placed widgets
into the container. `onStop`: `stopListening()`.
- Handle the add result (`onActivityResult` REQUEST_ADD_WIDGET) by inserting
the returned `AppWidgetHostView`, then persist and show the container.
- Removing: long-press a hosted widget → confirm dialog → remove.
- "Add widget" affordance when the feature is enabled: a small "+" in the
widget area when empty (and/or a settings entry — see below). Do not
break existing swipe/gesture handlers (`touchArea`, `appListTouchArea`
forwards vertical drags — the new area sits outside `appListTouchArea` so
no conflict, but verify touches on the widget still work).
- If the widget area is `gone` when empty, make sure the home grid layout
does not jump when the first widget is added.
### 3. Widget picker
A bottom sheet or `AlertDialog` listing installed widget providers:
- `appWidgetManager.getInstalledProviders()` (deprecated on API 33+ — use the
profile variant `getInstalledProvidersForProfile(UserHandle.CURRENT)` on
API 33+, guard with `Build.VERSION`). The app already has
`QUERY_ALL_PACKAGES`, so all providers are visible.
- Each row: `providerInfo.loadIcon(context, density)` + label; sort by label.
- Tapping a row → `widgetHostManager.requestAddWidget(...)` flow above.
- Opening the picker: add a settings entry (see 4). Optionally also a
long-press on empty home space if the launcher has no competing handler —
check first; if home long-press is already used, use settings only.
### 4. Settings
Follow the existing "Notification Dots" toggle pattern
(`SHOW_NOTIFICATION_DOTS` in `PreferenceHelper` +
`SettingsFeaturesFragment`):
- New pref `SHOW_HOME_WIDGETS` (default OFF).
- Toggle: "Home screen widgets".
- When ON, expose "Add widget" (opens the picker) — e.g. a secondary row or
the "+" on the home widget area.
- Add string resources in `values/strings.xml` only (the app is on Crowdin —
do not invent translations in other locales).
- Pref caveat from CLAUDE.md: don't re-push the value from
`observeUserInterfaceSettings` in a way that clobbers the on-disk value
(see the `SHOW_NOTIFICATION_DOTS` note).
### 5. Permissions
None new. Do NOT add `android.permission.BIND_APPWIDGET` (host role does not
need it). `QUERY_ALL_PACKAGES` already covers provider enumeration.
### 6. Documentation
Update `CLAUDE.md` in the repo root with a short section on the widget host
(service/manager name, pref key, how to place/remove a widget via adb if
useful, known quirks), matching the fork's existing documentation style.
## Acceptance criteria (verify on the phone)
1. Both flavors compile offline.
2. Signed `withInternet` APK installs in place over the current
`app.easy.launcher` (same signature, same versionCode 36) — `adb install -r`.
3. Settings → Features shows the new "Home screen widgets" toggle; enabling
it surfaces "Add widget".
4. The picker lists "K-9 Unread" (and other installed widgets). If K-9's
unread widget is missing, check `adb shell dumpsys package com.fsck.k9 |
grep -B2 -A6 UnreadWidgetProvider` — K-9's widget receivers are enabled
programmatically (`android:enabled` from a manifest bool); if disabled,
find the K-9 setting that enables home-screen widgets (ask the user if
unclear) or `adb shell pm enable
com.fsck.k9/com.fsck.k9.provider.UnreadWidgetProvider`.
5. Placing the K-9 Unread widget runs K-9's configuration (account picker),
then the widget renders on the home screen showing a live unread count
(currently 11 unread across the user's 3 accounts — a per-account widget
shows that account's count). Reading mail in K-9 decreases it; the dot
question is irrelevant here because the widget keeps counting until 0.
6. Widget survives: app restart (force-stop + relaunch via HOME), and a phone
reboot (after the user re-enters their PIN — CE-lock).
7. Long-press → remove works; after removal the area is hidden again and the
widget id is gone from prefs.
8. Regression: notification dots still appear (see CLAUDE.md validation
trick), home grid/weather/drawer unchanged, no crash on rotation.
9. Screenshot proof: `adb exec-out screencap -p > /tmp/widget-proof.png`
showing the K-9 Unread widget on the home screen. Keep the file and report
its path.
10. No leaked `AppWidgetHostView`s: `stopListening` on `onStop`, views
removed on removal. Watch `logcat` for host-related warnings.
## Deliverables
- Working feature per above, committed as `feat: host app widgets on home
screen (K-9 unread widget)` (or similar) — commit locally, do not push.
- `CLAUDE.md` updated.
- Verification evidence: build logs, screenshot path, list of things tested
on the phone and their outcome.
Report back concisely: what you built (files + key decisions), what you
verified on the phone, and anything the user must do (e.g. K-9 widget
setting, PIN unlock after reboot).