CarBox Launcher logo

CarBox Theme SDK

Design the entire CarBox home in Android Studio.

Build a real Android shell with XML and Java: custom bars, live information, app docks, animated drawers and the native DualApp panels exactly where your design needs them.

Visual XML editor Java behavior Native DualApp host Safe fallback
CurrentThemeActivity.java
public final class CurrentThemeActivity
    extends CarBoxThemeActivity {

  @Override
  protected void onCreate(Bundle state) {
    super.onCreate(state);
    setContentView(R.layout.theme_main);
    bindPanelArea(R.id.carboxPanelArea);

    findViewById(R.id.mapsButton)
      .setOnClickListener(v -> carBox().launchApp(
        "com.google.android.apps.maps",
        CarBoxTarget.LEFT_PANEL));
  }
}

Working source project

Choose a complete Android Studio theme.

Both demos include the Theme Bridge API, XML preview, panel hosting and are ready to be signed and published on Google Play.

TEMPLATE

Empty starting point

Empty theme template (plugin v2)

Minimal Android Studio project for the new PLUGIN model: no Activity, no VirtualDisplay. Compile, install and start replacing ThemePlugin.java.

JavaCarBoxThemeViewPlugin manifest
Download template
EXAMPLE

Ready-to-install demo

CarBox Dark Drive

Complete plugin theme: dock with clock, favorites, all system buttons, background picker, tutorial, per-icon visibility and expand-panel shortcuts. The reference implementation for the plugin API.

Java + XMLPlugin v2Full dock
Download source

New in CarBox v2

Plugin themes — the recommended way.

The plugin model is the new architecture for CarBox themes. Instead of running the theme as an Activity in a VirtualDisplay, the launcher loads a plain View class from the theme APK via PathClassLoader and adds it to its own layout.

01

Native touch

No IPC input injection. Taps and gestures run at the same speed as CarBox Classic.

02

Real dialogs

AlertDialog, Dialog and menus work out of the box because they attach to the launcher's Activity window.

03

Same APK

Still a normal Android Studio project. What changes is the manifest entry and the base class you extend.

AndroidManifest.xml — plugin declaration
<application>
  <!-- Point CarBox to your View class. No <activity> is needed. -->
  <meta-data
    android:name="carbox.theme.pluginClass"
    android:value="com.example.mytheme.ThemePlugin" />
</application>
ThemePlugin.java — the class CarBox loads
public class ThemePlugin extends CarBoxThemeView {

    private final FrameLayout panelArea;

    public ThemePlugin(Context ctx, CarBoxThemeBridge bridge) {
        super(ctx, bridge);
        // Build your UI (LayoutInflater.from(ctx).inflate(R.layout.theme_main, this, false); …)
        panelArea = new FrameLayout(ctx);
        addView(panelArea);
    }

    @Override
    public ViewGroup getPanelHost() { return panelArea; }
}
getPanelHost() is the key contract: CarBox re-parents the real DualApp panels into the ViewGroup you return, so your dock/clock/controls sit around them naturally.
Dialog context: use getDialogContext() (Activity of the launcher) when constructing Dialog/AlertDialog. Using the theme package context throws BadTokenException.

The empty template above already contains everything wired up. Start from it.

Architecture

The theme draws the shell. CarBox keeps control of the apps.

A theme is an independent Android package. Its Activity is rendered by CarBoxLauncher on a dedicated VirtualDisplay. The theme draws the background and controls, while the real DualApp container is placed above the transparent panel area reported by the theme.

01

Design visually

Build the home with normal Android XML layouts and use the Android Studio Design editor.

02

Program anything

Use Java for clocks, weather, media state, animations, drawers and custom Views.

03

Isolated and recoverable

Theme code has its own UID. If loading fails, CarBox clears the broken selection and opens CarBox Classic.

CarBox Classic is never replaced. The built-in interface remains the permanent fallback and is also used by Creative Mode.

Project setup

Give every theme its own Android package.

Duplicate the demo project, then change namespace, applicationId, Java package, visible theme name and the permanent ID in carbox_theme.json. Do not reuse another theme package ID.

