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 
21 import android.app.settings.SettingsEnums;
22 import android.compat.Compatibility.ChangeConfig;
23 import android.content.Context;
24 import android.content.pm.ApplicationInfo;
25 import android.content.pm.PackageManager;
26 import android.graphics.drawable.Drawable;
27 import android.os.Bundle;
28 import android.os.RemoteException;
29 import android.os.ServiceManager;
30 import android.util.ArraySet;
31 
32 import androidx.annotation.VisibleForTesting;
33 import androidx.preference.Preference;
34 import androidx.preference.Preference.OnPreferenceChangeListener;
35 import androidx.preference.PreferenceCategory;
36 import androidx.preference.SwitchPreferenceCompat;
37 import androidx.preference.TwoStatePreference;
38 
39 import com.android.internal.compat.CompatibilityChangeConfig;
40 import com.android.internal.compat.CompatibilityChangeInfo;
41 import com.android.internal.compat.IPlatformCompat;
42 import com.android.settings.R;
43 import com.android.settings.dashboard.DashboardFragment;
44 
45 import java.util.ArrayList;
46 import java.util.List;
47 import java.util.Map;
48 import java.util.TreeMap;
49 
50 /**
51  * Dashboard for Platform Compat preferences.
52  */
53 public class PlatformCompatDashboard extends DashboardFragment {
54     private static final String TAG = "PlatformCompatDashboard";
55     public static final String COMPAT_APP = "compat_app";
56 
57     private IPlatformCompat mPlatformCompat;
58 
59     private CompatibilityChangeInfo[] mChanges;
60 
61     @VisibleForTesting
62     String mSelectedApp;
63 
64     @Override
getMetricsCategory()65     public int getMetricsCategory() {
66         return SettingsEnums.SETTINGS_PLATFORM_COMPAT_DASHBOARD;
67     }
68 
69     @Override
getLogTag()70     protected String getLogTag() {
71         return TAG;
72     }
73 
74     @Override
getPreferenceScreenResId()75     protected int getPreferenceScreenResId() {
76         return R.xml.platform_compat_settings;
77     }
78 
79     @Override
getHelpResource()80     public int getHelpResource() {
81         return 0;
82     }
83 
getPlatformCompat()84     IPlatformCompat getPlatformCompat() {
85         if (mPlatformCompat == null) {
86             mPlatformCompat = IPlatformCompat.Stub
87                     .asInterface(ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE));
88         }
89         return mPlatformCompat;
90     }
91 
92     @Override
onCreate(Bundle icicle)93     public void onCreate(Bundle icicle) {
94         super.onCreate(icicle);
95         try {
96             mChanges = getPlatformCompat().listUIChanges();
97         } catch (RemoteException e) {
98             throw new RuntimeException("Could not list changes!", e);
99         }
100     }
101 
102     @Override
onResume()103     public void onResume() {
104         super.onResume();
105         if (isFinishingOrDestroyed()) {
106             return;
107         }
108         Bundle arguments = getArguments();
109         if (arguments == null) {
110             finish();
111             return;
112         }
113         mSelectedApp = arguments.getString(COMPAT_APP);
114         try {
115             final ApplicationInfo applicationInfo = getApplicationInfo();
116             addPreferences(applicationInfo);
117         } catch (PackageManager.NameNotFoundException ignored) {
118             finish();
119         }
120     }
121 
addPreferences(ApplicationInfo applicationInfo)122     private void addPreferences(ApplicationInfo applicationInfo) {
123         getPreferenceScreen().removeAll();
124         getPreferenceScreen().addPreference(createAppPreference(applicationInfo));
125         // Differentiate compatibility changes into default enabled, default disabled and enabled
126         // after target sdk.
127         final CompatibilityChangeConfig configMappings = getAppChangeMappings();
128         final List<CompatibilityChangeInfo> enabledChanges = new ArrayList<>();
129         final List<CompatibilityChangeInfo> disabledChanges = new ArrayList<>();
130         final Map<Integer, List<CompatibilityChangeInfo>> targetSdkChanges = new TreeMap<>();
131         for (CompatibilityChangeInfo change : mChanges) {
132             if (change.getEnableSinceTargetSdk() > 0) {
133                 List<CompatibilityChangeInfo> sdkChanges;
134                 if (!targetSdkChanges.containsKey(change.getEnableSinceTargetSdk())) {
135                     sdkChanges = new ArrayList<>();
136                     targetSdkChanges.put(change.getEnableSinceTargetSdk(), sdkChanges);
137                 } else {
138                     sdkChanges = targetSdkChanges.get(change.getEnableSinceTargetSdk());
139                 }
140                 sdkChanges.add(change);
141             } else if (change.getDisabled()) {
142                 disabledChanges.add(change);
143             } else {
144                 enabledChanges.add(change);
145             }
146         }
147         createChangeCategoryPreference(enabledChanges, configMappings,
148                 getString(R.string.platform_compat_default_enabled_title));
149         createChangeCategoryPreference(disabledChanges, configMappings,
150                 getString(R.string.platform_compat_default_disabled_title));
151         for (Integer sdk : targetSdkChanges.keySet()) {
152             createChangeCategoryPreference(targetSdkChanges.get(sdk), configMappings,
153                     getString(R.string.platform_compat_target_sdk_title, sdk));
154         }
155     }
156 
getAppChangeMappings()157     private CompatibilityChangeConfig getAppChangeMappings() {
158         try {
159             final ApplicationInfo applicationInfo = getApplicationInfo();
160             return getPlatformCompat().getAppConfig(applicationInfo);
161         } catch (RemoteException | PackageManager.NameNotFoundException e) {
162             throw new RuntimeException("Could not get app config!", e);
163         }
164     }
165 
166     /**
167      * Create a {@link Preference} for a changeId.
168      *
169      * <p>The {@link Preference} is a toggle switch that can enable or disable the given change for
170      * the currently selected app.</p>
171      */
createPreferenceForChange(Context context, CompatibilityChangeInfo change, CompatibilityChangeConfig configMappings)172     Preference createPreferenceForChange(Context context, CompatibilityChangeInfo change,
173             CompatibilityChangeConfig configMappings) {
174         final boolean currentValue = configMappings.isChangeEnabled(change.getId());
175         final TwoStatePreference item = new SwitchPreferenceCompat(context);
176         final String changeName =
177                 change.getName() != null ? change.getName() : "Change_" + change.getId();
178         item.setSummary(changeName);
179         item.setKey(changeName);
180         boolean shouldEnable = true;
181         try {
182             shouldEnable = getPlatformCompat().getOverrideValidator()
183                            .getOverrideAllowedState(change.getId(), mSelectedApp)
184                            .state == ALLOWED;
185         } catch (RemoteException e) {
186             throw new RuntimeException("Could not check if change can be overridden for app.", e);
187         }
188         item.setEnabled(shouldEnable);
189         item.setChecked(currentValue);
190         item.setOnPreferenceChangeListener(
191                 new CompatChangePreferenceChangeListener(change.getId()));
192         return item;
193     }
194 
195     /**
196      * Get {@link ApplicationInfo} for the currently selected app.
197      *
198      * @return an {@link ApplicationInfo} instance.
199      */
getApplicationInfo()200     ApplicationInfo getApplicationInfo() throws PackageManager.NameNotFoundException {
201         return getPackageManager().getApplicationInfo(mSelectedApp, 0);
202     }
203 
204     /**
205      * Create a {@link Preference} for the selected app.
206      *
207      * <p>The {@link Preference} contains the icon, package name and target SDK for the selected
208      * app. Selecting this preference will also re-trigger the app selection dialog.</p>
209      */
createAppPreference(ApplicationInfo applicationInfo)210     Preference createAppPreference(ApplicationInfo applicationInfo) {
211         final Context context = getPreferenceScreen().getContext();
212         final Drawable icon = applicationInfo.loadIcon(context.getPackageManager());
213         final Preference appPreference = new Preference(context);
214         appPreference.setIcon(icon);
215         appPreference.setSummary(getString(R.string.platform_compat_selected_app_summary,
216                                          mSelectedApp, applicationInfo.targetSdkVersion));
217         return appPreference;
218     }
219 
createChangeCategoryPreference(List<CompatibilityChangeInfo> changes, CompatibilityChangeConfig configMappings, String title)220     PreferenceCategory createChangeCategoryPreference(List<CompatibilityChangeInfo> changes,
221             CompatibilityChangeConfig configMappings, String title) {
222         final PreferenceCategory category =
223                 new PreferenceCategory(getPreferenceScreen().getContext());
224         category.setTitle(title);
225         getPreferenceScreen().addPreference(category);
226         addChangePreferencesToCategory(changes, category, configMappings);
227         return category;
228     }
229 
addChangePreferencesToCategory(List<CompatibilityChangeInfo> changes, PreferenceCategory category, CompatibilityChangeConfig configMappings)230     private void addChangePreferencesToCategory(List<CompatibilityChangeInfo> changes,
231             PreferenceCategory category, CompatibilityChangeConfig configMappings) {
232         for (CompatibilityChangeInfo change : changes) {
233             final Preference preference = createPreferenceForChange(getPreferenceScreen().getContext(),
234                     change, configMappings);
235             category.addPreference(preference);
236         }
237     }
238 
239     private class CompatChangePreferenceChangeListener implements OnPreferenceChangeListener {
240         private final long changeId;
241 
CompatChangePreferenceChangeListener(long changeId)242         CompatChangePreferenceChangeListener(long changeId) {
243             this.changeId = changeId;
244         }
245 
246         @Override
onPreferenceChange(Preference preference, Object newValue)247         public boolean onPreferenceChange(Preference preference, Object newValue) {
248             try {
249                 final ArraySet<Long> enabled = new ArraySet<>();
250                 final ArraySet<Long> disabled = new ArraySet<>();
251                 if ((Boolean) newValue) {
252                     enabled.add(changeId);
253                 } else {
254                     disabled.add(changeId);
255                 }
256                 final CompatibilityChangeConfig overrides =
257                         new CompatibilityChangeConfig(new ChangeConfig(enabled, disabled));
258                 getPlatformCompat().setOverrides(overrides, mSelectedApp);
259             } catch (RemoteException e) {
260                 e.printStackTrace();
261                 return false;
262             }
263             return true;
264         }
265     }
266 }
267