• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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