mirror of
https://github.com/fastfetch-cli/fastfetch.git
synced 2026-10-28 02:31:25 +01:00
Sound (Android): adds support via /dev/binder
This commit is contained in:
@@ -5,6 +5,7 @@ Changes:
|
||||
|
||||
Features:
|
||||
* Improved performance of LocalIP, NetIO and DNS on Windows
|
||||
* Added Sound detection on Android. (Sound, Android)
|
||||
|
||||
Bugfixes:
|
||||
* Fixed `--config`/`-c` bash completion offering one candidate per word instead of one candidate per file for paths containing spaces. (Completion, Bash)
|
||||
|
||||
+1
-1
@@ -706,7 +706,7 @@ elseif(ANDROID)
|
||||
src/detection/packages/packages_nix.c
|
||||
src/detection/poweradapter/poweradapter_nosupport.c
|
||||
src/detection/processes/processes_linux.c
|
||||
src/detection/sound/sound_linux.c
|
||||
src/detection/sound/sound_android.c
|
||||
src/detection/swap/swap_linux.c
|
||||
src/detection/terminalfont/terminalfont_android.c
|
||||
src/detection/terminalfont/terminalfont_linux.c
|
||||
|
||||
+217
-72
@@ -1,6 +1,7 @@
|
||||
#include "common/android/dex.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
@@ -32,19 +33,48 @@
|
||||
#define FF_DEX_FIELD_ID_SIZE 8
|
||||
#define FF_DEX_OFF_FIELD_ID_NAME 4
|
||||
|
||||
// The zip local file header, from APPNOTE.TXT 4.3.7. The central directory would carry the same
|
||||
// fields, but the local header sits right in front of the data, so one walk over the file finds
|
||||
// both the bounds and the payload.
|
||||
// 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_METHOD 8
|
||||
#define FF_ZIP_OFF_COMPRESSED_SIZE 18
|
||||
#define FF_ZIP_OFF_UNCOMPRESSED_SIZE 22
|
||||
#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
|
||||
// one of the ones past the bound is reported as missing.
|
||||
#define FF_DEX_MAX_ENTRIES 10
|
||||
#define FF_DEX_MAGIC "dex\n"
|
||||
|
||||
static uint16_t dexU16(const uint8_t* p) {
|
||||
@@ -96,18 +126,34 @@ static uint32_t dexUleb128(const uint8_t** p, const uint8_t* end) {
|
||||
}
|
||||
|
||||
// The type tag a dex encoded_value carries in the low five bits of its header byte, per the
|
||||
// `encoded_value` table of the dex format (`VALUE_INT = 0x04`, sign-extended four-byte integer).
|
||||
// 0x1f is VALUE_BOOLEAN in that same table, which is the trap: naming it VALUE_INT and comparing
|
||||
// against it rejects every real int and accepts booleans.
|
||||
#define FF_DEX_VALUE_INT 0x04
|
||||
// `encoded_value` table of the dex format.
|
||||
//
|
||||
// VALUE_NULL and VALUE_BOOLEAN are the trap. Their value *is* the header -- the type for a null,
|
||||
// the size nibble for a boolean -- and they carry **no payload bytes at all**, so the
|
||||
// `((size - 1) << 5) | type` rule that describes every other tag does not describe them. Taking the
|
||||
// size nibble at face value and reading one payload byte walks one byte too far, which pairs every
|
||||
// field after that with the wrong value. It is not a hypothetical: `IAudioService$Stub` declares ten
|
||||
// `PERMISSIONS_*` arrays that are all null, right behind its `DESCRIPTOR` string and ahead of its
|
||||
// 324 transaction constants, and those ten spurious bytes made
|
||||
// `TRANSACTION_getDevicesForAttributesUnprotected` read as 210 instead of 167 -- a code that reaches
|
||||
// a different method. `IWifiManager$Stub` has none ahead of the field the wifi module reads, which
|
||||
// is the only reason this went unnoticed.
|
||||
//
|
||||
// Naming VALUE_BOOLEAN also matters for a second reason: it is the tag a `static final boolean`
|
||||
// carries, and treating it as VALUE_INT would hand back a boolean where an int was asked for.
|
||||
typedef enum FFDexValueType : uint32_t {
|
||||
FF_DEX_VALUE_INT = 0x04, // sign-extended; one to four payload bytes
|
||||
FF_DEX_VALUE_NULL = 0x1e, // no payload
|
||||
FF_DEX_VALUE_BOOLEAN = 0x1f, // no payload; the boolean is in the header's size nibble
|
||||
} FFDexValueType;
|
||||
|
||||
// encoded_value: one header byte holding ((size - 1) << 5) | type, then that many payload bytes.
|
||||
//
|
||||
// The `static_values` array is not a list of ints: `IInterface.DESCRIPTOR` is a `static final
|
||||
// String` and sits in it, and a real Stub class mixes ints, strings and booleans. So a value whose
|
||||
// tag is not VALUE_INT is skipped -- its payload is stepped over to keep the walk aligned with the
|
||||
// field list -- and only `isInt` reports whether the *wanted* field ended up being one. Failing on
|
||||
// the first non-int instead would abort on `DESCRIPTOR`, before the transaction constants that
|
||||
// String` and sits in it, and a real Stub class mixes ints, strings, nulls and booleans. So a value
|
||||
// whose tag is not VALUE_INT is skipped -- its payload is stepped over to keep the walk aligned with
|
||||
// the field list -- and only `isInt` reports whether the *wanted* field ended up being one. Failing
|
||||
// on the first non-int instead would abort on `DESCRIPTOR`, before the transaction constants that
|
||||
// follow it are ever reached. Measured on a device: `IWifiManager$Stub` carries 347 static fields
|
||||
// whose values are mostly VALUE_INT but include that string, and `getConnectionInfo` is 98 there.
|
||||
//
|
||||
@@ -121,6 +167,15 @@ static bool dexEncodedValue(const uint8_t** p, const uint8_t* end, int32_t* resu
|
||||
}
|
||||
const uint8_t header = *(*p)++;
|
||||
const uint32_t type = (uint32_t) (header & 0x1f);
|
||||
|
||||
// The two tags whose value is the header byte itself. Returning here is what keeps the walk in
|
||||
// step; `isInt` is false because neither is an int.
|
||||
if (type == FF_DEX_VALUE_NULL || type == FF_DEX_VALUE_BOOLEAN) {
|
||||
*result = 0;
|
||||
*isInt = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
const uint32_t size = (uint32_t) (header >> 5) + 1;
|
||||
if (size > 8) {
|
||||
return false;
|
||||
@@ -176,7 +231,12 @@ static const char* dexString(const uint8_t* dex, const uint8_t* end, uint32_t in
|
||||
// Walks the class's static field list and the class's static value array in step, and returns the
|
||||
// value paired with `fieldName`. Both lists are ordered by field index and cover exactly the same
|
||||
// fields -- when they do not, the pairing is not trustworthy and nothing is returned.
|
||||
static const char* dexStaticInt(const uint8_t* dex, size_t size, const char* classDescriptor, const char* fieldName, int32_t* result) {
|
||||
//
|
||||
// `found` separates "this dex does not define the class" from a real failure, because a jar spreads
|
||||
// its classes over several dex files and the caller has to try the next one. It is set before any
|
||||
// of the walks below, so an error after it still means the class was here.
|
||||
static const char* dexStaticInt(const uint8_t* dex, size_t size, const char* classDescriptor, const char* fieldName, bool* found, int32_t* result) {
|
||||
*found = false;
|
||||
const uint8_t* end = dex + size;
|
||||
|
||||
if (size < FF_DEX_HEADER_SIZE || dexU32(dex + FF_DEX_OFF_ENDIAN_TAG) != FF_DEX_ENDIAN_TAG) {
|
||||
@@ -203,8 +263,9 @@ static const char* dexStaticInt(const uint8_t* dex, size_t size, const char* cla
|
||||
}
|
||||
}
|
||||
if (typeIndex == UINT32_MAX) {
|
||||
return "The class is not in the dex";
|
||||
return nullptr;
|
||||
}
|
||||
*found = true;
|
||||
|
||||
const uint8_t* classes = dex + dexU32(dex + FF_DEX_OFF_CLASS_DEFS);
|
||||
const uint32_t classCount = dexU32(dex + FF_DEX_OFF_CLASS_DEFS_SIZE);
|
||||
@@ -294,44 +355,94 @@ static void wrapDexMapping(FFDexMapping* mapping) {
|
||||
}
|
||||
}
|
||||
|
||||
// The local file header of `classes.dex` carries both the payload bounds and its compression, so a
|
||||
// single walk over the jar is enough to find it either way. AOSP builds framework jars with the
|
||||
// entry STORED so that ART can map it, which is the common case and needs no decompression.
|
||||
static const char* findDexEntry(const uint8_t* jar, size_t jarSize, const uint8_t** data, size_t* dataSize, uint32_t* uncompressedSize, uint16_t* method) {
|
||||
for (size_t i = 0; i + FF_ZIP_LOCAL_HEADER_SIZE <= jarSize; ++i) {
|
||||
if (memcmp(jar + i, "PK\x03\x04", 4) != 0) {
|
||||
continue;
|
||||
// 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.
|
||||
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";
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
const uint16_t nameLength = dexU16(jar + i + FF_ZIP_OFF_NAME_LENGTH);
|
||||
const uint16_t extraLength = dexU16(jar + i + FF_ZIP_OFF_EXTRA_LENGTH);
|
||||
if (nameLength != sizeof(FF_DEX_ENTRY) - 1) {
|
||||
continue;
|
||||
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;
|
||||
}
|
||||
const size_t headerSize = FF_ZIP_LOCAL_HEADER_SIZE + (size_t) nameLength + (size_t) extraLength;
|
||||
if (headerSize > jarSize - i) {
|
||||
continue;
|
||||
}
|
||||
if (eocd == SIZE_MAX) {
|
||||
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";
|
||||
}
|
||||
if (memcmp(jar + i + FF_ZIP_LOCAL_HEADER_SIZE, FF_DEX_ENTRY, sizeof(FF_DEX_ENTRY) - 1) != 0) {
|
||||
continue;
|
||||
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) {
|
||||
return "A zip directory entry runs past the directory";
|
||||
}
|
||||
|
||||
const uint32_t compressedSize = dexU32(jar + i + FF_ZIP_OFF_COMPRESSED_SIZE);
|
||||
if (compressedSize == 0 || compressedSize > jarSize - i - headerSize) {
|
||||
// A zero size means a data descriptor follows the payload instead of the header; the
|
||||
// magic scan in the caller still covers that case as long as the entry is STORED.
|
||||
continue;
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
*method = dexU16(jar + i + FF_ZIP_OFF_METHOD);
|
||||
*uncompressedSize = dexU32(jar + i + FF_ZIP_OFF_UNCOMPRESSED_SIZE);
|
||||
*data = jar + i + headerSize;
|
||||
*dataSize = compressedSize;
|
||||
return nullptr;
|
||||
|
||||
offset += entrySize;
|
||||
}
|
||||
return "The jar has no usable classes.dex entry";
|
||||
return "The jar has no usable dex entry";
|
||||
}
|
||||
|
||||
// Fallback for a jar whose entry is STORED but whose header has no sizes: the dex magic is then a
|
||||
// literal run of bytes in the file, and the header that follows it validates or it does not.
|
||||
// literal run of bytes in the file, and the header that follows it validates or it does not. It
|
||||
// only ever locates the *first* dex in the jar, so it is a fallback for `classes.dex` alone.
|
||||
static const uint8_t* findDexMagic(const uint8_t* jar, size_t jarSize, size_t* dexSize) {
|
||||
for (size_t i = 0; i + FF_DEX_HEADER_SIZE <= jarSize; ++i) {
|
||||
if (memcmp(jar + i, FF_DEX_MAGIC, sizeof(FF_DEX_MAGIC) - 1) != 0) {
|
||||
@@ -410,37 +521,71 @@ const char* ffDexStaticInt(const char* jarPath, const char* classDescriptor, con
|
||||
return "mmap(jar) failed";
|
||||
}
|
||||
|
||||
const uint8_t* data = nullptr;
|
||||
size_t dataSize = 0;
|
||||
uint32_t uncompressedSize = 0;
|
||||
uint16_t method = FF_ZIP_METHOD_STORED;
|
||||
|
||||
if (findDexEntry(mapping.mapped, mapping.mappedSize, &data, &dataSize, &uncompressedSize, &method) != nullptr) {
|
||||
data = findDexMagic(mapping.mapped, mapping.mappedSize, &dataSize);
|
||||
if (data == nullptr) {
|
||||
return "No dex in the jar";
|
||||
// A jar spreads its classes over `classes.dex`, `classes2.dex`, `classes3.dex`, ... and the
|
||||
// class being looked for is in whichever one the build put it in: `IAudioService$Stub` is in the
|
||||
// second dex of framework.jar, while `IWifiManager$Stub` is in the first of framework-wifi.jar.
|
||||
// The entries are therefore tried in order, and the first one that defines the class answers.
|
||||
// The count is a bound on the walk, not a claim about the jar -- AOSP has never shipped more
|
||||
// than a handful, and a class that is in none of them is reported as missing either way.
|
||||
const char* error = "The class is not in the jar";
|
||||
for (uint32_t index = 1; index <= FF_DEX_MAX_ENTRIES; ++index) {
|
||||
char entry[sizeof("classes999.dex")];
|
||||
if (index == 1) {
|
||||
memcpy(entry, FF_DEX_ENTRY, sizeof(FF_DEX_ENTRY));
|
||||
} else {
|
||||
(void) snprintf(entry, sizeof(entry), "classes%u.dex", index);
|
||||
}
|
||||
} else if (method == FF_ZIP_METHOD_DEFLATED) {
|
||||
#ifdef FF_HAVE_ZLIB
|
||||
const char* error = inflateDex(data, dataSize, uncompressedSize, &mapping.inflated);
|
||||
|
||||
const uint8_t* data = nullptr;
|
||||
size_t dataSize = 0;
|
||||
uint32_t uncompressedSize = 0;
|
||||
uint16_t method = FF_ZIP_METHOD_STORED;
|
||||
|
||||
if (findDexEntry(mapping.mapped, mapping.mappedSize, entry, &data, &dataSize, &uncompressedSize, &method) != nullptr) {
|
||||
if (index > 1) {
|
||||
// The magic scan only ever finds the first dex, so it cannot stand in for a later
|
||||
// entry: running past the end of the entries that exist ends the walk.
|
||||
break;
|
||||
}
|
||||
data = findDexMagic(mapping.mapped, mapping.mappedSize, &dataSize);
|
||||
if (data == nullptr) {
|
||||
return "No dex in the jar";
|
||||
}
|
||||
} else if (method == FF_ZIP_METHOD_DEFLATED) {
|
||||
#ifdef FF_HAVE_ZLIB
|
||||
// One buffer is reused across the walk, so the previous dex is released first.
|
||||
free(mapping.inflated);
|
||||
mapping.inflated = nullptr;
|
||||
error = inflateDex(data, dataSize, uncompressedSize, &mapping.inflated);
|
||||
if (error != nullptr) {
|
||||
return error;
|
||||
}
|
||||
data = mapping.inflated;
|
||||
dataSize = uncompressedSize;
|
||||
#else
|
||||
return "The jar deflates its dex entries and fastfetch was built without zlib";
|
||||
#endif
|
||||
} else if (method != FF_ZIP_METHOD_STORED) {
|
||||
return "A dex entry uses an unsupported compression method";
|
||||
}
|
||||
// A STORED entry needs no further work: `dataSize` already holds the `compressedSize` that
|
||||
// `findDexEntry` checked against the mapping, and for a STORED entry the payload is the file
|
||||
// itself. The header's `uncompressedSize` is deliberately not used for it -- nothing bounds
|
||||
// that value, so a corrupt one reaches past the end of the mapping, which is exactly what the
|
||||
// dex header would then be validated against.
|
||||
|
||||
mapping.data = data;
|
||||
mapping.size = dataSize;
|
||||
|
||||
bool found = false;
|
||||
error = dexStaticInt(mapping.data, mapping.size, classDescriptor, fieldName, &found, result);
|
||||
if (error != nullptr) {
|
||||
return error;
|
||||
}
|
||||
data = mapping.inflated;
|
||||
dataSize = uncompressedSize;
|
||||
#else
|
||||
return "The jar deflates classes.dex and fastfetch was built without zlib";
|
||||
#endif
|
||||
} else if (method != FF_ZIP_METHOD_STORED) {
|
||||
return "classes.dex uses an unsupported compression method";
|
||||
if (found) {
|
||||
return nullptr;
|
||||
}
|
||||
error = "The class is not in the jar";
|
||||
}
|
||||
// A STORED entry needs no further work: `dataSize` already holds the `compressedSize` that
|
||||
// `findDexEntry` checked against the mapping, and for a STORED entry the payload is the file
|
||||
// itself. The header's `uncompressedSize` is deliberately not used for it -- nothing bounds that
|
||||
// value, so a corrupt one reaches past the end of the mapping, which is exactly what the dex
|
||||
// header would then be validated against.
|
||||
|
||||
mapping.data = data;
|
||||
mapping.size = dataSize;
|
||||
return dexStaticInt(mapping.data, mapping.size, classDescriptor, fieldName, result);
|
||||
return error;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
|
||||
#include "fastfetch.h"
|
||||
|
||||
// Resolves the int value of `<classDescriptor>.<fieldName>` out of the `classes.dex` of `jarPath`.
|
||||
// Resolves the int value of `<classDescriptor>.<fieldName>` out of the dex entries of `jarPath`,
|
||||
// walking `classes.dex`, `classes2.dex`, ... in order until one of them defines the class.
|
||||
// `classDescriptor` is the dex type descriptor, e.g. "Landroid/net/wifi/IWifiManager$Stub;".
|
||||
// Returns nullptr on success, a static message otherwise.
|
||||
[[gnu::nonnull(1, 2, 3, 4), nodiscard]] const char* ffDexStaticInt(const char* jarPath, const char* classDescriptor, const char* fieldName, int32_t* result);
|
||||
|
||||
@@ -0,0 +1,608 @@
|
||||
#include "sound.h"
|
||||
|
||||
#include "detection/displayserver/displayserver.h"
|
||||
#include "common/android/binder.h"
|
||||
#include "common/android/dex.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/strutil.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
// Android has no desktop sound stack. PulseAudio is not part of the platform -- a Termux user can
|
||||
// start one, and then the module reads that, but its sink is a virtual device of Termux's own that
|
||||
// feeds Android's OpenSL ES output with a volume that is always at full scale, so the number says
|
||||
// nothing about the phone. ALSA and OSS are not there either: /dev/snd, /proc/asound and
|
||||
// /sys/class/sound are all EACCES from an app's SELinux domain, `dumpsys audio` needs
|
||||
// android.permission.DUMP, and `settings get system volume_music` needs INTERACT_ACROSS_USERS.
|
||||
// `cmd audio` is worse than useless: `cmd` hands the arguments to the service over binder and the
|
||||
// command body runs inside system_server, so from an app it hangs or answers ENOSYS.
|
||||
//
|
||||
// What is left is the service those all sit on: `audio`, i.e. android.media.IAudioService, which
|
||||
// lives in system_server. This file speaks to it over /dev/binder, the same way the battery, media
|
||||
// and wifi modules do. Two things about it are worth stating up front, because they are what makes
|
||||
// this module work at all:
|
||||
//
|
||||
// * Nothing here needs a permission. The methods used below are the ones `AudioManager` itself
|
||||
// calls on behalf of an ordinary app, and the service checks no permission for them. The one
|
||||
// that does the real work is deliberately named for it -- see getDevicesForAttributesUnprotected
|
||||
// below. The protected twin, `getDevicesForAttributes`, is refused with
|
||||
// "Missing MODIFY_AUDIO_ROUTING or QUERY_AUDIO_STATE permissions", and
|
||||
// `getLastAudibleStreamVolume` with "requires android.permission.QUERY_AUDIO_STATE"; neither is
|
||||
// used. So unlike the wifi module, this one works on a device where nothing has been granted.
|
||||
//
|
||||
// * Every transaction code is resolved from the device's own jar at run time, never written down.
|
||||
// `IAudioService$Stub` declares 283 methods in AOSP and 324 on the test device -- a vendor fork
|
||||
// inserts methods freely, and an insertion ahead of the one being called shifts it. Reading the
|
||||
// constants out of the dex is the only thing that survives that; see common/android/dex.h. The
|
||||
// class is in the *second* dex of framework.jar, which is why ffDexStaticInt walks the entries.
|
||||
//
|
||||
// The reply is not uniform, because the two kinds of service are not: IAudioService is Java, so a
|
||||
// failure arrives as a parcel exception code (-1 for a SecurityException, -4 for a null argument),
|
||||
// while the native services answer with a TF_STATUS_CODE carrying a raw status_t. Only the former
|
||||
// can happen here, but the check is cheap and the two are easy to confuse.
|
||||
|
||||
#define FF_SOUND_ANDROID_SERVICE "audio"
|
||||
#define FF_SOUND_ANDROID_DESCRIPTOR "android.media.IAudioService"
|
||||
|
||||
// IAudioService 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. The stub is in classes2.dex there.
|
||||
#define FF_SOUND_ANDROID_JAR "/system/framework/framework.jar"
|
||||
#define FF_SOUND_ANDROID_STUB "Landroid/media/IAudioService$Stub;"
|
||||
|
||||
// The methods this module calls, by the name of the constant the dex carries for each. Spelling the
|
||||
// names out rather than the numbers is the whole point: the lookup turns them into whatever this
|
||||
// build uses, and a build that does not declare one says so instead of reaching another method.
|
||||
#define FF_SOUND_ANDROID_GET_STREAM_VOLUME "TRANSACTION_getStreamVolume"
|
||||
#define FF_SOUND_ANDROID_GET_STREAM_MIN_VOLUME "TRANSACTION_getStreamMinVolume"
|
||||
#define FF_SOUND_ANDROID_GET_STREAM_MAX_VOLUME "TRANSACTION_getStreamMaxVolume"
|
||||
#define FF_SOUND_ANDROID_IS_STREAM_MUTE "TRANSACTION_isStreamMute"
|
||||
#define FF_SOUND_ANDROID_IS_MUSIC_ACTIVE "TRANSACTION_isMusicActive"
|
||||
#define FF_SOUND_ANDROID_GET_DEVICES_FOR_ATTRIBUTES "TRANSACTION_getDevicesForAttributesUnprotected"
|
||||
#define FF_SOUND_ANDROID_GET_BT_ACTIVE_DEVICE_NAME "TRANSACTION_getBtActiveDeviceName"
|
||||
|
||||
// `AudioManager.STREAM_MUSIC`. The one stream a phone's volume rocker moves, and the one the module
|
||||
// reports; the other eleven are internal (SYSTEM_ENFORCED, DTMF, ...) or belong to a call.
|
||||
typedef enum FFSoundAndroidStream : int32_t {
|
||||
FF_SOUND_ANDROID_STREAM_MUSIC = 3,
|
||||
} FFSoundAndroidStream;
|
||||
|
||||
// `AudioAttributes.USAGE_MEDIA`. Asking the policy what it routes for media is what makes the answer
|
||||
// "where the phone's sound is going"; other usages answer differently on purpose (a call goes to the
|
||||
// earpiece, an emergency call to the telephony port).
|
||||
typedef enum FFSoundAndroidUsage : int32_t {
|
||||
FF_SOUND_ANDROID_USAGE_MEDIA = 1,
|
||||
} FFSoundAndroidUsage;
|
||||
|
||||
// `AudioAttributes.SOURCE_*`, and the one value that matters here. A request whose source is
|
||||
// anything but AUDIO_SOURCE_INVALID takes the *capture* branch of
|
||||
// AudioPolicyManager::getDevicesForAttributes and answers with microphones, whatever the usage says:
|
||||
//
|
||||
// if (attr.source != AUDIO_SOURCE_INVALID) return getInputDevicesForAttributes(attr, devices);
|
||||
//
|
||||
// AUDIO_SOURCE_DEFAULT is 0, so leaving the field at zero -- which is what a zeroed parcel does --
|
||||
// silently reports the built-in microphone as the output device. It has to be written out as -1.
|
||||
#define FF_SOUND_ANDROID_SOURCE_INVALID (-1)
|
||||
|
||||
// `AudioAttributes.ATTR_PARCEL_IS_NULL_BUNDLE`. The tags and the bundle are the only variable-length
|
||||
// parts of the request; an empty tag list and no bundle is the smallest well-formed one.
|
||||
#define FF_SOUND_ANDROID_ATTR_NULL_BUNDLE (-1977)
|
||||
|
||||
// The interface token is 12 bytes of header plus a padded string16, and the largest request here adds
|
||||
// one AudioAttributes to it. The replies are one int, or one list of AudioDeviceAttributes. The
|
||||
// device reply is sized well past the 48 bytes a single device takes, because a reply that does not
|
||||
// fit is reported as a buffer error and would hide the exception message the service sent instead.
|
||||
#define FF_SOUND_ANDROID_SCALAR_PARCEL_SIZE 96
|
||||
#define FF_SOUND_ANDROID_SCALAR_REPLY_SIZE 32
|
||||
#define FF_SOUND_ANDROID_DEVICE_PARCEL_SIZE 128
|
||||
#define FF_SOUND_ANDROID_DEVICE_REPLY_SIZE 1024
|
||||
|
||||
// The reply to the Bluetooth device name request is one string16, and a device name is chosen by
|
||||
// whoever owns the headset: 248 bytes is the longest a Bluetooth name can be, so 512 bytes of reply
|
||||
// hold the count, the code units, the terminator and the padding with room to spare. A reply that
|
||||
// does not fit is reported as a buffer error, so sizing it generously is what keeps a long name
|
||||
// from looking like a failure.
|
||||
#define FF_SOUND_ANDROID_NAME_REPLY_SIZE 512
|
||||
|
||||
// `AudioDeviceInfo.TYPE_*`, as the test device's own dex declares them (read out of
|
||||
// `Landroid/media/AudioDeviceInfo;`). The value is what `AudioDeviceAttributes.type` carries in the
|
||||
// reply; the names are the AOSP constant names without the prefix, which is what goes into
|
||||
// `identifier`, and a spelled out name, which is what the module prints. An unrecognised value is
|
||||
// reported rather than guessed at: a vendor is free to add types, and a wrong name is worse than
|
||||
// "Unknown Device".
|
||||
typedef enum FFSoundAndroidDeviceType : int32_t {
|
||||
FF_SOUND_ANDROID_TYPE_UNKNOWN = 0,
|
||||
FF_SOUND_ANDROID_TYPE_BUILTIN_EARPIECE = 1,
|
||||
FF_SOUND_ANDROID_TYPE_BUILTIN_SPEAKER = 2,
|
||||
FF_SOUND_ANDROID_TYPE_WIRED_HEADSET = 3,
|
||||
FF_SOUND_ANDROID_TYPE_WIRED_HEADPHONES = 4,
|
||||
FF_SOUND_ANDROID_TYPE_LINE_ANALOG = 5,
|
||||
FF_SOUND_ANDROID_TYPE_LINE_DIGITAL = 6,
|
||||
FF_SOUND_ANDROID_TYPE_BLUETOOTH_SCO = 7,
|
||||
FF_SOUND_ANDROID_TYPE_BLUETOOTH_A2DP = 8,
|
||||
FF_SOUND_ANDROID_TYPE_HDMI = 9,
|
||||
FF_SOUND_ANDROID_TYPE_HDMI_ARC = 10,
|
||||
FF_SOUND_ANDROID_TYPE_USB_DEVICE = 11,
|
||||
FF_SOUND_ANDROID_TYPE_USB_ACCESSORY = 12,
|
||||
FF_SOUND_ANDROID_TYPE_DOCK = 13,
|
||||
FF_SOUND_ANDROID_TYPE_FM = 14,
|
||||
FF_SOUND_ANDROID_TYPE_BUILTIN_MIC = 15,
|
||||
FF_SOUND_ANDROID_TYPE_FM_TUNER = 16,
|
||||
FF_SOUND_ANDROID_TYPE_TV_TUNER = 17,
|
||||
FF_SOUND_ANDROID_TYPE_TELEPHONY = 18,
|
||||
FF_SOUND_ANDROID_TYPE_AUX_LINE = 19,
|
||||
FF_SOUND_ANDROID_TYPE_IP = 20,
|
||||
FF_SOUND_ANDROID_TYPE_BUS = 21,
|
||||
FF_SOUND_ANDROID_TYPE_USB_HEADSET = 22,
|
||||
FF_SOUND_ANDROID_TYPE_HEARING_AID = 23,
|
||||
FF_SOUND_ANDROID_TYPE_BUILTIN_SPEAKER_SAFE = 24,
|
||||
FF_SOUND_ANDROID_TYPE_REMOTE_SUBMIX = 25,
|
||||
FF_SOUND_ANDROID_TYPE_BLE_HEADSET = 26,
|
||||
FF_SOUND_ANDROID_TYPE_BLE_SPEAKER = 27,
|
||||
FF_SOUND_ANDROID_TYPE_ECHO_REFERENCE = 28,
|
||||
FF_SOUND_ANDROID_TYPE_HDMI_EARC = 29,
|
||||
FF_SOUND_ANDROID_TYPE_BLE_BROADCAST = 30,
|
||||
FF_SOUND_ANDROID_TYPE_DOCK_ANALOG = 31,
|
||||
FF_SOUND_ANDROID_TYPE_MULTICHANNEL_GROUP = 32,
|
||||
} FFSoundAndroidDeviceType;
|
||||
|
||||
typedef struct FFSoundAndroidDeviceName {
|
||||
FFSoundAndroidDeviceType type;
|
||||
const char* identifier;
|
||||
const char* name;
|
||||
} FFSoundAndroidDeviceName;
|
||||
|
||||
static const FFSoundAndroidDeviceName FF_SOUND_ANDROID_DEVICE_NAMES[] = {
|
||||
{ FF_SOUND_ANDROID_TYPE_UNKNOWN, "UNKNOWN", "Unknown Device" },
|
||||
{ FF_SOUND_ANDROID_TYPE_BUILTIN_EARPIECE, "BUILTIN_EARPIECE", "Built-in Earpiece" },
|
||||
{ FF_SOUND_ANDROID_TYPE_BUILTIN_SPEAKER, "BUILTIN_SPEAKER", "Built-in Speaker" },
|
||||
{ FF_SOUND_ANDROID_TYPE_WIRED_HEADSET, "WIRED_HEADSET", "Wired Headset" },
|
||||
{ FF_SOUND_ANDROID_TYPE_WIRED_HEADPHONES, "WIRED_HEADPHONES", "Wired Headphones" },
|
||||
{ FF_SOUND_ANDROID_TYPE_LINE_ANALOG, "LINE_ANALOG", "Line (Analog)" },
|
||||
{ FF_SOUND_ANDROID_TYPE_LINE_DIGITAL, "LINE_DIGITAL", "Line (Digital)" },
|
||||
{ FF_SOUND_ANDROID_TYPE_BLUETOOTH_SCO, "BLUETOOTH_SCO", "Bluetooth SCO" },
|
||||
{ FF_SOUND_ANDROID_TYPE_BLUETOOTH_A2DP, "BLUETOOTH_A2DP", "Bluetooth A2DP" },
|
||||
{ FF_SOUND_ANDROID_TYPE_HDMI, "HDMI", "HDMI" },
|
||||
{ FF_SOUND_ANDROID_TYPE_HDMI_ARC, "HDMI_ARC", "HDMI ARC" },
|
||||
{ FF_SOUND_ANDROID_TYPE_USB_DEVICE, "USB_DEVICE", "USB Device" },
|
||||
{ FF_SOUND_ANDROID_TYPE_USB_ACCESSORY, "USB_ACCESSORY", "USB Accessory" },
|
||||
{ FF_SOUND_ANDROID_TYPE_DOCK, "DOCK", "Dock" },
|
||||
{ FF_SOUND_ANDROID_TYPE_FM, "FM", "FM" },
|
||||
{ FF_SOUND_ANDROID_TYPE_BUILTIN_MIC, "BUILTIN_MIC", "Built-in Microphone" },
|
||||
{ FF_SOUND_ANDROID_TYPE_FM_TUNER, "FM_TUNER", "FM Tuner" },
|
||||
{ FF_SOUND_ANDROID_TYPE_TV_TUNER, "TV_TUNER", "TV Tuner" },
|
||||
{ FF_SOUND_ANDROID_TYPE_TELEPHONY, "TELEPHONY", "Telephony" },
|
||||
{ FF_SOUND_ANDROID_TYPE_AUX_LINE, "AUX_LINE", "Aux Line" },
|
||||
{ FF_SOUND_ANDROID_TYPE_IP, "IP", "IP" },
|
||||
{ FF_SOUND_ANDROID_TYPE_BUS, "BUS", "Bus" },
|
||||
{ FF_SOUND_ANDROID_TYPE_USB_HEADSET, "USB_HEADSET", "USB Headset" },
|
||||
{ FF_SOUND_ANDROID_TYPE_HEARING_AID, "HEARING_AID", "Hearing Aid" },
|
||||
{ FF_SOUND_ANDROID_TYPE_BUILTIN_SPEAKER_SAFE, "BUILTIN_SPEAKER_SAFE", "Built-in Speaker (Safe)" },
|
||||
{ FF_SOUND_ANDROID_TYPE_REMOTE_SUBMIX, "REMOTE_SUBMIX", "Remote Submix" },
|
||||
{ FF_SOUND_ANDROID_TYPE_BLE_HEADSET, "BLE_HEADSET", "Bluetooth LE Headset" },
|
||||
{ FF_SOUND_ANDROID_TYPE_BLE_SPEAKER, "BLE_SPEAKER", "Bluetooth LE Speaker" },
|
||||
{ FF_SOUND_ANDROID_TYPE_ECHO_REFERENCE, "ECHO_REFERENCE", "Echo Reference" },
|
||||
{ FF_SOUND_ANDROID_TYPE_HDMI_EARC, "HDMI_EARC", "HDMI eARC" },
|
||||
{ FF_SOUND_ANDROID_TYPE_BLE_BROADCAST, "BLE_BROADCAST", "Bluetooth LE Broadcast" },
|
||||
{ FF_SOUND_ANDROID_TYPE_DOCK_ANALOG, "DOCK_ANALOG", "Dock (Analog)" },
|
||||
{ FF_SOUND_ANDROID_TYPE_MULTICHANNEL_GROUP, "MULTICHANNEL_GROUP", "Multichannel Group" },
|
||||
};
|
||||
|
||||
static const FFSoundAndroidDeviceName* soundAndroidDeviceName(int32_t type) {
|
||||
for (size_t i = 0; i < ARRAY_SIZE(FF_SOUND_ANDROID_DEVICE_NAMES); ++i) {
|
||||
if ((int32_t) FF_SOUND_ANDROID_DEVICE_NAMES[i].type == type) {
|
||||
return &FF_SOUND_ANDROID_DEVICE_NAMES[i];
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// The types that stand for a Bluetooth device. They are the ones whose own name is worth asking the
|
||||
// service for: everything else is described well enough by its type, and the request is one more
|
||||
// round trip to system_server. A hearing aid is on the list because it is a Bluetooth LE link with
|
||||
// a name of its own, the same as a headset.
|
||||
static bool soundAndroidIsBluetooth(int32_t type) {
|
||||
switch ((FFSoundAndroidDeviceType) type) {
|
||||
case FF_SOUND_ANDROID_TYPE_BLUETOOTH_SCO:
|
||||
case FF_SOUND_ANDROID_TYPE_BLUETOOTH_A2DP:
|
||||
case FF_SOUND_ANDROID_TYPE_HEARING_AID:
|
||||
case FF_SOUND_ANDROID_TYPE_BLE_HEADSET:
|
||||
case FF_SOUND_ANDROID_TYPE_BLE_SPEAKER:
|
||||
case FF_SOUND_ANDROID_TYPE_BLE_BROADCAST:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Calls a method that takes no argument or one int, and answers with one int. Every transaction code
|
||||
// is resolved here rather than passed in, so that the dex is the only place a number lives.
|
||||
static const char* soundAndroidCallInt(FFBinder* binder, uint32_t handle, const char* transactionField, bool hasArgument, int32_t argument, int32_t* result) {
|
||||
int32_t transaction = 0;
|
||||
const char* error = ffDexStaticInt(FF_SOUND_ANDROID_JAR, FF_SOUND_ANDROID_STUB, transactionField, &transaction);
|
||||
if (error != nullptr) {
|
||||
FF_DEBUG("Reading \"%s\" from \"%s\" failed: %s", transactionField, FF_SOUND_ANDROID_JAR, error);
|
||||
return error;
|
||||
}
|
||||
|
||||
uint8_t parcelBuffer[FF_SOUND_ANDROID_SCALAR_PARCEL_SIZE];
|
||||
FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer));
|
||||
ffBinderParcelPutInterfaceToken(&parcel, FF_SOUND_ANDROID_DESCRIPTOR);
|
||||
if (hasArgument) {
|
||||
ffBinderParcelPutI32(&parcel, argument);
|
||||
}
|
||||
|
||||
uint8_t replyBuffer[FF_SOUND_ANDROID_SCALAR_REPLY_SIZE];
|
||||
FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer));
|
||||
error = ffBinderTransact(binder, handle, (uint32_t) transaction, 0, &parcel, &reply);
|
||||
if (error != nullptr) {
|
||||
return error;
|
||||
}
|
||||
if (ffBinderReplyIsStatus(&reply)) {
|
||||
FF_DEBUG("\"%s\" resolved to transaction %d, which this build does not answer", transactionField, transaction);
|
||||
return "The audio service does not answer that request";
|
||||
}
|
||||
const int32_t exception = ffBinderReadI32(reply.data, reply.size, 0);
|
||||
if (exception != 0) {
|
||||
FF_DEBUG("\"%s\" is transaction %d and raised exception %d", transactionField, transaction, exception);
|
||||
return "The audio service raised an exception";
|
||||
}
|
||||
*result = ffBinderReadI32(reply.data, reply.size, 4);
|
||||
FF_DEBUG("\"%s\" is transaction %d and answered %d", transactionField, transaction, *result);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Walks past a string16 and reports how long it was. The bytes themselves are not decoded: the only
|
||||
// strings in the reply are the device address and name, both of which are empty for every built-in
|
||||
// device and a partly anonymised MAC for a Bluetooth one -- `AudioService` blanks the address of
|
||||
// anything it considers identifying before answering an unprivileged caller, so neither is an
|
||||
// identifier worth carrying.
|
||||
static bool soundAndroidSkipString16(const uint8_t* data, size_t size, size_t* offset) {
|
||||
const int32_t length = ffBinderReadI32(data, size, *offset);
|
||||
*offset += sizeof(int32_t);
|
||||
if (length < 0) {
|
||||
return true;
|
||||
}
|
||||
// The code units plus the NUL terminator, padded to four bytes.
|
||||
const size_t payload = ((size_t) length * 2 + 2 + 3) & ~(size_t) 3;
|
||||
if (*offset + payload > size) {
|
||||
return false;
|
||||
}
|
||||
*offset += payload;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Reads a string16 out of a reply and appends it to `result`.
|
||||
//
|
||||
// The code units are UTF-16LE, and a device name is the first string here that is not guaranteed to
|
||||
// be ASCII -- a headset can be named in any script, and `vivo TWS A4` is only the one that happened
|
||||
// to be paired -- so they are converted rather than copied. A count of -1 is how the same field
|
||||
// spells null, which is what the service answers when there is no name to give; that is not a
|
||||
// failure, and the caller falls back to the type name.
|
||||
static bool soundAndroidReadString16(const uint8_t* data, size_t size, size_t offset, FFstrbuf* result) {
|
||||
if (offset + sizeof(int32_t) > size) {
|
||||
return false;
|
||||
}
|
||||
const int32_t length = ffBinderReadI32(data, size, offset);
|
||||
offset += sizeof(int32_t);
|
||||
if (length < 0) {
|
||||
return true;
|
||||
}
|
||||
// The code units plus the NUL terminator. The padding behind them is not part of the string, so
|
||||
// there is nothing to step over here.
|
||||
if ((size_t) length * 2 + 2 > size - offset) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int32_t i = 0; i < length; ++i) {
|
||||
uint32_t codePoint = (uint32_t) data[offset + (size_t) i * 2] | ((uint32_t) data[offset + (size_t) i * 2 + 1] << 8);
|
||||
if (codePoint >= 0xD800 && codePoint <= 0xDBFF && i + 1 < length) {
|
||||
// Anything outside the basic plane arrives as a surrogate pair, and the low half is what
|
||||
// turns it back into one code point. A high surrogate without one is malformed and is
|
||||
// passed through as it stands rather than guessed at.
|
||||
const uint32_t low = (uint32_t) data[offset + (size_t) (i + 1) * 2] | ((uint32_t) data[offset + (size_t) (i + 1) * 2 + 1] << 8);
|
||||
if (low >= 0xDC00 && low <= 0xDFFF) {
|
||||
codePoint = 0x10000u + ((codePoint - 0xD800u) << 10) + (low - 0xDC00u);
|
||||
++i;
|
||||
}
|
||||
}
|
||||
ffStrbufAppendUtf32CodePoint(result, codePoint);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Calls `getBtActiveDeviceName()`, which takes no argument and answers a string16. It is the only
|
||||
// call here that carries a human readable device name.
|
||||
//
|
||||
// The policy reply has a `name` field of its own, but on the test device `AudioService` leaves it
|
||||
// empty for every device -- a Bluetooth one included, whose address it blanks to
|
||||
// `XX:XX:XX:XX:D8:61` while the name goes with it -- so the type is all that reply can describe a
|
||||
// sink by. This call is what turns "Bluetooth A2DP" into the name of the headset.
|
||||
//
|
||||
// What it must not be used for is deciding *which* device is the output. It answers about the
|
||||
// Bluetooth route as a whole and reports it even when the policy has selected the speaker, so it is
|
||||
// only ever consulted after the device list has already picked a Bluetooth sink, and only its name
|
||||
// is taken from it.
|
||||
static const char* soundAndroidCallBtName(FFBinder* binder, uint32_t handle, FFstrbuf* result) {
|
||||
int32_t transaction = 0;
|
||||
const char* error = ffDexStaticInt(FF_SOUND_ANDROID_JAR, FF_SOUND_ANDROID_STUB, FF_SOUND_ANDROID_GET_BT_ACTIVE_DEVICE_NAME, &transaction);
|
||||
if (error != nullptr) {
|
||||
FF_DEBUG("Reading \"%s\" from \"%s\" failed: %s", FF_SOUND_ANDROID_GET_BT_ACTIVE_DEVICE_NAME, FF_SOUND_ANDROID_JAR, error);
|
||||
return error;
|
||||
}
|
||||
|
||||
uint8_t parcelBuffer[FF_SOUND_ANDROID_SCALAR_PARCEL_SIZE];
|
||||
FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer));
|
||||
ffBinderParcelPutInterfaceToken(&parcel, FF_SOUND_ANDROID_DESCRIPTOR);
|
||||
|
||||
uint8_t replyBuffer[FF_SOUND_ANDROID_NAME_REPLY_SIZE];
|
||||
FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer));
|
||||
error = ffBinderTransact(binder, handle, (uint32_t) transaction, 0, &parcel, &reply);
|
||||
if (error != nullptr) {
|
||||
return error;
|
||||
}
|
||||
if (ffBinderReplyIsStatus(&reply)) {
|
||||
FF_DEBUG("\"%s\" resolved to transaction %d, which this build does not answer", FF_SOUND_ANDROID_GET_BT_ACTIVE_DEVICE_NAME, transaction);
|
||||
return "The audio service does not answer that request";
|
||||
}
|
||||
const int32_t exception = ffBinderReadI32(reply.data, reply.size, 0);
|
||||
if (exception != 0) {
|
||||
FF_DEBUG("\"%s\" is transaction %d and raised exception %d", FF_SOUND_ANDROID_GET_BT_ACTIVE_DEVICE_NAME, transaction, exception);
|
||||
return "The audio service raised an exception";
|
||||
}
|
||||
if (!soundAndroidReadString16(reply.data, reply.size, sizeof(int32_t), result)) {
|
||||
return "The audio service answered a truncated device name";
|
||||
}
|
||||
FF_DEBUG("\"%s\" is transaction %d and answered \"%s\"", FF_SOUND_ANDROID_GET_BT_ACTIVE_DEVICE_NAME, transaction,
|
||||
result->chars != nullptr ? result->chars : "");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Reads one `AudioDeviceAttributes` out of the reply and advances `offset` past it.
|
||||
//
|
||||
// The layout is the one `AudioDeviceAttributes.writeToParcel` writes, and it was checked byte by
|
||||
// byte against the device's replies:
|
||||
//
|
||||
// int32 role, int32 type, string16 address, string16 name, int32 nativeType,
|
||||
// List<AudioProfile> profiles, List<AudioProfile> descriptors
|
||||
//
|
||||
// `role` is 1 for a source (an input) and 2 for a sink (an output). `type` is the
|
||||
// AudioDeviceInfo.TYPE_* above; `nativeType` repeats it as the older AUDIO_DEVICE_* bit value and is
|
||||
// not used. Neither profile list has carried an entry on any device seen so far, so they are walked
|
||||
// only far enough to confirm that. A device that does carry profiles is still reported -- everything
|
||||
// this module reads comes before them -- but nothing after it in the list can be reached, because
|
||||
// walking a profile means decoding one, and the caller stops there rather than reading the next
|
||||
// entry out of the middle of one.
|
||||
static bool soundAndroidReadDevice(const uint8_t* data, size_t size, size_t* offset, int32_t* type, int32_t* role, bool* ended) {
|
||||
*ended = false;
|
||||
if (*offset + 3 * sizeof(int32_t) > size) {
|
||||
return false;
|
||||
}
|
||||
*role = ffBinderReadI32(data, size, *offset);
|
||||
*offset += sizeof(int32_t);
|
||||
*type = ffBinderReadI32(data, size, *offset);
|
||||
*offset += sizeof(int32_t);
|
||||
if (!soundAndroidSkipString16(data, size, offset) || !soundAndroidSkipString16(data, size, offset)) {
|
||||
return false;
|
||||
}
|
||||
*offset += sizeof(int32_t); // nativeType, which follows the two strings
|
||||
for (int list = 0; list < 2; ++list) {
|
||||
const int32_t count = ffBinderReadI32(data, size, *offset);
|
||||
*offset += sizeof(int32_t);
|
||||
if (count > 0) {
|
||||
// A null list is a -1 and occupies nothing beyond the count that was just read, so only
|
||||
// a list with entries in it ends the walk.
|
||||
*ended = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Turns the three volume readings into the percentage the module prints. The service answers with
|
||||
// the index it keeps for the stream, which is not a percentage and is not even on a fixed scale:
|
||||
// `getStreamVolume` divides by ten before returning, so the numbers here are the ones an app sees,
|
||||
// and the maximum differs per stream -- 150 for music on the test device, 15 for the system stream,
|
||||
// 10 for a call. Music's minimum is 0, but that is a property of the stream rather than of the API:
|
||||
// a call's minimum is 1. So the percentage comes out of the range the service reports, and a stream
|
||||
// whose range is empty or inverted is reported as unknown rather than as 100%.
|
||||
static uint8_t soundAndroidPercent(int32_t volume, int32_t minimum, int32_t maximum, int32_t mute) {
|
||||
if (mute != 0) {
|
||||
// A muted stream is reported as 0 rather than as its index: the service keeps the index and
|
||||
// the mute flag separately, and it is the mute flag that decides what comes out of the
|
||||
// speaker. This is also what the PulseAudio backend does with a muted sink.
|
||||
return 0;
|
||||
}
|
||||
if (maximum <= minimum) {
|
||||
return FF_SOUND_VOLUME_UNKNOWN;
|
||||
}
|
||||
if (volume <= minimum) {
|
||||
return 0;
|
||||
}
|
||||
if (volume >= maximum) {
|
||||
return 100;
|
||||
}
|
||||
return (uint8_t) (((volume - minimum) * 100 + (maximum - minimum) / 2) / (maximum - minimum));
|
||||
}
|
||||
|
||||
static const char* detectNative(FFSoundOptions* options, FFlist* devices) {
|
||||
// Resolved before anything else so that a build without the method says so, rather than opening
|
||||
// binder and failing later with a message about the transport.
|
||||
int32_t transaction = 0;
|
||||
const char* error = ffDexStaticInt(FF_SOUND_ANDROID_JAR, FF_SOUND_ANDROID_STUB, FF_SOUND_ANDROID_GET_DEVICES_FOR_ATTRIBUTES, &transaction);
|
||||
if (error != nullptr) {
|
||||
FF_DEBUG("Reading \"%s\" from \"%s\" failed: %s", FF_SOUND_ANDROID_GET_DEVICES_FOR_ATTRIBUTES, FF_SOUND_ANDROID_JAR, error);
|
||||
return "The audio service does not declare getDevicesForAttributesUnprotected";
|
||||
}
|
||||
|
||||
[[gnu::cleanup(ffBinderClose)]] FFBinder binder = { .fd = -1 };
|
||||
error = ffBinderOpen(&binder);
|
||||
if (error != nullptr) {
|
||||
return error;
|
||||
}
|
||||
|
||||
// Released on every path out, including the early returns below.
|
||||
[[gnu::cleanup(ffBinderServiceHandleRelease)]] FFBinderServiceHandle service = { .binder = &binder };
|
||||
error = ffBinderLookupService(&binder, FF_SOUND_ANDROID_SERVICE, FF_BINDER_SM_GET_SERVICE, &service.handle);
|
||||
if (error != nullptr) {
|
||||
return error;
|
||||
}
|
||||
FF_DEBUG("The \"%s\" service is handle %u, getDevicesForAttributesUnprotected is transaction %d",
|
||||
FF_SOUND_ANDROID_SERVICE, service.handle, transaction);
|
||||
|
||||
uint8_t parcelBuffer[FF_SOUND_ANDROID_DEVICE_PARCEL_SIZE];
|
||||
FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer));
|
||||
ffBinderParcelPutInterfaceToken(&parcel, FF_SOUND_ANDROID_DESCRIPTOR);
|
||||
// The non-null marker `Parcel.writeTypedObject` puts in front of a Parcelable. The service reads
|
||||
// the argument with `readTypedObject`, which reads that int first and hands back a null object
|
||||
// when it is not 1 -- so leaving it out does not shift the fields, it loses the argument, and
|
||||
// the service answers with an exception rather than a device list.
|
||||
ffBinderParcelPutI32(&parcel, 1);
|
||||
// `in AudioAttributes`, whose writeToParcel is: usage, contentType, source, flags, the parcel
|
||||
// flags, the tag array and the bundle marker. A null bundle is the only form built here.
|
||||
ffBinderParcelPutI32(&parcel, (int32_t) FF_SOUND_ANDROID_USAGE_MEDIA);
|
||||
ffBinderParcelPutI32(&parcel, 0); // contentType, unused by the policy when a usage is set
|
||||
ffBinderParcelPutI32(&parcel, FF_SOUND_ANDROID_SOURCE_INVALID);
|
||||
ffBinderParcelPutI32(&parcel, 0); // flags
|
||||
ffBinderParcelPutI32(&parcel, 0); // parcel flags: the tags are not flattened
|
||||
ffBinderParcelPutI32(&parcel, 0); // an empty tag array
|
||||
ffBinderParcelPutI32(&parcel, FF_SOUND_ANDROID_ATTR_NULL_BUNDLE);
|
||||
|
||||
uint8_t replyBuffer[FF_SOUND_ANDROID_DEVICE_REPLY_SIZE];
|
||||
FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer));
|
||||
error = ffBinderTransact(&binder, service.handle, (uint32_t) transaction, 0, &parcel, &reply);
|
||||
if (error != nullptr) {
|
||||
return error;
|
||||
}
|
||||
if (ffBinderReplyIsStatus(&reply)) {
|
||||
FF_DEBUG("The reply carries status %d instead of a parcel", (int) ffBinderReadI32(reply.data, reply.size, 0));
|
||||
return "The audio service rejected the request";
|
||||
}
|
||||
|
||||
const int32_t exception = ffBinderReadI32(reply.data, reply.size, 0);
|
||||
if (exception != 0) {
|
||||
FF_DEBUG("getDevicesForAttributesUnprotected raised exception %d", exception);
|
||||
return "The audio service raised an exception";
|
||||
}
|
||||
|
||||
// The service answers with the devices the policy has selected for that usage, so there can be
|
||||
// more than one -- an alarm or a ringtone plays on the speaker and on a connected headset at the
|
||||
// same time -- and the first is the one the stream's volume belongs to.
|
||||
const int32_t count = ffBinderReadI32(reply.data, reply.size, 4);
|
||||
size_t offset = 2 * sizeof(int32_t);
|
||||
const FFSoundAndroidDeviceName* name = nullptr;
|
||||
int32_t deviceType = 0;
|
||||
for (int32_t index = 0; index < count; ++index) {
|
||||
const int32_t present = ffBinderReadI32(reply.data, reply.size, offset);
|
||||
offset += sizeof(int32_t);
|
||||
if (present != 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int32_t type = 0;
|
||||
int32_t role = 0;
|
||||
bool ended = false;
|
||||
if (!soundAndroidReadDevice(reply.data, reply.size, &offset, &type, &role, &ended)) {
|
||||
FF_DEBUG("The device list ended after %d of %d entries", index, count);
|
||||
break;
|
||||
}
|
||||
|
||||
if (role == 2 && name == nullptr) {
|
||||
// ROLE_SINK. A source would be a microphone, which the policy only answers when the
|
||||
// request asks for one, so this is a guard rather than a case that happens.
|
||||
name = soundAndroidDeviceName(type);
|
||||
deviceType = type;
|
||||
FF_DEBUG("Media output device %d of %d is type %d, which is \"%s\"",
|
||||
index + 1, count, type, name != nullptr ? name->identifier : "an unrecognised type");
|
||||
}
|
||||
|
||||
if (ended) {
|
||||
// A profile list with entries in it: the next entry cannot be reached without walking
|
||||
// one, and the first device is the one that matters.
|
||||
FF_DEBUG("Device %d carries audio profiles, so the rest of the list is not read", index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (name == nullptr) {
|
||||
return "The audio service selected no output device";
|
||||
}
|
||||
|
||||
int32_t volume = 0;
|
||||
int32_t minimum = 0;
|
||||
int32_t maximum = 0;
|
||||
int32_t mute = 0;
|
||||
error = soundAndroidCallInt(&binder, service.handle, FF_SOUND_ANDROID_GET_STREAM_VOLUME, true, (int32_t) FF_SOUND_ANDROID_STREAM_MUSIC, &volume);
|
||||
if (error == nullptr) {
|
||||
error = soundAndroidCallInt(&binder, service.handle, FF_SOUND_ANDROID_GET_STREAM_MIN_VOLUME, true, (int32_t) FF_SOUND_ANDROID_STREAM_MUSIC, &minimum);
|
||||
}
|
||||
if (error == nullptr) {
|
||||
error = soundAndroidCallInt(&binder, service.handle, FF_SOUND_ANDROID_GET_STREAM_MAX_VOLUME, true, (int32_t) FF_SOUND_ANDROID_STREAM_MUSIC, &maximum);
|
||||
}
|
||||
if (error == nullptr) {
|
||||
error = soundAndroidCallInt(&binder, service.handle, FF_SOUND_ANDROID_IS_STREAM_MUTE, true, (int32_t) FF_SOUND_ANDROID_STREAM_MUSIC, &mute);
|
||||
}
|
||||
if (error != nullptr) {
|
||||
return error;
|
||||
}
|
||||
|
||||
uint8_t percent = soundAndroidPercent(volume, minimum, maximum, mute);
|
||||
FF_DEBUG("Stream %d is %d of %d..%d, muted %d, which is %s",
|
||||
(int) FF_SOUND_ANDROID_STREAM_MUSIC, volume, minimum, maximum, mute,
|
||||
percent != FF_SOUND_VOLUME_UNKNOWN ? "a percentage" : "unknown");
|
||||
|
||||
// `isMusicActive` is what tells "this device is the output" apart from "this device is playing".
|
||||
// It answers about the media stream as a whole and not about one device, so it only ever adds
|
||||
// the ACTIVE bit to the device the policy already selected. A build that does not declare the
|
||||
// method, or a service that refuses it, leaves the bit clear rather than failing the module.
|
||||
int32_t active = 0;
|
||||
const char* activeError = soundAndroidCallInt(&binder, service.handle, FF_SOUND_ANDROID_IS_MUSIC_ACTIVE, true, 0, &active);
|
||||
if (activeError != nullptr) {
|
||||
FF_DEBUG("Whether anything is playing is not known: %s", activeError);
|
||||
active = 0;
|
||||
}
|
||||
|
||||
const FFSoundType type = (FFSoundType) (FF_SOUND_TYPE_MAIN | (active != 0 ? FF_SOUND_TYPE_ACTIVE : FF_SOUND_TYPE_NONE));
|
||||
if ((options->soundType & FF_SOUND_TYPE_MAIN) && !(type & FF_SOUND_TYPE_MAIN)) {
|
||||
return nullptr;
|
||||
}
|
||||
if ((options->soundType & FF_SOUND_TYPE_ACTIVE) && !(type & FF_SOUND_TYPE_ACTIVE)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// A Bluetooth sink is reported by the name of the device behind it when the service can be
|
||||
// asked, and by its type otherwise. Falling back is not an error path: a build that does not
|
||||
// declare the method, a service that refuses it, and a service that has no name to give all end
|
||||
// in the type name, which is what this module printed before it asked at all.
|
||||
FF_STRBUF_AUTO_DESTROY bluetoothName = ffStrbufCreate();
|
||||
if (soundAndroidIsBluetooth(deviceType)) {
|
||||
const char* nameError = soundAndroidCallBtName(&binder, service.handle, &bluetoothName);
|
||||
if (nameError != nullptr || bluetoothName.length == 0) {
|
||||
FF_DEBUG("The Bluetooth device is not named: %s",
|
||||
nameError != nullptr ? nameError : "the service answered an empty name");
|
||||
}
|
||||
}
|
||||
|
||||
FFSoundDevice* device = FF_LIST_ADD(FFSoundDevice, *devices);
|
||||
// The identifier stays the type: it is the machine readable half of the pair, and the Bluetooth
|
||||
// address the reply carries is not usable as one -- `AudioService` blanks it to
|
||||
// `XX:XX:XX:XX:D8:61` for an unprivileged caller.
|
||||
ffStrbufInitS(&device->identifier, name->identifier);
|
||||
if (bluetoothName.length > 0) {
|
||||
ffStrbufInitMove(&device->name, &bluetoothName);
|
||||
} else {
|
||||
ffStrbufInitS(&device->name, name->name);
|
||||
}
|
||||
ffStrbufInitStatic(&device->platformApi, "AudioService");
|
||||
device->volume = percent;
|
||||
device->type = type;
|
||||
FF_DEBUG("Reporting \"%s\" (%s) at %u%%%s", device->name.chars, device->identifier.chars, device->volume,
|
||||
active != 0 ? ", active" : "");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char* ffDetectSound(FFSoundOptions* options, FFlist* devices) {
|
||||
const FFDisplayServerResult* wm = ffConnectDisplayServer();
|
||||
if (!ffStrbufIgnCaseEqualS(&wm->wmProtocolName, FF_WM_PROTOCOL_SURFACEFLINGER)) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user