# Module card, spec 1 An installed Android TV app can declare a card for the launcher. A card is the app's tile with a label of its choice, an optional caption and an optional glow colour, placed ahead of the alphabetical grid. The launcher shows a card only after the TV owner has answered Show on a prompt, and it remembers that answer for the app's signing key. This document is the contract between a module (the app) and the launcher. The how-to for module authors is [`docs/modules.md`](../modules.md). ## Declaration In the module's `AndroidManifest.xml`, on the activity that already has the `LEANBACK_LAUNCHER` filter (the one the launcher shows as a tile): ```xml ``` `res/xml/module_card.xml`: ```xml ``` Rules for the declaration: - The card filter and the `meta-data` sit on an activity that also has `MAIN` + `LEANBACK_LAUNCHER`, is `exported` and is enabled. A card on any other activity is dropped. - The `meta-data` must use `android:resource` pointing at an XML resource. `android:value` is not read. - One card per package. If a package declares more than one, only the first the system lists is read. - The root element is `` with attributes only, no XML namespace. Child elements are ignored. Unknown attributes are ignored, up to 16 attributes in total. ## Fields Values are literal text written in the XML file. References (`@string/name`, `?attr/name`) are not resolved in spec 1 and make the card invalid, so a card is not localisable in spec 1. | Attribute | Required | Rule | | :- | :- | :- | | `spec` | yes | Decimal integer. This launcher reads spec `1`. Any other value: the card is ignored, with one log line. | | `label` | yes | 1 to 24 characters from `A-Z a-z 0-9` space `. & + ' -`, no leading or trailing space, at least one letter or digit. See the reserved names below. | | `caption` | no | Up to 32 characters from the same set. Shown after the label while the card is focused in the grid. The name line under the tile is one line; label and caption together show about 26 characters on a 4 column grid, so keep the caption to a few words. | | `accent` | no | `#RRGGBB`. The ambient glow colour while the card is focused in the dock. | The resource compiler stores some attribute values in a typed form; the launcher accepts the forms it hands back for the values above (`spec="1"` as an integer, `accent="#E8A33D"` as a colour, which comes back as `#ffe8a33d`). Keep labels and captions as words: a value that reads as a number with a unit or as a colour is stored in that typed form and will not match what you wrote. ### Reserved names Comparisons use the folded form of the label: lower case, with everything except ASCII letters and digits removed. "Live TV", "live-tv" and "LIVETV" are the same name. A label is rejected when: - it contains `verified` or `official` anywhere: words that imply endorsement are reserved, and nothing a module declares is ever presented as endorsed (the same two words are refused in the caption); - it is one of the names the launcher's own UI uses, in English or in the TV's language: `Home`, `Settings`, `Inputs`, `Wi-Fi`, `Live TV`, `Default home`, `Clock`, `cleanyourtv`, `tv-vibehack`; - it names an input the Inputs menu shows: `HDMI` and anything starting with it, `AV`; - while the prompt is due (a new card, or one whose signing key or label changed): it equals the label of another app installed on this TV (the names on the home screen), or the app's own label equals another installed app's label. The module's own app label is fine: a card named "Retro" from an app named "Retro" is the normal case. Once the owner has answered, the name stays with that card: an app installed afterwards with the same label does not displace it. The answer is for that name: a card that changes its label on an update is pending again and goes through these checks anew; - two cards share a label: the one the owner already decided on keeps the name (the oldest decision wins), the other is dropped; when neither has a decision, both are dropped and nobody is asked about a contested name. ## Limits The launcher never trusts a module's input to be small or well formed. Everything is bounded and any violation drops that one card, with a log line and no other effect. | What | Limit | | :- | :- | | Cards read per package list refresh | 32, the rest are ignored | | Cards shown as tiles | 8, by label; the rest are listed as over the limit in the Module cards list and keep their ordinary tile | | Decisions remembered in `cards.json` | 128, the oldest are dropped beyond that | | XML events walked per card | 64 | | XML nesting depth | 2 (`` and its direct children) | | Attributes on `` | 16 | | Label and caption length | 24 and 32 characters, checked before anything else looks at the value | | Signers | exactly one signing certificate; a package with several is rejected | ## Launcher behaviour 1. Discovery. Every time the home screen comes to the front, the launcher asks the package manager for activities with `MAIN` + `com.gathin.tvhome.category.DOCK_CARD` (a package manager query, no polling, no services, no network). For each one it checks the activity, reads the XML through the package manager, applies the rules above and computes the signer digest. Every card is read inside its own error boundary. 2. Signer digest. SHA-256 of the DER encoded signing certificate of the package's single signer, as `apksigner verify` with the `print-certs` option prints it ("Signer #1 certificate SHA-256 digest"). On Android 9 and newer the launcher reads the current signer; on Android 8 it reads the package's signature. A package signed with more than one certificate is rejected. 3. Decision record. The owner's decision is stored per package in the launcher's private storage (`files/cards.json`, format version 2): package, signer digest, the card's folded label, decision (`show` or `hide`), first seen, decided at. The file is written atomically and validated strictly when read; a file that does not validate is moved to `cards.json.bad` and the launcher continues as if no decision had been made. The record never leaves the TV (backups are off for the launcher). 4. Prompt. A card with no record, or whose signer digest or folded label differs from its record, is not shown. Once the grid is laid out and the home screen has focus, with no menu open, the launcher shows one prompt. It names the app by its own label (control and formatting characters removed, at most 40 characters) and package and the key by the first twelve hex characters of the digest; when the card label differs from the app label it says both names. The buttons are "Keep hidden" (focused first) and "Show card"; Back means Keep hidden. The prompt also says how to change the answer afterwards. Left and Right move between the buttons; Up and Down do nothing. After an answer, the next card with a pending prompt follows at once. 5. Signer change. A package whose signer digest differs from its record is treated as new: the card is not shown, and the prompt says the app was updated with a different signing key. The record is replaced only by the owner's answer, never by the detection alone. Label change. A card whose folded label differs from its record is treated the same way: not shown, and the prompt says the card changed its name. A decision is for one name under one key. 6. Placement. A shown card stands in for every tile of its package: the ordinary tile is not shown next to it. A package in `hidden_apps` (`config.xml`) shows neither a tile nor a card. A package in `dock_apps` gets its card in that dock slot. Other cards sort right after the configured dock apps and before the alphabetical grid, by label. On the default configuration, four dock apps and four columns, that is the first grid tile. With fewer `dock_apps` than columns, a shown card fills the empty dock slot, since the dock is simply the first row. In the dock, the top shelf shows the label and the glow uses the accent; in the grid, the name line under the focused tile shows the label and the caption, as far as they fit on one line. A card uses the app's own banner (or icon) for its artwork. 7. Hide. A hidden card leaves the app's ordinary tile exactly as before: hiding a card never removes an app, and showing one never installs anything. A card is hidden or shown again from the Module cards list: hold OK on the Settings pill. The list shows every discovered card with its app label, package, full signer digest and state; OK toggles Shown and Hidden. 8. Uninstall. Removing the module removes the card. Its record stays, so a reinstall with the same signing key needs no new prompt. 9. Launch. A card launches the declaring activity by explicit component with no extras and no data, the same way every tile is launched. No field of a card can name an intent, a URI, a class or a package. Holding OK on a card opens the app's App info page, like any tile. 10. Off switch. `module_cards` in `config.xml` or the `config.json` theme overlay (default `true`). When `false` the launcher never looks for cards, never asks, and the Module cards list only says that none are installed. ## Security - The launcher never installs, downloads, enables or disables anything, binds no service, loads no code, views or resources from a module beyond the one XML file and the label and artwork every tile already uses, and has no network permission. - Nothing is shown until the owner says so, and the answer is pinned to the signing key. The owner judges a package name and an app label; the launcher judges the digest. - A card can only make its own activity look different; it cannot start anything else. Launching it is the same as launching the app's ordinary tile. - Label rules are ASCII only so that look-alike letters cannot imitate another name, and labels that match another installed app or the launcher's own UI are refused. - The card code can never take the home screen down: if any part of it fails on a TV, cards switch off for that session with one log line and Home behaves as with `module_cards` false. - Residual risk: an app that holds the "draw over other apps" permission could draw over the prompt text. It cannot press the prompt's buttons from a remote. The prompt is only shown while the home screen has window focus and is dismissed when the home screen leaves the front. ## Rejections Every rejected card produces one line in logcat under the tag `TvHomeCards`, with the package and the reason, for example: ``` W TvHomeCards: io.example.spoof: label 'Retro' is the name of another installed app W TvHomeCards: io.example.deep: XML nested deeper than 2 W TvHomeCards: io.example.big: label is longer than 24 characters ``` Read them with `adb logcat -s TvHomeCards` (or `tools/tv.py 'logcat -d -s TvHomeCards'`). A rejected card has no other effect: the app keeps its ordinary tile.