# 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).