App 2.19.4: Export CarPlay data to the USB stick
Read-only diagnostic for CarPlay's resolution: window, SurfaceFlinger and display state captured while CarPlay is in front, display-related properties without identifiers, Allgo's APK and libraries and CarPlay-named system files, written to the app's folder on the stick. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
This commit is contained in:
@@ -118,6 +118,18 @@ tyre pressures, GPS (lat, lon, elevation, heading) and the is_* flags. It does *
|
|||||||
confirmed car facts, app structure, UI conventions, build/test/ship, documents. It replaces
|
confirmed car facts, app structure, UI conventions, build/test/ship, documents. It replaces
|
||||||
upstream's EVABRPUploader text. The test emulator is now in the flake: `nix run .#car-emulator`
|
upstream's EVABRPUploader text. The test emulator is now in the flake: `nix run .#car-emulator`
|
||||||
(Android 9 AOSP at 1920×720, 160 dpi; the car build installs as the system user — checked).
|
(Android 9 AOSP at 1920×720, 160 dpi; the car build installs as the system user — checked).
|
||||||
|
- 2026-10-23 — App 2.19.4: "Export CarPlay data" on the Logs tab (Foreground apps card), for the
|
||||||
|
question whether CarPlay's resolution can change. CarPlay with policy_control still leaves a grey
|
||||||
|
strip, also after reconnecting: Allgo announces a smaller picture to the iPhone. 20 s after the
|
||||||
|
press (time to open CarPlay) it writes to `<stick>/Android/data/<package>/files/
|
||||||
|
nilsmg4-carplay-<time>/` — the only place on a removable volume Android 9 lets an app write:
|
||||||
|
`dumpsys window windows`, `SurfaceFlinger`, `display`, `activity activities`, `wm size/density`,
|
||||||
|
the display-related system properties (identifiers filtered out), Allgo's APK and native
|
||||||
|
libraries, and CarPlay/Allgo/iAP2-named files under /system, /vendor, /odm, /product. It reads
|
||||||
|
the car and writes only to the stick. Owner's choice to export Allgo's software for analysis
|
||||||
|
(2026-10-23). On the emulator (no Allgo there) every dump was written, without permission
|
||||||
|
errors. XDA (leonkernan, page 30) found SAIC's SystemUI hides its top bar itself for a whitelist
|
||||||
|
that includes com.allgo.carplay.service — compiled into SystemUI, not changeable from the app.
|
||||||
- 2026-10-23 — App 2.19.3: an update log on the Logs tab, above the uploads. A USB install on the
|
- 2026-10-23 — App 2.19.3: an update log on the Logs tab, above the uploads. A USB install on the
|
||||||
car ended in an installer message about a broken package, while the same APK installed through
|
car ended in an installer message about a broken package, while the same APK installed through
|
||||||
the Bluetooth-settings route; the message was only a toast and logcat, so its exact words are
|
the Bluetooth-settings route; the message was only a toast and logcat, so its exact words are
|
||||||
|
|||||||
+2
-2
@@ -34,8 +34,8 @@ android {
|
|||||||
applicationId "com.stinnesbeck.nilsmg4"
|
applicationId "com.stinnesbeck.nilsmg4"
|
||||||
minSdk 27
|
minSdk 27
|
||||||
targetSdk 36
|
targetSdk 36
|
||||||
versionCode 102
|
versionCode 103
|
||||||
versionName "2.19.3"
|
versionName "2.19.4"
|
||||||
|
|
||||||
// Nothing about the server ships in the APK: its URL and ingest token are entered in
|
// Nothing about the server ships in the APK: its URL and ingest token are entered in
|
||||||
// the app's Server tab (ServerConfig), and the database is only reachable by the server.
|
// the app's Server tab (ServerConfig), and the database is only reachable by the server.
|
||||||
|
|||||||
@@ -0,0 +1,234 @@
|
|||||||
|
package com.evsuite.abrp;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.pm.ApplicationInfo;
|
||||||
|
import android.os.Environment;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read-only diagnostic for the CarPlay resolution question: copies what decides CarPlay's size
|
||||||
|
* to a folder on the USB stick, for reading on a computer. It reads the car and writes only to
|
||||||
|
* the stick.
|
||||||
|
*
|
||||||
|
* What it writes to {@code <stick>/Android/data/<package>/files/nilsmg4-carplay-<time>/}:
|
||||||
|
* <ul>
|
||||||
|
* <li>the CarPlay app's APK files and its own native libraries;</li>
|
||||||
|
* <li>libraries and configuration files under /system, /vendor, /odm and /product whose path
|
||||||
|
* names CarPlay (see {@link #isCarPlayPath});</li>
|
||||||
|
* <li>the display-related system properties, never identifiers ({@link #keepProperty});</li>
|
||||||
|
* <li>the window manager, SurfaceFlinger and display state, captured while CarPlay is in front.</li>
|
||||||
|
* </ul>
|
||||||
|
* Only the system user (the car build) may run dumpsys; elsewhere those files say why not.
|
||||||
|
*/
|
||||||
|
final class CarPlayExport {
|
||||||
|
|
||||||
|
private static final String TAG = "EVABRP.CarPlayExport";
|
||||||
|
static final String CARPLAY_PACKAGE = "com.allgo.carplay.service";
|
||||||
|
|
||||||
|
private static final long MAX_FILE_BYTES = 64L << 20;
|
||||||
|
private static final int MAX_FOUND_FILES = 200;
|
||||||
|
private static final String[] SEARCH_ROOTS = {
|
||||||
|
"/system/etc", "/system/lib", "/system/lib64", "/system/priv-app", "/system/app",
|
||||||
|
"/vendor/etc", "/vendor/lib", "/vendor/lib64", "/vendor/app", "/vendor/priv-app",
|
||||||
|
"/odm/etc", "/product/etc", "/product/app", "/product/priv-app",
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final Pattern CARPLAY_PATH = Pattern.compile("(?i)carplay|allgo|iap2|airplay");
|
||||||
|
private static final Pattern PROPERTY_KEEP = Pattern.compile(
|
||||||
|
"(?i)carplay|allgo|iap|airplay|display|lcd|screen|resolution|density|surface|hwui|"
|
||||||
|
+ "\\bsf\\.|\\.sf\\.|width|height|ro\\.build\\.(version|display|fingerprint|id)|"
|
||||||
|
+ "vehicle_type|projection");
|
||||||
|
private static final Pattern PROPERTY_DROP = Pattern.compile(
|
||||||
|
"(?i)serial|vin\\b|\\.vin|imei|iccid|imsi|mac|bt\\.addr|bdaddr|wifi|ssid|password|token|"
|
||||||
|
+ "key|account|phone|msisdn|sim");
|
||||||
|
|
||||||
|
private CarPlayExport() {}
|
||||||
|
|
||||||
|
/** A path whose name says it belongs to CarPlay. */
|
||||||
|
static boolean isCarPlayPath(String path) {
|
||||||
|
return path != null && CARPLAY_PATH.matcher(path).find();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A {@code [key]: [value]} getprop line worth exporting: display-related, never an identifier. */
|
||||||
|
static boolean keepProperty(String getpropLine) {
|
||||||
|
if (getpropLine == null) return false;
|
||||||
|
int end = getpropLine.indexOf("]:");
|
||||||
|
String key = end > 0 ? getpropLine.substring(0, end) : getpropLine;
|
||||||
|
return PROPERTY_KEEP.matcher(key).find() && !PROPERTY_DROP.matcher(key).find();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Runs the export; returns one line for the screen. Blocking: call it off the main thread. */
|
||||||
|
static String run(Context context) {
|
||||||
|
File stick = stickFolder(context);
|
||||||
|
if (stick == null) {
|
||||||
|
Log.w(TAG, "no USB stick");
|
||||||
|
return "No USB stick found";
|
||||||
|
}
|
||||||
|
String stamp = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US).format(new Date());
|
||||||
|
File out = new File(stick, "nilsmg4-carplay-" + stamp);
|
||||||
|
if (!out.mkdirs()) {
|
||||||
|
Log.w(TAG, "cannot create " + out);
|
||||||
|
return "Could not create " + out;
|
||||||
|
}
|
||||||
|
List<String> notes = new ArrayList<>();
|
||||||
|
int copied = 0;
|
||||||
|
|
||||||
|
// The live state first, while CarPlay is still in front.
|
||||||
|
command(out, "dumpsys-window.txt", notes, "dumpsys", "window", "windows");
|
||||||
|
command(out, "dumpsys-surfaceflinger.txt", notes, "dumpsys", "SurfaceFlinger");
|
||||||
|
command(out, "dumpsys-display.txt", notes, "dumpsys", "display");
|
||||||
|
command(out, "dumpsys-activity.txt", notes, "dumpsys", "activity", "activities");
|
||||||
|
command(out, "wm.txt", notes, "sh", "-c", "wm size; wm density");
|
||||||
|
properties(out, notes);
|
||||||
|
|
||||||
|
try {
|
||||||
|
ApplicationInfo app = context.getPackageManager().getApplicationInfo(CARPLAY_PACKAGE, 0);
|
||||||
|
notes.add("uid " + app.uid + ", flags 0x" + Integer.toHexString(app.flags)
|
||||||
|
+ ", source " + app.sourceDir + ", native " + app.nativeLibraryDir);
|
||||||
|
File apkDir = new File(out, "apk");
|
||||||
|
copied += copy(new File(app.sourceDir), apkDir, notes);
|
||||||
|
if (app.splitSourceDirs != null) {
|
||||||
|
for (String split : app.splitSourceDirs) copied += copy(new File(split), apkDir, notes);
|
||||||
|
}
|
||||||
|
File[] libs = app.nativeLibraryDir == null ? null : new File(app.nativeLibraryDir).listFiles();
|
||||||
|
if (libs != null) {
|
||||||
|
for (File lib : libs) copied += copy(lib, new File(out, "native"), notes);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
notes.add("CarPlay app not found: " + e);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<File> found = new ArrayList<>();
|
||||||
|
for (String root : SEARCH_ROOTS) find(new File(root), 0, found);
|
||||||
|
for (File f : found) {
|
||||||
|
copied += copy(f, new File(out, "found" + f.getParent()), notes);
|
||||||
|
}
|
||||||
|
notes.add(found.size() + " CarPlay-named files under the system partitions");
|
||||||
|
|
||||||
|
write(new File(out, "notes.txt"), String.join("\n", notes) + "\n");
|
||||||
|
String result = "Exported " + copied + " files to " + out;
|
||||||
|
Log.i(TAG, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This app's own folder on the removable volume, or on any volume (the emulator) when there is
|
||||||
|
* none: <stick>/Android/data/<package>/files. Android 9 lets an app write there and nowhere
|
||||||
|
* else on a removable volume — the stick's top folder is read-only to it.
|
||||||
|
*/
|
||||||
|
private static File stickFolder(Context context) {
|
||||||
|
File fallback = null;
|
||||||
|
for (File dir : context.getExternalFilesDirs(null)) {
|
||||||
|
if (dir == null || UsbVolume.root(dir) == null) continue;
|
||||||
|
boolean removable;
|
||||||
|
try {
|
||||||
|
removable = Environment.isExternalStorageRemovable(dir);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
removable = false;
|
||||||
|
}
|
||||||
|
if (removable) return dir;
|
||||||
|
if (fallback == null) fallback = dir;
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void find(File dir, int depth, List<File> found) {
|
||||||
|
if (depth > 5 || found.size() >= MAX_FOUND_FILES) return;
|
||||||
|
File[] entries = dir.listFiles();
|
||||||
|
if (entries == null) return;
|
||||||
|
for (File f : entries) {
|
||||||
|
if (found.size() >= MAX_FOUND_FILES) return;
|
||||||
|
if (f.isDirectory()) {
|
||||||
|
if (isCarPlayPath(f.getName())) {
|
||||||
|
File[] inner = f.listFiles();
|
||||||
|
if (inner != null) for (File g : inner) if (g.isFile()) found.add(g);
|
||||||
|
}
|
||||||
|
find(f, depth + 1, found);
|
||||||
|
} else if (isCarPlayPath(f.getName())) {
|
||||||
|
found.add(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void properties(File out, List<String> notes) {
|
||||||
|
String all = exec(notes, "getprop");
|
||||||
|
if (all == null) return;
|
||||||
|
StringBuilder kept = new StringBuilder();
|
||||||
|
for (String line : all.split("\n")) if (keepProperty(line)) kept.append(line).append('\n');
|
||||||
|
write(new File(out, "properties.txt"), kept.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void command(File out, String name, List<String> notes, String... argv) {
|
||||||
|
String text = exec(notes, argv);
|
||||||
|
if (text != null) write(new File(out, name), text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String exec(List<String> notes, String... argv) {
|
||||||
|
Process p = null;
|
||||||
|
try {
|
||||||
|
p = new ProcessBuilder(argv).redirectErrorStream(true).start();
|
||||||
|
p.getOutputStream().close();
|
||||||
|
byte[] bytes = readAll(p.getInputStream(), 8 << 20);
|
||||||
|
if (!p.waitFor(20, TimeUnit.SECONDS)) notes.add(String.join(" ", argv) + ": timed out");
|
||||||
|
return new String(bytes, StandardCharsets.UTF_8);
|
||||||
|
} catch (Exception e) {
|
||||||
|
notes.add(String.join(" ", argv) + ": " + e);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
if (p != null) p.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int copy(File source, File dir, List<String> notes) {
|
||||||
|
if (!source.isFile()) return 0;
|
||||||
|
if (source.length() > MAX_FILE_BYTES) {
|
||||||
|
notes.add("skipped (too large): " + source + " " + source.length());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (!dir.isDirectory() && !dir.mkdirs()) {
|
||||||
|
notes.add("no folder for " + source);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
try (InputStream in = new FileInputStream(source);
|
||||||
|
OutputStream os = new FileOutputStream(new File(dir, source.getName()))) {
|
||||||
|
byte[] buffer = new byte[64 * 1024];
|
||||||
|
int n;
|
||||||
|
while ((n = in.read(buffer)) != -1) os.write(buffer, 0, n);
|
||||||
|
return 1;
|
||||||
|
} catch (IOException e) {
|
||||||
|
notes.add("not copied: " + source + ": " + e.getMessage());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] readAll(InputStream in, int max) throws IOException {
|
||||||
|
java.io.ByteArrayOutputStream buffer = new java.io.ByteArrayOutputStream();
|
||||||
|
byte[] chunk = new byte[16 * 1024];
|
||||||
|
int n;
|
||||||
|
while ((n = in.read(chunk)) != -1 && buffer.size() < max) buffer.write(chunk, 0, n);
|
||||||
|
return buffer.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void write(File file, String text) {
|
||||||
|
try (OutputStream os = new FileOutputStream(file)) {
|
||||||
|
os.write(text.getBytes(StandardCharsets.UTF_8));
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.w(TAG, "not written: " + file, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -242,9 +242,46 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
if (r.statusBarPx != null) {
|
if (r.statusBarPx != null) {
|
||||||
sb.append('\n').append(getString(R.string.foreground_status_bar, r.statusBarPx));
|
sb.append('\n').append(getString(R.string.foreground_status_bar, r.statusBarPx));
|
||||||
}
|
}
|
||||||
|
if (lastCarPlayExport != null) {
|
||||||
|
sb.append('\n').append(getString(R.string.carplay_export_last, lastCarPlayExport));
|
||||||
|
}
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The last CarPlay export's outcome, for the Foreground apps card; null before the first. */
|
||||||
|
private static volatile String lastCarPlayExport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits 20 seconds, so the driver can bring CarPlay to the front, then exports what decides
|
||||||
|
* its size to the USB stick (CarPlayExport). Runs in the app's process, which the upload
|
||||||
|
* service keeps alive while this activity is in the background.
|
||||||
|
*/
|
||||||
|
private void startCarPlayExport() {
|
||||||
|
android.content.Context app = getApplicationContext();
|
||||||
|
android.os.Handler main = new android.os.Handler(android.os.Looper.getMainLooper());
|
||||||
|
Toast.makeText(app, R.string.carplay_export_started, Toast.LENGTH_LONG).show();
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
Thread.sleep(20_000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
main.post(() -> Toast.makeText(app, R.string.carplay_export_running, Toast.LENGTH_SHORT).show());
|
||||||
|
String result;
|
||||||
|
try {
|
||||||
|
result = CarPlayExport.run(app);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
result = "Export failed: " + t;
|
||||||
|
}
|
||||||
|
lastCarPlayExport = result;
|
||||||
|
final String shown = result;
|
||||||
|
main.post(() -> {
|
||||||
|
Toast.makeText(app, shown, Toast.LENGTH_LONG).show();
|
||||||
|
refreshForeground();
|
||||||
|
});
|
||||||
|
}, "carplay-export").start();
|
||||||
|
}
|
||||||
|
|
||||||
/** Refreshes state + call log while the screen is visible. */
|
/** Refreshes state + call log while the screen is visible. */
|
||||||
private final android.os.Handler uiHandler = new android.os.Handler(android.os.Looper.getMainLooper());
|
private final android.os.Handler uiHandler = new android.os.Handler(android.os.Looper.getMainLooper());
|
||||||
private final Runnable uiRefresh = new Runnable() {
|
private final Runnable uiRefresh = new Runnable() {
|
||||||
@@ -298,6 +335,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
logPane.findViewById(R.id.flush_now_button).setOnClickListener(v -> manualFlush());
|
logPane.findViewById(R.id.flush_now_button).setOnClickListener(v -> manualFlush());
|
||||||
foregroundText = logPane.findViewById(R.id.foreground_text);
|
foregroundText = logPane.findViewById(R.id.foreground_text);
|
||||||
logPane.findViewById(R.id.foreground_refresh_button).setOnClickListener(v -> refreshForeground());
|
logPane.findViewById(R.id.foreground_refresh_button).setOnClickListener(v -> refreshForeground());
|
||||||
|
logPane.findViewById(R.id.carplay_export_button).setOnClickListener(v -> startCarPlayExport());
|
||||||
|
|
||||||
setUpPager();
|
setUpPager();
|
||||||
// Back on the page that was open when the activity was recreated (accent colour change).
|
// Back on the page that was open when the activity was recreated (accent colour change).
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.evsuite.abrp;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
/** Where the top folder of a storage volume is, found without a file picker (the head unit has none). */
|
||||||
|
final class UsbVolume {
|
||||||
|
|
||||||
|
private UsbVolume() { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The top folder of the volume an app-specific folder lives on:
|
||||||
|
* /storage/1234-ABCD/Android/data/<package>/files → /storage/1234-ABCD. Null when the path
|
||||||
|
* does not have that shape.
|
||||||
|
*/
|
||||||
|
static File root(File appFilesDir) {
|
||||||
|
if (appFilesDir == null) return null;
|
||||||
|
File data = appFilesDir.getParentFile() == null ? null : appFilesDir.getParentFile().getParentFile();
|
||||||
|
File android = data == null ? null : data.getParentFile();
|
||||||
|
if (!"files".equals(appFilesDir.getName()) || data == null || !"data".equals(data.getName())
|
||||||
|
|| android == null || !"Android".equals(android.getName())) return null;
|
||||||
|
return android.getParentFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
<LinearLayout android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:orientation="vertical" android:paddingStart="24dp" android:paddingEnd="24dp" android:paddingTop="16dp" android:paddingBottom="16dp" android:background="@drawable/bg_glass">
|
<LinearLayout android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:orientation="vertical" android:paddingStart="24dp" android:paddingEnd="24dp" android:paddingTop="16dp" android:paddingBottom="16dp" android:background="@drawable/bg_glass">
|
||||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:gravity="center_vertical">
|
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:gravity="center_vertical">
|
||||||
<TextView android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:text="@string/foreground_title" android:textAppearance="@style/Text.EV.Eyebrow" />
|
<TextView android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:text="@string/foreground_title" android:textAppearance="@style/Text.EV.Eyebrow" />
|
||||||
|
<com.google.android.material.button.MaterialButton android:id="@+id/carplay_export_button" style="@style/Widget.EV.Button.Outlined" android:layout_width="wrap_content" android:layout_height="48dp" android:layout_marginEnd="8dp" android:minWidth="0dp" android:paddingStart="16dp" android:paddingEnd="16dp" android:text="@string/btn_carplay_export" />
|
||||||
<com.google.android.material.button.MaterialButton android:id="@+id/foreground_refresh_button" style="@style/Widget.EV.Button.Outlined" android:layout_width="48dp" android:layout_height="48dp" android:minWidth="0dp" android:padding="0dp" android:contentDescription="@string/btn_refresh" app:icon="@drawable/ic_refresh" app:iconGravity="textStart" app:iconPadding="0dp" app:iconTint="?attr/evAccent" />
|
<com.google.android.material.button.MaterialButton android:id="@+id/foreground_refresh_button" style="@style/Widget.EV.Button.Outlined" android:layout_width="48dp" android:layout_height="48dp" android:minWidth="0dp" android:padding="0dp" android:contentDescription="@string/btn_refresh" app:icon="@drawable/ic_refresh" app:iconGravity="textStart" app:iconPadding="0dp" app:iconTint="?attr/evAccent" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="4dp" android:text="@string/foreground_hint" android:textAppearance="@style/Text.EV.Secondary" />
|
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="4dp" android:text="@string/foreground_hint" android:textAppearance="@style/Text.EV.Secondary" />
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="24dp" android:background="@drawable/bg_glass" android:layout_marginTop="16dp">
|
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="24dp" android:background="@drawable/bg_glass" android:layout_marginTop="16dp">
|
||||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:gravity="center_vertical">
|
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:gravity="center_vertical">
|
||||||
<TextView android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:text="@string/foreground_title" android:textAppearance="@style/Text.EV.Eyebrow" />
|
<TextView android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:text="@string/foreground_title" android:textAppearance="@style/Text.EV.Eyebrow" />
|
||||||
|
<com.google.android.material.button.MaterialButton android:id="@+id/carplay_export_button" style="@style/Widget.EV.Button.Outlined" android:layout_width="wrap_content" android:layout_height="48dp" android:layout_marginEnd="8dp" android:minWidth="0dp" android:paddingStart="16dp" android:paddingEnd="16dp" android:text="@string/btn_carplay_export" />
|
||||||
<com.google.android.material.button.MaterialButton android:id="@+id/foreground_refresh_button" style="@style/Widget.EV.Button.Outlined" android:layout_width="48dp" android:layout_height="48dp" android:minWidth="0dp" android:padding="0dp" android:contentDescription="@string/btn_refresh" app:icon="@drawable/ic_refresh" app:iconGravity="textStart" app:iconPadding="0dp" app:iconTint="?attr/evAccent" />
|
<com.google.android.material.button.MaterialButton android:id="@+id/foreground_refresh_button" style="@style/Widget.EV.Button.Outlined" android:layout_width="48dp" android:layout_height="48dp" android:minWidth="0dp" android:padding="0dp" android:contentDescription="@string/btn_refresh" app:icon="@drawable/ic_refresh" app:iconGravity="textStart" app:iconPadding="0dp" app:iconTint="?attr/evAccent" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="4dp" android:text="@string/foreground_hint" android:textAppearance="@style/Text.EV.Secondary" />
|
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="4dp" android:text="@string/foreground_hint" android:textAppearance="@style/Text.EV.Secondary" />
|
||||||
|
|||||||
@@ -174,6 +174,10 @@
|
|||||||
<string name="update_log_title">Updates, newest first:</string>
|
<string name="update_log_title">Updates, newest first:</string>
|
||||||
<string name="btn_flush_now">Upload now</string>
|
<string name="btn_flush_now">Upload now</string>
|
||||||
<string name="foreground_title">Foreground apps</string>
|
<string name="foreground_title">Foreground apps</string>
|
||||||
|
<string name="btn_carplay_export">Export CarPlay data</string>
|
||||||
|
<string name="carplay_export_started">Open CarPlay now: the export to the USB stick starts in 20 seconds</string>
|
||||||
|
<string name="carplay_export_running">Exporting CarPlay data to the USB stick…</string>
|
||||||
|
<string name="carplay_export_last">Last export: %1$s</string>
|
||||||
<string name="foreground_hint">For the CarPlay full-screen test: open CarPlay, come back here, read its name off the list. Reads only.</string>
|
<string name="foreground_hint">For the CarPlay full-screen test: open CarPlay, come back here, read its name off the list. Reads only.</string>
|
||||||
<string name="foreground_recent">Last 30 minutes, newest first:</string>
|
<string name="foreground_recent">Last 30 minutes, newest first:</string>
|
||||||
<string name="foreground_none">No app came to the front in the last 30 minutes.</string>
|
<string name="foreground_none">No app came to the front in the last 30 minutes.</string>
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.evsuite.abrp;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
/** What the CarPlay export takes along — pure JVM. */
|
||||||
|
public class CarPlayExportTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void displayPropertiesAreKept() {
|
||||||
|
assertTrue(CarPlayExport.keepProperty("[ro.sf.lcd_density]: [160]"));
|
||||||
|
assertTrue(CarPlayExport.keepProperty("[persist.allgo.carplay.height]: [640]"));
|
||||||
|
assertTrue(CarPlayExport.keepProperty("[ro.build.mtk2712.vehicle_type]: [AS28]"));
|
||||||
|
assertTrue(CarPlayExport.keepProperty("[ro.build.display.id]: [SWI68-29958-1300R69]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void identifiersNeverLeave() {
|
||||||
|
assertFalse(CarPlayExport.keepProperty("[ro.serialno]: [ABC123]"));
|
||||||
|
assertFalse(CarPlayExport.keepProperty("[persist.vehicle.vin]: [LSJ...]"));
|
||||||
|
assertFalse(CarPlayExport.keepProperty("[persist.carplay.wifi.ssid]: [MG4-1234]"));
|
||||||
|
assertFalse(CarPlayExport.keepProperty("[persist.carplay.bt.addr]: [00:11:22:33:44:55]"));
|
||||||
|
assertFalse(CarPlayExport.keepProperty("[ro.boot.serialno]: [x]"));
|
||||||
|
assertFalse(CarPlayExport.keepProperty(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unrelatedPropertiesStay() {
|
||||||
|
assertFalse(CarPlayExport.keepProperty("[dalvik.vm.heapsize]: [512m]"));
|
||||||
|
assertFalse(CarPlayExport.keepProperty("[persist.sys.timezone]: [Europe/Berlin]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void carPlayPathsAreRecognised() {
|
||||||
|
assertTrue(CarPlayExport.isCarPlayPath("/vendor/etc/allgo/config.xml"));
|
||||||
|
assertTrue(CarPlayExport.isCarPlayPath("libCarPlayCore.so"));
|
||||||
|
assertTrue(CarPlayExport.isCarPlayPath("iap2_config.ini"));
|
||||||
|
assertFalse(CarPlayExport.isCarPlayPath("/system/etc/hosts"));
|
||||||
|
assertFalse(CarPlayExport.isCarPlayPath(null));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,19 +16,9 @@ final class UsbUpdate {
|
|||||||
|
|
||||||
private UsbUpdate() { }
|
private UsbUpdate() { }
|
||||||
|
|
||||||
/**
|
/** The top folder of the volume [appFilesDir] lives on; see {@link UsbVolume#root}. */
|
||||||
* The top folder of the volume an app-specific folder lives on:
|
|
||||||
* /storage/1234-ABCD/Android/data/<package>/files → /storage/1234-ABCD. Null when the path
|
|
||||||
* does not have that shape. This is how a stick is found without a file picker, which the
|
|
||||||
* head unit does not have.
|
|
||||||
*/
|
|
||||||
static File volumeRoot(File appFilesDir) {
|
static File volumeRoot(File appFilesDir) {
|
||||||
if (appFilesDir == null) return null;
|
return UsbVolume.root(appFilesDir);
|
||||||
File data = appFilesDir.getParentFile() == null ? null : appFilesDir.getParentFile().getParentFile();
|
|
||||||
File android = data == null ? null : data.getParentFile();
|
|
||||||
if (!"files".equals(appFilesDir.getName()) || data == null || !"data".equals(data.getName())
|
|
||||||
|| android == null || !"Android".equals(android.getName())) return null;
|
|
||||||
return android.getParentFile();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user