DashboardFeatureProviderImpl
package com.android.settings.dashboard;
import static android.content.Intent.EXTRA_USER;
import static com.android.settingslib.drawer.SwitchesProvider.EXTRA_SWITCH_CHECKED_STATE;
import static com.android.settingslib.drawer.SwitchesProvider.EXTRA_SWITCH_SET_CHECKED_ERROR;
import static com.android.settingslib.drawer.SwitchesProvider.EXTRA_SWITCH_SET_CHECKED_ERROR_MESSAGE;
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.SwitchesProvider.METHOD_IS_CHECKED;
import static com.android.settingslib.drawer.SwitchesProvider.METHOD_ON_CHECKED_CHANGED;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_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_SWITCH_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.app.PendingIntent;
import android.app.settings.SettingsEnums;
import android.content.ComponentName;
import android.content.Context;
import android.content.IContentProvider;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Bundle;
import android.os.UserHandle;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Pair;
import android.widget.Toast;
import androidx.annotation.VisibleForTesting;
import androidx.fragment.app.FragmentActivity;
import androidx.preference.Preference;
import androidx.preference.TwoStatePreference;
import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.Utils;
import com.android.settings.activityembedding.ActivityEmbeddingRulesController;
import com.android.settings.activityembedding.ActivityEmbeddingUtils;
import com.android.settings.dashboard.profileselector.ProfileSelectDialog;
import com.android.settings.homepage.TopLevelHighlightMixin;
import com.android.settings.homepage.TopLevelSettings;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.PrimarySwitchPreference;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.drawable.UserIconDrawable;
import com.android.settingslib.drawer.ActivityTile;
import com.android.settingslib.drawer.CategoryKey;
import com.android.settingslib.drawer.DashboardCategory;
import com.android.settingslib.drawer.Tile;
import com.android.settingslib.drawer.TileUtils;
import com.android.settingslib.utils.ThreadUtils;
import com.android.settingslib.widget.AdaptiveIcon;
import com.google.common.collect.Iterables;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Impl for {@code DashboardFeatureProvider}.
*/
public class DashboardFeatureProviderImpl implements DashboardFeatureProvider {
private static final String TAG = "DashboardFeatureImpl";
private static final String DASHBOARD_TILE_PREF_KEY_PREFIX = "dashboard_tile_pref_";
private static final String META_DATA_KEY_INTENT_ACTION = "com.android.settings.intent.action";
protected final Context mContext;
private final MetricsFeatureProvider mMetricsFeatureProvider;
private final CategoryManager mCategoryManager;
private final PackageManager mPackageManager;
public DashboardFeatureProviderImpl(Context context) {
mContext = context.getApplicationContext();
mCategoryManager = CategoryManager.get(context);
mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
mPackageManager = context.getPackageManager();
}
@Override
public DashboardCategory getTilesForCategory(String key) {
return mCategoryManager.getTilesByCategory(mContext, key);
}
@Override
public List<DashboardCategory> getAllCategories() {
return mCategoryManager.getCategories(mContext);
}
@Override
public String getDashboardKeyForTile(Tile tile) {
if (tile == null) {
return null;
}
if (tile.hasKey()) {
return tile.getKey(mContext);
}
final StringBuilder sb = new StringBuilder(DASHBOARD_TILE_PREF_KEY_PREFIX);
final ComponentName component = tile.getIntent().getComponent();
sb.append(component.getClassName());
return sb.toString();
}
@Override
public List<DynamicDataObserver> bindPreferenceToTileAndGetObservers(FragmentActivity activity,
DashboardFragment fragment, boolean forceRoundedIcon, Preference pref, Tile tile,
String key, int baseOrder) {
if (pref == null) {
return null;
}
if (!TextUtils.isEmpty(key)) {
pref.setKey(key);
} else {
pref.setKey(getDashboardKeyForTile(tile));
}
final List<DynamicDataObserver> outObservers = new ArrayList<>();
DynamicDataObserver observer = bindTitleAndGetObserver(pref, tile);
if (observer != null) {
outObservers.add(observer);
}
observer = bindSummaryAndGetObserver(pref, tile);
if (observer != null) {
outObservers.add(observer);
}
observer = bindSwitchAndGetObserver(pref, tile);
if (observer != null) {
outObservers.add(observer);
}
//*/ freeme.caoguofeng, 20230721. Settings-securityInject: add injection icon for system apps
if(!bindInjectionIcon(pref, tile))
//*/
bindIcon(pref, tile, forceRoundedIcon);
if (tile.hasPendingIntent()) {
// Pending intent cannot be launched within the settings app panel, and will thus always
// be executed directly.
pref.setOnPreferenceClickListener(preference -> {
launchPendingIntentOrSelectProfile(activity, tile, fragment.getMetricsCategory());
return true;
});
} else if (tile instanceof ActivityTile) {
final int sourceMetricsCategory = fragment.getMetricsCategory();
final Bundle metadata = tile.getMetaData();
String clsName = null;
String action = null;
if (metadata != null) {
clsName = metadata.getString(SettingsActivity.META_DATA_KEY_FRAGMENT_CLASS);
action = metadata.getString(META_DATA_KEY_INTENT_ACTION);
}
if (!TextUtils.isEmpty(clsName)) {
pref.setFragment(clsName);
} else {
final Intent intent = new Intent(tile.getIntent());
intent.putExtra(MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY,
sourceMetricsCategory);
if (action != null) {
intent.setAction(action);
}
// Register the rule for injected apps.
if (fragment instanceof TopLevelSettings) {
ActivityEmbeddingRulesController.registerTwoPanePairRuleForSettingsHome(
mContext,
new ComponentName(tile.getPackageName(), tile.getComponentName()),
action,
true /* clearTop */);
}
pref.setOnPreferenceClickListener(preference -> {
TopLevelHighlightMixin highlightMixin = null;
boolean isDuplicateClick = false;
if (fragment instanceof TopLevelSettings
&& ActivityEmbeddingUtils.isEmbeddingActivityEnabled(mContext)) {
// Highlight the preference whenever it's clicked
final TopLevelSettings topLevelSettings = (TopLevelSettings) fragment;
highlightMixin = topLevelSettings.getHighlightMixin();
isDuplicateClick = topLevelSettings.isDuplicateClick(preference);
topLevelSettings.setHighlightPreferenceKey(key);
}
launchIntentOrSelectProfile(activity, tile, intent, sourceMetricsCategory,
highlightMixin, isDuplicateClick);
return true;
});
}
}
if (tile.hasOrder()) {
final String skipOffsetPackageName = activity.getPackageName();
final int order = tile.getOrder();
boolean shouldSkipBaseOrderOffset = TextUtils.equals(
skipOffsetPackageName, tile.getIntent().getComponent().getPackageName());
if (shouldSkipBaseOrderOffset || baseOrder == Preference.DEFAULT_ORDER) {
pref.setOrder(order);
} else {
pref.setOrder(order + baseOrder);
}
}
return outObservers.isEmpty() ? null : outObservers;
}
@Override
public void openTileIntent(FragmentActivity activity, Tile tile) {
if (tile == null) {
Intent intent = new Intent(Settings.ACTION_SETTINGS)
.setPackage(mContext.getPackageName())
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
mContext.startActivity(intent);
return;
}
final Intent intent = new Intent(tile.getIntent())
.putExtra(MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY,
SettingsEnums.DASHBOARD_SUMMARY)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
launchIntentOrSelectProfile(activity, tile, intent, SettingsEnums.DASHBOARD_SUMMARY,
/* highlightMixin= */ null, /* isDuplicateClick= */ false);
}
private DynamicDataObserver createDynamicDataObserver(String method, Uri uri, Preference pref) {
return new DynamicDataObserver() {
@Override
public Uri getUri() {
return uri;
}
@Override
public void onDataChanged() {
switch (method) {
case METHOD_GET_DYNAMIC_TITLE:
refreshTitle(uri, pref, this);
break;
case METHOD_GET_DYNAMIC_SUMMARY:
refreshSummary(uri, pref, this);
break;
case METHOD_IS_CHECKED:
refreshSwitch(uri, pref, this);
break;
}
}
};
}
private DynamicDataObserver bindTitleAndGetObserver(Preference preference, Tile tile) {
final CharSequence title = tile.getTitle(mContext.getApplicationContext());
if (title != null) {
preference.setTitle(title);
return null;
}
if (tile.getMetaData() != null && tile.getMetaData().containsKey(
META_DATA_PREFERENCE_TITLE_URI)) {
// Set a placeholder title before starting to fetch real title, this is necessary
// to avoid preference height change.
if (preference.getTitle() == null) {
preference.setTitle(R.string.summary_placeholder);
}
final Uri uri = TileUtils.getCompleteUri(tile, META_DATA_PREFERENCE_TITLE_URI,
METHOD_GET_DYNAMIC_TITLE);
return createDynamicDataObserver(METHOD_GET_DYNAMIC_TITLE, uri, preference);
}
return null;
}
private void refreshTitle(Uri uri, Preference preference, DynamicDataObserver observer) {
ThreadUtils.postOnBackgroundThread(() -> {
final Map<String, IContentProvider> providerMap = new ArrayMap<>();
final String titleFromUri = TileUtils.getTextFromUri(
mContext, uri, providerMap, META_DATA_PREFERENCE_TITLE);
if (!TextUtils.equals(titleFromUri, preference.getTitle())) {
//*/ freeme.caoguofeng, 20211018, prevent title be null
if (TextUtils.isEmpty(titleFromUri)) return;
//*/
/*/ freeme.yangtengfei, 20241120. Settings-homepage: dynamic insert show blank
observer.post(() -> preference.setTitle(titleFromUri));
/*/
observer.post(() -> {
preference.setTitle(titleFromUri);
preference.setVisible(!TextUtils.isEmpty(titleFromUri));
});
//*/
}
});
}
private DynamicDataObserver bindSummaryAndGetObserver(Preference preference, Tile tile) {
final CharSequence summary = tile.getSummary(mContext);
if (summary != null) {
preference.setSummary(summary);
} else if (tile.getMetaData() != null
&& tile.getMetaData().containsKey(META_DATA_PREFERENCE_SUMMARY_URI)) {
// Set a placeholder summary before starting to fetch real summary, this is necessary
// to avoid preference height change.
if (preference.getSummary() == null) {
preference.setSummary(R.string.summary_placeholder);
}
final Uri uri = TileUtils.getCompleteUri(tile, META_DATA_PREFERENCE_SUMMARY_URI,
METHOD_GET_DYNAMIC_SUMMARY);
return createDynamicDataObserver(METHOD_GET_DYNAMIC_SUMMARY, uri, preference);
}
return null;
}
private void refreshSummary(Uri uri, Preference preference, DynamicDataObserver observer) {
ThreadUtils.postOnBackgroundThread(() -> {
final Map<String, IContentProvider> providerMap = new ArrayMap<>();
final String summaryFromUri = TileUtils.getTextFromUri(
mContext, uri, providerMap, META_DATA_PREFERENCE_SUMMARY);
if (!TextUtils.equals(summaryFromUri, preference.getSummary())) {
//*/ freeme.caoguofeng, 20211018, prevent summary be null
if (TextUtils.isEmpty(summaryFromUri)) return;
//*/
//*/ freeme.ligen, 20250709. customer hide summary
if ("top_level_wellbeing".equals(preference.getKey()) ||
"top_level_google".equals(preference.getKey())) {
observer.post(() -> preference.setSummary(null));
return;
}
//*/
observer.post(() -> preference.setSummary(summaryFromUri));
}
});
}
private DynamicDataObserver bindSwitchAndGetObserver(Preference preference, Tile tile) {
if (!tile.hasSwitch()) {
return null;
}
final Uri onCheckedChangedUri = TileUtils.getCompleteUri(tile,
META_DATA_PREFERENCE_SWITCH_URI, METHOD_ON_CHECKED_CHANGED);
preference.setOnPreferenceChangeListener((pref, newValue) -> {
onCheckedChanged(onCheckedChangedUri, pref, (boolean) newValue);
return true;
});
final Uri isCheckedUri = TileUtils.getCompleteUri(tile, META_DATA_PREFERENCE_SWITCH_URI,
METHOD_IS_CHECKED);
setSwitchEnabled(preference, false);
return createDynamicDataObserver(METHOD_IS_CHECKED, isCheckedUri, preference);
}
private void onCheckedChanged(Uri uri, Preference pref, boolean checked) {
setSwitchEnabled(pref, false);
ThreadUtils.postOnBackgroundThread(() -> {
final Map<String, IContentProvider> providerMap = new ArrayMap<>();
final Bundle result = TileUtils.putBooleanToUriAndGetResult(mContext, uri, providerMap,
EXTRA_SWITCH_CHECKED_STATE, checked);
ThreadUtils.postOnMainThread(() -> {
setSwitchEnabled(pref, true);
final boolean error = result.getBoolean(EXTRA_SWITCH_SET_CHECKED_ERROR);
if (!error) {
return;
}
setSwitchChecked(pref, !checked);
final String errorMsg = result.getString(EXTRA_SWITCH_SET_CHECKED_ERROR_MESSAGE);
if (!TextUtils.isEmpty(errorMsg)) {
Toast.makeText(mContext, errorMsg, Toast.LENGTH_SHORT).show();
}
});
});
}
private void refreshSwitch(Uri uri, Preference preference, DynamicDataObserver observer) {
ThreadUtils.postOnBackgroundThread(() -> {
final Map<String, IContentProvider> providerMap = new ArrayMap<>();
final boolean checked = TileUtils.getBooleanFromUri(mContext, uri, providerMap,
EXTRA_SWITCH_CHECKED_STATE);
observer.post(() -> {
setSwitchChecked(preference, checked);
setSwitchEnabled(preference, true);
});
});
}
private void setSwitchChecked(Preference pref, boolean checked) {
if (pref instanceof PrimarySwitchPreference primarySwitchPreference) {
primarySwitchPreference.setChecked(checked);
} else if (pref instanceof TwoStatePreference twoStatePreference) {
twoStatePreference.setChecked(checked);
}
}
private void setSwitchEnabled(Preference pref, boolean enabled) {
if (pref instanceof PrimarySwitchPreference primarySwitchPreference) {
primarySwitchPreference.setSwitchEnabled(enabled);
} else {
pref.setEnabled(enabled);
}
}
@VisibleForTesting
void bindIcon(Preference preference, Tile tile, boolean forceRoundedIcon) {
// Icon provided by the content provider overrides any static icon.
if (tile.getMetaData() != null
&& tile.getMetaData().containsKey(META_DATA_PREFERENCE_ICON_URI)) {
// Reserve the icon space to avoid preference padding change.
preference.setIconSpaceReserved(true);
//*/ freeme.ligen, 20250708. customer need to show icon
if ("top_level_wellbeing".equals(preference.getKey())) {
Log.w(TAG, "set icon ic_hompage_health_control ");
preference.setIcon(mContext.getDrawable(R.drawable.ic_hompage_health_control));
return;
} else if ("top_level_google".equals(preference.getKey())) {
Log.w(TAG, "set icon ic_homepage_google ");
preference.setIcon(mContext.getDrawable(R.drawable.ic_homepage_google));
return;
}
//*/
ThreadUtils.postOnBackgroundThread(() -> {
final Intent intent = tile.getIntent();
String packageName = null;
if (!TextUtils.isEmpty(intent.getPackage())) {
packageName = intent.getPackage();
} else if (intent.getComponent() != null) {
packageName = intent.getComponent().getPackageName();
}
final Map<String, IContentProvider> providerMap = new ArrayMap<>();
final Uri uri = TileUtils.getCompleteUri(tile, META_DATA_PREFERENCE_ICON_URI,
METHOD_GET_PROVIDER_ICON);
final Pair<String, Integer> iconInfo = TileUtils.getIconFromUri(
mContext, packageName, uri, providerMap);
if (iconInfo == null) {
Log.w(TAG, "Failed to get icon from uri " + uri);
return;
}
final Icon icon = Icon.createWithResource(iconInfo.first, iconInfo.second);
ThreadUtils.postOnMainThread(() -> {
setPreferenceIcon(preference, tile, forceRoundedIcon, iconInfo.first, icon);
});
});
return;
}
// Use preference context instead here when get icon from Tile, as we are using the context
// to get the style to tint the icon. Using mContext here won't get the correct style.
final Icon tileIcon = tile.getIcon(preference.getContext());
if (tileIcon == null) {
return;
}
setPreferenceIcon(preference, tile, forceRoundedIcon, tile.getPackageName(), tileIcon);
}
private void setPreferenceIcon(Preference preference, Tile tile, boolean forceRoundedIcon,
String iconPackage, Icon icon) {
Drawable iconDrawable = icon.loadDrawable(preference.getContext());
if (iconDrawable == null) {
Log.w(TAG, "Set null preference icon for: " + iconPackage);
preference.setIcon(null);
return;
}
if (TextUtils.equals(tile.getCategory(), CategoryKey.CATEGORY_HOMEPAGE)) {
/*/ freeme.caoguofeng, 20221111. homepage icon not need be tinted
iconDrawable.setTint(Utils.getHomepageIconColor(preference.getContext()));
/*/
preference.setIcon(iconDrawable);
//*/
}
if (forceRoundedIcon && !TextUtils.equals(mContext.getPackageName(), iconPackage)) {
iconDrawable = new AdaptiveIcon(mContext, iconDrawable,
R.dimen.dashboard_tile_foreground_image_inset);
((AdaptiveIcon) iconDrawable).setBackgroundColor(mContext, tile);
}
preference.setIcon(iconDrawable);
}
private void launchPendingIntentOrSelectProfile(FragmentActivity activity, Tile tile,
int sourceMetricCategory) {
ProfileSelectDialog.updatePendingIntentsIfNeeded(mContext, tile);
if (tile.pendingIntentMap.isEmpty()) {
Log.w(TAG, "Cannot resolve pendingIntent, skipping. " + tile.getIntent());
return;
}
mMetricsFeatureProvider.logSettingsTileClick(tile.getKey(mContext), sourceMetricCategory);
// Launch the pending intent directly if there's only one available.
if (tile.pendingIntentMap.size() == 1) {
PendingIntent pendingIntent = Iterables.getOnlyElement(tile.pendingIntentMap.values());
try {
pendingIntent.send();
} catch (PendingIntent.CanceledException e) {
Log.w(TAG, "Failed executing pendingIntent. " + pendingIntent.getIntent(), e);
}
return;
}
ProfileSelectDialog.show(activity.getSupportFragmentManager(), tile,
sourceMetricCategory, /* onShowListener= */ null,
/* onDismissListener= */ null, /* onCancelListener= */ null);
}
private void launchIntentOrSelectProfile(FragmentActivity activity, Tile tile, Intent intent,
int sourceMetricCategory, TopLevelHighlightMixin highlightMixin,
boolean isDuplicateClick) {
if (!isIntentResolvable(intent)) {
Log.w(TAG, "Cannot resolve intent, skipping. " + intent);
return;
}
ProfileSelectDialog.updateUserHandlesIfNeeded(mContext, tile);
if (tile.userHandle == null || tile.isPrimaryProfileOnly()) {
if (!isDuplicateClick) {
mMetricsFeatureProvider.logStartedIntent(intent, sourceMetricCategory);
activity.startActivity(intent);
}
} else if (tile.userHandle.size() == 1) {
if (!isDuplicateClick) {
mMetricsFeatureProvider.logStartedIntent(intent, sourceMetricCategory);
activity.startActivityAsUser(intent, tile.userHandle.get(0));
}
} else {
final UserHandle userHandle = intent.getParcelableExtra(EXTRA_USER);
if (userHandle != null && tile.userHandle.contains(userHandle)) {
if (!isDuplicateClick) {
mMetricsFeatureProvider.logStartedIntent(intent, sourceMetricCategory);
activity.startActivityAsUser(intent, userHandle);
}
return;
}
final List<UserHandle> resolvableUsers = getResolvableUsers(intent, tile);
if (resolvableUsers.size() == 1) {
if (!isDuplicateClick) {
mMetricsFeatureProvider.logStartedIntent(intent, sourceMetricCategory);
activity.startActivityAsUser(intent, resolvableUsers.get(0));
}
return;
}
// Show the profile select dialog regardless of the duplicate click.
mMetricsFeatureProvider.logStartedIntent(intent, sourceMetricCategory);
ProfileSelectDialog.show(activity.getSupportFragmentManager(), tile,
sourceMetricCategory, /* onShowListener= */ highlightMixin,
/* onDismissListener= */ highlightMixin,
/* onCancelListener= */ highlightMixin);
}
}
private boolean isIntentResolvable(Intent intent) {
return mPackageManager.resolveActivity(intent, 0) != null;
}
private List<UserHandle> getResolvableUsers(Intent intent, Tile tile) {
final ArrayList<UserHandle> eligibleUsers = new ArrayList<>();
for (UserHandle user : tile.userHandle) {
if (mPackageManager.resolveActivityAsUser(intent, 0, user.getIdentifier()) != null) {
eligibleUsers.add(user);
}
}
return eligibleUsers;
}
//*/ freeme.caoguofeng, 20230721. Settings-securityInject: add injection icon for system apps
private boolean bindInjectionIcon(Preference pref, Tile tile) {
final Icon icon = getIconFromUri(tile, pref.getContext());
if (icon == null && !tile.isAD()) return false;
refreshIconAsync(pref, tile, icon);
pref.setLayoutResource(tile.isAD() ? R.layout.preference_freeme_ad
: com.freeme.internal.R.layout.preference_freeme);
return true;
}
private Icon getIconFromUri(Tile tile, Context context) {
final String uriStr = tile.getMetaData().getString(META_DATA_PREFERENCE_ICON_URI);
if (uriStr == null) return null;
Uri uri = Uri.parse(uriStr);
final Map<String, IContentProvider> providerMap = new ArrayMap<>();
Bundle bundle = TileUtils.getBundleFromUri(context, uri, providerMap, null);
if (bundle == null) return null;
return bundle.getParcelable(META_DATA_PREFERENCE_ICON);
}
private void refreshIconAsync(Preference preference, Tile tile, Icon icon) {
ThreadUtils.postOnBackgroundThread(() -> {
Drawable originDrawable = icon.loadDrawable(preference.getContext());
if (tile.isAD()) {
ThreadUtils.postOnMainThread(() -> preference.setIcon(originDrawable));
return;
}
int iconSize = (int) preference.getContext().getResources()
.getDimension(R.dimen.freeme_circle_avatar_size);
final UserIconDrawable iconDrawable = new UserIconDrawable(iconSize)
.setIconDrawable(originDrawable).bake();
ThreadUtils.postOnMainThread(() -> preference.setIcon(iconDrawable));
});
}
//*/
}
CategoryManager
package com.android.settings.dashboard;
import android.content.ComponentName;
import android.content.Context;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.VisibleForTesting;
import com.android.settings.homepage.HighlightableMenu;
import com.android.settings.safetycenter.SafetyCenterManagerWrapper;
import com.android.settingslib.applications.InterestingConfigChanges;
import com.android.settingslib.drawer.CategoryKey;
import com.android.settingslib.drawer.DashboardCategory;
import com.android.settingslib.drawer.ProviderTile;
import com.android.settingslib.drawer.Tile;
import com.android.settingslib.drawer.TileUtils;
import com.google.android.setupcompat.util.WizardManagerHelper;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
public class CategoryManager {
private static final String TAG = "CategoryManager";
private static final boolean DEBUG = false;
private static CategoryManager sInstance;
private final InterestingConfigChanges mInterestingConfigChanges;
// Tile cache (key: <packageName, activityName>, value: tile)
private final Map<Pair<String, String>, Tile> mTileByComponentCache;
// Tile cache (key: category key, value: category)
private final Map<String, DashboardCategory> mCategoryByKeyMap;
private List<DashboardCategory> mCategories;
public static CategoryManager get(Context context) {
if (sInstance == null) {
sInstance = new CategoryManager(context);
}
return sInstance;
}
CategoryManager(Context context) {
mTileByComponentCache = new ArrayMap<>();
mCategoryByKeyMap = new ArrayMap<>();
mInterestingConfigChanges = new InterestingConfigChanges();
mInterestingConfigChanges.applyNewConfig(context.getResources());
}
public synchronized DashboardCategory getTilesByCategory(Context context, String categoryKey) {
tryInitCategories(context);
return mCategoryByKeyMap.get(categoryKey);
}
public synchronized List<DashboardCategory> getCategories(Context context) {
if (!WizardManagerHelper.isUserSetupComplete(context)) {
return new ArrayList<>();
}
tryInitCategories(context);
return mCategories;
}
public synchronized void reloadAllCategories(Context context) {
final boolean forceClearCache = mInterestingConfigChanges.applyNewConfig(
context.getResources());
mCategories = null;
tryInitCategories(context, forceClearCache);
}
/**
* Update category from deny list
* @param tileDenylist
*/
public synchronized void updateCategoryFromDenylist(Set<ComponentName> tileDenylist) {
if (mCategories == null) {
Log.w(TAG, "Category is null, skipping denylist update");
return;
}
for (int i = 0; i < mCategories.size(); i++) {
DashboardCategory category = mCategories.get(i);
for (int j = 0; j < category.getTilesCount(); j++) {
Tile tile = category.getTile(j);
if (tileDenylist.contains(tile.getIntent().getComponent())) {
category.removeTile(j--);
}
}
}
}
/** Return the current tile map */
public synchronized Map<ComponentName, Tile> getTileByComponentMap() {
final Map<ComponentName, Tile> result = new ArrayMap<>();
if (mCategories == null) {
Log.w(TAG, "Category is null, no tiles");
return result;
}
mCategories.forEach(category -> {
for (int i = 0; i < category.getTilesCount(); i++) {
final Tile tile = category.getTile(i);
result.put(tile.getIntent().getComponent(), tile);
}
});
return result;
}
private void logTiles(Context context) {
if (DEBUG) {
getTileByComponentMap().forEach((component, tile) -> {
Log.d(TAG, "Tile: " + tile.getCategory().replace("com.android.settings.", "")
+ ": " + tile.getTitle(context) + ", " + component.flattenToShortString());
});
}
}
private synchronized void tryInitCategories(Context context) {
// Keep cached tiles by default. The cache is only invalidated when InterestingConfigChange
// happens.
tryInitCategories(context, false /* forceClearCache */);
}
private synchronized void tryInitCategories(Context context, boolean forceClearCache) {
if (!WizardManagerHelper.isUserSetupComplete(context)) {
// Don't init while setup wizard is still running.
return;
}
if (mCategories == null) {
final boolean firstLoading = mCategoryByKeyMap.isEmpty();
if (forceClearCache) {
mTileByComponentCache.clear();
}
mCategoryByKeyMap.clear();
mCategories = TileUtils.getCategories(context, mTileByComponentCache);
for (DashboardCategory category : mCategories) {
mCategoryByKeyMap.put(category.key, category);
}
/*/ freeme.wanglei, 20180622. FreemeAppTheme, no need backword compat for freeme style
backwardCompatCleanupForCategory(mTileByComponentCache, mCategoryByKeyMap);
//*/
mergeSecurityPrivacyKeys(context, mTileByComponentCache, mCategoryByKeyMap);
sortCategories(context, mCategoryByKeyMap);
filterDuplicateTiles(mCategoryByKeyMap);
if (firstLoading) {
logTiles(context);
final DashboardCategory homepageCategory = mCategoryByKeyMap.get(
CategoryKey.CATEGORY_HOMEPAGE);
if (homepageCategory == null) {
return;
}
for (Tile tile : homepageCategory.getTiles()) {
final String key = tile.getKey(context);
if (TextUtils.isEmpty(key)) {
Log.w(TAG, "Key hint missing for homepage tile: " + tile.getTitle(context));
continue;
}
HighlightableMenu.addMenuKey(key);
}
}
}
}
@VisibleForTesting
synchronized void backwardCompatCleanupForCategory(
Map<Pair<String, String>, Tile> tileByComponentCache,
Map<String, DashboardCategory> categoryByKeyMap) {
// A package can use a) CategoryKey, b) old category keys, c) both.
// Check if a package uses old category key only.
// If yes, map them to new category key.
// Build a package name -> tile map first.
final Map<String, List<Tile>> packageToTileMap = new HashMap<>();
for (Entry<Pair<String, String>, Tile> tileEntry : tileByComponentCache.entrySet()) {
final String packageName = tileEntry.getKey().first;
List<Tile> tiles = packageToTileMap.get(packageName);
if (tiles == null) {
tiles = new ArrayList<>();
packageToTileMap.put(packageName, tiles);
}
tiles.add(tileEntry.getValue());
}
for (Entry<String, List<Tile>> entry : packageToTileMap.entrySet()) {
final List<Tile> tiles = entry.getValue();
// Loop map, find if all tiles from same package uses old key only.
boolean useNewKey = false;
boolean useOldKey = false;
for (Tile tile : tiles) {
if (CategoryKey.KEY_COMPAT_MAP.containsKey(tile.getCategory())) {
useOldKey = true;
} else {
useNewKey = true;
break;
}
}
// Uses only old key, map them to new keys one by one.
if (useOldKey && !useNewKey) {
for (Tile tile : tiles) {
final String newCategoryKey =
CategoryKey.KEY_COMPAT_MAP.get(tile.getCategory());
tile.setCategory(newCategoryKey);
// move tile to new category.
DashboardCategory newCategory = categoryByKeyMap.get(newCategoryKey);
if (newCategory == null) {
newCategory = new DashboardCategory(newCategoryKey);
categoryByKeyMap.put(newCategoryKey, newCategory);
}
newCategory.addTile(tile);
}
}
}
}
/**
* Merges {@link CategoryKey#CATEGORY_SECURITY_ADVANCED_SETTINGS} and {@link
* CategoryKey#CATEGORY_PRIVACY} into {@link
* CategoryKey#CATEGORY_MORE_SECURITY_PRIVACY_SETTINGS}
*/
@VisibleForTesting
synchronized void mergeSecurityPrivacyKeys(
Context context,
Map<Pair<String, String>, Tile> tileByComponentCache,
Map<String, DashboardCategory> categoryByKeyMap) {
if (!SafetyCenterManagerWrapper.get().isEnabled(context)) {
return;
}
for (Entry<Pair<String, String>, Tile> tileEntry : tileByComponentCache.entrySet()) {
Tile tile = tileEntry.getValue();
if (Objects.equals(tile.getCategory(), CategoryKey.CATEGORY_SECURITY_ADVANCED_SETTINGS)
|| Objects.equals(tile.getCategory(), CategoryKey.CATEGORY_PRIVACY)) {
final String newCategoryKey = CategoryKey.CATEGORY_MORE_SECURITY_PRIVACY_SETTINGS;
tile.setCategory(newCategoryKey);
// move tile to new category.
DashboardCategory newCategory = categoryByKeyMap.get(newCategoryKey);
if (newCategory == null) {
newCategory = new DashboardCategory(newCategoryKey);
categoryByKeyMap.put(newCategoryKey, newCategory);
}
newCategory.addTile(tile);
}
}
}
/**
* Sort the tiles injected from all apps such that if they have the same priority value,
* they wil lbe sorted by package name.
* <p/>
* A list of tiles are considered sorted when their priority value decreases in a linear
* scan.
*/
@VisibleForTesting
synchronized void sortCategories(Context context,
Map<String, DashboardCategory> categoryByKeyMap) {
for (Entry<String, DashboardCategory> categoryEntry : categoryByKeyMap.entrySet()) {
categoryEntry.getValue().sortTiles(context.getPackageName());
}
}
/**
* Filter out duplicate tiles from category. Duplicate tiles are the ones pointing to the
* same intent for ActivityTile, and also the ones having the same description for ProviderTile.
*/
@VisibleForTesting
synchronized void filterDuplicateTiles(Map<String, DashboardCategory> categoryByKeyMap) {
for (Entry<String, DashboardCategory> categoryEntry : categoryByKeyMap.entrySet()) {
final DashboardCategory category = categoryEntry.getValue();
final int count = category.getTilesCount();
final Set<String> descriptions = new ArraySet<>();
final Set<ComponentName> components = new ArraySet<>();
for (int i = count - 1; i >= 0; i--) {
final Tile tile = category.getTile(i);
if (tile instanceof ProviderTile) {
final String desc = tile.getDescription();
if (descriptions.contains(desc)) {
category.removeTile(i);
} else {
descriptions.add(desc);
}
} else {
final ComponentName tileComponent = tile.getIntent().getComponent();
if (components.contains(tileComponent)) {
category.removeTile(i);
} else {
components.add(tileComponent);
}
}
}
}
}
}
TileUtils
package com.android.settingslib.drawer;
import android.app.ActivityManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.IContentProvider;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ComponentInfo;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings.Global;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.VisibleForTesting;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Utils is a helper class that contains profile key, meta data, settings action
* and static methods for get icon or text from uri.
*/
public class TileUtils {
private static final boolean DEBUG_TIMING = false;
private static final String LOG_TAG = "TileUtils";
@VisibleForTesting
static final String SETTING_PKG = "com.android.settings";
/**
* Settings will search for system activities of this action and add them as a top level
* settings tile using the following parameters.
*
* <p>A category must be specified in the meta-data for the activity named
* {@link #EXTRA_CATEGORY_KEY}
*
* <p>The title may be defined by meta-data named {@link #META_DATA_PREFERENCE_TITLE}
* otherwise the label for the activity will be used.
*
* <p>The icon may be defined by meta-data named {@link #META_DATA_PREFERENCE_ICON}
* otherwise the icon for the activity will be used.
*
* <p>A summary my be defined by meta-data named {@link #META_DATA_PREFERENCE_SUMMARY}
*/
public static final String EXTRA_SETTINGS_ACTION = "com.android.settings.action.EXTRA_SETTINGS";
/**
* @See {@link #EXTRA_SETTINGS_ACTION}.
*/
public static final String IA_SETTINGS_ACTION = "com.android.settings.action.IA_SETTINGS";
/** Same as #EXTRA_SETTINGS_ACTION but used for the platform Settings activities. */
private static final String SETTINGS_ACTION = "com.android.settings.action.SETTINGS";
private static final String OPERATOR_SETTINGS =
"com.android.settings.OPERATOR_APPLICATION_SETTING";
private static final String OPERATOR_DEFAULT_CATEGORY =
"com.android.settings.category.wireless";
private static final String MANUFACTURER_SETTINGS =
"com.android.settings.MANUFACTURER_APPLICATION_SETTING";
private static final String MANUFACTURER_DEFAULT_CATEGORY =
"com.android.settings.category.device";
/**
* The key used to get the category from metadata of activities of action
* {@link #EXTRA_SETTINGS_ACTION}
* The value must be from {@link CategoryKey}.
*/
static final String EXTRA_CATEGORY_KEY = "com.android.settings.category";
/** The key used to get the package name of the icon resource for the preference. */
static final String EXTRA_PREFERENCE_ICON_PACKAGE = "com.android.settings.icon_package";
/**
* Name of the meta-data item that should be set in the AndroidManifest.xml
* to specify the key that should be used for the preference.
*/
public static final String META_DATA_PREFERENCE_KEYHINT = "com.android.settings.keyhint";
/**
* Name of the meta-data item that can be set in the AndroidManifest.xml or in the content
* provider to specify the key of a group / category where this preference belongs to.
*/
public static final String META_DATA_PREFERENCE_GROUP_KEY = "com.android.settings.group_key";
/**
* Order of the item that should be displayed on screen. Bigger value items displays closer on
* top.
*/
public static final String META_DATA_KEY_ORDER = "com.android.settings.order";
/**
* Name of the meta-data item that should be set in the AndroidManifest.xml
* to specify the icon that should be displayed for the preference.
*/
public static final String META_DATA_PREFERENCE_ICON = "com.android.settings.icon";
/**
* Name of the meta-data item that should be set in the AndroidManifest.xml
* to specify the icon background color. The value may or may not be used by Settings app.
*/
public static final String META_DATA_PREFERENCE_ICON_BACKGROUND_HINT =
"com.android.settings.bg.hint";
/**
* Name of the meta-data item that should be set in the AndroidManifest.xml
* to specify the icon background color as raw ARGB.
*/
public static final String META_DATA_PREFERENCE_ICON_BACKGROUND_ARGB =
"com.android.settings.bg.argb";
/**
* Name of the meta-data item that should be set in the AndroidManifest.xml to specify the
* content provider providing the icon that should be displayed for the preference.
*
* <p>Icon provided by the content provider overrides any static icon.
*/
public static final String META_DATA_PREFERENCE_ICON_URI = "com.android.settings.icon_uri";
/**
* Name of the meta-data item that should be set in the AndroidManifest.xml to specify whether
* the icon is tintable. This should be a boolean value {@code true} or {@code false}, set using
* {@code android:value}
*/
public static final String META_DATA_PREFERENCE_ICON_TINTABLE =
"com.android.settings.icon_tintable";
/**
* Name of the meta-data item that should be set in the AndroidManifest.xml
* to specify the title that should be displayed for the preference.
*
* <p>Note: It is preferred to provide this value using {@code android:resource} with a string
* resource for localization.
*/
public static final String META_DATA_PREFERENCE_TITLE = "com.android.settings.title";
/**
* Name of the meta-data item that should be set in the AndroidManifest.xml to specify the
* content provider providing the title text that should be displayed for the preference.
*
* <p>Title provided by the content provider overrides any static title.
*/
public static final String META_DATA_PREFERENCE_TITLE_URI = "com.android.settings.title_uri";
/**
* Name of the meta-data item that should be set in the AndroidManifest.xml to specify the
* summary text that should be displayed for the preference.
*/
public static final String META_DATA_PREFERENCE_SUMMARY = "com.android.settings.summary";
/**
* Name of the meta-data item that should be set in the AndroidManifest.xml to specify the
* content provider providing the summary text that should be displayed for the preference.
*
* <p>Summary provided by the content provider overrides any static summary.
*/
public static final String META_DATA_PREFERENCE_SUMMARY_URI =
"com.android.settings.summary_uri";
/**
* Name of the meta-data item that should be set in the AndroidManifest.xml to specify the
* content provider providing the switch that should be displayed for the preference.
*
* <p>This works with {@link #META_DATA_PREFERENCE_KEYHINT} which should also be set in the
* AndroidManifest.xml
*/
public static final String META_DATA_PREFERENCE_SWITCH_URI = "com.android.settings.switch_uri";
/**
* Name of the meta-data item that can be set from the content provider providing the intent
* that will be executed when the user taps on the preference.
*/
public static final String META_DATA_PREFERENCE_PENDING_INTENT =
"com.android.settings.pending_intent";
/**
* Value for {@link #META_DATA_KEY_PROFILE}. When the device has a managed profile, the app will
* always be run in the primary profile.
*
* @see #META_DATA_KEY_PROFILE
*/
public static final String PROFILE_PRIMARY = "primary_profile_only";
/**
* Value for {@link #META_DATA_KEY_PROFILE}. When the device has a managed profile, the user
* will be presented with a dialog to choose the profile the app will be run in.
*
* @see #META_DATA_KEY_PROFILE
*/
public static final String PROFILE_ALL = "all_profiles";
/**
* Name of the meta-data item that should be set in the AndroidManifest.xml to specify the
* profile in which the app should be run when the device has a managed profile. The default
* value is {@link #PROFILE_ALL} which means the user will be presented with a dialog to choose
* the profile. If set to {@link #PROFILE_PRIMARY} the app will always be run in the primary
* profile.
*
* @see #PROFILE_PRIMARY
* @see #PROFILE_ALL
*/
public static final String META_DATA_KEY_PROFILE = "com.android.settings.profile";
/**
* Name of the meta-data item that should be set in the AndroidManifest.xml to specify whether
* the {@link android.app.Activity} should be launched in a separate task. This should be a
* boolean value {@code true} or {@code false}, set using {@code android:value}
*/
public static final String META_DATA_NEW_TASK = "com.android.settings.new_task";
/** If the entry should be shown in settings search results. Defaults to true. */
public static final String META_DATA_PREFERENCE_SEARCHABLE = "com.android.settings.searchable";
/** Build a list of DashboardCategory. */
public static List<DashboardCategory> getCategories(Context context,
Map<Pair<String, String>, Tile> cache) {
final long startTime = System.currentTimeMillis();
final boolean setup =
Global.getInt(context.getContentResolver(), Global.DEVICE_PROVISIONED, 0) != 0;
final ArrayList<Tile> tiles = new ArrayList<>();
final UserManager userManager = (UserManager) context.getSystemService(
Context.USER_SERVICE);
for (UserHandle user : userManager.getUserProfiles()) {
// TODO: Needs much optimization, too many PM queries going on here.
if (user.getIdentifier() == ActivityManager.getCurrentUser()) {
// Only add Settings for this user.
loadTilesForAction(context, user, SETTINGS_ACTION, cache, null, tiles, true);
loadTilesForAction(context, user, OPERATOR_SETTINGS, cache,
OPERATOR_DEFAULT_CATEGORY, tiles, false);
loadTilesForAction(context, user, MANUFACTURER_SETTINGS, cache,
MANUFACTURER_DEFAULT_CATEGORY, tiles, false);
}
if (setup) {
loadTilesForAction(context, user, EXTRA_SETTINGS_ACTION, cache, null, tiles, false);
loadTilesForAction(context, user, IA_SETTINGS_ACTION, cache, null, tiles, false);
}
}
final HashMap<String, DashboardCategory> categoryMap = new HashMap<>();
for (Tile tile : tiles) {
final String categoryKey = tile.getCategory();
DashboardCategory category = categoryMap.get(categoryKey);
if (category == null) {
category = new DashboardCategory(categoryKey);
if (category == null) {
Log.w(LOG_TAG, "Couldn't find category " + categoryKey);
continue;
}
categoryMap.put(categoryKey, category);
}
category.addTile(tile);
}
final ArrayList<DashboardCategory> categories = new ArrayList<>(categoryMap.values());
for (DashboardCategory category : categories) {
category.sortTiles();
}
if (DEBUG_TIMING) {
Log.d(LOG_TAG, "getCategories took "
+ (System.currentTimeMillis() - startTime) + " ms");
}
return categories;
}
@VisibleForTesting
static void loadTilesForAction(Context context,
UserHandle user, String action, Map<Pair<String, String>, Tile> addedCache,
String defaultCategory, List<Tile> outTiles, boolean requireSettings) {
final Intent intent = new Intent(action);
if (requireSettings) {
intent.setPackage(SETTING_PKG);
}
loadActivityTiles(context, user, addedCache, defaultCategory, outTiles, intent);
loadProviderTiles(context, user, addedCache, defaultCategory, outTiles, intent);
}
private static void loadActivityTiles(Context context,
UserHandle user, Map<Pair<String, String>, Tile> addedCache,
String defaultCategory, List<Tile> outTiles, Intent intent) {
final PackageManager pm = context.getPackageManager();
final List<ResolveInfo> results = pm.queryIntentActivitiesAsUser(intent,
PackageManager.GET_META_DATA, user.getIdentifier());
for (ResolveInfo resolved : results) {
if (!resolved.system) {
// Do not allow any app to add to settings, only system ones.
continue;
}
final ActivityInfo activityInfo = resolved.activityInfo;
final Bundle metaData = activityInfo.metaData;
loadTile(user, addedCache, defaultCategory, outTiles, intent, metaData, activityInfo);
}
}
private static void loadProviderTiles(Context context,
UserHandle user, Map<Pair<String, String>, Tile> addedCache,
String defaultCategory, List<Tile> outTiles, Intent intent) {
final PackageManager pm = context.getPackageManager();
final List<ResolveInfo> results =
pm.queryIntentContentProvidersAsUser(intent, 0 /* flags */, user.getIdentifier());
for (ResolveInfo resolved : results) {
if (!resolved.system) {
// Do not allow any app to add to settings, only system ones.
continue;
}
final ProviderInfo providerInfo = resolved.providerInfo;
final List<Bundle> entryData = getEntryDataFromProvider(
// Build new context so the entry data is retrieved for the queried user.
context.createContextAsUser(user, 0 /* flags */),
providerInfo.authority);
if (entryData == null || entryData.isEmpty()) {
continue;
}
for (Bundle metaData : entryData) {
loadTile(user, addedCache, defaultCategory, outTiles, intent, metaData,
providerInfo);
}
}
}
private static void loadTile(UserHandle user, Map<Pair<String, String>, Tile> addedCache,
String defaultCategory, List<Tile> outTiles, Intent intent, Bundle metaData,
ComponentInfo componentInfo) {
// Skip loading tile if the component is tagged primary_profile_only but not running on
// the current user.
if (user.getIdentifier() != ActivityManager.getCurrentUser()
&& Tile.isPrimaryProfileOnly(componentInfo.metaData)) {
Log.w(LOG_TAG, "Found " + componentInfo.name + " for intent "
+ intent + " is primary profile only, skip loading tile for uid "
+ user.getIdentifier());
return;
}
String categoryKey = defaultCategory;
// Load category
if ((metaData == null || !metaData.containsKey(EXTRA_CATEGORY_KEY))
&& categoryKey == null) {
Log.w(LOG_TAG, "Found " + componentInfo.name + " for intent "
+ intent + " missing metadata "
+ (metaData == null ? "" : EXTRA_CATEGORY_KEY));
return;
} else {
categoryKey = metaData.getString(EXTRA_CATEGORY_KEY);
}
final boolean isProvider = componentInfo instanceof ProviderInfo;
final Pair<String, String> key = isProvider
? new Pair<>(((ProviderInfo) componentInfo).authority,
metaData.getString(META_DATA_PREFERENCE_KEYHINT))
: new Pair<>(componentInfo.packageName, componentInfo.name);
Tile tile = addedCache.get(key);
if (tile == null) {
tile = isProvider
? new ProviderTile((ProviderInfo) componentInfo, categoryKey, metaData)
: new ActivityTile((ActivityInfo) componentInfo, categoryKey);
addedCache.put(key, tile);
} else {
tile.setMetaData(metaData);
}
tile.setGroupKey(metaData.getString(META_DATA_PREFERENCE_GROUP_KEY));
if (!tile.userHandle.contains(user)) {
tile.userHandle.add(user);
}
if (metaData.containsKey(META_DATA_PREFERENCE_PENDING_INTENT)) {
tile.pendingIntentMap.put(
user, metaData.getParcelable(META_DATA_PREFERENCE_PENDING_INTENT));
}
if (!outTiles.contains(tile)) {
outTiles.add(tile);
}
}
/** Returns the entry data of the key specified from the provider */
// TODO(b/144732809): rearrange methods by access level modifiers
static Bundle getEntryDataFromProvider(Context context, String authority, String key) {
final Map<String, IContentProvider> providerMap = new ArrayMap<>();
final Uri uri = buildUri(authority, EntriesProvider.METHOD_GET_ENTRY_DATA, key);
Bundle result = getBundleFromUri(context, uri, providerMap, null /* bundle */);
if (result == null) {
Uri fallbackUri = buildUri(authority, EntriesProvider.METHOD_GET_SWITCH_DATA, key);
result = getBundleFromUri(context, fallbackUri, providerMap, null /* bundle */);
}
return result;
}
/** Returns all entry data from the provider */
private static List<Bundle> getEntryDataFromProvider(Context context, String authority) {
final Map<String, IContentProvider> providerMap = new ArrayMap<>();
final Uri uri = buildUri(authority, EntriesProvider.METHOD_GET_ENTRY_DATA);
final Bundle result = getBundleFromUri(context, uri, providerMap, null /* bundle */);
if (result != null) {
return result.getParcelableArrayList(EntriesProvider.EXTRA_ENTRY_DATA);
} else {
Uri fallbackUri = buildUri(authority, EntriesProvider.METHOD_GET_SWITCH_DATA);
Bundle fallbackResult =
getBundleFromUri(context, fallbackUri, providerMap, null /* bundle */);
return fallbackResult != null
? fallbackResult.getParcelableArrayList(EntriesProvider.EXTRA_SWITCH_DATA)
: null;
}
}
/**
* Returns the complete uri from the meta data key of the tile.
*
* <p>A complete uri should contain at least one path segment and be one of the following types:
* <br>content://authority/method
* <br>content://authority/method/key
*
* <p>If the uri from the tile is not complete, build a uri by the default method and the
* preference key.
*
* @param tile Tile which contains meta data
* @param metaDataKey Key mapping to the uri in meta data
* @param defaultMethod Method to be attached to the uri by default if it has no path segment
* @return Uri associated with the key
*/
public static Uri getCompleteUri(Tile tile, String metaDataKey, String defaultMethod) {
final String uriString = tile.getMetaData().getString(metaDataKey);
if (TextUtils.isEmpty(uriString)) {
return null;
}
final Uri uri = Uri.parse(uriString);
final List<String> pathSegments = uri.getPathSegments();
if (pathSegments != null && !pathSegments.isEmpty()) {
return uri;
}
final String key = tile.getMetaData().getString(META_DATA_PREFERENCE_KEYHINT);
if (TextUtils.isEmpty(key)) {
Log.w(LOG_TAG, "Please specify the meta-data " + META_DATA_PREFERENCE_KEYHINT
+ " in AndroidManifest.xml for " + uriString);
return buildUri(uri.getAuthority(), defaultMethod);
}
return buildUri(uri.getAuthority(), defaultMethod, key);
}
static Uri buildUri(String authority, String method, String key) {
return new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(authority)
.appendPath(method)
.appendPath(key)
.build();
}
private static Uri buildUri(String authority, String method) {
return new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(authority)
.appendPath(method)
.build();
}
/**
* Gets the icon package name and resource id from content provider.
*
* @param context context
* @param packageName package name of the target activity
* @param uri URI for the content provider
* @param providerMap Maps URI authorities to providers
* @return package name and resource id of the icon specified
*/
public static Pair<String, Integer> getIconFromUri(Context context, String packageName,
Uri uri, Map<String, IContentProvider> providerMap) {
final Bundle bundle = getBundleFromUri(context, uri, providerMap, null /* bundle */);
if (bundle == null) {
return null;
}
final String iconPackageName = bundle.getString(EXTRA_PREFERENCE_ICON_PACKAGE);
if (TextUtils.isEmpty(iconPackageName)) {
return null;
}
int resId = bundle.getInt(META_DATA_PREFERENCE_ICON, 0);
if (resId == 0) {
return null;
}
// Icon can either come from the target package or from the Settings app.
if (iconPackageName.equals(packageName)
|| iconPackageName.equals(context.getPackageName())) {
return Pair.create(iconPackageName, resId);
}
return null;
}
/**
* Gets text associated with the input key from the content provider.
*
* @param context context
* @param uri URI for the content provider
* @param providerMap Maps URI authorities to providers
* @param key Key mapping to the text in bundle returned by the content provider
* @return Text associated with the key, if returned by the content provider
*/
public static String getTextFromUri(Context context, Uri uri,
Map<String, IContentProvider> providerMap, String key) {
final Bundle bundle = getBundleFromUri(context, uri, providerMap, null /* bundle */);
return (bundle != null) ? bundle.getString(key) : null;
}
/**
* Gets boolean associated with the input key from the content provider.
*
* @param context context
* @param uri URI for the content provider
* @param providerMap Maps URI authorities to providers
* @param key Key mapping to the text in bundle returned by the content provider
* @return Boolean associated with the key, if returned by the content provider
*/
public static boolean getBooleanFromUri(Context context, Uri uri,
Map<String, IContentProvider> providerMap, String key) {
final Bundle bundle = getBundleFromUri(context, uri, providerMap, null /* bundle */);
return (bundle != null) ? bundle.getBoolean(key) : false;
}
/**
* Puts boolean associated with the input key to the content provider.
*
* @param context context
* @param uri URI for the content provider
* @param providerMap Maps URI authorities to providers
* @param key Key mapping to the text in bundle returned by the content provider
* @param value Boolean associated with the key
* @return Bundle associated with the action, if returned by the content provider
*/
public static Bundle putBooleanToUriAndGetResult(Context context, Uri uri,
Map<String, IContentProvider> providerMap, String key, boolean value) {
final Bundle bundle = new Bundle();
bundle.putBoolean(key, value);
return getBundleFromUri(context, uri, providerMap, bundle);
}
/*/ freeme.caoguofeng, 20230721. Settings-init: add injection icon for system apps
private static Bundle getBundleFromUri(Context context, Uri uri,
/*/
public static Bundle getBundleFromUri(Context context, Uri uri,
//*/
Map<String, IContentProvider> providerMap, Bundle bundle) {
final Pair<String, String> args = getMethodAndKey(uri);
if (args == null) {
return null;
}
final String method = args.first;
final String key = args.second;
if (TextUtils.isEmpty(method)) {
return null;
}
final IContentProvider provider = getProviderFromUri(context, uri, providerMap);
if (provider == null) {
return null;
}
if (!TextUtils.isEmpty(key)) {
if (bundle == null) {
bundle = new Bundle();
}
bundle.putString(META_DATA_PREFERENCE_KEYHINT, key);
}
try {
return provider.call(context.getAttributionSource(),
uri.getAuthority(), method, uri.toString(), bundle);
/*/ freeme.yangtengfei, 20240516. Settings-init: restart to enter settings and obtain droi vip icon uri crash
} catch (RemoteException e) {
return null;
}
/*/
} catch (Exception e) {
e.printStackTrace();
return null;
}
//*/
}
private static IContentProvider getProviderFromUri(Context context, Uri uri,
Map<String, IContentProvider> providerMap) {
if (uri == null) {
return null;
}
final String authority = uri.getAuthority();
if (TextUtils.isEmpty(authority)) {
return null;
}
if (!providerMap.containsKey(authority)) {
providerMap.put(authority, context.getContentResolver().acquireUnstableProvider(uri));
}
return providerMap.get(authority);
}
/** Returns method and key of the complete uri. */
private static Pair<String, String> getMethodAndKey(Uri uri) {
if (uri == null) {
return null;
}
final List<String> pathSegments = uri.getPathSegments();
if (pathSegments == null || pathSegments.isEmpty()) {
return null;
}
final String method = pathSegments.get(0);
final String key = pathSegments.size() > 1 ? pathSegments.get(1) : null;
return Pair.create(method, key);
}
//*/ freeme.caoguofeng, 20230712. Settings-init: add for injected tiles feature.
public final static String META_DATA_PREFERENCE_AUTHORITY = "com.android.settings.authority";
public final static String KEY_IS_FEATURE_ON = "isFeatureOn";
private static IContentProvider getProviderFromAuthority(Context context, String authority,
Map<String, IContentProvider> providerMap) {
if (TextUtils.isEmpty(authority)) {
return null;
}
if (!providerMap.containsKey(authority)) {
providerMap.put(authority,
context.getContentResolver().acquireUnstableProvider(authority));
}
return providerMap.get(authority);
}
public static Bundle isFeatureOnFromAuthority(Context context, String authority,
String className, Map<String, IContentProvider> providerMap) {
final IContentProvider provider = getProviderFromAuthority(context, authority, providerMap);
if (provider == null) {
return null;
}
try {
return provider.call(context.getAttributionSource(), authority, KEY_IS_FEATURE_ON,
className, null);
} catch (RemoteException e) {
return null;
}
}
//*/
//*/ freeme.caoguofeng, 20230721. Settings-init: add injection icon for system apps
public final static String META_DATA_PREFERENCE_AD = "com.android.settings.ad";
//*/
}
CategoryMixin
package com.android.settings.core;
import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE;
import static androidx.lifecycle.Lifecycle.Event.ON_RESUME;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.AsyncTask;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.OnLifecycleEvent;
import com.android.settings.dashboard.CategoryManager;
import com.android.settingslib.drawer.Tile;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* A mixin that handles live categories for Injection
*/
public class CategoryMixin implements LifecycleObserver {
private static final String TAG = "CategoryMixin";
private static final String DATA_SCHEME_PKG = "package";
// Serves as a temporary list of tiles to ignore until we heard back from the PM that they
// are disabled.
private static final ArraySet<ComponentName> sTileDenylist = new ArraySet<>();
private final Context mContext;
private final PackageReceiver mPackageReceiver = new PackageReceiver();
private final List<CategoryListener> mCategoryListeners = new ArrayList<>();
private int mCategoriesUpdateTaskCount;
private boolean mFirstOnResume = true;
public CategoryMixin(Context context) {
mContext = context;
}
/**
* Resume Lifecycle event
*/
@OnLifecycleEvent(ON_RESUME)
public void onResume() {
final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
filter.addDataScheme(DATA_SCHEME_PKG);
mContext.registerReceiver(mPackageReceiver, filter);
if (mFirstOnResume) {
// Skip since all tiles have been refreshed in DashboardFragment.onCreatePreferences().
Log.d(TAG, "Skip categories update");
mFirstOnResume = false;
return;
}
updateCategories();
}
/**
* Pause Lifecycle event
*/
@OnLifecycleEvent(ON_PAUSE)
public void onPause() {
mContext.unregisterReceiver(mPackageReceiver);
}
/**
* Add a category listener
*/
public void addCategoryListener(CategoryListener listener) {
mCategoryListeners.add(listener);
}
/**
* Remove a category listener
*/
public void removeCategoryListener(CategoryListener listener) {
mCategoryListeners.remove(listener);
}
/**
* Updates dashboard categories.
*/
public void updateCategories() {
updateCategories(false /* fromBroadcast */);
}
void addToDenylist(ComponentName component) {
sTileDenylist.add(component);
}
void removeFromDenylist(ComponentName component) {
sTileDenylist.remove(component);
}
@VisibleForTesting
void onCategoriesChanged(Set<String> categories) {
mCategoryListeners.forEach(listener -> listener.onCategoriesChanged(categories));
}
private void updateCategories(boolean fromBroadcast) {
// Only allow at most 2 tasks existing at the same time since when the first one is
// executing, there may be new data from the second update request.
// Ignore the third update request because the second task is still waiting for the first
// task to complete in a serial thread, which will get the latest data.
if (mCategoriesUpdateTaskCount < 2) {
new CategoriesUpdateTask().execute(fromBroadcast);
}
}
/**
* A handler implementing a {@link CategoryMixin}
*/
public interface CategoryHandler {
/** returns a {@link CategoryMixin} */
CategoryMixin getCategoryMixin();
}
/**
* A listener receiving category change events.
*/
public interface CategoryListener {
/**
* @param categories the changed categories that have to be refreshed, or null to force
* refreshing all.
*/
void onCategoriesChanged(@Nullable Set<String> categories);
}
private class CategoriesUpdateTask extends AsyncTask<Boolean, Void, Set<String>> {
private final CategoryManager mCategoryManager;
private Map<ComponentName, Tile> mPreviousTileMap;
CategoriesUpdateTask() {
mCategoriesUpdateTaskCount++;
mCategoryManager = CategoryManager.get(mContext);
}
@Override
protected Set<String> doInBackground(Boolean... params) {
mPreviousTileMap = mCategoryManager.getTileByComponentMap();
mCategoryManager.reloadAllCategories(mContext);
mCategoryManager.updateCategoryFromDenylist(sTileDenylist);
return getChangedCategories(params[0]);
}
@Override
protected void onPostExecute(Set<String> categories) {
if (categories == null || !categories.isEmpty()) {
onCategoriesChanged(categories);
}
mCategoriesUpdateTaskCount--;
}
// Return the changed categories that have to be refreshed, or null to force refreshing all.
private Set<String> getChangedCategories(boolean fromBroadcast) {
if (!fromBroadcast) {
// Always refresh for non-broadcast case.
return null;
}
final Set<String> changedCategories = new ArraySet<>();
final Map<ComponentName, Tile> currentTileMap =
mCategoryManager.getTileByComponentMap();
currentTileMap.forEach((component, currentTile) -> {
final Tile previousTile = mPreviousTileMap.get(component);
// Check if the tile is newly added.
if (previousTile == null) {
Log.i(TAG, "Tile added: " + component.flattenToShortString());
changedCategories.add(currentTile.getCategory());
return;
}
// Check if the title or summary has changed.
if (!TextUtils.equals(currentTile.getTitle(mContext),
previousTile.getTitle(mContext))
|| !TextUtils.equals(currentTile.getSummary(mContext),
previousTile.getSummary(mContext))) {
Log.i(TAG, "Tile changed: " + component.flattenToShortString());
changedCategories.add(currentTile.getCategory());
}
});
// Check if any previous tile is removed.
final Set<ComponentName> removal = new ArraySet(mPreviousTileMap.keySet());
removal.removeAll(currentTileMap.keySet());
removal.forEach(component -> {
Log.i(TAG, "Tile removed: " + component.flattenToShortString());
changedCategories.add(mPreviousTileMap.get(component).getCategory());
});
return changedCategories;
}
}
private class PackageReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
updateCategories(true /* fromBroadcast */);
}
}
}
DynamicDataObserver
package com.android.settings.dashboard;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import com.android.settingslib.utils.ThreadUtils;
import java.util.concurrent.CountDownLatch;
/**
* Observer for updating injected dynamic data.
*/
public abstract class DynamicDataObserver extends ContentObserver {
private Runnable mUpdateRunnable;
private CountDownLatch mCountDownLatch;
private boolean mUpdateDelegated;
protected DynamicDataObserver() {
super(new Handler(Looper.getMainLooper()));
mCountDownLatch = new CountDownLatch(1);
// Load data for the first time
onDataChanged();
}
/** Returns the uri of the callback. */
public abstract Uri getUri();
/** Called when data changes. */
public abstract void onDataChanged();
/** Calls the runnable to update UI */
public synchronized void updateUi() {
mUpdateDelegated = true;
if (mUpdateRunnable != null) {
mUpdateRunnable.run();
}
}
/** Returns the count-down latch */
public CountDownLatch getCountDownLatch() {
return mCountDownLatch;
}
@Override
public void onChange(boolean selfChange) {
onDataChanged();
}
protected synchronized void post(Runnable runnable) {
if (mUpdateDelegated) {
ThreadUtils.postOnMainThread(runnable);
} else {
mUpdateRunnable = runnable;
mCountDownLatch.countDown();
}
}
}