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

Commit cb6a88b8 authored by Julia Reynolds's avatar Julia Reynolds
Browse files

Remove jank on DND schedule page

Only add/or remove preferences when absolutely needed.
Also fix a 'load data from backend' method that wasn't.

Fixes: 216747934
Test: ZenModeAutomaticRulesPreferenceControllerTest
Test: view schedules page
Test: add schedule
Test: remove schedule
Test: view schedule child page and return to schedules page
Change-Id: I237c2ca7ea89ee6e42354470a76712068a7f4dd7
parent 019ce4c0
Loading
Loading
Loading
Loading
+1 −3
Original line number Diff line number Diff line
@@ -66,9 +66,7 @@ abstract public class AbstractZenModeAutomaticRulePreferenceController extends
    }

    protected Map.Entry<String, AutomaticZenRule>[] getRules() {
        if (mRules == null) {
        mRules = mBackend.getAutomaticZenRules();
        }
        return mRules;
    }

+46 −61
Original line number Diff line number Diff line
@@ -18,7 +18,6 @@ package com.android.settings.notification.zen;

import android.app.AutomaticZenRule;
import android.content.Context;
import android.util.ArrayMap;

import androidx.annotation.VisibleForTesting;
import androidx.fragment.app.Fragment;
@@ -28,23 +27,21 @@ import androidx.preference.PreferenceScreen;

