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.

Multilingual themes

Support multiple languages with strings.xml

Themes use Android's standard localization system. Create separate strings.xml files for each language in res/values and res/values-LANG directories.

res/values/strings.xml (English)
<resources>
    <string name="app_name">My CarBox Theme</string>
    <string name="theme_name">MY THEME</string>
    <string name="open_drawer">Open drawer</string>
    <string name="close_drawer">Close drawer</string>
</resources>
res/values-it/strings.xml (Italian)
<resources>
    <string name="app_name">Mio tema CarBox</string>
    <string name="theme_name">MIO TEMA</string>
    <string name="open_drawer">Apri cassetto</string>
    <string name="close_drawer">Chiudi cassetto</string>
</resources>
Java code — reference translations
// Android automatically selects the right strings.xml based on device language
String openLabel = getString(R.string.open_drawer);  // "Open drawer" or "Apri cassetto"
String themeName = getString(R.string.theme_name);

TextView header = findViewById(R.id.header);
header.setText(getString(R.string.theme_name));  // Automatically localized

Supported locales: Create directories for each language: values-de (German), values-es (Spanish), values-fr (French), values-ja (Japanese), etc. Android selects the closest match based on device settings.

Discovery contract

Declare the plugin class at the app level.

Plugin v2 themes declare the View class CarBox loads via app-level meta-data. No Activity is needed for theme loading. Optional: add a PreviewActivity for the Play Store or standalone preview.

AndroidManifest.xml — plugin v2 (recommended)
<application>
  <!-- App-level: tells CarBox which View class to load -->
  <meta-data
    android:name="carbox.theme.pluginClass"
    android:value="com.example.mytheme.ThemePlugin" />

  <!-- Optional: PreviewActivity for Play Store or standalone preview -->
  <activity
    android:name=".PreviewActivity"
    android:exported="true"
    android:screenOrientation="landscape">
    <intent-filter>
      <action android:name="android.intent.action.MAIN" />
      <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
  </activity>

  <!-- Optional: Settings button entry point (used when theme is active) -->
  <activity-alias
    android:name=".ThemeMarker"
    android:targetActivity=".PreviewActivity"
    android:exported="true">
    <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.settings_entry"
      android:value="true" />
  </activity-alias>
</application>
Plugin v2 is the recommended model. CarBox automatically loads the ThemePlugin class via reflection. No virtual display or activity hosting overhead.
When submitting to CarBox Store: the manifest.json in your .cbtheme file must also include "pluginClass": "com.example.mytheme.ThemePlugin" for the launcher to recognize plugin v2 themes from .cbtheme bundles.

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.FULLSCREEN);
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
Modes and lifecycle
carBox().openCreativeMode();    // switch to creative mode
carBox().restartLauncher();     // restart the launcher process
carBox().reloadPanels();        // re-render dual-app panels
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
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
}

launchApp

Open an app on the left panel, right panel or fullscreen

carBox().launchApp("com.spotify.music", CarBoxTarget.LEFT_PANEL);

openDrawer

Open the CarBox app drawer

carBox().openDrawer();

openPhone

Open the CarBox phone panel

carBox().openPhone();

openContacts

Open the contacts list

carBox().openContacts();

openNotifications

Open the notifications panel

carBox().openNotifications();

openSettings

Open launcher settings

carBox().openSettings();

openAssistant

Start the voice assistant

carBox().openAssistant();

goHome

Return to the launcher home

carBox().goHome();

openCreativeMode

Switch CarBox to creative mode

carBox().openCreativeMode();

restartLauncher

Restart the launcher process

carBox().restartLauncher();

reloadPanels

Re-render the dual-app panels

carBox().reloadPanels();

reportPanelArea

Report the exact rectangle reserved for DualApp

carBox().reportPanelArea(new Rect(0, 0, 800, 480));

setPanelHandlesVisible

Show or hide native panel handles for this theme session

carBox().setPanelHandlesVisible(false);

showPanelHandles

Show native panel handles (convenience wrapper)

carBox().showPanelHandles();

togglePanelHandles

Toggle current handle visibility

carBox().togglePanelHandles();

expandLeftPanel

Expand the left panel; call again to restore

carBox().expandLeftPanel();

expandRightPanel

Expand the right panel; call again to restore

carBox().expandRightPanel();

getPanelSplitRatio

Read the current left-panel fraction (0..1)

float ratio = carBox().getPanelSplitRatio();
Toast.makeText(ctx, "Left: " + (ratio*100) + "%", Toast.LENGTH_SHORT).show();

setPanelSplitRatio

Restore an exact left-panel fraction (0.15..0.85)

carBox().setPanelSplitRatio(0.7f);  // 70% left, 30% right

setDialogMode

Move panels aside while a theme dialog is open

carBox().setDialogMode(true);
dialog.setOnDismissListener(d -> carBox().setDialogMode(false));
dialog.show();

isHosted

Check if running inside CarBox

if (carBox().isHosted(this)) {
    // Inside CarBox, bridge is active
} else {
    // Preview mode, commands are no-ops
}

speak(text)

Speak a string using CarBox's configured voice

carBox().speak("Welcome to CarBox");

goToPage

Navigate to a specific creative mode page (1-indexed)

carBox().goToPage(2);  // Go to page 2

nextPage

Jump to the next creative mode page

carBox().nextPage();

previousPage

Jump to the previous creative mode page

carBox().previousPage();

setEditMode

Set the CreativeLayout edit state explicitly

carBox().setEditMode(true);   // Show + button
carBox().setEditMode(false);  // Hide + button

openAppDrawer

Open the app drawer/launcher apps list

carBox().openAppDrawer();

launchFavorite

Launch a favorite app by index (0-based)

carBox().launchFavorite(0);  // Launch first favorite

launchAppTarget

Launch an app on a specific target (requires launcher 1.87+)

carBox().launchAppTarget("com.google.android.maps", "left");
// Targets: left, right, single, fullscreen, portal

setMode

Switch layout mode: single or dual (requires launcher 1.96+)

carBox().setMode("single");  // Single panel mode
carBox().setMode("dual");    // Dual panel mode

toggleMode

Toggle between single and dual panel modes

carBox().toggleMode();

getLanguage()

Read the user's configured language preference from CarBox settings. Returns a lowercase language code or falls back to system locale.

String lang = carBox().getLanguage();  // "it", "en", etc.
if ("it".equals(lang)) {
    tvWelcome.setText("Benvenuto in CarBox");
} else {
    tvWelcome.setText("Welcome to CarBox");
}

setThemePanelBounds

Set custom rectangle bounds for the DualApp panel area

Rect bounds = new Rect(0, 100, 800, 1080);
carBox().setThemePanelBounds(bounds);

openLauncherSettings

Open the launcher's main settings/preferences Activity

carBox().openLauncherSettings();

call

Call a phone number or contact

carBox().call("+1234567890");
carBox().call("John Doe");

cleanMemory

Clean memory and close background apps

int closed = carBox().cleanMemory();
Toast.makeText(ctx, "Freed " + closed + " apps", Toast.LENGTH_SHORT).show();

closeAllApps

Close all running apps (returns count of closed apps)

int count = carBox().closeAllApps();
Log.d("CarBox", "Closed " + count + " apps");

setPanelsBehind

Set panels behind or in front of other UI elements

carBox().setPanelsBehind(true);   // Panels behind
carBox().setPanelsBehind(false);  // Panels in front

showThemeOverlay

Show an overlay Activity supplied by the theme

carBox().showThemeOverlay("left_drawer");
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.

Advanced Features

Store theme settings and add customization

Themes can save user preferences using SharedPreferences, integrate weather, display media player info, and let users customize colors, dock height, and icon visibility.

SharedPreferences — save theme settings
private SharedPreferences prefs;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Accedi alle prefs DOPO attachHostActivity (launcher context)
    prefs = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE);

    // Leggi impostazioni salvate
    int dockHeight = prefs.getInt("dock_height_dp", 56);
    int dockColor = prefs.getInt("dock_color", 0xE6111B24);

    // Salva impostazioni
    prefs.edit()
        .putInt("dock_height_dp", 64)
        .putInt("dock_color", 0xFF1E88E5)
        .putStringSet("visible_icons", new HashSet<>(Arrays.asList("phone", "apps")))
        .apply();
}
Settings Activity — custom theme configuration
// AndroidManifest.xml
<activity
    android:name=".SettingsActivity"
    android:label="Theme Settings"
    android:theme="@style/Theme.MyTheme.Settings">
    <intent-filter>
        <action android:name="com.ferra.carboxlauncher.action.THEME_SETTINGS" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>

