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:
2026-10-23 20:50:35 +02:00
co-authored by Claude Opus 5.5
parent 209a231511
commit 50217aef74
10 changed files with 219 additions and 14 deletions
@@ -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();
}
}
@@ -10,14 +10,22 @@
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_full_screen" android:textAppearance="@style/Text.EV.Secondary" />
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/voice_key_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_voice_key" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" />
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_voice_key" android:textAppearance="@style/Text.EV.Secondary" />
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/label_tire_unit" android:textAppearance="@style/Text.EV.Body" android:layout_marginTop="8dp" />
<com.google.android.material.textfield.TextInputLayout style="@style/Widget.EV.Dropdown" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="4dp">
<com.google.android.material.textfield.MaterialAutoCompleteTextView android:id="@+id/tire_unit_choices" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:inputType="none" android:textSize="20sp" app:simpleItemLayout="@layout/dropdown_item" />
</com.google.android.material.textfield.TextInputLayout>
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/label_accent" android:textAppearance="@style/Text.EV.Body" android:layout_marginTop="8dp" />
<com.google.android.material.textfield.TextInputLayout style="@style/Widget.EV.Dropdown" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="4dp">
<com.google.android.material.textfield.MaterialAutoCompleteTextView android:id="@+id/accent_choices" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:inputType="none" android:textSize="20sp" app:simpleItemLayout="@layout/dropdown_item" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/carplay_full_screen_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_carplay_full_screen" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" />
<TextView android:id="@+id/carplay_full_screen_hint" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_carplay_full_screen" android:textAppearance="@style/Text.EV.Secondary" />
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:baselineAligned="false">
<LinearLayout android:layout_width="0dp" android:layout_weight="2" android:layout_height="wrap_content" android:layout_marginEnd="12dp" android:orientation="vertical">
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/label_tire_unit" android:textAppearance="@style/Text.EV.Body" android:layout_marginTop="8dp" />
<com.google.android.material.textfield.TextInputLayout style="@style/Widget.EV.Dropdown" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="4dp">
<com.google.android.material.textfield.MaterialAutoCompleteTextView android:id="@+id/tire_unit_choices" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:inputType="none" android:textSize="20sp" app:simpleItemLayout="@layout/dropdown_item" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<LinearLayout android:layout_width="0dp" android:layout_weight="3" android:layout_height="wrap_content" android:orientation="vertical">
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/label_accent" android:textAppearance="@style/Text.EV.Body" android:layout_marginTop="8dp" />
<com.google.android.material.textfield.TextInputLayout style="@style/Widget.EV.Dropdown" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="4dp">
<com.google.android.material.textfield.MaterialAutoCompleteTextView android:id="@+id/accent_choices" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:inputType="none" android:textSize="20sp" app:simpleItemLayout="@layout/dropdown_item" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical" android:layout_marginEnd="16dp">
@@ -7,6 +7,8 @@
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_full_screen" android:textAppearance="@style/Text.EV.Secondary" />
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/voice_key_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_voice_key" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" />
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_voice_key" android:textAppearance="@style/Text.EV.Secondary" />
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/carplay_full_screen_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_carplay_full_screen" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" />
<TextView android:id="@+id/carplay_full_screen_hint" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_carplay_full_screen" android:textAppearance="@style/Text.EV.Secondary" />
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/label_tire_unit" android:textAppearance="@style/Text.EV.Body" android:layout_marginTop="20dp" />
<com.google.android.material.textfield.TextInputLayout style="@style/Widget.EV.Dropdown" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp">
<com.google.android.material.textfield.MaterialAutoCompleteTextView android:id="@+id/tire_unit_choices" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:inputType="none" android:textSize="20sp" app:simpleItemLayout="@layout/dropdown_item" />
+5 -1
View File
@@ -28,7 +28,7 @@
<string name="vehicle_row_power_average">Power, 5-min average</string>
<string name="power_kw">%1$.1f kW</string>
<string name="power_waiting">needs a minute of data</string>
<string name="label_tire_unit">Tyre pressure unit</string>
<string name="label_tire_unit">Tyre pressure</string>
<string name="label_accent">Accent colour</string>
<string name="energy_kwh">%1$.1f kWh</string>
<string name="energy_trip_note">net, after recuperation</string>
@@ -114,6 +114,10 @@
<string name="label_full_screen">Full screen</string>
<string name="hint_full_screen">Hides the car\'s top and bottom bars while this app is open. Swipe in from the top or bottom edge to show them briefly, e.g. for the climate controls.</string>
<string name="label_carplay_full_screen">Full screen for CarPlay (experiment)</string>
<string name="hint_carplay_full_screen">Hides the car\'s bars over CarPlay too, from its next start. A black strip? Reconnect the phone. Off restores the setting.</string>
<string name="hint_carplay_full_screen_unavailable">Needs the car build: only a system app may change this.</string>
<string name="carplay_full_screen_failed">Could not change the CarPlay full-screen setting</string>
<string name="label_voice_key">Double-press the voice key to open this app</string>
<string name="hint_voice_key">Press the voice key on the steering wheel twice quickly. The voice assistant may still react to the presses.</string>
<string name="label_abrp_upload_enabled">Send live data</string>