← bleelblep
experimental · reverse-engineered · nothing os

!thing widgets

third-party widgets that render on aod, lockscreen, and homescreen — by hooking the surface nothing's own widgets register through. the system treats them as stock.

nothing os 4.1 · phone (3) · undocumented api · expect breakage
heads up

this uses an undocumented api. only tested on nothing os 4.1, phone (3). it will probably break in future os updates. don't build on this — and don't take anything here as a stable public surface.

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.

credit ▮▯▯

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" }
          ] }
      ]
    }
  ]
}
the view object's invocation array is "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

04 the apk

here it is — a public build you can sideload and poke at. tick the boxes to unlock the download.

v0.2 built2026-05-30 · targetnothing os 4.1 · phone (3) · size~25 MB · signingunsigned (debug)
VirusTotal view scan report →
download apk tick the boxes above to unlock.

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.