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>Discovery contract
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.
<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>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.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+// 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);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// 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"));// 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.
}// 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();carBox().openCreativeMode(); // switch to creative mode
carBox().restartLauncher(); // restart the launcher process
carBox().reloadPanels(); // re-render dual-app panels// 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 }// 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
});// 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// 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");
}// 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
}| API | Parameter | Action |
|---|---|---|
launchApp | package, CarBoxTarget | Open 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 |
callNumber | String (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. |
getCompanionContacts | Consumer<String> onJson, Consumer<String> onError | Async 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 |
reportPanelArea | Rect | Report the exact rectangle reserved for DualApp |
setPanelHandlesVisible | boolean | Show 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 |
setPanelSplitRatio | float 0..1 | Restore an exact left-panel fraction (clamped to 0.15..0.85, debounced 120 ms). Requires launcher 1.75+ |
setLayoutMode | String ("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+ |
openPanelSettings | String 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+ |
setDialogMode | boolean | Move the panels aside while a theme dialog is open (call true on show, false on dismiss) |
isHosted | Activity | True when the theme is being rendered by CarBox (bridge active) |
setDialogMode(true/false). Without it the DualApp panels stay above the theme and cover the dialog.Runtime permissions
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:
getPackageInfo(pkg, GET_PERMISSIONS) of your theme APK.carbox.theme.PERMISSIONS extra.<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>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
}
}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
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