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

This commit is contained in:
Jason Chiu
2021-06-18 09:25:46 +00:00
committed by Android (Google) Code Review
6 changed files with 378 additions and 168 deletions

View File

@@ -0,0 +1,225 @@
/*
* 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 */);
}
}
}

View File

@@ -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 */);
}
}
}