skills/gplay-screenshot-automation/SKILL.md
Automate Android screenshot capture across devices and locales using adb, Espresso/UI Automator, device framing, and gplay CLI upload. Use when building screenshot pipelines for Google Play listings.
npx skillsauth add tamtom/gplay-cli-skills gplay-screenshot-automationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use this skill for agent-driven screenshot workflows where Android screenshots are captured via emulators or connected devices, organized by locale and device type, and uploaded to Google Play via gplay.
adb shell screencap and Android test frameworks (Espresso, UI Automator).gplay images upload or gplay sync import-images../screenshots/raw./screenshots/framed./metadata# Phone (Pixel 7, API 34)
sdkmanager "system-images;android-34;google_apis;x86_64"
avdmanager create avd \
--name "pixel7_api34" \
--device "pixel_7" \
--package "system-images;android-34;google_apis;x86_64"
# 10-inch Tablet
avdmanager create avd \
--name "tablet10_api34" \
--device "pixel_tablet" \
--package "system-images;android-34;google_apis;x86_64"
# 7-inch Tablet
avdmanager create avd \
--name "tablet7_api34" \
--device "Nexus 7" \
--package "system-images;android-34;google_apis;x86_64"
emulator -avd pixel7_api34 -no-audio -no-window -gpu swiftshader_indirect &
adb wait-for-device
adb shell getprop sys.boot_completed # Wait until "1"
For headless CI environments, always use -no-window -no-audio -gpu swiftshader_indirect.
adb shell screencap -p /sdcard/screenshot.png
adb pull /sdcard/screenshot.png ./screenshots/raw/en-US/phone/home.png
adb shell rm /sdcard/screenshot.png
capture() {
local NAME="$1"
local LOCALE="$2"
local DEVICE_TYPE="$3"
local SERIAL="$4"
local OUTPUT_DIR="./screenshots/raw/$LOCALE/$DEVICE_TYPE"
mkdir -p "$OUTPUT_DIR"
adb -s "$SERIAL" shell screencap -p "/sdcard/$NAME.png"
adb -s "$SERIAL" pull "/sdcard/$NAME.png" "$OUTPUT_DIR/$NAME.png"
adb -s "$SERIAL" shell rm "/sdcard/$NAME.png"
echo "Captured $OUTPUT_DIR/$NAME.png"
}
# Usage
capture "home" "en-US" "phoneScreenshots" "emulator-5554"
capture "settings" "en-US" "phoneScreenshots" "emulator-5554"
capture "home" "en-US" "tenInchScreenshots" "emulator-5556"
For repeatable, state-driven screenshots, use Android instrumentation tests.
// app/src/androidTest/java/com/example/app/ScreenshotTest.kt
@RunWith(AndroidJUnit4::class)
class ScreenshotTest {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Test
fun captureHomeScreen() {
// Wait for content to load
onView(withId(R.id.main_content))
.check(matches(isDisplayed()))
takeScreenshot("home")
}
@Test
fun captureSearchScreen() {
onView(withId(R.id.search_button)).perform(click())
onView(withId(R.id.search_input)).perform(typeText("example"))
takeScreenshot("search")
}
private fun takeScreenshot(name: String) {
val bitmap = InstrumentationRegistry.getInstrumentation()
.uiAutomation.takeScreenshot()
val dir = File(
InstrumentationRegistry.getInstrumentation()
.targetContext.getExternalFilesDir(null),
"screenshots"
)
dir.mkdirs()
val file = File(dir, "$name.png")
file.outputStream().use {
bitmap.compress(Bitmap.CompressFormat.PNG, 100, it)
}
}
}
# Build and run instrumented tests
./gradlew connectedAndroidTest \
-Pandroid.testInstrumentationRunnerArguments.class=com.example.app.ScreenshotTest
# Pull screenshots from device
adb pull /sdcard/Android/data/com.example.app/files/screenshots/ ./screenshots/raw/en-US/phoneScreenshots/
@RunWith(AndroidJUnit4::class)
class UiAutomatorScreenshotTest {
private lateinit var device: UiDevice
@Before
fun setup() {
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
}
@Test
fun captureNotificationScreen() {
device.openNotification()
device.wait(Until.hasObject(By.pkg("com.android.systemui")), 3000)
takeScreenshot("notifications")
}
private fun takeScreenshot(name: String) {
val file = File(
InstrumentationRegistry.getInstrumentation()
.targetContext.getExternalFilesDir(null),
"screenshots/$name.png"
)
file.parentFile?.mkdirs()
device.takeScreenshot(file)
}
}
set_locale() {
local SERIAL="$1"
local LOCALE="$2" # e.g. "de-DE"
local LANG="${LOCALE%%-*}" # e.g. "de"
local REGION="${LOCALE##*-}" # e.g. "DE"
adb -s "$SERIAL" shell "setprop persist.sys.locale ${LANG}-${REGION}"
adb -s "$SERIAL" shell "setprop persist.sys.language ${LANG}"
adb -s "$SERIAL" shell "setprop persist.sys.country ${REGION}"
adb -s "$SERIAL" shell "settings put system system_locales ${LANG}-${REGION}"
# Restart the app to pick up locale change
adb -s "$SERIAL" shell am force-stop com.example.app
adb -s "$SERIAL" shell am start -n com.example.app/.MainActivity
sleep 3
}
#!/bin/bash
# multi-locale-capture.sh
SERIAL="emulator-5554"
PACKAGE="com.example.app"
LOCALES=("en-US" "de-DE" "fr-FR" "es-ES" "ja" "ko" "pt-BR" "zh-CN")
for LOCALE in "${LOCALES[@]}"; do
echo "Capturing locale: $LOCALE"
set_locale "$SERIAL" "$LOCALE"
mkdir -p "./screenshots/raw/$LOCALE/phoneScreenshots"
# Capture each screen
for SCREEN in "home" "search" "settings" "profile"; do
adb -s "$SERIAL" shell screencap -p "/sdcard/$SCREEN.png"
adb -s "$SERIAL" pull "/sdcard/$SCREEN.png" \
"./screenshots/raw/$LOCALE/phoneScreenshots/$SCREEN.png"
adb -s "$SERIAL" shell rm "/sdcard/$SCREEN.png"
# Navigate to next screen (app-specific logic)
# adb -s "$SERIAL" shell input tap X Y
done
echo "Done: $LOCALE"
done
# Run screenshot tests with a specific locale
./gradlew connectedAndroidTest \
-Pandroid.testInstrumentationRunnerArguments.class=com.example.app.ScreenshotTest \
-Pandroid.testInstrumentationRunnerArguments.locale=de-DE
#!/bin/bash
# multi-device-capture.sh
declare -A DEVICES=(
["phoneScreenshots"]="emulator-5554" # Pixel 7
["tenInchScreenshots"]="emulator-5556" # Pixel Tablet
["sevenInchScreenshots"]="emulator-5558" # Nexus 7
["tvScreenshots"]="emulator-5560" # Android TV
["wearScreenshots"]="emulator-5562" # Wear OS
)
LOCALE="en-US"
for DEVICE_TYPE in "${!DEVICES[@]}"; do
SERIAL="${DEVICES[$DEVICE_TYPE]}"
echo "Capturing $DEVICE_TYPE on $SERIAL"
mkdir -p "./screenshots/raw/$LOCALE/$DEVICE_TYPE"
for SCREEN in "home" "search" "settings"; do
adb -s "$SERIAL" shell screencap -p "/sdcard/$SCREEN.png"
adb -s "$SERIAL" pull "/sdcard/$SCREEN.png" \
"./screenshots/raw/$LOCALE/$DEVICE_TYPE/$SCREEN.png"
adb -s "$SERIAL" shell rm "/sdcard/$SCREEN.png"
done
echo "Done: $DEVICE_TYPE"
done
Use third-party tools to wrap raw screenshots in device frames for polished store listings.
gem install fastlane
# Place Framefile.json alongside screenshots
cat > ./screenshots/raw/Framefile.json << 'EOF'
{
"device_frame_version": "latest",
"default": {
"keyword": { "font": "./fonts/SF-Pro-Display-Bold.otf" },
"title": { "font": "./fonts/SF-Pro-Display-Regular.otf" }
}
}
EOF
cd ./screenshots/raw && fastlane frameit
#!/bin/bash
# frame-screenshots.sh
# Requires ImageMagick
FRAME_IMAGE="./frames/pixel7_frame.png"
RAW_DIR="./screenshots/raw"
FRAMED_DIR="./screenshots/framed"
for LOCALE_DIR in "$RAW_DIR"/*/; do
LOCALE=$(basename "$LOCALE_DIR")
for TYPE_DIR in "$LOCALE_DIR"*/; do
TYPE=$(basename "$TYPE_DIR")
mkdir -p "$FRAMED_DIR/$LOCALE/$TYPE"
for IMG in "$TYPE_DIR"*.png; do
NAME=$(basename "$IMG")
convert "$FRAME_IMAGE" "$IMG" \
-gravity center -geometry +0+0 -composite \
"$FRAMED_DIR/$LOCALE/$TYPE/$NAME"
echo "Framed: $FRAMED_DIR/$LOCALE/$TYPE/$NAME"
done
done
done
Always validate screenshots before uploading:
gplay validate screenshots --dir ./screenshots/framed --output table
Check specific locale:
gplay validate screenshots --dir ./screenshots/framed --locale en-US --output table
Organize screenshots in FastLane format:
metadata/
en-US/
images/
phoneScreenshots/
1_home.png
2_search.png
tenInchScreenshots/
1_home.png
de-DE/
images/
phoneScreenshots/
1_home.png
2_search.png
Then import:
gplay sync import-images \
--package com.example.app \
--dir ./metadata
EDIT_ID=$(gplay edits create --package com.example.app | jq -r '.id')
# Upload phone screenshots
gplay images upload \
--package com.example.app \
--edit $EDIT_ID \
--locale en-US \
--type phoneScreenshots \
--file ./screenshots/framed/en-US/phoneScreenshots/home.png
gplay images upload \
--package com.example.app \
--edit $EDIT_ID \
--locale en-US \
--type phoneScreenshots \
--file ./screenshots/framed/en-US/phoneScreenshots/search.png
# Upload tablet screenshots
gplay images upload \
--package com.example.app \
--edit $EDIT_ID \
--locale en-US \
--type tenInchScreenshots \
--file ./screenshots/framed/en-US/tenInchScreenshots/home.png
# Upload feature graphic
gplay images upload \
--package com.example.app \
--edit $EDIT_ID \
--locale en-US \
--type featureGraphic \
--file ./screenshots/framed/en-US/featureGraphic.png
# Validate and commit
gplay edits validate --package com.example.app --edit $EDIT_ID
gplay edits commit --package com.example.app --edit $EDIT_ID
gplay release \
--package com.example.app \
--track production \
--bundle app-release.aab \
--screenshots-dir ./metadata \
--release-notes @release-notes.json
Delete before uploading to replace:
EDIT_ID=$(gplay edits create --package com.example.app | jq -r '.id')
# Delete all existing phone screenshots for a locale
gplay images delete-all \
--package com.example.app \
--edit $EDIT_ID \
--locale en-US \
--type phoneScreenshots
# Upload new ones
gplay images upload \
--package com.example.app \
--edit $EDIT_ID \
--locale en-US \
--type phoneScreenshots \
--file ./screenshots/framed/en-US/phoneScreenshots/home.png
gplay edits validate --package com.example.app --edit $EDIT_ID
gplay edits commit --package com.example.app --edit $EDIT_ID
#!/bin/bash
# screenshot-pipeline.sh
# End-to-end: boot emulators, capture, frame, validate, upload
PACKAGE="com.example.app"
SERIAL="emulator-5554"
LOCALES=("en-US" "de-DE" "fr-FR" "es-ES" "ja")
RAW_DIR="./screenshots/raw"
METADATA_DIR="./metadata"
# Step 1: Boot emulator
emulator -avd pixel7_api34 -no-audio -no-window -gpu swiftshader_indirect &
adb wait-for-device
adb -s "$SERIAL" shell "while [[ \$(getprop sys.boot_completed) != '1' ]]; do sleep 1; done"
# Step 2: Build and install app
./gradlew assembleRelease
adb -s "$SERIAL" install -r app/build/outputs/apk/release/app-release.apk
# Step 3: Capture per locale
for LOCALE in "${LOCALES[@]}"; do
set_locale "$SERIAL" "$LOCALE"
mkdir -p "$RAW_DIR/$LOCALE/phoneScreenshots"
for SCREEN in "home" "search" "settings" "profile"; do
adb -s "$SERIAL" shell screencap -p "/sdcard/$SCREEN.png"
adb -s "$SERIAL" pull "/sdcard/$SCREEN.png" "$RAW_DIR/$LOCALE/phoneScreenshots/$SCREEN.png"
adb -s "$SERIAL" shell rm "/sdcard/$SCREEN.png"
done
done
# Step 4: Organize into FastLane metadata structure
for LOCALE in "${LOCALES[@]}"; do
mkdir -p "$METADATA_DIR/$LOCALE/images/phoneScreenshots"
cp "$RAW_DIR/$LOCALE/phoneScreenshots/"*.png "$METADATA_DIR/$LOCALE/images/phoneScreenshots/"
done
# Step 5: Validate
gplay validate screenshots --dir "$METADATA_DIR" --output table
# Step 6: Upload
gplay sync import-images --package "$PACKAGE" --dir "$METADATA_DIR"
# Step 7: Kill emulator
adb -s "$SERIAL" emu kill
echo "Screenshot pipeline complete."
name: Screenshot Pipeline
on:
workflow_dispatch:
schedule:
- cron: '0 4 * * 1' # Weekly Monday 4am
jobs:
screenshots:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: AVD cache
uses: actions/cache@v4
with:
path: |
~/.android/avd/*
~/.android/adb*
key: avd-api-34
- name: Create AVD
run: |
sdkmanager "system-images;android-34;google_apis;x86_64"
avdmanager create avd -n pixel7_api34 -d pixel_7 \
--package "system-images;android-34;google_apis;x86_64" --force
- name: Build app
run: ./gradlew assembleRelease
- name: Capture screenshots
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
target: google_apis
arch: x86_64
profile: pixel_7
script: ./scripts/capture-screenshots.sh
- name: Validate screenshots
run: gplay validate screenshots --dir ./metadata --output table
- name: Upload to Play Store
run: |
gplay sync import-images \
--package ${{ secrets.PACKAGE_NAME }} \
--dir ./metadata
env:
GPLAY_SERVICE_ACCOUNT: ${{ secrets.GPLAY_SERVICE_ACCOUNT_PATH }}
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: screenshots
path: ./screenshots/
| Type | Usage |
|------|-------|
| phoneScreenshots | Phone screenshots (required, 2-8) |
| sevenInchScreenshots | 7-inch tablet screenshots |
| tenInchScreenshots | 10-inch tablet screenshots |
| tvScreenshots | Android TV screenshots |
| wearScreenshots | Wear OS screenshots |
| featureGraphic | Feature graphic (1024x500) |
| promoGraphic | Promo graphic (180x120) |
| icon | App icon (512x512, usually set in Console) |
| tvBanner | TV banner (1280x720) |
--help before running commands.gplay validate screenshots before uploading.gplay sync import-images for bulk uploads over individual gplay images upload calls.--output table for user review.--package, --edit, --locale, --type, --file).sys.boot_completed == 1) before capturing.gplay images list to verify uploaded images.gplay images delete-all to clear screenshots before re-uploading.--help to verify flags for the exact command.development
App vitals monitoring for crashes, ANRs, performance metrics, and errors via gplay vitals commands. Use when asked to check app stability, crash rates, ANR rates, or performance data from Google Play Console.
development
User and grant management for Google Play Console via gplay users and gplay grants commands. Use when asked to manage developer account users, permissions, or app-level access grants.
development
Beta testing groups and tester management for Google Play closed testing tracks. Use when managing testers and beta groups.
tools
Bulk-localize subscription display names, descriptions, and offer tags across all Google Play locales using gplay. Use when you want to fill in subscription metadata for every language without clicking through Play Console manually.