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

Commit c960b0b2 authored by Darrell Shi's avatar Darrell Shi
Browse files

Introduction to communal conditions monitor.

Communal conditions monitor takes a set of conditions, monitors
whether all have been fulfilled, and informs its registered
listeners.

Test: atest CommunalConditionTest CommunalConditionsMonitorTest
Bug: 202778351
Change-Id: I5b09ec1d678864141788a84eefe5d6de80bfcb77
parent fb6d7eaa
Loading
Loading
Loading
Loading
+130 −0
Original line number Original line 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.systemui.communal.conditions;

import android.util.Log;

import com.android.systemui.statusbar.policy.CallbackController;

import org.jetbrains.annotations.NotNull;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Iterator;

/**
 * Base class for a condition that needs to be fulfilled in order for Communal Mode to display.
 */
public abstract class CommunalCondition implements CallbackController<CommunalCondition.Callback> {
    private final String mTag = getClass().getSimpleName();

    private final ArrayList<WeakReference<Callback>> mCallbacks = new ArrayList<>();
    private boolean mIsConditionMet = false;
    private boolean mStarted = false;

    /**
     * Starts monitoring the condition.
     */
    protected abstract void start();

    /**
     * Stops monitoring the condition.
     */
    protected abstract void stop();

    /**
     * Registers a callback to receive updates once started. This should be called before
     * {@link #start()}. Also triggers the callback immediately if already started.
     */
    @Override
    public void addCallback(@NotNull Callback callback) {
        if (shouldLog()) Log.d(mTag, "adding callback");
        mCallbacks.add(new WeakReference<>(callback));

        if (mStarted) {
            callback.onConditionChanged(this, mIsConditionMet);
            return;
        }

        start();
        mStarted = true;
    }

    /**
     * Removes the provided callback from further receiving updates.
     */
    @Override
    public void removeCallback(@NotNull Callback callback) {
        if (shouldLog()) Log.d(mTag, "removing callback");
        final Iterator<WeakReference<Callback>> iterator = mCallbacks.iterator();
        while (iterator.hasNext()) {
            final Callback cb = iterator.next().get();
            if (cb == null || cb == callback) {
                iterator.remove();
            }
        }

        if (!mCallbacks.isEmpty() || !mStarted) {
            return;
        }

        stop();
        mStarted = false;
    }

    /**
     * Updates the value for whether the condition has been fulfilled, and sends an update if the
     * value changes and any callback is registered.
     *
     * @param isConditionMet True if the condition has been fulfilled. False otherwise.
     */
    protected void updateCondition(boolean isConditionMet) {
        if (mIsConditionMet == isConditionMet) {
            return;
        }

        if (shouldLog()) Log.d(mTag, "updating condition to " + isConditionMet);
        mIsConditionMet = isConditionMet;

        final Iterator<WeakReference<Callback>> iterator = mCallbacks.iterator();
        while (iterator.hasNext()) {
            final Callback cb = iterator.next().get();
            if (cb == null) {
                iterator.remove();
            } else {
                cb.onConditionChanged(this, mIsConditionMet);
            }
        }
    }

    private boolean shouldLog() {
        return Log.isLoggable(mTag, Log.DEBUG);
    }

    /**
     * Callback that receives updates about whether the condition has been fulfilled.
     */
    public interface Callback {
        /**
         * Called when the fulfillment of the condition changes.
         *
         * @param condition The condition in question.
         * @param isConditionMet True if the condition has been fulfilled. False otherwise.
         */
        void onConditionChanged(CommunalCondition condition, boolean isConditionMet);
    }
}
+154 −0
Original line number Original line 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.systemui.communal.conditions;

import static com.android.systemui.communal.dagger.CommunalModule.COMMUNAL_CONDITIONS;

import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.statusbar.policy.CallbackController;

import org.jetbrains.annotations.NotNull;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;

import javax.inject.Inject;
import javax.inject.Named;

