Skip to content

feat: app-list widget - #52

Open
wassupluke wants to merge 15 commits into
mainfrom
feature/app-list-widget
Open

wassupluke wants to merge 15 commits into
mainfrom
feature/app-list-widget

Conversation

@wassupluke

@wassupluke wassupluke commented Aug 10, 2026 •

Copy link
Copy Markdown
Owner

Summary

Adds a third widget family: an app-list widget — a home-screen grid of user-selected apps, configured per instance, where taps launch the app. Follows the existing pure-data-layer + thin-Android-glue split used by the weather and alarm widgets.

What's included

  • Data layer (data/, JVM-unit-tested): AppEntry model, AppDisplayMode (icon / label / icon+label), and AppSelection encode/decode helpers.
  • Per-widget preferences (AppListStore): selected packages, display mode, font size, and text alignment keyed by appWidgetId.
  • Provider + adapter (AppListWidgetProvider): builds the RemoteViews collection, computes reflow columns, and wires per-widget launch taps.
  • Configure activity (AppListConfigureActivity) + manifest wiring, layouts, strings, and widget-info resources.
  • Refinements: inline icons that follow per-widget text alignment, stable reflow columns, tight tap targets, and config save moved to the title-bar action.

Recent cleanup

The final commit drops the separate vertical/horizontal layout toggle — icon-only is the only mode that reflows into multiple columns, so the preference was redundant. Columns are now derived purely from the display mode, with a DEFAULT_WIDTH_DP fallback so hosts that report no size still yield a real grid instead of collapsing to one column.

Testing

  • ./gradlew :app:testDebugUnitTest — green (26 tasks, fresh --rerun-tasks run).

Summary by Sourcery

Introduce a new configurable app-list widget family that displays a home-screen grid of user-selected apps and wires launch behavior per widget instance.

New Features:

  • Add an App List home-screen widget backed by a RemoteViews GridView collection that launches selected apps on tap.
  • Provide an AppListConfigureActivity for per-widget configuration of displayed apps, display mode, font size, and text alignment.
  • Introduce a pure data layer for app-list modeling and selection, including AppEntry and AppDisplayMode with JVM unit tests.
  • Add AppListStore for per-widget SharedPreferences-backed persistence of app selections and visual settings.

Enhancements:

  • Implement responsive column computation and inline icon sizing so icon-only grids reflow based on widget width and font size, while labelled layouts remain single column.
  • Align empty-state and item layouts with existing widget styling, including Hack font, shared text color, and window-inset-aware configuration UI.

Build:

  • Register the app-list widget provider, RemoteViewsService, and configure activity in the manifest with appropriate intents and appwidget metadata.

Documentation:

  • Add design and implementation-planning docs for the new app-list widget, covering architecture, behavior, and testing approach.

Tests:

  • Add JVM unit tests for app-list selection helpers, including sorting, encode/decode, and display mode defaults.

- Render per-widget text alignment on app-list items (was stored but
  never applied); also aligns the label's own text gravity.
- Apply edge-to-edge window insets to the configure screen so content
  clears the status/nav bars at targetSdk 36.
- Style the empty-state view with dynamic color and per-widget font
  size instead of a static color.
- Fall back to the placeholder icon drawable on icon-load failure
  instead of leaving a blank cell.
- Remove hardcoded SeekBar min/max in the configure layout now that
  runtime code is the single source of truth.
- Guard getViewAt against an out-of-range position instead of
  crashing.
- Item layout is a horizontal row (icon beside label) instead of stacked.
- Replace GridView.AUTO_FIT (which re-measures and collapses to one column)
  with an integer column count computed from the widget's current width;
  recompute on onAppWidgetOptionsChanged (resize).
- Fold the column count into the RemoteViews collection's adapter data URI so
  the host rebuilds the grid when columns change (it caches column layout and
  ignores setNumColumns on a plain update) — fixes reconfigure not reflowing.
- Set the tap fill-in on the icon and label views only, not the whole cell.
- Icon size scales with font (larger when icon-only, smaller inline).
- AppListConfigureActivity: replace the bottom 'Add widget' button with a
  top-right action-bar item (checkmark); save() now reads its own views.
- Widen icon-only grid cells (footprint = icon + padding + spacing) and drop
  the icon's trailing margin when there's no label, so a small-font icon grid
  no longer packs cells tight enough to clip the icons.
Icon-only is the only mode that reflows into multiple columns, so the
separate vertical/horizontal layout preference was redundant. Remove the
AppListLayout enum, its store accessors, the config-screen toggle, and its
strings. Add a DEFAULT_WIDTH_DP fallback so hosts that report no size still
yield a real icon grid instead of collapsing to one column.
@sourcery-ai

