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

11 KiB

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):
    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):
    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 AppWidgetHostViews 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 AppWidgetHostViews: 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).