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