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

Commit 195c698c authored by Matías Hernández's avatar Matías Hernández
Browse files

Fix refresh of "apps that can interrupt" segment

Switching from Priority Apps to None would still show the icons of the priority apps. There were several minor things wrong:
* We were keeping the AppSession when a mode switches from "priority apps" to "none", and because onResume always fires the callback, we were loading the icons.
* Even when deactivating the session, its own onResume lifecycle listener was still firing one extra time, before we had a chance to deactivate it.

Also two minor improvements to CircularIconsPreference:
* Update mLoadedIcons when we early exit due to mIconSet having no items. Mostly for testing, since the icons wouldn't be visible anyway.
* Use notifyChanged() when we get a measured view, instead of holding a reference to the view. Technically the previous code violated the "shouldn't keep references to views outside of onBindViewHolder" (although I didn't see this having an effect in practice).

Fixes: 360077747
Test: atest ZenModeAppsLinkPreferenceControllerTest
Flag: android.app.modes_ui
Change-Id: I6d4b7a344542da46f5ebfcd0774f1af27ed113ac
parent e3702e9b
Loading
Loading
Loading
Loading
+5 −2
Original line number Diff line number Diff line
@@ -49,7 +49,9 @@ public class CircularIconsPreference extends RestrictedPreference {

    private static final float DISABLED_ITEM_ALPHA = 0.3f;

    record LoadedIcons(ImmutableList<Drawable> icons, int extraItems) { }
    record LoadedIcons(ImmutableList<Drawable> icons, int extraItems) {
        static final LoadedIcons EMPTY = new LoadedIcons(ImmutableList.of(), 0);
    }

    private Executor mUiExecutor;

@@ -126,6 +128,7 @@ public class CircularIconsPreference extends RestrictedPreference {
            // We know what icons we want, but haven't yet loaded them.
            if (mIconSet.size() == 0) {
                container.setVisibility(View.GONE);
                mLoadedIcons = LoadedIcons.EMPTY;
                return;
            }
            container.setVisibility(View.VISIBLE);
@@ -137,7 +140,7 @@ public class CircularIconsPreference extends RestrictedPreference {
                            @Override
                            public void onGlobalLayout() {
                                container.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                                startLoadingIcons(container, mIconSet);
                                notifyChanged();
                            }
                        }
                );
+20 −6
Original line number Diff line number Diff line
@@ -21,6 +21,8 @@ import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;

import android.app.Application;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.UserHandle;
import android.os.UserManager;
@@ -48,6 +50,7 @@ import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;

/**
 * Preference with a link and summary about what apps can break through the mode
@@ -64,24 +67,26 @@ class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferenceContr
    private ZenMode mZenMode;
    private CircularIconsPreference mPreference;
    private final Fragment mHost;
    private final Function<ApplicationInfo, Drawable> mAppIconRetriever;

    ZenModeAppsLinkPreferenceController(Context context, String key, Fragment host,
            ZenModesBackend backend, ZenHelperBackend helperBackend) {
        this(context, key, host,
                ApplicationsState.getInstance((Application) context.getApplicationContext()),
                backend, helperBackend);
                backend, helperBackend, appInfo -> Utils.getBadgedIcon(context, appInfo));
    }

    @VisibleForTesting
    ZenModeAppsLinkPreferenceController(Context context, String key, Fragment host,
            ApplicationsState applicationsState, ZenModesBackend backend,
            ZenHelperBackend helperBackend) {
            ZenHelperBackend helperBackend, Function<ApplicationInfo, Drawable> appIconRetriever) {
        super(context, key, backend);
        mSummaryHelper = new ZenModeSummaryHelper(mContext, helperBackend);
        mHelperBackend = helperBackend;
        mApplicationsState = applicationsState;
        mUserManager = context.getSystemService(UserManager.class);
        mHost = host;
        mAppIconRetriever = appIconRetriever;
    }

    @Override
@@ -105,13 +110,18 @@ class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferenceContr
        if (zenMode.getPolicy().getAllowedChannels() == ZenPolicy.CHANNEL_POLICY_NONE) {
            mPreference.setSummary(R.string.zen_mode_apps_none_apps);
            mPreference.displayIcons(CircularIconSet.EMPTY);
            if (mAppSession != null) {
                mAppSession.deactivateSession();
            }
        } else {
            if (TextUtils.isEmpty(mPreference.getSummary())) {
                mPreference.setSummary(R.string.zen_mode_apps_calculating);
            }
            if (mApplicationsState != null && mHost != null) {
            if (mAppSession == null) {
                mAppSession = mApplicationsState.newSession(mAppSessionCallbacks,
                        mHost.getLifecycle());
            } else {
                mAppSession.activateSession();
            }
            triggerUpdateAppsBypassingDnd();
        }
@@ -133,12 +143,16 @@ class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferenceContr
    }

    private void displayAppsBypassingDnd(List<AppEntry> allApps) {
        ImmutableList<AppEntry> apps = getAppsBypassingDndSortedByName(allApps);
        if (mZenMode.getPolicy().getAllowedChannels() == ZenPolicy.CHANNEL_POLICY_NONE) {
            // Can get this callback when resuming, if we had CHANNEL_POLICY_PRIORITY and just
            // switched to CHANNEL_POLICY_NONE.
            return;
        }

        ImmutableList<AppEntry> apps = getAppsBypassingDndSortedByName(allApps);
        mPreference.setSummary(mSummaryHelper.getAppsSummary(mZenMode, apps));

        mPreference.displayIcons(new CircularIconSet<>(apps,
                app -> Utils.getBadgedIcon(mContext, app.info)),
                app -> mAppIconRetriever.apply(app.info)),
                APP_ENTRY_EQUIVALENCE);
    }

+87 −2
Original line number Diff line number Diff line
@@ -38,6 +38,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.UserInfo;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.os.UserHandle;
import android.os.UserManager;
@@ -102,11 +103,12 @@ public final class ZenModeAppsLinkPreferenceControllerTest {
        mContext = RuntimeEnvironment.application;
        CircularIconSet.sExecutorService = MoreExecutors.newDirectExecutorService();
        mPreference = new TestableCircularIconsPreference(mContext);

        when(mApplicationsState.newSession(any(), any())).thenReturn(mSession);

        mController = new ZenModeAppsLinkPreferenceController(
                mContext, "controller_key", mock(Fragment.class), mApplicationsState,
                mZenModesBackend, mHelperBackend);
                mZenModesBackend, mHelperBackend,
                /* appIconRetriever= */ appInfo -> new ColorDrawable());

        // Ensure the preference view is bound & measured (needed to add child ImageViews).
        View preferenceView = LayoutInflater.from(mContext).inflate(mPreference.getLayoutResource(),
@@ -295,6 +297,89 @@ public final class ZenModeAppsLinkPreferenceControllerTest {
        verify(mSession, times(2)).rebuild(any(), any(), eq(false));
    }

    @Test
    public void updateState_noneToPriority_loadsBypassingAppsAndListensForChanges() {
        ZenMode zenModeWithNone = new TestModeBuilder()
                .setZenPolicy(new ZenPolicy.Builder().allowPriorityChannels(false).build())
                .build();
        ZenMode zenModeWithPriority = new TestModeBuilder()
                .setZenPolicy(new ZenPolicy.Builder().allowPriorityChannels(true).build())
                .build();
        ArrayList<ApplicationsState.AppEntry> appEntries = new ArrayList<>();
        appEntries.add(createAppEntry("test", mContext.getUserId()));
        when(mHelperBackend.getPackagesBypassingDnd(mContext.getUserId(), false))
                .thenReturn(List.of("test"));

        mController.updateState(mPreference, zenModeWithNone);

        assertThat(mPreference.getLoadedIcons().icons()).hasSize(0);
        verifyNoMoreInteractions(mApplicationsState);
        verifyNoMoreInteractions(mSession);

        mController.updateState(mPreference, zenModeWithPriority);

        verify(mApplicationsState).newSession(any(), any());
        verify(mSession).rebuild(any(), any(), anyBoolean());
        mController.mAppSessionCallbacks.onRebuildComplete(appEntries);
        assertThat(mPreference.getLoadedIcons().icons()).hasSize(1);
    }

    @Test
    public void updateState_priorityToNone_clearsBypassingAppsAndStopsListening() {
        ZenMode zenModeWithNone = new TestModeBuilder()
                .setZenPolicy(new ZenPolicy.Builder().allowPriorityChannels(false).build())
                .build();
        ZenMode zenModeWithPriority = new TestModeBuilder()
                .setZenPolicy(new ZenPolicy.Builder().allowPriorityChannels(true).build())
                .build();
        ArrayList<ApplicationsState.AppEntry> appEntries = new ArrayList<>();
        appEntries.add(createAppEntry("test", mContext.getUserId()));
        when(mHelperBackend.getPackagesBypassingDnd(mContext.getUserId(), false))
                .thenReturn(List.of("test"));

        mController.updateState(mPreference, zenModeWithPriority);

        verify(mApplicationsState).newSession(any(), any());
        verify(mSession).rebuild(any(), any(), anyBoolean());
        mController.mAppSessionCallbacks.onRebuildComplete(appEntries);
        assertThat(mPreference.getLoadedIcons().icons()).hasSize(1);

        mController.updateState(mPreference, zenModeWithNone);

        assertThat(mPreference.getLoadedIcons().icons()).hasSize(0);
        verify(mSession).deactivateSession();
        verifyNoMoreInteractions(mSession);
        verifyNoMoreInteractions(mApplicationsState);

        // An errant callback (triggered by onResume and received asynchronously after
        // updateState()) is ignored.
        mController.mAppSessionCallbacks.onRebuildComplete(appEntries);

        assertThat(mPreference.getLoadedIcons().icons()).hasSize(0);
    }

    @Test
    public void updateState_priorityToNoneToPriority_restartsListening() {
        ZenMode zenModeWithNone = new TestModeBuilder()
                .setZenPolicy(new ZenPolicy.Builder().allowPriorityChannels(false).build())
                .build();
        ZenMode zenModeWithPriority = new TestModeBuilder()
                .setZenPolicy(new ZenPolicy.Builder().allowPriorityChannels(true).build())
                .build();

        mController.updateState(mPreference, zenModeWithPriority);
        verify(mApplicationsState).newSession(any(), any());
        verify(mSession).rebuild(any(), any(), anyBoolean());

        mController.updateState(mPreference, zenModeWithNone);
        verifyNoMoreInteractions(mApplicationsState);
        verify(mSession).deactivateSession();

        mController.updateState(mPreference, zenModeWithPriority);
        verifyNoMoreInteractions(mApplicationsState);
        verify(mSession).activateSession();
    }

    @Test
    public void testNoCrashIfAppsReadyBeforeRuleAvailable() {
        mController.mAppSessionCallbacks.onLoadEntriesCompleted();