App 2.19.2: Full screen for CarPlay (experiment)
Adds com.allgo.carplay.service to immersive.full in Android 9's policy_control setting when switched on, removes only that entry when off. Car build (system user) only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
This commit is contained in:
@@ -1066,6 +1066,56 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
// ---------- Upload page ----------
|
||||
|
||||
/** The car's CarPlay app, as the Logs tab's foreground list showed it on SWI68. */
|
||||
private static final String CARPLAY_PACKAGE = "com.allgo.carplay.service";
|
||||
|
||||
/**
|
||||
* Experiment: forces CarPlay into immersive mode through Android 9's policy_control setting.
|
||||
* The switch shows what the setting holds, not a preference, and switching off removes only
|
||||
* our entry. Writing it needs WRITE_SECURE_SETTINGS, which only the car build (system user)
|
||||
* has; elsewhere the switch is disabled.
|
||||
*/
|
||||
private void bindCarPlayFullScreen() {
|
||||
com.google.android.material.switchmaterial.SwitchMaterial sw =
|
||||
settingsPane.findViewById(R.id.carplay_full_screen_switch);
|
||||
boolean mayWrite = checkSelfPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
|
||||
== android.content.pm.PackageManager.PERMISSION_GRANTED;
|
||||
String current = android.provider.Settings.Global.getString(getContentResolver(), PolicyControl.KEY);
|
||||
sw.setOnCheckedChangeListener(null);
|
||||
sw.setChecked(PolicyControl.hasFull(current, CARPLAY_PACKAGE));
|
||||
sw.setEnabled(mayWrite);
|
||||
if (!mayWrite) {
|
||||
((TextView) settingsPane.findViewById(R.id.carplay_full_screen_hint))
|
||||
.setText(R.string.hint_carplay_full_screen_unavailable);
|
||||
return;
|
||||
}
|
||||
sw.setOnCheckedChangeListener((btn, checked) -> diagReader.execute(() -> {
|
||||
boolean ok;
|
||||
try {
|
||||
android.content.ContentResolver cr = getContentResolver();
|
||||
String before = android.provider.Settings.Global.getString(cr, PolicyControl.KEY);
|
||||
String after = checked ? PolicyControl.withFull(before, CARPLAY_PACKAGE)
|
||||
: PolicyControl.withoutFull(before, CARPLAY_PACKAGE);
|
||||
android.provider.Settings.Global.putString(cr, PolicyControl.KEY, after);
|
||||
String now = android.provider.Settings.Global.getString(cr, PolicyControl.KEY);
|
||||
ok = PolicyControl.hasFull(now, CARPLAY_PACKAGE) == checked;
|
||||
android.util.Log.i("EVABRP.Settings", "policy_control " + (checked ? "on" : "off") + ": " + now);
|
||||
} catch (RuntimeException e) {
|
||||
android.util.Log.w("EVABRP.Settings", "policy_control not written", e);
|
||||
ok = false;
|
||||
}
|
||||
final boolean written = ok;
|
||||
runOnUiThread(() -> {
|
||||
if (isDestroyed()) return;
|
||||
if (!written) {
|
||||
Toast.makeText(this, R.string.carplay_full_screen_failed, Toast.LENGTH_LONG).show();
|
||||
bindCarPlayFullScreen();
|
||||
}
|
||||
refreshForeground();
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
private void bindServicePane() {
|
||||
voiceKeySwitch = settingsPane.findViewById(R.id.voice_key_switch);
|
||||
voiceKeySwitch.setChecked(UploadSettings.voiceKeyOpensApp(prefs));
|
||||
@@ -1079,6 +1129,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
prefs.edit().putBoolean(UploadSettings.KEY_FULL_SCREEN, checked).apply();
|
||||
applyFullScreen();
|
||||
});
|
||||
bindCarPlayFullScreen();
|
||||
liveTelemetrySwitch = servicePane.findViewById(R.id.abrp_upload_switch);
|
||||
intervalChoices = servicePane.findViewById(R.id.interval_choices);
|
||||
boostSwitch = servicePane.findViewById(R.id.boost_switch);
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* The value of Android 9's {@code policy_control} global setting, which forces other apps into
|
||||
* immersive mode: {@code immersive.full=pkg1,pkg2:immersive.status=pkg3}. This adds or removes
|
||||
* one package in {@code immersive.full} and leaves every other entry as it was, so switching
|
||||
* our experiment off restores what was there before. Pure Java, so it is unit-tested.
|
||||
*/
|
||||
final class PolicyControl {
|
||||
|
||||
static final String KEY = "policy_control";
|
||||
static final String FULL = "immersive.full";
|
||||
|
||||
private PolicyControl() {}
|
||||
|
||||
/** Whether {@code pkg} is named in {@code immersive.full}. */
|
||||
static boolean hasFull(String value, String pkg) {
|
||||
List<String> list = parse(value).get(FULL);
|
||||
return list != null && list.contains(pkg);
|
||||
}
|
||||
|
||||
/** {@code value} with {@code pkg} added to {@code immersive.full}. */
|
||||
static String withFull(String value, String pkg) {
|
||||
Map<String, List<String>> map = parse(value);
|
||||
List<String> list = map.get(FULL);
|
||||
if (list == null) {
|
||||
list = new ArrayList<>();
|
||||
map.put(FULL, list);
|
||||
}
|
||||
if (!list.contains(pkg)) list.add(pkg);
|
||||
return format(map);
|
||||
}
|
||||
|
||||
/** {@code value} without {@code pkg} in {@code immersive.full}; null when nothing is left. */
|
||||
static String withoutFull(String value, String pkg) {
|
||||
Map<String, List<String>> map = parse(value);
|
||||
List<String> list = map.get(FULL);
|
||||
if (list != null) {
|
||||
list.remove(pkg);
|
||||
if (list.isEmpty()) map.remove(FULL);
|
||||
}
|
||||
return format(map);
|
||||
}
|
||||
|
||||
private static Map<String, List<String>> parse(String value) {
|
||||
Map<String, List<String>> map = new LinkedHashMap<>();
|
||||
if (value == null) return map;
|
||||
for (String part : value.split(":")) {
|
||||
int eq = part.indexOf('=');
|
||||
if (eq <= 0) continue;
|
||||
String name = part.substring(0, eq).trim();
|
||||
List<String> list = map.get(name);
|
||||
if (list == null) {
|
||||
list = new ArrayList<>();
|
||||
map.put(name, list);
|
||||
}
|
||||
for (String p : part.substring(eq + 1).split(",")) {
|
||||
String t = p.trim();
|
||||
if (!t.isEmpty() && !list.contains(t)) list.add(t);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private static String format(Map<String, List<String>> map) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Map.Entry<String, List<String>> e : map.entrySet()) {
|
||||
if (e.getValue().isEmpty()) continue;
|
||||
if (sb.length() > 0) sb.append(':');
|
||||
sb.append(e.getKey()).append('=').append(String.join(",", e.getValue()));
|
||||
}
|
||||
return sb.length() == 0 ? null : sb.toString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user