A Smartspacer plugin that brings back the Pixel
"Daily Forecast" card, built from the data of the Pixel Weather (Google Weather,
com.google.android.apps.weather) app.
- In the morning — today's high / low and condition.
- In the evening — tomorrow's high / low and condition.
- Hidden the rest of the day. The card appears and disappears exactly on the boundaries (06:00, 10:00, 20:00 and midnight).
- The condition icon is Pixel Weather's own, untinted, like on a Pixel.
- Tapping the card opens Pixel Weather.
Pixel Weather exposes no content provider, no broadcast and no readable service — its only first-party surfaces are system-signature. So the plugin reads the two things the app does publish, through Smartspacer's SDK:
| Source | Mechanism | Provides |
|---|---|---|
The "Forecasted weather" widget (FullForecastWidgetReceiver) |
SmartspacerWidgetProvider hosts the widget at 380×300dp and walks the inflated RemoteViews |
location, current condition + icon, today's high/low, and daily rows (weekday, high/low, icon) for the next five days |
The evening "Tomorrow's forecast" notification (channel NOTIFICATION_CHANNEL_ID_WEATHER_FORECAST) |
SmartspacerNotificationProvider |
tomorrow's high/low, condition text and icon |
Both feed one merged, persisted state. Widget data wins on temperatures; the notification supplies tomorrow's condition text, which the widget's daily rows do not carry.
The Pixel Weather widget is built with Jetpack Glance and the APK's resource names are stripped
(0_resource_name_obfuscated), so there is no com.google.android.apps.weather:id/high_temp to look
up — every view id is generated at runtime. PixelWeatherWidget therefore matches by content and
shape rather than by id:
- a temperature is anything matching
^[-−–+]?\d{1,3}\s?°[CF]?$, - today's high/low is the one row of exactly
[temp, "·", temp], - a daily row is a layout whose only direct label is a weekday name (resolved in the device locale
as well as English) wrapping a nested
[temp, temp]pair, - the location is the first non-temperature text in the tree.
Glance also inserts empty FrameLayout spacers between children, so only non-blank TextViews are
considered. Debug builds log the whole inflated tree under the PixelWeatherWidget tag, which is the
fastest way to re-derive this if a Pixel Weather update changes the layout.
Pixel Weather refreshes its widget roughly every 30 minutes. When the cached data is older than an hour, the plugin nudges the widget to recompose by toggling the requested widget height by one pixel — Glance only recomposes when the size actually changes.
The 06:00 / 10:00 / 20:00 / 00:00 boundaries are hit with an exact AlarmManager alarm that re-arms
itself on every fire, on boot and on time/timezone changes. Smartspacer's refreshPeriodMinutes is
documented as a minimum, not a schedule, so it cannot be relied on for a card that must vanish at a
specific minute.
Everything is persisted to files/forecast.json, so the card survives process death; only the click
PendingIntents are memory-only and fall back to a plain launch intent.
app/src/main/java/dev/flexy/smartspacerforecast/
ForecastWindow.kt time windows and the next boundary
targets/DailyForecastTarget.kt the Smartspacer Target (the card itself)
widgets/PixelWeatherWidget.kt widget host + RemoteViews parser
widgets/WidgetRefresher.kt forces a recompose by toggling the size
notifications/PixelWeatherNotification.kt forecast-notification parser
repository/ForecastRepository.kt merged, persisted forecast state
receivers/ window alarms, boot, manual refresh
The plugin is submitted to the Smartspacer Plugin Repository,
so it can be installed from Smartspacer itself, under Plugin Repository. Because it declares
com.google.android.apps.weather as its required package, it shows up in the Recommended section
for anyone who has Pixel Weather installed.
Otherwise grab the APK from Releases.
plugin.json in the repository root is the manifest the plugin repository reads for updates.
AGP 9.2.1, Kotlin 2.4.0, compileSdk 37, minSdk 31. The Gradle daemon runs on Java 21, requested
through gradle/gradle-daemon-jvm.properties, so the JDK on your PATH does not matter — Gradle
finds or provisions one.
./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk
Release builds are signed with the key described in keystore.properties, which is not in the
repository. Without that file assembleRelease falls back to the debug key.
android.newDsl=false is set because AGP 9's new DSL is not yet compatible with the external Kotlin
Gradle plugin, and the SDK requires Kotlin 2.4.
- Install Pixel Weather and Smartspacer, and give Smartspacer notification access.
- Smartspacer → Targets → add Daily Forecast. Smartspacer binds the Pixel Weather widget itself and asks to forward Pixel Weather's notifications.
- Optionally turn on "Weather forecasts" notifications inside Pixel Weather, so the evening card carries the condition text ("Rain showers") and not only the two temperatures.
Force a refresh from a terminal:
adb shell am broadcast -a dev.flexy.smartspacerforecast.REFRESH -p dev.flexy.smartspacerforecast
- The card needs Pixel Weather installed; the Target reports itself incompatible otherwise.
- Tomorrow's condition text only appears once Pixel Weather has posted its evening forecast notification. Until then the evening card shows just the high/low from the widget.
- Parsing depends on the Glance layout Pixel Weather renders today. A redesign of that widget will need the structure rules above revisited — the debug tree dump is there for exactly that.
- Feature type is
FEATURE_UNDEFINEDon purpose.FEATURE_WEATHERmakes Smartspacer merge the card into the date line as a weather complication instead of showing it as its own card.
- Smartspacer and its plugin SDK by Kieron Quinn.
- The widget-scraping approach follows Smartspacer's own
GoogleWeatherForecastWidgetand thebbcweatherplugin.