/**
 * {@link CommunalConditionsMonitor} takes in a set of conditions, monitors whether all of them have
 * been fulfilled, and informs any registered listeners.
 */
@SysUISingleton
public class CommunalConditionsMonitor implements
        CallbackController<CommunalConditionsMonitor.Callback> {
    private final String mTag = getClass().getSimpleName();

    private final ArrayList<WeakReference<Callback>> mCallbacks = new ArrayList<>();

    // Set of all conditions that need to be monitored.
    private final Set<CommunalCondition> mConditions;

    // Map of values of each condition.
    private final HashMap<CommunalCondition, Boolean> mConditionsMap = new HashMap<>();

    // Whether all conditions have been met.
    private boolean mAllConditionsMet = false;

    // Whether the monitor has started listening for all the conditions.
    private boolean mHaveConditionsStarted = false;

    // Callback for when each condition has been updated.
    private final CommunalCondition.Callback mConditionCallback = (condition, isConditionMet) -> {
        mConditionsMap.put(condition, isConditionMet);

        final boolean newAllConditionsMet = !mConditionsMap.containsValue(false);

        if (newAllConditionsMet == mAllConditionsMet) {
            return;
        }

        if (shouldLog()) Log.d(mTag, "all conditions met: " + newAllConditionsMet);
        mAllConditionsMet = newAllConditionsMet;

        // Updates all callbacks.
        final Iterator<WeakReference<Callback>> iterator = mCallbacks.iterator();
        while (iterator.hasNext()) {
            final Callback callback = iterator.next().get();
            if (callback == null) {
                iterator.remove();
            } else {
                callback.onConditionsChanged(mAllConditionsMet);
            }
        }
    };

    @Inject
    public CommunalConditionsMonitor(
            @Named(COMMUNAL_CONDITIONS) Set<CommunalCondition> communalConditions) {
        mConditions = communalConditions;

        // Initializes the conditions map and registers a callback for each condition.
        mConditions.forEach((condition -> mConditionsMap.put(condition, false)));
    }

    @Override
    public void addCallback(@NotNull Callback callback) {
        if (shouldLog()) Log.d(mTag, "adding callback");
        mCallbacks.add(new WeakReference<>(callback));

        // Updates the callback immediately.
        callback.onConditionsChanged(mAllConditionsMet);

        if (!mHaveConditionsStarted) {
            if (shouldLog()) Log.d(mTag, "starting all conditions");
            mConditions.forEach(condition -> condition.addCallback(mConditionCallback));
            mHaveConditionsStarted = true;
        }
    }

    @Override
    public void removeCallback(@NotNull Callback callback) {
        if (shouldLog()) Log.d(mTag, "removing callback");
        final Iterator<WeakReference<Callback>> iterator = mCallbacks.iterator();
        while (iterator.hasNext()) {
            final Callback cb = iterator.next().get();
            if (cb == null || cb == callback) {
                iterator.remove();
            }
        }

        if (mCallbacks.isEmpty() && mHaveConditionsStarted) {
            if (shouldLog()) Log.d(mTag, "stopping all conditions");
            mConditions.forEach(condition -> condition.removeCallback(mConditionCallback));

            mAllConditionsMet = false;
            mHaveConditionsStarted = false;
        }
    }

    /**
     * Force updates each condition to the value provided.
     */
    @VisibleForTesting
    public void overrideAllConditionsMet(boolean value) {
        mConditions.forEach(condition -> condition.updateCondition(value));
    }

    private boolean shouldLog() {
        return Log.isLoggable(mTag, Log.DEBUG);
    }

    /**
     * Callback that receives updates of whether all conditions have been fulfilled.
     */
    public interface Callback {
        /**
         * Triggered when the fulfillment of all conditions have been met.
         *
         * @param allConditionsMet True if all conditions have been fulfilled. False if none or
         *                         only partial conditions have been fulfilled.
         */
        void onConditionsChanged(boolean allConditionsMet);
    }
}
+16 −0
Original line number Original line Diff line number Diff line
@@ -20,15 +20,20 @@ import android.content.Context;
import android.view.View;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.FrameLayout;