1. Duplicate

Copy the demo and open the copied folder in Android Studio.

2. Rename

Assign a unique namespace, applicationId and theme ID.

3. Design

Edit theme_main.xml in landscape Design view.

4. Connect

Bind the panel area and use CarBoxThemeBridge for launcher actions.

app/build.gradle
android {
  namespace 'com.example.carbox.theme'
  compileSdk 34

  defaultConfig {
    applicationId 'com.example.carbox.theme'
    minSdk 21
    targetSdk 34
    versionCode 1
    versionName '1.0.0'
  }
}

XML editor

Leave one transparent area for the real panels.

Everything outside CarBoxPanelArea belongs to your theme. The panel area must remain transparent at runtime. Preview-only children can be shown with tools:visibility so Android Studio still displays the intended composition.

res/layout/theme_main.xml
<FrameLayout
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:background="@drawable/theme_background">

  <LinearLayout android:id="@+id/topBar" ... />

  <FrameLayout
    android:id="@+id/carboxPanelArea"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="1"
    android:background="@android:color/transparent">

    <LinearLayout
      android:visibility="gone"
      tools:visibility="visible">
      <!-- XML-only panel preview -->
    </LinearLayout>
  </FrameLayout>

  <LinearLayout android:id="@+id/appDock" ... />
</FrameLayout>
Call bindPanelArea(R.id.carboxPanelArea) after setContentView(). CarBox receives its exact screen bounds and places DualApp there.

Discovery contract

Export only the CarBox theme entry point.

The main Activity declares the THEME action and metadata. Release builds should not expose a launcher icon. A development launcher alias is acceptable only in debug builds.

AndroidManifest.xml
<activity
  android:name=".MyThemeActivity"
  android:allowEmbedded="true"
  android:configChanges="orientation|screenSize|smallestScreenSize"
  android:exported="true"
  android:screenOrientation="landscape">
  <intent-filter>
    <action android:name="com.ferra.carboxlauncher.action.THEME" />
    <category android:name="android.intent.category.DEFAULT" />
  </intent-filter>
  <meta-data android:name="carbox.theme.api" android:value="1" />
  <meta-data android:name="carbox.theme.id" android:value="my_theme" />
  <meta-data android:name="carbox.theme.settings_entry" android:value="true" />
  <meta-data android:name="carbox.theme.manifest" android:resource="@raw/carbox_theme" />
</activity>
Every theme must provide a visible Settings button wired to carBox().openSettings() and declare settingsEntry=true. Themes without it cannot be installed or selected.

Secure communication

Always send commands through CarBoxThemeBridge.

The hosted Activity receives a private session token. The bridge adds it to every explicit broadcast. Commands without the current token are ignored, so another installed app cannot control the active theme session.

MyThemeActivity.java
public final class MyThemeActivity
    extends CarBoxThemeActivity {
  @Override
  protected void onCreate(Bundle state) {
    super.onCreate(state);
    setContentView(R.layout.theme_main);
    bindPanelArea(R.id.carboxPanelArea);
  }
}

Theme API

Control CarBox without owning its privileges.

The bridge delegates actions to the launcher on the correct display. Never call startActivity() for panel apps directly from the hosted theme.

