Firebase App Distribution の落とし穴 4 選(3/3 実践編)
Flutter + Firebase App Distribution の配信パイプラインを構築した後に直面しやすい 4 つの落とし穴と、その解決策を実体験ベースで解説します。フィードバック SDK のスクリーンショット黒塗り問題から CI での Bash 変数衝突まで、公式ドキュメントだけでは辿り着けない実践知をまとめました。
| 回 | テーマ | 内容 |
|---|---|---|
| 第1回 | 設計編 | 配信フローの全体設計と技術選定 |
| 第2回 | 実装編 | 配信スクリプト・Gradle 署名設定・GitHub Actions |
| 第3回(本記事) | 実践編 | ハマりポイント4事例と ADB 検証の自動化 |
フィードバック SDK のスクリーンショットが黒塗りになるのはなぜ?
配信パイプラインを構築し、テスターにフィードバック SDK を使ってもらうと、送信されたスクリーンショットが真っ黒、という報告が上がります。ギャラリーからの画像添付は正常なのにスクリーンショット撮影だけ黒くなるため、原因の特定に時間がかかりがちです。
原因: FlutterSurfaceView の GPU 直接描画
Flutter はデフォルトで FlutterSurfaceView を使い、GPU バッファに直接描画します。一方、Firebase フィードバック SDK は Android ネイティブの View.draw() 系 API でスクリーンショットを取得します。GPU バッファの内容はこの API では読み取れないため、Flutter が描画した領域が黒塗りになります。
| 描画方式 | レンダリング先 |
View.draw() でのキャプチャ |
パフォーマンス |
|---|---|---|---|
FlutterSurfaceView(デフォルト) |
GPU バッファ | 不可(黒塗り) | 最高 |
FlutterImageView(RenderMode.image) |
ImageReader → Canvas
|
可能 | やや劣る |
出典: FlutterSurfaceView API、FlutterImageView API
解決策: RenderMode.image への切り替え
MainActivity.kt で getRenderMode() をオーバーライドし、RenderMode.image を返すようにします。
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.android.RenderMode
class MainActivity : FlutterActivity() {
override fun getRenderMode(): RenderMode {
return RenderMode.image
}
}
RenderMode.image は ImageReader 経由で Canvas に描画するモードです。Canvas ベースの描画になるため、Android 標準の View.draw() API でスクリーンショットを正常にキャプチャできます。
関連 Issue: firebase/firebase-android-sdk#5386、firebase/flutterfire#11687
Android 13 以降でテスターに通知が届かないのはなぜ?
テスターから「新しいビルドの通知が来ない」と報告されたら、まず Android のバージョンを確認してください。Android 13(API 33)以降では、通知の送信にランタイム権限 POST_NOTIFICATIONS の明示的な許可が必要です。
原因: POST_NOTIFICATIONS 権限の未取得
Android 13 以降では、アプリが通知を送信するには POST_NOTIFICATIONS 権限をユーザーから取得する必要があります。この権限がないと、Firebase App Distribution の新規ビルド通知を含むすべての通知がブロックされます。
出典: Notification runtime permission(Android Developers)
解決策: 動的権限要求の実装
まず AndroidManifest.xml に権限を宣言します。
<manifest ...>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<application ...>
...
</application>
</manifest>
次に、Flutter 側で permission_handler(v12.0.1)を使って動的に権限を要求します。
まず依存パッケージを追加します。
flutter pub add permission_handler
次に権限要求ロジックを実装します。
import 'package:permission_handler/permission_handler.dart';
Future<void> requestNotificationPermission() async {
var status = await Permission.notification.status;
if (status.isDenied) {
status = await Permission.notification.request(); // 戻り値を再代入して最新の状態を取得
}
// ユーザーが「今後表示しない」を選択した場合、設定画面へ誘導
if (status.isPermanentlyDenied) {
openAppSettings();
}
}
ADB を使ったテスト方法も押さえておくと便利です。
# 権限を取り消し(新規インストール状態をシミュレート)
adb shell pm revoke com.example.app android.permission.POST_NOTIFICATIONS
# 権限を付与(アップグレード状態をシミュレート)
adb shell pm grant com.example.app android.permission.POST_NOTIFICATIONS
CI の配信スクリプトで変数が空になるのはなぜ?
ローカルでは問題なく動いていた配信スクリプトが、GitHub Actions では --groups に空文字が渡されて配信に失敗します。原因は Bash のビルトイン変数 GROUPS との名前衝突です。
原因: GROUPS は Bash の予約済みビルトイン変数
Bash には GROUPS というビルトイン変数が存在し、現在のユーザーが所属するグループ ID の配列を保持しています。GNU Bash 公式マニュアルには「Assignments to GROUPS have no effect」と明記されており、この変数への代入は無視されます。
出典: Bash Variables(GNU Bash Manual)
さらに厄介なのは、macOS と GitHub Actions で挙動が異なる点です。
| 環境 | Bash バージョン |
GROUPS="qa-team" の挙動 |
|---|---|---|
| macOS | 3.2.x | 代入が黙って無視される(エラーなし) |
| GitHub Actions(Ubuntu) | 5.x | 代入が無視される。環境によっては readonly variable エラー |
| sh(dash) | N/A | 正常に動作(GROUPS は予約されていない) |
ローカルの macOS では代入が黙って無視されるため、デフォルト値 ${GROUPS:-qa-team} で qa-team が使われて問題が表面化しません。しかし GitHub Actions の Ubuntu 環境(Bash 5.x)では、GROUPS にビルトインの配列値が入っているため、意図しない値が --groups に渡されます。
解決策: 変数名を TESTER_GROUPS にリネーム
# NG: GROUPS は Bash ビルトイン変数
GROUPS="qa-team"
echo "$GROUPS" # macOS Bash 3.2: プライマリグループ ID の数値(例: 20)、Ubuntu Bash 5.x: グループ ID の配列
# OK: プレフィックスをつけて衝突を回避
TESTER_GROUPS="qa-team"
echo "$TESTER_GROUPS" # どの環境でも "qa-team"
第2回で紹介した配信スクリプトの該当箇所を修正する場合は、以下のように変更します。
# 修正前
GROUPS="${GROUPS:-qa-team}"
firebase appdistribution:distribute "$APK_PATH" \
--groups "$GROUPS"
# 修正後
TESTER_GROUPS="${TESTER_GROUPS:-qa-team}"
firebase appdistribution:distribute "$APK_PATH" \
--groups "$TESTER_GROUPS"
サービスアカウントで 403 エラーが出るのはなぜ?
GitHub Actions から firebase appdistribution:distribute を実行すると PERMISSION_DENIED: The caller does not have permission が返る。サービスアカウントを作成して Secrets に登録したのに、なぜ権限が足りないのか。
原因: 必要な IAM ロールの不足
サービスアカウントの作成だけでは不十分です。Firebase App Distribution への操作に必要な IAM ロールを明示的に付与する必要があります。
| ロール | 役割 | 付与しないと |
|---|---|---|
roles/firebaseappdistro.admin |
APK アップロード・テスター管理 |
PERMISSION_DENIED で配信失敗 |
roles/serviceusage.serviceUsageConsumer |
Firebase API の呼び出し権限 | API 呼び出し自体が拒否される |
出典: Firebase App Distribution roles(Cloud IAM)
roles/serviceusage.serviceUsageConsumer は見落としやすいロールです。このロールが必要になった背景には、Firebase-admin SDK 側の仕様変更があります(参考: firebase-admin-node Discussion #2624)。
解決策: GCP Console で IAM ロールを付与
- GCP IAM コンソール を開く
- 対象のサービスアカウントを選択
- 「ロールを追加」から以下を付与する
-
Firebase App Distribution 管理者(
roles/firebaseappdistro.admin) -
Service Usage ユーザー(
roles/serviceusage.serviceUsageConsumer)
-
Firebase App Distribution 管理者(
- 保存して数分待つ
ADB でフィードバック送信を自動検証する方法は?
ハマりポイント 1 で紹介した RenderMode.image の設定後、フィードバック SDK が正しく動作するか手動で毎回確認するのは非効率です。ADB と uiautomator dump を組み合わせることで、フィードバック送信の動作確認を自動化できます。
検証スクリプトの全体像
以下のコードブロックを順番に scripts/send_feedback_adb.sh という 1 つのファイルとして保存し、実行権限を付与してください。
chmod +x scripts/send_feedback_adb.sh
スクリプトは、フィードバックフォームの起動からボタンタップ、送信完了の確認までを自動で行います。
#!/bin/bash
set -euo pipefail
PACKAGE_NAME="${1:-com.example.app}"
DEVICE_ID="${2:-}"
ADB="adb"
if [ -n "$DEVICE_ID" ]; then
ADB="adb -s $DEVICE_ID"
fi
echo "[INFO] フィードバックフォームを起動します..."
$ADB shell input keyevent 82 # メニューキー(Android KeyEvent.KEYCODE_MENU)でフォームを起動
sleep 2
冒頭部分では、パッケージ名とデバイス ID を引数で受け取ります。複数デバイスが接続されている場合は $DEVICE_ID を指定してターゲットを絞ります。
uiautomator dump で UI 構造を取得する
uiautomator dump は、現在表示されている画面の UI 要素を XML 形式で出力するコマンドです。各要素のテキスト、座標(bounds 属性)、クリック可否などの情報が含まれます。
echo "[INFO] UI 構造を取得します..."
$ADB shell uiautomator dump /sdcard/window_dump.xml
$ADB pull /sdcard/window_dump.xml /tmp/window_dump.xml
取得した XML は以下のような構造です。
<node text="送信"
class="android.widget.Button"
clickable="true"
bounds="[63,1626][1017,1752]">
</node>
bounds="[63,1626][1017,1752]" は要素の左上座標 (63, 1626) と右下座標 (1017, 1752) を示しています。
Python で送信ボタンの座標を算出する
XML から「送信」ボタンを検索し、bounds 属性から中心座標を計算します。
COORDS=$(python3 - <<'EOF'
import xml.etree.ElementTree as ET
import re
tree = ET.parse("/tmp/window_dump.xml")
for elem in tree.iter("node"):
text = elem.get("text", "")
if text in ["送信", "Submit", "送信する"]:
bounds = elem.get("bounds", "")
nums = re.findall(r'\d+', bounds)
if len(nums) == 4:
x = (int(nums[0]) + int(nums[2])) // 2
y = (int(nums[1]) + int(nums[3])) // 2
print(f"{x} {y}")
break
EOF
)
if [ -z "$COORDS" ]; then
echo "[ERROR] 送信ボタンが見つかりません"
exit 1
fi
X=$(echo "$COORDS" | cut -d' ' -f1)
Y=$(echo "$COORDS" | cut -d' ' -f2)
echo "[INFO] 送信ボタンをタップします: ($X, $Y)"
$ADB shell input tap "$X" "$Y"
中心座標は (x1 + x2) / 2、(y1 + y2) / 2 で算出します。bounds="[63,1626][1017,1752]" の場合、中心は (540, 1689) です。adb shell input tap でこの座標をタップすることで、送信ボタンの押下をシミュレートします。
送信完了の確認
送信後に再度 uiautomator dump を実行し、送信ボタンが画面から消えたか(フォームが閉じたか)を確認します。
sleep 1
echo "[INFO] フィードバック送信を確認します..."
$ADB shell uiautomator dump /sdcard/window_dump_after.xml
$ADB pull /sdcard/window_dump_after.xml /tmp/window_dump_after.xml
python3 - <<'EOF2'
import xml.etree.ElementTree as ET
tree = ET.parse("/tmp/window_dump_after.xml")
for elem in tree.iter("node"):
text = elem.get("text", "")
if text in ["送信", "Submit", "送信する"]:
print("[WARN] 送信ボタンがまだ表示されています。送信に失敗した可能性があります。")
exit(1) # CI で失敗として検知させる
print("[INFO] フィードバック送信が完了しました。")
EOF2
実行例を示します。
# デフォルトのパッケージ名で実行
./scripts/send_feedback_adb.sh
# パッケージ名とデバイス ID を指定
./scripts/send_feedback_adb.sh com.example.myapp emulator-5554
まとめ
全3回の連載を通じて、Firebase App Distribution による Flutter アプリの配信自動化を設計から実践まで解説しました。
本記事で取り上げた 4 つの落とし穴を振り返ります。
| 落とし穴 | 原因 | 解決策 |
|---|---|---|
| スクリーンショット黒塗り |
FlutterSurfaceView の GPU 直接描画 |
RenderMode.image に切り替え |
| テスター通知が届かない | Android 13+ の POST_NOTIFICATIONS 権限 |
permission_handler で動的要求 |
| CI で変数が空になる | Bash ビルトイン変数 GROUPS との衝突 |
TESTER_GROUPS にリネーム |
| サービスアカウントで 403 | IAM ロール(firebaseappdistro.admin + serviceUsageConsumer)の不足 |
GCP Console で両ロールを付与 |
連載全体のロードマップも整理します。
- 第1回(設計編): 手動配布の課題を整理し、Firebase App Distribution を中心としたアーキテクチャを設計しました
- 第2回(実装編): 配信シェルスクリプト・Gradle ハイブリッド署名・GitHub Actions ワークフローを実装しました
- 第3回(本記事): 実運用で遭遇する落とし穴 4 選と ADB による自動検証を紹介しました
これらを組み合わせることで、「Slack に APK を貼る」運用から「main へのマージで自動配信、テスターからのフィードバックは Firebase Console に集約」という仕組みへ移行できます。
連載全3回を読んでいただきありがとうございました。実際に導入してみて「ここでハマった」「こう改善した」といった経験があれば、ぜひコメントで共有してください。
Discussion