"""Read an APK's package, label, version, SDK level, ABIs and icon, stdlib only. Replaces `aapt2 dump badging`, so installing APKs needs no Android SDK. It parses the binary AndroidManifest.xml and, for values the manifest points at (the label, version name and icon are often @string or @mipmap references), the resource table in resources.arsc. """ import struct import zipfile # android: attribute resource ids; names can be stripped by shrinkers, ids can't. ATTR = {0x01010001: 'label', 0x01010002: 'icon', 0x01010003: 'name', 0x0101021b: 'versionCode', 0x0101021c: 'versionName', 0x0101020c: 'minSdkVersion'} T_REF, T_STRING, T_INT_DEC, T_INT_HEX = 0x01, 0x03, 0x10, 0x11 class ApkError(Exception): pass def _string_pool(buf, off): """Strings of the ResStringPool chunk at off.""" _, hsize, _, count, _, flags, start = struct.unpack_from(' end: break yield ctype, hsize, off, size off += size def manifest_elements(data): """[(tag, {attr: (type, data, raw string or None)})] for each start tag.""" if len(data) < 8 or struct.unpack_from(' [(language, density, type, data)] self.strings = [] if len(data) < 12 or struct.unpack_from('> 8, struct.unpack_from('= 2}), 'min_sdk': min_sdk[1] if min_sdk and min_sdk[0] in (T_INT_DEC, T_INT_HEX) else None, 'icon_png': None, } try: info['icon_png'] = _icon_png(z, names, _icons(app.get('icon'), res)) except Exception: # noqa: BLE001 - any unreadable icon just means no icon pass return info def _icon_png(z, names, icons): for icon in icons: if icon.endswith('.png') and icon in names: return z.read(icon) # Adaptive icons are XML; fall back to the largest launcher PNG. pngs = sorted((n for n in names if n.endswith('.png') and 'ic_launcher' in n and 'foreground' not in n), key=lambda n: z.getinfo(n).file_size) return z.read(pngs[-1]) if pngs else None if __name__ == '__main__': import sys for p in sys.argv[1:]: i = apk_info(p) i['icon_png'] = len(i['icon_png'] or b'') print(p, i)