apkfile • Read, inspect, and install Android app packages
apkfile reads metadata out of Android .apk, .apkm, .xapk, .apks, and .apkv files — package name,
version, permissions, supported ABIs/languages/densities, icons, signing certificates, manifest security
posture (exported components, deep links, dangerous permissions), size/DEX composition, OBB expansion files,
and more — and can install them to a connected device over adb, or diff two apks against each other.
Full documentation: apkfile.readthedocs.io
pip install -U apkfile
# or
uv add apkfilefrom apkfile import ApkFile, XapkFile, ApkmFile, ApksFile, ApkvFile
# Get apk info
apk = ApkFile("/home/david/Downloads/wa.apk")
print(apk.package_name, apk.version_name, apk.version_code)
print(apk.as_dict())
# Signing certificates, manifest security posture, size/DEX composition
print(apk.signing.is_debug_signed, [c.sha256 for c in apk.signing.all_certificates])
print(apk.security.dangerous_permissions, apk.security.unprotected_exported_components)
print(apk.size_breakdown, apk.dex_info)
# Diff two apks (e.g. two versions of the same app)
old, new = ApkFile("wa-1.apk"), ApkFile("wa-2.apk")
result = old.diff(new)
print(result.permissions_added, result.size_delta, result.signing_changed)
# A quick manifest security report
for component in apk.security.unprotected_exported_components:
print(
f"{component.type.value} {component.name} is exported with no permission required"
)
for name in apk.security.dangerous_permissions:
print(f"requests dangerous permission: {name}")
# Check whether a split apk can run on a given device ABI
from apkfile import Abi
device_abi = Abi.ARM64
print(all(device_abi.is_compatible_with(a) for a in apk.abis))
# Icons are objects, not just paths -- pick one and extract it
icon = apk.best_icon(max_dpi=320)
icon.extract("icon.png") # or icon.read_bytes()
print(icon.density, icon.bucket) # e.g. 320 DensityBucket.XHDPI
# Get apkm info — base/splits are read lazily, straight out of the archive
apkm = ApkmFile("/home/david/Downloads/chrome.apkm")
for split in apkm.splits:
print(split.split_name, split.split_type)
apkm.install(check=True, upgrade=True)
# Get xapk info
xapk = XapkFile("/home/david/Downloads/telegram.xapk")
print(xapk.abis, xapk.permissions, xapk.langs)
# Get apks info
apks = ApksFile("/home/david/Downloads/facebook.apks")
print(apks.base.permissions, apks.md5, apks.sha256)
# Build a real, on-disk .apks bundle from a folder of apks (a base apk + its splits)
built = ApksFile.create(
"/home/david/Downloads/facebook_apks/"
) # or an explicit list of paths
print(
built.path
) # com.facebook.katana-<version_code>.apks in the current dir, by default
# Get apkv info (VInstall's format — https://github.com/vinstall/apkv-spec); optionally encrypted
apkv = ApkvFile(
"/home/david/Downloads/backup.apkv", password="hunter2"
) # password=None if unencrypted
print(apkv.app_name, apkv.exported_at)
# XAPK-bundled OBB expansion files, and pushing a standalone OBB alongside a plain apk ("apk + obb")
for obb in xapk.obb_files:
print(obb.name, obb.is_patch, obb.size)
xapk.install() # bundled OBBs are pushed to /sdcard/Android/obb/<package>/ automatically
apk.install(obb_paths=["main.1.com.example.game.obb"])
# Installing to every connected device happens in parallel, not one at a time
apk.install(grant_permissions=True, allow_downgrade=True) # pm install -g -d
# Launch the app after installing (main activity by default, or a specific one)
apk.install(launch=True)
apk.install(launch_activity="com.example.app.SettingsActivity")
# Uninstall (same multi-device behavior as install: every connected device, in parallel)
apk.uninstall()
from apkfile import uninstall_apks
uninstall_apks("com.example.app", keep_data=True)apkfile info app.apk # print an apk/bundle's metadata as JSON (--full for every detail)
apkfile pack /path/to/apk_folder # build a real, on-disk .apks bundle from a base apk + splits
apkfile diff old.apk new.apk # print the differences between two apks/bundles as JSON
apkfile install app.apk # install to connected device(s)
apkfile install app.apk --upgrade --installer com.android.vending --adb-path /path/to/adb
apkfile install app.apk --launch # ...and launch it afterwards (main activity, or --launch-activity)
apkfile uninstall com.example.app # uninstall from connected device(s)
apkfile uninstall app.apk # ...or by apk/bundle path, reading its package nameapkfile parses AndroidManifest.xml and resources.arsc directly, using
androguard — a pure-Python library, so there's nothing to
install beyond apkfile itself.
- For the archive formats (
.apkm,.xapk,.apks), basic info (package_name,version_name,version_code, ...) comes from the archive's own JSON manifest. Everything else (base,splits, permissions, languages, ABIs, ...) is parsed lazily, directly from the archive's bytes the first time you access it — no disk extraction happens just to read metadata.ApkFileobjects obtained this way havepath is Noneuntil you call.save(path)on them. - The library can also install files (optionally checking compatibility first:
min_sdk_version,abis, andlangs/densities for split apks) using adb — connect a device and call.install(), or use the standaloneinstall_apks()function directly. Installing extracts only what's needed into a temporary directory for the duration of the push, and cleans up automatically afterwards.
If you want to use .install(), you need adb.
- You can manually provide a path to
adb:apk.install(adb_path="/path/to/adb").
ApkFile.path(and every bundle's.path) is now apathlib.Path, not astr. Comparisons against a bare string (apk.path == "/some/path") no longer match — compare againstPath("/some/path"), or usestr(apk.path).- New:
.signing(SigningInfo— signing scheme(s) + certificate(s)),.security(SecurityInfo— permissions with AOSP protection levels, exported components, deep links,debuggable/allowBackup/ cleartext-traffic flags),.size_breakdown(SizeBreakdown— size by dex/resources/native libs/assets/...),.dex_info(DexInfo— method/class/string counts), and.diff(other)/apkfile.diff.diff(a, b)for comparing two apks. All available onApkFileand every bundle class (bundle.signing/.securitydelegate to the base apk;.size_breakdown/.dex_infosum base + splits). - Two behavior-affecting bug fixes, verified against the official Android manifest/NDK docs:
Abi.is_compatible_with()no longer claimsx86/x86_64devices can runarm/arm64code — stock Android has no built-in ARM↔x86 translation layer, so that was always wrong and could have causedinstall_apks()to push an incompatible native-code split onto an x86 emulator.InstallLocation's default (whenandroid:installLocationisn't declared) is nowINTERNAL_ONLY, per the docs — it was previously (incorrectly) reported asAUTO.
ExportedComponentgainedread_permission/write_permission(for<provider>'sandroid:readPermission/android:writePermission), and a provider's defaultexportedvalue is now resolved correctly — it depends ontargetSdkVersion(Trueup to API 16,Falsefrom API 17), unlike activities/services/ receivers, whose default instead depends on whether they declare an<intent-filter>.ApkFile.iconsis nowtuple[Icon, ...](wasdict[int, str]) — eachIconhas.density,.bucket(aDensityBucket),.path, and self-serving.read_bytes()/.extract(path)methods. It's also complete now: the old implementation missedanydpi(adaptive icon) andnodpivariants entirely. Useapk.best_icon(max_dpi=...)to pick a single icon the wayandroguard/Android itself would.ApkFile.supported_screensis nowtuple[ScreenSize, ...](wastuple[str, ...]).- New fields:
max_sdk_version,form_factors(tuple[FormFactor, ...]— TV/wearable heuristics),SecurityInfo.implied_permissions(permissions Android silently grants under legacy compatibility rules), and onCertificate:public_key_algorithm/public_key_bit_size,canonical_subject/canonical_issuer(Java-X500Principal-compatible identity strings, safe for comparison unlikesubject/issuer), and onSigningInfo:has_duplicate_signature_ids(a tamper/verifier-confusion smell).
apkfile 1.0 is a from-scratch rewrite. The highlights:
aaptis gone. Everyaapt_pathparameter has been removed, as hasget_raw_aapt().extract_path,delete_extracted_files(), and thewith XapkFile(...) as xf:context-manager pattern are gone — reading metadata never touches disk anymore, so there's nothing to clean up..install()still uses a temporary directory, but manages it internally.- Exceptions are now a proper hierarchy under
apkfile.ApkFileError(InvalidApkError,InvalidBundleError,AdbError,AdbNotFoundError) instead of repurposed builtins. Abi,InstallLocation, andSplitTypearestrenums now — comparisons against plain strings (apk.install_location == "auto") still work.- Minimum supported Python version is 3.10.
See CHANGELOG.md for the full list.