// SettingsActivity.java
public class SettingsActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        SharedPreferences prefs = getSharedPreferences("theme_prefs", MODE_PRIVATE);

        // Color picker
        findViewById(R.id.dock_color_btn).setOnClickListener(v -> {
            showColorPicker(prefs, "dock_color", 0xE6111B24);
        });

        // Dock height slider
        SeekBar dockHeightSlider = findViewById(R.id.dock_height);
        dockHeightSlider.setProgress(prefs.getInt("dock_height_dp", 56));
        dockHeightSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                prefs.edit().putInt("dock_height_dp", Math.max(44, Math.min(84, progress))).apply();
            }
        });
    }
}
Weather integration — display temperature
// Request location + weather API
private void fetchWeather(double lat, double lon) {
    new Thread(() -> {
        try {
            URL url = new URL("https://api.open-meteo.com/v1/forecast?latitude="
                + lat + "&longitude=" + lon
                + "¤t_weather=true&temperature_unit=celsius");
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));

            JSONObject response = new JSONObject(reader.readLine());
            JSONObject current = response.getJSONObject("current_weather");
            double temp = current.getDouble("temperature");

            runOnUiThread(() -> {
                TextView weatherView = findViewById(R.id.weather);
                weatherView.setText(String.format("%.1f°C", temp));
            });
        } catch (Exception e) {
            Log.e("Weather", "Error fetching weather", e);
        }
    }).start();
}
NowPlaying sidebar — show media info
// Listen to media session callbacks
private void setupNowPlayingBar() {
    findViewById(R.id.now_playing_toggle).setOnClickListener(v -> {
        boolean isOpen = prefs.getBoolean("now_playing_open", false);
        prefs.edit().putBoolean("now_playing_open", !isOpen).apply();

        // Expand/collapse sidebar (280dp wide)
        FrameLayout nowPlaying = findViewById(R.id.now_playing_sidebar);
        TransitionManager.beginDelayedTransition(nowPlaying);
        nowPlaying.getLayoutParams().width = isOpen ? 0 : dpToPx(280);
        nowPlaying.setLayoutParams(nowPlaying.getLayoutParams());
    });
}
Icon visibility & ordering — let users customize dock
// Store visible icons as JSON array
private void saveIconOrder(List<String> iconIds) {
    JSONArray arr = new JSONArray(iconIds);
    prefs.edit().putString("dock_icon_order", arr.toString()).apply();
}

// Load and apply saved order
private List<String> loadIconOrder() {
    String json = prefs.getString("dock_icon_order", "[]");
    List<String> icons = new ArrayList<>();
    try {
        JSONArray arr = new JSONArray(json);
        for (int i = 0; i < arr.length(); i++) {
            icons.add(arr.getString(i));
        }
    } catch (JSONException e) {
        Log.e("IconOrder", "Error parsing icon order", e);
    }
    return icons;
}

// Check if icon should be visible
private boolean isIconVisible(String iconId) {
    return prefs.getBoolean("dock_icon_visible_" + iconId, true);
}
Dock & icon colors — personalization
private void applyThemeColors() {
    int dockColor = prefs.getInt("dock_color", 0xE6111B24);
    int iconColor = prefs.getInt("icon_color", 0xFFE8F4F8);

    findViewById(R.id.dock_bar).setBackgroundColor(dockColor);

    ViewGroup iconContainer = findViewById(R.id.icon_container);
    for (int i = 0; i < iconContainer.getChildCount(); i++) {
        ImageView icon = (ImageView) iconContainer.getChildAt(i);
        icon.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN);
    }
}
Theme preferences isolation: Each theme has its own SharedPreferences namespace. Use unique keys to avoid conflicts with other themes.

Theme Bridge Security

Control CarBox without inheriting its privileges

The Theme Bridge uses explicit broadcasts with a private session token. Your theme code does not run with launcher privileges — CarBox controls permissions and dispatches commands from your theme to the launcher. This means:

  • Your theme cannot access the launcher's settings or protected files
  • Commands fail safely if the token is invalid or the launcher is not available
  • You cannot call startActivity() directly — use the bridge instead
  • All actions go through CarBoxThemeBridge, which enforces the security contract

How the Bridge Works

Theme Activity receives a session token
public class MyThemeActivity extends CarBoxThemeActivity {
  @Override
  protected void onCreate(Bundle state) {
    super.onCreate(state);
    setContentView(R.layout.theme_main);
    // The bridge is initialized with a unique session token
    // Every command includes this token for validation
    bindPanelArea(R.id.carboxPanelArea);
  }
}
Commands are explicit broadcasts, not system calls
// This does NOT run launcher code in your process.
// CarBox receives the broadcast, validates the token, and executes safely.
carBox().launchApp("com.spotify.music", CarBoxTarget.LEFT_PANEL);

// Under the hood:
// 1. Theme sends explicit broadcast with EXTRA_TOKEN
// 2. Launcher validates token matches current session
// 3. Launcher executes the action in its own process
// 4. Token becomes invalid when theme closes
Safe error handling
// If the token is invalid, the command fails silently.
// If the launcher crashes, the theme continues running
// (it won't inherit launcher privileges and crash with it).
try {
  if (carBox().isHosted(this)) {
    // Bridge is active, commands work
    carBox().openDrawer();
  } else {
    // Preview mode or launcher offline
    Toast.makeText(this, "Not running in CarBox", Toast.LENGTH_SHORT).show();
  }
} catch (Exception e) {
  // Commands never throw; they fail gracefully
}

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()