Skip to content

Commit 85b5eb7

Browse files
committed
Update docs
1 parent 6d4a900 commit 85b5eb7

File tree

3 files changed

+313
-34
lines changed

3 files changed

+313
-34
lines changed

AGENTS.md

Lines changed: 222 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -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
301471
7. ✅ Maximum 500 lines retained in buffer
302472
8. ✅ Wider dialog (1200px height) for better readability
303473
9. ✅ 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

Comments
 (0)