Do NOT change the package declaration to main.java.com.custom.launcher or similar.
- The correct package for all app code is
com.custom.launcher(and subpackages). - The source root is
app/src/main/java/. - The directory structure under
java/must becom/custom/launcher/.... - Do NOT add or expect a
main/java/com/custom/launcherpackage or directory. - If you see errors about
main.java.com.custom.launcher, it means the IDE or build system is misconfigured, not the code.
If you are an agent or developer, do NOT edit package declarations or move files to match a main.java prefix.
This note is to prevent repeated, unnecessary changes that break the build.
The car's firewall BLOCKS all ADB access:
- iptables rules in
/system/bin/arp_update.shDROP packets on port 5555 (ADB) - These rules run on EVERY boot
- Cannot use
adb shellto fix the problem that blocksadb shell - This is a chicken-and-egg problem
Why "just use ADB" suggestions are WRONG:
- ❌ "Use adb shell to modify the script" - ADB port is blocked by the script!
- ❌ "Use adb to push files" - Cannot connect via ADB at all
- ❌ "Flash with fastboot" - Bootloader is locked, fastboot not accessible
- ❌ "Root the device first" - Cannot root without ADB/fastboot access
The ONLY solution: Create a modified OTA update package that the car will accept via USB stick, which contains the patched firewall script that removes the ADB blocks.
- ✅ Extracted OTA package (R67 firmware) using payload-dumper-go
- ✅ Decompressed payload.bin to partition images (system.img, boot.img, vendor.img)
- ✅ Extracted system.img filesystem
- ✅ Modified
/system/bin/arp_update.sh- commented out 8 firewall rules - ✅ Created new modified system.img (1.9GB sparse format) with patched script
- ✅ Set up WSL2 on Windows (Ubuntu 24.04) with 400GB space on E: drive
- ✅ Built complete AOSP Android 9 source tree (200GB+)
- ✅ Compiled delta_generator and all OTA tools
- ✅ Generated modified payload.bin from patched system.img
- ✅ Fixed ALL metadata format issues:
- checksoc.txt: Exactly 20 bytes (no extra newline)
- payload_properties.txt: Base64 hash format (not hex)
- ota-property-files: Excludes checksoc.txt and otacert (matches original)
- ota-streaming-property-files: Includes metadata field at end
- ✅ Created multiple OTA packages with correct structure and signatures
Car STILL rejects the modified OTA package despite all metadata being correct and matching the original format exactly. All visible format checks pass:
- ✅ File structure matches
- ✅ Metadata offsets correct
- ✅ Signature valid (AOSP test keys)
- ✅ All file sizes and checksums correct
Hypothesis: The payload.bin itself may have issues:
- Partition sizes might be incorrect
- Delta operations may be malformed
- Manifest proto format may have subtle differences
- Recovery may be validating payload internals we haven't checked yet
Since all metadata checks pass but car still rejects the OTA, we need to investigate the payload.bin file itself:
-
Extract and Examine Payload Manifest:
- Use
delta_generatoror Python script to extract the manifest protobuf - Compare manifest structure with original payload.bin
- Check partition list, operations, and signatures
- Use
-
Validate Partition Compatibility:
- Check if modified partition sizes match expected sizes
- Verify partition signatures/checksums
- Ensure target partition versions are compatible
-
Compare Delta Operations:
- Extract operation lists from both payloads
- Check if delta operations reference correct partition offsets
- Verify install operations are valid
-
Alternative Approaches if Payload is Broken:
- Try full partition images instead of delta operations
- Use different delta_generator flags
- Consider modifying original payload.bin directly instead of regenerating
Location: /Users/adam/Downloads/1300 SWI68 R67/ota_modification_workspace/final_ota_with_checksoc.sh
Purpose: Automated OTA package builder with all metadata fixes applied
Status: Fully functional, generates properly structured OTA packages
Key Features:
- Extracts proper base64 hashes from payload.bin using brillo_update_payload.py
- Creates exact 20-byte checksoc.txt using printf
- Iterative metadata offset calculation (handles size convergence)
- Excludes checksoc.txt/otacert from ota-property-files (car requires this)
- Includes metadata field in ota-streaming-property-files (critical)
- Signs with AOSP test keys matching original
- Comprehensive verification at end
Usage:
cd /Users/adam/Downloads/1300\ SWI68\ R67/ota_modification_workspace
./final_ota_with_checksoc.sh
# Output: usb_ota_update_TRULY_FINAL.zipPrerequisites:
- payload.bin must exist in ~/aosp/ota_build/payload.bin (WSL path)
- All original files in ota_signed_extract/: care_map.txt, compatibility.zip, metadata, otacert, checksoc.txt
- Python 2.7 and 3.11 available
- Java 11 with signapk.jar configured
Script Breakdown (418 lines):
# Step 0: Extract payload_properties.txt with correct base64 hashes
cd ~/aosp
python3 build/tools/releasetools/brillo_update_payload.py properties \
ota_build/payload.bin > payload_properties.txt
# Step 0b: Create checksoc.txt with EXACTLY 20 bytes
printf "SWI68-29958-1300R67\n" > checksoc.txt
# Step 1: Create dummy metadata for iteration
cat > metadata << 'EOF'
ota-property-files=payload_metadata.bin:0:88046,payload.bin:0:918571585,...
ota-streaming-property-files=payload.bin:0:918571585,...,metadata:0:602
EOF
# Step 2: Iterative convergence loop (max 5 iterations)
for i in 1 2 3 4 5; do
# Create unsigned ZIP with current metadata
cd ~/aosp/ota_signed_extract
zip -X ../usb_ota_update_unsigned.zip ./*
# Calculate actual byte offsets from ZIP
python3 << 'PYTHON'
import zipfile
with zipfile.ZipFile('usb_ota_update_unsigned.zip', 'r') as z:
for info in z.infolist():
print(f"{info.filename}:{info.header_offset}:{info.file_size}")
PYTHON
# Update metadata with new offsets
# Check if metadata size changed (indicates need for another iteration)
if [ "$old_size" == "$new_size" ]; then
echo "Converged after $i iterations"
break
fi
done
# Step 3-7: Sign, extract, recalculate post-signing offsets, reassemble, sign again
java -jar signapk.jar -w testkey.x509.pem testkey.pk8 \
usb_ota_update_unsigned.zip usb_ota_update_signed.zip
# Step 8: Verification
echo "Verifying all offsets match metadata..."
# Checks each file's offset matches metadata valuesCritical Metadata Format (Fixed):
ota-property-files=payload_metadata.bin:6809:88046,payload.bin:6809:918571585,
payload_properties.txt:918578452:154,care_map.txt:719:217,
compatibility.zip:1057:5705,metadata:69:602
ota-streaming-property-files=payload.bin:6809:918571585,
payload_properties.txt:918578452:154,care_map.txt:719:217,
compatibility.zip:1057:5705,metadata:69:602
NOTE: This script generates valid OTA packages, but the car still rejects them. The problem appears to be inside payload.bin, not the package structure.
WSL (Windows) Workspace: ~/aosp/ (Ubuntu 24.04 on E: drive)
~/aosp/
├── build/ # AOSP build system
│ └── tools/releasetools/
│ ├── ota_from_target_files.py
│ ├── brillo_update_payload.py
│ └── signapk.jar
├── out/host/linux-x86/bin/
│ └── delta_generator # OTA payload generator
├── ota_build/
│ ├── payload.bin # ⭐ Modified payload (918MB, UNDER INVESTIGATION)
│ ├── system.img # Modified partition with patched arp_update.sh
│ ├── boot.img # Unmodified
│ └── vendor.img # Unmodified
└── ota_signed_extract/ # Final OTA package files
├── payload.bin → ../ota_build/payload.bin
├── payload_properties.txt # Base64 hashes (✅ CORRECT)
├── checksoc.txt # 20 bytes: "SWI68-29958-1300R67\n" (✅ CORRECT)
├── care_map.txt # Partition verification map
├── compatibility.zip # Device compatibility checks
├── otacert # AOSP test certificate
└── META-INF/com/android/
└── metadata # Streaming offsets (✅ CORRECT)
Mac Workspace: /Users/adam/Downloads/1300 SWI68 R67/ota_modification_workspace/
ota_modification_workspace/
├── final_ota_with_checksoc.sh # ⭐ Working OTA builder script (418 lines)
├── usb_ota_update_TRULY_FINAL.zip # Generated OTA (876MB, CAR REJECTS)
├── ota_comparison/
│ ├── original/ # Extracted original OTA for comparison
│ │ ├── payload.bin # Original (952MB)
│ │ ├── payload_properties.txt
│ │ ├── checksoc.txt
│ │ ├── care_map.txt
│ │ └── META-INF/com/android/metadata
│ └── new/ # Extracted generated OTA
│ ├── payload.bin # Modified (918MB, 34MB smaller)
│ ├── payload_properties.txt
│ ├── checksoc.txt
│ └── META-INF/com/android/metadata
└── modified_ota_final/
├── system.img # Modified partition (1.9GB)
└── system_mount/system/bin/
├── arp_update.sh # ⭐ MODIFIED - 8 lines commented
└── arp_update.sh.original # Backup
Original OTA: /Users/adam/Downloads/1300 SWI68 R67/AVN_MPU/usb_ota_update.zip (909MB)
Despite fixing all 4 critical metadata issues, the car's recovery still rejects the OTA. This means the validation failure occurs inside the payload.bin processing, not at the package level.
What We Fixed (All Confirmed Working):
- ✅ checksoc.txt format (20 bytes exactly)
- ✅ payload_properties.txt hashes (base64 format)
- ✅ ota-property-files structure (excludes checksoc/otacert)
- ✅ ota-streaming-property-files structure (includes metadata)
What Remains to Check (Payload Internals):
The payload.bin file has a protobuf manifest at the beginning that describes the update:
# Extract manifest from payload (WSL)
cd ~/aosp
python3 build/tools/releasetools/brillo_update_payload.py info \
ota_build/payload.bin > payload_manifest.txt
# Compare with original
python3 build/tools/releasetools/brillo_update_payload.py info \
/path/to/original/payload.bin > original_manifest.txt
diff -u original_manifest.txt payload_manifest.txtLook for:
- Partition list differences (system, boot, vendor)
- Partition sizes mismatches
- Operation counts (REPLACE vs REPLACE_BZ, SOURCE_COPY)
- Signature differences
- Manifest hash differences
The car's recovery validates partition sizes and versions:
# Check partition sizes in manifest
grep -E "partition_name|new_partition_info" payload_manifest.txt
# Expected sizes (from original OTA):
# system: ~2.5GB
# boot: ~32MB
# vendor: ~512MBPossible Issues:
- Modified system.img is wrong size (should be exactly 2621440000 bytes for R67)
- Sparse image not converted correctly
- Partition version/timestamp mismatch
The payload contains update operations. If delta_generator created incompatible operations:
# Count operation types
grep "type:" payload_manifest.txt | sort | uniq -c
# Original OTA should use:
# REPLACE_BZ - Compressed replacement
# ZERO - Zero out blocks
# SOURCE_COPY - Copy blocks from current partitionRed Flags:
- If modified payload has different operation types
- If operation block ranges are invalid
- If operations reference non-existent source blocks
The payload.bin has internal signatures that the recovery verifies:
# Check signature information
grep -A 10 "signatures_size" payload_manifest.txtPossible Issues:
- Payload signed with wrong keys
- Signature offset incorrect
- Hash tree descriptor mismatch
The manifest may contain device-specific checks:
# Look for device fingerprints
grep -E "pre-device|post-device|fingerprint" payload_manifest.txtExpected:
pre-device: mt2712_saic_eh32- No
pre-buildfield (this is a FULL OTA, not incremental)
If payload regeneration is broken, try modifying the original payload.bin:
# Extract original payload partitions
cd ~/aosp
python3 build/tools/releasetools/brillo_update_payload.py extract \
/path/to/original/payload.bin extracted_partitions/
# Replace system.img with modified one
cp ~/ota_build/system.img extracted_partitions/system.img
# Repack payload (if tool exists)
# This may not be possible - delta_generator doesn't have a "repack" modeVerify that delta_generator created a FULL OTA, not an incremental delta:
# Check if payload has "old_partition_info" fields
grep "old_partition_info" payload_manifest.txt
# Should be EMPTY for full OTA
# If present, delta_generator incorrectly made an incremental updateWSL (Ubuntu 24.04):
delta_generator- Create new payloadsbrillo_update_payload.py- Extract manifest, verify signaturespayload_info.py- Parse protobuf manifestota_from_target_files.py- Generate complete OTA packages
Commands:
# Extract full manifest details
python3 build/tools/releasetools/brillo_update_payload.py info payload.bin
# Verify payload signature
python3 build/tools/releasetools/brillo_update_payload.py verify payload.bin
# Extract partition images from payload
python3 build/tools/releasetools/brillo_update_payload.py extract payload.bin output_dir/Current Progress:
- ✅ WSL2 installed (Ubuntu 24.04)
- ✅ Moved to E: drive (400GB available)
- ✅ SSH access configured
- ✅ All build dependencies installed
- ✅ AOSP 9.0.0_r61 downloaded and built (~200GB)
- ✅ delta_generator compiled successfully
- ✅ Modified payload.bin generated
- ✅ All metadata format issues fixed
⚠️ Car still rejects OTA - payload.bin investigation required
Requirements Met:
- ✅ ~400GB free disk space on E: drive
- ✅ Ubuntu 24.04 (WSL2 on Windows)
- ✅ 16GB+ RAM
- ✅ Fast SSD
Steps Completed:
Steps Completed:
# Updated for Ubuntu 24.04
sudo apt-get update
sudo apt-get install -y \
git-core gnupg flex bison build-essential zip curl \
zlib1g-dev libc6-dev-i386 libncurses5 libncurses5-dev \
x11proto-core-dev libx11-dev libgl1-mesa-dev \
libxml2-utils xsltproc unzip fontconfig \
python3 python3-pip gcc g++ make
pip3 install --user protobuf==3.20.3 --break-system-packagesmkdir -p ~/bin
echo 'export PATH=~/bin:$PATH' >> ~/.bashrc
source ~/.bashrc
curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo
chmod a+x ~/bin/repo
git config --global user.email "adam@example.com"
git config --global user.name "Adam"mkdir ~/aosp
cd ~/aosp
repo init -u https://android.googlesource.com/platform/manifest -b android-9.0.0_r61
repo sync -c -j8 # Took 4-8 hours, downloaded ~200GBcd ~/aosp
source build/envsetup.sh
lunch aosp_arm64-eng
make delta_generator -j$(nproc)
# Binary created at: out/host/linux-x86/bin/delta_generatorTransferred modified system.img from Mac to WSL, then generated payload.bin:
cd ~/aosp
# Created target_files structure
mkdir -p ota_build
cp ~/modified_ota_final/system.img ota_build/
cp ~/modified_ota_final/boot.img ota_build/
cp ~/modified_ota_final/vendor.img ota_build/
# Generated payload.bin using delta_generator
out/host/linux-x86/bin/delta_generator \
--partition_names=system,boot,vendor \
--new_partitions=ota_build/system.img,ota_build/boot.img,ota_build/vendor.img \
--out_file=ota_build/payload.bin
# Result: payload.bin (918MB) with modified system partitionUsed final_ota_with_checksoc.sh script to create complete OTA package:
# On Mac, transferred files back from WSL
# Ran final_ota_with_checksoc.sh with all metadata fixes
cd /Users/adam/Downloads/1300\ SWI68\ R67/ota_modification_workspace
./final_ota_with_checksoc.sh
# Generated: usb_ota_update_TRULY_FINAL.zip (876MB)
# Contains: payload.bin, payload_properties.txt, checksoc.txt,
# care_map.txt, compatibility.zip, otacert, metadataInstallation steps ready, but car rejects OTA:
Copyusb_ota_update_TRULY_FINAL.zipto USB stick rootInsert USB into carNavigate to Settings → System → Software Update → USB UpdateCar will install modified OTARebootNOW ADB will work:adb connect car-ip:5555
Current Issue: Car rejects OTA during recovery validation - need to investigate payload.bin internals.
# /system/bin/arp_update.sh (lines 11-15, 23-26 commented out)
### ADB BLOCK REMOVED - Network ADB now enabled ###
# iptables -I INPUT -p tcp --dport 5555 -j DROP
# iptables -I OUTPUT -p tcp --sport 5555 -j DROP
### CARPLAY WIRELESS BLOCK REMOVED ###
# iptables -I INPUT -p tcp --dport 7000 -j DROP
# iptables -I INPUT -p udp --dport 5353 -j DROPGoogle's OTA tools are NOT standalone - they require:
- Full AOSP source tree (~200GB)
- Complete build environment with all dependencies compiled
- Generated protobuf Python modules (apex_manifest, ota_metadata_pb2, care_map_pb2)
- Compiled C++ binaries (delta_generator, img2simg, simg2img, bsdiff, etc.)
- Build configuration files and Android.bp (Soong) build system
Attempted approaches that failed:
- ❌ Clone only build + update_engine repos → missing dependencies
- ❌ Use brillo_update_payload script → needs delta_generator binary
- ❌ Use ota_from_target_files.py directly → needs protobuf modules from full build
- ❌ Compile delta_generator standalone → uses Android.bp, needs full AOSP tree
- ❌ Find pre-built delta_generator → none available for Android 9 Pie
The ONLY solution: Build full AOSP source tree (Android 9 to match MG4 firmware).
❌ "Just use adb shell to modify the script"
- ADB port is blocked by iptables
- Cannot connect via ADB
❌ "Use adb push to replace the file"
- ADB is blocked, cannot push anything
❌ "Flash with fastboot"
- Bootloader is locked
- No unlock method available
❌ "Root the device first"
- Cannot root without ADB or fastboot
- Catch-22 situation
❌ "Use an init.d script to override firewall"
- Cannot install init.d script without ADB or root
- /system is read-only
❌ "Fix the checksoc.txt format" (ALREADY DONE)
- Fixed to exactly 20 bytes with printf
- Not the issue
❌ "Fix the payload_properties.txt hashes" (ALREADY DONE)
- Switched from hex to base64 format
- Not the issue
❌ "Fix the metadata structure" (ALREADY DONE)
- Excluded checksoc.txt/otacert from ota-property-files
- Added metadata to ota-streaming-property-files
- All metadata validated correct
- Not the issue
✅ THE ONLY SOLUTION:
- Investigate payload.bin internals (manifest, signatures, partition compatibility)
- Fix whatever is causing the car's recovery to reject the payload
- Build modified OTA package with corrected payload
- Install via USB stick
- THEN ADB works
Developing a custom Android launcher for MG4 electric vehicle to replace the stock SAIC launcher with a cleaner, more focused interface showing battery level, range, time/date, and media playback.
-
Extracted APKs from multiple firmware versions:
- R40 (1100 SWI69 R40): Original firmware with
EvCharge_eh32_ll.apk - R46 (1300 SWI68 R46): Intermediate version
- R67 (1300 SWI68 R67): Current firmware on user's car
- R40 (1100 SWI69 R40): Original firmware with
-
Tools Used:
payload-dumper-go- Extract payload.bin from OTA packages7zip- Extract ext4 filesystem images (system.img, vendor.img)apktool- Decompile APKs for analysis
-
Key Findings:
- EvCharge app with widgets exists in R40 only
- R46 and R67 removed the standalone EvCharge app
- Charging functionality integrated into launcher in R67
- Stock launcher:
launcher_eh32_eu_P.apk(package:com.saicmotor.hmi.launcher)
EvCharge_eh32_ll.apk (R40 only):
- Two widgets identified:
EVAppWidget2x1- 2x1 rectangular battery widget (500x250dp)- Shows: "Battery" title, battery icon, range (km), battery percentage
- Layout:
res/layout-w1778dp/ev_app_widget_2x1.xml - Config:
res/xml/ev_app_widget2x1_info.xml - Background: Color-coded (green/yellow/red)
EVAppWidgetIcon1x1- 1x1 compact icon (250x250dp)- Layout:
res/layout-w1778dp/ev_app_widget_icon1x1.xml - Config:
res/xml/ev_app_widget_icon1x1_info.xml
- Layout:
R67 Launcher Analysis:
- Vehicle data accessed via:
com.saicmotor.telematics.VehicleService - Key classes found in decompiled launcher:
VehicleStatusManager- Manages vehicle stateVehicleChargingCallback- Receives charging updatesChargingViewModel- Business logicChargingFragment- UI display- Layout:
fragment_energy_charging.xml&function_card_energy.xml
Created Complete Android Project:
- Location:
CustomLauncher/(current directory) - Package:
com.custom.launcher - Target: Android 9+ (API 28+)
Project Structure:
CustomLauncher/
├── app/
│ ├── build.gradle
│ └── src/main/
│ ├── AndroidManifest.xml
│ ├── java/com/custom/launcher/
│ │ ├── MainActivity.java
│ │ └── service/
│ │ ├── VehicleDataService.java
│ │ └── MediaListenerService.java
│ └── res/
│ ├── layout/activity_main.xml
│ ├── drawable/*.xml (icons)
│ └── values/*.xml (colors, strings, styles)
├── build.gradle
├── settings.gradle
└── README.md
Features Implemented:
- ✅ Battery & Range Display (color-coded card)
- ✅ Time & Date Widget (auto-updating)
- ✅ Now Playing Card (media info + controls)
- ✅ Heated Seats & Steering Wheel Controls (with vehicle sync)
- ✅ App Shortcuts Grid
- ✅ Vehicle Service Binding (with mock fallback)
- ✅ Media Session Integration
- ✅ Dark Theme UI
- ✅ Debug Dialog (triple-tap clock to view live logs)
CRITICAL DISCOVERY: SAIC SDK classes are NOT in the system classloader - they are packaged in the stock launcher APK.
Working Implementation (tested on car 28 Jan 2026):
// Step 1: Load SDK from launcher package context
Context launcherContext = context.createPackageContext(
"com.saicmotor.hmi.launcher",
Context.CONTEXT_INCLUDE_CODE
);
ClassLoader launcherClassLoader = launcherContext.getClassLoader();
// Step 2: Load VehicleChargingManager class via reflection
Class<?> managerClass = launcherClassLoader.loadClass(
"com.saicmotor.sdk.vehiclesettings.manager.VehicleChargingManager"
);
// Step 3: Create dynamic proxy for IVehicleServiceListener (MUST NOT BE NULL)
Class<?> listenerInterface = launcherClassLoader.loadClass(
"com.saicmotor.sdk.vehiclesettings.listener.IVehicleServiceListener"
);
Object listenerProxy = Proxy.newProxyInstance(
launcherClassLoader,
new Class<?>[] { listenerInterface },
(proxy, method, args) -> {
if ("onServiceConnected".equals(method.getName())) {
// Save manager instance and read data
managerInstance = args[0];
readVehicleData();
}
return null;
}
);
// Step 4: Initialize SDK with listener proxy
Method initMethod = managerClass.getMethod(
"init",
Context.class,
listenerInterface,
Long.TYPE
);
initMethod.invoke(null, context, listenerProxy, 1500L);
// Step 5: Get singleton instance
Method getInstanceMethod = managerClass.getMethod("getInstance");
Object manager = getInstanceMethod.invoke(null);
// Step 6: Get vehicle data bean
Method getStatusMethod = managerClass.getMethod("getVehicleChargingStatus");
Object chargingBean = getStatusMethod.invoke(manager);
// Step 7: Extract battery percentage (CORRECT METHOD)
Method getBatteryMethod = chargingBean.getClass().getMethod("getCurrentElectricQuantity");
Float batteryPercent = (Float) getBatteryMethod.invoke(chargingBean);
int batteryLevel = Math.round(batteryPercent); // Convert 78.5 → 79
// Step 8: Extract range in kilometers
Method getRangeMethod = chargingBean.getClass().getMethod("getCurrentEnduranceMileage");
Integer rangeKm = (Integer) getRangeMethod.invoke(chargingBean);Key SDK Classes and Methods (from com.saicmotor.hmi.launcher APK):
-
VehicleChargingManager
init(Context, IVehicleServiceListener, long)- Initialize SDK (listener MUST NOT be null)getInstance()- Get singleton instancegetVehicleChargingStatus()- Returns VehicleChargingBean
-
VehicleChargingBean (data container)
- ✅
getCurrentElectricQuantity()→ Float - Battery percentage (e.g., 78.5) - ✅
getCurrentEnduranceMileage()→ Integer - Range in kilometers - ❌
getElectricityLevel()→ Integer - NOT battery percentage (different metric)
- ✅
-
IVehicleServiceListener (callback interface)
onServiceConnected(BaseManager)- Called when service binds successfullyonServiceDisconnected()- Called when service unbinds
Common Pitfalls to Avoid:
- ❌ Using
Class.forName()- SDK classes not in app classloader - ❌ Using
ClassLoader.getSystemClassLoader()- SDK not in system classpath - ❌ Passing
nulllistener toinit()- Causes NullPointerException crash - ❌ Using
getElectricityLevel()for battery - Returns wrong metric (not percentage) - ✅ Use
createPackageContext()to access launcher's classloader - ✅ Use dynamic
Proxy.newProxyInstance()for listener interface - ✅ Use
getCurrentElectricQuantity()for battery percentage
Tested on car: 28 Jan 2026 - Album art successfully loads from Bluetooth storage
Implementation (MediaListenerService.java):
// Bluetooth stores album art in specific directory structure
// Path: /storage/emulated/0/bluetooth/[MAC_ADDRESS]/AVRCP_BIP_IMG_*.JPEG
// Step 1: Extract URI from media metadata
String albumArtUriString = metadata.getString(MediaMetadata.METADATA_KEY_ART_URI);
if (albumArtUriString == null || albumArtUriString.isEmpty()) {
albumArtUriString = metadata.getString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI);
}
// Step 2: Decode URI (handles %20 spaces, etc.)
String decodedPath = URLDecoder.decode(albumArtUriString, "UTF-8");
// Step 3: Handle Bluetooth MAC format (colons vs encoded)
// Bluetooth paths may have "AA:BB:CC:DD:EE:FF" or "AA%3ABB%3ACC..."
decodedPath = decodedPath.replaceAll(": ", ":"); // Normalize spacing
// Step 4: Extract file path from URI
Uri albumArtUri = Uri.parse(decodedPath);
String filePath = albumArtUri.getPath(); // e.g., "/storage/emulated/0/bluetooth/..."
// Step 5: Use ContentResolver to open file (avoids permission issues)
try {
InputStream inputStream = getContentResolver().openInputStream(albumArtUri);
Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
inputStream.close();
// Update UI with bitmap
if (listener != null) {
listener.onAlbumArtChanged(bitmap);
}
} catch (IOException e) {
Log.e(TAG, "Failed to load album art: " + e.getMessage());
}Required Permissions (AndroidManifest.xml):
<!-- Bluetooth permissions -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!-- Storage permissions for album art access -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<!-- Notification listener for media metadata -->
<uses-permission android:name="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE" />
<!-- Legacy storage access for Android 10+ -->
<application
android:requestLegacyExternalStorage="true">Grant Permissions via ADB (if not auto-granted):
adb shell pm grant com.custom.launcher android.permission.READ_EXTERNAL_STORAGE
adb shell pm grant com.custom.launcher android.permission.WRITE_EXTERNAL_STORAGEBluetooth Album Art File Structure:
/storage/emulated/0/bluetooth/
├── AA_BB_CC_DD_EE_FF/ # Bluetooth MAC address (underscores)
│ ├── AVRCP_BIP_IMG_001.JPEG # Current track album art
│ ├── AVRCP_BIP_IMG_002.JPEG # Previous/next track art
│ └── ...
Key Points:
- Use
ContentResolver.openInputStream()instead of direct file access - Decode URI with
URLDecoder.decode()to handle encoded characters - Bluetooth MAC addresses may use colons or underscores in path
- AVRCP (Audio/Video Remote Control Profile) handles album art transfer
- BIP (Basic Imaging Profile) is the Bluetooth protocol for image transfer
- Album art updates automatically when track changes
- Gradle build system
- Min SDK: 28 (Android 9)
- Target SDK: 33
- Dependencies: AndroidX, Material Design, ConstraintLayout, CardView
- MainActivity.java - Main launcher logic, updates UI with vehicle/media data
- VehicleDataService.java - Binds to SAIC vehicle service for battery/range
- MediaListenerService.java - NotificationListenerService for media tracking
- activity_main.xml - Material Design layout with cards
- Java JDK 17:
brew install openjdk@17 - Android SDK tools:
brew install --cask android-commandlinetools - Gradle (included in project)
# From the CustomLauncher directory
./gradlew assembleDebug # Build debug APK
./gradlew assembleRelease # Build release APK# Set correct display settings (160 dpi for MG4)
adb shell wm density 160 && adb shell wm size 1920x1080
# Install and launch
adb install -r app/build/outputs/apk/debug/app-debug.apk
adb shell am start -n com.custom.launcher/.MainActivity
# Check logs for errors
adb logcat | grep -E "custom.launcher|AndroidRuntime"# Connect via ADB to car's Android system
adb devices
adb install -r app/build/outputs/apk/debug/app-debug.apk
# Set as default launcher
# Settings → Apps → Default Apps → Home app → Custom LauncherAll firmware files are located in the mg_firmware/ directory within this repository:
- R40:
mg_firmware/1100 SWI69 R40 11950861 13.06.25/ - R46:
mg_firmware/1300 SWI68 R46/ - R67:
mg_firmware/1300 SWI68 R67/
Note: The mg_firmware/ directory is git-ignored (except for .gitkeep) to keep firmware files out of version control.
- R40:
mg_firmware/1100 SWI69 R40 11950861 13.06.25/extracted_apks/EvCharge_eh32_ll.apk✅ (has widgets) - R46:
mg_firmware/1300 SWI68 R46/extracted_apks/❌ (no EvCharge) - R67:
mg_firmware/1300 SWI68 R67/extracted_apks/launcher_eh32_eu_P.apk(current system launcher)
- EvCharge:
mg_firmware/1100 SWI69 R40 11950861 13.06.25/extracted_apks/EvCharge_eh32_ll/ - R67 Launcher:
mg_firmware/1300 SWI68 R67/extracted_apks/launcher_eh32_eu_P/
-
Vehicle Service Access ✅ SOLVED
May require system-level permissions- No special permissions neededAPK might need platform signature- Regular APK works fineConsider installing as system app- User app installation works- Solution: Use
createPackageContext()to access launcher's SDK classes
-
Testing Limitations ✅ PARTIALLY SOLVED
- Cannot fully test vehicle data on Mac/emulator (still true)
- Mock data included for development (fallback works well)
- Direct deployment to car recommended (confirmed working)
- Debug Dialog: Triple-tap clock to view logs on car without ADB
-
Signature Requirements ✅ NOT NEEDED
System apps may need platform certificate- Not requiredMay need to extract signing keys- Not required- Confirmed: Regular debug APK works perfectly on car
-
New Challenge: Display Resolution
⚠️ IMPORTANT- Car display is 1778×720 NOT 1920×1080 as initially documented
- Use
layout-w1778dp/qualifier for car-specific layouts - Test on car, not emulator, for accurate UI sizing
- Build the project:
./gradlew assembleDebug - Enable ADB debugging on car
- Connect Mac to car via USB
- Install and test:
adb install -r app/build/outputs/apk/debug/app-debug.apk
- Add AIDL interface for vehicle service (if specifications available)
- Implement actual media control commands
- Add more customization options
- Extract and use actual platform keys if system permissions needed
- Consider creating widget version if launcher widgets are supported
- Analyze logcat output:
adb logcat | grep VehicleData - Check available services:
adb shell service list | grep vehicle - May need to reverse engineer AIDL interface from stock launcher
- Alternative: Monitor system broadcasts for battery/charging events
MG4 Infotainment Display:
- Resolution: 1920×1080 pixels (Full HD)
- Density: 160 dpi (mdpi)
- Width: 1778+ dp (layout-w1778dp qualifier found in R40 widgets)
- Orientation: Landscape (forced)
- Aspect Ratio: 16:9
Widget Dimensions (from EvCharge R40):
- 2×1 Battery Widget: 500×250 dp
- 1×1 Icon Widget: 250×250 dp
Extracted from:
vendor/build.prop:ro.sf.lcd_density=160- EvCharge layouts:
res/layout-w1778dp/
Android Studio AVD Config:
- Resolution: 1920 × 1080 pixels
- Density: 160 dpi (mdpi)
- API Level: 28 (Android 9)
- Orientation: Landscape
- Model: MG4 (1100 model, older version)
- Current Firmware: R67 (1300 SWI68 R67)
- Current Launcher:
com.saicmotor.hmi.launcher(SaicLoader) - Has charging display showing battery % and range
- No widget support in current firmware
- Platform: macOS
- Preferred IDE: VS Code (Android Studio for emulator testing)
- Build Tool: Gradle command line
- Testing: Android Automotive emulator + Direct deployment to vehicle via ADB
- Android SDK:
/Users/adam/Library/Android/sdk
CRITICAL DISCOVERY (30 Jan 2026): All MG4 system apps are signed with publicly available AOSP platform test keys, eliminating the need for root access for most system modifications!
Certificate Details (verified with keytool):
- Serial Number:
b3998086d056cffa - SHA1 Fingerprint:
27:19:6E:38:6B:87:5E:76:AD:F7:00:E7:EA:84:E4:C6:EE:E3:3D:FA - Owner:
android@android.com - Verified APKs: launcher_eh32_eu_P.apk, vehiclesettings_eh32_eu_P.apk, hvac_eh32_eu_P.apk, systemsettings_eh32_eu_P.apk, AllgoCarplay_EH32.apk, AAP_EH32.apk
Public Key Location:
- AOSP Repository:
https://android.googlesource.com/platform/build/+/refs/heads/master/target/product/security/ - Files:
platform.pk8(private key),platform.x509.pem(certificate)
Implications:
- ✅ We can sign custom apps with system-level privileges WITHOUT root
- ✅ Can modify system apps (AllgoCarplay, launcher) and re-sign them
- ✅ Can create system-level apps with
android:sharedUserId="android.uid.system" - ✅ Wireless CarPlay modifications possible without root
⚠️ This is a major security vulnerability in SAIC firmware
Documentation:
- AOSP Keys Discovery:
docs/AOSP_TEST_KEYS_DISCOVERY.md(comprehensive analysis) - CarPlay USB Analysis:
docs/CARPLAY_USB_ANALYSIS.md(how AllgoCarplay works) - Wireless CarPlay Feasibility:
docs/WIRELESS_CARPLAY_FEASIBILITY.md(implementation paths) - AllgoCarplay Modification:
docs/ALLGOCARPLAY_MODIFICATION_ANALYSIS.md(system app modification guide)
Previous understanding: The MG4 infotainment system is NOT rooted, which restricts modifications.
What We Previously Thought We COULDN'T Do:
- ❌ Replace system apps → Now possible with platform signing
- ❌ Modify
/systempartition → Still read-only, but can replace system apps - ❌ Install APKs with system signature → Now possible with AOSP test keys
- ❌ Access protected APIs → Now possible with platform signing
- ❌ Enable wireless CarPlay → Now possible with AllgoCarplay modification
Current Approach (No Root Required):
- ✅ Custom launcher runs as user app (still works fine)
- ✅ Uses reflection to access SAIC SDK from launcher/HVAC packages
- ✅ Can launch system activities via Intent
- ✅ NEW: Can create system-level apps with platform signing
- ✅ NEW: Can modify and re-sign system apps like AllgoCarplay
- Full rooting guide:
docs/ROOTING_GUIDE.md(if you still want root) - System info script:
scripts/gather_system_info.sh - Backup script:
scripts/backup_firmware.sh
- Download AOSP platform keys from repository
- Sign custom launcher with
platform.pk8and addandroid:sharedUserId="android.uid.system" - Test system-level access (vehicle data, media control, settings)
- Modify AllgoCarplay for wireless support
- Re-sign modified AllgoCarplay with platform keys
- Install modified system apps via ADB (no root needed)
NOTE: Rooting is now optional for most modifications. Platform signing provides system privileges without the risks of rooting.
Build completed: 24 Jan 2026
APK Location: app/build/outputs/apk/debug/app-debug.apk
Features Added:
- ✅ Triple-tap clock to open debug dialog
- ✅ Live log viewer with auto-refresh (1 second intervals)
- ✅ Filters logs for custom launcher, vehicle service, and media service
- ✅ Clear logs button
- ✅ Auto-scrolls to show latest logs (only when new content arrives)
- ✅ Scrollable and selectable text
- ✅ Maximum 500 lines retained in buffer
- ✅ Wider dialog (1200px height) for better readability
- ✅ Auto-retry vehicle service binding every 10 seconds if connection fails
- ✅ Save logs to USB stick button (searches multiple mount points)
Testing completed on car: 28 Jan 2026 Status: All features working perfectly
Discoveries and Fixes:
-
✅ SAIC SDK Access Method Found
- SDK classes are in
com.saicmotor.hmi.launcherAPK, not system classloader - Use
createPackageContext()to load launcher's classloader - Use reflection to access VehicleChargingManager
- CRITICAL: Must provide non-null IVehicleServiceListener or app crashes
- Solution: Dynamic
Proxy.newProxyInstance()handles callbacks
- SDK classes are in
-
✅ Battery Percentage Method Corrected
- ❌ Initial:
getElectricityLevel()- Wrong method, returns different metric - ✅ Correct:
getCurrentElectricQuantity()- Returns Float battery percentage - Tested on car: Shows accurate battery level
- ❌ Initial:
-
✅ Range Data Working
- Method:
getCurrentEnduranceMileage()returns Integer kilometers - Tested on car: Shows accurate remaining range
- Method:
-
✅ Bluetooth Album Art Working
- Path:
/storage/emulated/0/bluetooth/[MAC]/AVRCP_BIP_IMG_*.JPEG - Use
ContentResolver.openInputStream()to load images URLDecoder.decode()handles URI encoding- Tested on car: Album art displays correctly for Bluetooth media
- Path:
Note: Original debug modal dialog implementation was replaced with full-screen LogViewerActivity (see Phase 10).
Date: 31 Jan 2026 Status: Production-ready signed APKs for both car and emulator
Build Artifacts:
-
Car APK (System UID):
- Location:
app/build/outputs/apk/car/debug/app-car-debug-signed.apk - Size: ~13 MB
- System Privileges: ✅ (android.uid.system)
- Direct SAIC SDK access without reflection
- Build Command:
./gradlew assembleCarDebug && ./sign_apk.sh
- Location:
-
Emulator APK (System UID):
- Location:
app/build/outputs/apk/emulator/debug/app-emulator-debug-signed.apk - Size: ~12 MB
- System Privileges: ✅ (android.uid.system)
- Mock data fallback for vehicle service
- Build Command:
./gradlew assembleEmulatorDebug && ./sign_apk.sh emulator
- Location:
Signing Scripts:
-
✅
sign_apk.sh- Feature-rich signing script (FIXED 31 Jan 2026)- Supports car/emulator flavors
- Supports debug/release build types
- Auto-detects APK paths
- Verifies signature after signing
- Shows helpful error messages
- Fixed syntax errors (unclosed quote, bash-specific syntax)
-
✅
sign_with_platform_keys.sh- Simple alternative signing script- Takes APK path as argument
- Quick and reliable
- Less features but always works
Usage:
# Build and sign car APK (default)
./gradlew assembleCarDebug && ./sign_apk.sh
# Build and sign emulator APK
./gradlew assembleEmulatorDebug && ./sign_apk.sh emulator
# Install to device
adb install -r app/build/outputs/apk/car/debug/app-car-debug-signed.apkBuild completed: 20 Jan 2026
Status:
Original Implementation:
- Modal dialog with triple-tap activation
- Auto-refreshing logs every 1 second
- Clear button and close functionality
Issues Encountered:
- Modal was flaky and hard to interact with
- Scrolling behavior was unreliable
- Logs would jump to top during refresh
- Difficult to pause and read specific entries
Resolution:
Converted to full-screen LogViewerActivity in Phase 10 with reversed log order, eliminating all scrolling issues.
Build completed: 24 Jan 2026
APK Location: app/build/outputs/apk/debug/app-debug.apk
Features Added:
- ✅ Battery card now opens Charge Management Activity
- ✅ Integrated with SAIC VehicleSettings app
- ✅ Fixed all import errors (Handler, ImageView, Context)
Implementation Details:
- Target Package:
com.saicmotor.hmi.vehiclesettings - Target Activity:
com.saicmotor.hmi.vehiclesettings.chargemanagement.ui.ChargeManagementActivity - Click Handler: Added
batteryCard.setOnClickListener()to open the activity - Intent Flags: Uses
FLAG_ACTIVITY_NEW_TASKfor proper activity launch
Charge Management Activity Features:
The stock SAIC ChargeManagementActivity includes three tabs:
- Charging Management - Charge scheduling, SOC limits, charging lock
- Discharging Management - V2L (Vehicle-to-Load) settings
- Energy - Energy flow visualization
Code Changes:
// Added in MainActivity.java onCreate()
batteryCard.setOnClickListener(v -> {
openChargeManagement();
});
// New method to launch SAIC Charge Management
private void openChargeManagement() {
try {
Intent intent = new Intent();
intent.setComponent(new ComponentName(
"com.saicmotor.hmi.vehiclesettings",
"com.saicmotor.hmi.vehiclesettings.chargemanagement.ui.ChargeManagementActivity"
));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
Log.d(TAG, "Opening Charge Management Activity");
} catch (Exception e) {
Log.e(TAG, "Failed to open Charge Management: " + e.getMessage());
}
}Testing:
- ✅ App compiles successfully
- ✅ APK installs on device
- ✅ Launcher starts without errors
- ⏳ Awaiting on-car testing to verify Charge Management opens correctly
Next Steps:
- Test on actual MG4 vehicle
- Verify battery card click opens Charge Management Activity
- Confirm all three tabs are accessible in the opened activity
- Test back navigation returns to custom launcher
Date: 30 Jan 2026 Status: Fully functional with SAIC SDK integration
Implementation:
Created HeatingControlService to interface with SAIC AirConditionManager for heated seats and steering wheel control. The service uses reflection to access the SDK classes from the HVAC app package.
Key Features:
-
✅ Bidirectional Communication
- Send heating commands to vehicle
- Receive status updates from vehicle
- Automatic UI sync when vehicle changes heating levels
-
✅ Driver & Passenger Seat Heating
- 4 levels: OFF → Level 3 (High) → Level 2 (Med) → Level 1 (Low) → OFF
- Independent control for left and right seats
- Visual feedback with different icon states
-
✅ Steering Wheel Heating
- Binary control: OFF ↔ ON
- Visual feedback with on/off icons
-
✅ Vehicle Auto-Adjustment Tracking
- Car automatically reduces seat heating after time
- Callback system updates UI to match actual vehicle state
- No desync between UI and vehicle
SAIC SDK Methods Used:
// From AirConditionManager
setDrvSeatHeatLevel(int level) // Set driver seat (0-3)
setPsgSeatHeatLevel(int level) // Set passenger seat (0-3)
setSteeringWheelHeat(int level) // Set steering wheel (0-1)
// From AirConditionBean (via callback)
getDrvSeatHeatLevel() // Get driver seat current level
getPsgSeatHeatLevel() // Get passenger seat current level
getSteeringWheelHeatLevel() // Get steering wheel current stateInitialization Pattern:
// Load SDK from HVAC app package
Context hvacContext = context.createPackageContext(
"com.saicmotor.hmi.hvac",
Context.CONTEXT_INCLUDE_CODE
);
ClassLoader hvacClassLoader = hvacContext.getClassLoader();
// Load AirConditionManager
Class<?> managerClass = hvacClassLoader.loadClass(
"com.saicmotor.sdk.vehiclesettings.manager.AirConditionManager"
);
// Initialize with listener for connection callbacks
Method initMethod = managerClass.getMethod(
"init",
Context.class,
IVehicleServiceListener.class,
Long.TYPE
);
initMethod.invoke(null, context, listenerProxy, 1500L);
// Register callback for status updates
Method registerMethod = managerInstance.getClass().getMethod(
"registerAirConditionCallback",
IAirConditionCallback.class
);
registerMethod.invoke(managerInstance, callbackProxy);Files Modified:
-
HeatingControlService.java (NEW)
- Manages connection to AirConditionManager
- Sends heating commands to vehicle
- Receives status updates via callbacks
- Similar architecture to VehicleDataService
-
MainActivity.java
- Added HeatingControlService initialization
- Connected toggle buttons to service methods
- Added listener for vehicle status updates
- Removed placeholder sendHeatingCommand()
- UI automatically updates when vehicle changes state
Testing Notes:
- Service connects successfully to HVAC system
- Heating commands sent via reflection work correctly
- Callback system receives status updates from vehicle
- UI stays in sync even when car auto-adjusts heating levels
- No permission issues (uses same pattern as VehicleDataService)
Technical Challenges Solved:
- ✅ Found correct SDK classes in HVAC app (not system)
- ✅ Implemented callback proxy for bidirectional communication
- ✅ Handled reflection for all SDK method calls
- ✅ Synchronized UI updates on main thread from callbacks
- ✅ Proper resource cleanup in onDestroy()
Known Behavior:
- Vehicle automatically reduces seat heating after ~30 minutes for safety
- HeatingControlService callback detects this and updates UI
- This is expected vehicle behavior, not a bug
Date: 31 Jan 2026 Status: Fully functional with reversed log order
Problem:
Original debug logs were displayed in a modal dialog that had multiple issues:
- Modal was flaky and hard to interact with
- Scrolling was difficult and unreliable
- Logs would randomly jump to top during auto-refresh
- No way to pause log streaming when reading specific entries
Solution Iterations:
-
Attempt 1: Converted modal to full-screen
LogViewerActivity- Created
activity_log_viewer.xmlwith ScrollView - Added pause/resume button to stop refresh temporarily
- Issue: Scroll jumping persisted during TextView updates
- Created
-
Attempt 2: Implemented scroll position tracking
- Used ScrollView listeners to detect user scroll position
- Preserved scroll position during TextView updates
- Issue: Race conditions between scroll tracking and text updates
-
Attempt 3: Smart auto-scroll detection
- Only auto-scroll when user is at bottom
- Complex logic to determine if user is reading older logs
- Issue: Still had edge cases causing jumps
-
Final Solution: Reversed log order (newest at top) ✅
- Split logs by newline, reverse array, rebuild string
- Newest logs always appear at top (default scroll position)
- Eliminated ALL scrolling logic complexity
- Removed scroll listeners and position tracking
- Simple and reliable solution
Implementation Details:
-
New Files:
LogViewerActivity.java- Full-screen log viewer activityactivity_log_viewer.xml- Layout with header buttons and scrollable log text
-
Key Features:
- ✅ Full-screen activity (replaces modal dialog)
- ✅ Pause/resume button (yellow when paused, green when running)
- ✅ Auto-refresh every 1 second when not paused
- ✅ Reversed log order (newest at top)
- ✅ Clear logs button
- ✅ Refresh logs button (force immediate update)
- ✅ Save logs to USB button
- ✅ Close button returns to launcher
- ✅ Only updates TextView when content actually changes
-
Log Reversal Code:
// Split logs into lines, reverse order, rebuild String[] lines = logText.split("\n"); StringBuilder reversed = new StringBuilder(); for (int i = lines.length - 1; i >= 0; i--) { reversed.append(lines[i]).append("\n"); } String reversedLog = reversed.toString();
-
Activation: Triple-tap the clock widget in main launcher
-
Log Filters: Shows only relevant logs:
CustomLauncher:V- Main launcher logsVehicleDataService:V- Vehicle service binding logsHeatingControlService:V- Heating control service logsMediaListenerService:V- Media service logsAndroidRuntime:E- Critical errors
Testing:
- ✅ Activity opens smoothly from triple-tap
- ✅ No scroll jumping issues (logs stay stable)
- ✅ Pause/resume button works correctly
- ✅ Color feedback (yellow/green) is clear
- ✅ Clear button wipes logs as expected
- ✅ Save to USB searches multiple mount points
- ✅ Close button properly stops log reader and cleans up
Lessons Learned:
Sometimes the simplest solution (reversing order) is better than complex scroll tracking logic. By putting newest logs at top, we eliminated the need for any scroll management code entirely.
- OTA extraction tools installed:
payload-dumper-go,p7zip,apktool,jadx - All firmware packages available in workspace
- Stock launcher decompiled and analyzed
- Custom launcher built and tested on emulator
- Wireless CarPlay Bridge project created - See
WirelessCarPlayBridge/directory
Project Location: WirelessCarPlayBridge/
Purpose: Software-based wireless CarPlay adapter for MG4
Status: ✅ Built and ready for testing on car
How It Works:
- BLE Advertisement: Broadcasts CarPlay capability (UUID: 0x0000FD8F)
- WiFi Direct: Establishes P2P connection with iPhone (5 GHz)
- Virtual USB Device: Creates USB gadget that AllgoCarplay sees as "wired iPhone"
- Data Bridge: Routes WiFi packets through virtual USB interface
Build & Install:
cd WirelessCarPlayBridge
./gradlew assembleDebug && ./sign_apk.sh
adb install -r app/build/outputs/apk/debug/app-debug-signed.apkKey Files:
WirelessCarPlayBridgeService.java- Main service coordinatorCarPlayBleAdvertiser.java- BLE advertisement (announces CarPlay)WiFiDirectManager.java- WiFi P2P connection managementUsbGadgetController.java- Virtual USB device creation (Linux ConfigFS)DataBridge.java- Bidirectional WiFi ↔ USB packet routing
Requirements:
- ✅ Bluetooth LE 4.0+ (MG4 R67 confirmed)
- ✅ System-level permissions (android.uid.system via platform signing)
- ❓ WiFi 5 GHz with P2P support (need to verify on car)
- ❓ Kernel CONFIG_USB_CONFIGFS (need to check:
adb shell ls /config/usb_gadget)
Success Probability: 40-60% (depends on USB Gadget kernel support)
Fallback: If USB Gadget doesn't work, recommend Carlinkit 3.0 adapter ($80-120, 95% success)
Documentation:
WirelessCarPlayBridge/README.md- Complete technical documentationWirelessCarPlayBridge/QUICKSTART.md- Quick start guideWirelessCarPlayBridge/sign_apk.sh- Automated APK signing with AOSP keys
- Main README:
README.md- Project overview and build instructions - Agent Context:
AGENTS.md- This file, for session resumption
- CarPlay USB Analysis:
docs/CARPLAY_USB_ANALYSIS.md- How AllgoCarplay interfaces with USB ports
- AOAP (Android Open Accessory Protocol) architecture
- USB communication flow and SAIC SDK integration
- Wireless CarPlay Feasibility:
docs/WIRELESS_CARPLAY_FEASIBILITY.md- Three implementation paths (software mod, adapter, hybrid)
- Hardware requirements (Bluetooth LE + WiFi Direct 5 GHz)
- Cost analysis and recommendations
- AllgoCarplay Modification Analysis:
docs/ALLGOCARPLAY_MODIFICATION_ANALYSIS.md- System app location:
/system/app/AllgoCarplay_EH32/(corrected) - sharedUserId requirements
- Platform signing requirements
- System app location:
- AOSP Test Keys Discovery:
docs/AOSP_TEST_KEYS_DISCOVERY.md⭐ CRITICAL- Certificate verification results (serial: b3998086d056cffa)
- Security implications (major vulnerability)
- Step-by-step signing instructions
- System-level app creation guide
- Rooting Guide:
docs/ROOTING_GUIDE.md- Comprehensive rooting procedures
- Magisk method, bootloader unlocking
- Recovery plans and backup procedures
- Note: Now optional due to AOSP keys discovery
- scripts/README.md - Script documentation
- scripts/gather_system_info.sh - Collects system info via ADB
- scripts/backup_firmware.sh - Creates partition backups