From 71aceaa626a2c083e5ce13f94c05ca6a589ffe85 Mon Sep 17 00:00:00 2001 From: Nils Stinnesbeck Date: Fri, 23 Oct 2026 22:20:18 +0200 Subject: [PATCH] App 2.19.9: remove the Foreground apps card from the Logs tab Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6 --- AGENTS.md | 2 +- README.md | 2 +- SPECS.md | 3 + app/build.gradle | 4 +- .../com/evsuite/abrp/ForegroundHistory.java | 64 ---------- .../com/evsuite/abrp/ForegroundProbe.java | 110 ------------------ .../java/com/evsuite/abrp/MainActivity.java | 53 +-------- app/src/main/res/layout-w1000dp/pane_log.xml | 19 --- app/src/main/res/layout/pane_log.xml | 12 +- app/src/main/res/values/strings.xml | 9 -- .../evsuite/abrp/ForegroundHistoryTest.java | 69 ----------- 11 files changed, 14 insertions(+), 333 deletions(-) delete mode 100644 app/src/main/java/com/evsuite/abrp/ForegroundHistory.java delete mode 100644 app/src/main/java/com/evsuite/abrp/ForegroundProbe.java delete mode 100644 app/src/main/res/layout-w1000dp/pane_log.xml delete mode 100644 app/src/test/java/com/evsuite/abrp/ForegroundHistoryTest.java diff --git a/AGENTS.md b/AGENTS.md index dc3835c..c5642d4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -73,7 +73,7 @@ Repository: `ssh://git@git.stinnesbeck.com:2222/nils/mg4.git` (remote `origin`), | Live data | on/off, ABRP API key and user token, upload frequency | | Server | URL, pairing by QR, ingest token, installation ID (read-only), config import | | Settings | full screen, voice key, tyre unit, accent colour, battery, charging price, version, update check, install from USB, About | -| Logs | upload log | +| Logs | update log (every install step, with the platform's words), upload log | Key classes (`app/src/main/java/com/evsuite/abrp/`): `AbrpUploadService` (foreground service, 15 s tick), `TelemetryUploader` (+ `TelemetryQueue`, persisted), `TelemetryPayload` (server diff --git a/README.md b/README.md index 0d0ab67..65e59a6 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ with the server using a token of its own, obtained by pairing with a phone (belo | **Live data** | Live data on/off, ABRP credentials for sending directly from the car, upload frequency | | **Server** | Server URL, pairing by QR code, ingest token, installation ID, config import | | **Settings** | Full screen, voice key, tyre pressure unit, accent colour, battery (manual SoH, usable capacity), charging price, entry on the phone, version, update check, install from USB stick, About | -| **Logs** | Upload log | +| **Logs** | Update log (every install step), upload log | - **Opening the app:** the MG4 launcher does not list third-party apps. **Double-press the voice key** on the steering wheel and the app comes to the front (switch on the Settings diff --git a/SPECS.md b/SPECS.md index 6713462..3f5e586 100644 --- a/SPECS.md +++ b/SPECS.md @@ -118,6 +118,9 @@ 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 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). +- 2026-10-23 — App 2.19.9: the Foreground apps card is gone from the Logs tab (it found CarPlay's + package; nothing left to find), with ForegroundProbe and ForegroundHistory. The Logs tab is one + card again — update log, then uploads — with the other tabs' page padding. - 2026-10-23 — App 2.19.8: the charging curve's labels no longer overlap: "kW" has a row of its own above the plot, the power values are right-aligned against the axis, and the SOC ticks read "0 %" … "100 %" (the separate "SOC %" sat on top of "100"). The wide Charging tab splits 35/65, diff --git a/app/build.gradle b/app/build.gradle index 3e95a38..410c5b4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -34,8 +34,8 @@ android { applicationId "com.stinnesbeck.nilsmg4" minSdk 27 targetSdk 36 - versionCode 107 - versionName "2.19.8" + versionCode 108 + versionName "2.19.9" // 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. diff --git a/app/src/main/java/com/evsuite/abrp/ForegroundHistory.java b/app/src/main/java/com/evsuite/abrp/ForegroundHistory.java deleted file mode 100644 index 1deb666..0000000 --- a/app/src/main/java/com/evsuite/abrp/ForegroundHistory.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.evsuite.abrp; - -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.TimeZone; - -/** - * Which app was in front, and when: the lines of the Logs tab's "Foreground apps" card. - * - * Built from the platform's foreground events, oldest first. Consecutive events for the same - * screen collapse into one, the newest comes first, and an activity in its own package is - * shortened to ".Name" so the line stays readable. Pure Java, so it is unit-tested. - */ -final class ForegroundHistory { - - /** One move to the foreground. className may be null when the platform gives none. */ - static final class Event { - final long timeMs; - final String packageName; - final String className; - - Event(long timeMs, String packageName, String className) { - this.timeMs = timeMs; - this.packageName = packageName; - this.className = className; - } - } - - private ForegroundHistory() {} - - /** At most {@code limit} lines, newest first. */ - static List lines(List oldestFirst, int limit, TimeZone zone) { - List collapsed = new ArrayList<>(); - for (Event e : oldestFirst) { - if (e == null || e.packageName == null || e.packageName.isEmpty()) continue; - Event last = collapsed.isEmpty() ? null : collapsed.get(collapsed.size() - 1); - if (last != null && last.packageName.equals(e.packageName) - && equal(last.className, e.className)) continue; - collapsed.add(e); - } - SimpleDateFormat time = new SimpleDateFormat("HH:mm:ss", Locale.ROOT); - time.setTimeZone(zone); - List out = new ArrayList<>(); - for (int i = collapsed.size() - 1; i >= 0 && out.size() < limit; i--) { - Event e = collapsed.get(i); - String screen = shortClass(e.packageName, e.className); - out.add(time.format(e.timeMs) + " " + e.packageName + (screen == null ? "" : " " + screen)); - } - return out; - } - - /** ".Name" for an activity inside its own package, the full name otherwise. */ - static String shortClass(String packageName, String className) { - if (className == null || className.isEmpty()) return null; - if (className.startsWith(packageName + ".")) return className.substring(packageName.length()); - return className; - } - - private static boolean equal(String a, String b) { - return a == null ? b == null : a.equals(b); - } -} diff --git a/app/src/main/java/com/evsuite/abrp/ForegroundProbe.java b/app/src/main/java/com/evsuite/abrp/ForegroundProbe.java deleted file mode 100644 index 03ae645..0000000 --- a/app/src/main/java/com/evsuite/abrp/ForegroundProbe.java +++ /dev/null @@ -1,110 +0,0 @@ -package com.evsuite.abrp; - -import android.annotation.SuppressLint; -import android.app.ActivityManager; -import android.app.usage.UsageEvents; -import android.app.usage.UsageStatsManager; -import android.content.ComponentName; -import android.content.Context; -import android.content.res.Resources; -import android.provider.Settings; -import android.util.Log; - -import java.util.ArrayList; -import java.util.List; -import java.util.TimeZone; - -/** - * Read-only diagnostic: which apps were in front lately, which tasks are open now, and the - * system values a full-screen experiment for another app would touch. It changes nothing. - * - * The usage events and the full task list are open to the system user, which the car build - * runs as; the stable build sees only its own tasks and no events, and says so. Called on - * demand from the Logs tab, never in the background. - */ -final class ForegroundProbe { - - private static final String TAG = "EVABRP.Foreground"; - static final long WINDOW_MS = 30 * 60_000L; - private static final int MAX_LINES = 8; - private static final int MAX_TASKS = 4; - - static final class Result { - /** Null when the platform would not give the events (not the system user). */ - List recent; - List tasks = new ArrayList<>(); - /** The {@code policy_control} global setting; null when unset. */ - String policyControl; - /** The platform's status bar height in pixels; null when unknown. */ - Integer statusBarPx; - } - - private ForegroundProbe() {} - - static Result read(Context context) { - Result r = new Result(); - r.recent = recentForeground(context); - r.tasks = runningTasks(context); - try { - r.policyControl = Settings.Global.getString(context.getContentResolver(), "policy_control"); - } catch (RuntimeException e) { - Log.w(TAG, "policy_control unreadable", e); - } - try { - Resources sys = Resources.getSystem(); - int id = sys.getIdentifier("status_bar_height", "dimen", "android"); - if (id != 0) r.statusBarPx = sys.getDimensionPixelSize(id); - } catch (RuntimeException e) { - Log.w(TAG, "status bar height unreadable", e); - } - return r; - } - - // No PACKAGE_USAGE_STATS in the manifest: the usage service lets the system user through on - // its own, and any other caller gets an empty stream, reported as "not readable". - @SuppressLint("MissingPermission") - private static List recentForeground(Context context) { - try { - UsageStatsManager usage = (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE); - if (usage == null) return null; - long now = System.currentTimeMillis(); - UsageEvents events = usage.queryEvents(now - WINDOW_MS, now); - if (events == null) return null; - List list = new ArrayList<>(); - UsageEvents.Event e = new UsageEvents.Event(); - boolean any = false; - while (events.hasNextEvent()) { - events.getNextEvent(e); - any = true; - if (e.getEventType() == UsageEvents.Event.MOVE_TO_FOREGROUND) { - list.add(new ForegroundHistory.Event(e.getTimeStamp(), e.getPackageName(), e.getClassName())); - } - } - // A caller without access gets an empty stream rather than an error; an empty - // stream for the system user would still contain our own app coming to the front. - if (!any) return null; - return ForegroundHistory.lines(list, MAX_LINES, TimeZone.getDefault()); - } catch (RuntimeException e) { - Log.w(TAG, "usage events unreadable", e); - return null; - } - } - - @SuppressWarnings("deprecation") // getRunningTasks: the system user still gets every task - private static List runningTasks(Context context) { - List out = new ArrayList<>(); - try { - ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); - if (am == null) return out; - for (ActivityManager.RunningTaskInfo t : am.getRunningTasks(MAX_TASKS)) { - ComponentName top = t.topActivity; - if (top == null) continue; - String screen = ForegroundHistory.shortClass(top.getPackageName(), top.getClassName()); - out.add(top.getPackageName() + (screen == null ? "" : " " + screen)); - } - } catch (RuntimeException e) { - Log.w(TAG, "tasks unreadable", e); - } - return out; - } -} diff --git a/app/src/main/java/com/evsuite/abrp/MainActivity.java b/app/src/main/java/com/evsuite/abrp/MainActivity.java index cb4bd6b..19889be 100644 --- a/app/src/main/java/com/evsuite/abrp/MainActivity.java +++ b/app/src/main/java/com/evsuite/abrp/MainActivity.java @@ -203,47 +203,11 @@ public class MainActivity extends AppCompatActivity { } ((TextView) findViewById(R.id.header_title)).setText(PAGE_TITLES[position]); ((TextView) findViewById(R.id.header_subtitle)).setText(PAGE_SUBTITLES[position]); - if (panes != null && panes[position] == logPane) refreshForeground(); } - private TextView foregroundText; - private final java.util.concurrent.ExecutorService diagReader = - java.util.concurrent.Executors.newSingleThreadExecutor(r -> new Thread(r, "foreground-read")); - - /** Logs tab diagnostic: reads which apps were in front, off the main thread. Changes nothing. */ - private void refreshForeground() { - if (foregroundText == null) return; - try { - diagReader.execute(() -> { - ForegroundProbe.Result r = ForegroundProbe.read(getApplicationContext()); - runOnUiThread(() -> { if (!isDestroyed()) foregroundText.setText(foregroundReport(r)); }); - }); - } catch (java.util.concurrent.RejectedExecutionException ignored) { - // Activity going away. - } - } - - private String foregroundReport(ForegroundProbe.Result r) { - StringBuilder sb = new StringBuilder(); - if (r.recent == null) { - sb.append(getString(R.string.foreground_unavailable)).append('\n'); - } else if (r.recent.isEmpty()) { - sb.append(getString(R.string.foreground_none)).append('\n'); - } else { - sb.append(getString(R.string.foreground_recent)).append('\n'); - for (String line : r.recent) sb.append(line).append('\n'); - } - if (!r.tasks.isEmpty()) { - sb.append('\n').append(getString(R.string.foreground_tasks)).append('\n'); - for (String t : r.tasks) sb.append(t).append('\n'); - } - sb.append('\n').append(getString(R.string.foreground_policy, - r.policyControl == null ? getString(R.string.foreground_policy_unset) : r.policyControl)); - if (r.statusBarPx != null) { - sb.append('\n').append(getString(R.string.foreground_status_bar, r.statusBarPx)); - } - return sb.toString(); - } + /** Background thread for the one system-setting write (clearCarPlayFullScreen). */ + private final java.util.concurrent.ExecutorService settingsWriter = + java.util.concurrent.Executors.newSingleThreadExecutor(r -> new Thread(r, "settings-write")); /** Refreshes state + call log while the screen is visible. */ private final android.os.Handler uiHandler = new android.os.Handler(android.os.Looper.getMainLooper()); @@ -296,8 +260,6 @@ public class MainActivity extends AppCompatActivity { statusText = servicePane.findViewById(R.id.status_text); callLogText = logPane.findViewById(R.id.call_log_text); logPane.findViewById(R.id.flush_now_button).setOnClickListener(v -> manualFlush()); - foregroundText = logPane.findViewById(R.id.foreground_text); - logPane.findViewById(R.id.foreground_refresh_button).setOnClickListener(v -> refreshForeground()); setUpPager(); // Back on the page that was open when the activity was recreated (accent colour change). @@ -386,11 +348,6 @@ public class MainActivity extends AppCompatActivity { // Poll state and log only while the screen is up; onPause cancels it. uiHandler.post(uiRefresh); uiHandler.post(overviewRefresh); - // Back from another app (CarPlay) on the Logs tab: show where we were. - if (panes != null) { - ViewPager2 pager = findViewById(R.id.content); - if (panes[pager.getCurrentItem()] == logPane) refreshForeground(); - } } @Override @@ -443,7 +400,7 @@ public class MainActivity extends AppCompatActivity { @Override protected void onDestroy() { carReader.shutdownNow(); - diagReader.shutdownNow(); + settingsWriter.shutdownNow(); super.onDestroy(); } @@ -1076,7 +1033,7 @@ public class MainActivity extends AppCompatActivity { private void clearCarPlayFullScreen() { if (checkSelfPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) != android.content.pm.PackageManager.PERMISSION_GRANTED) return; - diagReader.execute(() -> { + settingsWriter.execute(() -> { try { android.content.ContentResolver cr = getContentResolver(); String before = android.provider.Settings.Global.getString(cr, PolicyControl.KEY); diff --git a/app/src/main/res/layout-w1000dp/pane_log.xml b/app/src/main/res/layout-w1000dp/pane_log.xml deleted file mode 100644 index 2050239..0000000 --- a/app/src/main/res/layout-w1000dp/pane_log.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/pane_log.xml b/app/src/main/res/layout/pane_log.xml index 863c052..19c5a94 100644 --- a/app/src/main/res/layout/pane_log.xml +++ b/app/src/main/res/layout/pane_log.xml @@ -1,18 +1,10 @@ - - + + - - - - - - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ed5bfab..91711e6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -169,15 +169,6 @@ No upload attempted yet. Updates, newest first: Upload now - Foreground apps - Which app was in front and when, from Android\'s own record. Reads only. - Last 30 minutes, newest first: - No app came to the front in the last 30 minutes. - Not readable in this build: only the car build (system app) may see other apps. - Open tasks now, top first: - policy_control: %1$s - not set - Status bar height: %1$d px Starting… Running — waiting for first upload Last upload: %1$s (%2$s) diff --git a/app/src/test/java/com/evsuite/abrp/ForegroundHistoryTest.java b/app/src/test/java/com/evsuite/abrp/ForegroundHistoryTest.java deleted file mode 100644 index 0fb24cd..0000000 --- a/app/src/test/java/com/evsuite/abrp/ForegroundHistoryTest.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.evsuite.abrp; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.TimeZone; - -import org.junit.Test; - -/** The Logs tab's foreground-app lines — pure JVM. */ -public class ForegroundHistoryTest { - - private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); - - private static ForegroundHistory.Event at(long seconds, String pkg, String cls) { - return new ForegroundHistory.Event(seconds * 1000, pkg, cls); - } - - @Test - public void newestFirstWithTheActivityShortened() { - List lines = ForegroundHistory.lines(Arrays.asList( - at(3600, "com.saic.launcher", "com.saic.launcher.Home"), - at(3700, "com.example.carplay", "com.example.carplay.ui.ProjectionActivity")), - 10, UTC); - assertEquals(Arrays.asList( - "01:01:40 com.example.carplay .ui.ProjectionActivity", - "01:00:00 com.saic.launcher .Home"), lines); - } - - @Test - public void repeatsOfTheSameScreenCollapse() { - List lines = ForegroundHistory.lines(Arrays.asList( - at(1, "a.b", "a.b.Main"), - at(2, "a.b", "a.b.Main"), - at(3, "a.b", "a.b.Other"), - at(4, "c.d", null), - at(5, "c.d", null)), 10, UTC); - assertEquals(Arrays.asList( - "00:00:04 c.d", - "00:00:03 a.b .Other", - "00:00:01 a.b .Main"), lines); - } - - @Test - public void limitKeepsTheNewest() { - List lines = ForegroundHistory.lines(Arrays.asList( - at(1, "one", null), at(2, "two", null), at(3, "three", null)), 2, UTC); - assertEquals(Arrays.asList("00:00:03 three", "00:00:02 two"), lines); - } - - @Test - public void emptyAndBlankEventsGiveNoLines() { - assertTrue(ForegroundHistory.lines(Collections.emptyList(), 10, UTC).isEmpty()); - assertTrue(ForegroundHistory.lines(Arrays.asList(null, at(1, "", null), at(2, null, "x")), 10, UTC).isEmpty()); - } - - @Test - public void aClassOutsideItsPackageStaysWhole() { - assertEquals("com.other.Activity", ForegroundHistory.shortClass("a.b", "com.other.Activity")); - assertEquals(".Main", ForegroundHistory.shortClass("a.b", "a.b.Main")); - assertNull(ForegroundHistory.shortClass("a.b", null)); - // "a.bc.Main" is not inside "a.b". - assertEquals("a.bc.Main", ForegroundHistory.shortClass("a.b", "a.bc.Main")); - } -}