Skip to content

Customizable launcher widget#22

Open
jcalado wants to merge 23 commits into
Meshcore-Portugal:fix/performancefrom
jcalado:feat/customizable-widget
Open

Customizable launcher widget#22
jcalado wants to merge 23 commits into
Meshcore-Portugal:fix/performancefrom
jcalado:feat/customizable-widget

Conversation

@jcalado
Copy link
Copy Markdown

@jcalado jcalado commented May 7, 2026

Summary

Adds per-instance widget customization plus a few visual upgrades:

  • Configuration activity launched on widget add and via Reconfigure (Android 12+). Material 3 UI: Material toolbar/buttons/checkboxes/switches, MaterialAlertDialog for the RGB picker, cutout-aware insets.
  • Buttons: pick 3–5 from a pool of 7 (SOS, Anúncio, Ligar, Chats, Mapa, Plano 3-3-3, Telemetria), drag to reorder, per-button "Mostrar etiqueta" switch.
  • Accent color: 11 preset swatches plus a custom HSV picker. Online status dot, active connect button, low-battery indicator and signal bars all inherit the accent.
  • Signal indicator mirroring the app's appbar — 4 stepped bars derived from best LoRa SNR over the last 5 minutes.
  • Layout polish: signal bars + battery moved into the header, metric strip is now contacts · channels · timestamp, status dot and active connect button keep their oval shape under a non-default accent (uses setBackgroundTintList instead of setBackgroundColor).
  • Two new deep-link actions: meshcore-widget://nav/apps/plan333 and …/telemetry mapped through WidgetAction and _handleWidgetAction.

Bug fixes shipped along the way

  • Warm-start widget taps land on the wrong screenFlutterActivity.onNewIntent was pushing the widget URI into flutter/navigation, which triggered GoRouter's scheme redirect to /channels after our dispatcher had already navigated. MainActivity.onNewIntent now manually routes meshcore-widget URIs to plugins only and skips the deep-link push.
  • Circular dep crashbestSignalSnrProvider watches connectionProvider, which is the StateNotifier that calls _pushWidget, so reading it from there raised CircularDependencyError on every channel-info packet. Inlined the 5-minute best-SNR scan in ConnectionNotifier.
  • Config activity crashing on launch — theme conflict from Theme.AppCompat.DayNight supplying an action bar while the activity also called setSupportActionBar(toolbar); switched to Theme.Material3.DayNight.NoActionBar.

Files

  • New: WidgetConfigActivity.kt, WidgetConfig.kt data model, configuration layouts, neutral tintable drawables, plan333 / telemetry / drag-handle vector icons.
  • Modified: MeshCoreWidgetProvider.kt (5 generic slots, accent tinting, signal bars, onDeleted cleanup), widget_meshcore.xml, widget_info.xml (configure + reconfigurable), AndroidManifest.xml (Material theme, configure activity), MainActivity.kt (intent routing fix), lib/services/widget_service.dart (signalBars + new actions), lib/main.dart (action dispatcher cases), lib/providers/parts/connection_notifier.dart (inline SNR), build.gradle.kts (recyclerview, flexbox, appcompat, material).

Test plan

  • Cold-start: tap each widget button, app lands on the matching screen
  • Warm-start: tap a different widget button repeatedly while the app is backgrounded — each tap routes to the correct screen
  • Reconfigure: long-press → Reconfigure pre-populates current selection / accent
  • Removal cleanup: removing a widget clears its widget_<id>_* keys; new widget starts at defaults
  • Custom RGB picker persists across setBackgroundTintList so the dot, active button, low-battery icon and signal bars all recolor together
  • Manually validate the SOS button on a connected radio (existing flow unchanged but worth a smoke test on this branch)

jcalado added 23 commits May 7, 2026 19:16
…nect button

