Add compatibility change preference

Add UI for modifying the compatibility change overrides per-app.

Test: make RunSettingsRoboTests ROBOTEST_FILTER=PlatformCompatDashboardTest
Bug: 138280620
Change-Id: I07c7602e7a439e47b0b1fa59b047231afbbc0ab6
This commit is contained in:
Andrei Onea
2019-09-26 14:44:49 +01:00
parent 60bd816476
commit 486fd49e87
7 changed files with 496 additions and 0 deletions

View File

@@ -10696,6 +10696,17 @@
<item>@string/game_driver_app_preference_system</item>
</string-array>
<!-- Title for App Compatibility Changes dashboard where developers can configure per-app overrides for compatibility changes [CHAR LIMIT=50] -->
<string name="platform_compat_dashboard_title">App Compatibility Changes</string>
<!-- Summary for App Compatibility Changes dashboard [CHAR LIMIT=NONE] -->
<string name="platform_compat_dashboard_summary">Modify app compatibility change overrides</string>
<!-- Title for default enabled app compat changes category [CHAR LIMIT=50] -->
<string name="platform_compat_default_enabled_title">Default enabled changes</string>
<!-- Title for default disabled app compat changes category [CHAR LIMIT=50] -->
<string name="platform_compat_default_disabled_title">Default disabled changes</string>
<!-- Title for target SDK gated app compat changes category [CHAR LIMIT=50] -->
<string name="platform_compat_target_sdk_title">Enabled after SDK <xliff:g id="number" example="29">%d</xliff:g></string>
<!-- Slices Strings -->
<!-- Summary text on a card explaining that a setting does not exist / is not supported on the device [CHAR_LIMIT=NONE]-->

View File

@@ -226,6 +226,13 @@
android:fragment="com.android.settings.development.gamedriver.GameDriverDashboard"
settings:searchable="false" />
<Preference
android:key="platform_compat_dashboard"
android:title="@string/platform_compat_dashboard_title"
android:summary="@string/platform_compat_dashboard_summary"
android:fragment="com.android.settings.development.compat.PlatformCompatDashboard"
/>
</PreferenceCategory>
<PreferenceCategory

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2019 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:settings="http://schemas.android.com/apk/res-auto"
android:key="platform_compat_dashboard"
android:title="@string/platform_compat_dashboard_title">
</PreferenceScreen>

View File

@@ -31,4 +31,6 @@ public interface DevelopmentOptionsActivityRequestCodes {
int REQUEST_CODE_ANGLE_DRIVER_PKGS = 4;
int REQUEST_CODE_ANGLE_DRIVER_VALUES = 5;
int REQUEST_COMPAT_CHANGE_APP = 6;
}

View File