import com.android.systemui.communal.conditions.CommunalCondition;
import com.android.systemui.idle.AmbientLightModeMonitor;
import com.android.systemui.idle.AmbientLightModeMonitor;
import com.android.systemui.idle.LightSensorEventsDebounceAlgorithm;
import com.android.systemui.idle.LightSensorEventsDebounceAlgorithm;
import com.android.systemui.idle.dagger.IdleViewComponent;
import com.android.systemui.idle.dagger.IdleViewComponent;


import java.util.HashSet;
import java.util.Set;

import javax.inject.Named;
import javax.inject.Named;


import dagger.Binds;
import dagger.Binds;
import dagger.Module;
import dagger.Module;
import dagger.Provides;
import dagger.Provides;
import dagger.multibindings.ElementsIntoSet;


/**
/**
 * Dagger Module providing Communal-related functionality.
 * Dagger Module providing Communal-related functionality.
@@ -39,6 +44,7 @@ import dagger.Provides;
})
})
public interface CommunalModule {
public interface CommunalModule {
    String IDLE_VIEW = "idle_view";
    String IDLE_VIEW = "idle_view";
    String COMMUNAL_CONDITIONS = "communal_conditions";


    /** */
    /** */
    @Provides
    @Provides
@@ -56,4 +62,14 @@ public interface CommunalModule {
    @Binds
    @Binds
    AmbientLightModeMonitor.DebounceAlgorithm ambientLightDebounceAlgorithm(
    AmbientLightModeMonitor.DebounceAlgorithm ambientLightDebounceAlgorithm(
            LightSensorEventsDebounceAlgorithm algorithm);
            LightSensorEventsDebounceAlgorithm algorithm);

    /**
     * Provides a set of conditions that need to be fulfilled in order for Communal Mode to display.
     */
    @Provides
    @ElementsIntoSet
    @Named(COMMUNAL_CONDITIONS)
    static Set<CommunalCondition> provideCommunalConditions() {
        return new HashSet<>();
    }
}
}
+133 −0
Original line number Original line 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.systemui.communal;

import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
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;

import android.testing.AndroidTestingRunner;

import androidx.test.filters.SmallTest;

import com.android.systemui.SysuiTestCase;
import com.android.systemui.communal.conditions.CommunalCondition;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

@SmallTest
@RunWith(AndroidTestingRunner.class)
public class CommunalConditionTest extends SysuiTestCase {
    private FakeCommunalCondition mCondition;

    @Before
    public void setup() {
        mCondition = spy(new FakeCommunalCondition());
    }

    @Test
    public void addCallback_addFirstCallback_triggerStart() {
        final CommunalCondition.Callback callback = mock(CommunalCondition.Callback.class);
        mCondition.addCallback(callback);
        verify(mCondition).start();
    }

    @Test
    public void addCallback_addMultipleCallbacks_triggerStartOnlyOnce() {
        final CommunalCondition.Callback callback1 = mock(CommunalCondition.Callback.class);
        final CommunalCondition.Callback callback2 = mock(CommunalCondition.Callback.class);
        final CommunalCondition.Callback callback3 = mock(CommunalCondition.Callback.class);

        mCondition.addCallback(callback1);
        mCondition.addCallback(callback2);
        mCondition.addCallback(callback3);

        verify(mCondition, times(1)).start();
    }

    @Test
    public void addCallback_alreadyStarted_triggerUpdate() {
        final CommunalCondition.Callback callback1 = mock(CommunalCondition.Callback.class);
        mCondition.addCallback(callback1);

        mCondition.fakeUpdateCondition(true);

        final CommunalCondition.Callback callback2 = mock(CommunalCondition.Callback.class);
        mCondition.addCallback(callback2);
        verify(callback2).onConditionChanged(mCondition, true);
    }

