App 2.19.9: remove the Foreground apps card from the Logs tab
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
This commit is contained in:
@@ -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<String> lines(List<Event> oldestFirst, int limit, TimeZone zone) {
|
||||
List<Event> 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<String> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<String> recent;
|
||||
List<String> 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<String> 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<ForegroundHistory.Event> 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<String> runningTasks(Context context) {
|
||||
List<String> 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user