import com.android.settingslib.core.lifecycle.Lifecycle;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class ZenModeAutomaticRulesPreferenceController extends
        AbstractZenModeAutomaticRulePreferenceController {

    protected static final String KEY = "zen_mode_automatic_rules";

    @VisibleForTesting
    protected PreferenceCategory mPreferenceCategory;

    // Map of rule key -> preference so that we can update each preference as needed
    @VisibleForTesting
    protected Map<String, ZenRulePreference> mZenRulePreferences = new ArrayMap<>();
    Map.Entry<String, AutomaticZenRule>[] mSortedRules;

    public ZenModeAutomaticRulesPreferenceController(Context context, Fragment parent, Lifecycle
            lifecycle) {
            lifecycle, ZenModeBackend backend) {
        super(context, KEY, parent, lifecycle);
        mBackend = backend;
    }

    @Override
@@ -60,81 +57,69 @@ public class ZenModeAutomaticRulesPreferenceController extends
    @Override
    public void displayPreference(PreferenceScreen screen) {
        super.displayPreference(screen);
        mPreferenceCategory = screen.findPreference(getPreferenceKey());
        mPreferenceCategory.setPersistent(false);

        // if mPreferenceCategory was un-set, make sure to clear out mZenRulePreferences too, just
        // in case
        if (mPreferenceCategory.getPreferenceCount() == 0) {
            mZenRulePreferences.clear();
        }
        PreferenceCategory preferenceCategory = screen.findPreference(getPreferenceKey());
        preferenceCategory.setPersistent(false);
        mSortedRules = getRules();
        updateRules(preferenceCategory);
    }

    @Override
    public void updateState(Preference preference) {
        super.updateState(preference);
        Map.Entry<String, AutomaticZenRule>[] sortedRules = getRules();

        // refresh the whole preference category list if the total number of rules has changed, or
        // if any individual rules have changed, so we can rebuild the list & keep things in sync
        boolean refreshPrefs = false;
        if (mPreferenceCategory.getPreferenceCount() != sortedRules.length) {
            refreshPrefs = true;
        boolean rulesChanged = false;
        if (sortedRules.length != mSortedRules.length) {
            rulesChanged = true;
        } else {
            // check whether any rules in sortedRules are not in mZenRulePreferences; that should
            // be enough to see whether something has changed
            for (int i = 0; i < sortedRules.length; i++) {
                if (!mZenRulePreferences.containsKey(sortedRules[i].getKey())) {
                    refreshPrefs = true;
            for (int i = 0; i < mSortedRules.length; i++) {
                if (!Objects.equals(mSortedRules[i].getKey(), sortedRules[i].getKey())
                || !Objects.equals(mSortedRules[i].getValue(), sortedRules[i].getValue())) {
                    rulesChanged = true;
                    break;
                }
            }
        }

        // if we need to refresh the whole list, clear the preference category and also start a
        // new map of preferences according to the preference category contents
        // we need to not update the existing one yet, as we'll need to know what preferences
        // previously existed in order to update and re-attach them to the preference category
        Map<String, ZenRulePreference> newPrefs = new ArrayMap<>();
        if (refreshPrefs) {
            mPreferenceCategory.removeAll();
        if (rulesChanged) {
            mSortedRules = sortedRules;
            updateRules((PreferenceCategory) preference);
        }
    }

    private void updateRules(PreferenceCategory preferenceCategory) {
        Map<String, ZenRulePreference> originalPreferences = new HashMap<>();
        for (int i = 0; i < preferenceCategory.getPreferenceCount(); i++) {
            ZenRulePreference pref = (ZenRulePreference) preferenceCategory.getPreference(i);
            originalPreferences.put(pref.getKey(), pref);
        }

        // Loop through each rule, either updating the existing rule or creating the rule's
        // preference if needed (and, in the case where we need to rebuild the preference category
        // list, do so as well)
        for (int i = 0; i < sortedRules.length; i++) {
            String key = sortedRules[i].getKey();
            if (mZenRulePreferences.containsKey(key)) {
                // existing rule; update its info if it's changed since the last display
                AutomaticZenRule rule = sortedRules[i].getValue();
                ZenRulePreference pref = mZenRulePreferences.get(key);
                pref.updatePreference(rule);
        // preference
        for (int i = 0; i < mSortedRules.length; i++) {
            String key = mSortedRules[i].getKey();

                // only add to preference category if the overall set of rules has changed so this
                // needs to be rearranged
                if (refreshPrefs) {
                    mPreferenceCategory.addPreference(pref);
                    newPrefs.put(key, pref);
                }
            if (originalPreferences.containsKey(key)) {
                // existing rule; update its info if it's changed since the last display
                AutomaticZenRule rule = mSortedRules[i].getValue();
                originalPreferences.get(key).updatePreference(rule);
            } else {
                // new rule; create a new ZenRulePreference & add it to the preference category
                // and the map so we'll know about it later
                ZenRulePreference pref = createZenRulePreference(sortedRules[i]);
                mPreferenceCategory.addPreference(pref);
                newPrefs.put(key, pref);
            }
                ZenRulePreference pref = createZenRulePreference(
                        mSortedRules[i], preferenceCategory);
                preferenceCategory.addPreference(pref);
            }

        // If anything was new, then make sure we overwrite mZenRulePreferences with our new data
        if (refreshPrefs) {
            mZenRulePreferences = newPrefs;
            originalPreferences.remove(key);
        }
        // Remove preferences that no longer have a rule
        for (String key : originalPreferences.keySet()) {
            preferenceCategory.removePreferenceRecursively(key);
        }
    }

    @VisibleForTesting
    ZenRulePreference createZenRulePreference(Map.Entry<String, AutomaticZenRule> rule) {
        return new ZenRulePreference(mPreferenceCategory.getContext(),
                rule, mParent, mMetricsFeatureProvider);
    ZenRulePreference createZenRulePreference(Map.Entry<String, AutomaticZenRule> rule,
            PreferenceCategory preferenceCategory) {
        return new ZenRulePreference(preferenceCategory.getContext(),
                rule, mParent, mMetricsFeatureProvider, mBackend);
    }
}
+3 −2
Original line number Diff line number Diff line
@@ -22,7 +22,6 @@ import android.app.NotificationManager;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.service.notification.ConditionProviderService;
import android.view.Menu;
import android.view.MenuInflater;
@@ -59,10 +58,12 @@ public class ZenModeAutomationSettings extends ZenModeSettingsBase {

    private static List<AbstractPreferenceController> buildPreferenceControllers(Context context,
            Fragment parent, ZenServiceListing serviceListing, Lifecycle lifecycle) {
        ZenModeBackend backend = new ZenModeBackend(context);
        List<AbstractPreferenceController> controllers = new ArrayList<>();
        controllers.add(new ZenModeAddAutomaticRulePreferenceController(context, parent,
                serviceListing, lifecycle));
        controllers.add(new ZenModeAutomaticRulesPreferenceController(context, parent, lifecycle));
        controllers.add(new ZenModeAutomaticRulesPreferenceController(
                context, parent, lifecycle, backend));

        return controllers;
    }
+4 −2
Original line number Diff line number Diff line
@@ -60,13 +60,15 @@ public class ZenRulePreference extends PrimarySwitchPreference {

    public ZenRulePreference(Context context,
            final Map.Entry<String, AutomaticZenRule> ruleEntry,
            Fragment parent, MetricsFeatureProvider metricsProvider) {
            Fragment parent, MetricsFeatureProvider metricsProvider,
            ZenModeBackend backend) {
        super(context);
        mBackend = ZenModeBackend.getInstance(context);
        mBackend = backend;
        mContext = context;
        mRule = ruleEntry.getValue();
        mName = mRule.getName();
        mId = ruleEntry.getKey();
        setKey(mId);
        mParent = parent;
        mPm = mContext.getPackageManager();
        mServiceListing = new ZenServiceListing(mContext, CONFIG);
+104 −89
Original line number Diff line number Diff line
@@ -17,13 +17,15 @@
package com.android.settings.notification.zen;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -31,13 +33,19 @@ import static org.mockito.Mockito.when;

import android.app.AutomaticZenRule;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.provider.Settings;

import androidx.fragment.app.Fragment;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceScreen;
import androidx.test.core.app.ApplicationProvider;

import com.google.common.collect.ImmutableList;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -47,6 +55,7 @@ import org.robolectric.RobolectricTestRunner;
import org.robolectric.util.ReflectionHelpers;

import java.lang.reflect.Field;
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.Map;

@@ -56,101 +65,122 @@ public class ZenModeAutomaticRulesPreferenceControllerTest {
    private ZenModeAutomaticRulesPreferenceController mController;
    @Mock
    private ZenModeBackend mBackend;
    @Mock
    private PreferenceCategory mockPref;
    @Mock
    private PreferenceCategory mPreferenceCategory;
    private PreferenceScreen mPreferenceScreen;
    @Mock
    private ZenRulePreference mZenRulePreference;
    private Context mContext;
    @Mock
    PackageManager mPm;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
        mContext = ApplicationProvider.getApplicationContext();
        mController = spy(new ZenModeAutomaticRulesPreferenceController(mContext, mock(Fragment.class),
            null));
        ReflectionHelpers.setField(mController, "mBackend", mBackend);
        when(mPreferenceScreen.findPreference(mController.getPreferenceKey())).thenReturn(
            mockPref);
        mController.displayPreference(mPreferenceScreen);
        doReturn(mZenRulePreference).when(mController).createZenRulePreference(any());
        mContext = spy(ApplicationProvider.getApplicationContext());
        when(mContext.getPackageManager()).thenReturn(mPm);
        when(mPm.queryIntentActivities(any(), any())).thenReturn(
                ImmutableList.of(mock(ResolveInfo.class)));
        when(mBackend.getAutomaticZenRules()).thenReturn(new Map.Entry[0]);
        mController = spy(new ZenModeAutomaticRulesPreferenceController(
                mContext, mock(Fragment.class), null, mBackend));
        final PreferenceManager preferenceManager = new PreferenceManager(mContext);
        mPreferenceScreen = preferenceManager.createPreferenceScreen(mContext);
        mPreferenceCategory = spy(new PreferenceCategory(mContext));
        mPreferenceCategory.setKey(mController.getPreferenceKey());
        mPreferenceScreen.addPreference(mPreferenceCategory);
    }

    @Test
    public void testDisplayPreference_resetsPreferencesWhenCategoryEmpty() {
        // when the PreferenceCategory is empty (no preferences), make sure we clear out any
        // stale state in the cached set of zen rule preferences
        mController.mZenRulePreferences.put("test1_id", mZenRulePreference);
        when(mockPref.getPreferenceCount()).thenReturn(0);
    public void testDisplayPreference_notPersistent() {
        mController.displayPreference(mPreferenceScreen);
        assertTrue(mController.mZenRulePreferences.isEmpty());
        assertFalse(mPreferenceCategory.isPersistent());
    }

    @Test
    public void testUpdateState_clearsPreferencesWhenAddingNewPreferences() {
        final int NUM_RULES = 3;
    public void testDisplayThenUpdateState_onlyAddsOnceRulesUnchanged() {
        final int NUM_RULES = 1;
        Map<String, AutomaticZenRule> rMap = new HashMap<>();

        String ruleId1 = "test1_id";
        String ruleId2 = "test2_id";
        String ruleId3 = "test3_id";

        AutomaticZenRule autoRule1 = new AutomaticZenRule("test_rule_1", null, null,
                null, null, Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, true, 10);
        AutomaticZenRule autoRule2 = new AutomaticZenRule("test_rule_2", null, null,
            null, null, Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, true, 20);
        AutomaticZenRule autoRule3 = new AutomaticZenRule("test_rule_3", null, null,
            null, null, Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, true, 30);

        rMap.put(ruleId1, autoRule1);
        rMap.put(ruleId2, autoRule2);
        rMap.put(ruleId3, autoRule3);

        // should add 3 new preferences to mockPref
        // should add 1 new preferences to mockPref
        mockGetAutomaticZenRules(NUM_RULES, rMap);
        mController.updateState(mockPref);
        verify(mockPref, times(1)).removeAll();
        verify(mockPref, times(NUM_RULES)).addPreference(any());
        assertEquals(NUM_RULES, mController.mZenRulePreferences.size());
        mController.displayPreference(mPreferenceScreen);
        mController.updateState(mPreferenceCategory);
        assertEquals(NUM_RULES, mPreferenceCategory.getPreferenceCount());
        verify(mPreferenceCategory, times(1)).addPreference(any());
    }

    @Test
    public void testUpdateState_clearsPreferencesWhenRemovingPreferences(){
        final int NUM_RULES = 2;
    public void testDisplayThenUpdateState_addsIfRulesChange() {
        Map<String, AutomaticZenRule> rMap = new HashMap<>();

        String ruleId1 = "test1_id";
        AutomaticZenRule autoRule1 = new AutomaticZenRule("test_rule_1", null, null,
                null, null, Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, true, 10);
        rMap.put(ruleId1, autoRule1);
        mockGetAutomaticZenRules(1, rMap);
        // adds one
        mController.displayPreference(mPreferenceScreen);

        String ruleId2 = "test2_id";
        AutomaticZenRule autoRule2 = new AutomaticZenRule("test_rule_2", null, null,
                null, null, Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, true, 20);
        rMap.put(ruleId2, autoRule2);
        mockGetAutomaticZenRules(2, rMap);

        mController.updateState(mPreferenceCategory);
        assertEquals(2, mPreferenceCategory.getPreferenceCount());
        verify(mPreferenceCategory, times(2)).addPreference(any());
    }

    @Test
    public void testUpdateState_addingNewPreferences() {
        mController.displayPreference(mPreferenceScreen);
        final int NUM_RULES = 3;
        Map<String, AutomaticZenRule> rMap = new HashMap<>();

        String ruleId1 = "test1_id";
        String ruleId2 = "test2_id";
        String ruleId3 = "test3_id";

        AutomaticZenRule autoRule1 = new AutomaticZenRule("test_rule_1", null, null,
            null, null, Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, true, 10);
        AutomaticZenRule autoRule2 = new AutomaticZenRule("test_rule_2", null, null,
            null, null, Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, true, 20);
        AutomaticZenRule autoRule3 = new AutomaticZenRule("test_rule_3", null, null,
            null, null, Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, true, 30);

        rMap.put(ruleId1, autoRule1);
        rMap.put(ruleId2, autoRule2);
        rMap.put(ruleId3, autoRule3);

        // Add three preferences to the set of previously-known-about ZenRulePreferences; in this
        // case, test3_id is "deleted"
        mController.mZenRulePreferences.put("test1_id", mZenRulePreference);
        mController.mZenRulePreferences.put("test2_id", mZenRulePreference);
        mController.mZenRulePreferences.put("test3_id", mZenRulePreference);

        // update state should re-add all preferences since a preference was deleted
        when(mockPref.getPreferenceCount()).thenReturn(NUM_RULES + 1);
        // should add 3 new preferences to mockPref
        mockGetAutomaticZenRules(NUM_RULES, rMap);
        mController.updateState(mockPref);
        verify(mockPref, times(1)).removeAll();
        verify(mockPref, times(NUM_RULES)).addPreference(any());
        assertEquals(NUM_RULES, mController.mZenRulePreferences.size());
        mController.updateState(mPreferenceCategory);
        assertEquals(NUM_RULES, mPreferenceCategory.getPreferenceCount());
    }

    @Test
    public void testUpdateState_clearsPreferencesWhenSameNumberButDifferentPrefs() {
    public void testUpdateState_addsAndRemoves(){
        mController.displayPreference(mPreferenceScreen);
        final int NUM_RULES = 2;
        Map<String, AutomaticZenRule> rMap = new HashMap<>();

        String FAKE_1 = "fake key 1";
        String FAKE_2 = "fake 2";
        mPreferenceCategory.addPreference(new ZenRulePreference(mContext,
                new AbstractMap.SimpleEntry<>(FAKE_1, mock(AutomaticZenRule.class)),
                null, null, mBackend));
        mPreferenceCategory.addPreference(new ZenRulePreference(mContext,
                new AbstractMap.SimpleEntry<>(FAKE_2, mock(AutomaticZenRule.class)),
                null, null, mBackend));
        assertNotNull(mPreferenceCategory.findPreference(FAKE_1));
        assertNotNull(mPreferenceCategory.findPreference(FAKE_2));

        String ruleId1 = "test1_id";
        String ruleId2 = "test2_id";

@@ -162,52 +192,37 @@ public class ZenModeAutomaticRulesPreferenceControllerTest {
        rMap.put(ruleId1, autoRule1);
        rMap.put(ruleId2, autoRule2);

        // Add two preferences to the set of previously-known-about ZenRulePreferences; in this
        // case, test3_id is "deleted" but test2_id is "added"
        mController.mZenRulePreferences.put("test1_id", mZenRulePreference);
        mController.mZenRulePreferences.put("test3_id", mZenRulePreference);

        // update state should re-add all preferences since a preference was deleted
        when(mockPref.getPreferenceCount()).thenReturn(NUM_RULES);
        mockGetAutomaticZenRules(NUM_RULES, rMap);
        mController.updateState(mockPref);
        verify(mockPref, times(1)).removeAll();
        verify(mockPref, times(NUM_RULES)).addPreference(any());
        assertEquals(NUM_RULES, mController.mZenRulePreferences.size());
        mController.updateState(mPreferenceCategory);
        assertNull(mPreferenceCategory.findPreference(FAKE_1));
        assertNull(mPreferenceCategory.findPreference(FAKE_2));
        assertNotNull(mPreferenceCategory.findPreference(ruleId1));
        assertNotNull(mPreferenceCategory.findPreference(ruleId2));
    }

    @Test
    public void testUpdateState_updateEnableState() throws NoSuchFieldException {
        final int NUM_RULES = 1;
        Map<String, AutomaticZenRule> rMap = new HashMap<>();
    public void testUpdateState_updateEnableState() {
        mController.displayPreference(mPreferenceScreen);
        String testId = "test1_id";
        AutomaticZenRule rule = new AutomaticZenRule("rule_name", null, null,
                null, null, Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, true, 10);
        rMap.put(testId, rule);

        when(mockPref.getPreferenceCount()).thenReturn(NUM_RULES);
        when(mockPref.getPreference(anyInt())).thenReturn(mZenRulePreference);
        mController.mZenRulePreferences.put("test1_id", mZenRulePreference);
        mPreferenceCategory.addPreference(new ZenRulePreference(mContext,
                new AbstractMap.SimpleEntry<>(testId, rule),
                null, null, mBackend));

        // update state should NOT re-add all the preferences, should only update enable state
        rule.setEnabled(false);
        rMap.put(testId, rule);
        final int NUM_RULES = 1;
        Map<String, AutomaticZenRule> rMap = new HashMap<>();
        AutomaticZenRule ruleUpdated = new AutomaticZenRule("rule_name", null, null,
                null, null, Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, true, 10);
        ruleUpdated.setEnabled(false);
        rMap.put(testId, ruleUpdated);
        mockGetAutomaticZenRules(NUM_RULES, rMap);
        setZenRulePreferenceField("mId", testId);
        mController.updateState(mockPref);
        verify(mZenRulePreference, times(1)).updatePreference(any());
        verify(mockPref, never()).removeAll();
        assertEquals(NUM_RULES, mController.mZenRulePreferences.size());
    }

    private void setZenRulePreferenceField(String name, Object value) {
        try {
            Field field = ZenRulePreference.class.getDeclaredField("mId");
            field.setAccessible(true);
            field.set(mZenRulePreference, value);
        } catch (ReflectiveOperationException e) {
            fail("Unable to set mZenRulePreference field: " + name);
        }
        mController.updateState(mPreferenceCategory);
        assertFalse(mPreferenceCategory.findPreference(testId).isEnabled());
        assertEquals(NUM_RULES, mPreferenceCategory.getPreferenceCount());
    }

    private void mockGetAutomaticZenRules(int numRules, Map<String, AutomaticZenRule> rules) {