master ohmu/fastfetch-openbsd / src / detection / wallpaper / wallpaper_android.c
ohmu  ·  2026-10-15
  1#include "wallpaper.h"
  2#include "common/android/binder.h"
  3#include "common/android/dex.h"
  4#include "common/android/package.h"
  5#include "common/debug.h"
  6#include "common/io.h"
  7#include "detection/displayserver/displayserver.h"
  8
  9#include <errno.h>
 10#include <string.h>
 11#include <unistd.h>
 12
 13// Android keeps the wallpaper as a file per user, in a directory no app can enter:
 14//
 15//     /data/system/users/<userId>/wallpaper        the image the launcher draws
 16//     /data/system/users/<userId>/wallpaper_orig   the picture as it was uploaded, before cropping
 17//
 18// /data/system/users/<userId> is mode 0700 and owned by system:system, so `ls`, `stat` and `open` all
 19// answer EACCES to an app UID: the path is not readable even though it is not secret. What is
 20// reachable is a descriptor to the file, because android.app.IWallpaperManager opens it inside
 21// system_server and passes one back, and a descriptor is an ordinary file from then on. readlink(2)
 22// on it names the file, which is the string this module is asked for -- the image itself is never
 23// read or decoded.
 24//
 25// The gate is the calling package. `getWallpaper(String callingPackage, int which, int userId)`
 26// answers only when that name is the caller's own: the two-argument overload, which is the null name,
 27// comes back as a SecurityException raised by StorageManager.checkPermissionReadImages(), and an
 28// empty string, an unknown package and a real package belonging to a different uid all leave with no
 29// descriptor. That READ_MEDIA_IMAGES denial is not something a Termux-style app can lift -- the
 30// permission is not in its manifest and it targets SDK 28 -- so naming the caller is the only route.
 31//
 32// Measured on an Android 16 device as an app UID: the descriptor for `which` 1 is
 33// /data/system/users/0/wallpaper, a 2833949-byte JPEG whose SOF0 reads 2159x3168, while `which` 2
 34// answers with no descriptor at all because that lock screen is a live wallpaper.
 35//
 36// Only one of the two display servers is ever asked. The binder route is Android's, and it answers
 37// while SurfaceFlinger is the display server; a Termux:X11 or Wayland session puts a real compositor
 38// on top, and there the wallpaper belongs to that desktop -- so that case goes to wallpaper_linux.c,
 39// which is compiled alongside this file for exactly this reason.
 40
 41#define FF_WALLPAPER_ANDROID_SERVICE "wallpaper"
 42#define FF_WALLPAPER_ANDROID_DESCRIPTOR "android.app.IWallpaperManager"
 43
 44// WallpaperManager.FLAG_SYSTEM and FLAG_LOCK.
 45typedef enum FFWallpaperAndroidWhich : int32_t {
 46    FF_WALLPAPER_ANDROID_WHICH_SYSTEM = 1,
 47    FF_WALLPAPER_ANDROID_WHICH_LOCK = 2,
 48} FFWallpaperAndroidWhich;
 49
 50// The jar and the stub class the transaction code is read out of. framework.jar answered on every
 51// device measured (Android 11, 13 and 16), so that is the one path and there is no fallback; the
 52// stub sits in classes.dex there, which is the entry the lookup tries first.
 53#define FF_WALLPAPER_ANDROID_JAR "/system/framework/framework.jar"
 54#define FF_WALLPAPER_ANDROID_STUB "Landroid/app/IWallpaperManager$Stub;"
 55
 56// The method this module calls, by the name of the constant the dex carries for it. `getWallpaper`
 57// is the fourth method of IWallpaperManager$Stub, but the number is resolved from the device's own
 58// jar rather than written down here: the numbering is a property of the `.aidl` the ROM was built
 59// from, and a vendor that inserts a method ahead of this one shifts it, after which a transcribed
 60// code reaches a different method or none. Spelling the name out is the whole point -- the lookup
 61// turns it into whatever this build uses, and a build that does not declare it says so instead of
 62// reaching another method. See common/android/dex.h.
 63#define FF_WALLPAPER_ANDROID_GET_WALLPAPER "TRANSACTION_getWallpaper"
 64
 65// `ffAndroidGetOwnPackage` writes at most this many bytes, terminator included.
 66#define FF_WALLPAPER_ANDROID_PACKAGE_SIZE 128
 67
 68// The request is the interface token, the package name, `which` and `userId`. A string16 costs
 69// 4 + 2 * (length + 1) bytes padded to four, and the token puts three int32 in front of the
 70// descriptor. Deriving the size from the two names rather than rounding it up is what keeps the
 71// longest package name `ffAndroidGetOwnPackage` can produce from marking the parcel truncated --
 72// that fails the whole detection, rather than degrading to a call without a name.
 73#define FF_WALLPAPER_ANDROID_PARCEL_SIZE \
 74    (12 + ((4 + 2 * sizeof(FF_WALLPAPER_ANDROID_DESCRIPTOR) + 3) & ~3u) \
 75        + ((4 + 2 * FF_WALLPAPER_ANDROID_PACKAGE_SIZE + 3) & ~3u) + 8)
 76
 77// The reply carries the descriptor object plus, for this method, a Bundle of two ints: 108 bytes on
 78// the device it was measured on, 80 when the wallpaper is a live one and the Bundle comes alone. Only
 79// the descriptor table is read out of it, so this is sized for headroom rather than for a field --
 80// being wrong here would fail the detection, not misread it.
 81#define FF_WALLPAPER_ANDROID_REPLY_SIZE 512
 82
 83static const char* getWallpaperFile(FFBinder* binder, uint32_t handle, const char* package, FFWallpaperAndroidWhich which, int32_t transaction, FFstrbuf* result) {
 84    uint8_t parcelBuffer[FF_WALLPAPER_ANDROID_PARCEL_SIZE];
 85    FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer));
 86    ffBinderParcelPutInterfaceToken(&parcel, FF_WALLPAPER_ANDROID_DESCRIPTOR);
 87    ffBinderParcelPutString16(&parcel, package);
 88    ffBinderParcelPutI32(&parcel, (int32_t) which);
 89    ffBinderParcelPutI32(&parcel, 0); // userId, which is the one user a phone has
 90
 91    uint8_t replyBuffer[FF_WALLPAPER_ANDROID_REPLY_SIZE];
 92    FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer));
 93
 94    // TF_ACCEPT_FDS is neither optional nor a hint: a reply carrying a descriptor is only delivered
 95    // when the request asked for one, and a request that did not is answered with BR_FAILED_REPLY --
 96    // which reads like a service that is not running rather than like a missing flag.
 97    const char* error = ffBinderTransact(binder, handle, (uint32_t) transaction, TF_ACCEPT_FDS, &parcel, &reply);
 98
 99    // Whatever else happened, a descriptor that did arrive is ours: the kernel installed it in our fd