    @Test
    public void removeCallback_removeLastCallback_triggerStop() {
        final CommunalCondition.Callback callback = mock(CommunalCondition.Callback.class);
        mCondition.addCallback(callback);
        verify(mCondition, never()).stop();

        mCondition.removeCallback(callback);
        verify(mCondition).stop();
    }

    @Test
    public void updateCondition_falseToTrue_reportTrue() {
        mCondition.fakeUpdateCondition(false);

        final CommunalCondition.Callback callback = mock(CommunalCondition.Callback.class);
        mCondition.addCallback(callback);

        mCondition.fakeUpdateCondition(true);
        verify(callback).onConditionChanged(eq(mCondition), eq(true));
    }

    @Test
    public void updateCondition_trueToFalse_reportFalse() {
        mCondition.fakeUpdateCondition(true);

        final CommunalCondition.Callback callback = mock(CommunalCondition.Callback.class);
        mCondition.addCallback(callback);

        mCondition.fakeUpdateCondition(false);
        verify(callback).onConditionChanged(eq(mCondition), eq(false));
    }

    @Test
    public void updateCondition_trueToTrue_reportNothing() {
        mCondition.fakeUpdateCondition(true);

        final CommunalCondition.Callback callback = mock(CommunalCondition.Callback.class);
        mCondition.addCallback(callback);

        mCondition.fakeUpdateCondition(true);
        verify(callback, never()).onConditionChanged(eq(mCondition), anyBoolean());
    }

    @Test
    public void updateCondition_falseToFalse_reportNothing() {
        mCondition.fakeUpdateCondition(false);

        final CommunalCondition.Callback callback = mock(CommunalCondition.Callback.class);
        mCondition.addCallback(callback);

        mCondition.fakeUpdateCondition(false);
        verify(callback, never()).onConditionChanged(eq(mCondition), anyBoolean());
    }
}
+178 −0
Original line number Original line 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.systemui.communal;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.anyBoolean;
import static org.mockito.Mockito.clearInvocations;
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;

import android.testing.AndroidTestingRunner;

import androidx.test.filters.SmallTest;

import com.android.systemui.SysuiTestCase;
import com.android.systemui.communal.conditions.CommunalCondition;
import com.android.systemui.communal.conditions.CommunalConditionsMonitor;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.MockitoAnnotations;

import java.util.Arrays;
import java.util.HashSet;

@SmallTest
@RunWith(AndroidTestingRunner.class)
public class CommunalConditionsMonitorTest extends SysuiTestCase {
    private FakeCommunalCondition mCondition1;
    private FakeCommunalCondition mCondition2;
    private FakeCommunalCondition mCondition3;
    private HashSet<CommunalCondition> mConditions;

