DashboardFeatureProviderImpl

发布于:2025-08-04 ⋅ 阅读:(16) ⋅ 点赞:(0)

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();
        }
    }
}

网站公告

今日签到

点亮在社区的每一天
去签到