01 the discovery
nothing's first-party widgets — clock, quotes, battery — don't use stock android AppWidgetProvider. they register through a private app called com.nothing.cardservice, which holds a sqlite store of widget content json and pipes it to the launcher (com.nothing.launcher), the lockscreen host (com.nothing.hearthstone), and the always-on display.
with the right ContentProvider, an aidl binding, and a card descriptor in your manifest, the system accepts your apk as a card provider. the picker lists your widget alongside stock ones. the layout gets inflated inside the host process from your apk's resources. nothing distinguishes you from itself.
all credit goes to Martmists, who got there first. that work is what made this possible — i'd never have known the cardservice surface existed without it. this page is a separate exploration, but it stands on theirs.
the bug that took the longest to find: when cardservice asks your provider for content, responding synchronously isn't enough. you have to also push the same json outbound to content://com.nothing.cardservice via ContentResolver.call — otherwise the host's db never receives it and the widget renders blank. one line of misunderstanding; days of "why is it empty."
02 how it works
three pieces have to line up: the manifest registration, the json schema cardservice expects, and — critically — the outbound push that gets your content into the host's db.
// the part everyone misses
// when cardservice asks for content, respond AND push outbound. // without the second step, the host's db stays empty // and your widget renders blank. days of debugging live here. override fun call(method: String, arg: String?, extras: Bundle?): Bundle { val response = Bundle() val widgetId = arg?.toIntOrNull() ?: return response when (method) { "updateAppWidget", "partUpdateWidget" -> { val cardInfo = buildCardInfo(widgetId) // (1) respond synchronously… response.putString("card_info", cardInfo) // (2) …AND push outbound. this is the line. mainHandler.post { CardServiceClient.notifyUpdate(widgetId, cardInfo) } } } return response }
// the json the host actually wants
{
"config_info": { "layout_id": 2131296789, "package_name": "com.yourpkg", ... },
"view_info": [
{
"item_type": 0,
"view_id": 2131231044,
"view_type": "view",
"invoke": [
{ "item_type": 17, "invoke_method": "setText",
"invoke_params": [
{ "item_type": 5, "param_type": 2, "param_value": "hello" }
] }
]
}
]
}
"invoke" — not "invoke_params". the invocation's parameter array is "invoke_params" — not "params". for setText, param_type=2 (CharSequence), not 8 (String). easy to miss; impossible to guess.
aod is the same json with a second copy of the structure nested under the key "simple_card_info" — different layout id, simpler view tree (1dp outlines, no fills, white text, no brand colors). if that key is absent the widget just doesn't render on aod. no separate manifest attribute, no separate code path; the data shape is the signal.
03 what works · what doesn't
-
works
rendering on aod / lockscreen / homescreen. tap actions. multi-frame fade animations (push a sequence of
setAlphajsons on a handler). resize, long-press configure popup, and multi-page viewpager2 (vertical only — the launcher eats horizontal swipes unless you implementIHorizontalScrollableView). -
flaky
anything that touches the launcher's theme.
TextView,ProgressBar, and friends pull defaults through the launcher's theme at inflate — which is incomplete for non-system packages. workaround: wrap framework views inandroid:theme="@android:style/Theme.DeviceDefault"and use literal colors. -
doesn't
there's no way to make a third-party widget lockscreen-only through the card descriptor.
cardWidgetTypeandcardFeaturesdon't gate the surface — both pickers accept the same values. the filter lives somewhere in systemui that hasn't been traced yet.
04 the apk
here it is — a public build you can sideload and poke at. tick the boxes to unlock the download.
VirusTotal view scan report →this sits in /projects under experiments rather than on the homepage for a reason. if you're tinkering with cardservice yourself and want to compare findings, that's the audience.