|
| 1 | +#!/usr/bin/env zsh |
| 2 | + |
| 3 | +set -euo pipefail |
| 4 | + |
| 5 | +# pull-apks-by-user.sh |
| 6 | +# Usage: |
| 7 | +# ./pull-apks-by-user.sh <search-term> [output-dir] |
| 8 | +# |
| 9 | +# Examples: |
| 10 | +# ./pull-apks-by-user.sh filemacros |
| 11 | +# ./pull-apks-by-user.sh com.virtualize ./tab-s9-apks |
| 12 | +# |
| 13 | +# Notes: |
| 14 | +# - Attempts to pull APK dirs per Android user/profile. |
| 15 | +# - Some profiles (e.g., Samsung Secure Folder / Knox) may deny shell access; those get skipped. |
| 16 | + |
| 17 | +SEARCH_TERM="${1:-}" |
| 18 | +OUT_ROOT="${2:-./pulled-apks}" |
| 19 | + |
| 20 | +if [[ -z "$SEARCH_TERM" ]]; then |
| 21 | + echo "Usage: $0 <search-term> [output-dir]" >&2 |
| 22 | + exit 2 |
| 23 | +fi |
| 24 | + |
| 25 | +command -v adb >/dev/null 2>&1 || { echo "adb not found in PATH" >&2; exit 1; } |
| 26 | + |
| 27 | +mkdir -p "$OUT_ROOT" |
| 28 | + |
| 29 | +safe_name() { |
| 30 | + local s="$1" |
| 31 | + echo "$s" | tr -c 'A-Za-z0-9._-' '_' |
| 32 | +} |
| 33 | + |
| 34 | +# Best-effort user type guesser using the label from cmd user list |
| 35 | +guess_user_type() { |
| 36 | + local label="$1" |
| 37 | + local id="$2" |
| 38 | + |
| 39 | + # Normalize |
| 40 | + local l |
| 41 | + l="$(echo "$label" | tr '[:upper:]' '[:lower:]')" |
| 42 | + |
| 43 | + if [[ "$id" == "0" ]]; then |
| 44 | + echo "owner" |
| 45 | + return |
| 46 | + fi |
| 47 | + |
| 48 | + if echo "$l" | grep -qE 'work|managed|profile'; then |
| 49 | + echo "work" |
| 50 | + return |
| 51 | + fi |
| 52 | + |
| 53 | + if echo "$l" | grep -qE 'secure folder|securefolder|knox'; then |
| 54 | + echo "secure" |
| 55 | + return |
| 56 | + fi |
| 57 | + |
| 58 | + # Common Samsung patterns sometimes show "UserInfo{150:...}" without clear label |
| 59 | + echo "user" |
| 60 | +} |
| 61 | + |
| 62 | +echo "==> Output dir: $OUT_ROOT" |
| 63 | +echo "==> Search term: $SEARCH_TERM" |
| 64 | +echo |
| 65 | + |
| 66 | +# Get list of users: lines like "UserInfo{0:Owner:13} running" |
| 67 | +USER_LIST_RAW="$(adb shell cmd user list 2>/dev/null || true)" |
| 68 | + |
| 69 | +if [[ -z "$USER_LIST_RAW" ]]; then |
| 70 | + echo "!! Failed to read user list (adb shell cmd user list)." >&2 |
| 71 | + exit 1 |
| 72 | +fi |
| 73 | + |
| 74 | +# Parse user ids + labels |
| 75 | +# Extract: id and label inside UserInfo{<id>:<label>:...} |
| 76 | +lines="$(echo "$USER_LIST_RAW" \ |
| 77 | + | sed -En 's/.*UserInfo{([0-9]+):([^:}]*).*/\1|\2/p' \ |
| 78 | + | sort -n)" |
| 79 | +USERS=("${(f)lines}") |
| 80 | + |
| 81 | +if [[ ${#USERS[@]} -eq 0 ]]; then |
| 82 | + echo "!! Could not parse any users from cmd user list output:" >&2 |
| 83 | + echo "$USER_LIST_RAW" >&2 |
| 84 | + exit 1 |
| 85 | +fi |
| 86 | + |
| 87 | +echo "==> Detected users:" |
| 88 | +for U in "${USERS[@]}"; do |
| 89 | + USER_ID="${U%%|*}" |
| 90 | + ULABEL="${U#*|}" |
| 91 | + UTYPE="$(guess_user_type "$ULABEL" "$USER_ID")" |
| 92 | + echo " - u$USER_ID ($UTYPE) label='$ULABEL'" |
| 93 | +done |
| 94 | +echo |
| 95 | + |
| 96 | +TS_GLOBAL="$(date +%Y%m%d-%H%M%S)" |
| 97 | + |
| 98 | +# For each user, try to list packages and filter by SEARCH_TERM |
| 99 | +for U in "${USERS[@]}"; do |
| 100 | + USER_ID="${U%%|*}" |
| 101 | + ULABEL="${U#*|}" |
| 102 | + UTYPE="$(guess_user_type "$ULABEL" "$USER_ID")" |
| 103 | + |
| 104 | + echo "------------------------------------------------------------" |
| 105 | + echo "==> Scanning user u$USER_ID ($UTYPE) label='$ULABEL'" |
| 106 | + |
| 107 | + # Listing packages per user can be blocked for some users (Secure Folder/Knox) |
| 108 | + # Capture stderr to detect SecurityException. |
| 109 | + LIST_OUT="$( |
| 110 | + adb shell pm list packages --user "$USER_ID" 2>&1 || true |
| 111 | + )" |
| 112 | + |
| 113 | + if echo "$LIST_OUT" | grep -q 'SecurityException'; then |
| 114 | + echo "!! Skipping u$USER_ID ($UTYPE): shell lacks permission (SecurityException)." |
| 115 | + continue |
| 116 | + fi |
| 117 | + |
| 118 | + if [[ -z "$LIST_OUT" ]]; then |
| 119 | + echo "!! Skipping u$USER_ID ($UTYPE): no output from pm list packages." |
| 120 | + continue |
| 121 | + fi |
| 122 | + |
| 123 | + pkg_lines="$(echo "$LIST_OUT" \ |
| 124 | + | sed 's/^package://g' \ |
| 125 | + | grep -i -- "$SEARCH_TERM" \ |
| 126 | + | sort -u)" |
| 127 | + PACKAGES=("${(f)pkg_lines}") |
| 128 | + |
| 129 | + if [[ ${#PACKAGES[@]} -eq 0 ]]; then |
| 130 | + echo "No matches in u$USER_ID." |
| 131 | + continue |
| 132 | + fi |
| 133 | + |
| 134 | + echo "Matched ${#PACKAGES[@]} package(s) in u$USER_ID:" |
| 135 | + printf ' - %s\n' "${PACKAGES[@]}" |
| 136 | + echo |
| 137 | + |
| 138 | + for PKG in "${PACKAGES[@]}"; do |
| 139 | + echo "==> Resolving paths for $PKG (u$USER_ID)" |
| 140 | + |
| 141 | + # pm path per user can also be blocked even if list packages worked, so guard it. |
| 142 | + PATH_OUT="$( |
| 143 | + adb shell pm path --user "$USER_ID" "$PKG" 2>&1 || true |
| 144 | + )" |
| 145 | + |
| 146 | + if echo "$PATH_OUT" | grep -q 'SecurityException'; then |
| 147 | + echo "!! Cannot read paths for $PKG in u$USER_ID: SecurityException" |
| 148 | + continue |
| 149 | + fi |
| 150 | + |
| 151 | + if ! echo "$PATH_OUT" | grep -q '^package:'; then |
| 152 | + echo "!! No package paths returned for $PKG in u$USER_ID" |
| 153 | + continue |
| 154 | + fi |
| 155 | + |
| 156 | + apk_lines="$(echo "$PATH_OUT" | sed 's/^package://g' | sed 's/\r$//g')" |
| 157 | + APK_PATHS=("${(f)apk_lines}") |
| 158 | + |
| 159 | + FIRST_PATH="${APK_PATHS[0]}" |
| 160 | + INSTALL_DIR="$(echo "$FIRST_PATH" | sed 's#/[^/]*\.apk$##')" |
| 161 | + |
| 162 | + if [[ -z "$INSTALL_DIR" ]]; then |
| 163 | + echo "!! Failed to deduce install dir for $PKG in u$USER_ID" |
| 164 | + continue |
| 165 | + fi |
| 166 | + |
| 167 | + TS="$(date +%Y%m%d-%H%M%S)" |
| 168 | + PKG_SAFE="$(safe_name "$PKG")" |
| 169 | + OUT_DIR="$OUT_ROOT/${PKG_SAFE}_${UTYPE}_u${USER_ID}_${TS}" |
| 170 | + mkdir -p "$OUT_DIR" |
| 171 | + |
| 172 | + echo "==> Pulling install dir:" |
| 173 | + echo " $INSTALL_DIR" |
| 174 | + echo "==> -> $OUT_DIR" |
| 175 | + |
| 176 | + # Pull the whole dir (base + splits). This is the installed code location. |
| 177 | + adb pull "$INSTALL_DIR" "$OUT_DIR" >/dev/null |
| 178 | + |
| 179 | + # Metadata snapshot |
| 180 | + { |
| 181 | + echo "package=$PKG" |
| 182 | + echo "user_id=$USER_ID" |
| 183 | + echo "user_label=$ULABEL" |
| 184 | + echo "user_type=$UTYPE" |
| 185 | + echo "install_dir=$INSTALL_DIR" |
| 186 | + echo "pulled_at=$TS" |
| 187 | + echo |
| 188 | + echo "== pm path --user $USER_ID ==" |
| 189 | + echo "$PATH_OUT" |
| 190 | + echo |
| 191 | + echo "== dumpsys package (summary) ==" |
| 192 | + adb shell dumpsys package "$PKG" | sed -n '1,160p' | sed 's/\r$//g' || true |
| 193 | + echo |
| 194 | + echo "== user list ==" |
| 195 | + echo "$USER_LIST_RAW" |
| 196 | + } > "$OUT_DIR/metadata.txt" |
| 197 | + |
| 198 | + echo "==> Done: $PKG (u$USER_ID)" |
| 199 | + echo |
| 200 | + done |
| 201 | +done |
| 202 | + |
| 203 | +echo "============================================================" |
| 204 | +echo "Done. Output in: $OUT_ROOT" |
| 205 | +echo "Timestamp base: $TS_GLOBAL" |
0 commit comments