blob: 7373cd7f15d48ef881782ed8def1b0e358ab4be3 [file] [log] [blame]
/*
* 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;
}
}
}
}