sourcery-ai Bot commented Aug 10, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Implements a new app-list widget family with a pure data layer, per-widget SharedPreferences store, RemoteViews-based collection provider/service, a configuration activity with menu-based saving, and all associated resources and docs; grid column reflow is driven by display mode and widget size instead of a separate layout toggle, with inline icons aligned per-widget text alignment and font size-driven icon sizing.

Sequence diagram for configuring and using the app-list widget

sequenceDiagram
    actor User
    participant ConfigureActivity as AppListConfigureActivity
    participant Store as AppListStore
    participant Manager as AppWidgetManager
    participant Provider as AppListWidgetProvider
    participant Service as AppListRemoteViewsService
    participant Factory as AppListFactory
    participant PackageManager
    participant TargetApp

    User->>ConfigureActivity: open via APPWIDGET_CONFIGURE
    ConfigureActivity->>PackageManager: queryIntentActivities(ACTION_MAIN, CATEGORY_LAUNCHER)
    PackageManager-->>ConfigureActivity: launchable apps
    ConfigureActivity->>Store: setSelectedPackages(id, packages)
    ConfigureActivity->>Store: setDisplayMode(id, AppDisplayMode)
    ConfigureActivity->>Store: setFontSize(id, size)
    ConfigureActivity->>Store: setTextAlign(id, Settings.TextAlign)
    ConfigureActivity->>Manager: getInstance(this)
    ConfigureActivity->>Provider: renderAppListWidgets(context, mgr, [id])

    Note over Provider,Service: Widget RemoteViews + collection set up

    Provider->>Manager: updateAppWidget(id, RemoteViews)
    Manager->>Service: startService(AppListRemoteViewsService)
    Service->>Factory: onGetViewFactory(intent)
    Factory->>Store: selectedPackages(context, id)
    Factory->>PackageManager: queryIntentActivities(ACTION_MAIN, CATEGORY_LAUNCHER)
    Factory->>AppSelection: resolveSelected(stored, installed)
    Factory->>AppSelection: sortAlphabetically(entries, locale)
    Factory-->>Manager: getViewAt(position) with fill-in EXTRA_PACKAGE

    User->>Provider: tap app icon (PendingIntent ACTION_LAUNCH_APP)
    Provider->>Provider: launchApp(context, EXTRA_PACKAGE)
    Provider->>PackageManager: getLaunchIntentForPackage(packageName)
    PackageManager-->>Provider: launch Intent
    Provider->>TargetApp: startActivity(launch)
Loading

File-Level Changes

Change Details Files
Introduce pure data-layer models and helpers for app selection and display mode, with JVM unit tests.
  • Add AppEntry model representing launchable apps and AppDisplayMode enum for icon/label/both modes, including robust fromNameOrDefault parsing with sensible defaults.
  • Implement AppSelection helpers for locale-aware alphabetical sorting, encode/decode of selected package sets, and pruning of uninstalled apps.
  • Add AppSelectionTest to verify sorting, encode/decode behavior, pruning, and display-mode defaulting.
app/src/main/java/com/wassupluke/widgets/data/AppEntry.kt
app/src/main/java/com/wassupluke/widgets/data/AppSelection.kt
app/src/test/java/com/wassupluke/widgets/data/AppSelectionTest.kt
Add AppListStore SharedPreferences wrapper to manage per-widget app-list configuration keyed by appWidgetId.
  • Persist selected app package set using AppSelection encode/decode helpers.
  • Store per-widget AppDisplayMode, font size (clamped to Settings bounds), and text alignment using enum names, with safe defaults.
  • Provide clear() utility to remove all per-widget keys when a widget instance is deleted.
app/src/main/java/com/wassupluke/widgets/AppListStore.kt
Create the app-list widget UI resources including layouts, icons, widget-info metadata, and configuration menu.
  • Add strings for app-list widget label, empty state, display mode options, selection actions, and save label.
  • Define ic_app_grid vector drawable for the widget picker and ic_check for the configure action bar.
  • Add widget_app_list.xml and widget_app_list_item.xml for the grid container and item row, including Hack font, text styling, and icon/label arrangement.
  • Define activity_app_list_configure.xml for the configuration screen with toggles for display mode and text alignment, font-size SeekBar, select-all/deselect-all buttons, and an app checklist container.
  • Add app_list_widget_info.xml appwidget-provider metadata pointing to layouts, configure activity, resize behavior, and widget category.
  • Add app_list_configure.xml menu for the title-bar "Add widget" action.