@@ -0,0 +1,276 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.development.compat;
import static com.android.settings.development.DevelopmentOptionsActivityRequestCodes.REQUEST_COMPAT_CHANGE_APP;
import android.app.Activity;
import android.app.settings.SettingsEnums;
import android.compat.Compatibility.ChangeConfig;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.util.ArraySet;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.Preference.OnPreferenceChangeListener;
import androidx.preference.PreferenceCategory;
import androidx.preference.SwitchPreference;
import com.android.internal.compat.CompatibilityChangeConfig;
import com.android.internal.compat.CompatibilityChangeInfo;
import com.android.internal.compat.IPlatformCompat;
import com.android.settings.R;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.development.AppPicker;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
/**
* Dashboard for Platform Compat preferences.
*/
public class PlatformCompatDashboard extends DashboardFragment {
private static final String TAG = "PlatformCompatDashboard";
private static final String COMPAT_APP = "compat_app";
private IPlatformCompat mPlatformCompat;
private CompatibilityChangeInfo[] mChanges;
@VisibleForTesting
String mSelectedApp;
@Override
public int getMetricsCategory() {
return SettingsEnums.SETTINGS_PLATFORM_COMPAT_DASHBOARD;
}
@Override
protected String getLogTag() {
return TAG;
}
@Override
protected int getPreferenceScreenResId() {
return R.xml.platform_compat_settings;
}
@Override
public int getHelpResource() {
return 0;
}
IPlatformCompat getPlatformCompat() {
if (mPlatformCompat == null) {
mPlatformCompat = IPlatformCompat.Stub
.asInterface(ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE));
}
return mPlatformCompat;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
try {
mChanges = getPlatformCompat().listAllChanges();
} catch (RemoteException e) {
throw new RuntimeException("Could not list changes!", e);
}
startAppPicker();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(COMPAT_APP, mSelectedApp);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_COMPAT_CHANGE_APP) {
if (resultCode == Activity.RESULT_OK) {
mSelectedApp = data.getAction();
addPreferences();
}
return;
}
super.onActivityResult(requestCode, resultCode, data);
}
private void addPreferences() {
getPreferenceScreen().removeAll();
getPreferenceScreen().addPreference(
createAppPreference(getApplicationInfo().loadIcon(getPackageManager())));
// Differentiate compatibility changes into default enabled, default disabled and enabled
// after target sdk.
final CompatibilityChangeConfig configMappings = getAppChangeMappings();
final List<CompatibilityChangeInfo> enabledChanges = new ArrayList<>();
final List<CompatibilityChangeInfo> disabledChanges = new ArrayList<>();
final Map<Integer, List<CompatibilityChangeInfo>> targetSdkChanges = new TreeMap<>();
for (CompatibilityChangeInfo change : mChanges) {
if (change.getEnableAfterTargetSdk() != 0) {
List<CompatibilityChangeInfo> sdkChanges;
if (!targetSdkChanges.containsKey(change.getEnableAfterTargetSdk())) {
sdkChanges = new ArrayList<>();
targetSdkChanges.put(change.getEnableAfterTargetSdk(), sdkChanges);
} else {
sdkChanges = targetSdkChanges.get(change.getEnableAfterTargetSdk());
}
sdkChanges.add(change);
} else if (change.getDisabled()) {
disabledChanges.add(change);
} else {
enabledChanges.add(change);
}
}
createChangeCategoryPreference(enabledChanges, configMappings,
getString(R.string.platform_compat_default_enabled_title));
createChangeCategoryPreference(disabledChanges, configMappings,
getString(R.string.platform_compat_default_disabled_title));
for (Integer sdk : targetSdkChanges.keySet()) {
createChangeCategoryPreference(targetSdkChanges.get(sdk), configMappings,
getString(R.string.platform_compat_target_sdk_title, sdk));
}
}
private CompatibilityChangeConfig getAppChangeMappings() {
try {
final ApplicationInfo applicationInfo = getApplicationInfo();
return getPlatformCompat().getAppConfig(applicationInfo);
} catch (RemoteException e) {
throw new RuntimeException("Could not get app config!", e);
}
}
/**
* Create a {@link Preference} for a changeId.
*
* <p>The {@link Preference} is a toggle switch that can enable or disable the given change for
* the currently selected app.</p>
*/
Preference createPreferenceForChange(Context context, CompatibilityChangeInfo change,
CompatibilityChangeConfig configMappings) {
final boolean currentValue = configMappings.isChangeEnabled(change.getId());
final SwitchPreference item = new SwitchPreference(context);
final String changeName =
change.getName() != null ? change.getName() : "Change_" + change.getId();
item.setSummary(changeName);
item.setKey(changeName);
item.setEnabled(true);
item.setChecked(currentValue);
item.setOnPreferenceChangeListener(
new CompatChangePreferenceChangeListener(change.getId()));
return item;
}
/**
* Get {@link ApplicationInfo} for the currently selected app.
*
* @return an {@link ApplicationInfo} instance.
*/
ApplicationInfo getApplicationInfo() {
try {
return getPackageManager().getApplicationInfo(mSelectedApp, 0);
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException("Could not get ApplicationInfo for selected app!", e);
}
}
/**
* Create a {@link Preference} for the selected app.
*
* <p>The {@link Preference} contains the icon, package name and target SDK for the selected
* app. Selecting this preference will also re-trigger the app selection dialog.</p>
*/
Preference createAppPreference(Drawable icon) {
final ApplicationInfo applicationInfo = getApplicationInfo();
final Preference appPreference = new Preference(getPreferenceScreen().getContext());
appPreference.setIcon(icon);
appPreference.setSummary(mSelectedApp
+ " SDK "
+ applicationInfo.targetSdkVersion);
appPreference.setKey(mSelectedApp);
appPreference.setOnPreferenceClickListener(
preference -> {
startAppPicker();
return true;
});
return appPreference;
}
PreferenceCategory createChangeCategoryPreference(List<CompatibilityChangeInfo> changes,
CompatibilityChangeConfig configMappings, String title) {
final PreferenceCategory category =
new PreferenceCategory(getPreferenceScreen().getContext());
category.setTitle(title);
getPreferenceScreen().addPreference(category);
addChangePreferencesToCategory(changes, category, configMappings);
return category;
}
private void addChangePreferencesToCategory(List<CompatibilityChangeInfo> changes,
PreferenceCategory category, CompatibilityChangeConfig configMappings) {
for (CompatibilityChangeInfo change : changes) {
final Preference preference = createPreferenceForChange(getPreferenceScreen().getContext(),
change, configMappings);
category.addPreference(preference);
}
}
private void startAppPicker() {
final Intent intent = new Intent(getContext(), AppPicker.class);
startActivityForResult(intent, REQUEST_COMPAT_CHANGE_APP);
}
private class CompatChangePreferenceChangeListener implements OnPreferenceChangeListener {
private final long changeId;
CompatChangePreferenceChangeListener(long changeId) {
this.changeId = changeId;
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
try {
final ArraySet<Long> enabled = new ArraySet<>();
final ArraySet<Long> disabled = new ArraySet<>();
if ((Boolean) newValue) {
enabled.add(changeId);
} else {
disabled.add(changeId);
}
final CompatibilityChangeConfig overrides =
new CompatibilityChangeConfig(new ChangeConfig(enabled, disabled));
getPlatformCompat().setOverrides(overrides, mSelectedApp);
} catch (RemoteException e) {
e.printStackTrace();
return false;
}
return true;
}
}
}

