Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3ab404c6 authored by Jason Chiu's avatar Jason Chiu Committed by Android (Google) Code Review
Browse files

Merge "Support category changed mechanism in homepage" into sc-dev

parents 86a3084c c713c3e8
Loading
Loading
Loading
Loading
+1 −5
Original line number Diff line number Diff line
@@ -36,10 +36,8 @@ import android.os.Bundle;
import android.os.UserHandle;
import android.os.UserManager;
import android.text.TextUtils;
import android.util.FeatureFlagUtils;
import android.util.Log;
import android.view.View;
import android.view.Window;
import android.widget.Button;

import androidx.annotation.Nullable;
@@ -55,7 +53,6 @@ import androidx.preference.PreferenceManager;
import com.android.internal.util.ArrayUtils;
import com.android.settings.Settings.WifiSettingsActivity;
import com.android.settings.applications.manageapplications.ManageApplications;
import com.android.settings.core.FeatureFlags;
import com.android.settings.core.OnActivityResultListener;
import com.android.settings.core.SettingsBaseActivity;
import com.android.settings.core.SubSettingLauncher;
@@ -70,7 +67,6 @@ import com.android.settingslib.core.instrumentation.SharedPreferencesLogger;
import com.android.settingslib.development.DevelopmentSettingsEnabler;
import com.android.settingslib.drawer.DashboardCategory;

import com.google.android.material.transition.platform.MaterialSharedAxis;
import com.google.android.setupcompat.util.WizardManagerHelper;

import java.util.ArrayList;
@@ -689,7 +685,7 @@ public class SettingsActivity extends SettingsBaseActivity
        if (somethingChanged) {
            Log.d(LOG_TAG, "Enabled state changed for some tiles, reloading all categories "
                    + changedList.toString());
            updateCategories();
            mCategoryMixin.updateCategories();
        } else {
            Log.d(LOG_TAG, "No enabled state changed, skipping updateCategory call");
        }
+225 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.settings.core;

import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE;
import static androidx.lifecycle.Lifecycle.Event.ON_RESUME;

import android.annotation.Nullable;
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.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;

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

        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 */);
        }
    }
}
+16 −155
Original line number Diff line number Diff line
@@ -16,21 +16,15 @@
package com.android.settings.core;

import android.annotation.LayoutRes;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.res.TypedArray;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
@@ -40,14 +34,14 @@ import android.view.Window;
import android.widget.Toolbar;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;

import com.android.settings.R;
import com.android.settings.SubSettings;
import com.android.settings.Utils;
import com.android.settings.dashboard.CategoryManager;
import com.android.settings.core.CategoryMixin.CategoryHandler;
import com.android.settingslib.core.lifecycle.HideNonSystemOverlayMixin;
import com.android.settingslib.drawer.Tile;
import com.android.settingslib.transition.SettingsTransitionHelper;
import com.android.settingslib.transition.SettingsTransitionHelper.TransitionType;

