Media (Android): adds app uid support

This commit is contained in:
Carter Li
2026-10-11 10:41:56 +08:00
parent 3ccaf2d7dd
commit ab6ec4c3f5
8 changed files with 849 additions and 138 deletions
+2
View File
@@ -14,6 +14,8 @@ Features:
* Added Sound detection on Android. (Sound, Android)
* Added display detection on Android over binder, which needs neither a child process nor a permission. (Display, Android)
* Added battery temperature, remaining time and charger type (AC / USB / wireless) detection on Android. (Battery, Android)
* Added media detection on Android when fastfetch runs as an app UID. (Media, Android)
* Requires `Notification access` permission
* Performance improvements & internal cleanups
* Added Finnish language support. (General)
* Added pkgsrc package detection on macOS. (#2627, Packages, Apple)
+2
View File
@@ -678,9 +678,11 @@ if(LINUX)
)
elseif(ANDROID)
list(APPEND LIBFASTFETCH_SRC
src/common/android/applabel.c
src/common/android/binder.c
src/common/android/dex.c
src/common/android/package.c
src/common/android/zip.c
src/common/impl/dbus.c
src/common/impl/io_unix.c
src/common/impl/netif_linux.c
+560
View File
@@ -0,0 +1,560 @@
#include "common/android/applabel.h"
#include "common/android/binder.h"
#include "common/android/dex.h"
#include "common/android/zip.h"
#include "common/debug.h"
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
// The package manager, and the one method that describes an app to a caller that is not the shell.
// IPackageManager has always been part of framework.jar -- it is not one of the frameworks that
// moved into an APEX -- so there is one path and no fallback.
#define FF_ANDROID_LABEL_SERVICE "package"
#define FF_ANDROID_LABEL_DESCRIPTOR "android.content.pm.IPackageManager"
#define FF_ANDROID_LABEL_JAR "/system/framework/framework.jar"
#define FF_ANDROID_LABEL_STUB "Landroid/content/pm/IPackageManager$Stub;"
#define FF_ANDROID_LABEL_TRANSACTION "TRANSACTION_getApplicationInfo"
#define FF_ANDROID_LABEL_TABLE "resources.arsc"
// The longest package name AIDL accepts is 255 characters, and the request is one string16 of it, the
// interface token, a 64-bit flags argument and a 32-bit user id.
// clang-format off
#define FF_ANDROID_LABEL_REQUEST_SIZE \
(12 + ((4 + 2 * (sizeof(FF_ANDROID_LABEL_DESCRIPTOR) - 1) + 3) & ~3u) \
+ ((4 + 2 * 255 + 3) & ~3u) + 8u + 4u)
// clang-format on
// An ApplicationInfo parcel runs 2.0 to 3.2 KB on the test device, and the surplus is what a reply
// with more split names and directories than any of those would spend.
#define FF_ANDROID_LABEL_REPLY_SIZE (16 * 1024)
// Long enough for the `/data/app/~~<22 base64 chars>==/<package>-<22 chars>==/base.apk` the platform
// installs to, with room to spare.
#define FF_ANDROID_LABEL_PATH_SIZE 512
// ---------------------------------------------------------------------------------------------
// The resource table
// ---------------------------------------------------------------------------------------------
//
// `resources.arsc` is a ResTable: a header, a global string pool, and then one ResTable_package per
// package it defines, each carrying its own pools for type names and entry keys followed by the
// types themselves. Resolving `labelRes` is two indices deep -- the package's number names the
// package and the type, the low half names the entry -- and every step below reads the file rather
// than a table of what the file is expected to hold, because the layout is the platform's and the
// numbers are per APK.
// ResChunk_header: type(2) headerSize(2) size(4). Every chunk in the file starts with one.
#define FF_ARSC_TABLE_TYPE 0x0002
#define FF_ARSC_STRING_POOL_TYPE 0x0001
#define FF_ARSC_TABLE_PACKAGE_TYPE 0x0200
#define FF_ARSC_TABLE_TYPE_TYPE 0x0201
// ResStringPool: header(8) stringCount(4) styleCount(4) flags(4) stringsStart(4) stylesStart(4)
#define FF_ARSC_POOL_HEADER_SIZE 28
#define FF_ARSC_OFF_POOL_CHUNK_SIZE 4
#define FF_ARSC_OFF_POOL_COUNT 8
#define FF_ARSC_OFF_POOL_FLAGS 16
#define FF_ARSC_OFF_POOL_STRINGS_START 20
// A UTF-8 pool says so with this bit, and then holds the strings as UTF-8 rather than as UTF-16.
#define FF_ARSC_POOL_FLAG_UTF8 0x0100
// ResTable_package: header(8) id(4) name(256, a UTF-16 array) typeStrings(4) lastPublicType(4) ...
#define FF_ARSC_OFF_PACKAGE_ID 8
#define FF_ARSC_OFF_PACKAGE_TYPE_STRINGS (8 + 4 + 256)
// ResTable_type: header(8) id(1) flags(1) reserved(2) entryCount(4) entriesStart(4) config(...)
#define FF_ARSC_OFF_TYPE_ID 8
#define FF_ARSC_OFF_TYPE_FLAGS 9
#define FF_ARSC_OFF_TYPE_ENTRY_COUNT 12
#define FF_ARSC_OFF_TYPE_ENTRIES_START 16
#define FF_ARSC_OFF_TYPE_CONFIG 20
// The two shapes a type can take that this does not read: a sparse type stores its offsets as
// index-offset pairs rather than as a flat table, and a 16-bit one stores them at half width. Neither
// is in the APKs measured, and both would resolve an entry to the wrong offset rather than to none,
// so they are refused rather than read as if they were the plain shape.
#define FF_ARSC_TYPE_FLAG_SPARSE 0x01
#define FF_ARSC_TYPE_FLAG_OFFSET16 0x02
// ResTable_entry: size(2) flags(2), where size is the header's own length and the value follows it.
#define FF_ARSC_ENTRY_FLAG_COMPLEX 0x0001
// Res_value: size(2) res0(1) dataType(1) data(4). TYPE_STRING makes data an index into the global
// string pool; every other type is either a number or a reference to another resource.
#define FF_ARSC_OFF_VALUE_DATA_TYPE 3
#define FF_ARSC_OFF_VALUE_DATA 4
#define FF_ARSC_TYPE_STRING 0x03
// A type chunk's offset table has one word per entry, and this is what an entry with no value in that
// configuration looks like. It is not zero: offset zero is a real entry, at the start of the block.
#define FF_ARSC_NO_ENTRY 0xffffffffu
// The type a label resolves to. `string` is what `@string/...` compiles to, and it is what tells a
// label from the icons and banners that sit beside it in the same reply -- those point at `mipmap`
// or `drawable` entries whose value is a path inside the APK, so resolving them would hand back
// `res/9w.png` instead of a name.
#define FF_ANDROID_LABEL_STRING_TYPE "string"
static uint16_t readU16(const uint8_t* p) {
return (uint16_t) ((uint16_t) p[0] | (uint16_t) ((uint16_t) p[1] << 8));
}
static uint32_t readU32(const uint8_t* p) {
return (uint32_t) p[0] | ((uint32_t) p[1] << 8) | ((uint32_t) p[2] << 16) | ((uint32_t) p[3] << 24);
}
// One ResStringPool, of which a table has three.
typedef struct FFAndroidLabelPool {
const uint8_t* table;
size_t tableSize;
size_t offset;
uint32_t chunkSize;
uint32_t count;
uint32_t headerSize;
uint32_t stringsStart;
bool utf8;
} FFAndroidLabelPool;
[[gnu::nonnull(2), nodiscard]] static bool poolInit(FFAndroidLabelPool* pool, const uint8_t* table, size_t tableSize, size_t offset) {
if (offset > tableSize || tableSize - offset < FF_ARSC_POOL_HEADER_SIZE) {
return false;
}
if (readU16(table + offset) != FF_ARSC_STRING_POOL_TYPE) {
return false;
}
pool->table = table;
pool->tableSize = tableSize;
pool->offset = offset;
pool->chunkSize = readU32(table + offset + FF_ARSC_OFF_POOL_CHUNK_SIZE);
pool->count = readU32(table + offset + FF_ARSC_OFF_POOL_COUNT);
pool->headerSize = readU16(table + offset + 2);
pool->stringsStart = readU32(table + offset + FF_ARSC_OFF_POOL_STRINGS_START);
pool->utf8 = (readU32(table + offset + FF_ARSC_OFF_POOL_FLAGS) & FF_ARSC_POOL_FLAG_UTF8) != 0;
return pool->headerSize >= FF_ARSC_POOL_HEADER_SIZE && pool->chunkSize > pool->headerSize;
}
// The length in front of a string. A UTF-8 pool writes one or two bytes, a UTF-16 pool one or two
// 16-bit words, and the top bit of the first says which of the two it is.
[[gnu::nonnull(1, 3), nodiscard]] static bool readLength8(const FFAndroidLabelPool* pool, size_t* position, uint32_t* length) {
if (*position >= pool->tableSize) {
return false;
}
uint32_t value = pool->table[(*position)++];
if (value & 0x80) {
if (*position >= pool->tableSize) {
return false;
}
value = ((value & 0x7F) << 8) | pool->table[(*position)++];
}
*length = value;
return true;
}
[[gnu::nonnull(1, 3), nodiscard]] static bool readLength16(const FFAndroidLabelPool* pool, size_t* position, uint32_t* length) {
if (*position + 2 > pool->tableSize) {
return false;
}
uint32_t value = readU16(pool->table + *position);
*position += 2;
if (value & 0x8000) {
if (*position + 2 > pool->tableSize) {
return false;
}
value = ((value & 0x7FFF) << 16) | readU16(pool->table + *position);
*position += 2;
}
*length = value;
return true;
}
[[gnu::nonnull(1)]] static void appendCodePoint(FFstrbuf* result, uint32_t code) {
char encoded[4] = {};
uint32_t length = 0;
if (code < 0x80) {
encoded[length++] = (char) code;
} else if (code < 0x800) {
encoded[length++] = (char) (0xC0 | (code >> 6));
encoded[length++] = (char) (0x80 | (code & 0x3F));
} else if (code < 0x10000) {
encoded[length++] = (char) (0xE0 | (code >> 12));
encoded[length++] = (char) (0x80 | ((code >> 6) & 0x3F));
encoded[length++] = (char) (0x80 | (code & 0x3F));
} else {
encoded[length++] = (char) (0xF0 | (code >> 18));
encoded[length++] = (char) (0x80 | ((code >> 12) & 0x3F));
encoded[length++] = (char) (0x80 | ((code >> 6) & 0x3F));
encoded[length++] = (char) (0x80 | (code & 0x3F));
}
ffStrbufAppendNS(result, length, encoded);
}
// One string out of a pool.
//
// The two lengths in front of a UTF-8 string are a *character* count and a *byte* count, and they
// differ for everything outside ASCII. Decoding `characters` bytes is the trap: a label of a few
// multi-byte characters comes back as its first character followed by replacement bytes. Measured on
// the test device, where every resource table read is a UTF-8 pool, this was the difference between
// the right label and mojibake.
[[gnu::nonnull(1, 3), nodiscard]] static bool poolGet(const FFAndroidLabelPool* pool, uint32_t index, FFstrbuf* result) {
if (index >= pool->count) {
return false;
}
const size_t offsets = pool->offset + pool->headerSize;
if (offsets + ((size_t) index + 1) * 4 > pool->tableSize) {
return false;
}
size_t position = pool->offset + pool->stringsStart + readU32(pool->table + offsets + (size_t) index * 4);
if (position >= pool->tableSize) {
return false;
}
if (pool->utf8) {
uint32_t characters = 0;
uint32_t bytes = 0;
if (!readLength8(pool, &position, &characters) || !readLength8(pool, &position, &bytes)) {
return false;
}
// An empty string is reported as a miss rather than written: the caller sets its fallback
// before asking, and ffStrbufSetNS() with a length of zero *clears* the buffer, which would
// throw that fallback away instead of leaving it alone.
if (bytes == 0 || bytes > pool->tableSize - position) {
return false;
}
ffStrbufSetNS(result, bytes, (const char*) (pool->table + position));
return true;
}
uint32_t units = 0;
if (!readLength16(pool, &position, &units)) {
return false;
}
if (units == 0 || (size_t) units * 2 > pool->tableSize - position) {
return false;
}
for (uint32_t i = 0; i < units; i++) {
uint32_t code = readU16(pool->table + position + (size_t) i * 2);
// A code point outside the basic plane arrives as a surrogate pair, and the two halves are
// not characters of their own: half of a pair encoded on its own is not valid UTF-8.
if (code >= 0xD800 && code <= 0xDBFF && i + 1 < units) {
const uint32_t low = readU16(pool->table + position + ((size_t) i + 1) * 2);
if (low >= 0xDC00 && low <= 0xDFFF) {
code = 0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00);
i++;
}
}
appendCodePoint(result, code);
}
return true;
}
// The one ResTable_package this table defines, with the two pools a lookup starts from.
typedef struct FFAndroidLabelTable {
const uint8_t* data;
size_t size;
FFAndroidLabelPool values;
FFAndroidLabelPool types;
size_t packageOffset;
size_t packageHeaderSize;
size_t packageSize;
uint32_t packageId;
} FFAndroidLabelTable;
[[gnu::nonnull(1), nodiscard]] static bool tableInit(FFAndroidLabelTable* table, const uint8_t* data, size_t size) {
// ResTable_header: header(8) packageCount(4). The header's own size is where the global pool
// starts, which is 12 in every table measured -- but it is the field rather than the number.
if (size < 12 || readU16(data) != FF_ARSC_TABLE_TYPE) {
return false;
}
table->data = data;
table->size = size;
const uint32_t headerSize = readU16(data + 2);
if (!poolInit(&table->values, data, size, headerSize)) {
return false;
}
size_t position = (size_t) headerSize + table->values.chunkSize;
while (position + 8 <= size) {
const uint16_t type = readU16(data + position);
const uint16_t chunkHeader = readU16(data + position + 2);
const uint32_t chunkSize = readU32(data + position + 4);
if (chunkSize < 8 || chunkSize > size - position) {
break;
}
// A table can define several packages -- an app that links a shared library's resources has
// more than one -- and the id in the resource reference is what picks the one to read.
if (type == FF_ARSC_TABLE_PACKAGE_TYPE && chunkSize >= chunkHeader && chunkHeader >= FF_ARSC_OFF_PACKAGE_TYPE_STRINGS + 4) {
table->packageOffset = position;
table->packageHeaderSize = chunkHeader;
table->packageSize = chunkSize;
table->packageId = readU32(data + position + FF_ARSC_OFF_PACKAGE_ID);
if (!poolInit(&table->types, data, size, position + readU32(data + position + FF_ARSC_OFF_PACKAGE_TYPE_STRINGS))) {
return false;
}
return true;
}
position += chunkSize;
}
return false;
}
// A type chunk whose ResTable_config is all zeroes is the one that applies when nothing about the
// device or the locale matches a more specific chunk. An app with a translation has several chunks
// for one type, and picking the first one that happens along would answer with whatever language it
// was written in.
[[gnu::nonnull(1), nodiscard]] static bool isDefaultConfig(const uint8_t* config, size_t length) {
if (length == 0) {
return false;
}
for (size_t i = 0; i < length; i++) {
if (config[i] != 0) {
return false;
}
}
return true;
}
[[gnu::nonnull(1, 5), nodiscard]] static bool resolveIn(const FFAndroidLabelTable* table, uint32_t typeId, uint32_t entryId, bool defaultOnly, FFstrbuf* result) {
const size_t end = table->packageOffset + table->packageSize;
for (size_t position = table->packageOffset + table->packageHeaderSize; position + 8 <= end;) {
const uint16_t type = readU16(table->data + position);
const uint16_t header = readU16(table->data + position + 2);
const uint32_t size = readU32(table->data + position + 4);
if (size < 8 || size > end - position) {
return false;
}
if (type == FF_ARSC_TABLE_TYPE_TYPE && header >= FF_ARSC_OFF_TYPE_CONFIG) {
const uint8_t id = table->data[position + FF_ARSC_OFF_TYPE_ID];
const uint8_t flags = table->data[position + FF_ARSC_OFF_TYPE_FLAGS];
if (id == typeId && (flags & (FF_ARSC_TYPE_FLAG_SPARSE | FF_ARSC_TYPE_FLAG_OFFSET16)) == 0 && size >= header) {
const uint32_t entryCount = readU32(table->data + position + FF_ARSC_OFF_TYPE_ENTRY_COUNT);
const uint32_t entriesStart = readU32(table->data + position + FF_ARSC_OFF_TYPE_ENTRIES_START);
const size_t offsets = position + header;
if (entryId < entryCount && offsets + ((size_t) entryId + 1) * 4 <= end) {
if (defaultOnly && !isDefaultConfig(table->data + position + FF_ARSC_OFF_TYPE_CONFIG, (size_t) header - FF_ARSC_OFF_TYPE_CONFIG)) {
position += size;
continue;
}
const uint32_t offset = readU32(table->data + offsets + (size_t) entryId * 4);
// `entriesStart` is relative to the chunk, not to the package, and both it and
// the entry's own offset come out of the file.
if (offset != FF_ARSC_NO_ENTRY && entriesStart <= size && offset <= size - entriesStart) {
const size_t entry = position + entriesStart + offset;
const size_t value = entry + readU16(table->data + entry);
if (value >= entry && value + 8 <= end && (readU16(table->data + entry + 2) & FF_ARSC_ENTRY_FLAG_COMPLEX) == 0
&& table->data[value + FF_ARSC_OFF_VALUE_DATA_TYPE] == FF_ARSC_TYPE_STRING) {
return poolGet(&table->values, readU32(table->data + value + FF_ARSC_OFF_VALUE_DATA), result);
}
}
}
}
}
position += size;
}
return false;
}
// Resolves one resource id to its string, and only when the type it names is a `string`.
//
// The type name is the whole reason this reads the package's type pool rather than trusting the id:
// `labelRes` and the icon, logo and banner ids that sit beside it in the same reply are all resource
// ids of the same package, and the difference between them is the type they point at. A `mipmap` id
// resolves perfectly well -- to `res/9w.png`.
[[gnu::nonnull(1, 3), nodiscard]] static bool tableResolve(const FFAndroidLabelTable* table, uint32_t resourceId, FFstrbuf* result) {
const uint32_t typeId = (resourceId >> 16) & 0xFF;
if (((resourceId >> 24) & 0xFF) != table->packageId || typeId == 0) {
return false;
}
FFstrbuf typeName;
ffStrbufInit(&typeName);
const bool isString = poolGet(&table->types, typeId - 1, &typeName) && ffStrbufEqualS(&typeName, FF_ANDROID_LABEL_STRING_TYPE);
ffStrbufDestroy(&typeName);
if (!isString) {
return false;
}
// The configuration that applies is the unqualified one; a chunk that only applies to a locale
// or a density is the fallback rather than the answer, which is what the second pass is for.
return resolveIn(table, typeId, resourceId & 0xFFFF, true, result)
|| resolveIn(table, typeId, resourceId & 0xFFFF, false, result);
}
// ---------------------------------------------------------------------------------------------
// The reply
// ---------------------------------------------------------------------------------------------
// The reply's own copy of the APK path. `sourceDir` is a string in the ApplicationInfo parcel and it
// ends in `.apk`; nothing else in the reply does, which is what makes it findable without walking
// the parcel's field order -- that would mean recovering `ApplicationInfo.writeToParcel` out of the
// device's dex and then framing sixty fields of unknown width, to reach one string that is already
// recognizable.
[[gnu::nonnull(1, 3), nodiscard]] static bool findApkPath(const uint8_t* data, size_t size, char* result, size_t capacity) {
for (size_t i = 8; i < size; i++) {
if (data[i] != '/') {
continue;
}
size_t end = i;
while (end < size && data[end] >= 0x20 && data[end] < 0x7F) {
end++;
}
const size_t length = end - i;
if (length >= 5 && length < capacity && memcmp(data + end - 4, ".apk", 4) == 0) {
memcpy(result, data + i, length);
result[length] = '\0';
return true;
}
i = end;
}
return false;
}
// ---------------------------------------------------------------------------------------------
typedef struct FFAndroidLabelMapping {
const uint8_t* data;
size_t size;
} FFAndroidLabelMapping;
[[gnu::nonnull(1)]] static void labelUnmap(FFAndroidLabelMapping* mapping) {
if (mapping->data != nullptr) {
munmap((void*) mapping->data, mapping->size);
}
}
[[gnu::nonnull(2), nodiscard]] static const char* labelMap(const char* path, FFAndroidLabelMapping* mapping) {
const int fd = open(path, O_RDONLY | O_CLOEXEC);
if (fd < 0) {
FF_DEBUG("open(%s) failed: %s", path, strerror(errno));
return "open(apk) failed";
}
struct stat status = {};
const bool failed = fstat(fd, &status) != 0;
if (failed || status.st_size <= 0) {
FF_DEBUG("fstat(%s) reported %s", path, failed ? strerror(errno) : "an empty file");
close(fd);
return "fstat(apk) failed";
}
void* mapped = mmap(nullptr, (size_t) status.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
close(fd);
if (mapped == MAP_FAILED) {
FF_DEBUG("mmap(%s, %lld bytes) failed: %s", path, (long long) status.st_size, strerror(errno));
return "mmap(apk) failed";
}
mapping->data = mapped;
mapping->size = (size_t) status.st_size;
return nullptr;
}
// One reply buffer for every call this file makes, and it makes one per query.
static uint8_t s_reply[FF_ANDROID_LABEL_REPLY_SIZE];
const char* ffAndroidGetAppLabel(const char* packageName, FFstrbuf* result) {
// The transaction code comes out of the jar rather than from a table here: it is a property of
// the `.aidl` the device's ROM was built from, and a vendor that inserts a method ahead of this
// one shifts every code behind it. See common/android/dex.h.
int32_t transaction = FF_DEX_STATIC_INT_UNRESOLVED;
const FFDexStaticIntRequest lookup = {
.classDescriptor = FF_ANDROID_LABEL_STUB,
.fieldName = FF_ANDROID_LABEL_TRANSACTION,
.result = &transaction,
};
const char* error = ffDexStaticInts(FF_ANDROID_LABEL_JAR, &lookup, 1);
if (error != nullptr) {
return error;
}
if (transaction == FF_DEX_STATIC_INT_UNRESOLVED) {
return "framework.jar does not declare IPackageManager.getApplicationInfo";
}
[[gnu::cleanup(ffBinderClose)]] FFBinder binder = { .fd = -1 };
error = ffBinderOpen(&binder);
if (error != nullptr) {
return error;
}
[[gnu::cleanup(ffBinderServiceHandleRelease)]] FFBinderServiceHandle service = { .binder = &binder };
error = ffBinderLookupService(&binder, FF_ANDROID_LABEL_SERVICE, FF_BINDER_SM_GET_SERVICE, &service.handle);
if (error != nullptr) {
FF_DEBUG("Looking up \"%s\" failed: %s", FF_ANDROID_LABEL_SERVICE, error);
return error;
}
uint8_t request[FF_ANDROID_LABEL_REQUEST_SIZE];
FFBinderParcel parcel = ffBinderParcelCreate(request, sizeof(request));
ffBinderParcelPutInterfaceToken(&parcel, FF_ANDROID_LABEL_DESCRIPTOR);
ffBinderParcelPutString16(&parcel, packageName);
// `getApplicationInfo(String, long flags, int userId)`. The flags argument is 64-bit from Android
// 13 on and 32-bit before it, and the wider one is what works on both: a release that reads an
// int takes the low word and leaves the zero high word unread, because an AIDL stub stops
// reading once its own arguments are in. The narrow one is not symmetric -- a release that reads
// a long would take half of its own flags argument for the user id.
ffBinderParcelPutU64(&parcel, 0);
ffBinderParcelPutI32(&parcel, 0);
FFBinderReply reply = ffBinderReplyCreate(s_reply, sizeof(s_reply));
error = ffBinderTransact(&binder, service.handle, (uint32_t) transaction, 0, &parcel, &reply);
if (error != nullptr) {
return error;
}
if (ffBinderReplyIsStatus(&reply)) {
return "The package manager rejected the request";
}
// The AIDL status first, then the presence marker of a nullable Parcelable: a package the caller
// is not allowed to see answers an empty ApplicationInfo rather than an error.
if (ffBinderReadI32(reply.data, reply.size, 0) != 0) {
return "The package manager refused to describe the app";
}
if (ffBinderReadI32(reply.data, reply.size, 4) == 0) {
FF_DEBUG("The package manager described \"%s\" as an empty ApplicationInfo", packageName);
return nullptr;
}
char path[FF_ANDROID_LABEL_PATH_SIZE];
if (!findApkPath(reply.data, reply.size, path, sizeof(path))) {
return "The reply carries no APK path";
}
FF_DEBUG("\"%s\" is installed at \"%s\"", packageName, path);
[[gnu::cleanup(labelUnmap)]] FFAndroidLabelMapping mapping = {};
error = labelMap(path, &mapping);
if (error != nullptr) {
return error;
}
FFAndroidZipEntry table = {};
error = ffAndroidZipFindEntry(mapping.data, mapping.size, FF_ANDROID_LABEL_TABLE, &table);
if (error != nullptr) {
return error;
}
if (table.method != FF_ZIP_METHOD_STORED) {
FF_DEBUG("\"%s\" stores resources.arsc with compression method %u", packageName, (unsigned) table.method);
return "The resource table is stored compressed";
}
FFAndroidLabelTable resources = {};
if (!tableInit(&resources, table.payload, table.payloadSize)) {
return "The resource table could not be read";
}
// Every word of the reply shaped like one of this package's resource ids, in the order they
// appear, and the first one that resolves to a `string` is the label. See the header for what
// that rule was audited against and why it beats recovering the parcel's field order.
for (size_t offset = 8; offset + 4 <= reply.size; offset += 4) {
if (tableResolve(&resources, readU32(reply.data + offset), result)) {
FF_DEBUG("\"%s\" shows itself as \"%s\"", packageName, result->chars);
return nullptr;
}
}
FF_DEBUG("No resource id in the reply of \"%s\" resolves to a string", packageName);
return nullptr;
}
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include "fastfetch.h"
// The name an app presents to the user, which is not its package name: an app installed as
// `com.termux` presents itself as `Termux:Launcher` on the launcher, in its notifications and on
// the settings screen.
//
// Every route that answers with an *already resolved* label is closed to an app UID, and each one
// was measured on the test device rather than assumed:
//
// * `dumpsys package` needs android.permission.DUMP.
// * `cmd package dump` is filtered down to 929 bytes for an app UID, against 1.2 MB for the shell.
// * The task descriptions the activity service hands out only ever describe the caller's own task.
// * `getApplicationInfo` answers with `labelRes` -- a resource id -- and leaves `nonLocalizedLabel`
// null, so there is no string in the reply to read.
//
// What is open is the APK itself. `getApplicationInfo` carries its path (`sourceDir`), APKs are
// world readable, and the `resources.arsc` inside one is *stored* rather than deflated -- the
// platform expects to mmap it -- so resolving an id out of it needs no decompressor. That is the
// route this takes: one transaction, one mapping, no fork and no child process.
//
// The label is the resource id in the reply whose type is `string`. That is a rule about the reply
// rather than about the parcel's field order, and it was audited against the manifest's own
// `android:label` over every package installed on the test device: nine in ten have exactly one
// `string` candidate and it is the right one every time, a handful more have two and the first is
// still the right one, and the rest have none because their label is a literal string rather than a
// reference (`android:label="Example"`), so the reply carries no id at all. No package resolved to
// a wrong string; the counts behind that are in doc/app-label.md.
//
// Writes the label into `result` when it can be resolved, and leaves `result` alone otherwise --
// which includes the cases above where there is nothing to resolve. The caller sets its fallback
// first, so that an unresolved label costs nothing but the query.
//
// Returns nullptr on success, a static message when the query itself could not be made. An error is
// not a fallback: it says the label was not looked for, not that the app has none.
[[gnu::nonnull(1, 2), nodiscard]] const char* ffAndroidGetAppLabel(const char* packageName, FFstrbuf* result);
+13 -125
View File
@@ -1,4 +1,5 @@
#include "common/android/dex.h"
#include "common/android/zip.h"
#include "common/debug.h"
#include "common/io.h"
#include "common/mallocHelper.h"
@@ -54,43 +55,6 @@
#define FF_DEX_OP_FIELD_FIRST 0x52
#define FF_DEX_OP_FIELD_LAST 0x5F
// The zip local file header, from APPNOTE.TXT 4.3.7. Only the two length fields are read from it:
// the payload starts right behind them, and the local extra field is allowed to differ from the
// central one, so the offset cannot be computed from the directory alone.
#define FF_ZIP_LOCAL_HEADER_SIZE 30
#define FF_ZIP_OFF_NAME_LENGTH 26
#define FF_ZIP_OFF_EXTRA_LENGTH 28
// The central directory. This is what makes an entry reachable without walking everything in front
// of it, and that matters here: framework.jar is 52 MB and its classes2.dex starts 9.6 MB in, so
// looking for a local header by scanning the file costs a pass over everything before it -- once per
// lookup. Measured on the test device, where the sound module resolves seven codes, that was the
// difference between 30 ms and 2 seconds.
#define FF_ZIP_CENTRAL_MAGIC "PK\x01\x02"
#define FF_ZIP_CENTRAL_SIZE 46
#define FF_ZIP_OFF_CENTRAL_METHOD 10
#define FF_ZIP_OFF_CENTRAL_COMPRESSED_SIZE 20
#define FF_ZIP_OFF_CENTRAL_UNCOMPRESSED_SIZE 24
#define FF_ZIP_OFF_CENTRAL_NAME_LENGTH 28
#define FF_ZIP_OFF_CENTRAL_EXTRA_LENGTH 30
#define FF_ZIP_OFF_CENTRAL_COMMENT_LENGTH 32
#define FF_ZIP_OFF_CENTRAL_LOCAL_OFFSET 42
// The end of central directory record, which points at the directory. It is the last thing in the
// file, behind a comment of at most 64 KiB, so it is searched for backwards over that much.
#define FF_ZIP_EOCD_MAGIC "PK\x05\x06"
#define FF_ZIP_EOCD_SIZE 22
#define FF_ZIP_EOCD_MAX_COMMENT 0xffffu
#define FF_ZIP_OFF_EOCD_DIRECTORY_SIZE 12
#define FF_ZIP_OFF_EOCD_DIRECTORY 16
// A size or offset of 0xFFFFFFFF means the real one is in a zip64 extra field. AOSP's framework jars
// are orders of magnitude below that, and carrying the extra field reader for them is not worth it.
#define FF_ZIP_ZIP64_SENTINEL 0xffffffffu
#define FF_ZIP_METHOD_STORED 0
#define FF_ZIP_METHOD_DEFLATED 8
#define FF_DEX_ENTRY "classes.dex"
// How many `classesN.dex` entries a jar is walked for. A jar that spreads its classes over more
// than this still resolves everything in the entries that were walked; only a class that lives in
@@ -879,96 +843,20 @@ static const char* dexMapJar(const char* jarPath, FFDexMapping* mapping) {
return nullptr;
}
// Locates an entry through the jar's central directory.
//
// The sizes and the compression method are read from the directory rather than from the entry's own
// local header, which is the more reliable of the two: a local header is allowed to leave its sizes
// at zero and put them in a data descriptor behind the payload, and an archive writer is free to
// choose that. Only the local header's two length fields are used, because they are what says where
// the payload begins -- the local extra field does not have to match the central one.
// Locates `classesN.dex` in the jar's central directory. The walk that finds an entry is the same
// one the app label reader needs for an APK's resource table, so it lives in common/android/zip.h
// and this is the dex-specific half of the question: which name to ask for.
static const char* findDexEntry(const uint8_t* jar, size_t jarSize, const char* entry, const uint8_t** data, size_t* dataSize, uint32_t* uncompressedSize, uint16_t* method) {
const size_t entryLength = strlen(entry);
if (jarSize < FF_ZIP_EOCD_SIZE) {
return "The jar is too small to be a zip";
FFAndroidZipEntry found = {};
const char* error = ffAndroidZipFindEntry(jar, jarSize, entry, &found);
if (error != nullptr) {
return error;
}
// The record is the last thing in the file, so it is searched for backwards -- over at most the
// 64 KiB a trailing comment can take, not over the whole archive.
const size_t newest = jarSize - FF_ZIP_EOCD_SIZE;
const size_t oldest = jarSize > FF_ZIP_EOCD_MAX_COMMENT + FF_ZIP_EOCD_SIZE
? jarSize - FF_ZIP_EOCD_MAX_COMMENT - FF_ZIP_EOCD_SIZE
: 0;
size_t eocd = SIZE_MAX;
for (size_t i = newest;; --i) {
if (memcmp(jar + i, FF_ZIP_EOCD_MAGIC, sizeof(FF_ZIP_EOCD_MAGIC) - 1) == 0) {
eocd = i;
break;
}
if (i == oldest) {
// The walk stops here rather than at zero: a `size_t` countdown that has to reach zero
// before it stops would wrap and read the whole file backwards, which is the very scan
// this is here to avoid.
break;
}
}
if (eocd == SIZE_MAX) {
FF_DEBUG("No end-of-central-directory record in the last %zu bytes of a %zu byte jar",
newest - oldest + FF_ZIP_EOCD_SIZE, jarSize);
return "The jar has no zip directory";
}
const uint32_t directorySize = dexU32(jar + eocd + FF_ZIP_OFF_EOCD_DIRECTORY_SIZE);
const uint32_t directoryOffset = dexU32(jar + eocd + FF_ZIP_OFF_EOCD_DIRECTORY);
if (directoryOffset > jarSize || directorySize > jarSize - directoryOffset) {
return "The zip directory is out of range";
}
const size_t directoryEnd = (size_t) directoryOffset + directorySize;
for (size_t offset = directoryOffset; offset + FF_ZIP_CENTRAL_SIZE <= directoryEnd;) {
if (memcmp(jar + offset, FF_ZIP_CENTRAL_MAGIC, sizeof(FF_ZIP_CENTRAL_MAGIC) - 1) != 0) {
return "A zip directory entry is malformed";
}
const uint16_t nameLength = dexU16(jar + offset + FF_ZIP_OFF_CENTRAL_NAME_LENGTH);
const uint16_t extraLength = dexU16(jar + offset + FF_ZIP_OFF_CENTRAL_EXTRA_LENGTH);
const uint16_t commentLength = dexU16(jar + offset + FF_ZIP_OFF_CENTRAL_COMMENT_LENGTH);
const size_t entrySize = FF_ZIP_CENTRAL_SIZE + (size_t) nameLength + (size_t) extraLength + (size_t) commentLength;
if (entrySize > directoryEnd - offset) {
FF_DEBUG("A zip entry claims %zu bytes but only %zu remain in the directory",
entrySize, (size_t) (directoryEnd - offset));
return "A zip directory entry runs past the directory";
}
if (nameLength == entryLength && memcmp(jar + offset + FF_ZIP_CENTRAL_SIZE, entry, entryLength) == 0) {
const uint32_t compressedSize = dexU32(jar + offset + FF_ZIP_OFF_CENTRAL_COMPRESSED_SIZE);
const uint32_t uncompressed = dexU32(jar + offset + FF_ZIP_OFF_CENTRAL_UNCOMPRESSED_SIZE);
const uint32_t localOffset = dexU32(jar + offset + FF_ZIP_OFF_CENTRAL_LOCAL_OFFSET);
if (compressedSize == FF_ZIP_ZIP64_SENTINEL || uncompressed == FF_ZIP_ZIP64_SENTINEL || localOffset == FF_ZIP_ZIP64_SENTINEL) {
FF_DEBUG("The dex entry is zip64: compressed=%u, uncompressed=%u, localOffset=%u",
compressedSize, uncompressed, localOffset);
return "A dex entry is stored as zip64, which is not supported";
}
if (localOffset > jarSize || FF_ZIP_LOCAL_HEADER_SIZE > jarSize - localOffset) {
return "A zip local header is out of range";
}
const uint16_t localNameLength = dexU16(jar + localOffset + FF_ZIP_OFF_NAME_LENGTH);
const uint16_t localExtraLength = dexU16(jar + localOffset + FF_ZIP_OFF_EXTRA_LENGTH);
const size_t headerSize = FF_ZIP_LOCAL_HEADER_SIZE + (size_t) localNameLength + (size_t) localExtraLength;
if (headerSize > jarSize - localOffset || compressedSize > jarSize - localOffset - headerSize) {
return "The zip entry bounds are out of range";
}
*method = dexU16(jar + offset + FF_ZIP_OFF_CENTRAL_METHOD);
*uncompressedSize = uncompressed;
*data = jar + localOffset + headerSize;
*dataSize = compressedSize;
return nullptr;
}
offset += entrySize;
}
FF_DEBUG("No entry in the %zu byte jar is named %s", jarSize, entry);
return "The jar has no usable dex entry";
*data = found.payload;
*dataSize = found.payloadSize;
*uncompressedSize = found.uncompressedSize;
*method = found.method;
return nullptr;
}
// Fallback for a jar whose entry is STORED but whose header has no sizes: the dex magic is then a
+100
View File
@@ -0,0 +1,100 @@
#include "common/android/zip.h"
#include "common/debug.h"
#include <string.h>
static uint16_t zipU16(const uint8_t* p) {
return (uint16_t) ((uint16_t) p[0] | (uint16_t) ((uint16_t) p[1] << 8));
}
static uint32_t zipU32(const uint8_t* p) {
return (uint32_t) p[0] | ((uint32_t) p[1] << 8) | ((uint32_t) p[2] << 16) | ((uint32_t) p[3] << 24);
}
const char* ffAndroidZipFindEntry(const uint8_t* archive, size_t archiveSize, const char* entry, FFAndroidZipEntry* result) {
const size_t entryLength = strlen(entry);
if (archiveSize < FF_ZIP_EOCD_SIZE) {
return "The archive is too small to be a zip";
}
// The record is the last thing in the file, so it is searched for backwards -- over at most the
// 64 KiB a trailing comment can take, not over the whole archive.
const size_t newest = archiveSize - FF_ZIP_EOCD_SIZE;
const size_t oldest = archiveSize > FF_ZIP_EOCD_MAX_COMMENT + FF_ZIP_EOCD_SIZE
? archiveSize - FF_ZIP_EOCD_MAX_COMMENT - FF_ZIP_EOCD_SIZE
: 0;
size_t eocd = SIZE_MAX;
for (size_t i = newest;; --i) {
if (memcmp(archive + i, FF_ZIP_EOCD_MAGIC, sizeof(FF_ZIP_EOCD_MAGIC) - 1) == 0) {
eocd = i;
break;
}
if (i == oldest) {
// The walk stops here rather than at zero: a `size_t` countdown that has to reach zero
// before it stops would wrap and read the whole file backwards, which is the very scan
// this is here to avoid.
break;
}
}
if (eocd == SIZE_MAX) {
FF_DEBUG("No end-of-central-directory record in the last %zu bytes of a %zu byte archive",
newest - oldest + FF_ZIP_EOCD_SIZE, archiveSize);
return "The archive has no zip directory";
}
const uint32_t directorySize = zipU32(archive + eocd + FF_ZIP_OFF_EOCD_DIRECTORY_SIZE);
const uint32_t directoryOffset = zipU32(archive + eocd + FF_ZIP_OFF_EOCD_DIRECTORY);
if (directoryOffset > archiveSize || directorySize > archiveSize - directoryOffset) {
return "The zip directory is out of range";
}
// The directory is walked to its end rather than for the entry count the record carries: that
// count is a 16-bit field which a 65535-entry archive pins at 0xFFFF and moves into a zip64
// record, while the two bounds above describe the directory itself on every archive this small.
const size_t directoryEnd = (size_t) directoryOffset + directorySize;
for (size_t offset = directoryOffset; offset + FF_ZIP_CENTRAL_SIZE <= directoryEnd;) {
if (memcmp(archive + offset, FF_ZIP_CENTRAL_MAGIC, sizeof(FF_ZIP_CENTRAL_MAGIC) - 1) != 0) {
return "A zip directory entry is malformed";
}
const uint16_t nameLength = zipU16(archive + offset + FF_ZIP_OFF_CENTRAL_NAME_LENGTH);
const uint16_t extraLength = zipU16(archive + offset + FF_ZIP_OFF_CENTRAL_EXTRA_LENGTH);
const uint16_t commentLength = zipU16(archive + offset + FF_ZIP_OFF_CENTRAL_COMMENT_LENGTH);
const size_t entrySize = FF_ZIP_CENTRAL_SIZE + (size_t) nameLength + (size_t) extraLength + (size_t) commentLength;
if (entrySize > directoryEnd - offset) {
FF_DEBUG("A zip entry claims %zu bytes but only %zu remain in the directory",
entrySize, (size_t) (directoryEnd - offset));
return "A zip directory entry runs past the directory";
}
if (nameLength == entryLength && memcmp(archive + offset + FF_ZIP_CENTRAL_SIZE, entry, entryLength) == 0) {
const uint32_t compressedSize = zipU32(archive + offset + FF_ZIP_OFF_CENTRAL_COMPRESSED_SIZE);
const uint32_t uncompressedSize = zipU32(archive + offset + FF_ZIP_OFF_CENTRAL_UNCOMPRESSED_SIZE);
const uint32_t localOffset = zipU32(archive + offset + FF_ZIP_OFF_CENTRAL_LOCAL_OFFSET);
if (compressedSize == FF_ZIP_ZIP64_SENTINEL || uncompressedSize == FF_ZIP_ZIP64_SENTINEL || localOffset == FF_ZIP_ZIP64_SENTINEL) {
FF_DEBUG("The zip entry \"%s\" is zip64: compressed=%u, uncompressed=%u, localOffset=%u",
entry, compressedSize, uncompressedSize, localOffset);
return "The zip entry is stored as zip64, which is not supported";
}
if (localOffset > archiveSize || FF_ZIP_LOCAL_HEADER_SIZE > archiveSize - localOffset) {
return "A zip local header is out of range";
}
const uint16_t localNameLength = zipU16(archive + localOffset + FF_ZIP_OFF_NAME_LENGTH);
const uint16_t localExtraLength = zipU16(archive + localOffset + FF_ZIP_OFF_EXTRA_LENGTH);
const size_t headerSize = FF_ZIP_LOCAL_HEADER_SIZE + (size_t) localNameLength + (size_t) localExtraLength;
if (headerSize > archiveSize - localOffset || compressedSize > archiveSize - localOffset - headerSize) {
return "The zip entry bounds are out of range";
}
result->method = zipU16(archive + offset + FF_ZIP_OFF_CENTRAL_METHOD);
result->uncompressedSize = uncompressedSize;
result->payload = archive + localOffset + headerSize;
result->payloadSize = compressedSize;
return nullptr;
}
offset += entrySize;
}
FF_DEBUG("No entry in the %zu byte archive is named \"%s\"", archiveSize, entry);
return "The archive has no entry with that name";
}
+66
View File
@@ -0,0 +1,66 @@
#pragma once
#include "fastfetch.h"
// Locating an entry inside a zip, which is what both the jar reader and the app label reader need:
// one looks for `classesN.dex`, the other for the resource table of an APK.
//
// The entry is found through the central directory rather than by scanning for its local header,
// and that is not a micro-optimisation. A local header is preceded by everything that came before
// it, so scanning costs a pass over all of it, once per lookup: `framework.jar` is 52 MB and its
// `classes2.dex` starts 9.6 MB in. Measured on the test device, where the sound module resolves
// seven codes, looking the entries up by scanning was the difference between 30 ms and 2 seconds.
//
// The sizes and the compression method are read from the directory rather than from the entry's own
// local header, which is the more reliable of the two: a local header is allowed to leave its sizes
// at zero and put them in a data descriptor behind the payload, and an archive writer is free to
// choose that. Only the local header's two length fields are used, because they are what says where
// the payload begins -- the local extra field does not have to match the central one.
// The zip local file header, from APPNOTE.TXT 4.3.7.
#define FF_ZIP_LOCAL_HEADER_SIZE 30
#define FF_ZIP_OFF_NAME_LENGTH 26
#define FF_ZIP_OFF_EXTRA_LENGTH 28
// The central directory.
#define FF_ZIP_CENTRAL_MAGIC "PK\x01\x02"
#define FF_ZIP_CENTRAL_SIZE 46
#define FF_ZIP_OFF_CENTRAL_METHOD 10
#define FF_ZIP_OFF_CENTRAL_COMPRESSED_SIZE 20
#define FF_ZIP_OFF_CENTRAL_UNCOMPRESSED_SIZE 24
#define FF_ZIP_OFF_CENTRAL_NAME_LENGTH 28
#define FF_ZIP_OFF_CENTRAL_EXTRA_LENGTH 30
#define FF_ZIP_OFF_CENTRAL_COMMENT_LENGTH 32
#define FF_ZIP_OFF_CENTRAL_LOCAL_OFFSET 42
// The end of central directory record, which points at the directory. It is the last thing in the
// file, behind a comment of at most 64 KiB, so it is searched for backwards over that much.
#define FF_ZIP_EOCD_MAGIC "PK\x05\x06"
#define FF_ZIP_EOCD_SIZE 22
#define FF_ZIP_EOCD_MAX_COMMENT 0xffffu
#define FF_ZIP_OFF_EOCD_DIRECTORY_SIZE 12
#define FF_ZIP_OFF_EOCD_DIRECTORY 16
// A size or offset of 0xFFFFFFFF means the real one is in a zip64 extra field. AOSP's framework jars
// and the APKs measured so far are orders of magnitude below that, and carrying the extra field
// reader for them is not worth it.
#define FF_ZIP_ZIP64_SENTINEL 0xffffffffu
#define FF_ZIP_METHOD_STORED 0
#define FF_ZIP_METHOD_DEFLATED 8
// One entry, as the archive describes it. `payload` points into the mapping the caller passed in and
// is not a copy: for a STORED entry it is the file's own bytes, and for a DEFLATED one it is the raw
// deflate stream, which the caller inflates with `uncompressedSize` as the bound.
typedef struct FFAndroidZipEntry {
const uint8_t* payload;
size_t payloadSize;
uint32_t uncompressedSize;
uint16_t method;
} FFAndroidZipEntry;
// Resolves `entry` through the archive's central directory, writing where its payload is into
// `result`. Returns nullptr on success, a static message otherwise -- an archive that is not a zip,
// a directory that is out of range, an entry that is not there and an entry stored as zip64 are all
// reported rather than answered with a pointer into nothing.
[[gnu::nonnull(1, 3, 4), nodiscard]] const char* ffAndroidZipFindEntry(const uint8_t* archive, size_t archiveSize, const char* entry, FFAndroidZipEntry* result);
+69 -13
View File
@@ -1,7 +1,9 @@
#include "media.h"
#include "common/android/api.h"
#include "common/android/applabel.h"
#include "common/android/binder.h"
#include "common/android/dex.h"
#include "common/android/package.h"
#include "common/debug.h"
#include "common/io.h"
#include "common/mallocHelper.h"
@@ -31,8 +33,9 @@
// service and reaches onTransact, and is then answered with a SecurityException carrying the server's
// own stack (`enforceMediaPermissions` <- `verifySessionsRequest` <- `getSessions`), while the shell
// UID gets the session list from the same transaction code. Nothing in this file tests the uid: the
// service is asked and its answer is believed, so a device whose policy is looser, or one where a
// notification listener is registered, works without a change here.
// service is asked and its answer is believed, so a device whose policy is looser works without a
// change here. What the answer does depend on is the ComponentName in the request, which is not a
// filter and not decoration either -- see the request build in androidDetectMediaSession().
//
// Only one of the two routes is ever asked, and which one it is depends on the display server. The
// binder route is Android's, and the session it reads is the one the phone is playing only while
@@ -94,8 +97,21 @@
// strings; a reply that did not fit would be reported rather than misread.
#define FF_MEDIA_ANDROID_REPLY_SIZE (16 * 1024)
// An interface token plus, for getSessions, two int32s.
#define FF_MEDIA_ANDROID_REQUEST_SIZE 128
// ffAndroidGetOwnPackage() writes at most this many bytes, terminator included.
#define FF_MEDIA_ANDROID_PACKAGE_SIZE 128
// The request is the interface token, the ComponentName and the user id. A ComponentName is a
// presence marker in front of the package name and the class name, so the two string16s are what
// took this past the 128 it used to be. That size had room for a seven-character package name --
// `com.termux` with an empty class is already 136 bytes -- so the size is derived from the longest
// name that can be written rather than rounded up by hand, because a request that does not fit is
// dropped and reported as truncated: that fails the whole detection instead of degrading to a call
// without a name. The empty class name costs a 4-byte length, a NUL terminator and the padding, so 8.
// clang-format off
#define FF_MEDIA_ANDROID_REQUEST_SIZE \
(12 + ((4 + 2 * sizeof(FF_MEDIA_ANDROID_SESSIONS_DESCRIPTOR) + 3) & ~3u) \
+ 4 + ((4 + 2 * FF_MEDIA_ANDROID_PACKAGE_SIZE + 3) & ~3u) + 8u + 4u)
// clang-format on
// Lossless WebP reads quality as effort rather than as fidelity: 0 is "compress quickly and accept a
// larger file", 100 is "spend the time". Measured on a 363x363 cover, the whole range is worth 1.2%
@@ -794,12 +810,18 @@ static void androidReadPackageName(FFBinder* binder, uint32_t token, uint32_t tr
return;
}
// `player` wants the name the app shows, and there is no way to get one from here: `cmd package
// dump` for this app is 1.2 MB and carries `labelRes`, a resource id, rather than a name, and
// resolving it would mean reading the app's resource table. The package name is what the
// notification and the settings screen fall back to as well, so both fields get it -- the same
// fallback media_linux.c uses when MPRIS answers without an Identity.
// `player` wants the name the app shows rather than the name it is installed under, and the
// package name is only the fallback. The label comes out of the APK's own resource table, which
// the package manager points at with `sourceDir` and `labelRes`: the table is *stored* rather
// than deflated -- the platform expects to mmap it -- so resolving an id out of it needs no
// decompressor. What keeps the package name are the apps whose label is a literal string in
// their manifest instead of a resource reference, since their reply carries no id at all; that
// is also what the notification and the settings screen fall back to. See doc/app-label.md.
ffStrbufSet(&result->player, &result->playerId);
const char* labelError = ffAndroidGetAppLabel(result->playerId.chars, &result->player);
if (labelError != nullptr) {
FF_DEBUG("The label of \"%s\" could not be resolved: %s", result->playerId.chars, labelError);
}
}
static void androidReadMetadata(FFBinder* binder, uint32_t token, uint32_t transaction, FFMediaResult* result, bool saveCover) {
@@ -859,7 +881,8 @@ static void androidReadSession(FFBinder* binder, uint32_t token, const FFMediaAn
// is the service's decision to make and it is not the only way in -- a notification listener is let
// through too, an OEM can loosen the check, a release can change it -- so the module asks and reads the
// answer. That costs one transaction, and it keeps the module working wherever the check happens to
// pass, instead of hard-coding today's policy into the client.
// pass, instead of hard-coding today's policy into the client. What the request has to carry for the
// check to be consulted at all is the ComponentName: see the request build below.
static const char* androidDetectMediaSession(FFMediaResult* media, bool saveCover) {
// Resolved before the binder is opened, so that a jar which does not declare the methods says so
// rather than the module failing later with a message about the transport. The reader's own
@@ -911,7 +934,38 @@ static const char* androidDetectMediaSession(FFMediaResult* media, bool saveCove
uint8_t request[FF_MEDIA_ANDROID_REQUEST_SIZE];
FFBinderParcel parcel = ffBinderParcelCreate(request, sizeof(request));
ffBinderParcelPutInterfaceToken(&parcel, FF_MEDIA_ANDROID_SESSIONS_DESCRIPTOR);
ffBinderParcelPutI32(&parcel, 0); // ComponentName: null asks for every session
// The ComponentName is not a filter and not decoration: it decides which branch of the
// service's permission check runs, and with a null one the branch this module needs is never
// reached. MediaSessionService.enforceMediaPermissions() admits the shell and root, a holder of
// android.permission.MEDIA_CONTENT_CONTROL, and a UID that owns an enabled notification
// listener -- but the last one is reached through isEnabledNotificationListener(compName, uid),
// which answers false for a null compName by construction. A null therefore asks for every
// session only on behalf of a UID that was going to be admitted anyway.
//
// Measured on the test device, one binary run by three UIDs: the shell gets the session list
// for every shape below; the app UID that owns an enabled listener gets -1 EX_SECURITY for a
// null ComponentName and the session list for a non-null one; an app UID that owns none gets
// -1 EX_SECURITY for both. Two more facts
// come out of the same run: the class inside the ComponentName is never validated -- a name no
// package declares and an empty string are answered exactly like a real one -- and the package
// has to be one the calling uid owns, because the service's own enforcePackageName() rejects
// anything else with EX_ILLEGAL_ARGUMENT ("packageName does not belong to the calling uid")
// before the permission check is reached.
//
// So the caller declares its own package and the class is left empty rather than invented:
// this is a standalone binary with no component of its own to name, and naming one the device
// does not have would be a claim that cannot be backed. A build whose package cannot be
// derived sends the null request, which is what this did before.
char package[FF_MEDIA_ANDROID_PACKAGE_SIZE];
if (ffAndroidGetOwnPackage(package, sizeof(package))) {
ffBinderParcelPutI32(&parcel, 1);
ffBinderParcelPutString16(&parcel, package);
ffBinderParcelPutString16(&parcel, "");
} else {
FF_DEBUG("Cannot determine the package of this process, so the request declares no component");
ffBinderParcelPutI32(&parcel, 0);
}
ffBinderParcelPutI32(&parcel, 0); // userId: the single user a phone has
FFBinderReply reply = ffBinderReplyCreate(s_reply, sizeof(s_reply));
@@ -927,11 +981,13 @@ static const char* androidDetectMediaSession(FFMediaResult* media, bool saveCove
if (exception != 0) {
// -1 is EX_SECURITY, which is what a UID the service will not read sessions for gets. The
// number goes to the debug log rather than into the message: what tells the two cases apart
// for the user is which UID is asking.
// for the user is which UID is asking. On an app UID the missing privilege is named,
// because it is one the user can grant -- the request below already declares the caller,
// so whatever is left is the listener the UID does not own.
FF_DEBUG("getSessions came back as exception %d%s", exception, exception == -1 ? " (EX_SECURITY)" : "");
return ffAndroidIsRootOrShell(instance.state.platform.uid)
? "The media session service refused the request"
: "Reading the media session needs the shell UID or root: an app UID is refused by the media_session service";
: "Reading the media session from an app UID needs an enabled notification listener, which this UID does not have";
}
// The tokens arrive as flat binder objects, which ffBinderTransact() collects in payload order