yurii.
к работам
КЕЙС

AOSP Platform Customizations

В РАБОТЕ
Начато: Sep 2023Цель: -Роль: Platform Engineer

Customizing Android 13 for a Dedicated Security Panel

At Nice N.A. I worked on an Android panel built on a Rockchip RK3568 with 2 GB of RAM and a 1024×800 display - a controlled device, not a general-purpose phone: one platform-signed application, installed under /system/priv-app, was the entire user interface and talked to an MQTT broker, cameras, and audio through middleware. What follows is what I actually did at the boundary between the application and the platform, without rounding it up to "built the whole BSP."

Camera and Microphone Indicators

Android 13 shows a system privacy indicator on any camera or microphone access. On a panel where the camera runs in a permanent, controlled mode rather than being accessed occasionally, that system indicator didn't match the product's UX - but hiding camera usage outright wasn't an option, only replacing the indicator with the product's own. I found the config.xml inside SystemUI in the AOSP build tree that controls this behavior, configured the dedicated Android 13 build so the stock indicators no longer appeared, and wired the result to the panel's own UI indicator instead.

Notification Listener: from a Regular App Grant to an AOSP Default

The panel needed to read and act on incoming system notifications. The first attempt declared NotificationListenerService the way any regular third-party app does - a manifest entry backed by BIND_NOTIFICATION_LISTENER_SERVICE. That path relies on the user opening Settings → Notification Access and enabling it by hand, which doesn't exist as an option on a kiosk device with no user ever touching system settings - so onListenerConnected() never fired, not because of a platform bug, but because nothing had ever granted the service access in the first place.

The actual fix lived in the AOSP build tree: config_defaultListenerAccessPackages, a colon-separated list in config.xml (overridable per product/device overlay) that the system reads once at boot/upgrade and uses to grant notification listener access automatically — writing straight into Settings.Secure.ENABLED_NOTIFICATION_LISTENERS — without any user interaction. Adding the panel's package there made the Listener connect and receive notifications correctly, and it's what the product still runs on.

Device Owner and DND for System-Level Notification Suppression

A separate, unrelated problem needed a separate mechanism: some system UI notifications - for example, the Wi-Fi stack reporting a wrong password or a dropped connection — had to be suppressed outright, since the product ran its own managed connection flow and didn't want the stock Android message competing with it. For that, I used the panel's Device Owner status to control the interruption filter, including INTERRUPTION_FILTER_NONE, which is a policy-level tool for blocking system notifications rather than something tied to the Notification Listener path above.

Priv-App Deployment and Gradle Tooling

The main application wasn't installed as an ordinary APK - it shipped as part of the system image: platform signing, /system/priv-app placement, privileged permissions, privapp-permissions configuration. I removed the repeated manual steps that came with that by writing a Gradle plugin that automated deploying the system app and handling its artifacts, turning platform deployment into a normal part of the developer workflow instead of a separate manual procedure.

An init.rc Service for Mosquitto's Logs

The Mosquitto MQTT broker wrote logs that needed rotating independently of the UI application's own lifecycle. I added an init.rc service that starts at boot and rotates Mosquitto's logs - small in scope, but a real change to the AOSP init layer rather than something bolted onto the app.

Hidden Wi-Fi API

The product needed to control SoftAP functionality that's only exposed through a hidden API (SoftApConfiguration and related interfaces) - the public SDK didn't cover it. I compiled the app against a vendor-provided stub JAR (compileOnly), while the real implementation lived separately, on the device itself. It worked, but it was a forced approach, and I named its risks explicitly at the time: tight coupling to one specific vendor build, no isolation around the privileged calls, a real possibility of the stub and the runtime API drifting apart, and no easy way to test any of it off the target hardware.

Platform Bridge

Calling hidden APIs and holding privileged permissions directly from a large UI application was blurring the line between the app and the platform. I designed an alternative: a small system service, built alongside the BSP, with legitimate access to the hidden and privileged APIs, exposing a narrow AIDL contract that the main application would call instead - with caller verification through Binder identity, an allowlist of clients, input validation, and rate limiting. The design was worked out and partially prototyped, but it never reached production: the vendor went with the cheaper stub-JAR path instead, because there wasn't engineering time to build the service properly.

Diagnostics Below the Application Layer

Twice, understanding a problem meant going past the ordinary Android SDK entirely.

First: a process running from /system/priv-app couldn't load native libraries from /vendor/lib. I traced the cause to a linker-namespace and Treble partition-boundary restriction — being a privileged/system app doesn't by itself grant access to every vendor library.

Second: playing several RTSP streams at once through multiple ExoPlayer instances made the device degrade and reboot, which looked to a user like a system crash. I looked past the app's own heap, compared memory use between the application and the system's media processes, and found the real cost sitting in android.hardware.media.c2@1.1-service and the C2.RK.avc.decoder hardware decoder. A dma-buf_dump showed roughly 400 MB of DMA-BUF allocations for six 1080p streams; checking /proc/<pid>/fd and /proc/<pid>/maps for the Codec2 process and comparing hardware and software decoder behavior showed the software decoder freeing memory once a player instance was destroyed, while Rockchip's hardware decoder didn't - not after an ordinary release().

Neither case turned into a platform patch - both stayed diagnostics: I found and localized the cause without touching linker configuration, SELinux policy, or the Codec2/MPP implementation itself. That's the honest boundary of this work: real changes at the application-platform edge — SystemUI, init, priv-app deployment, Device Owner, hidden APIs — plus diagnostics reaching into Binder, HAL, vendor, and native memory, but not writing a HAL, kernel, or framework component from scratch.

Стек

Android Open SourceKotlinJava