The oldest app I still maintain shipped in 2014. I opened its AndroidManifest.xml last week for an unrelated build fix and found a <capability> tag I have no memory of writing.
It was there so the app could be opened by voice. And if I do not remember writing it, I certainly do not remember testing it.
Starting September 4th, Google Assistant on Android is progressively replaced by Gemini. I cannot predict what each of those entry points will do afterwards. What I can do is find out where they are while the current behaviour still exists.
Counting is possible even when predicting is not. grep turns out to be enough for the counting part.
What changes on September 4th, and what does not
The scope first.
| Surface | After September 4th |
|---|---|
| Android phones and tablets | Progressively replaced by Gemini |
| Wear OS smartwatches | Progressively replaced by Gemini |
| Assistant-enabled headphones | Progressively replaced by Gemini |
| Android Auto projected from a phone | Progressively replaced by Gemini |
| Cars with Google built-in | Assistant continues |
The rollout happens in stages, and once a device has switched, there is no option to move it back to the old Assistant.
That last part is what makes this different from a normal platform change. Usually "wait until it breaks, then fix it" is a reasonable strategy: you can see the breakage, and you can roll back. Here you cannot roll back. Once your own device flips, the previous behaviour is no longer observable to you.
Which is exactly why the list of places worth checking should exist before the flip, not after.
Translate "assistant integration" into strings you can search for
The phrase "assistant integration" is not greppable. It has to be reduced to text that actually appears in files.
| Entry point | What to search for | Where it usually lives |
|---|---|---|
| App Actions capability | <capability | res/xml/shortcuts.xml |
| ASSIST intent | android.intent.action.ASSIST / ACTION_ASSIST | Manifest, activity code |
| Voice interaction | VoiceInteraction / VOICE_COMMAND | Manifest, service code |
| Leftovers from actions.xml | Names in the actions.intent.XXX form | Under res/xml/ |
| Static shortcuts | <shortcut / android:shortcutId | shortcuts.xml, manifest |
| Dynamic shortcuts | ShortcutManager class names | Kotlin / Java code |
| Auto-verified app links | android:autoVerify | Manifest |
Shortcuts and app links are not voice-only mechanisms, and you may well decide they are out of scope. Include them anyway for now: they are routes an assistant can use to open your app, and ruling them out is something to do after looking, not before.
grep is enough — a script that produces a ledger
The table above becomes the script. Point it at a project root and it prints a count per entry point and writes a CSV ledger.
#!/usr/bin/env bash
# assist-inventory.sh — find every entry point an assistant could open, and log it
# Usage: ./assist-inventory.sh <android-project-root> [output.csv]
set -u
ROOT="${1:-.}"
OUT="${2:-assist-inventory.csv}"
SEEN=$(mktemp)
printf 'surface,file,line,snippet,checked_on_device\n' > "$OUT"
# Match the most specific entry points first; a line already logged is skipped
while IFS='|' read -r label pattern; do
[ -z "$label" ] && continue
found=$(grep -rnE --include='*.xml' --include='*.kt' --include='*.java' \
--exclude-dir=build --exclude-dir='.git' -- "$pattern" "$ROOT" 2>/dev/null)
n=0
while IFS= read -r hit; do
[ -z "$hit" ] && continue
file=${hit%%:*}; rest=${hit#*:}; num=${rest%%:*}; snip=${rest#*:}
key="$file:$num"
grep -qxF "$key" "$SEEN" && continue
printf '%s\n' "$key" >> "$SEEN"
snip=$(printf '%s' "$snip" | tr -d '\r' | sed 's/^[[:space:]]*//; s/"/""/g' | cut -c1-80)
printf '%s,%s,%s,"%s",\n' "$label" "$file" "$num" "$snip" >> "$OUT"
n=$((n + 1))
done <<EOF
$found
EOF
printf '%-26s %s\n' "$label" "$n"
done <<'SURFACES'
App Actions capability|<capability[ >]
ASSIST intent|android\.intent\.action\.ASSIST|ACTION_ASSIST
Voice interaction|VoiceInteraction|android\.intent\.action\.VOICE_COMMAND
actions.xml leftovers|actions\.intent\.[A-Z_]+
Static shortcut|<shortcut |android:shortcutId|android\.app\.shortcuts
Dynamic shortcut|ShortcutManager|pushDynamicShortcut|ShortcutManagerCompat
Auto-verified app link|android:autoVerify="true"
SURFACES
rm -f "$SEEN"
echo "---"
echo "Ledger: $OUT ($(( $(grep -c . "$OUT") - 1 )) rows)"Three things happen here. Each row of the table is read as label|regex, grep -rnE looks for it, and only a file:line pair that has not been logged yet gets appended.
Run against a small project containing one capability, one ASSIST intent and one static shortcut, the output looks like this:
App Actions capability 1
ASSIST intent 1
Voice interaction 0
actions.xml leftovers 0
Static shortcut 2
Dynamic shortcut 0
Auto-verified app link 0
---
Ledger: assist-inventory.csv (4 rows)
And the CSV:
surface,file,line,snippet,checked_on_device
App Actions capability,./app/src/main/res/xml/shortcuts.xml,2,"<capability android:name=""actions.intent.GET_THING"">",
ASSIST intent,./app/src/main/AndroidManifest.xml,12,"<action android:name=""android.intent.action.ASSIST"" />",
Static shortcut,./app/src/main/res/xml/shortcuts.xml,5,"<shortcut android:shortcutId=""daily"" ...",
Static shortcut,./app/src/main/AndroidManifest.xml,8,"<meta-data android:name=""android.app.shortcuts"" ...",
The trailing checked_on_device column is deliberately empty. It is where you write the date you verified that row on a real device.
Two things only running it revealed
My first version was not shaped like this. Running it produced two corrections.
The first was a false positive. Written as <shortcut, the static-shortcut pattern also matched the root element <shortcuts> on line 1 of the file. One character apart, and invisible when you read the pattern rather than run it. Adding a trailing space — <shortcut — removed it.
The second was double counting. The single line <capability android:name="actions.intent.GET_THING"> matches both <capability and actions.intent.. When one line appears twice under two labels, the totals read higher than reality.
Hence the file:line bookkeeping that skips a line the second time around. But that choice has a consequence: match order now determines the label. Whichever pattern hits first owns the line.
So the list is ordered from specific to general, with <capability deliberately ahead of actions.intent.. In the run above, "actions.xml leftovers" shows 0 not because nothing matched, but because that one line was already counted as a capability.
When you run this on your own project, treat the ordering as something to question. Rearranging it moves the numbers.
Turn the ledger into utterances you can say out loud
A list of file paths does not tell you what to say to the device. That part is worth handing to Gemini: read the unchecked rows, produce one test utterance per row in both languages.
First, building the prompt from the CSV:
import csv
def load_unchecked(path):
with open(path, newline="", encoding="utf-8") as f:
return [r for r in csv.DictReader(f) if not r["checked_on_device"]]
def build_prompt(rows, app_name):
lines = [
f"{i + 1}. [{r['surface']}] {r['file']}:{r['line']} - {r['snippet']}"
for i, r in enumerate(rows)
]
return (
f"Below are the entry points in the Android app \"{app_name}\" "
"that a voice assistant could use to open it.\n"
"For each one, write a single spoken phrase, in English and in Japanese, "
"that I can say to a device to verify the behaviour.\n"
"Always include the app name, and keep it to something a person "
"would realistically say.\n\n"
+ "\n".join(lines)
)Then the call itself. Since the result feeds the next step rather than my eyes, the response is constrained by a schema:
import os
from google import genai
from pydantic import BaseModel
class Utterance(BaseModel):
surface: str
file: str
en: str
ja: str
client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
rows = load_unchecked("assist-inventory.csv")
response = client.models.generate_content(
model="gemini-3.7-flash",
contents=build_prompt(rows, "Wallpaper"),
config={
"response_mime_type": "application/json",
"response_schema": list[Utterance],
},
)
for u in response.parsed:
print(f"[{u.surface}] {u.file}\n EN: {u.en}\n JA: {u.ja}\n")Passing list[Utterance] as the response_schema means the response comes back as a list of Utterance objects, with no JSON parsing of your own.
One note on what is missing: there is no temperature. The sampling parameters are deprecated now, and if the goal is a stable shape rather than a stable wording, pinning the schema does that job better than tuning a number ever did. It is an easy line to copy forward from older sample code without noticing.
Measuring how voice-driven launches change across the cutover is a separate exercise, and it has to start before the switch for the same reason. If the measurement side interests you, The Assistant replacement cannot be undone, so the baseline has to be taken now covers that ground.
What to decide in the two weeks left
If only one thing gets done, make it this: switch a single device to Gemini ahead of the rollout and start filling in the checked_on_device column.
You can move to Gemini on your own schedule from the Google app or from device settings, without waiting for September 4th. Switch one device, not all of them, or you lose your comparison. Say each utterance on that device, record whether the app opened, and the rows that did not open become your work list for September.
The counting takes about fifteen minutes today and becomes impossible once the device has flipped. That ordering is the whole point.
For what it is worth, I still have not verified the tag I wrote four years ago. If you are carrying similar leftovers, I hope this makes the first fifteen minutes of it easier.