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