- Status dot ONLINE swaps to brand orange (was emerald green)
- Active connect button forces brand orange even under Material You
- LIGADO label and power glyph recolor when connected
- Battery icon/text gain semantic tinting (red ≤15%, orange ≤40%)
- Replace mangled broadcast vector with clean Material campaign icon
- Add tonal action button backgrounds with Material You light/dark variants
Theme.AppCompat.DayNight already supplies a window action bar, so calling setSupportActionBar(toolbar) inside the activity raised IllegalStateException at start. Use the NoActionBar variant so the activity-supplied Toolbar is the only one.
- WidgetConfig: store labels: Set<String> alongside buttons; default {sos, connect}
- Provider: derive label visibility from config.labels; localize labels for non-special buttons (advert/chats/map/plan333/telemetry)
- Config screen: add per-row Switch "Etiqueta" visible only on selected rows
- Swatches: replace XML selector + tint dance with programmatic StateListDrawable; selected state now draws a visible ring stroke around the colored circle
Pushes signal_bars (0-4 derived from best SNR over last 5 min) from Dart and renders 4 stepped bars in the widget header. Color and threshold mapping mirrors lib/ui/screens/home_screen.dart::_SignalBarsIcon. Bars are hidden (rendered as 0) when disconnected.
ConnectionNotifier._pushWidget reading bestSignalSnrProvider triggers CircularDependencyError because that provider watches connectionProvider — the same StateNotifier we are. Inline the 5-min best-SNR scan against rxLogProvider instead.
…al bars

- Status dot and active connect button now keep their circular drawables and tint via setBackgroundTintList instead of setBackgroundColor (which replaced the drawable with a solid rectangle).
- Signal bars now use the configured accent color (filled = accent, unfilled = accent at low alpha) so the indicator matches the rest of the widget's accent.
…ut-aware insets

- Switch theme from AppCompat to Theme.Material3.DayNight.NoActionBar
- Add com.google.android.material:material dep
- Replace AppCompat Button/CheckBox/SwitchCompat/Toolbar with MaterialButton/MaterialCheckBox/MaterialSwitch/MaterialToolbar
- Use MaterialAlertDialogBuilder for the RGB picker and validation dialogs
- fitsSystemWindows + windowLayoutInDisplayCutoutMode=shortEdges so the toolbar clears the camera cutout
- Replace dated drag handle (ic_menu_sort_by_size) with a clean two-line vector
…plugin

FlutterActivity.onNewIntent does not call setIntent(intent), so HomeWidget.widgetClicked kept seeing the original launch URI. Result: only the first tap routed correctly; subsequent taps fell through to the /channels redirect. Override onNewIntent to forward.
FlutterActivity.onNewIntent dispatches the intent to plugins (so home_widget can read intent.data) and then unconditionally pushes intent.data to the flutter/navigation channel as a deep-link route. That second step caused GoRouter to receive the meshcore-widget:// URI after our Dart-side dispatcher had already navigated, triggering the scheme redirect to /channels and overwriting the user's intended destination on every warm tap.

For meshcore-widget URIs we now dispatch to plugins manually via FlutterEngine.activityControlSurface.onNewIntent and skip Flutter's built-in deep-link push. Other URIs go through the default path unchanged.
Copilot AI review requested due to automatic review settings May 7, 2026 18:24
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR enhances the Android home-screen launcher widget by adding per-widget-instance customization (buttons, ordering, labels, accent color), visual polish (status dot tinting, header signal/battery indicators), and new widget deep-link actions (Plan 3-3-3, Telemetry), alongside intent-routing fixes for warm-start widget taps.

Changes:

  • Added a Material 3 widget configuration activity and per-widget persisted config (buttons/labels/accent).
  • Extended widget → app action plumbing (new URI actions, GoRouter redirect hardening, intent routing fix, widget signal bars + accent-driven tinting).
  • Introduced/updated Android widget layouts, drawables, and build dependencies; added a Dart unit test for WidgetAction.fromUri.

Reviewed changes

