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.text.TextUtils; 35 import android.util.ArraySet; 36 37 import androidx.annotation.VisibleForTesting; 38 import androidx.preference.Preference; 39 import androidx.preference.Preference.OnPreferenceChangeListener; 40 import androidx.preference.PreferenceCategory; 41 import androidx.preference.SwitchPreference; 42 43 import com.android.internal.compat.AndroidBuildClassifier; 44 import com.android.internal.compat.CompatibilityChangeConfig; 45 import com.android.internal.compat.CompatibilityChangeInfo; 46 import com.android.internal.compat.IPlatformCompat; 47 import com.android.settings.R; 48 import com.android.settings.dashboard.DashboardFragment; 49 import com.android.settings.development.AppPicker; 50 51 import java.util.ArrayList; 52 import java.util.List; 53 import java.util.Map; 54 import java.util.TreeMap; 55 56 57 /** 58 * Dashboard for Platform Compat preferences. 59 */ 60 public class PlatformCompatDashboard extends DashboardFragment { 61 private static final String TAG = "PlatformCompatDashboard"; 62 private static final String COMPAT_APP = "compat_app"; 63 64 private IPlatformCompat mPlatformCompat; 65 66 private CompatibilityChangeInfo[] mChanges; 67 68 private AndroidBuildClassifier mAndroidBuildClassifier = new AndroidBuildClassifier(); 69 70 private boolean mShouldStartAppPickerOnResume = true; 71 72 @VisibleForTesting 73 String mSelectedApp; 74 75 @Override getMetricsCategory()76 public int getMetricsCategory() { 77 return SettingsEnums.SETTINGS_PLATFORM_COMPAT_DASHBOARD; 78 } 79 80 @Override getLogTag()81 protected String getLogTag() { 82 return TAG; 83 } 84 85 @Override getPreferenceScreenResId()86 protected int getPreferenceScreenResId() { 87 return R.xml.platform_compat_settings; 88 } 89 90 @Override getHelpResource()91 public int getHelpResource() { 92 return 0; 93 } 94 getPlatformCompat()95 IPlatformCompat getPlatformCompat() { 96 if (mPlatformCompat == null) { 97 mPlatformCompat = IPlatformCompat.Stub 98 .asInterface(ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE)); 99 } 100 return mPlatformCompat; 101 } 102 103 @Override onCreate(Bundle icicle)104 public void onCreate(Bundle icicle) { 105 super.onCreate(icicle); 106 try { 107 mChanges = getPlatformCompat().listUIChanges(); 108 } catch (RemoteException e) { 109 throw new RuntimeException("Could not list changes!", e); 110 } 111 if (icicle != null) { 112 mShouldStartAppPickerOnResume = false; 113 mSelectedApp = icicle.getString(COMPAT_APP); 114 } 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 mShouldStartAppPickerOnResume = false; 121 switch (resultCode) { 122 case Activity.RESULT_OK: 123 mSelectedApp = data.getAction(); 124 break; 125 case Activity.RESULT_CANCELED: 126 if (TextUtils.isEmpty(mSelectedApp)) { 127 finish(); 128 } 129 break; 130 case AppPicker.RESULT_NO_MATCHING_APPS: 131 mSelectedApp = null; 132 break; 133 } 134 return; 135 } 136 super.onActivityResult(requestCode, resultCode, data); 137 } 138 139 @Override onResume()140 public void onResume() { 141 super.onResume(); 142 if (isFinishingOrDestroyed()) { 143 return; 144 } 145 if (!mShouldStartAppPickerOnResume) { 146 if (TextUtils.isEmpty(mSelectedApp)) { 147 new AlertDialog.Builder(getContext()) 148 .setTitle(R.string.platform_compat_dialog_title_no_apps) 149 .setMessage(R.string.platform_compat_dialog_text_no_apps) 150 .setPositiveButton(R.string.okay, (dialog, which) -> finish()) 151 .setOnDismissListener(dialog -> finish()) 152 .setCancelable(false) 153 .show(); 154 return; 155 } 156 try { 157 final ApplicationInfo applicationInfo = getApplicationInfo(); 158 addPreferences(applicationInfo); 159 return; 160 } catch (PackageManager.NameNotFoundException e) { 161 mShouldStartAppPickerOnResume = true; 162 mSelectedApp = null; 163 } 164 } 165 startAppPicker(); 166 } 167 168 @Override onSaveInstanceState(Bundle outState)169 public void onSaveInstanceState(Bundle outState) { 170 super.onSaveInstanceState(outState); 171 outState.putString(COMPAT_APP, mSelectedApp); 172 } 173 addPreferences(ApplicationInfo applicationInfo)174 private void addPreferences(ApplicationInfo applicationInfo) { 175 getPreferenceScreen().removeAll(); 176 getPreferenceScreen().addPreference(createAppPreference(applicationInfo)); 177 // Differentiate compatibility changes into default enabled, default disabled and enabled 178 // after target sdk. 179 final CompatibilityChangeConfig configMappings = getAppChangeMappings(); 180 final List<CompatibilityChangeInfo> enabledChanges = new ArrayList<>(); 181 final List<CompatibilityChangeInfo> disabledChanges = new ArrayList<>(); 182 final Map<Integer, List<CompatibilityChangeInfo>> targetSdkChanges = new TreeMap<>(); 183 for (CompatibilityChangeInfo change : mChanges) { 184 if (change.getEnableSinceTargetSdk() > 0) { 185 List<CompatibilityChangeInfo> sdkChanges; 186 if (!targetSdkChanges.containsKey(change.getEnableSinceTargetSdk())) { 187 sdkChanges = new ArrayList<>(); 188 targetSdkChanges.put(change.getEnableSinceTargetSdk(), sdkChanges); 189 } else { 190 sdkChanges = targetSdkChanges.get(change.getEnableSinceTargetSdk()); 191 } 192 sdkChanges.add(change); 193 } else if (change.getDisabled()) { 194 disabledChanges.add(change); 195 } else { 196 enabledChanges.add(change); 197 } 198 } 199 createChangeCategoryPreference(enabledChanges, configMappings, 200 getString(R.string.platform_compat_default_enabled_title)); 201 createChangeCategoryPreference(disabledChanges, configMappings, 202 getString(R.string.platform_compat_default_disabled_title)); 203 for (Integer sdk : targetSdkChanges.keySet()) { 204 createChangeCategoryPreference(targetSdkChanges.get(sdk), configMappings, 205 getString(R.string.platform_compat_target_sdk_title, sdk)); 206 } 207 } 208 getAppChangeMappings()209 private CompatibilityChangeConfig getAppChangeMappings() { 210 try { 211 final ApplicationInfo applicationInfo = getApplicationInfo(); 212 return getPlatformCompat().getAppConfig(applicationInfo); 213 } catch (RemoteException | PackageManager.NameNotFoundException e) { 214 throw new RuntimeException("Could not get app config!", e); 215 } 216 } 217 218 /** 219 * Create a {@link Preference} for a changeId. 220 * 221 * <p>The {@link Preference} is a toggle switch that can enable or disable the given change for 222 * the currently selected app.</p> 223 */ createPreferenceForChange(Context context, CompatibilityChangeInfo change, CompatibilityChangeConfig configMappings)224 Preference createPreferenceForChange(Context context, CompatibilityChangeInfo change, 225 CompatibilityChangeConfig configMappings) { 226 final boolean currentValue = configMappings.isChangeEnabled(change.getId()); 227 final SwitchPreference item = new SwitchPreference(context); 228 final String changeName = 229 change.getName() != null ? change.getName() : "Change_" + change.getId(); 230 item.setSummary(changeName); 231 item.setKey(changeName); 232 boolean shouldEnable = true; 233 try { 234 shouldEnable = getPlatformCompat().getOverrideValidator() 235 .getOverrideAllowedState(change.getId(), mSelectedApp) 236 .state == ALLOWED; 237 } catch (RemoteException e) { 238 throw new RuntimeException("Could not check if change can be overridden for app.", e); 239 } 240 item.setEnabled(shouldEnable); 241 item.setChecked(currentValue); 242 item.setOnPreferenceChangeListener( 243 new CompatChangePreferenceChangeListener(change.getId())); 244 return item; 245 } 246 247 /** 248 * Get {@link ApplicationInfo} for the currently selected app. 249 * 250 * @return an {@link ApplicationInfo} instance. 251 */ getApplicationInfo()252 ApplicationInfo getApplicationInfo() throws PackageManager.NameNotFoundException { 253 return getPackageManager().getApplicationInfo(mSelectedApp, 0); 254 } 255 256 /** 257 * Create a {@link Preference} for the selected app. 258 * 259 * <p>The {@link Preference} contains the icon, package name and target SDK for the selected 260 * app. Selecting this preference will also re-trigger the app selection dialog.</p> 261 */ createAppPreference(ApplicationInfo applicationInfo)262 Preference createAppPreference(ApplicationInfo applicationInfo) { 263 final Context context = getPreferenceScreen().getContext(); 264 final Drawable icon = applicationInfo.loadIcon(context.getPackageManager()); 265 final Preference appPreference = new Preference(context); 266 appPreference.setIcon(icon); 267 appPreference.setSummary(getString(R.string.platform_compat_selected_app_summary, 268 mSelectedApp, applicationInfo.targetSdkVersion)); 269 appPreference.setKey(mSelectedApp); 270 appPreference.setOnPreferenceClickListener( 271 preference -> { 272 startAppPicker(); 273 return true; 274 }); 275 return appPreference; 276 } 277 createChangeCategoryPreference(List<CompatibilityChangeInfo> changes, CompatibilityChangeConfig configMappings, String title)278 PreferenceCategory createChangeCategoryPreference(List<CompatibilityChangeInfo> changes, 279 CompatibilityChangeConfig configMappings, String title) { 280 final PreferenceCategory category = 281 new PreferenceCategory(getPreferenceScreen().getContext()); 282 category.setTitle(title); 283 getPreferenceScreen().addPreference(category); 284 addChangePreferencesToCategory(changes, category, configMappings); 285 return category; 286 } 287 addChangePreferencesToCategory(List<CompatibilityChangeInfo> changes, PreferenceCategory category, CompatibilityChangeConfig configMappings)288 private void addChangePreferencesToCategory(List<CompatibilityChangeInfo> changes, 289 PreferenceCategory category, CompatibilityChangeConfig configMappings) { 290 for (CompatibilityChangeInfo change : changes) { 291 final Preference preference = createPreferenceForChange(getPreferenceScreen().getContext(), 292 change, configMappings); 293 category.addPreference(preference); 294 } 295 } 296 startAppPicker()297 private void startAppPicker() { 298 final Intent intent = new Intent(getContext(), AppPicker.class) 299 .putExtra(AppPicker.EXTRA_INCLUDE_NOTHING, false); 300 // If build is neither userdebug nor eng, only include debuggable apps 301 final boolean debuggableBuild = mAndroidBuildClassifier.isDebuggableBuild(); 302 if (!debuggableBuild) { 303 intent.putExtra(AppPicker.EXTRA_DEBUGGABLE, true /* value */); 304 } 305 startActivityForResult(intent, REQUEST_COMPAT_CHANGE_APP); 306 } 307 308 private class CompatChangePreferenceChangeListener implements OnPreferenceChangeListener { 309 private final long changeId; 310 CompatChangePreferenceChangeListener(long changeId)311 CompatChangePreferenceChangeListener(long changeId) { 312 this.changeId = changeId; 313 } 314 315 @Override onPreferenceChange(Preference preference, Object newValue)316 public boolean onPreferenceChange(Preference preference, Object newValue) { 317 try { 318 final ArraySet<Long> enabled = new ArraySet<>(); 319 final ArraySet<Long> disabled = new ArraySet<>(); 320 if ((Boolean) newValue) { 321 enabled.add(changeId); 322 } else { 323 disabled.add(changeId); 324 } 325 final CompatibilityChangeConfig overrides = 326 new CompatibilityChangeConfig(new ChangeConfig(enabled, disabled)); 327 getPlatformCompat().setOverrides(overrides, mSelectedApp); 328 } catch (RemoteException e) { 329 e.printStackTrace(); 330 return false; 331 } 332 return true; 333 } 334 } 335 } 336