100    // table, and on the paths below that return early nothing else would close it.
101    FF_AUTO_CLOSE_FD FFNativeFD fd = reply.fdCount > 0 ? reply.fds[0] : -1;
102
103    if (error != nullptr) {
104        FF_DEBUG("getWallpaper(which %d) could not be transacted: %s", (int) which, error);
105        return error;
106    }
107    if (ffBinderReplyIsStatus(&reply)) {
108        FF_DEBUG("getWallpaper(which %d) came back as a status reply of %d instead of a parcel",
109            (int) which, (int) ffBinderReadI32(reply.data, reply.size, 0));
110        return "Wallpaper service rejected the request";
111    }
112
113    const int32_t exception = ffBinderReadI32(reply.data, reply.size, 0);
114    if (exception != 0) {
115        // -1 is a SecurityException, and for this method it is what the package name check raises.
116        FF_DEBUG("getWallpaper(which %d) raised exception %d over %zu bytes of reply", (int) which, exception, reply.size);
117        return "Wallpaper service refused the request";
118    }
119    if (!ffIsValidNativeFD(fd)) {
120        FF_DEBUG("getWallpaper(which %d) carried no descriptor, which is how a live wallpaper answers: "
121                 "%zu bytes of reply",
122            (int) which, reply.size);
123        return "The wallpaper is not a file";
124    }
125
126    char path[64];
127    const int pathLength = snprintf(path, sizeof(path), "/proc/self/fd/%d", (int) fd);
128    if (pathLength <= 0 || (size_t) pathLength >= sizeof(path)) {
129        FF_DEBUG("Failed to build the /proc path of the wallpaper descriptor");
130        return "Failed to build the /proc path of the wallpaper descriptor";
131    }
132
133    char target[512];
134    const ssize_t length = readlink(path, target, sizeof(target) - 1);
135    if (length <= 0) {
136        FF_DEBUG("readlink(%s) failed: %s", path, strerror(errno));
137        return "Failed to resolve the wallpaper path";
138    }
139    target[length] = '\0';
140
141    ffStrbufSetNS(result, (uint32_t) length, target);
142    FF_DEBUG("The wallpaper for which %d is \"%s\"", (int) which, result->chars);
143    return nullptr;
144}
145
146const char* ffDetectWallpaper(FFstrbuf* result) {
147    // The binder route below reads Android's own wallpaper, which is the one behind the launcher
148    // only while SurfaceFlinger is the display server. A Termux:X11 or Wayland session runs a real
149    // compositor on top of the device, and the wallpaper visible there belongs to that desktop, not
150    // to Android -- so that case is handed to the Linux implementation, which knows GTK and Qt
151    // settings. Wallpaper is only reported for one of the two, never merged.
152    const FFDisplayServerResult* wm = ffConnectDisplayServer();
153    if (!ffStrbufIgnCaseEqualS(&wm->wmProtocolName, FF_WM_PROTOCOL_SURFACEFLINGER)) {
154        FF_DEBUG("The display server is \"%s\", so the Linux implementation answers", wm->wmProtocolName.chars);
155        const char* ffDetectWallpaperLinux(FFstrbuf* result);
156        return ffDetectWallpaperLinux(result);
157    }
158
159    char package[FF_WALLPAPER_ANDROID_PACKAGE_SIZE];
160    if (!ffAndroidGetOwnPackage(package, sizeof(package))) {
161        FF_DEBUG("Cannot determine the package name of this process");
162        return "Cannot determine the package name of this process";
163    }
164
165    // Resolved before the binder is opened, so that a jar which does not declare the method says so
166    // rather than the module failing later with a message about the transport. The reader reports a
167    // code it cannot find by leaving the sentinel in the result and says nothing more -- what a
168    // missing code means for this module is decided here, not there.
169    int32_t transaction = FF_DEX_STATIC_INT_UNRESOLVED;
170    const FFDexStaticIntRequest request = { FF_WALLPAPER_ANDROID_STUB, FF_WALLPAPER_ANDROID_GET_WALLPAPER, &transaction };
171    const char* error = ffDexStaticInts(FF_WALLPAPER_ANDROID_JAR, &request, 1);
172    if (error != nullptr) {
173        FF_DEBUG("Reading \"%s\" from \"%s\" failed: %s", FF_WALLPAPER_ANDROID_GET_WALLPAPER, FF_WALLPAPER_ANDROID_JAR, error);
174        return error;
175    }
176    if (transaction == FF_DEX_STATIC_INT_UNRESOLVED) {
177        FF_DEBUG("The wallpaper service does not declare getWallpaper");
178        return "The wallpaper service does not declare getWallpaper";
179    }
180
181    [[gnu::cleanup(ffBinderClose)]] FFBinder binder = { .fd = -1 };
182    error = ffBinderOpen(&binder);
183    if (error != nullptr) {
184        return error;
185    }
186
187    // Released on every path out of this function, including the ones below that return early.
188    [[gnu::cleanup(ffBinderServiceHandleRelease)]] FFBinderServiceHandle service = { .binder = &binder };
189    error = ffBinderLookupService(&binder, FF_WALLPAPER_ANDROID_SERVICE, FF_BINDER_SM_GET_SERVICE, &service.handle);
190    if (error != nullptr) {
191        FF_DEBUG("Looking up the \"%s\" service failed: %s", FF_WALLPAPER_ANDROID_SERVICE, error);
192        return error;
193    }
194    FF_DEBUG("The \"%s\" service is handle %u, getWallpaper is transaction %d", FF_WALLPAPER_ANDROID_SERVICE, service.handle, transaction);
195
196    // The system wallpaper is what the module means by "the current wallpaper"; the lock screen is a
197    // second, separate picture, and is only reported when the system one is not a file -- which is
198    // the live-wallpaper case, where a component draws it and there is no image behind it.
199    error = getWallpaperFile(&binder, service.handle, package, FF_WALLPAPER_ANDROID_WHICH_SYSTEM, transaction, result);
200    if (error == nullptr) {
201        return nullptr;
202    }
203
204    FF_DEBUG("The system wallpaper is not a file (%s), so the lock screen is asked for instead", error);
205    return getWallpaperFile(&binder, service.handle, package, FF_WALLPAPER_ANDROID_WHICH_LOCK, transaction, result);
206}