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