Skip to content

Instantly share code, notes, and snippets.

@Noah-Huppert
Last active December 25, 2023 07:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Noah-Huppert/79f61ffb3456eca74ad198b980220bd2 to your computer and use it in GitHub Desktop.
Save Noah-Huppert/79f61ffb3456eca74ad198b980220bd2 to your computer and use it in GitHub Desktop.
Script which gets the name and Google Play Store link of every third party app installed on your phone.

Instructions

Edit list-apps.py with your own values for BIN_ADB and BIN_AAPT. Then run the script with your phone plugged in on debug mode.

This will output a CSV with the name, package, and Google Play store link for each app. For some apps the name can not be determined so the package name will be used instead.

#!/usr/bin/env python
from typing import List, TypedDict, Optional
import subprocess
import csv
import argparse
import logging
import os
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
BIN_ADB = "$HOME/.local/lib/android-sdk/platform-tools/adb"
BIN_AAPT = "$HOME/.local/lib/android-sdk/build-tools/30.0.3/aapt"
class ShellRunRes(TypedDict):
stdout: Optional[List[str]]
stderr: Optional[List[str]]
return_code: int
def _shell_run(
cmd: List[str],
non_zero_exception: Optional[Exception]=None,
require_stdout_exception: Optional[Exception]=None,
) -> ShellRunRes:
proc_args = [
"bash",
"-c",
" ".join(cmd),
]
logger.debug("shell run %s", proc_args)
proc = subprocess.Popen(
proc_args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
raw_stdout, raw_stderr = proc.communicate()
stdout = raw_stdout.decode().split("\n") if raw_stdout is not None else None
stderr = raw_stderr.decode().split("\n") if raw_stderr is not None else None
if proc.returncode != 0 and non_zero_exception is not None:
raise non_zero_exception(stderr)
if stdout is None or len(stdout) == 0 and require_stdout_exception is not None:
raise require_stdout_exception("stdout empty")
return ShellRunRes(
stdout=stdout,
stderr=stderr,
return_code=proc.returncode,
)
class ListThirdPartyAppsError(Exception):
pass
def list_third_party_apps() -> List[str]:
res = _shell_run(
[
BIN_ADB,
"shell",
"pm",
"list",
"package",
"-3",
],
non_zero_exception=ListThirdPartyAppsError,
require_stdout_exception=ListThirdPartyAppsError,
)
return list(map(lambda line: line.replace("package:", ""), res["stdout"]))
class GetDeviceAPKPathError(Exception):
pass
def get_device_apk_paths(pkg: str) -> List[str]:
res = _shell_run(
[
BIN_ADB,
"shell",
"pm",
"path",
pkg,
],
non_zero_exception=GetDeviceAPKPathError,
require_stdout_exception=GetDeviceAPKPathError,
)
return list(map(lambda line: line.replace("package:", ""), res["stdout"]))
class DownloadAPKToHostError(Exception):
pass
def download_apk_to_host(device_apk_path: str, host_apk_path: str) -> None:
_shell_run(
[
BIN_ADB,
"pull",
device_apk_path,
host_apk_path,
],
non_zero_exception=DownloadAPKToHostError,
)
class GetAPKApplicationLabelError(Exception):
pass
def get_apk_application_label(host_apk_path: str) -> Optional[str]:
res = _shell_run(
[
BIN_AAPT,
"d",
"-c",
"application-label",
"badging",
host_apk_path,
],
require_stdout_exception=GetAPKApplicationLabelError,
)
if res["return_code"] != 0:
if res["stderr"] is not None and "ERROR: dump failed because no AndroidManifest.xml found" in "\n".join(res["stderr"]):
return None
else:
raise GetAPKApplicationLabelError(res["stderr"])
for line in res["stdout"]:
if "application-label" in line:
return line.replace("application-label:", "")
raise GetAPKApplicationLabelError("Could not find application label")
def download_pkg_apk_with_application_label(app_pkg: str, host_apk_path: str) -> Optional[str]:
device_apk_paths = get_device_apk_paths(app_pkg)
for device_apk_path in device_apk_paths:
download_apk_to_host(device_apk_path, host_apk_path)
app_name = get_apk_application_label(host_apk_path)
if app_name is not None:
return app_name
return None
def main():
# Parse arguments
arg_parser = argparse.ArgumentParser(description="Generates a list of installed app names for Android devices connected via debug mode")
arg_parser.add_argument(
"-o", "--output-csv",
help="Path to CSV file to write results",
default="apps-list.csv",
)
arg_parser.add_argument(
"-d", "--download-apks-dir",
help="Directory in which APK downloads will be placed",
default="./apks",
)
arg_parser.add_argument(
"-v", "--verbose",
help="Show verbose logging",
action='store_true',
default=False,
)
args = arg_parser.parse_args()
# Setup logging
if args.verbose:
logger.setLevel(logging.DEBUG)
# Check APK downloads dir exists
if not os.path.isdir(args.download_apks_dir):
os.makedirs(args.download_apks_dir)
# List third party apps
app_pkgs = list_third_party_apps()
with open(args.output_csv, "w") as output_f:
output_writer = csv.DictWriter(output_f, fieldnames=["name", "package", "playstore", "errors"])
output_writer.writeheader()
app_pkg_i = 0
for app_pkg in app_pkgs:
logger.info("Processing %s (%d/%d)", app_pkg, app_pkg_i+1, len(app_pkgs))
app_errors = []
# Download APK from device
host_apk_path = os.path.join(args.download_apks_dir, f"{app_pkg}.apk")
app_name = None
try:
if not os.path.isfile(host_apk_path):
app_name = download_pkg_apk_with_application_label(app_pkg, host_apk_path)
else:
app_name = get_apk_application_label(host_apk_path)
except GetAPKApplicationLabelError as e:
app_errors.append(f"failed to get app name: {e}")
app_link = f"https://play.google.com/store/apps/details?id={app_pkg}"
if app_name is None:
logger.warning("Could not find app name for %s", app_pkg)
app_errors.append("could not find app name")
output_writer.writerow({
"name": app_name if app_name is not None else app_pkg,
"package": app_pkg,
"playstore": app_link,
"errors": ",".join(app_errors),
})
app_pkg_i += 1
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment