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.
Download template
CarBox Theme SDK
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.
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
Both demos include the Theme Bridge API, XML preview, panel hosting and are ready to be signed and published on Google Play.
Empty starting point
Minimal Android Studio project for the new PLUGIN model: no Activity, no VirtualDisplay. Compile, install and start replacing ThemePlugin.java.
Ready-to-install demo
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.
New in CarBox v2
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.
No IPC input injection. Taps and gestures run at the same speed as CarBox Classic.
AlertDialog, Dialog and menus work out of the box because they attach to the launcher's Activity window.
Still a normal Android Studio project. What changes is the manifest entry and the base class you extend.
<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>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; }
}The empty template above already contains everything wired up. Start from it.
Architecture
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.
Build the home with normal Android XML layouts and use the Android Studio Design editor.
Use Java for clocks, weather, media state, animations, drawers and custom Views.
Theme code has its own UID. If loading fails, CarBox clears the broken selection and opens CarBox Classic.
Project setup
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.
Copy the demo and open the copied folder in Android Studio.
Assign a unique namespace, applicationId and theme ID.
Edit theme_main.xml in landscape Design view.
Bind the panel area and use CarBoxThemeBridge for launcher actions.
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
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.
<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>Multilingual themes
Themes use Android's standard localization system. Create separate strings.xml files for each language in res/values and res/values-LANG directories.
<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><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>// 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 localizedSupported 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
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.
<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>Secure communication
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.
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
The bridge delegates actions to the launcher on the correct display. Never call startActivity() for panel apps directly from the hosted theme.
carBox().launchApp("com.spotify.music", CarBoxTarget.LEFT_PANEL);
carBox().launchApp("com.spotify.music", CarBoxTarget.RIGHT_PANEL);
carBox().launchApp("com.spotify.music", CarBoxTarget.FULLSCREEN);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 homecarBox().openCreativeMode(); // switch to creative mode
carBox().restartLauncher(); // restart the launcher process
carBox().reloadPanels(); // re-render dual-app panels// 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();// Same behaviour as tapping the launcher's bottom handles.
// A second call restores the saved split ratio.
carBox().expandLeftPanel();
carBox().expandRightPanel();// 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// 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();if (carBox().isHosted(this)) {
// running inside CarBox: bridge commands are active
} else {
// running standalone (preview mode): commands are no-ops
}Open an app on the left panel, right panel or fullscreen
carBox().launchApp("com.spotify.music", CarBoxTarget.LEFT_PANEL);Open the CarBox app drawer
carBox().openDrawer();Open the CarBox phone panel
carBox().openPhone();Open the contacts list
carBox().openContacts();Open the notifications panel
carBox().openNotifications();Open launcher settings
carBox().openSettings();Start the voice assistant
carBox().openAssistant();Return to the launcher home
carBox().goHome();Switch CarBox to creative mode
carBox().openCreativeMode();Restart the launcher process
carBox().restartLauncher();Re-render the dual-app panels
carBox().reloadPanels();Report the exact rectangle reserved for DualApp
carBox().reportPanelArea(new Rect(0, 0, 800, 480));Show or hide native panel handles for this theme session
carBox().setPanelHandlesVisible(false);Show native panel handles (convenience wrapper)
carBox().showPanelHandles();Toggle current handle visibility
carBox().togglePanelHandles();Expand the left panel; call again to restore
carBox().expandLeftPanel();Expand the right panel; call again to restore
carBox().expandRightPanel();Read the current left-panel fraction (0..1)
float ratio = carBox().getPanelSplitRatio();
Toast.makeText(ctx, "Left: " + (ratio*100) + "%", Toast.LENGTH_SHORT).show();Restore an exact left-panel fraction (0.15..0.85)
carBox().setPanelSplitRatio(0.7f); // 70% left, 30% rightMove panels aside while a theme dialog is open
carBox().setDialogMode(true);
dialog.setOnDismissListener(d -> carBox().setDialogMode(false));
dialog.show();Check if running inside CarBox
if (carBox().isHosted(this)) {
// Inside CarBox, bridge is active
} else {
// Preview mode, commands are no-ops
}Speak a string using CarBox's configured voice
carBox().speak("Welcome to CarBox");Navigate to a specific creative mode page (1-indexed)
carBox().goToPage(2); // Go to page 2Jump to the next creative mode page
carBox().nextPage();Jump to the previous creative mode page
carBox().previousPage();Set the CreativeLayout edit state explicitly
carBox().setEditMode(true); // Show + button
carBox().setEditMode(false); // Hide + buttonOpen the app drawer/launcher apps list
carBox().openAppDrawer();Launch a favorite app by index (0-based)
carBox().launchFavorite(0); // Launch first favoriteLaunch an app on a specific target (requires launcher 1.87+)
carBox().launchAppTarget("com.google.android.maps", "left");
// Targets: left, right, single, fullscreen, portalSwitch layout mode: single or dual (requires launcher 1.96+)
carBox().setMode("single"); // Single panel mode
carBox().setMode("dual"); // Dual panel modeToggle between single and dual panel modes
carBox().toggleMode();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");
}Set custom rectangle bounds for the DualApp panel area
Rect bounds = new Rect(0, 100, 800, 1080);
carBox().setThemePanelBounds(bounds);Open the launcher's main settings/preferences Activity
carBox().openLauncherSettings();Call a phone number or contact
carBox().call("+1234567890");
carBox().call("John Doe");Clean memory and close background apps
int closed = carBox().cleanMemory();
Toast.makeText(ctx, "Freed " + closed + " apps", Toast.LENGTH_SHORT).show();Close all running apps (returns count of closed apps)
int count = carBox().closeAllApps();
Log.d("CarBox", "Closed " + count + " apps");Set panels behind or in front of other UI elements
carBox().setPanelsBehind(true); // Panels behind
carBox().setPanelsBehind(false); // Panels in frontShow an overlay Activity supplied by the theme
carBox().showThemeOverlay("left_drawer");setDialogMode(true/false). Without it the DualApp panels stay above the theme and cover the dialog.Advanced Features
Themes can save user preferences using SharedPreferences, integrate weather, display media player info, and let users customize colors, dock height, and icon visibility.
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();
}
// 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();
}
});
}
}
// 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();
}
// 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());
});
}
// 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);
}
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 Bridge Security
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:
How the Bridge Works
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);
}
}// 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// 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
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().
<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
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.
<intent-filter>
<action android:name="com.ferra.carboxlauncher.action.THEME_SETTINGS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>Safe loading
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.
Build & publish
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.
$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# 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:bundleReleaseDistribution
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.
No APK on our server — Play hosts the binary, we hold only the card metadata.
The price shown on the card is a label; the real transaction happens in Google Play.
If the declared versionCode is higher than the installed one, CarBox marks the card as updatable.
# 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 PNGversion_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.Send it in
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.
version_code so the update badge lights up in the CarBox Store.Before publishing