• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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.car.settings.common;
18 
19 import static com.android.settingslib.drawer.SwitchesProvider.METHOD_GET_DYNAMIC_SUMMARY;
20 import static com.android.settingslib.drawer.SwitchesProvider.METHOD_GET_DYNAMIC_TITLE;
21 import static com.android.settingslib.drawer.SwitchesProvider.METHOD_GET_PROVIDER_ICON;
22 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_URI;
23 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY;
24 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY_URI;
25 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE;
26 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE_URI;
27 
28 import android.car.drivingstate.CarUxRestrictions;
29 import android.content.ContentResolver;
30 import android.content.Context;
31 import android.content.IContentProvider;
32 import android.content.Intent;
33 import android.database.ContentObserver;
34 import android.graphics.drawable.Drawable;
35 import android.net.Uri;
36 import android.os.Bundle;
37 import android.os.Handler;
38 import android.os.Looper;
39 import android.text.TextUtils;
40 import android.util.ArrayMap;
41 import android.util.Pair;
42 
43 import androidx.annotation.VisibleForTesting;
44 import androidx.preference.Preference;
45 import androidx.preference.PreferenceGroup;
46 
47 import com.android.car.settings.R;
48 import com.android.settingslib.drawer.TileUtils;
49 import com.android.settingslib.utils.ThreadUtils;
50 
51 import java.util.ArrayList;
52 import java.util.List;
53 import java.util.Map;
54 
55 /**
56  * Injects preferences from other system applications at a placeholder location. The placeholder
57  * should be a {@link PreferenceGroup} which sets the controller attribute to the fully qualified
58  * name of this class. The preference should contain an intent which will be passed to
59  * {@link ExtraSettingsLoader#loadPreferences(Intent)}.
60  *
61  * {@link com.android.settingslib.drawer.TileUtils#EXTRA_SETTINGS_ACTION} is automatically added
62  * for backwards compatibility. Please make sure to use
63  * {@link com.android.settingslib.drawer.TileUtils#IA_SETTINGS_ACTION} instead.
64  *
65  * <p>For example:
66  * <pre>{@code
67  * <PreferenceCategory
68  *     android:key="@string/pk_system_extra_settings"
69  *     android:title="@string/system_extra_settings_title"
70  *     settings:controller="com.android.settings.common.ExtraSettingsPreferenceController">
71  *     <intent android:action="com.android.settings.action.IA_SETTINGS">
72  *         <extra android:name="com.android.settings.category"
73  *                android:value="com.android.settings.category.system"/>
74  *     </intent>
75  * </PreferenceCategory>
76  * }</pre>
77  *
78  * @see ExtraSettingsLoader
79  */
80 // TODO: investigate using SettingsLib Tiles.
81 public class ExtraSettingsPreferenceController extends PreferenceController<PreferenceGroup> {
82     private static final Logger LOG = new Logger(ExtraSettingsPreferenceController.class);
83 
84     @VisibleForTesting
85     static final String META_DATA_DISTRACTION_OPTIMIZED = "distractionOptimized";
86 
87     private Context mContext;
88     private ContentResolver mContentResolver;
89     private ExtraSettingsLoader mExtraSettingsLoader;
90     private boolean mSettingsLoaded;
91     @VisibleForTesting
92     List<DynamicDataObserver> mObservers = new ArrayList<>();
93 
ExtraSettingsPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions restrictionInfo)94     public ExtraSettingsPreferenceController(Context context, String preferenceKey,
95             FragmentController fragmentController, CarUxRestrictions restrictionInfo) {
96         super(context, preferenceKey, fragmentController, restrictionInfo);
97         mContext = context;
98         mContentResolver = context.getContentResolver();
99         mExtraSettingsLoader = new ExtraSettingsLoader(context);
100     }
101 
102     @VisibleForTesting(otherwise = VisibleForTesting.NONE)
setExtraSettingsLoader(ExtraSettingsLoader extraSettingsLoader)103     public void setExtraSettingsLoader(ExtraSettingsLoader extraSettingsLoader) {
104         mExtraSettingsLoader = extraSettingsLoader;
105     }
106 
107     @Override
getPreferenceType()108     protected Class<PreferenceGroup> getPreferenceType() {
109         return PreferenceGroup.class;
110     }
111 
112     @Override
onApplyUxRestrictions(CarUxRestrictions uxRestrictions)113     protected void onApplyUxRestrictions(CarUxRestrictions uxRestrictions) {
114         // If preference intents into an activity that's not distraction optimized, disable the
115         // preference. This will override the UXRE flags config_ignore_ux_restrictions and
116         // config_always_ignore_ux_restrictions because navigating to these non distraction
117         // optimized activities will cause the blocking activity to come up, which dead ends the
118         // user.
119         for (int i = 0; i < getPreference().getPreferenceCount(); i++) {
120             boolean restricted = false;
121             Preference preference = getPreference().getPreference(i);
122             if (uxRestrictions.isRequiresDistractionOptimization()
123                     && !preference.getExtras().getBoolean(META_DATA_DISTRACTION_OPTIMIZED)
124                     && getAvailabilityStatus() != AVAILABLE_FOR_VIEWING) {
125                 restricted = true;
126             }
127             preference.setEnabled(getAvailabilityStatus() != AVAILABLE_FOR_VIEWING);
128             restrictPreference(preference, restricted);
129         }
130     }
131 
132     @Override
updateState(PreferenceGroup preference)133     protected void updateState(PreferenceGroup preference) {
134         Map<Preference, Bundle> preferenceBundleMap = mExtraSettingsLoader.loadPreferences(
135                 preference.getIntent());
136         if (!mSettingsLoaded) {
137             addExtraSettings(preferenceBundleMap);
138             mSettingsLoaded = true;
139         }
140         preference.setVisible(preference.getPreferenceCount() > 0);
141     }
142 
143     @Override
onStartInternal()144     protected void onStartInternal() {
145         mObservers.forEach(observer -> {
146             observer.register(mContentResolver, /* register= */ true);
147         });
148     }
149 
150     @Override
onStopInternal()151     protected void onStopInternal() {
152         mObservers.forEach(observer -> {
153             observer.register(mContentResolver, /* register= */ false);
154         });
155     }
156 
157     /**
158      * Adds the extra settings from the system based on the intent that is passed in the preference
159      * group. All the preferences that resolve these intents will be added in the preference group.
160      *
161      * @param preferenceBundleMap a map of {@link Preference} and {@link Bundle} representing
162      * settings injected from system apps and their metadata.
163      */
addExtraSettings(Map<Preference, Bundle> preferenceBundleMap)164     protected void addExtraSettings(Map<Preference, Bundle> preferenceBundleMap) {
165         for (Preference setting : preferenceBundleMap.keySet()) {
166             Bundle metaData = preferenceBundleMap.get(setting);
167             boolean distractionOptimized = false;
168             if (metaData.containsKey(META_DATA_DISTRACTION_OPTIMIZED)) {
169                 distractionOptimized =
170                         metaData.getBoolean(META_DATA_DISTRACTION_OPTIMIZED);
171             }
172             setting.getExtras().putBoolean(META_DATA_DISTRACTION_OPTIMIZED, distractionOptimized);
173             getDynamicData(setting, metaData);
174             getPreference().addPreference(setting);
175         }
176     }
177 
178     /**
179      * Retrieve dynamic injected preference data and create observers for updates.
180      */
getDynamicData(Preference preference, Bundle metaData)181     protected void getDynamicData(Preference preference, Bundle metaData) {
182         if (metaData.containsKey(META_DATA_PREFERENCE_TITLE_URI)) {
183             // Set a placeholder title before starting to fetch real title to prevent vertical
184             // preference shift.
185             preference.setTitle(R.string.empty_placeholder);
186             Uri uri = ExtraSettingsUtil.getCompleteUri(metaData, META_DATA_PREFERENCE_TITLE_URI,
187                     METHOD_GET_DYNAMIC_TITLE);
188             refreshTitle(uri, preference);
189             mObservers.add(
190                     new DynamicDataObserver(METHOD_GET_DYNAMIC_TITLE, uri, metaData, preference));
191         }
192         if (metaData.containsKey(META_DATA_PREFERENCE_SUMMARY_URI)) {
193             // Set a placeholder summary before starting to fetch real summary to prevent vertical
194             // preference shift.
195             preference.setSummary(R.string.empty_placeholder);
196             Uri uri = ExtraSettingsUtil.getCompleteUri(metaData, META_DATA_PREFERENCE_SUMMARY_URI,
197                     METHOD_GET_DYNAMIC_SUMMARY);
198             refreshSummary(uri, preference);
199             mObservers.add(
200                     new DynamicDataObserver(METHOD_GET_DYNAMIC_SUMMARY, uri, metaData, preference));
201         }
202         if (metaData.containsKey(META_DATA_PREFERENCE_ICON_URI)) {
203             // Set a placeholder icon before starting to fetch real icon to prevent horizontal
204             // preference shift.
205             preference.setIcon(R.drawable.ic_placeholder);
206             Uri uri = ExtraSettingsUtil.getCompleteUri(metaData, META_DATA_PREFERENCE_ICON_URI,
207                     METHOD_GET_PROVIDER_ICON);
208             refreshIcon(uri, metaData, preference);
209             mObservers.add(
210                     new DynamicDataObserver(METHOD_GET_PROVIDER_ICON, uri, metaData, preference));
211         }
212     }
213 
214     @VisibleForTesting
executeBackgroundTask(Runnable r)215     void executeBackgroundTask(Runnable r) {
216         ThreadUtils.postOnBackgroundThread(r);
217     }
218 
219     @VisibleForTesting
executeUiTask(Runnable r)220     void executeUiTask(Runnable r) {
221         ThreadUtils.postOnMainThread(r);
222     }
223 
refreshTitle(Uri uri, Preference preference)224     private void refreshTitle(Uri uri, Preference preference) {
225         executeBackgroundTask(() -> {
226             Map<String, IContentProvider> providerMap = new ArrayMap<>();
227             String titleFromUri = TileUtils.getTextFromUri(
228                     mContext, uri, providerMap, META_DATA_PREFERENCE_TITLE);
229             if (!TextUtils.equals(titleFromUri, preference.getTitle())) {
230                 executeUiTask(() -> preference.setTitle(titleFromUri));
231             }
232         });
233     }
234 
refreshSummary(Uri uri, Preference preference)235     private void refreshSummary(Uri uri, Preference preference) {
236         executeBackgroundTask(() -> {
237             Map<String, IContentProvider> providerMap = new ArrayMap<>();
238             String summaryFromUri = TileUtils.getTextFromUri(
239                     mContext, uri, providerMap, META_DATA_PREFERENCE_SUMMARY);
240             if (!TextUtils.equals(summaryFromUri, preference.getSummary())) {
241                 executeUiTask(() -> preference.setSummary(summaryFromUri));
242             }
243         });
244     }
245 
refreshIcon(Uri uri, Bundle metaData, Preference preference)246     private void refreshIcon(Uri uri, Bundle metaData, Preference preference) {
247         executeBackgroundTask(() -> {
248             Intent intent = preference.getIntent();
249             String packageName = null;
250             if (!TextUtils.isEmpty(intent.getPackage())) {
251                 packageName = intent.getPackage();
252             } else if (intent.getComponent() != null) {
253                 packageName = intent.getComponent().getPackageName();
254             }
255             Map<String, IContentProvider> providerMap = new ArrayMap<>();
256             Pair<String, Integer> iconInfo = TileUtils.getIconFromUri(
257                     mContext, packageName, uri, providerMap);
258             Drawable icon;
259             if (iconInfo != null) {
260                 icon = ExtraSettingsUtil.createIcon(mContext, metaData, iconInfo.first,
261                         iconInfo.second);
262             } else {
263                 LOG.w("Failed to get icon from uri " + uri);
264                 icon = ExtraSettingsUtil.createIcon(mContext, metaData, packageName, 0);
265             }
266             if (icon != null) {
267                 executeUiTask(() -> {
268                     preference.setIcon(icon);
269                 });
270             }
271         });
272     }
273 
274     /**
275      * Observer for updating injected dynamic data.
276      */
277     private class DynamicDataObserver extends ContentObserver {
278         private final String mMethod;
279         private final Uri mUri;
280         private final Bundle mMetaData;
281         private final Preference mPreference;
282 
DynamicDataObserver(String method, Uri uri, Bundle metaData, Preference preference)283         DynamicDataObserver(String method, Uri uri, Bundle metaData, Preference preference) {
284             super(new Handler(Looper.getMainLooper()));
285             mMethod = method;
286             mUri = uri;
287             mMetaData = metaData;
288             mPreference = preference;
289         }
290 
291         /** Registers or unregisters this observer to the given content resolver. */
register(ContentResolver cr, boolean register)292         void register(ContentResolver cr, boolean register) {
293             if (register) {
294                 cr.registerContentObserver(mUri, /* notifyForDescendants= */ false,
295                         /* observer= */ this);
296             } else {
297                 cr.unregisterContentObserver(this);
298             }
299         }
300 
301         @Override
onChange(boolean selfChange)302         public void onChange(boolean selfChange) {
303             switch (mMethod) {
304                 case METHOD_GET_DYNAMIC_TITLE:
305                     refreshTitle(mUri, mPreference);
306                     break;
307                 case METHOD_GET_DYNAMIC_SUMMARY:
308                     refreshSummary(mUri, mPreference);
309                     break;
310                 case METHOD_GET_PROVIDER_ICON:
311                     refreshIcon(mUri, mMetaData, mPreference);
312                     break;
313             }
314         }
315     }
316 }
317