/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.car.settings.common; import static com.android.settingslib.drawer.SwitchesProvider.METHOD_GET_DYNAMIC_SUMMARY; import static com.android.settingslib.drawer.SwitchesProvider.METHOD_GET_DYNAMIC_TITLE; import static com.android.settingslib.drawer.SwitchesProvider.METHOD_GET_PROVIDER_ICON; import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_URI; import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY; import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY_URI; import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE; import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE_URI; import android.car.drivingstate.CarUxRestrictions; import android.content.ContentResolver; import android.content.Context; import android.content.IContentProvider; import android.content.Intent; import android.database.ContentObserver; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.text.TextUtils; import android.util.ArrayMap; import android.util.Pair; import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceGroup; import com.android.car.settings.R; import com.android.settingslib.drawer.TileUtils; import com.android.settingslib.utils.ThreadUtils; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * Injects preferences from other system applications at a placeholder location. The placeholder * should be a {@link PreferenceGroup} which sets the controller attribute to the fully qualified * name of this class. The preference should contain an intent which will be passed to * {@link ExtraSettingsLoader#loadPreferences(Intent)}. * * {@link com.android.settingslib.drawer.TileUtils#EXTRA_SETTINGS_ACTION} is automatically added * for backwards compatibility. Please make sure to use * {@link com.android.settingslib.drawer.TileUtils#IA_SETTINGS_ACTION} instead. * * <p>For example: * <pre>{@code * <PreferenceCategory * android:key="@string/pk_system_extra_settings" * android:title="@string/system_extra_settings_title" * settings:controller="com.android.settings.common.ExtraSettingsPreferenceController"> * <intent android:action="com.android.settings.action.IA_SETTINGS"> * <extra android:name="com.android.settings.category" * android:value="com.android.settings.category.system"/> * </intent> * </PreferenceCategory> * }</pre> * * @see ExtraSettingsLoader */ // TODO: investigate using SettingsLib Tiles. public class ExtraSettingsPreferenceController extends PreferenceController<PreferenceGroup> { private static final Logger LOG = new Logger(ExtraSettingsPreferenceController.class); @VisibleForTesting static final String META_DATA_DISTRACTION_OPTIMIZED = "distractionOptimized"; private Context mContext; private ContentResolver mContentResolver; private ExtraSettingsLoader mExtraSettingsLoader; private boolean mSettingsLoaded; @VisibleForTesting List<DynamicDataObserver> mObservers = new ArrayList<>(); public ExtraSettingsPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions restrictionInfo) { super(context, preferenceKey, fragmentController, restrictionInfo); mContext = context; mContentResolver = context.getContentResolver(); mExtraSettingsLoader = new ExtraSettingsLoader(context); } @VisibleForTesting(otherwise = VisibleForTesting.NONE) public void setExtraSettingsLoader(ExtraSettingsLoader extraSettingsLoader) { mExtraSettingsLoader = extraSettingsLoader; } @Override protected Class<PreferenceGroup> getPreferenceType() { return PreferenceGroup.class; } @Override protected void onApplyUxRestrictions(CarUxRestrictions uxRestrictions) { // If preference intents into an activity that's not distraction optimized, disable the // preference. This will override the UXRE flags config_ignore_ux_restrictions and // config_always_ignore_ux_restrictions because navigating to these non distraction // optimized activities will cause the blocking activity to come up, which dead ends the // user. for (int i = 0; i < getPreference().getPreferenceCount(); i++) { boolean restricted = false; Preference preference = getPreference().getPreference(i); if (uxRestrictions.isRequiresDistractionOptimization() && !preference.getExtras().getBoolean(META_DATA_DISTRACTION_OPTIMIZED) && getAvailabilityStatus() != AVAILABLE_FOR_VIEWING) { restricted = true; } preference.setEnabled(getAvailabilityStatus() != AVAILABLE_FOR_VIEWING); restrictPreference(preference, restricted); } } @Override protected void updateState(PreferenceGroup preference) { Map<Preference, Bundle> preferenceBundleMap = mExtraSettingsLoader.loadPreferences( preference.getIntent()); if (!mSettingsLoaded) { addExtraSettings(preferenceBundleMap); mSettingsLoaded = true; } preference.setVisible(preference.getPreferenceCount() > 0); } @Override protected void onStartInternal() { mObservers.forEach(observer -> { observer.register(mContentResolver, /* register= */ true); }); } @Override protected void onStopInternal() { mObservers.forEach(observer -> { observer.register(mContentResolver, /* register= */ false); }); } /** * Adds the extra settings from the system based on the intent that is passed in the preference * group. All the preferences that resolve these intents will be added in the preference group. * * @param preferenceBundleMap a map of {@link Preference} and {@link Bundle} representing * settings injected from system apps and their metadata. */ protected void addExtraSettings(Map<Preference, Bundle> preferenceBundleMap) { for (Preference setting : preferenceBundleMap.keySet()) { Bundle metaData = preferenceBundleMap.get(setting); boolean distractionOptimized = false; if (metaData.containsKey(META_DATA_DISTRACTION_OPTIMIZED)) { distractionOptimized = metaData.getBoolean(META_DATA_DISTRACTION_OPTIMIZED); } setting.getExtras().putBoolean(META_DATA_DISTRACTION_OPTIMIZED, distractionOptimized); getDynamicData(setting, metaData); getPreference().addPreference(setting); } } /** * Retrieve dynamic injected preference data and create observers for updates. */ protected void getDynamicData(Preference preference, Bundle metaData) { if (metaData.containsKey(META_DATA_PREFERENCE_TITLE_URI)) { // Set a placeholder title before starting to fetch real title to prevent vertical // preference shift. preference.setTitle(R.string.empty_placeholder); Uri uri = ExtraSettingsUtil.getCompleteUri(metaData, META_DATA_PREFERENCE_TITLE_URI, METHOD_GET_DYNAMIC_TITLE); refreshTitle(uri, preference); mObservers.add( new DynamicDataObserver(METHOD_GET_DYNAMIC_TITLE, uri, metaData, preference)); } if (metaData.containsKey(META_DATA_PREFERENCE_SUMMARY_URI)) { // Set a placeholder summary before starting to fetch real summary to prevent vertical // preference shift. preference.setSummary(R.string.empty_placeholder); Uri uri = ExtraSettingsUtil.getCompleteUri(metaData, META_DATA_PREFERENCE_SUMMARY_URI, METHOD_GET_DYNAMIC_SUMMARY); refreshSummary(uri, preference); mObservers.add( new DynamicDataObserver(METHOD_GET_DYNAMIC_SUMMARY, uri, metaData, preference)); } if (metaData.containsKey(META_DATA_PREFERENCE_ICON_URI)) { // Set a placeholder icon before starting to fetch real icon to prevent horizontal // preference shift. preference.setIcon(R.drawable.ic_placeholder); Uri uri = ExtraSettingsUtil.getCompleteUri(metaData, META_DATA_PREFERENCE_ICON_URI, METHOD_GET_PROVIDER_ICON); refreshIcon(uri, metaData, preference); mObservers.add( new DynamicDataObserver(METHOD_GET_PROVIDER_ICON, uri, metaData, preference)); } } @VisibleForTesting void executeBackgroundTask(Runnable r) { ThreadUtils.postOnBackgroundThread(r); } @VisibleForTesting void executeUiTask(Runnable r) { ThreadUtils.postOnMainThread(r); } private void refreshTitle(Uri uri, Preference preference) { executeBackgroundTask(() -> { Map<String, IContentProvider> providerMap = new ArrayMap<>(); String titleFromUri = TileUtils.getTextFromUri( mContext, uri, providerMap, META_DATA_PREFERENCE_TITLE); if (!TextUtils.equals(titleFromUri, preference.getTitle())) { executeUiTask(() -> preference.setTitle(titleFromUri)); } }); } private void refreshSummary(Uri uri, Preference preference) { executeBackgroundTask(() -> { Map<String, IContentProvider> providerMap = new ArrayMap<>(); String summaryFromUri = TileUtils.getTextFromUri( mContext, uri, providerMap, META_DATA_PREFERENCE_SUMMARY); if (!TextUtils.equals(summaryFromUri, preference.getSummary())) { executeUiTask(() -> preference.setSummary(summaryFromUri)); } }); } private void refreshIcon(Uri uri, Bundle metaData, Preference preference) { executeBackgroundTask(() -> { Intent intent = preference.getIntent(); String packageName = null; if (!TextUtils.isEmpty(intent.getPackage())) { packageName = intent.getPackage(); } else if (intent.getComponent() != null) { packageName = intent.getComponent().getPackageName(); } Map<String, IContentProvider> providerMap = new ArrayMap<>(); Pair<String, Integer> iconInfo = TileUtils.getIconFromUri( mContext, packageName, uri, providerMap); Drawable icon; if (iconInfo != null) { icon = ExtraSettingsUtil.createIcon(mContext, metaData, iconInfo.first, iconInfo.second); } else { LOG.w("Failed to get icon from uri " + uri); icon = ExtraSettingsUtil.createIcon(mContext, metaData, packageName, 0); } if (icon != null) { executeUiTask(() -> { preference.setIcon(icon); }); } }); } /** * Observer for updating injected dynamic data. */ private class DynamicDataObserver extends ContentObserver { private final String mMethod; private final Uri mUri; private final Bundle mMetaData; private final Preference mPreference; DynamicDataObserver(String method, Uri uri, Bundle metaData, Preference preference) { super(new Handler(Looper.getMainLooper())); mMethod = method; mUri = uri; mMetaData = metaData; mPreference = preference; } /** Registers or unregisters this observer to the given content resolver. */ void register(ContentResolver cr, boolean register) { if (register) { cr.registerContentObserver(mUri, /* notifyForDescendants= */ false, /* observer= */ this); } else { cr.unregisterContentObserver(this); } } @Override public void onChange(boolean selfChange) { switch (mMethod) { case METHOD_GET_DYNAMIC_TITLE: refreshTitle(mUri, mPreference); break; case METHOD_GET_DYNAMIC_SUMMARY: refreshSummary(mUri, mPreference); break; case METHOD_GET_PROVIDER_ICON: refreshIcon(mUri, mMetaData, mPreference); break; } } } }