View File

@@ -30,6 +30,7 @@ com.android.settings.datausage.AppDataUsage
com.android.settings.datausage.DataUsageList
com.android.settings.datausage.DataUsageSummary
com.android.settings.datetime.timezone.TimeZoneSettings
com.android.settings.development.compat.PlatformCompatDashboard
com.android.settings.deviceinfo.PrivateVolumeSettings
com.android.settings.deviceinfo.PublicVolumeSettings
com.android.settings.deviceinfo.StorageProfileFragment

View File

@@ -0,0 +1,176 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.development.compat;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import android.compat.Compatibility.ChangeConfig;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.graphics.drawable.Drawable;
import android.os.RemoteException;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceScreen;
import androidx.preference.SwitchPreference;
import com.android.internal.compat.CompatibilityChangeConfig;
import com.android.internal.compat.CompatibilityChangeInfo;
import com.android.internal.compat.IPlatformCompat;
import com.android.settings.R;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@RunWith(RobolectricTestRunner.class)
public class PlatformCompatDashboardTest {
private PlatformCompatDashboard mDashboard;
@Mock
private IPlatformCompat mPlatformCompat;
private PreferenceScreen mPreferenceScreen;
@Mock
private ApplicationInfo mApplicationInfo;
@Mock
private PreferenceManager mPreferenceManager;
private Context mContext;
private CompatibilityChangeInfo[] mChanges;
private static final String APP_NAME = "foo.bar.baz";
@Before
public void setUp() throws RemoteException, NameNotFoundException {
MockitoAnnotations.initMocks(this);
mChanges = new CompatibilityChangeInfo[5];
mChanges[0] = new CompatibilityChangeInfo(1L, "Default_Enabled", 0, false);
mChanges[1] = new CompatibilityChangeInfo(2L, "Default_Disabled", 0, true);
mChanges[2] = new CompatibilityChangeInfo(3L, "Enabled_After_SDK_1_1", 1, false);
mChanges[3] = new CompatibilityChangeInfo(4L, "Enabled_After_SDK_1_2", 1, false);
mChanges[4] = new CompatibilityChangeInfo(5L, "Enabled_After_SDK_2", 2, false);
when(mPlatformCompat.listAllChanges()).thenReturn(mChanges);
mContext = RuntimeEnvironment.application;
mPreferenceManager = new PreferenceManager(mContext);
mPreferenceScreen = mPreferenceManager.createPreferenceScreen(mContext);
mApplicationInfo.packageName = APP_NAME;
mDashboard = spy(new PlatformCompatDashboard());
mDashboard.mSelectedApp = APP_NAME;
doReturn(mApplicationInfo).when(mDashboard).getApplicationInfo();
doReturn(mPlatformCompat).when(mDashboard).getPlatformCompat();
doReturn(mPreferenceScreen).when(mDashboard).getPreferenceScreen();
doReturn(mPreferenceManager).when(mDashboard).getPreferenceManager();
}
@Test
public void getHelpResource_shouldNotHaveHelpResource() {
assertThat(mDashboard.getHelpResource()).isEqualTo(0);
}
@Test
public void getPreferenceScreenResId_shouldBePlatformCompatSettingsResId() {
assertThat(mDashboard.getPreferenceScreenResId())
.isEqualTo(R.xml.platform_compat_settings);
}
@Test
public void createAppPreference_targetSdkEquals1_summaryReturnsAppNameAndTargetSdk() {
mApplicationInfo.targetSdkVersion = 1;
Preference appPreference = mDashboard.createAppPreference(any(Drawable.class));
assertThat(appPreference.getSummary()).isEqualTo(APP_NAME + " SDK 1");
}
@Test
public void createPreferenceForChange_defaultEnabledChange_createCheckedEntry() {
CompatibilityChangeInfo enabledChange = mChanges[0];
CompatibilityChangeConfig config = new CompatibilityChangeConfig(
new ChangeConfig(new HashSet<Long>(Arrays.asList(enabledChange.getId())),
new HashSet<Long>()));
Preference enabledPreference = mDashboard.createPreferenceForChange(mContext, enabledChange,
config);
SwitchPreference enabledSwitchPreference = (SwitchPreference) enabledPreference;
assertThat(enabledPreference.getSummary()).isEqualTo(mChanges[0].getName());
assertThat(enabledPreference instanceof SwitchPreference).isTrue();
assertThat(enabledSwitchPreference.isChecked()).isTrue();
}
@Test
public void createPreferenceForChange_defaultDisabledChange_createUncheckedEntry() {
CompatibilityChangeInfo disabledChange = mChanges[1];
CompatibilityChangeConfig config = new CompatibilityChangeConfig(
new ChangeConfig(new HashSet<Long>(),
new HashSet<Long>(Arrays.asList(disabledChange.getId()))));
Preference disabledPreference = mDashboard.createPreferenceForChange(mContext,
disabledChange, config);
assertThat(disabledPreference.getSummary()).isEqualTo(mChanges[1].getName());
SwitchPreference disabledSwitchPreference = (SwitchPreference) disabledPreference;
assertThat(disabledSwitchPreference.isChecked()).isFalse();
}
@Test
public void createChangeCategoryPreference_enabledAndDisabled_hasTitleAndEntries() {
Set<Long> enabledChanges = new HashSet<>();
enabledChanges.add(mChanges[0].getId());
enabledChanges.add(mChanges[1].getId());
enabledChanges.add(mChanges[2].getId());
Set<Long> disabledChanges = new HashSet<>();
disabledChanges.add(mChanges[3].getId());
disabledChanges.add(mChanges[4].getId());
CompatibilityChangeConfig config = new CompatibilityChangeConfig(
new ChangeConfig(enabledChanges, disabledChanges));
List<CompatibilityChangeInfo> changesToAdd = new ArrayList<>();
for (int i = 0; i < mChanges.length; ++i) {
changesToAdd.add(new CompatibilityChangeInfo(mChanges[i].getId(), mChanges[i]
.getName(),
mChanges[i].getEnableAfterTargetSdk(), mChanges[i].getDisabled()));
}
PreferenceCategory category = mDashboard.createChangeCategoryPreference(changesToAdd,
config, "foo");
assertThat(category.getTitle()).isEqualTo("foo");
assertThat(category.getPreferenceCount()).isEqualTo(mChanges.length);
for (int i = 0; i < mChanges.length; ++i) {
Preference childPreference = category.getPreference(i);
assertThat(childPreference instanceof SwitchPreference).isTrue();
}
}
}