app/src/main/res/values/strings.xml
app/src/main/res/drawable/ic_app_grid.xml
app/src/main/res/drawable/ic_check.xml
app/src/main/res/layout/widget_app_list.xml
app/src/main/res/layout/widget_app_list_item.xml
app/src/main/res/layout/activity_app_list_configure.xml
app/src/main/res/xml/app_list_widget_info.xml
app/src/main/res/menu/app_list_configure.xml
Implement AppListWidgetProvider to render the app-list RemoteViews, manage column reflow based on widget size and display mode, and handle app launches and package-change broadcasts.
  • Handle onUpdate, onDeleted, and onAppWidgetOptionsChanged to re-render widgets and clean up per-widget prefs.
  • Implement ACTION_LAUNCH_APP trampoline to start the selected app via PackageManager, with logging when no launch intent is available.
  • Listen for PACKAGE_ADDED/REMOVED/CHANGED broadcasts and trigger notifyAppWidgetViewDataChanged on app_grid so the grid reflects installed/uninstalled apps.
  • Compute icon size ratios for icon-only vs inline modes and derive per-widget column count from current widget width and font size, with a DEFAULT_WIDTH_DP fallback when hosts do not report dimensions.
  • Configure RemoteViews: set RemoteViewsService adapter with a generation-specific data URI, set empty view styling based on WidgetStyle and per-widget font size, setNumColumns to the computed value, and wire a PendingIntent template for per-item fill-in intents.
  • Expose static helpers (ACTION/EXTRA constants, iconRatio, renderAppListWidgets) for reuse by the RemoteViewsFactory and configure activity.
app/src/main/java/com/wassupluke/widgets/AppListWidgetProvider.kt
Implement AppListRemoteViewsService and RemoteViewsFactory to supply per-widget grid items, with font-size-driven icon sizing and per-widget text alignment reflected in item gravity and margins.
  • Query PackageManager for MAIN/LAUNCHER activities, dedupe by package, intersect with stored selection using AppSelection.resolveSelected, and sort with AppSelection.sortAlphabetically.
  • Render each item RemoteViews with icon/label visibility based on AppDisplayMode, applying WidgetStyle text color and per-widget font size to labels.
  • Adjust item root gravity to START/CENTER/END based on AppListStore.textAlign so inline icons follow the chosen alignment.
  • Scale icon size as a multiple of font size via AppListWidgetProvider.iconRatio, using setViewLayoutWidth/Height and margins on API 31+ and fallback to bitmap scaling otherwise, with ic_app_grid as a drawable fallback when an app icon cannot be loaded.
  • Set click fill-in intents on both icon and label views so taps carry the package name to the provider's PendingIntent template.
  • Implement standard RemoteViewsFactory methods with stable IDs derived from packageName hashes.
app/src/main/java/com/wassupluke/widgets/AppListRemoteViewsService.kt
Add AppListConfigureActivity for per-widget configuration and wire it to the manifest, including title-bar saving and window-insets-aware layout padding.
  • Initialize RESULT_CANCELED and read the incoming AppWidgetManager.EXTRA_APPWIDGET_ID, bailing out if invalid.
  • Apply window insets to the configuration root view to preserve base padding while accommodating system bars.
  • Bind MaterialButtonToggleGroups for display mode and text alignment, pre-selecting values from AppListStore for reconfiguration scenarios.
  • Configure font-size SeekBar with Settings min/max, reflect current value in a label, and update text on user changes.
  • Populate the app_checklist container with CheckBoxes for all launchable apps (queried and sorted via AppSelection), pre-checking those in AppListStore.selectedPackages.
  • Wire select-all/deselect-all buttons to toggle all CheckBoxes.
  • Provide an app-bar menu with an "Add widget" action that triggers save(): persists selected packages, displayMode, textAlign, and fontSize via AppListStore, re-renders the widget via AppListWidgetProvider.renderAppListWidgets and notifyAppWidgetViewDataChanged, sets RESULT_OK with the widget ID, and finishes.
  • Register AppListWidgetProvider receiver, AppListRemoteViewsService service, and AppListConfigureActivity in AndroidManifest with appropriate intent filters, export flags, BIND_REMOTEVIEWS permission, icon, label, and meta-data linking to app_list_widget_info.xml.
app/src/main/java/com/wassupluke/widgets/AppListConfigureActivity.kt
app/src/main/AndroidManifest.xml
Add documentation for the design and implementation plan of the app-list widget to support agentic tooling and future maintenance.
  • Create a detailed implementation plan outlining tasks, files, interfaces, and testing strategy for the app-list widget.
  • Add a design spec describing architecture, decisions, data layer, storage, Android glue components, resources, testing, and out-of-scope features.