CarBoxThemeBridge — launching apps
carBox().launchApp("com.spotify.music", CarBoxTarget.LEFT_PANEL);
carBox().launchApp("com.spotify.music", CarBoxTarget.RIGHT_PANEL);
carBox().launchApp("com.spotify.music", CarBoxTarget.SINGLE_PANEL);   // single-panel layout (launcher 1.96+)
carBox().launchApp("com.spotify.music", CarBoxTarget.FULLSCREEN);
carBox().launchApp("com.spotify.music", CarBoxTarget.PORTAL);   // launcher 1.80+
CarBox Portal — floating window (launcher 1.80+)
// PORTAL opens the app in a small floating window the user can freely
// drag, resize (bottom-right handle) and drop against a screen edge:
//   • left edge   → app becomes the left panel  (task moved, session kept)
//   • right edge  → app becomes the right panel (task moved, session kept)
//   • top edge    → app goes fullscreen         (task moved, session kept)
//   • bottom edge → app + Portal are closed
// The user must enable "CarBox Portal" in Launcher settings for the target
// to activate; when the toggle is OFF the launcher silently falls back to
// fullscreen so PORTAL never fails visibly for the theme.
// Two settings-side sliders let the user tune the initial window size and
// the DPI-scale of the app inside the window (default 50/50).
carBox().launchApp("com.google.android.youtube", CarBoxTarget.PORTAL);
Launcher screens
carBox().openDrawer();          // app drawer
carBox().openPhone();           // phone / dialer
carBox().openContacts();        // contacts
carBox().openNotifications();   // notifications panel
carBox().openSettings();        // launcher settings
carBox().openAssistant();       // voice assistant
carBox().goHome();              // back to launcher home
Place a call via Companion (launcher 1.87+)
// Starts a phone call on the paired Android smartphone through the CarBox
// Companion app. The AI-box has NO SIM: the call is dialed BY THE PHONE.
// numberOrName can be:
//   - a raw phone number (e.g. "+391234567890" or "1234567890")
//   - a contact name — the Companion looks it up in the phone's address book
//     and, if the contact has several numbers, prompts on the box to pick one.
// Requires the user to have paired a Companion (Bluetooth or LAN); the
// launcher shows a toast "Companion non connesso" if nothing is paired.
// Wraps LauncherActivity.apiCall(String) — in-process reflection when the
// theme runs as plugin, broadcast (THEME_COMMAND "call" + EXTRA_TEXT) as
// fallback. Never opens the local Android dialer.
carBox().callNumber("+391234567890");   // dial a number
carBox().callNumber("Mario Rossi");     // dial a contact by name

// Themes that pre-date the Bridge helper can call the API by hand via the
// same broadcast the SDK uses under the hood:
context.sendBroadcast(new Intent("com.ferra.carboxlauncher.action.THEME_COMMAND")
        .setPackage("com.ferra.carboxlauncher")
        .putExtra("command", "call")
        .putExtra("com.ferra.carboxlauncher.extra.TEXT", "+391234567890"));
Paired phone address book (launcher 1.88+)
// Fetch the contacts stored on the paired Android smartphone via Companion.
// Returns async (BT/LAN request); the callbacks fire on a worker thread —
// hop to the UI thread yourself before touching Views. Not on the classic
// broadcast bridge: needs in-process reflection (theme running as plugin).
// getCompanionContacts() returns false when the launcher predates the API
// or the theme is not in-process (preview mode) — treat that as "no data",
// falling back to the local ContactsContract if it fits your use case.
//
// JSON shape (matches the wire format used by PhoneActivity):
//   [ { "name": "Mario Rossi",
//       "numbers": [ { "number": "+391234567890", "label": "Mobile" }, ... ] },
//     ... ]
Handler ui = new Handler(Looper.getMainLooper());
boolean asked = carBox().getCompanionContacts(
    json -> ui.post(() -> {
        try {
            JSONArray arr = new JSONArray(json);
            for (int i = 0; i < arr.length(); i++) {
                JSONObject o = arr.getJSONObject(i);
                String name    = o.optString("name", "");
                JSONArray nums = o.optJSONArray("numbers");
                // … render your picker row per contact …
            }
        } catch (Throwable ignored) {}
    }),
    err  -> ui.post(() -> toast(err))   // "Companion non connesso", timeout, etc.
);
if (!asked) {
    // Launcher older than 1.88, or theme running standalone — no Companion.
}
Paired phone battery (launcher 1.97+)
// The launcher polls the paired phone battery via Companion every ~90s
// and caches the last value in SharedPreferences "cbx_phone_battery".
// Themes should read the cache — never hit Companion directly — so the
// UI stays fast and offline-tolerant. If the launcher pre-dates 1.97 the
// getters return -1/false silently: check level>=0 before showing anything.
//
// SharedPreferences keys inside "cbx_phone_battery":
//   level          int   0..100, -1 if never received
//   charging       bool  true when AC/USB/wireless
//   plug           str   "none" | "ac" | "usb" | "wireless"
//   temperature_c  float phone battery temperature (optional)
//   last_ts        long  System.currentTimeMillis of the last reading
//   last_try_ts    long  ts of last request attempt (whether successful)
//   state          str   "ok" | "unreach" | "nocfg" (last poll outcome)