    private CommunalConditionsMonitor mCommunalConditionsMonitor;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);

        mCondition1 = spy(new FakeCommunalCondition());
        mCondition2 = spy(new FakeCommunalCondition());
        mCondition3 = spy(new FakeCommunalCondition());
        mConditions = new HashSet<>(Arrays.asList(mCondition1, mCondition2, mCondition3));

        mCommunalConditionsMonitor = new CommunalConditionsMonitor(mConditions);
    }

    @Test
    public void addCallback_addFirstCallback_addCallbackToAllConditions() {
        final CommunalConditionsMonitor.Callback callback1 =
                mock(CommunalConditionsMonitor.Callback.class);
        mCommunalConditionsMonitor.addCallback(callback1);
        mConditions.forEach(condition -> verify(condition).addCallback(any()));

        final CommunalConditionsMonitor.Callback callback2 =
                mock(CommunalConditionsMonitor.Callback.class);
        mCommunalConditionsMonitor.addCallback(callback2);
        mConditions.forEach(condition -> verify(condition, times(1)).addCallback(any()));
    }

    @Test
    public void addCallback_addFirstCallback_reportWithDefaultValue() {
        final CommunalConditionsMonitor.Callback callback =
                mock(CommunalConditionsMonitor.Callback.class);
        mCommunalConditionsMonitor.addCallback(callback);
        verify(callback).onConditionsChanged(false);
    }

    @Test
    public void addCallback_addSecondCallback_reportWithExistingValue() {
        final CommunalConditionsMonitor.Callback callback1 =
                mock(CommunalConditionsMonitor.Callback.class);
        mCommunalConditionsMonitor.addCallback(callback1);

        mCommunalConditionsMonitor.overrideAllConditionsMet(true);

        final CommunalConditionsMonitor.Callback callback2 =
                mock(CommunalConditionsMonitor.Callback.class);
        mCommunalConditionsMonitor.addCallback(callback2);
        verify(callback2).onConditionsChanged(true);
    }

    @Test
    public void removeCallback_shouldNoLongerReceiveUpdate() {
        final CommunalConditionsMonitor.Callback callback =
                mock(CommunalConditionsMonitor.Callback.class);
        mCommunalConditionsMonitor.addCallback(callback);
        clearInvocations(callback);
        mCommunalConditionsMonitor.removeCallback(callback);

        mCommunalConditionsMonitor.overrideAllConditionsMet(true);
        verify(callback, never()).onConditionsChanged(true);

        mCommunalConditionsMonitor.overrideAllConditionsMet(false);
        verify(callback, never()).onConditionsChanged(false);
    }

    @Test
    public void removeCallback_removeLastCallback_removeCallbackFromAllConditions() {
        final CommunalConditionsMonitor.Callback callback1 =
                mock(CommunalConditionsMonitor.Callback.class);
        final CommunalConditionsMonitor.Callback callback2 =
                mock(CommunalConditionsMonitor.Callback.class);
        mCommunalConditionsMonitor.addCallback(callback1);
        mCommunalConditionsMonitor.addCallback(callback2);

        mCommunalConditionsMonitor.removeCallback(callback1);
        mConditions.forEach(condition -> verify(condition, never()).removeCallback(any()));

        mCommunalConditionsMonitor.removeCallback(callback2);
        mConditions.forEach(condition -> verify(condition).removeCallback(any()));
    }

    @Test
    public void updateCallbacks_allConditionsMet_reportTrue() {
        final CommunalConditionsMonitor.Callback callback =
                mock(CommunalConditionsMonitor.Callback.class);
        mCommunalConditionsMonitor.addCallback(callback);
        clearInvocations(callback);

        mCondition1.fakeUpdateCondition(true);
        mCondition2.fakeUpdateCondition(true);
        mCondition3.fakeUpdateCondition(true);

        verify(callback).onConditionsChanged(true);
    }

    @Test
    public void updateCallbacks_oneConditionStoppedMeeting_reportFalse() {
        final CommunalConditionsMonitor.Callback callback =
                mock(CommunalConditionsMonitor.Callback.class);
        mCommunalConditionsMonitor.addCallback(callback);

        mCondition1.fakeUpdateCondition(true);
        mCondition2.fakeUpdateCondition(true);
        mCondition3.fakeUpdateCondition(true);
        clearInvocations(callback);

        mCondition1.fakeUpdateCondition(false);
        verify(callback).onConditionsChanged(false);
    }

    @Test
    public void updateCallbacks_shouldOnlyUpdateWhenValueChanges() {
        final CommunalConditionsMonitor.Callback callback =
                mock(CommunalConditionsMonitor.Callback.class);
        mCommunalConditionsMonitor.addCallback(callback);
        verify(callback).onConditionsChanged(false);
        clearInvocations(callback);

        mCondition1.fakeUpdateCondition(true);
        verify(callback, never()).onConditionsChanged(anyBoolean());

        mCondition2.fakeUpdateCondition(true);
        verify(callback, never()).onConditionsChanged(anyBoolean());

        mCondition3.fakeUpdateCondition(true);
        verify(callback).onConditionsChanged(true);
    }
}
Loading