Copilot reviewed 52 out of 52 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
test/widget_action_uri_test.dart Adds coverage for widget action URI parsing.
lib/ui/router.dart Hardens redirect handling for meshcore-widget:// URIs.
lib/services/widget_service.dart Adds signal bars persistence + SNR→bars mapping + new widget actions.
lib/providers/parts/connection_notifier.dart Inlines best-SNR scan to avoid provider circular dependency and pushes signal bars to widget.
lib/main.dart Routes new widget actions to app destinations.
android/app/src/main/res/xml/widget_info.xml Enables configure + reconfigure support for the widget.
android/app/src/main/res/values/styles.xml Adds Material 3 theme for widget config activity.
android/app/src/main/res/values/strings.xml Adds widget config UI strings and button labels.
android/app/src/main/res/values/colors.xml Adds widget on-surface color tokens (light).
android/app/src/main/res/values-night/colors.xml Adds widget on-surface color tokens (dark).
android/app/src/main/res/layout/widget_meshcore.xml Updates widget layout (header signal/battery, 5 generic action slots).
android/app/src/main/res/layout/item_widget_button_pick.xml RecyclerView row layout for selecting/reordering widget buttons.
android/app/src/main/res/layout/activity_widget_config.xml Widget configuration activity UI layout (toolbar, list, swatches).
android/app/src/main/res/drawable/widget_swatch_bg.xml Adds swatch background selector drawable.
android/app/src/main/res/drawable/widget_status_dot_online.xml Online dot drawable.
android/app/src/main/res/drawable/widget_status_dot_offline.xml Offline dot drawable.
android/app/src/main/res/drawable/widget_status_dot_neutral.xml Neutral dot drawable (tinted at runtime).
android/app/src/main/res/drawable/widget_status_chip_online.xml Online chip background drawable.
android/app/src/main/res/drawable/widget_status_chip_offline.xml Offline chip background drawable.
android/app/src/main/res/drawable/widget_background.xml Widget background styling for pre-v31.
android/app/src/main/res/drawable/widget_action_btn_sos_bg.xml SOS action button background.
android/app/src/main/res/drawable/widget_action_btn_bg.xml Neutral action button background.
android/app/src/main/res/drawable/widget_action_btn_active_neutral.xml Neutral “active” button background (tinted at runtime).
android/app/src/main/res/drawable/widget_action_btn_active_bg.xml Active button background (static fallback).
android/app/src/main/res/drawable/ic_widget_warning.xml SOS/warning vector icon.
android/app/src/main/res/drawable/ic_widget_telemetry.xml Telemetry vector icon.
android/app/src/main/res/drawable/ic_widget_tag.xml Channels/tag vector icon.
android/app/src/main/res/drawable/ic_widget_power.xml Connect/power vector icon.
android/app/src/main/res/drawable/ic_widget_plan333.xml Plan 3-3-3 vector icon.
android/app/src/main/res/drawable/ic_widget_people.xml People/contacts vector icon.
android/app/src/main/res/drawable/ic_widget_map.xml Map vector icon.
android/app/src/main/res/drawable/ic_widget_location.xml GPS/location vector icon.
android/app/src/main/res/drawable/ic_widget_drag_handle.xml Drag handle vector icon.
android/app/src/main/res/drawable/ic_widget_chat.xml Chat vector icon.
android/app/src/main/res/drawable/ic_widget_broadcast.xml Broadcast/advert vector icon.
android/app/src/main/res/drawable/ic_widget_battery_mid.xml Battery mid vector icon.
android/app/src/main/res/drawable/ic_widget_battery_low.xml Battery low vector icon.
android/app/src/main/res/drawable/ic_widget_battery_high.xml Battery high vector icon.
android/app/src/main/res/drawable/ic_widget_battery_full.xml Battery full vector icon.
android/app/src/main/res/drawable/ic_widget_battery_alert.xml Battery alert vector icon.
android/app/src/main/res/drawable-v31/widget_background.xml Widget background leveraging system dynamic colors on v31+.
android/app/src/main/res/drawable-v31/widget_action_btn_bg.xml Action button background using system dynamic colors on v31+.
android/app/src/main/res/drawable-v31/widget_action_btn_active_bg.xml v31+ active background resource.
android/app/src/main/res/drawable-night-v31/widget_background.xml Night v31+ widget background.
android/app/src/main/res/drawable-night-v31/widget_action_btn_sos_bg.xml Night v31+ SOS background.
android/app/src/main/res/drawable-night-v31/widget_action_btn_bg.xml Night v31+ action button background.
android/app/src/main/kotlin/pt/meshcore/lusoapp/WidgetConfigActivity.kt New widget configuration activity (button picking, swatches, custom color dialog).
android/app/src/main/kotlin/pt/meshcore/lusoapp/WidgetConfig.kt New per-widget persisted config model + prefs helpers.
android/app/src/main/kotlin/pt/meshcore/lusoapp/MeshCoreWidgetProvider.kt Updates widget rendering to use per-widget config, accent tinting, signal bars, cleanup on delete.
android/app/src/main/kotlin/pt/meshcore/lusoapp/MainActivity.kt Fixes intent routing for warm-start widget taps (skip Flutter deep-link push for widget URIs).
android/app/src/main/AndroidManifest.xml Registers widget configure activity with Material theme.
android/app/build.gradle.kts Adds AndroidX/Material/Flexbox dependencies for config UI.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +51 to 53
if (signalBars != null)
HomeWidget.saveWidgetData<int>('signal_bars', signalBars.clamp(0, 4)),
]);
Comment thread lib/main.dart
Comment on lines 160 to +173
switch (action) {
case WidgetAction.open:
// Just bring the app to the foreground — no navigation change.
break;
case WidgetAction.openChats:
router.go('/channels');
case WidgetAction.openMap:
router.go('/map');
case WidgetAction.openConnect:
router.go('/connect');
case WidgetAction.openPlan333:
router.go('/apps/plan333');
case WidgetAction.openTelemetry:
router.go('/apps/telemetry');
Comment on lines +117 to +121
views.setColorStateList(
R.id.widget_status_dot,
"setBackgroundTintList",
if (connected) ColorStateList.valueOf(accent) else null,
)
Comment on lines +248 to 252
views.setColorStateList(
frame,
"setBackgroundTintList",
if (bgTint != null) ColorStateList.valueOf(bgTint) else null,
)
Comment on lines +203 to +209
val container = LinearLayout(ctx).apply {
orientation = LinearLayout.VERTICAL
setPadding(48, 32, 48, 16)
}
val preview = View(ctx).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, 80,
Comment on lines +259 to +260
"Escolhe entre ${WidgetConfig.MIN_BUTTONS} e " +
"${WidgetConfig.MAX_BUTTONS} botões.",
Comment on lines +131 to +136
private fun makeSwatch(color: Int, isCustom: Boolean): View {
val density = resources.displayMetrics.density
val size = (density * 40).toInt()
val margin = (density * 6).toInt()
val ringPx = (density * 3).toInt()

Comment on lines +33 to 41
<ImageView
android:id="@+id/widget_gps_badge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dp"
android:text="📍"
android:textSize="12sp"
android:layout_width="14dp"
android:layout_height="14dp"
android:layout_marginEnd="10dp"
android:src="@drawable/ic_widget_location"
android:tint="@color/widget_on_surface_variant"
android:contentDescription="GPS"
android:visibility="gone" />
Comment on lines +60 to +67
<LinearLayout
android:id="@+id/widget_signal_bars"
android:layout_width="wrap_content"
android:layout_height="14dp"
android:layout_marginStart="8dp"
android:orientation="horizontal"
android:gravity="bottom"
android:contentDescription="Sinal">
Comment on lines +1 to +23
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true">
<layer-list>
<item>
<shape android:shape="oval">
<solid android:color="@android:color/white" />
</shape>
</item>
<item android:left="3dp" android:top="3dp"
android:right="3dp" android:bottom="3dp">
<shape android:shape="oval">
<solid android:color="@android:color/white" />
</shape>
</item>
</layer-list>
</item>
<item>
<shape android:shape="oval">
<solid android:color="@android:color/white" />
</shape>
</item>
</selector>
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.

2 participants