個人開発で運用しているアプリのうち、いちばん古いものは2014年に出したものです。先日その AndroidManifest.xml を別件で開いたところ、自分がいつ書いたのか思い出せない <capability> タグが残っていました。
音声から開かれることを想定して書いた記述です。書いた記憶が薄いということは、動作を確かめた記憶も薄いということでもあります。
9月4日から、Android の Google アシスタントが Gemini へ順次置き換わります。置き換えの結果どうなるかを先回りして当てることはできませんが、「自分のアプリのどこがアシスタントに触れているか」を先に知っておくことはできます。当てるのは無理でも、数え上げることはできます。
数え上げは grep だけで足ります。特別なツールは要りませんでした。
9月4日に変わるものと、変わらないもの
まず対象の範囲です。
| 対象 | 9月4日以降 |
|---|---|
| Android のスマートフォン・タブレット | 順次 Gemini へ置き換え |
| Wear OS のスマートウォッチ | 順次 Gemini へ置き換え |
| アシスタント対応のヘッドホン | 順次 Gemini へ置き換え |
| スマートフォンから投影する Android Auto | 順次 Gemini へ置き換え |
| Google ビルトイン搭載の車両 | 従来のアシスタントが継続 |
置き換えは段階的に進みます。そして、置き換わった端末を元のアシスタントへ戻す選択肢は用意されていません。
ここが今回の面倒なところです。ふだんのプラットフォーム変更なら「壊れてから直す」で足ります。壊れたことに気づけて、元へ戻す手段があるからです。今回は後者がありません。手元の端末が置き換わってしまうと、置き換え前の挙動をもう一度見ることができなくなります。
だからこそ、確かめるべき場所の一覧だけは、置き換わる前に手元に持っておきたいところです。
「アシスタントに触れている」を、探せる文字列に置き換える
「アシスタント連携」という言葉のままでは検索できません。ファイルの中に実際に書かれている文字列まで落とします。
| 入口の種類 | 探す文字列 | だいたいどこにあるか |
|---|---|---|
| App Actions の capability | <capability | res/xml/shortcuts.xml |
| ASSIST インテント | android.intent.action.ASSIST / ACTION_ASSIST | マニフェスト、Activity のコード |
| 音声インタラクション | VoiceInteraction / VOICE_COMMAND | マニフェスト、サービスのコード |
| 旧 actions.xml 由来の記述 | actions.intent.XXX 形式の名前 | res/xml/ 以下 |
| 静的ショートカット | <shortcut / android:shortcutId | shortcuts.xml、マニフェスト |
| 動的ショートカット | ShortcutManager 系のクラス名 | Kotlin / Java のコード |
| 自動検証つきアプリリンク | android:autoVerify | マニフェスト |
このうち、ショートカットとアプリリンクは音声専用の仕組みではありません。それでも一覧に入れているのは、アシスタントがアプリを開くときの経路として使われうるからです。関係ないと決めるのは、確かめたあとで構いません。
grep で足ります — 入口を台帳にするスクリプト
上の表をそのままスクリプトにします。プロジェクトのルートを渡すと、種類ごとの件数を表示し、CSV の台帳を書き出します。
#!/usr/bin/env bash
# assist-inventory.sh — アシスタント経由で開かれうる入口を洗い出して台帳にする
# 使い方: ./assist-inventory.sh <Androidプロジェクトのルート> [出力CSV]
set -u
ROOT="${1:-.}"
OUT="${2:-assist-inventory.csv}"
SEEN=$(mktemp)
printf 'surface,file,line,snippet,checked_on_device\n' > "$OUT"
# 具体的な入口から先に照合する(同じ行が複数回当たったら最初の1つだけ台帳へ入れる)
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 '%-24s %s\n' "$label" "$n"
done <<'SURFACES'
App Actions の capability|<capability[ >]
ASSIST インテント|android\.intent\.action\.ASSIST|ACTION_ASSIST
音声インタラクション|VoiceInteraction|android\.intent\.action\.VOICE_COMMAND
旧 actions.xml 由来の記述|actions\.intent\.[A-Z_]+
静的ショートカット|<shortcut |android:shortcutId|android\.app\.shortcuts
動的ショートカット|ShortcutManager|pushDynamicShortcut|ShortcutManagerCompat
自動検証つきアプリリンク|android:autoVerify="true"
SURFACES
rm -f "$SEEN"
echo "---"
echo "台帳: $OUT($(( $(grep -c . "$OUT") - 1 )) 行)"やっていることは3つだけです。表の各行を 種類|正規表現 の形で読み、grep -rnE で当て、まだ台帳に入っていない ファイル:行番号 だけを追記します。
手元で、capability と ASSIST と静的ショートカットを1つずつ置いた小さなプロジェクトに対して走らせた結果がこれです。
App Actions の capability 1
ASSIST インテント 1
音声インタラクション 0
旧 actions.xml 由来の記述 0
静的ショートカット 2
動的ショートカット 0
自動検証つきアプリリンク 0
---
台帳: assist-inventory.csv(4 行)
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 インテント,./app/src/main/AndroidManifest.xml,12,"<action android:name=""android.intent.action.ASSIST"" />",
静的ショートカット,./app/src/main/res/xml/shortcuts.xml,5,"<shortcut android:shortcutId=""daily"" ...",
静的ショートカット,./app/src/main/AndroidManifest.xml,8,"<meta-data android:name=""android.app.shortcuts"" ...",
末尾の checked_on_device 列は空のままにしてあります。実機で確かめた日付を、あとから自分で書き込むための欄です。
実行して初めて分かった、照合順序の効き方
最初に書いた版は、この形ではありませんでした。走らせてみて2つ直しています。
1つ目は誤検出です。静的ショートカットのパターンを <shortcut と書いたところ、ファイルの1行目にあるルート要素の <shortcuts> にも当たりました。1文字違いで、目で読む分には気づきません。末尾に半角スペースを足して <shortcut にしたら消えました。
2つ目は二重計上です。<capability android:name="actions.intent.GET_THING"> という1行は、<capability にも actions.intent. にも当たります。同じ1行が2つの種類として台帳に並ぶと、件数が実態より多く見えます。
この2つがあったので、ファイル:行番号 を記録して、2回目以降は飛ばす形にしました。ただしこうすると、照合の順序がそのまま結果を決めます。先に当たった種類が、その行の名前になります。
だから表の並びは、具体的な入口から先に置いてあります。<capability を actions.intent. より前に置いているのはそのためです。上の実行結果で「旧 actions.xml 由来の記述」が 0 件になっているのは、その1行が先に capability として数えられたからで、記述が無いという意味ではありません。
自分のプロジェクトで走らせるときは、この並びを疑ってみてください。順序を入れ替えると数字が動く種類の集計です。
台帳を、実機で読み上げる発話に変える
一覧ができても、実機で何と言えばよいかがすぐには出てきません。ここは Gemini に任せます。台帳のうち未確認の行だけを読み、確かめるための発話を日本語と英語で作らせます。
まず、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"次は Android アプリ「{app_name}」で、"
"音声アシスタント経由で開かれる可能性のある入口の一覧です。\n"
"各項目について、実機で読み上げて動作を確かめるための発話を"
"日本語と英語で1つずつ作ってください。\n"
"アプリ名を必ず含め、実際に人が言いそうな短い言い方にしてください。\n\n"
+ "\n".join(lines)
)手元の CSV に対して走らせると、こういう文字列になります。
次は Android アプリ「壁紙アプリ」で、音声アシスタント経由で開かれる可能性のある入口の一覧です。
各項目について、実機で読み上げて動作を確かめるための発話を日本語と英語で1つずつ作ってください。
アプリ名を必ず含め、実際に人が言いそうな短い言い方にしてください。
1. [App Actions の capability] ./app/src/main/res/xml/shortcuts.xml:2 — <capability android:name="actions.intent.GET_THING">
2. [ASSIST インテント] ./app/src/main/AndroidManifest.xml:12 — <action android:name="android.intent.action.ASSIST" />
3. [静的ショートカット] ./app/src/main/res/xml/shortcuts.xml:5 — <shortcut android:shortcutId="daily" ...
4. [静的ショートカット] ./app/src/main/AndroidManifest.xml:8 — <meta-data android:name="android.app.shortcuts" ...
あとは投げるだけです。戻り値を手で読むのではなく、そのまま次の処理へ渡したいので、構造化出力にします。
import os
from google import genai
from pydantic import BaseModel
class Utterance(BaseModel):
surface: str
file: str
ja: str
en: 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, "壁紙アプリ"),
config={
"response_mime_type": "application/json",
"response_schema": list[Utterance],
},
)
for u in response.parsed:
print(f"[{u.surface}] {u.file}\n JA: {u.ja}\n EN: {u.en}\n")response_schema に list[Utterance] を渡しているので、返ってくるのは Utterance のリストです。JSON を自分でパースする行は要りません。
temperature を書いていない点だけ補足します。サンプリング系のパラメータは非推奨になりました。出力を安定させたいときは、値を絞りにいくよりも、こうしてスキーマで型を固定するほうが確実です。古いサンプルコードを写すときに、ここだけ引きずらないようにしています。
この記事とは別に、置き換えの前後で音声からの起動がどう変わったかを数字で残す方法もあります。切り替わってからでは対照群を作れないという点に関心があれば、Google アシスタントの置き換えは戻せないので、基準線は今しか取れませんのほうに、計測側の話をまとめてあります。
残り2週間で決めること
やることを1つに絞るなら、手元の端末を1台だけ先に Gemini へ寄せて、台帳の checked_on_device 列を埋め始めることです。
Google アプリまたは端末の設定から、9月4日を待たずに自分のタイミングで切り替えられます。全台を一度に切り替えてしまうと比較対象が無くなりますので、まず1台です。切り替えた端末で発話し、開いたか開かなかったかを台帳に書き込みます。開かなかった行が、9月4日以降に手を入れる候補になります。
数え上げは今日15分でできて、置き換わったあとにはもうできません。順番としては、こちらが先です。
私自身、4年前に自分で書いたタグの動作をまだ確かめきれていません。同じように積み残しを抱えている方の、最初の15分になれば嬉しく思います。