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