Read and click Android home screen widgets from Tasker.
Lots of apps show information in a widget that they expose nowhere else — no API, no notification, no content provider. WidgetRelay hosts those widgets invisibly in the background, extracts their contents, and hands them to Tasker as plain text or JSON. It can also click elements inside them, so a widget button becomes something a task can trigger.
Typical uses:
- Read a value that only exists in a widget (unread counts, a sensor reading, the next departure, a balance, "now playing")
- Poll a widget on a schedule and act when the value changes
- Press a widget's button from a task (refresh, play/pause, toggle)
- Actions
- Requirements
- Setup
- Building
- How it works
- Implementation notes (the hacks)
- Limitations
- License
WidgetRelay registers four Tasker plugin actions. All of them are configured the same way: pick an app and widget, choose a size, and — for all but the JSON action — tap the element you want in the extracted-data list.
| Action | Input | Output |
|---|---|---|
| Get Widget Data | element path | %widget_value — the element's text, content description or resource id |
| Get Widget JSON | — | %widget_json — the whole widget as a JSON tree |
| Click Widget Path | element path | — |
| Click Widget Text | element text | — |
Elements are addressed by their path in the widget's view tree: /root is
the widget itself, /root/0/2 is the third child of the first child, and so on.
The configuration screen shows every extracted element with its path, so you
normally never type one by hand — you tap the element and the path is filled in.
Paths are stable as long as the widget's layout doesn't change. If a widget
rearranges itself (many do, depending on state or size), prefer Click Widget
Text, which searches by the visible text or content description instead. Both
input fields accept Tasker variables, so %par1 or %text work too.
Get Widget JSON returns the widget as a nested tree. type is the view
class, children is only present when a node has any:
{
"type": "AppWidgetHostView",
"children": [
{
"type": "LinearLayout",
"children": [
{ "type": "ImageView" },
{
"type": "LinearLayout",
"children": [
{ "type": "TextView", "text": "WhatsApp" },
{ "type": "TextView", "text": "2 ungelesene Nachrichten" }
]
},
{ "type": "ImageView", "description": "Neuer Chat" }
]
}
]
}Parse it in Tasker with the Java Function action, JSON Read, or by using
the JSON path support in newer Tasker versions.
- Android 6.0 (API 23) or newer
- Tasker (or another Locale/Tasker plugin host)
- Two permissions, both requested during action configuration:
- Bind widget — a system dialog shown the first time you pick a widget
- Display over other apps — needed so widgets load their contents while Tasker runs the action in the background (why)
WidgetRelay has no launcher icon and no UI of its own — everything happens inside the Tasker action configuration screen.
- Install WidgetRelay.
- In Tasker, add an action: Plugin → WidgetRelay → (pick one of the four).
- The widget picker opens immediately. Search for the app or widget you want.
- Grant the widget bind permission when the system asks.
- Choose the widget size in cells. This matters — many widgets render a different layout at different sizes, so pick the size you actually want to read.
- Tap the element you want in the extracted-data list below the preview. For the click actions, elements that can't be clicked are dimmed and can't be selected.
- Save. On first save you'll be sent to Android's "Display over other apps" settings if that permission is still missing.
git clone https://github.com/CennoxX/WidgetRelay.git./gradlew assembleDebugThe APK lands in app/build/outputs/apk/debug/. To install directly:
./gradlew installDebugassembleRelease produces an unsigned APK unless you provide a keystore.
Create one (once — and back it up, an APK signed with a different key cannot
replace an installed one):
keytool -genkey -v -keystore widgetrelay.jks -alias widgetrelay -keyalg RSA -keysize 2048 -validity 10000Then put a keystore.properties in the project root:
storeFile=widgetrelay.jks
storePassword=<the password you chose>
keyAlias=widgetrelay
keyPassword=<the key password you chose>Both the keystore and that file are gitignored. With them in place,
./gradlew assembleRelease writes a signed APK to
app/build/outputs/apk/release/.
app/src/main/java/com/cennoxx/widgetrelay/
├── widget/
│ ├── WidgetHost.kt AppWidgetHost singleton: allocate, bind, create views
│ ├── WidgetExtractor.kt walks the inflated view tree into WidgetNodes
│ ├── WidgetNode.kt one extracted element
│ ├── WidgetGrid.kt launcher cell geometry (see hack 6)
│ ├── WidgetListModels.kt picker models + default span calculation
│ ├── ActivityWidgetSelector.kt searchable widget picker
│ └── WidgetExpandableAdapter.kt picker list, grouped by app
└── tasker/widgets/
├── WidgetActionInput.kt shared plugin input (widget id, size, query)
├── WidgetActionRuntime.kt the runtime core: attach, poll, extract, click
├── ActivityConfigWidgetActionBase.kt shared configuration UI
├── GetWidgetData.kt ─┐
├── GetWidgetJson.kt │ the four actions:
├── ClickWidget.kt │ runner + config helper + config activity
└── ClickWidgetText.kt ─┘
The flow is the same for every action:
- At configuration time a widget is bound and its
appWidgetIdis stored in the Tasker action. The binding is persistent, so it survives reboots. - At run time the runner recreates that widget off-screen, waits for the
provider to deliver its
RemoteViews, and then reads or clicks the view tree.
Android has no supported way for an app to read what a widget displays, and no way at all to click one. Everything below exists to work around that. If you're here to fix a bug or port the idea, read this section first.
AppWidgetHost is the API launchers use. Nothing stops a normal app from
creating one, but the widget lifecycle has to be managed by hand:
allocateAppWidgetId()reserves an id,bindAppWidgetIdIfAllowed()binds a provider to it. That call fails unless the user granted the bind permission, so the failure path launchesACTION_APPWIDGET_BINDand lets the system ask.- Some widgets have their own configuration activity, which must run through
startAppWidgetConfigureActivityForResult()— going through a plain intent fails for providers whose config activity isn't exported. - Allocated ids are a leaked system resource if you forget them. The config activity deletes the id again if you cancel, pick a different widget, or replace the widget of an existing action.
QUERY_ALL_PACKAGESis needed forgetInstalledProviders()to return widgets from every installed app instead of a small subset.
(WidgetHost.kt, ActivityConfigWidgetActionBase.kt)
This is the central hack. A widget's ListView, GridView or StackView
does not carry its rows in the RemoteViews. It connects to the provider's
RemoteViewsService and loads them asynchronously — but only once the host view
is attached to a window and laid out.
The first implementation inflated the widget completely detached. That works in the configuration screen (the preview sits in a real activity window), which is exactly why it was so confusing: every list row was visible while configuring, and gone when Tasker ran the action.
The fix is to give the widget a real window at runtime: it's added to a
TYPE_APPLICATION_OVERLAY window that is sized like a home screen widget but
alpha = 0, FLAG_NOT_TOUCHABLE and FLAG_NOT_FOCUSABLE, then removed as soon
as the action is done. That's what the "Display over other apps" permission is
for — an app cannot add a window from the background without it.
(WidgetActionRuntime.withAttachedWidget())
Nothing tells you when a provider has finished delivering its views, and collection rows arrive later still. So the runtime polls the tree every 100 ms starting 50 ms after attaching, up to an 8 s timeout, with two different exit conditions:
- Path and text actions stop the moment the element they need exists. That usually happens on the first or second poll, so those actions are fast.
- Get Widget JSON needs everything, so it waits for the tree to settle: unchanged for 500 ms, and never finishing earlier than 800 ms after attach. Without that lower bound, two identical polls taken before a list service has even connected would look "stable" and the capture would come back empty.
(WidgetActionRuntime.captureNodes())
RemoteViews attaches its PendingIntent wherever the widget's author put it,
which is almost never the TextView you actually want to click — it's usually a
row or container several levels up. So a click walks up from the target view
until some ancestor both has a click listener and consumes performClick().
Rows of a collection are different again: they are clicked through the
AdapterView, which merges the row's fill-in intent into the provider's
PendingIntent template. Calling performClick() on such a row does nothing, so
when the walk-up reaches an AdapterView it calls performItemClick() instead.
(WidgetActionRuntime.performClickOn())
The configuration screen dims elements that can't be clicked. Clickability is
computed while extracting, using the same rule the click walk-up uses, so what
looks selectable is exactly what the runner can actually click: a node counts as
clickable if it has a click listener itself, inherits one from an ancestor, or
its parent is an AdapterView. The flag is carried down the traversal queue
rather than looked up per node, so it can't drift out of sync.
(WidgetExtractor.extractFromRemoteViews())
Widgets pick their layout based on the size they're given, so the configuration
preview and the background runtime have to agree on how big "2 x 2" is — if they
disagree, you configure against one layout and read another. There is no API for
the launcher's cell size, so it's approximated: 5 columns across the usable
screen width, cells 1.4× as tall as they are wide. updateAppWidgetSize() then
tells the provider which size to render for.
The math lives in one place so the preview and runtime cannot drift apart.
(WidgetGrid.kt)
Tasker runs plugin actions on a background thread, but everything involving
AppWidgetHost, windows and views must happen on the main thread. Each action
therefore posts its work to a main-thread Handler and blocks on a
CountDownLatch until the polling loop finishes or the timeout expires — the
runner stays synchronous, which is what Tasker expects, while all view work
happens where Android requires it.
(WidgetActionRuntime.withAttachedWidget())
There's no public way to inspect a RemoteViews object's contents. The widget
has to be inflated into a real AppWidgetHostView and the resulting view
tree walked instead: class name, TextView.getText(), content description, and
resource id name, with a breadth-first walk producing the /root/0/1 paths.
That's also why extraction is deliberately shallow — text and content description are the only things a widget reliably exposes to a host.
(WidgetExtractor.kt)
- The configuration screen opens the widget picker immediately when adding a new action, so there's no empty config screen flashing past first.
- Tasker's generated action blurbs are disabled for every input field
(
ignoreInStringBlurb) and written by hand instead, so the action list shows something readable. - The widget preview is only measured after a layout pass — sizing it before that gives a zero-width frame and a wrongly rendered widget.
- Background activity launch restrictions. Widget buttons that fire a
PendingIntentfor a broadcast or service (refresh, play/pause, toggles) work fine. Ones that open an activity may be blocked by Android when the click comes from the background. - Timing. A widget that needs longer than 8 s to deliver its content will
time out; a list that delivers its first rows more than 500 ms after the static
layout can still be captured empty by the JSON action. Both bounds are
constants at the top of
WidgetActionRuntime.kt. - Paths are brittle. Widgets that rearrange their layout invalidate stored paths. Use Click Widget Text where possible.
- One element per read. Get Widget Data returns a single value; use Get Widget JSON if you need several.
- No conditions or events. WidgetRelay only provides actions — polling with a Tasker profile is the way to react to changes.
- Widgets are re-created on every run, so this is not free: expect a run to take a few hundred milliseconds.
WidgetRelay is licensed under the GNU General Public License v3.0 — see LICENSE.
The project is a fork of joaomgcd/TaskerPluginSample, whose Tasker plugin library is GPL-3.0 licensed and is compiled into this app.
See NOTICE for third-party attribution and the list of changes made to the upstream project.
- João Dias (joaomgcd) for the Tasker plugin library this is built on
- Pent (dinglisch) for Tasker and its plugin protocol