# Widget layout and editing design ## Context and goals Index Launcher has up to five finite widget pages to the left of its apps page. Wide windows show one widget page on the left and pin the current app view on the right; folding returns to apps while remembering the selected widget page. It has no app-icon grid, and widgets should not require one. **Implemented on home and left-side pages:** a scrolling two-column flow of full-width and paired half-width widgets, order and size persistence, provider-informed default dimensions, and page-local Edit mode with edge handles for height/width, reorder controls, and removal confirmation. `WidgetPlacementRepository` migrates existing ID-only entries to full-width placements without rebinding. Hiding a left-side page retains its widgets; home widgets use a separate stable key independent of the page count. `MainActivity` owns the `AppWidgetHost` lifecycle, gallery selection/binding/configuration flow, and widget-ID deletion. Adding uses the **app-grouped widget gallery** described below. Home keeps widgets above favorites in one bottom-aligned, vertically scrolling list. The shared model is a scrollable **two-column flow** on both the apps page and the left-side pages. A widget can occupy one column (half-width) or span both (full-width). Two half-width widgets sit side by side with a visible gap. Users can choose a widget's height, with a sensible provider-informed initial height. On the apps page, widgets precede the bottom-anchored favorites in the same vertical list; the two never overlap. Layouts are locked in normal use; Edit mode provides sizing, placement, adding, and removing. On home, the Homescreen Widgets tile at the end of all apps opens Edit mode; left-side pages retain long-press on widget content to edit and on empty page space to open the gallery. Drag-to-reorder remains a future refinement. ## Status and scope The layout/editor/gallery described here are implemented unless explicitly labeled as a gap or future refinement. Constraint explanations and some availability/configuration scenarios still need work; see [Remaining work](#remaining-work) and the [active backlog](../TODO.md). Design goals below must not be mistaken for verified device behavior. ## Widget sizing contract - The host chooses a widget's outer rectangle; the provider renders within it. There is no universal provider-reported "ideal size." `AppWidgetProviderInfo.minWidth` and `minHeight` are default-size hints; `minResizeWidth`/`minResizeHeight`, optional maximum resize sizes, and `resizeMode` describe supported resizing. On Android 12+, `targetCellWidth`/`targetCellHeight` describe a desired footprint **in a launcher's grid cells**, not dp. Index Launcher has no such cells, so do not treat them as physical dimensions or fabricate a grid conversion. Use the declared default dimensions and actual available column widths for the initial choice, while acknowledging that third-party providers may not adapt gracefully. - Provider dimension fields are returned in **px**, although their XML values are commonly declared in **dp**. Convert with the current density before comparing with Compose dp dimensions; never treat the integer field as dp. Give a newly added widget the smallest supported width mode that accommodates its default width (half if it fits, otherwise full), and start near its declared default height. Missing provider info or an unusable default height shows the existing unavailable state, using a 140 dp fallback rectangle to keep the placement reachable. Missing/unusable default width falls back to full width. **Gap:** if even full width cannot meet the requested width, the host currently uses the available width without a dedicated incompatibility warning; surfacing that constraint remains work. - Allow the user to increase a widget's outer height even if the provider does **not** advertise vertical resizing; this can help with clipped content, but does not guarantee the provider will reflow internally. For such widgets, do not shrink below their default height, and explain that the content may not adapt. When vertical resizing is advertised, honor the provider's declared minimum/maximum resize range. Only offer half-width when the provider can fit or supports resizing narrow enough. There should be no blanket 360 dp height cap: tall widgets may extend beyond the viewport and the page must scroll. - Pass the **measured area of the `AppWidgetHostView`**, not the page/card dimensions or a guessed cell size, to `AppWidgetHostView.updateAppWidgetSize(Bundle, List)` in dp when it changes. The host view's framework-managed padding is accounted for by that API; don't subtract it twice. A single current size is sufficient for the initial design, but refresh it on user resize and changes to width/density/window configuration. Providers, including responsive widgets, decide how to draw inside that space. Avoid sending redundant size updates during recomposition. ## Flow and placement decisions Let `W` be the width available **inside safe drawing insets and page horizontal padding**, and `G` the inter-column gap. Each half-width slot has width `(W - G) / 2`; a full-width slot has width `W`. Do not calculate from physical screen width or forget the gap. Settings → Appearance → Widget spacing independently controls outside padding, inter-column gap, and vertical row spacing (0–40 dp each), shared by home and left-side pages. Defaults remain 20, 12, and 16 dp respectively. `WidgetSpacingRepository` persists these values in appearance preferences; the editor recomputes slot widths and half-width suitability when they change. Internal provider/framework padding is unchanged. Persist an ordered sequence of widgets on each stable page, with each widget's preferred width mode (`AUTO`, `HALF`, or `FULL`) and optional user-selected height in dp. `AUTO` chooses half if the provider's default width fits; legacy widgets are `FULL`. Pack widgets in that order: a full-width widget takes a row; two consecutive half-width widgets share a row; an unpaired half-width widget takes the left slot, leaving the right slot empty until another half-width widget follows. Row height is the taller of its two widgets, and the next row begins beneath it. Each widget keeps its own height, so an uneven pair can leave blank space under the shorter one; **do not use masonry packing** that changes reading order. Current Move up/down buttons let a user put two chosen half-width widgets next to each other without dragging. Home widgets use the stable **`home` page key**, distinct from `left-1` and available even when the widget page count is zero. Old right-side workspace placements are intentionally not migrated. Hosting, packing, resizing, and editing are shared across pages. The gallery/binding/configuration flow accepts either key without requiring a `HomePage.Widgets` ordinal; hiding a left-side page does not hide home widgets. If a saved half-width preference no longer fits after a display/window change, lay it out full-width **temporarily** and reflow in saved order; keep the user's half-width preference so it can return when space becomes available. Never place widgets offscreen or discard saved placements to resolve a constraint. If the page is too narrow even for full width, the placement remains reachable in the vertically scrolling page; an explicit provider-incompatibility explanation is still pending. Widget IDs remain attached to their page even when that page is hidden. The persisted `heightDp` represents a **user choice**. For an untouched widget, derive the default anew from provider metadata on the current configuration instead of treating an old calculated pixel size as a user preference. The editor rereads provider metadata when its widget IDs or window configuration change. If a saved user height is outside the current supported range, rendering clamps it without rewriting the saved choice. **Gap:** explaining that adjustment in Edit mode is not implemented; provider/profile availability changes without an ID/configuration change also need validation. Migrate existing ordered ID lists to records in the same order, with full-width preference and unset height, preserving the current page keys and existing widget bindings. ## Home page: widgets above favorites In **favorites mode**, home widgets use the same two-column rows as the left-side pages and are the first item in the favorites `LazyColumn`. The widget region is a non-scrolling, uncapped `Column`: **widgets and favorites scroll vertically together**. When the combined content fits, it is bottom-aligned with widgets directly above the favorites; when it overflows, the whole list scrolls from the top. Oversized widgets retain their provider-informed/user-selected heights without reserving a separate favorites viewport. With no home widgets and Edit mode closed, favorites keep their original full-height layout. The Homescreen Widgets tile opens a compact Add/Done editor even when there are no home widgets; removing the last widget leaves that editor open until Done or Back. The editor controls scroll with the widget region. Verify tall widgets, editing controls, and favorites remain reachable in short windows and at large font/display settings. Home widget width is measured from the full safe-area width excluding page padding, **not** the full-height A–Z rail touch area. The rail overlays the widget region, so taps on the widget's extreme right edge reach the rail instead; favorites and all apps still reserve that rail width. A full-width widget on home can thus differ in width from a left-side-page widget. In two-pane mode, measure each widget from its pane's constraints, never the whole unfolded display. Preserve rail scrubbing and horizontal paging. Swiping up or overscrolling favorites does not open search; use the search button in all apps. All-apps and search modes replace the favorites composition on the apps page: temporarily hide home widgets in those modes without moving or deleting them. Android’s Home action and selecting `★` reset the shared list to index 0 with zero scroll offset, including when already in favorites or partway through an oversized widget. Back, ordinary resumes, and pane changes preserve its scroll state. ## Edit mode and accessibility Outside Edit mode, widgets render directly without an Index Launcher background or clipping container and get normal touch interaction; no resize controls or persistent Remove buttons take their space. The left-side pages have no visible title, toolbar, or page-wide wallpaper scrim. There, long-press on empty space opens the gallery (an empty page also offers a visible Add widget action), and long-press on widget content enters page-local Edit mode even if provider child views handle touch: `LongPressWidgetFrame` observes the press before forwarding it and cancels it on movement so ordinary widget taps and scrolling continue to work. On home, long-press does not add or edit; enter Edit mode from the all-apps tile instead. In Edit mode, a translucent surface and outline show widget bounds behind the controls. A bottom bar provides Add widget and Done; Back also exits Edit mode without deleting widgets. In Edit mode, drag the bottom edge handle to resize height continuously within provider constraints; the preview updates during dragging and saves the chosen dp height on release. The side edge handle changes the discrete half/full-width mode after a 32 dp directional drag, respecting minimum provider width and layout direction. Size labels appear during dragging, and the existing provider warning explains that non-resizable widget contents may not reflow. Per-widget arrow controls preserve Move up/down ordering, and a trash control asks for confirmation before deleting the widget and releasing its host ID. Accessible page actions provide Add/Edit without long-pressing, and handles expose stepwise height and width actions without requiring drag. Keep sufficient touch targets and avoid claiming page scroll or horizontal swipe gestures while a handle is in use. ## App-grouped widget gallery and work profiles `MainActivity.addWidget()` opens `WidgetGalleryScreen` rather than `ACTION_APPWIDGET_PICK`, retaining `WidgetPageScreen`'s layout, `WidgetPlacementRepository`, and the host's configuration and deletion paths. `WidgetCatalogRepository` lists **widget providers**, not just apps with launchable activities: an app can publish widgets without an activity in `InstalledAppsRepository`. It queries `AppWidgetManager.getInstalledProvidersForProfile(user)` for the current user and each accessible managed profile from `LauncherApps.profiles`, follows `InstalledAppsRepository`'s managed-profile check, and skips `UserManager.isQuietModeEnabled(user)`. Private/other profiles are excluded; no `QUERY_ALL_PACKAGES` permission is needed. The existing `APPWIDGET_UPDATE` package query helps resolve provider resources. The catalog refreshes on resume and package/profile availability changes, and provider availability is rechecked on selection. Groups use **(profile serial number, package name)**, so personal and work installations never collapse into one app card; individual tiles use that group and provider `ComponentName`. App sections show an icon, name, and widget count; expanding a section reveals separately labeled preview tiles. `LauncherApps.getApplicationInfo(package, 0, user)` supplies the app's label/icon where available; `AppWidgetProviderInfo.loadLabel()` and `loadDescription()` supply widget text, and `getProfile()` identifies the provider's profile. Work app icons are badged with `PackageManager.getUserBadgedIcon()` and work groups are labeled. Provider-only apps do not go through `AppIconRepository.loadIcon(AppEntry)`, which requires a launchable activity. Tiles show suggested dimensions from provider dp-converted `minWidth`/`minHeight` without inventing grid-cell dimensions. Only home-screen-category providers appear. Visible tiles request `AppWidgetManager.getWidgetPreview(provider, profile, WIDGET_CATEGORY_HOME_SCREEN)` (public on the project's API 35+ floor). If no generated `RemoteViews` exists or inflation fails, the gallery tries `AppWidgetProviderInfo.previewLayout` as a `RemoteViews` for **personal** widgets, then `loadPreviewImage(context, density)`, then a badged icon or text placeholder. A manually constructed `RemoteViews(packageName, layoutId)` does not identify a work profile and could resolve a personal installation of the same package, so work tiles skip that fallback. Previews render in bounded, noninteractive containers; they are **not** live widgets or allocated widget IDs. Broken/missing cross-package or cross-profile preview resources show a labeled fallback and log a diagnostic without blocking an otherwise valid provider. Lazy tiles retain previews only while composed; work previews are not saved to disk and are removed when the catalog refreshes for a paused or inaccessible profile. `MainActivity` allocates an `AppWidgetHost` ID **only after** the user selects a tile, remembering the page key, provider, and profile through activity recreation. It tries `bindAppWidgetIdIfAllowed(id, profile, provider, options)` with `OPTION_APPWIDGET_HOST_CATEGORY` set to `WIDGET_CATEGORY_HOME_SCREEN`; if that returns false, it launches `ACTION_APPWIDGET_BIND` with `EXTRA_APPWIDGET_ID`, `EXTRA_APPWIDGET_PROVIDER`, `EXTRA_APPWIDGET_PROVIDER_PROFILE`, and `EXTRA_APPWIDGET_OPTIONS` for system consent. Declaring `BIND_APPWIDGET` alone would not grant signature-level binding privileges. On approval, the returned/bound ID and `getAppWidgetInfo(id)` are checked against the chosen provider and profile; then `startAppWidgetConfigureActivityForResult()` handles provider configuration if required. Placement is persisted **only after** successful binding and configuration. Cancellation, denial, provider/profile disappearance, failed launch, or invalid results release the pending ID; saved widgets are retained while a work profile is merely paused. Work apps appearing in `LauncherApps` **does not** mean their widgets can be hosted on the personal launcher. The managed profile's owner/admin must allowlist widget-provider packages for cross-profile hosting using `DevicePolicyManager.addCrossProfileWidgetProvider()` (or its set equivalent); Index Launcher cannot grant that policy. Enumeration yields candidates; binding is the final authority, and consent does not override an admin restriction. **Validation gap:** the prior emulator checks did not cover a managed profile. Test enumeration, previews, consent, and configuration on a managed-profile device with both allowlisted and denied providers, as well as same-package personal/work installs, provider-only apps, quiet-mode transitions during selection/binding, and rotation. References: [AppWidgetManager APIs](https://developer.android.com/reference/android/appwidget/AppWidgetManager), [widget previews](https://developer.android.com/develop/ui/views/appwidgets/previews), [hosting and binding](https://developer.android.com/develop/ui/views/appwidgets/host), and [work widget allowlisting](https://developer.android.com/reference/android/app/admin/DevicePolicyManager#addCrossProfileWidgetProvider(android.content.ComponentName,%20java.lang.String)). ## Remaining work - **Constraint feedback:** explain when provider minimum width exceeds the available full-width slot and when a saved height has been clamped. Current Edit mode only warns about unsupported half-width and potentially non-reflowing content. - **Validation:** managed-profile enumeration/preview/binding/configuration, pause/disappearance handling, provider metadata refresh, activity recreation, large font/display settings, and narrow/rotated windows still need device coverage. These remain active in [TODO](../TODO.md); existing JVM layout/serialization tests do not exercise Android hosting. - **Future refinement, not an initial-release commitment:** drag-to-reorder. Move up/down arrows already provide persistent ordering; any drag design must preserve row reading order, scrolling, paging, and accessible alternatives. ## Display settings and verification Physical pixels, dp, and sp serve different purposes. Display size changes effective density and the **available dp width**, so half-width suitability and row packing must be recomputed from current constraints. Font size changes text sizing (sp), including nonlinear scaling on modern Android; it does not imply a proportional change to dp layout. The provider owns its internal text/layout, but Index Launcher's headers and Edit controls must wrap and remain usable with large fonts. Respect safe drawing insets and test portrait/landscape, narrow/wide windows, and configuration changes without persisting px coordinates. Verify: two half widgets share a row with the specified gap and independent heights; full-width widgets break pairs; odd half widgets remain reachable; saved order and explicit heights survive restart and hidden/revealed pages; legacy widget IDs migrate without lost bindings; provider size options reflect the measured host view after add, resize, rotation, and display-size changes; missing-provider widgets show the unavailable state and oversized/incompatible widgets remain reachable (dedicated width/clamping feedback is pending); Edit mode works through touch, keyboard/accessibility controls, and Back; normal widget interaction still works outside Edit mode. On home, test empty/short/overflowing favorites, no/few/oversized widgets, shared scrolling without overlap, zero widget pages, fold/unfold transitions, all-apps/search round trips, rail hits, horizontal swipes, Home/`★` resetting scroll to the top, and upward overscroll never opening search. Check widget padding/gaps at zero, defaults, and maximum, including half-width suitability and provider size updates. Test with providers of different default sizes and resize modes, and with large font and display settings. Android references: [Build a widget host](https://developer.android.com/develop/ui/views/appwidgets/host), [Widget sizing and responsive layouts](https://developer.android.com/develop/ui/views/appwidgets/layouts), [AppWidgetProviderInfo](https://developer.android.com/reference/android/appwidget/AppWidgetProviderInfo), and [AppWidgetHostView.updateAppWidgetSize](https://developer.android.com/reference/android/appwidget/AppWidgetHostView#updateAppWidgetSize(android.os.Bundle,%20java.util.List%3Candroid.util.SizeF%3E)).