diff --git a/src/com/android/settings/SettingsActivity.java b/src/com/android/settings/SettingsActivity.java index 7dd5fe40e42..12f63ea226f 100644 --- a/src/com/android/settings/SettingsActivity.java +++ b/src/com/android/settings/SettingsActivity.java @@ -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"); } diff --git a/src/com/android/settings/core/CategoryMixin.java b/src/com/android/settings/core/CategoryMixin.java new file mode 100644 index 00000000000..8d0a412a60c --- /dev/null +++ b/src/com/android/settings/core/CategoryMixin.java @@ -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 sTileDenylist = new ArraySet<>(); + + private final Context mContext; + private final PackageReceiver mPackageReceiver = new PackageReceiver(); + private final List 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 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 categories); + } + + private class CategoriesUpdateTask extends AsyncTask> { + + private final CategoryManager mCategoryManager; + private Map mPreviousTileMap; + + CategoriesUpdateTask() { + mCategoriesUpdateTaskCount++; + mCategoryManager = CategoryManager.get(mContext); + } + + @Override + protected Set doInBackground(Boolean... params) { + mPreviousTileMap = mCategoryManager.getTileByComponentMap(); + mCategoryManager.reloadAllCategories(mContext); + mCategoryManager.updateCategoryFromDenylist(sTileDenylist); + return getChangedCategories(params[0]); + } + + @Override + protected void onPostExecute(Set 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 getChangedCategories(boolean fromBroadcast) { + if (!fromBroadcast) { + // Always refresh for non-broadcast case. + return null; + } + + final Set changedCategories = new ArraySet<>(); + final Map 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 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 */); + } + } +} diff --git a/src/com/android/settings/core/SettingsBaseActivity.java b/src/com/android/settings/core/SettingsBaseActivity.java index 6dba83b8aff..47993cfbd5a 100644 --- a/src/com/android/settings/core/SettingsBaseActivity.java +++ b/src/com/android/settings/core/SettingsBaseActivity.java @@ -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 sTileDenylist = new ArraySet<>(); - - private final PackageReceiver mPackageReceiver = new PackageReceiver(); - private final List 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 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 categories); - } - - private class CategoriesUpdateTask extends AsyncTask> { - - private final Context mContext; - private final CategoryManager mCategoryManager; - private Map mPreviousTileMap; - - public CategoriesUpdateTask() { - mCategoriesUpdateTaskCount++; - mContext = SettingsBaseActivity.this; - mCategoryManager = CategoryManager.get(mContext); - } - - @Override - protected Set doInBackground(Boolean... params) { - mPreviousTileMap = mCategoryManager.getTileByComponentMap(); - mCategoryManager.reloadAllCategories(mContext); - mCategoryManager.updateCategoryFromDenylist(sTileDenylist); - return getChangedCategories(params[0]); - } - - @Override - protected void onPostExecute(Set 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 getChangedCategories(boolean fromBroadcast) { - if (!fromBroadcast) { - // Always refresh for non-broadcast case. - return null; - } - - final Set changedCategories = new ArraySet<>(); - final Map 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 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 */); - } - } } diff --git a/src/com/android/settings/dashboard/DashboardFragment.java b/src/com/android/settings/dashboard/DashboardFragment.java index 29a11a3e056..dfd931db7e9 100644 --- a/src/com/android/settings/dashboard/DashboardFragment.java +++ b/src/com/android/settings/dashboard/DashboardFragment.java @@ -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; } diff --git a/src/com/android/settings/homepage/SettingsHomepageActivity.java b/src/com/android/settings/homepage/SettingsHomepageActivity.java index 5950e4b8ba1..f3fdf5a6bc7 100644 --- a/src/com/android/settings/homepage/SettingsHomepageActivity.java +++ b/src/com/android/settings/homepage/SettingsHomepageActivity.java @@ -38,13 +38,16 @@ import androidx.fragment.app.FragmentTransaction; import com.android.settings.R; import com.android.settings.Utils; 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; import com.android.settingslib.transition.SettingsTransitionHelper; -public class SettingsHomepageActivity extends FragmentActivity { +/** Settings homepage activity */ +public class SettingsHomepageActivity extends FragmentActivity implements + CategoryMixin.CategoryHandler { private static final String TAG = "SettingsHomepageActivity"; @@ -52,6 +55,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 @@ -87,6 +96,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. diff --git a/tests/robotests/src/com/android/settings/core/CategoryMixinTest.java b/tests/robotests/src/com/android/settings/core/CategoryMixinTest.java new file mode 100644 index 00000000000..d64f95dc4ad --- /dev/null +++ b/tests/robotests/src/com/android/settings/core/CategoryMixinTest.java @@ -0,0 +1,117 @@ +/* + * 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 com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import android.content.BroadcastReceiver; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.util.ArraySet; + +import androidx.appcompat.app.AppCompatActivity; + +import com.android.settings.core.CategoryMixin.CategoryListener; +import com.android.settingslib.R; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.android.controller.ActivityController; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +public class CategoryMixinTest { + private ActivityController mActivityController; + + @Before + public void setUp() { + mActivityController = Robolectric.buildActivity(TestActivity.class); + } + + @Test + public void resumeActivity_shouldRegisterReceiver() { + mActivityController.setup(); + + final TestActivity activity = mActivityController.get(); + assertThat(activity.getRegisteredReceivers()).isNotEmpty(); + } + + @Test + public void pauseActivity_shouldUnregisterReceiver() { + mActivityController.setup().pause(); + + final TestActivity activity = mActivityController.get(); + assertThat(activity.getRegisteredReceivers()).isEmpty(); + } + + @Test + public void onCategoriesChanged_listenerAdded_shouldNotifyChanged() { + mActivityController.setup().pause(); + final CategoryMixin categoryMixin = mActivityController.get().getCategoryMixin(); + final CategoryListener listener = mock(CategoryListener.class); + categoryMixin.addCategoryListener(listener); + + categoryMixin.onCategoriesChanged(new ArraySet<>()); + + verify(listener).onCategoriesChanged(anySet()); + } + + static class TestActivity extends AppCompatActivity implements CategoryMixin.CategoryHandler { + + private CategoryMixin mCategoryMixin; + private List mRegisteredReceivers = new ArrayList<>(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setTheme(R.style.Theme_AppCompat); + mCategoryMixin = new CategoryMixin(this); + getLifecycle().addObserver(mCategoryMixin); + } + + @Override + public CategoryMixin getCategoryMixin() { + return mCategoryMixin; + } + + @Override + public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { + mRegisteredReceivers.add(receiver); + return super.registerReceiver(receiver, filter); + } + + @Override + public void unregisterReceiver(BroadcastReceiver receiver) { + mRegisteredReceivers.remove(receiver); + super.unregisterReceiver(receiver); + } + + List getRegisteredReceivers() { + return mRegisteredReceivers; + } + } +}