// One-shot read (call in refresh loop or on-demand):
int  pct      = carBox().getPhoneBatteryLevel();     // 0..100, -1 if none
boolean charging = carBox().isPhoneCharging();
String  plug     = carBox().getPhonePlug();          // "none" | "ac" | "usb" | "wireless"
long    ageMs    = System.currentTimeMillis() - carBox().getPhoneBatteryLastTs();

// Live updates: register the broadcast to redraw on every poll (~90s):
BroadcastReceiver rx = new BroadcastReceiver() {
    @Override public void onReceive(Context c, Intent i) { refreshBatteryUi(); }
};
IntentFilter f = new IntentFilter(CarBoxThemeBridge.ACTION_PHONE_BATTERY_UPDATED);
if (Build.VERSION.SDK_INT >= 33)
    ctx.registerReceiver(rx, f, Context.RECEIVER_NOT_EXPORTED);
else
    ctx.registerReceiver(rx, f);
// …in onDetachedFromWindow: ctx.unregisterReceiver(rx);

// Alternative: read the prefs directly and listen with OnSharedPreferenceChange:
SharedPreferences bp = carBox().getPhoneBatteryPrefs();
bp.registerOnSharedPreferenceChangeListener((p, key) -> refreshBatteryUi());

// Force an immediate fetch (skip the timer) on user interaction — e.g. tap
// on a battery widget to get a fresh value instead of the cached one:
carBox().refreshPhoneBattery();
Modes and lifecycle
carBox().openCreativeMode();    // switch to creative mode
carBox().restartLauncher();     // restart the launcher process
carBox().reloadPanels();        // re-render dual-app panels
Memory management (launcher 1.87+)
// Two launcher APIs are exposed for themes that want a "clean memory" or
// "close everything" button. They are NOT wrapped by the current Bridge
// SDK: call them via reflection on the LauncherActivity (in-process, same
// UID as the launcher when the theme runs as plugin). Both return an int
// with the number of packages killed.
//
// apiCleanMemory()   → closes background apps outside the launcher
//                      whitelist. Dual panels, the active theme plugin,
//                      the Portal window, the launcher itself, the user's
//                      Startup Apps and the manual whitelist stay alive.
// apiCloseAllApps()  → aggressive: closes EVERY user background app.
//                      Excludes only the launcher and base system
//                      processes; dual panels and the active theme are
//                      killed too.
Activity host = getActivity(); // hosted Activity of the launcher
int killed = 0;
try {
    java.lang.reflect.Method m = host.getClass().getMethod("apiCleanMemory");
    killed = (Integer) m.invoke(host);
} catch (Throwable ignored) { // launcher older than 1.87 → no-op }
Creative edit mode — probe & toggle (launcher 1.85+)
// In creative mode the CreativeLayout hosts widgets and shows the "+" edit
// button only when edit mode is ON. A theme can bind long-press on its own
// creative icon to toggle the "+" without leaving creative — the pattern used
// by Dark Drive on its dock creativeButton.
boolean inCreative = carBox().isCreativeMode();   // query; false if unsupported
carBox().toggleEditMode();                        // no-op when NOT in creative

// Example: long-press on the theme's own "creative" icon toggles the "+"
// only when we are already in creative — short tap keeps the classic behaviour.
creativeIcon.setOnClickListener(v -> carBox().openCreativeMode());
creativeIcon.setOnLongClickListener(v -> {
    if (carBox().isCreativeMode()) carBox().toggleEditMode();
    return true; // consume the long-press even outside creative
});
Panels bounds and handles
// Legacy VD path only: reports the DualApp rect to CarBox.
// Plugin v2: skip this — the launcher inserts panels into getPanelHost().
carBox().reportPanelArea(new Rect(0, 0, 800, 480));

carBox().hidePanelHandles();
carBox().showPanelHandles();
carBox().setPanelHandlesVisible(false);
carBox().togglePanelHandles();
Expand a single panel (v2)
// Same behaviour as tapping the launcher's bottom handles.
// A second call restores the saved split ratio.
carBox().expandLeftPanel();
carBox().expandRightPanel();
Panel split ratio — save & restore
// New in launcher 1.75+. Read the CURRENT left-panel fraction (0..1) so a
// theme can save "user pinned panels at 70/30" as part of a preset.
// Falls back to 0.5 if the launcher is older or the panels are not ready.
float ratio = carBox().getPanelSplitRatio();

// Restore the exact split when the user re-applies a saved preset.
// Range is clamped to [0.15, 0.85] on the launcher side (safety, no tiny panels).
// The launcher debounces rapid calls (120 ms) so rebuilding a whole preset in a loop is safe.
carBox().setPanelSplitRatio(0.7f);   // 70% left, 30% right
Single-panel layout — one app instead of two (launcher 1.96+)
// Themes that need only ONE app panel (Android Auto style, dashboards, ecc.)
// can ask the launcher to hide the right panel + divider + all handles.
// The remaining LEFT panel expands to fill the whole panelHost.
//
// Two ways to declare it:
//
// 1) STATIC (manifest meta-data) — read at boot, applies before the panels
//    are even measured, so no 50/50 flash. Ideal when the theme is always
//    single-panel.
<meta-data android:name="carbox.theme.layout_mode" android:value="single" />

// 2) RUNTIME (bridge) — call in onHostAttached() or later. The preference
//    is persisted so the next boot honors it before your plugin is loaded.
carBox().setLayoutMode("single");   // pannello unico (LEFT full)
carBox().setLayoutMode("dual");     // default: LEFT + RIGHT + divider

// The launcher target "single" routes to the visible panel automatically
// (== LEFT in single mode, == LEFT in dual mode with right fallback).
carBox().launchApp("com.spotify.music", CarBoxTarget.SINGLE_PANEL);

// In single mode all native handles (barra inferiore + divider pill) sono
// nascoste dal launcher — il tema deve offrire le proprie affordance. Per
// aprire il dialog di configurazione del pannello (delay, orientazione,
// scala, picker app) usa:
carBox().openPanelSettings("single");   // ignora side in single mode
carBox().openPanelSettings("left");     // dual mode: LEFT
carBox().openPanelSettings("right");    // dual mode: RIGHT

// Sicuro: quando il tema è disattivato (utente cambia tema o disinstalla),
// il launcher forza automaticamente il ritorno a "dual" così il classic
// vede entrambi i pannelli. Puoi comunque essere esplicito nel detach.
@Override
protected void onThemeDetached() {
    carBox().setLayoutMode("dual");
}
Dialogs on top of the panels — REQUIRED
// Dual-app panels sit above the launcher window and cover normal dialogs.
// Call setDialogMode(true) BEFORE showing a dialog and false when it dismisses:
// CarBox will move the panels aside without suspending the running apps.
carBox().setDialogMode(true);
dialog.setOnDismissListener(d -> carBox().setDialogMode(false));
dialog.show();
Detect hosted mode
if (carBox().isHosted(this)) {
    // running inside CarBox: bridge commands are active
} else {
    // running standalone (preview mode): commands are no-ops
}
APIParameterAction
launchApppackage, CarBoxTargetOpen an app on the left panel, right panel, single panel (SINGLE_PANEL, launcher 1.96+ — routes to the visible panel in both single and dual mode), fullscreen, or in the floating PORTAL window (launcher 1.80+; falls back to fullscreen when the user has Portal disabled)
openDrawer-Open the CarBox app drawer
openPhone-Open the CarBox phone panel
openContacts-Open the contacts list
callNumberString (number or contact name)Place a phone call through the paired Android smartphone via CarBox Companion. Accepts a raw phone number (e.g. +391234567890) or a contact name (Companion looks it up in the phone's address book; multi-number contacts prompt on the box). Never opens the local Android dialer. Wraps LauncherActivity.apiCall(String). Requires launcher 1.87+ and a paired Companion.
getCompanionContactsConsumer<String> onJson, Consumer<String> onErrorAsync fetch of the paired phone's address book via Companion. onJson receives an array [{name, numbers:[{number,label},...]},...]; onError receives a human message when Companion is not paired or unreachable. Both callbacks fire on a worker thread — post to the UI thread before touching Views. Returns false if the launcher predates the API or the theme is not in-process (fallback to local ContactsContract if needed). Wraps LauncherActivity.apiGetCompanionContacts. Requires launcher 1.88+.
openNotifications-Open the notifications panel
openSettings-Open launcher settings
openAssistant-Start the voice assistant
goHome-Return to the launcher home
openCreativeMode-Switch CarBox to creative mode
isCreativeMode-Sync query. Returns true if the launcher is currently in creative mode. Returns false on launchers older than 1.85 or if the launcher is not reachable
toggleEditMode-Toggle the CreativeLayout edit state (shows/hides the "+" button). No-op if the launcher is not in creative mode. Requires launcher 1.85+
apiCleanMemory (launcher)-Not on the Bridge — call via reflection on the LauncherActivity. Closes background apps outside the launcher whitelist (dual panels, active theme, portal, launcher, startup apps and user whitelist stay alive). Returns int (killed). Requires launcher 1.87+
apiCloseAllApps (launcher)-Not on the Bridge — call via reflection. Aggressive close of every user background app. Excludes only the launcher and base system processes; dual panels and active theme are killed too. Returns int (killed). Requires launcher 1.87+
restartLauncher-Restart the launcher process
reloadPanels-Re-render the dual-app panels
reportPanelAreaRectReport the exact rectangle reserved for DualApp
setPanelHandlesVisiblebooleanShow or hide native panel handles for this theme session
showPanelHandles, hidePanelHandles-Convenience wrappers around setPanelHandlesVisible
togglePanelHandles-Toggle current handle visibility
expandLeftPanel-Same action as tapping the left bottom handle: expand the left panel; call again to restore
expandRightPanel-Same action as tapping the right bottom handle: expand the right panel; call again to restore
getPanelSplitRatio-Read the current left-panel fraction (0..1) — use it to save the exact panel size in a preset
setPanelSplitRatiofloat 0..1Restore an exact left-panel fraction (clamped to 0.15..0.85, debounced 120 ms). Requires launcher 1.75+
setLayoutModeString ("single" or "dual")Switch the launcher between single-panel (LEFT full, RIGHT + divider + handles hidden) and dual-panel. The choice is persisted so the next boot applies it before your plugin loads (no 50/50 flash). Same effect as declaring <meta-data android:name="carbox.theme.layout_mode" android:value="single"/>. Requires launcher 1.96+
openPanelSettingsString side ("left", "right", "single")Open the launcher's panel configuration dialog (launch delay, orientation, scale, app picker). In single mode side is ignored and always targets the visible panel. In dual mode use "left" or "right". Required for themes with hidden handles. Requires launcher 1.96+
setDialogModebooleanMove the panels aside while a theme dialog is open (call true on show, false on dismiss)
isHostedActivityTrue when the theme is being rendered by CarBox (bridge active)
Handle visibility is temporary. CarBox restores the user's previous Classic preference when the theme closes or fails.
Dialogs shown by the theme MUST wrap show/dismiss with setDialogMode(true/false). Without it the DualApp panels stay above the theme and cover the dialog.

Runtime permissions

Play Store safety — ask permissions via PermissionRequestActivity.

A theme APK published on Play must ask its dangerous permissions (LOCATION, BLUETOOTH_CONNECT, CAMERA, RECORD_AUDIO, ...) via a runtime prompt. A theme running as plugin has no Activity of its own — so CarBox launches a small proxy Activity that you provide, with the missing permissions passed as extra. The proxy shows the system dialog and closes.

When the user activates your theme, the Launcher automatically:

  1. Reads getPackageInfo(pkg, GET_PERMISSIONS) of your theme APK.
  2. Filters to permissions with protection level DANGEROUS.
  3. Checks which of those are not granted for the theme package.
  4. If any missing, launches the theme's PermissionRequestActivity with the missing array in the carbox.theme.PERMISSIONS extra.
  5. If the Launcher itself is uid=system (platform-signed CarBox box), skips the prompt entirely — those permissions are granted at firmware level.
AndroidManifest.xml — declare the activity
<activity
  android:name=".PermissionRequestActivity"
  android:exported="true"
  android:theme="@android:style/Theme.Translucent.NoTitleBar"
  android:excludeFromRecents="true"
  android:launchMode="singleInstance"
  android:noHistory="true">
  <intent-filter>
    <action android:name="com.carbox.theme.action.REQUEST_PERMISSIONS" />
    <category android:name="android.intent.category.DEFAULT" />
  </intent-filter>
</activity>
PermissionRequestActivity.java (copy/paste boilerplate)
public class PermissionRequestActivity extends Activity {
    public static final String EXTRA_PERMISSIONS = "carbox.theme.PERMISSIONS";
    private static final int REQ = 9701;

    @Override protected void onCreate(Bundle b) {
        super.onCreate(b);
        String[] req = getIntent() != null
                ? getIntent().getStringArrayExtra(EXTRA_PERMISSIONS) : null;
        if (req == null || req.length == 0) { finish(); return; }
        java.util.List<String> missing = new java.util.ArrayList<>();
        for (String p : req) {
            if (checkSelfPermission(p) != PackageManager.PERMISSION_GRANTED) missing.add(p);
        }
        if (missing.isEmpty()) { finish(); return; }
        requestPermissions(missing.toArray(new String[0]), REQ);
    }

    @Override public void onRequestPermissionsResult(int rc, String[] perms, int[] grants) {
        super.onRequestPermissionsResult(rc, perms, grants);
        finish();   // CarBox restarts the theme after this
    }
}
Requires launcher 1.89+. Older launchers ignore the flow and the theme just fails silently on permission-guarded APIs — always keep graceful fallbacks (e.g. speedometer shows 0 if LocationManager throws SecurityException).
The Activity must be exported="true" so the Launcher can start it across packages. The Theme.Translucent.NoTitleBar theme keeps it invisible: the user only sees the system permission dialog.

Drawers and animation

Build side drawers as translucent overlay Activities.

An overlay is normal Java and XML running on the main display. It can animate panels, show menus and call the same bridge. Register each overlay with a unique metadata ID, then open it with showOverlay().

AndroidManifest.xml
<activity
  android:name=".LeftDrawerActivity"
  android:excludeFromRecents="true"
  android:exported="true"
  android:screenOrientation="landscape"
  android:theme="@style/Theme.CarBoxTheme.Overlay">
  <intent-filter>
    <action android:name="com.ferra.carboxlauncher.action.THEME_OVERLAY" />
    <category android:name="android.intent.category.DEFAULT" />
  </intent-filter>
  <meta-data android:name="carbox.theme.overlay.id" android:value="left_drawer" />
</activity>

Optional configuration

A theme may provide its own settings Activity.

Declare a second Activity with THEME_SETTINGS. CarBox shows its settings button only when that Activity exists. Keep it immersive and store configuration inside the theme package.

AndroidManifest.xml
<intent-filter>
  <action android:name="com.ferra.carboxlauncher.action.THEME_SETTINGS" />
  <category android:name="android.intent.category.DEFAULT" />
</intent-filter>

Safe loading

A broken theme can never lock users out of the launcher.

CarBox waits for both the first rendered frame and panel bounds. Missing packages, VirtualDisplay errors, interrupted surfaces, timeouts and incomplete previous starts all trigger CarBox Classic and clear the failed selection.

  • Draw the first frame quickly; do network work after the UI appears
  • Always call bindPanelArea after setContentView
  • Never block the main thread during startup
  • Keep all primary controls usable without internet

Build & publish

Test the APK, then publish it on Google Play.

Themes are distributed as normal Android apps on the Play Store. Install the debug APK with ADB, select it from Settings > Theme and test rotation, cold start, overlays, panel targets and fallback behavior. When it is stable, build a signed release AAB and upload it to a Play Console app.

1. Debug install for local tests
$env:JAVA_HOME = 'C:\Program Files\Android\Android Studio\jbr'
.\gradlew.bat :app:assembleDebug

adb -s BOX_IP:5555 install -r `
  app\build\outputs\apk\debug\app-debug.apk
2. Signed release bundle for Google Play
# keystore.properties (root, git-ignored) — set your own values
storeFile=..\\..\\release_key.jks
storePassword=***
keyAlias=upload
keyPassword=***

# app/build.gradle — enable release signing
signingConfigs {
  release {
    def props = new Properties()
    file('../keystore.properties').withInputStream { props.load(it) }
    storeFile     file(props['storeFile'])
    storePassword props['storePassword']
    keyAlias      props['keyAlias']
    keyPassword   props['keyPassword']
  }
}
buildTypes { release { signingConfig signingConfigs.release; minifyEnabled false } }

# build the .aab to upload to Play Console
.\gradlew.bat :app:bundleRelease
The launcher's built-in Theme Store lists themes registered by the admin and opens each entry directly on Google Play (market://details?id=…). Silent install then happens through the Play Store, without leaving CarBox.
On Play Store the theme still appears as a normal app. Build a PreviewActivity as the launcher entry point that shows a screenshot and an "Open in CarBox Launcher" button — never expose the theme Activity itself with a launcher icon.

Distribution

List your theme in the CarBox in-app Theme Store.

Once the theme is live on Google Play, ask the CarBox admin to register it in the in-app Theme Store. The launcher shows the card, the user taps it and Google Play opens on the exact market://details?id=… entry. Installation, payment and updates are handled entirely by Play — CarBox only tracks the declared versionCode to show an "Update available" badge on the card.

01

Zero hosting

No APK on our server — Play hosts the binary, we hold only the card metadata.

02

Free or paid

The price shown on the card is a label; the real transaction happens in Google Play.

03

Update badge

If the declared versionCode is higher than the installed one, CarBox marks the card as updatable.

Info to send to the admin
# Identity
package_name        = com.example.mytheme
theme_id            = my_theme            # immutable, internal
play_store_url      = https://play.google.com/store/apps/details?id=com.example.mytheme

# Card metadata (EN + IT)
name_en / name_it
description_en / description_it
author

# Versioning (MUST match Google Play)
version_name         = 1.0.0
version_code         = 1                  # drives the "update available" badge
min_launcher_version = 1

# Pricing (label only — real price on Play)
price_type          = free | paid
price_string        = "€2.99"             # optional display label

# Media
preview             = screenshot PNG/JPG/WebP
icon                = 512×512 PNG
Every time you push a new APK on Google Play, ask the admin to bump the version_code in the CarBox catalog to the same number. Without that, the launcher will not offer the update to users who already have the theme installed.
Users can always install the theme by opening the Play link directly. Registering it in the CarBox Theme Store is optional — but strongly recommended, because that is where users browse and discover themes without leaving the launcher.

Send it in

Submit your theme to the CarBox Store.

The submission form is a separate page: pick whether you are sending a brand-new theme or an update to one already in the CarBox catalog. The request lands in the admin queue for review; on approval the card appears in the in-app Theme Store and opens Google Play when tapped.

Submissions are reviewed manually. You do NOT upload the APK — the launcher opens Google Play for install. Every time you push a new version on Play, submit an update bumping version_code so the update badge lights up in the CarBox Store.

Before publishing

Theme release checklist

  • Unique applicationId and permanent theme ID
  • No launcher icon in the release build
  • Transparent and correctly reported panel area
  • Immersive mode in theme, overlays and settings
  • No startup work that delays the first frame
  • Left, right and fullscreen app targets tested
  • Panel handles restored after leaving the theme
  • Cold-start and failure fallback tested on a real box
  • Visible Settings button connected to carBox().openSettings()