@@ -107,21 +107,183 @@ CustomLauncher/
107107
108108## Technical Details
109109
110- ### Vehicle Service Integration
110+ ### Vehicle Service Integration ✅ WORKING
111+
112+ ** CRITICAL DISCOVERY** : SAIC SDK classes are NOT in the system classloader - they are packaged in the stock launcher APK.
113+
114+ ** Working Implementation** (tested on car 28 Jan 2026):
115+
116+ ``` java
117+ // Step 1: Load SDK from launcher package context
118+ Context launcherContext = context. createPackageContext(
119+ " com.saicmotor.hmi.launcher" ,
120+ Context . CONTEXT_INCLUDE_CODE
121+ );
122+ ClassLoader launcherClassLoader = launcherContext. getClassLoader();
123+
124+ // Step 2: Load VehicleChargingManager class via reflection
125+ Class<?> managerClass = launcherClassLoader. loadClass(
126+ " com.saicmotor.sdk.vehiclesettings.manager.VehicleChargingManager"
127+ );
128+
129+ // Step 3: Create dynamic proxy for IVehicleServiceListener (MUST NOT BE NULL)
130+ Class<?> listenerInterface = launcherClassLoader. loadClass(
131+ " com.saicmotor.sdk.vehiclesettings.listener.IVehicleServiceListener"
132+ );
133+ Object listenerProxy = Proxy . newProxyInstance(
134+ launcherClassLoader,
135+ new Class<?>[] { listenerInterface },
136+ (proxy, method, args) - > {
137+ if (" onServiceConnected" . equals(method. getName())) {
138+ // Save manager instance and read data
139+ managerInstance = args[0 ];
140+ readVehicleData();
141+ }
142+ return null ;
143+ }
144+ );
145+
146+ // Step 4: Initialize SDK with listener proxy
147+ Method initMethod = managerClass. getMethod(
148+ " init" ,
149+ Context . class,
150+ listenerInterface,
151+ Long . TYPE
152+ );
153+ initMethod. invoke(null , context, listenerProxy, 1500L );
154+
155+ // Step 5: Get singleton instance
156+ Method getInstanceMethod = managerClass. getMethod(" getInstance" );
157+ Object manager = getInstanceMethod. invoke(null );
158+
159+ // Step 6: Get vehicle data bean
160+ Method getStatusMethod = managerClass. getMethod(" getVehicleChargingStatus" );
161+ Object chargingBean = getStatusMethod. invoke(manager);
162+
163+ // Step 7: Extract battery percentage (CORRECT METHOD)
164+ Method getBatteryMethod = chargingBean. getClass(). getMethod(" getCurrentElectricQuantity" );
165+ Float batteryPercent = (Float ) getBatteryMethod. invoke(chargingBean);
166+ int batteryLevel = Math . round(batteryPercent); // Convert 78.5 → 79
167+
168+ // Step 8: Extract range in kilometers
169+ Method getRangeMethod = chargingBean. getClass(). getMethod(" getCurrentEnduranceMileage" );
170+ Integer rangeKm = (Integer ) getRangeMethod. invoke(chargingBean);
171+ ```
172+
173+ ** Key SDK Classes and Methods** (from ` com.saicmotor.hmi.launcher ` APK):
174+
175+ - ** VehicleChargingManager**
176+ - ` init(Context, IVehicleServiceListener, long) ` - Initialize SDK (listener MUST NOT be null)
177+ - ` getInstance() ` - Get singleton instance
178+ - ` getVehicleChargingStatus() ` - Returns VehicleChargingBean
179+
180+ - ** VehicleChargingBean** (data container)
181+ - ✅ ` getCurrentElectricQuantity() ` → Float - Battery percentage (e.g., 78.5)
182+ - ✅ ` getCurrentEnduranceMileage() ` → Integer - Range in kilometers
183+ - ❌ ` getElectricityLevel() ` → Integer - NOT battery percentage (different metric)
184+
185+ - ** IVehicleServiceListener** (callback interface)
186+ - ` onServiceConnected(BaseManager) ` - Called when service binds successfully
187+ - ` onServiceDisconnected() ` - Called when service unbinds
188+
189+ ** Common Pitfalls to Avoid** :
190+
191+ 1 . ❌ Using ` Class.forName() ` - SDK classes not in app classloader
192+ 2 . ❌ Using ` ClassLoader.getSystemClassLoader() ` - SDK not in system classpath
193+ 3 . ❌ Passing ` null ` listener to ` init() ` - Causes NullPointerException crash
194+ 4 . ❌ Using ` getElectricityLevel() ` for battery - Returns wrong metric (not percentage)
195+ 5 . ✅ Use ` createPackageContext() ` to access launcher's classloader
196+ 6 . ✅ Use dynamic ` Proxy.newProxyInstance() ` for listener interface
197+ 7 . ✅ Use ` getCurrentElectricQuantity() ` for battery percentage
198+
199+ ### Bluetooth Album Art Integration ✅ WORKING
200+
201+ ** Tested on car** : 28 Jan 2026 - Album art successfully loads from Bluetooth storage
202+
203+ ** Implementation** (MediaListenerService.java):
111204
112205``` java
113- // Correct service binding (extracted from R67 launcher decompiled code)
114- VEHICLE_SERVICE_PACKAGE = " com.saicmotor.service.vehicle"
115- VEHICLE_SERVICE_ACTION = " com.saicmotor.service.vehicle.VehicleService"
116-
117- // Key SDK classes used by launcher:
118- // - com.saicmotor.sdk.vehiclesettings.manager.VehicleChargingManager
119- // - Available methods:
120- // - getElectricityLevel()I - Battery level (SOC)
121- // - getCurrentEnduranceMileage()I - Current range in km
122- // - getVehicleChargingStatus() - Full charging bean
206+ // Bluetooth stores album art in specific directory structure
207+ // Path: /storage/emulated/0/bluetooth/[MAC_ADDRESS]/AVRCP_BIP_IMG_*.JPEG
208+
209+ // Step 1: Extract URI from media metadata
210+ String albumArtUriString = metadata. getString(MediaMetadata . METADATA_KEY_ART_URI );
211+ if (albumArtUriString == null || albumArtUriString. isEmpty()) {
212+ albumArtUriString = metadata. getString(MediaMetadata . METADATA_KEY_ALBUM_ART_URI );
213+ }
214+
215+ // Step 2: Decode URI (handles %20 spaces, etc.)
216+ String decodedPath = URLDecoder . decode(albumArtUriString, " UTF-8" );
217+
218+ // Step 3: Handle Bluetooth MAC format (colons vs encoded)
219+ // Bluetooth paths may have "AA:BB:CC:DD:EE:FF" or "AA%3ABB%3ACC..."
220+ decodedPath = decodedPath. replaceAll(" : " , " :" ); // Normalize spacing
221+
222+ // Step 4: Extract file path from URI
223+ Uri albumArtUri = Uri . parse(decodedPath);
224+ String filePath = albumArtUri. getPath(); // e.g., "/storage/emulated/0/bluetooth/..."
225+
226+ // Step 5: Use ContentResolver to open file (avoids permission issues)
227+ try {
228+ InputStream inputStream = getContentResolver(). openInputStream(albumArtUri);
229+ Bitmap bitmap = BitmapFactory . decodeStream(inputStream);
230+ inputStream. close();
231+
232+ // Update UI with bitmap
233+ if (listener != null ) {
234+ listener. onAlbumArtChanged(bitmap);
235+ }
236+ } catch (IOException e) {
237+ Log . e(TAG , " Failed to load album art: " + e. getMessage());
238+ }
123239```
124240
241+ ** Required Permissions** (AndroidManifest.xml):
242+
243+ ``` xml
244+ <!-- Bluetooth permissions -->
245+ <uses-permission android : name =" android.permission.BLUETOOTH" />
246+ <uses-permission android : name =" android.permission.BLUETOOTH_ADMIN" />
247+
248+ <!-- Storage permissions for album art access -->
249+ <uses-permission android : name =" android.permission.READ_EXTERNAL_STORAGE" />
250+ <uses-permission android : name =" android.permission.WRITE_EXTERNAL_STORAGE" />
251+ <uses-permission android : name =" android.permission.MANAGE_EXTERNAL_STORAGE" />
252+
253+ <!-- Notification listener for media metadata -->
254+ <uses-permission android : name =" android.permission.BIND_NOTIFICATION_LISTENER_SERVICE" />
255+
256+ <!-- Legacy storage access for Android 10+ -->
257+ <application
258+ android : requestLegacyExternalStorage =" true" >
259+ ```
260+
261+ ** Grant Permissions via ADB** (if not auto-granted):
262+
263+ ``` bash
264+ adb shell pm grant com.custom.launcher android.permission.READ_EXTERNAL_STORAGE
265+ adb shell pm grant com.custom.launcher android.permission.WRITE_EXTERNAL_STORAGE
266+ ```
267+
268+ ** Bluetooth Album Art File Structure** :
269+
270+ ```
271+ /storage/emulated/0/bluetooth/
272+ ├── AA_BB_CC_DD_EE_FF/ # Bluetooth MAC address (underscores)
273+ │ ├── AVRCP_BIP_IMG_001.JPEG # Current track album art
274+ │ ├── AVRCP_BIP_IMG_002.JPEG # Previous/next track art
275+ │ └── ...
276+ ```
277+
278+ ** Key Points** :
279+
280+ - Use ` ContentResolver.openInputStream() ` instead of direct file access
281+ - Decode URI with ` URLDecoder.decode() ` to handle encoded characters
282+ - Bluetooth MAC addresses may use colons or underscores in path
283+ - AVRCP (Audio/Video Remote Control Profile) handles album art transfer
284+ - BIP (Basic Imaging Profile) is the Bluetooth protocol for image transfer
285+ - Album art updates automatically when track changes
286+
125287### Build Configuration
126288
127289- Gradle build system
@@ -204,19 +366,27 @@ adb install -r app/build/outputs/apk/debug/app-debug.apk
204366
205367## Known Challenges
206368
207- 1 . ** Vehicle Service Access**
208- - May require system-level permissions
209- - APK might need platform signature
210- - Consider installing as system app in ` /system/priv-app/ `
369+ 1 . ** Vehicle Service Access** ✅ SOLVED
370+ - ~~ May require system-level permissions~~ - No special permissions needed
371+ - ~~ APK might need platform signature~~ - Regular APK works fine
372+ - ~~ Consider installing as system app~~ - User app installation works
373+ - ** Solution** : Use ` createPackageContext() ` to access launcher's SDK classes
374+
375+ 2 . ** Testing Limitations** ✅ PARTIALLY SOLVED
376+ - Cannot fully test vehicle data on Mac/emulator (still true)
377+ - Mock data included for development (fallback works well)
378+ - Direct deployment to car recommended (confirmed working)
379+ - ** Debug Dialog** : Triple-tap clock to view logs on car without ADB
211380
212- 2 . ** Testing Limitations **
213- - Cannot fully test vehicle data on Mac/emulator
214- - Mock data included for development
215- - Direct deployment to car recommended
381+ 3 . ** Signature Requirements ** ✅ NOT NEEDED
382+ - ~~ System apps may need platform certificate ~~ - Not required
383+ - ~~ May need to extract signing keys ~~ - Not required
384+ - ** Confirmed ** : Regular debug APK works perfectly on car
216385
217- 3 . ** Signature Requirements**
218- - System apps may need platform certificate
219- - May need to extract signing keys from car system
386+ 4 . ** New Challenge: Display Resolution** ⚠️ IMPORTANT
387+ - Car display is ** 1778×720** NOT 1920×1080 as initially documented
388+ - Use ` layout-w1778dp/ ` qualifier for car-specific layouts
389+ - Test on car, not emulator, for accurate UI sizing
220390
221391## Next Steps
222392
@@ -301,6 +471,36 @@ adb install -r app/build/outputs/apk/debug/app-debug.apk
3014717 . ✅ Maximum 500 lines retained in buffer
3024728 . ✅ Wider dialog (1200px height) for better readability
3034739 . ✅ Auto-retry vehicle service binding every 10 seconds if connection fails
474+ 10 . ✅ Save logs to USB stick button (searches multiple mount points)
475+
476+ ### Phase 8: Vehicle Data & Album Art Integration ✅ WORKING ON CAR
477+
478+ ** Testing completed on car** : 28 Jan 2026
479+ ** Status** : All features working perfectly
480+
481+ ** Discoveries and Fixes:**
482+
483+ 1 . ✅ ** SAIC SDK Access Method Found**
484+ - SDK classes are in ` com.saicmotor.hmi.launcher ` APK, not system classloader
485+ - Use ` createPackageContext() ` to load launcher's classloader
486+ - Use reflection to access VehicleChargingManager
487+ - ** CRITICAL** : Must provide non-null IVehicleServiceListener or app crashes
488+ - Solution: Dynamic ` Proxy.newProxyInstance() ` handles callbacks
489+
490+ 2 . ✅ ** Battery Percentage Method Corrected**
491+ - ❌ Initial: ` getElectricityLevel() ` - Wrong method, returns different metric
492+ - ✅ Correct: ` getCurrentElectricQuantity() ` - Returns Float battery percentage
493+ - Tested on car: Shows accurate battery level
494+
495+ 3 . ✅ ** Range Data Working**
496+ - Method: ` getCurrentEnduranceMileage() ` returns Integer kilometers
497+ - Tested on car: Shows accurate remaining range
498+
499+ 4 . ✅ ** Bluetooth Album Art Working**
500+ - Path: ` /storage/emulated/0/bluetooth/[MAC]/AVRCP_BIP_IMG_*.JPEG `
501+ - Use ` ContentResolver.openInputStream() ` to load images
502+ - ` URLDecoder.decode() ` handles URI encoding
503+ - Tested on car: Album art displays correctly for Bluetooth media
304504
305505** Implementation Details:**
306506
0 commit comments