1 /* 2 * Copyright 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.development.compat; 18 19 import static com.android.internal.compat.OverrideAllowedState.ALLOWED; 20 import static com.android.settings.development.DevelopmentOptionsActivityRequestCodes.REQUEST_COMPAT_CHANGE_APP; 21 22 import android.app.Activity; 23 import android.app.AlertDialog; 24 import android.app.settings.SettingsEnums; 25 import android.compat.Compatibility.ChangeConfig; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.pm.ApplicationInfo; 29 import android.content.pm.PackageManager; 30 import android.graphics.drawable.Drawable; 31 import android.os.Bundle; 32 import android.os.RemoteException; 33 import android.os.ServiceManager; 34 import android.util.ArraySet; 35 36 import androidx.annotation.VisibleForTesting; 37 import androidx.preference.Preference; 38 import androidx.preference.Preference.OnPreferenceChangeListener; 39 import androidx.preference.PreferenceCategory; 40 import androidx.preference.SwitchPreference; 41 42 import com.android.internal.compat.AndroidBuildClassifier; 43 import com.android.internal.compat.CompatibilityChangeConfig; 44 import com.android.internal.compat.CompatibilityChangeInfo; 45 import com.android.internal.compat.IPlatformCompat; 46 import com.android.settings.R; 47 import com.android.settings.dashboard.DashboardFragment; 48 import com.android.settings.development.AppPicker; 49 50 import java.util.ArrayList; 51 import java.util.List; 52 import java.util.Map; 53 import java.util.TreeMap; 54 55 56 /** 57 * Dashboard for Platform Compat preferences. 58 */ 59 public class PlatformCompatDashboard extends DashboardFragment { 60 private static final String TAG = "PlatformCompatDashboard"; 61 private static final String COMPAT_APP = "compat_app"; 62 63 private IPlatformCompat mPlatformCompat; 64 65 private CompatibilityChangeInfo[] mChanges; 66 67 private AndroidBuildClassifier mAndroidBuildClassifier = new AndroidBuildClassifier(); 68 69 @VisibleForTesting 70 String mSelectedApp; 71 72 @Override getMetricsCategory()73 public int getMetricsCategory() { 74 return SettingsEnums.SETTINGS_PLATFORM_COMPAT_DASHBOARD; 75 } 76 77 @Override getLogTag()78 protected String getLogTag() { 79 return TAG; 80 } 81 82 @Override getPreferenceScreenResId()83 protected int getPreferenceScreenResId() { 84 return R.xml.platform_compat_settings; 85 } 86 87 @Override getHelpResource()88 public int getHelpResource() { 89 return 0; 90 } 91 getPlatformCompat()92 IPlatformCompat getPlatformCompat() { 93 if (mPlatformCompat == null) { 94 mPlatformCompat = IPlatformCompat.Stub 95 .asInterface(ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE)); 96 } 97 return mPlatformCompat; 98 } 99 100 @Override onActivityCreated(Bundle savedInstanceState)101 public void onActivityCreated(Bundle savedInstanceState) { 102 super.onActivityCreated(savedInstanceState); 103 try { 104 mChanges = getPlatformCompat().listUIChanges(); 105 } catch (RemoteException e) { 106 throw new RuntimeException("Could not list changes!", e); 107 } 108 startAppPicker(); 109 } 110 111 @Override onSaveInstanceState(Bundle outState)112 public void onSaveInstanceState(Bundle outState) { 113 super.onSaveInstanceState(outState); 114 outState.putString(COMPAT_APP, mSelectedApp); 115 } 116 117 @Override onActivityResult(int requestCode, int resultCode, Intent data)118 public void onActivityResult(int requestCode, int resultCode, Intent data) { 119 if (requestCode == REQUEST_COMPAT_CHANGE_APP) { 120 if (resultCode == Activity.RESULT_OK) { 121 mSelectedApp = data.getAction(); 122 try { 123 final ApplicationInfo applicationInfo = getApplicationInfo(); 124 addPreferences(applicationInfo); 125 } catch (PackageManager.NameNotFoundException e) { 126 startAppPicker(); 127 } 128 } else if (resultCode == AppPicker.RESULT_NO_MATCHING_APPS) { 129 new AlertDialog.Builder(getContext()) 130 .setTitle(R.string.platform_compat_dialog_title_no_apps) 131 .setMessage(R.string.platform_compat_dialog_text_no_apps) 132 .setPositiveButton(R.string.okay, (dialog, which) -> finish()) 133 .setOnDismissListener(dialog -> finish()) 134 .setCancelable(false) 135 .show(); 136 } 137 return; 138 } 139 super.onActivityResult(requestCode, resultCode, data); 140 } 141 addPreferences(ApplicationInfo applicationInfo)142 private void addPreferences(ApplicationInfo applicationInfo) { 143 getPreferenceScreen().removeAll(); 144 getPreferenceScreen().addPreference(createAppPreference(applicationInfo)); 145 // Differentiate compatibility changes into default enabled, default disabled and enabled 146 // after target sdk. 147 final CompatibilityChangeConfig configMappings = getAppChangeMappings(); 148 final List<CompatibilityChangeInfo> enabledChanges = new ArrayList<>(); 149 final List<CompatibilityChangeInfo> disabledChanges = new ArrayList<>(); 150 final Map<Integer, List<CompatibilityChangeInfo>> targetSdkChanges = new TreeMap<>(); 151 for (CompatibilityChangeInfo change : mChanges) { 152 if (change.getEnableSinceTargetSdk() > 0) { 153 List<CompatibilityChangeInfo> sdkChanges; 154 if (!targetSdkChanges.containsKey(change.getEnableSinceTargetSdk())) { 155 sdkChanges = new ArrayList<>(); 156 targetSdkChanges.put(change.getEnableSinceTargetSdk(), sdkChanges); 157 } else { 158 sdkChanges = targetSdkChanges.get(change.getEnableSinceTargetSdk()); 159 } 160 sdkChanges.add(change); 161 } else if (change.getDisabled()) { 162 disabledChanges.add(change); 163 } else { 164 enabledChanges.add(change); 165 } 166 } 167 createChangeCategoryPreference(enabledChanges, configMappings, 168 getString(R.string.platform_compat_default_enabled_title)); 169 createChangeCategoryPreference(disabledChanges, configMappings, 170 getString(R.string.platform_compat_default_disabled_title)); 171 for (Integer sdk : targetSdkChanges.keySet()) { 172 createChangeCategoryPreference(targetSdkChanges.get(sdk), configMappings, 173 getString(R.string.platform_compat_target_sdk_title, sdk)); 174 } 175 } 176 getAppChangeMappings()177 private CompatibilityChangeConfig getAppChangeMappings() { 178 try { 179 final ApplicationInfo applicationInfo = getApplicationInfo(); 180 return getPlatformCompat().getAppConfig(applicationInfo); 181 } catch (RemoteException | PackageManager.NameNotFoundException e) { 182 throw new RuntimeException("Could not get app config!", e); 183 } 184 } 185 186 /** 187 * Create a {@link Preference} for a changeId. 188 * 189 * <p>The {@link Preference} is a toggle switch that can enable or disable the given change for 190 * the currently selected app.</p> 191 */ createPreferenceForChange(Context context, CompatibilityChangeInfo change, CompatibilityChangeConfig configMappings)192 Preference createPreferenceForChange(Context context, CompatibilityChangeInfo change, 193 CompatibilityChangeConfig configMappings) { 194 final boolean currentValue = configMappings.isChangeEnabled(change.getId()); 195 final SwitchPreference item = new SwitchPreference(context); 196 final String changeName = 197 change.getName() != null ? change.getName() : "Change_" + change.getId(); 198 item.setSummary(changeName); 199 item.setKey(changeName); 200 boolean shouldEnable = true; 201 try { 202 shouldEnable = getPlatformCompat().getOverrideValidator() 203 .getOverrideAllowedState(change.getId(), mSelectedApp) 204 .state == ALLOWED; 205 } catch (RemoteException e) { 206 throw new RuntimeException("Could not check if change can be overridden for app.", e); 207 } 208 item.setEnabled(shouldEnable); 209 item.setChecked(currentValue); 210 item.setOnPreferenceChangeListener( 211 new CompatChangePreferenceChangeListener(change.getId())); 212 return item; 213 } 214 215 /** 216 * Get {@link ApplicationInfo} for the currently selected app. 217 * 218 * @return an {@link ApplicationInfo} instance. 219 */ getApplicationInfo()220 ApplicationInfo getApplicationInfo() throws PackageManager.NameNotFoundException { 221 return getPackageManager().getApplicationInfo(mSelectedApp, 0); 222 } 223 224 /** 225 * Create a {@link Preference} for the selected app. 226 * 227 * <p>The {@link Preference} contains the icon, package name and target SDK for the selected 228 * app. Selecting this preference will also re-trigger the app selection dialog.</p> 229 */ createAppPreference(ApplicationInfo applicationInfo)230 Preference createAppPreference(ApplicationInfo applicationInfo) { 231 final Context context = getPreferenceScreen().getContext(); 232 final Drawable icon = applicationInfo.loadIcon(context.getPackageManager()); 233 final Preference appPreference = new Preference(context); 234 appPreference.setIcon(icon); 235 appPreference.setSummary(getString(R.string.platform_compat_selected_app_summary, 236 mSelectedApp, applicationInfo.targetSdkVersion)); 237 appPreference.setKey(mSelectedApp); 238 appPreference.setOnPreferenceClickListener( 239 preference -> { 240 startAppPicker(); 241 return true; 242 }); 243 return appPreference; 244 } 245 createChangeCategoryPreference(List<CompatibilityChangeInfo> changes, CompatibilityChangeConfig configMappings, String title)246 PreferenceCategory createChangeCategoryPreference(List<CompatibilityChangeInfo> changes, 247 CompatibilityChangeConfig configMappings, String title) { 248 final PreferenceCategory category = 249 new PreferenceCategory(getPreferenceScreen().getContext()); 250 category.setTitle(title); 251 getPreferenceScreen().addPreference(category); 252 addChangePreferencesToCategory(changes, category, configMappings); 253 return category; 254 } 255 addChangePreferencesToCategory(List<CompatibilityChangeInfo> changes, PreferenceCategory category, CompatibilityChangeConfig configMappings)256 private void addChangePreferencesToCategory(List<CompatibilityChangeInfo> changes, 257 PreferenceCategory category, CompatibilityChangeConfig configMappings) { 258 for (CompatibilityChangeInfo change : changes) { 259 final Preference preference = createPreferenceForChange(getPreferenceScreen().getContext(), 260 change, configMappings); 261 category.addPreference(preference); 262 } 263 } 264 startAppPicker()265 private void startAppPicker() { 266 final Intent intent = new Intent(getContext(), AppPicker.class) 267 .putExtra(AppPicker.EXTRA_INCLUDE_NOTHING, false); 268 // If build is neither userdebug nor eng, only include debuggable apps 269 final boolean debuggableBuild = mAndroidBuildClassifier.isDebuggableBuild(); 270 if (!debuggableBuild) { 271 intent.putExtra(AppPicker.EXTRA_DEBUGGABLE, true /* value */); 272 } 273 startActivityForResult(intent, REQUEST_COMPAT_CHANGE_APP); 274 } 275 276 private class CompatChangePreferenceChangeListener implements OnPreferenceChangeListener { 277 private final long changeId; 278 CompatChangePreferenceChangeListener(long changeId)279 CompatChangePreferenceChangeListener(long changeId) { 280 this.changeId = changeId; 281 } 282 283 @Override onPreferenceChange(Preference preference, Object newValue)284 public boolean onPreferenceChange(Preference preference, Object newValue) { 285 try { 286 final ArraySet<Long> enabled = new ArraySet<>(); 287 final ArraySet<Long> disabled = new ArraySet<>(); 288 if ((Boolean) newValue) { 289 enabled.add(changeId); 290 } else { 291 disabled.add(changeId); 292 } 293 final CompatibilityChangeConfig overrides = 294 new CompatibilityChangeConfig(new ChangeConfig(enabled, disabled)); 295 getPlatformCompat().setOverrides(overrides, mSelectedApp); 296 } catch (RemoteException e) { 297 e.printStackTrace(); 298 return false; 299 } 300 return true; 301 } 302 } 303 } 304