docs/superpowers/plans/2026-08-09-app-list-widget.md
docs/superpowers/specs/2026-08-09-app-list-widget-design.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The app-list query/sorting logic is duplicated between AppListConfigureActivity.loadApps() and AppListFactory.onDataSetChanged(); consider extracting a shared helper to keep selection ordering and filtering behavior in sync over time.
  • The DEFAULT_WIDTH_DP fallback in computeColumns is hard-coded to match app_list_widget_info.xml minWidth; consider deriving this from resources or a single shared constant so future layout tweaks don’t silently desync column computation from the widget-info.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The app-list query/sorting logic is duplicated between `AppListConfigureActivity.loadApps()` and `AppListFactory.onDataSetChanged()`; consider extracting a shared helper to keep selection ordering and filtering behavior in sync over time.
- The `DEFAULT_WIDTH_DP` fallback in `computeColumns` is hard-coded to match `app_list_widget_info.xml` minWidth; consider deriving this from resources or a single shared constant so future layout tweaks don’t silently desync column computation from the widget-info.

## Individual Comments

### Comment 1
<location path="app/src/main/java/com/wassupluke/widgets/data/AppSelection.kt" line_range="9-11" />
<code_context>
+/** Pure, JVM-testable helpers for the app-list widget. No Android APIs. */
+object AppSelection {
+
+    /** Locale-aware, case-insensitive-primary alphabetical sort by label. */
+    fun sortAlphabetically(entries: List<AppEntry>, locale: Locale): List<AppEntry> {
+        val collator = Collator.getInstance(locale)
+        return entries.sortedWith(Comparator { a, b -> collator.compare(a.label, b.label) })
+    }
</code_context>
<issue_to_address>
**issue (bug_risk):** Collator configuration doesn't match the "case-insensitive-primary" comment.

The implementation doesn’t currently enforce the documented "case-insensitive-primary" behavior. `Collator.getInstance(locale)` may use a strength that distinguishes case or diacritics depending on the locale. Please explicitly set `collator.strength = Collator.PRIMARY` (and likely `collator.isCaseLevel = false`) so the sort order consistently matches the comment across locales.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +9 to +11
/** Locale-aware, case-insensitive-primary alphabetical sort by label. */
fun sortAlphabetically(entries: List<AppEntry>, locale: Locale): List<AppEntry> {
val collator = Collator.getInstance(locale)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Collator configuration doesn't match the "case-insensitive-primary" comment.

The implementation doesn’t currently enforce the documented "case-insensitive-primary" behavior. Collator.getInstance(locale) may use a strength that distinguishes case or diacritics depending on the locale. Please explicitly set collator.strength = Collator.PRIMARY (and likely collator.isCaseLevel = false) so the sort order consistently matches the comment across locales.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct — Collator.getInstance() defaults to TERTIARY, which distinguishes case, contradicting the KDoc. Set strength = Collator.PRIMARY so the sort ignores case and accents as documented.

@wassupluke

Copy link
Copy Markdown
Owner Author

Overall comments:

  1. Duplicated query/sort logic — extracted a shared PackageManager.launcherApps() helper so both loadApps() and onDataSetChanged() run the same MAIN/LAUNCHER query, dedup, and label resolution; each still applies its own filter/sort on top. Kept it in the Android layer (touches PackageManager) rather than the pure data/ package.

  2. DEFAULT_WIDTH_DP hard-coded to the widget-info minWidth — leaving as-is. There is no clean shared home: the value lives in XML (framework-consumed) and Kotlin. AppWidgetProviderInfo.minWidth is available at runtime but in px, needing density conversion, which is more fragile than a documented constant. The coupling is already called out in the comment above it.

Icon-only widgets now render as grid rows (one factory item per row of
up to MAX_COLUMNS icons) laid out by the new AppGridLayout, so a partial
last row honors the widget's START/CENTER/END alignment instead of
left-packing. The collection is a single column; each row lays out its
own icon columns with per-icon tap targets. Extract the duplicated
launcher-query logic into PackageManager.launcherApps(), and sort with a
PRIMARY-strength collator for case/accent-insensitive ordering.
Give the alarm widget its own text-alignment setting (defaulting to the
weather value so upgrades keep the shared alignment). WidgetStyle.gravity
now takes an explicit align, and each provider passes its own. Rebuild
the settings screen into Material cards grouped by Appearance / Weather /
Alarm / Permissions, replacing the single status line with per-permission
check/cross rows, and extract a shared bindAlignGroup() helper.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant