This document summarizes the structure, design, and common workflows inside the ControlX2 project. It is intended to help future AI agents orient themselves quickly when making changes.
- The project is an Android multi-module Gradle build with
mobile(phone UI + background service),wear(Wear OS UI + complications), andshared(cross-module utilities and serializers) modules. - Both apps depend on the external PumpX2 libraries (
pumpx2-android,pumpx2-messages,pumpx2-shared) for the Tandem pump protocol, alongside Android Jetpack (Compose, Room, DataStore) and Google Play Services for Wearable messaging. - The mobile app runs a long-lived
CommServicethat speaks Bluetooth to the pump and mirrors data to the wear app through the Wearable Message/Data APIs. Compose-based UIs in both apps observe sharedDataStoreinstances backed byMutableLiveDatato display the latest state.
- Global Gradle config in
build.gradledefines versions (controlx2_version,pumpx2_version,compose_version,kotlin_version, etc.) and optionally switches PumpX2 dependencies between Maven Central/JitPack vs. locally published artifacts via theuse_local_pumpx2property. mobile/build.gradleandwear/build.gradleapply Compose andhu.supercluster.paperworkplugins, depend on thesharedmodule, and include Play Services Wearable, Room (mobile only), and numerous Compose material libraries. The wear app also bundles Horologist and wear-compose libraries, plus watchface APIs for complications.- Signing defaults to the provided debug keystore unless overridden with
RELEASE_*system properties. - To work on PumpX2 protocol messages you often need the PumpX2 repo checked out locally; follow the README’s instructions if you need to publish new jars/aar into
~/.m2and flipuse_local_pumpx2.
- Android Studio remains the easiest path: open the repo, select either the
mobileorwearrun configuration, and click Run to install on a connected device or emulator. Pair an actual Tandem pump only when you need hardware tests—most UI adjustments can be previewed without hardware. - Command-line builds:
./gradlew :mobile:assembleDebug/:wear:assembleDebugproduce installable debug APKs../gradlew :mobile:installDebug(and the wear equivalent) deploy directly to a connected device over ADB../gradlew :shared:lintDebugruns lint on the shared code;:mobile:lintDebugand:wear:lintDebugcatch Compose/manifest issues for each app../gradlew testDebugUnitTestexecutes JVM unit tests across modules (there are few today, but the task guards against regressions when new ones are added)../gradlew :mobile:connectedDebugAndroidTest(or:wear:connectedDebugAndroidTest) runs instrumentation tests; requires an attached device/emulator with the service enabled.
- Command-line prerequisites when running inside the Codex container (fixes
SDK location not foundfailures), for Codex runtime environment (repo-local, non-root):.codex/setup.sh: installs command-line tools + SDK packages, accepts licenses, writeslocal.properties..codex/build.sh: runs./gradlew :wear:compileDebugKotlin --console=plainwith the same repo-local SDK env..codex/test.sh: runs./gradlew testDebugUnitTest --console=plainwith the same repo-local SDK env.
- Compose previews are heavily relied upon for iteration. Use
setUpPreviewState(mobile) or the preview helpers inside wear UI files to seed fake data—this prevents Compose previews from crashing when new observable fields are introduced. - When PumpX2 artifacts change, clear Gradle caches with
./gradlew --stop && ./gradlew cleanor./gradlew build --refresh-dependenciesbefore rebuilding so the new protocol definitions flow through to both apps.
- Holds code shared between phone and watch:
CommServiceCodesenumerates the message types thatCommService’s handlers understand.- Serializers (
PumpMessageSerializer,InitiateConfirmedBolusSerializer,PumpQualifyingEventsSerializer) bridge PumpX2 message objects to JSON/byte blobs suitable for Wear messaging, notification extras, or persistence. - Enum wrappers (
BasalStatus,CGMSessionState,UserMode) give more meaningful display strings. - Utility helpers (
SendType,UnitsRenderer,TimberUtil,DebugTree) centralize formatting, logging configuration, and message routing into Timber.setupTimberis used by both apps to pipe PumpX2’sLlogging callbacks through Timber with file logging gates viaShouldLogToFile(mobile) or verbose toggles. SendTypedetermines whether pump commands should be sent freshly (STANDARD), bust the cached response, re-use cached data, or trigger debug prompts. Respect these when adding new message flows so caching keeps working.
MainActivityhosts the Compose UI. It initializes logging, determines the navigation start destination (FirstLaunch → PumpSetup → AppSetup → Landing), binds to the Wearable Message API, and proxies UI actions back to the background service through lambdas (sendMessage,sendPumpCommands,sendServiceBolusRequest, etc.).DataStore(mobile) is aMutableLiveDatahub for pump, CGM, and bolus state. UI layers observe it; service and message handlers update it. It also logs every mutation via Timber (heavy logging in production).LocalDataStorecomposition local exposes the singletonDataStoreto Composables.Prefswraps shared preferences and controls feature toggles (service enablement, pairing, insulin delivery actions, connection sharing, verbose logging, history-log auto-fetch). Always update prefs via this helper to keep behaviour consistent.
- Extends
WearableListenerServiceand runs inside the phone app even when the UI is closed. - Internally owns two
Handlersubclasses:PumpCommHandlermanages aTandemPumpinstance for real pump communication. It handles scanning, pairing (includingPumpFinderhand-off), sending commands, caching recent responses (lastResponseMessage), bulk command routing, cache busting, bolus handling (with safety checks to require confirmation unless below threshold), and periodic refresh tasks.PumpFinderCommHandleris a lighter handler that usesTandemPumpFinderto discover pumps during initial setup.
- Message routing:
- Wear ↔ Phone path strings follow a convention (
/to-phone/...for watch→phone,/to-wear/...for phone→watch,/from-pump/...for pump→clients). The service re-broadcasts pump responses to both the phone UI and wear app usingsendWearCommMessage. CommServiceCodesvalues map to handler operations (start/stop, send command, cached command, pump finder, debug dumps, etc.). When adding new service actions, add enum cases and handle them in the relevant handler.
- Wear ↔ Phone path strings follow a convention (
- Bolus flow:
confirmBolusRequestbuilds a signed payload stored in prefs, sends notifications for user confirmation, and eventually routes tosendPumpCommBolusMessage.BolusNotificationBroadcastReceiverlistens for notification actions, validates signatures viaInitiateConfirmedBolusSerializer, and forwards commands (or cancellation) to the pump. HistoryLogFetchercoordinates background retrieval of pump history logs (withLruCache, concurrency control via coroutines +Mutex, and DB persistence).AppVersionCheckscheduled viacheckForUpdatesTaskhitshttps://versioncheck.controlx2.orgshortly after service start when the user allows update checks.- Service also listens to BLE state broadcasts, maintains a persistent notification summarizing pump status, and saves recent pump data into
DataClientState(so the wear app can access last-known values via DataClient/SharedPreferences).
HistoryLogDatabaseis a Room database storingHistoryLogItementities keyed by sequence ID + pump SID.HistoryLogDaoexposes typed queries for counts, ranges, and latest logs, whileHistoryLogRepo/HistoryLogViewModelprovide coroutine/LiveData wrappers for the UI.HistoryLogFetcheruses the repo to detect missing IDs and issues batches ofHistoryLogRequests until the DB is filled.db/util/ConvertershandleLocalDateTime↔ epoch conversions.
MobileAppwraps the rootNavHostand injects the lambdas that ultimately talk toCommService. Its four destinations form the top-level flow (FirstLaunch → PumpSetup → AppSetup → Landing) and can be deep-linked when onboarding state inPrefschanges.- Primary screens:
- FirstLaunch presents the legal disclaimer inside
DialogScreen; agreeing flips ToS/service prefs and kicks off pump discovery via/to-phone/start-pump-finder, while cancelling terminates the process. - PumpSetup walks through pairing using
PumpSetupStage. It surfaces pairing code entry, advanced settings, and progress UI (PumpSetupStageProgress/Description). Button wiring manipulatesPrefs, advances or rewindsPumpSetupStage, and pushes commands like/to-phone/set-pairing-codeor/to-phone/start-pump-finder. - AppSetup configures global prefs (connection sharing, insulin actions, confirmation thresholds, update checks, history-log fetching). Each toggle mutates
Prefs, triggers/to-phone/app-reload, and conditionally shows warnings (e.g., AlertDialog before enabling insulin delivery actions). - Landing is the authenticated home surface. It renders a top bar reflecting pump connectivity, a Material3 bottom nav built from
LandingSection, and aBottomSheetScaffoldthat hosts bolus/temp-rate sheets. It hoists the currently selected section, bottom-sheet visibility, and nav actions (navigateToSection,openTempRateWindow).
- FirstLaunch presents the legal disclaimer inside
- Landing sections (all live in
presentation/screens/sections):- Dashboard (
Dashboard.kt) drives the overview cards. It fetches pump status (dashboardCommands), hydratesdashboardFields, showsPumpStatusBar, CGM chart (DashboardCgmChart), Control-IQ status, and latest notifications/history banners. It reusesServiceDisabledMessageand setup progress widgets when the pump is offline. - Notifications lists active pump alerts by observing
notificationBundleand mapping items intoNotificationItemrows. Refresh logic mirrors the dashboard but emphasises that some devices cannot dismiss notifications. - Actions surfaces pump control toggles. It reads
actionsFields, exposes suspend/resume pumping menus, manual temp-rate entry (viaTempRateWindow), and shortcuts to bolus/debug sections. Menu booleans gate confirmation dialogs before dispatching control requests (e.g.,SuspendPumpingRequest). - CGMActions handles Dexcom management. It offers menus for starting/stopping G6/G7 sessions, transmitter ID entry (
DexcomG6TransmitterCode,DexcomG6SensorCodecomponents), and fetches saved pairing codes. Alerts guard destructive actions and ensure the appropriate PumpX2 request (e.g.,SetDexcomG7PairingCodeRequest) fires. - CartridgeActions drives cartridge/fill workflows. It enables enter/exit cartridge and fill modes, tracks state streams (
EnterChangeCartridgeModeStateStreamResponse, etc.), and requests cannula fill volumes with validation throughDecimalOutlinedText. - ProfileActions orchestrates profile (IDP) management by iterating
IDPManager.nextMessages(), presenting basal schedule/program lists, and exposing profile-activation/delete actions with confirmation dialogs. It tracks state progress across multiple request rounds. - Settings hosts service toggles (start/stop service, reconfigure pump, debug options entry). Items send
/to-phone/force-reload, navigate back into setup flows, and provide build metadata (VersionInfo). - Debug is the power-user toolbox: it shows cached pump messages, provides arbitrary message senders, log export/clear options, database wipe, history log fetch utilities, and in-app calculators. Most actions require double-checking
Prefsand dispatching raw PumpX2 messages for diagnostics.
- Dashboard (
- Modal sheets:
- BolusWindow collects bolus units/carbs/BG with
DecimalOutlinedText&IntegerOutlinedText, watches bolus calculator LiveData, and orchestrates the multi-step permission/signature flow (BolusPermissionRequest,sendServiceBolusRequest, cancellation handling). It aggressively resets state usingresetBolusDataStoreStateto avoid stale calculator inputs. - TempRateWindow manages temporary basal rates with numeric inputs,
SetTempRateRequestvalidation, and progress dialogs. It intentionally skips cached reads for safety and resetsDataStoretemp-rate fields whenever the sheet closes.
- BolusWindow collects bolus units/carbs/BG with
- Shared Compose components live in
presentation/componentsandpresentation/screens/sections/components:DialogScreenstandardizes title/body/button layout for modal-style screens (FirstLaunch, setup steps).HeaderLine,Line, andServiceDisabledMessageprovide consistent typography and service gating banners.PumpStatusBar,HorizBatteryIcon, andHorizCartridgeIconsummarize pump vitals.DashboardCgmChart,CurrentCGMText, and related CGM widgets visualize glucose trends.- Form inputs (
DecimalOutlinedText,IntegerOutlinedText, Dexcom code fields) convert between human-entered strings and the raw ints/floats stored inDataStore. VersionInforeads Paperwork build metadata for display across both setup and settings flows.
setUpPreviewStateprimesLocalDataStorewith mock data for previews; when adding new LiveData fields update it to prevent preview crashes.LifecycleStateObserver,FixedHeightContainer, and focus helpers such asTextFieldOnFocusSelectavoid duplicate refreshes, enforce layout constraints, and improve text-field UX.
DataClientStatewrites summarized pump data into the WearableDataClient(key/value pairs) for complications;PhoneCommServiceon the watch reads them back viaStatePrefs.AppVersionCheck/AppVersionInfohandle version telemetry and update notifications.ShouldLogToFilegates file logging based on prefs.AppVersionCheck,VolleyQueue, and network calls rely on theandroid.permission.INTERNETdeclared in the manifest.LaunchCommServiceBroadcastReceiverexists as a stub (currently no-op) if you need to bootstrap the service from broadcast events (e.g., BOOT_COMPLETED).
MainActivity(wear) initializes Timber, reads the start route from the launching intent, createsWearApp, and wires send lambdas similar to the mobile activity. It also listens for Wearable messages to update the localDataStoreand routes actions to the phone viasendMessageorsendPumpCommands.WearAppdefines aSwipeDismissableNavHostwhose destinations come fromwear.presentation.navigation.Screen(waiting states, landing dashboard, bolus flow, mode prompts, etc.). It wraps everything in a HorologistScaffoldso the time text, vignette, and position indicator automatically hide/show while scrolling.- Theme colors are read from
WearAppTheme; user toggles for vignette/time text are persisted viarememberSaveable. - Scroll state is hoisted through
ScalingLazyListStateViewModel/ScrollStateViewModel, allowingPositionIndicatorand fade-out time text to reuse state on process death. - Bolus inputs (units/carbs/BG) live in hoisted mutable state so intermediate pickers can round-trip values before calling the phone service.
RequestFocusOnResumeensures rotary input focuses the active list when returning to Landing/Bolus, andBottomTextrenders the watch face footer on each screen.- Waiting/alert routes lean on
IndeterminateProgressIndicator,Alert, orFullScreenTexthelpers, whileReportFullyDrawnmarks startup completion for performance tools.
- Theme colors are read from
- The wear
DataStoremirrors the fields in the phone store but omits some phone-only aspects. It also logs every update via Timber for debugging.
PhoneCommService(wear) mirrorsCommServicebut only handles Wearable messaging, notifications about connection state, and updatesStatePrefsfor complications. It starts in the foreground, listens for pump status messages from the phone (/from-pump/...paths), and forwards them to the watch UI viaLocalDataStore. It also ensures notifications are shown when the phone disconnects or background actions occur.StatePrefspersists small bits of state (connection, battery, IOB, CGM reading) in shared preferences; use this when complications need last-known values offline.UpdateComplicationtriggers watchface complication refreshes when new data arrives.BolusActivityis a simple launcher that opensMainActivitydirectly to the bolus screen (used by complications).
WearAppThemebundles multiple color palettes (defaultTheme,greenTheme,redTheme, etc.) and exposes typography that mirrors ControlX2 branding on the round display. Screens callWearAppThemeindirectly throughWearApp, so update the palette definitions when adding new color families.- Primary watch screens in
presentation/ui:- WaitingForPhone / WaitingToFindPump / ConnectingToPump / PairingToPump / MissingPairingCode / PumpDisconnectedReconnecting show progress via
IndeterminateProgressIndicator, keeping the user informed during hand-offs between the phone service and pump discovery. - LandingScreen renders the watch dashboard. It uses a
ScalingLazyColumnwithFlowRowchips to display pump vitals (LineInfoChip,CurrentCGMText), quick actions (sleep/exercise toggles, open-on-phone, bolus entry), and status summaries. It drives refreshes throughsendPumpCommands, reacts toLocalDataStore, and delegates navigation toScreen.*routes when chips are tapped. - SleepModeSet and ExerciseModeSet pop Horologist
Alertdialogs that let the user toggle Control-IQ modes. Buttons dispatchSetModesRequestcommands, and the dialog text adapts to the currentUserModefromDataStore. - BolusScreen mirrors the phone bolus window: it walks through calculator states, displays condition prompts, drives permission/confirmation dialogs, and forwards approved requests to the phone via
sendPhoneBolusRequest. The screen maintains booleans for each dialog (permission, confirm, in-progress, cancelled, approved) and listens toLocalDataStorefor calculator updates. - BolusSelectUnits/Carbs/BG screens provide rotary-friendly pickers (
DecimalNumberPicker,SingleNumberPicker) with exponential scrolling, respect pump-imposed maxima, and write results back to the hoisted bolus state before popping. - BolusBlocked, BolusNotEnabled, and BolusRejectedOnPhone surface blocking conditions with
FullScreenTextorAlertUI, guiding the user back to Landing when manual intervention is required.
- WaitingForPhone / WaitingToFindPump / ConnectingToPump / PairingToPump / MissingPairingCode / PumpDisconnectedReconnecting show progress via
- Supporting composables:
FullScreenText,IndeterminateProgressIndicator, andReportFullyDrawn(startup reporting) live alongside the screens for reuse.presentation/componentshouses chips (FirstRowChip,LineInfoChip,MiniChip), text widgets (TopText,BottomText,CustomTimeText), glucose displays (CurrentCGMText), and numeric pickers. Each component expects aLocalDataStorecontext and handles round-watch ergonomics (padded touch targets, rotary input, large text).presentation/utiladds helpers likeLifecycleStateObserverthat mirror the mobile patterns for periodic refresh and state reset.
- Complication services under
wear/complicationssupply pump battery, pump IOB, CGM reading, and quick actions (bolus/app launch). They share helper data classes (e.g.,ButtonComplicationData) and useStatePrefsto read cached values. When adding new complications, follow the existing pattern: build data viaDataFields, respect recency thresholds, and provide preview data. ComplicationToggleReceiverandComplicationToggleArgssupport interactive complications (currently mostly boilerplate for future toggles).
- Message paths:
- Phone UI ↔ service: Compose screens call
sendMessageorsendPumpCommands(withSendType) which forward toMainActivity→ Wearable MessageClient →CommService. - Phone service → UI/watch:
CommServiceusessendWearCommMessageto broadcast pump events (/from-pump/...) and service state updates. - Watch → phone:
MainActivity(wear) orPhoneCommServicesends/to-phone/...messages thatCommServicehandles inonMessageReceived.
- Phone UI ↔ service: Compose screens call
- When introducing new pump operations, decide whether they should bypass cache (
BUST_CACHE), request cached data (CACHED), or just send raw commands. Update the corresponding command list constants so the periodic refresh logic keeps them in sync. CommServiceCodes+ handlers are the authoritative map of what the service understands. Always add new codes there and ensure both handlers deal with them if needed (e.g., PumpFinder vs PumpComm).
- Timber is globally configured via
setupTimberwith a customDebugTreethat can write to files (debugLog-*.txt) when enabled. File logging path is/data/user/0/com.jwoglom.controlx2/files/. - The mobile Debug screen lets users send arbitrary pump messages, inspect caches/history logs, share logs, and toggle developer settings (Only Snoop Bluetooth, verbose logging, etc.). When you add new debugging utilities, hook them into this screen for easy access.
ShouldLogToFileuses prefs to restrict which tags are persisted; keep tags consistent (e.g.,L:Messages,CommService) to benefit from existing filters.
- Build metadata is provided by the Paperwork plugin (
build_time,build_version) and shown in the UI viaVersionInfocomponents. AppVersionCheckposts device metadata to the version server and notifies users viaNotificationCompatif a newer build exists. Respect thePrefs.checkForUpdates()toggle before initiating network calls.
setUpPreviewState(mobile) and default preview functions populateLocalDataStorefor Compose previews; reuse when creating new screens/components so previews render meaningful data.- Several previews rely on
LocalDataStorebeing a mutable singleton. If you changeDataStore’s constructor, ensure previews still initialize the expected fields. - For watch UI,
WearApppreviews can userememberSwipeDismissableNavController()and reuse data store stubs similar to the phone.
- Insulin delivery (bolus) actions are disabled by default:
Prefs.insulinDeliveryActions()must return true before bolus commands are forwarded. UI surfaces and notifications respect this, so any new insulin-affecting feature must check the same preference. - Connection sharing (
Prefs.connectionSharingEnabled()) toggles whether the service enables PumpX2 features that coexist with the official t:connect app. Preserve this behaviour when altering pairing/connection flows. - Many screens abort their refresh loops if
Prefs.serviceEnabled()is false—keep this guard in mind when adding new polling coroutines to avoid busy loops when the service is stopped.
- LiveData observers in
DataStorelog aggressively. When adding new fields, follow the same pattern (initializeMutableLiveData, optionally seed a default, and addobserveForeverlogging if useful). - When creating new pump commands, prefer using PumpX2 request builders (
CurrentBatteryRequestBuilder, etc.) to ensure correct opcode/cargo formatting. - The watch relies on phone state via Wearable DataClient (
DataClientState) and SharedPreferences (StatePrefs); keep both in sync when introducing new data that complications or offline screens should display. - Bolus/Temp rate windows manage raw user input via dedicated
MutableLiveDatafields (bolusUnitsRawValue, etc.). If you introduce new modal workflows, model them similarly so that state survives recomposition and can be reset cleanly when the modal closes. - Respect the message throttling/caching logic (
CacheSeconds,lastResponseMessagemap). Clearing the cache too aggressively can increase BLE load and battery consumption. HistoryLogFetcheruses coroutines withMutexto serialize fetch ranges. If you adjust fetch sizes/timeouts, update the constants (InitialHistoryLogCount,FetchGroupTimeoutMs) thoughtfully.
- The VM has JDK 21 pre-installed, which satisfies AGP 8.13.2's JDK 17+ requirement.
- Run
.codex/setup.shto bootstrap a repo-local Android SDK at.android-sdk/, accept licenses, install required packages (platforms;android-35,platforms;android-36,build-tools;35.0.0,platform-tools), and generatelocal.properties. - The helper scripts are self-contained and use
python3directly (no extrapythonsymlink step is required).
Before any Gradle command, export the SDK environment variables (or source them from the helper scripts):
export ANDROID_SDK_ROOT="/workspace/controlX2/.android-sdk"
export ANDROID_HOME="/workspace/controlX2/.android-sdk"
export PATH="/workspace/controlX2/.android-sdk/cmdline-tools/latest/bin:/workspace/controlX2/.android-sdk/platform-tools:$PATH"
export GRADLE_USER_HOME="/workspace/controlX2/.gradle-home"
Then use the standard commands documented in the "Running & testing" section above.
- This is an Android-only project — there is no web frontend, backend server, or Docker dependency. The "hello world" verification is a successful
assembleDebugproducing APK files, since running the app requires an Android device or emulator. compileSdkis 36; ensure platform 36 is installed (the current.codex/setup.shhandles this automatically).- The root
build.gradlereadslocal.propertieseagerly at configuration time. Iflocal.propertiesis missing, Gradle will fail immediately. Always run.codex/setup.shfirst. - Roborazzi screenshot tests: record with
./gradlew :mobile:recordRoborazziDebug :wear:recordRoborazziDebug, verify with./gradlew :mobile:verifyRoborazziDebug :wear:verifyRoborazziDebug. Golden images are stored in each module's test snapshots directory.
- The VM has JDK 21 pre-installed and a repo-local Android SDK at
.android-sdk/with onlycmdline-toolspre-installed. - The environment routes all outbound traffic through an authenticated HTTP proxy configured via
JAVA_TOOL_OPTIONS. The proxy credentials (user/password) are embedded in that env var.
1. Configure Gradle proxy settings — Gradle needs explicit proxy config in ~/.gradle/gradle.properties. Extract the proxy credentials from JAVA_TOOL_OPTIONS and write them:
PROXY_USER=$(echo "$JAVA_TOOL_OPTIONS" | grep -oP '(?<=-Dhttp.proxyUser=)[^ ]+')
PROXY_PASS=$(echo "$JAVA_TOOL_OPTIONS" | grep -oP '(?<=-Dhttp.proxyPassword=)[^ ]+')
cat > ~/.gradle/gradle.properties << EOF
systemProp.http.proxyHost=21.0.0.17
systemProp.http.proxyPort=15004
systemProp.https.proxyHost=21.0.0.17
systemProp.https.proxyPort=15004
systemProp.http.proxyUser=$PROXY_USER
systemProp.http.proxyPassword=$PROXY_PASS
systemProp.https.proxyUser=$PROXY_USER
systemProp.https.proxyPassword=$PROXY_PASS
systemProp.http.nonProxyHosts=localhost|127.0.0.1
systemProp.jdk.http.auth.tunneling.disabledSchemes=
systemProp.jdk.http.auth.proxying.disabledSchemes=
org.gradle.jvmargs=-Xmx4g -Dhttp.nonProxyHosts=localhost|127.0.0.1
EOFImportant: The default
JAVA_TOOL_OPTIONSincludes*.googleapis.com|*.google.cominhttp.nonProxyHosts, which bypasses the proxy for Google domains. This breaks the Android SDK manager (which downloads fromdl.google.com). The Gradle properties above override this with a minimal nonProxyHosts list.
2. Install Android SDK components manually — The sdkmanager CLI also fails to download through the proxy because it inherits the broken nonProxyHosts from JAVA_TOOL_OPTIONS. Download build-tools and platform JARs directly using curl:
PROXY="http://${PROXY_USER}:${PROXY_PASS}@21.0.0.17:15004"
SDK=.android-sdk
# Download build-tools 35.0.0
curl -x "$PROXY" -sL "https://dl.google.com/android/repository/build-tools_r35_linux.zip" -o /tmp/bt35.zip
mkdir -p "$SDK/build-tools/35.0.0"
unzip -qo /tmp/bt35.zip -d "$SDK/build-tools/35.0.0/"
# The zip extracts into a subdirectory (e.g. android-15/) — move contents up
mv "$SDK/build-tools/35.0.0/android-"*/* "$SDK/build-tools/35.0.0/"
# Download platforms
curl -x "$PROXY" -sL "https://dl.google.com/android/repository/platform-35_r02.zip" -o /tmp/p35.zip
curl -x "$PROXY" -sL "https://dl.google.com/android/repository/platform-36_r02.zip" -o /tmp/p36.zip
unzip -qo /tmp/p35.zip -d "$SDK/platforms/"
unzip -qo /tmp/p36.zip -d "$SDK/platforms/"3. Create package.xml files — AGP requires these metadata files to recognize SDK components:
# build-tools/35.0.0/package.xml
cat > "$SDK/build-tools/35.0.0/package.xml" << 'XML'
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ns2:repository xmlns:ns2="http://schemas.android.com/repository/android/common/02" xmlns:ns7="http://schemas.android.com/sdk/android/repo/repository2/03">
<localPackage path="build-tools;35.0.0" obsolete="false">
<type-details xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="ns7:genericDetailsType"/>
<revision><major>35</major><minor>0</minor><micro>0</micro></revision>
<display-name>Android SDK Build-Tools 35</display-name>
</localPackage>
</ns2:repository>
XML
# platforms/android-35/package.xml
cat > "$SDK/platforms/android-35/package.xml" << 'XML'
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ns2:repository xmlns:ns2="http://schemas.android.com/repository/android/common/02" xmlns:ns7="http://schemas.android.com/sdk/android/repo/repository2/03">
<localPackage path="platforms;android-35" obsolete="false">
<type-details xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="ns7:platformDetailsType"><api-level>35</api-level><codename></codename></type-details>
<revision><major>2</major></revision>
<display-name>Android SDK Platform 35</display-name>
</localPackage>
</ns2:repository>
XML
# platforms/android-36/package.xml
cat > "$SDK/platforms/android-36/package.xml" << 'XML'
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ns2:repository xmlns:ns2="http://schemas.android.com/repository/android/common/02" xmlns:ns7="http://schemas.android.com/sdk/android/repo/repository2/03">
<localPackage path="platforms;android-36" obsolete="false">
<type-details xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="ns7:platformDetailsType"><api-level>36</api-level><codename></codename></type-details>
<revision><major>2</major></revision>
<display-name>Android SDK Platform 36</display-name>
</localPackage>
</ns2:repository>
XML4. Configure Robolectric for offline mode — Robolectric downloads instrumented Android JARs at test time, which also fails through the proxy. Download them manually and configure offline mode:
# Download the instrumented JARs Robolectric needs
mkdir -p ~/.robolectric
curl -x "$PROXY" -sL "https://repo1.maven.org/maven2/org/robolectric/android-all-instrumented/14-robolectric-10818077-i7/android-all-instrumented-14-robolectric-10818077-i7.jar" \
-o ~/.robolectric/android-all-instrumented-14-robolectric-10818077-i7.jar
curl -x "$PROXY" -sL "https://repo1.maven.org/maven2/org/robolectric/android-all-instrumented/15-robolectric-12650502-i7/android-all-instrumented-15-robolectric-12650502-i7.jar" \
-o ~/.robolectric/android-all-instrumented-15-robolectric-12650502-i7.jarThen add Robolectric offline system properties to mobile/build.gradle inside testOptions > unitTests > all:
systemProperty 'robolectric.offline', 'true'
systemProperty 'robolectric.dependency.dir', "${System.getProperty('user.home')}/.robolectric"export ANDROID_HOME=/home/user/controlX2/.android-sdk
# Compile
./gradlew :mobile:compileDebugKotlin
# Run CommService integration tests
./gradlew :mobile:testDebugUnitTest --tests "com.jwoglom.controlx2.CommServiceIntegrationTest"
# Full test suite
./gradlew :mobile:testDebugUnitTest- The
JAVA_TOOL_OPTIONSenv var is read-only and set at container level. You cannot override itsnonProxyHosts— instead, set the correct values in~/.gradle/gradle.propertieswhich Gradle picks up as system properties for its own JVM. - SDK component URLs can be found by downloading the repository manifest:
curl -x "$PROXY" -sL "https://dl.google.com/android/repository/repository2-3.xml"and searching for the package path. - If Robolectric tests fail with
MavenArtifactFetcher/407 Proxy Authentication Required, the offline mode JARs are missing or the system property wasn't applied. Check that the JAR filenames match exactly what Robolectric requests (visible in--infooutput).