@@ -56,12 +50,8 @@ import com.google.android.material.resources.TextAppearanceConfig;
import com.google.android.setupcompat.util.WizardManagerHelper;
import com.google.android.setupdesign.util.ThemeHelper;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class SettingsBaseActivity extends FragmentActivity {
/** Base activity for Settings pages */
public class SettingsBaseActivity extends FragmentActivity implements CategoryHandler {

    /**
     * What type of page transition should be apply.
@@ -70,20 +60,17 @@ public class SettingsBaseActivity extends FragmentActivity {

    protected static final boolean DEBUG_TIMING = false;
    private static final String TAG = "SettingsBaseActivity";
    private static final String DATA_SCHEME_PKG = "package";
    private static final int DEFAULT_REQUEST = -1;

    // Serves as a temporary list of tiles to ignore until we heard back from the PM that they
    // are disabled.
    private static ArraySet<ComponentName> sTileDenylist = new ArraySet<>();

    private final PackageReceiver mPackageReceiver = new PackageReceiver();
    private final List<CategoryListener> mCategoryListeners = new ArrayList<>();

    protected CategoryMixin mCategoryMixin;
    protected CollapsingToolbarLayout mCollapsingToolbarLayout;
    private int mCategoriesUpdateTaskCount;
    private Toolbar mToolbar;

    @Override
    public CategoryMixin getCategoryMixin() {
        return mCategoryMixin;
    }

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        if (Utils.isPageTransitionEnabled(this)) {
@@ -102,6 +89,9 @@ public class SettingsBaseActivity extends FragmentActivity {
        getLifecycle().addObserver(new HideNonSystemOverlayMixin(this));
        TextAppearanceConfig.setShouldLoadFontSynchronously(true);

        mCategoryMixin = new CategoryMixin(this);
        getLifecycle().addObserver(mCategoryMixin);

        final TypedArray theme = getTheme().obtainStyledAttributes(android.R.styleable.Theme);
        if (!theme.getBoolean(android.R.styleable.Theme_windowNoTitle, false)) {
            requestWindowFeature(Window.FEATURE_NO_TITLE);
@@ -192,37 +182,15 @@ public class SettingsBaseActivity extends FragmentActivity {
                userHandle);
    }

    @Override
    protected void onResume() {
        super.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);
        registerReceiver(mPackageReceiver, filter);

        updateCategories();
    }

    @Override
    protected void onPause() {
        // For accessibility activities launched from setup wizard.
        if (getTransitionType(getIntent()) == TransitionType.TRANSITION_FADE) {
            overridePendingTransition(R.anim.sud_stay, android.R.anim.fade_out);
        }
        unregisterReceiver(mPackageReceiver);
        super.onPause();
    }

    public void addCategoryListener(CategoryListener listener) {
        mCategoryListeners.add(listener);
    }

    public void remCategoryListener(CategoryListener listener) {
        mCategoryListeners.remove(listener);
    }

    @Override
    public void setContentView(@LayoutRes int layoutResID) {
        final ViewGroup parent = findViewById(R.id.content_frame);
@@ -270,13 +238,6 @@ public class SettingsBaseActivity extends FragmentActivity {
        return true;
    }

    private void onCategoriesChanged(Set<String> categories) {
        final int N = mCategoryListeners.size();
        for (int i = 0; i < N; i++) {
            mCategoryListeners.get(i).onCategoriesChanged(categories);
        }
    }

    private boolean isLockTaskModePinned() {
        final ActivityManager activityManager =
                getApplicationContext().getSystemService(ActivityManager.class);
@@ -300,9 +261,9 @@ public class SettingsBaseActivity extends FragmentActivity {
        boolean isEnabled = state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
        if (isEnabled != enabled || state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) {
            if (enabled) {
                sTileDenylist.remove(component);
                mCategoryMixin.removeFromDenylist(component);
            } else {
                sTileDenylist.add(component);
                mCategoryMixin.addToDenylist(component);
            }
            pm.setComponentEnabledSetting(component, enabled
                            ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
@@ -313,29 +274,12 @@ public class SettingsBaseActivity extends FragmentActivity {
        return false;
    }

    /**
     * Updates dashboard categories. Only necessary to call this after setTileEnabled
     */
    public void updateCategories() {
        updateCategories(false /* fromBroadcast */);
    }

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

    private int getTransitionType(Intent intent) {
        return intent.getIntExtra(EXTRA_PAGE_TRANSITION_TYPE,
                SettingsTransitionHelper.TransitionType.TRANSITION_SHARED_AXIS);
    }

    @androidx.annotation.Nullable
    @Nullable
    private Bundle createActivityOptionsBundleForTransition(
            @androidx.annotation.Nullable Bundle options) {
        if (mToolbar == null) {
@@ -352,87 +296,4 @@ public class SettingsBaseActivity extends FragmentActivity {
        return mergedOptions;
    }

    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 Context mContext;
        private final CategoryManager mCategoryManager;
        private Map<ComponentName, Tile> mPreviousTileMap;

        public CategoriesUpdateTask() {
            mCategoriesUpdateTaskCount++;
            mContext = SettingsBaseActivity.this;
            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 */);
        }
    }
}
+7 −7
Original line number Diff line number Diff line
@@ -35,8 +35,9 @@ import androidx.preference.SwitchPreference;
import com.android.settings.R;
import com.android.settings.SettingsPreferenceFragment;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.core.CategoryMixin.CategoryHandler;
import com.android.settings.core.CategoryMixin.CategoryListener;
import com.android.settings.core.PreferenceControllerListHelper;
import com.android.settings.core.SettingsBaseActivity;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.widget.PrimarySwitchPreference;
import com.android.settingslib.core.AbstractPreferenceController;
@@ -61,8 +62,7 @@ import java.util.concurrent.ExecutionException;
 * Base fragment for dashboard style UI containing a list of static and dynamic setting items.
 */
public abstract class DashboardFragment extends SettingsPreferenceFragment
        implements SettingsBaseActivity.CategoryListener, Indexable,
        PreferenceGroup.OnExpandButtonClickListener,
        implements CategoryListener, Indexable, PreferenceGroup.OnExpandButtonClickListener,
        BasePreferenceController.UiBlockListener {
    public static final String CATEGORY = "category";
    private static final String TAG = "DashboardFragment";
@@ -198,9 +198,9 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment
            return;
        }
        final Activity activity = getActivity();
        if (activity instanceof SettingsBaseActivity) {
        if (activity instanceof CategoryHandler) {
            mListeningToCategoryChange = true;
            ((SettingsBaseActivity) activity).addCategoryListener(this);
            ((CategoryHandler) activity).getCategoryMixin().addCategoryListener(this);
        }
        final ContentResolver resolver = getContentResolver();
        mDashboardTilePrefKeys.values().stream()
@@ -243,8 +243,8 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment
        unregisterDynamicDataObservers(new ArrayList<>(mRegisteredObservers));
        if (mListeningToCategoryChange) {
            final Activity activity = getActivity();
            if (activity instanceof SettingsBaseActivity) {
                ((SettingsBaseActivity) activity).remCategoryListener(this);
            if (activity instanceof CategoryHandler) {
                ((CategoryHandler) activity).getCategoryMixin().removeCategoryListener(this);
            }
            mListeningToCategoryChange = false;
        }
+12 −1
Original line number Diff line number Diff line
@@ -34,12 +34,15 @@ import androidx.fragment.app.FragmentTransaction;

import com.android.settings.R;
import com.android.settings.accounts.AvatarViewMixin;
import com.android.settings.core.CategoryMixin;
import com.android.settings.core.FeatureFlags;
import com.android.settings.homepage.contextualcards.ContextualCardsFragment;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.core.lifecycle.HideNonSystemOverlayMixin;

public class SettingsHomepageActivity extends FragmentActivity {
/** Settings homepage activity */
public class SettingsHomepageActivity extends FragmentActivity implements
        CategoryMixin.CategoryHandler {

    private static final String TAG = "SettingsHomepageActivity";

@@ -47,6 +50,12 @@ public class SettingsHomepageActivity extends FragmentActivity {

    private View mHomepageView;
    private View mSuggestionView;
    private CategoryMixin mCategoryMixin;

    @Override
    public CategoryMixin getCategoryMixin() {
        return mCategoryMixin;
    }

    /**
     * Shows the homepage and shows/hides the suggestion together. Only allows to be executed once
@@ -76,6 +85,8 @@ public class SettingsHomepageActivity extends FragmentActivity {
                .initSearchToolbar(this /* activity */, toolbar, SettingsEnums.SETTINGS_HOMEPAGE);

        getLifecycle().addObserver(new HideNonSystemOverlayMixin(this));
        mCategoryMixin = new CategoryMixin(this);
        getLifecycle().addObserver(mCategoryMixin);

        if (!getSystemService(ActivityManager.class).isLowRamDevice()) {
            // Only allow features on high ram devices.
Loading