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

Commit 02a6fc45 authored by petsjonkin's avatar petsjonkin
Browse files

Adding multi-display support to Plugins framework

Bug: b/391563191
Test: atest PluginStorageTest
Flag: EXEMPT feature improvement
Change-Id: Ia074c047b978dc83659f85e1906cb5d74a989dd3
parent e80236e2
Loading
Loading
Loading
Loading
+6 −4
Original line number Diff line number Diff line
@@ -74,15 +74,17 @@ public class PluginManager {
    /**
     * Adds change listener for particular plugin type
     */
    public <T> void subscribe(PluginType<T> type, PluginChangeListener<T> listener) {
        mPluginStorage.addListener(type, listener);
    public <T> void subscribe(PluginType<T> type, String uniqueDisplayId,
            PluginChangeListener<T> listener) {
        mPluginStorage.addListener(type, uniqueDisplayId, listener);
    }

    /**
     * Removes change listener
     */
    public <T> void unsubscribe(PluginType<T> type, PluginChangeListener<T> listener) {
        mPluginStorage.removeListener(type, listener);
    public <T> void unsubscribe(PluginType<T> type, String uniqueDisplayId,
            PluginChangeListener<T> listener) {
        mPluginStorage.removeListener(type, uniqueDisplayId, listener);
    }

    /**
+158 −42
Original line number Diff line number Diff line
@@ -20,10 +20,13 @@ import android.annotation.Nullable;
import android.util.Slog;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.tools.r8.keepanno.annotations.KeepForApi;

import java.io.PrintWriter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@@ -35,42 +38,97 @@ import java.util.Set;
public class PluginStorage {
    private static final String TAG = "PluginStorage";

    // Special ID used to indicate that given value is to be applied globally, rather than to a
    // specific display. If both GLOBAL and specific display values are present - specific display
    // value is selected.
    @VisibleForTesting
    static final String GLOBAL_ID = "GLOBAL";

    private final Object mLock = new Object();
    @GuardedBy("mLock")
    private final Map<PluginType<?>, Object> mValues = new HashMap<>();
    private final Map<PluginType<?>, ValuesContainer<?>> mValues = new HashMap<>();
    @GuardedBy("mLock")
    private final Map<PluginType<?>, ListenersContainer<?>> mListeners = new HashMap<>();
    @GuardedBy("mLock")
    private final PluginEventStorage mPluginEventStorage = new PluginEventStorage();
    private final Map<String, PluginEventStorage> mPluginEventStorages = new HashMap<>();

    /**
     * Updates value in storage and forwards it to corresponding listeners for all displays
     * that does not have display specific value.
     * Should be called by OEM Plugin implementation in order to communicate with Framework
     */
    @KeepForApi
    public <T> void updateGlobalValue(PluginType<T> type, @Nullable T value) {
        updateValue(type, GLOBAL_ID, value);
    }

    /**
     * Updates value in storage and forwards it to corresponding listeners.
     * Should be called by OEM Plugin implementation in order to provide communicate with Framework
     * Updates value in storage and forwards it to corresponding listeners for specific display.
     * Should be called by OEM Plugin implementation in order to communicate with Framework
     * @param type - plugin type, that need to be updated
     * @param uniqueDisplayId - uniqueDisplayId that this type/value should be applied to
     * @param value - plugin value for particular type and display
     */
    @KeepForApi
    public <T> void updateValue(PluginType<T> type, @Nullable T value) {
        Slog.d(TAG, "updateValue, type=" + type.mName + "; value=" + value);
    public <T> void updateValue(PluginType<T> type, String uniqueDisplayId, @Nullable T value) {
        Slog.d(TAG, "updateValue, type=" + type.mName + "; value=" + value
                + "; displayId=" + uniqueDisplayId);
        Set<PluginManager.PluginChangeListener<T>> localListeners;
        T valueToNotify;
        synchronized (mLock) {
            mValues.put(type, value);
            mPluginEventStorage.onValueUpdated(type);
            ListenersContainer<T> container = getListenersContainerForTypeLocked(type);
            localListeners = new LinkedHashSet<>(container.mListeners);
            ValuesContainer<T> valuesByType = getValuesContainerLocked(type);
            valuesByType.updateValueLocked(uniqueDisplayId, value);
            // if value was set to null, we might need to notify with GLOBAL value instead
            valueToNotify = valuesByType.getValueLocked(uniqueDisplayId);

            PluginEventStorage storage = mPluginEventStorages.computeIfAbsent(uniqueDisplayId,
                    d -> new PluginEventStorage());
            storage.onValueUpdated(type);

            localListeners =  getListenersForUpdateLocked(type, uniqueDisplayId);
        }
        Slog.d(TAG, "updateValue, notifying listeners=" + localListeners);
        localListeners.forEach(l -> l.onChanged(value));
        localListeners.forEach(l -> l.onChanged(valueToNotify));
    }

    @GuardedBy("mLock")
    private <T> Set<PluginManager.PluginChangeListener<T>> getListenersForUpdateLocked(
            PluginType<T> type, String uniqueDisplayId) {
        ListenersContainer<T> listenersContainer = getListenersContainerLocked(type);
        Set<PluginManager.PluginChangeListener<T>> localListeners = new LinkedHashSet<>();
        // if GLOBAL value change we need to notify only listeners for displays that does not
        // have display specific value
        if (GLOBAL_ID.equals(uniqueDisplayId)) {
            ValuesContainer<T> valuesContainer = getValuesContainerLocked(type);
            Set<String> excludedDisplayIds = valuesContainer.getNonGlobalDisplaysLocked();
            listenersContainer.mListeners.forEach((localDisplayId, listeners) -> {
                if (!excludedDisplayIds.contains(localDisplayId)) {
                    localListeners.addAll(listeners);
                }
            });
        } else {
            localListeners.addAll(
                    listenersContainer.mListeners.getOrDefault(uniqueDisplayId, Set.of()));
        }
        return localListeners;
    }

    /**
     * Adds listener for PluginType. If storage already has value for this type, listener will
     * be notified immediately.
     */
    <T> void addListener(PluginType<T> type, PluginManager.PluginChangeListener<T> listener) {
    <T> void addListener(PluginType<T> type, String uniqueDisplayId,
            PluginManager.PluginChangeListener<T> listener) {
        if (GLOBAL_ID.equals(uniqueDisplayId)) {
            Slog.d(TAG, "addListener ignored for GLOBAL_ID, type=" + type.mName);
            return;
        }
        T value = null;
        synchronized (mLock) {
            ListenersContainer<T> container = getListenersContainerForTypeLocked(type);
            if (container.mListeners.add(listener)) {
                value = getValueForTypeLocked(type);
            ListenersContainer<T> container = getListenersContainerLocked(type);
            if (container.addListenerLocked(uniqueDisplayId, listener)) {
                ValuesContainer<T> valuesContainer = getValuesContainerLocked(type);
                value = valuesContainer.getValueLocked(uniqueDisplayId);
            }
        }
        if (value != null) {
@@ -81,10 +139,15 @@ public class PluginStorage {
    /**
     * Removes listener
     */
    <T> void removeListener(PluginType<T> type, PluginManager.PluginChangeListener<T> listener) {
    <T> void removeListener(PluginType<T> type, String uniqueDisplayId,
            PluginManager.PluginChangeListener<T> listener) {
        if (GLOBAL_ID.equals(uniqueDisplayId)) {
            Slog.d(TAG, "removeListener ignored for GLOBAL_ID, type=" + type.mName);
            return;
        }
        synchronized (mLock) {
            ListenersContainer<T> container = getListenersContainerForTypeLocked(type);
            container.mListeners.remove(listener);
            ListenersContainer<T> container = getListenersContainerLocked(type);
            container.removeListenerLocked(uniqueDisplayId, listener);
        }
    }

@@ -92,42 +155,37 @@ public class PluginStorage {
     * Print the object's state and debug information into the given stream.
     */
    void dump(PrintWriter pw) {
        Map<PluginType<?>, Object> localValues;
        Map<PluginType<?>, Map<String, Object>> localValues = new HashMap<>();
        @SuppressWarnings("rawtypes")
        Map<PluginType, Set> localListeners = new HashMap<>();
        List<PluginEventStorage.TimeFrame> timeFrames;
        Map<PluginType, Map<String, Set>> localListeners = new HashMap<>();
        Map<String, List<PluginEventStorage.TimeFrame>> timeFrames = new HashMap<>();
        synchronized (mLock) {
            timeFrames = mPluginEventStorage.getTimeFrames();
            localValues = new HashMap<>(mValues);
            mListeners.forEach((type, container) -> localListeners.put(type, container.mListeners));
            mPluginEventStorages.forEach((displayId, storage) -> {
                timeFrames.put(displayId, storage.getTimeFrames());
            });
            mValues.forEach((type, valueContainer) -> {
                localValues.put(type, new HashMap<>(valueContainer.mValues));
            });
            mListeners.forEach((type, container) -> {
                localListeners.put(type, new HashMap<>(container.mListeners));
            });
        }
        pw.println("PluginStorage:");
        pw.println("values=" + localValues);
        pw.println("listeners=" + localListeners);
        pw.println("PluginEventStorage:");
        for (PluginEventStorage.TimeFrame timeFrame: timeFrames) {
        for (Map.Entry<String, List<PluginEventStorage.TimeFrame>> timeFrameEntry :
                timeFrames.entrySet()) {
            pw.println("TimeFrames for displayId=" + timeFrameEntry.getKey());
            for (PluginEventStorage.TimeFrame timeFrame : timeFrameEntry.getValue()) {
                timeFrame.dump(pw);
            }
        }

    @GuardedBy("mLock")
    @SuppressWarnings("unchecked")
    private <T> T getValueForTypeLocked(PluginType<T> type) {
        Object value = mValues.get(type);
        if (value == null) {
            return null;
        } else if (type.mType == value.getClass()) {
            return (T) value;
        } else {
            Slog.d(TAG, "getValueForType: unexpected value type=" + value.getClass().getName()
                    + ", expected=" + type.mType.getName());
            return null;
        }
    }

    @GuardedBy("mLock")
    @SuppressWarnings("unchecked")
    private <T> ListenersContainer<T> getListenersContainerForTypeLocked(PluginType<T> type) {
    private <T> ListenersContainer<T> getListenersContainerLocked(PluginType<T> type) {
        ListenersContainer<?> container = mListeners.get(type);
        if (container == null) {
            ListenersContainer<T> lc = new ListenersContainer<>();
@@ -138,7 +196,65 @@ public class PluginStorage {
        }
    }

    @GuardedBy("mLock")
    @SuppressWarnings("unchecked")
    private <T> ValuesContainer<T> getValuesContainerLocked(PluginType<T> type) {
        ValuesContainer<?> container = mValues.get(type);
        if (container == null) {
            ValuesContainer<T> vc = new ValuesContainer<>();
            mValues.put(type, vc);
            return vc;
        } else {
            return (ValuesContainer<T>) container;
        }
    }

    private static final class ListenersContainer<T> {
        private final Set<PluginManager.PluginChangeListener<T>> mListeners = new LinkedHashSet<>();
        private final Map<String, Set<PluginManager.PluginChangeListener<T>>> mListeners =
                new LinkedHashMap<>();

        private boolean addListenerLocked(
                String uniqueDisplayId, PluginManager.PluginChangeListener<T> listener) {
            Set<PluginManager.PluginChangeListener<T>> listenersForDisplay =
                    mListeners.computeIfAbsent(uniqueDisplayId, k -> new LinkedHashSet<>());
            return listenersForDisplay.add(listener);
        }

        private void removeListenerLocked(String uniqueDisplayId,
                PluginManager.PluginChangeListener<T> listener) {
            Set<PluginManager.PluginChangeListener<T>> listenersForDisplay = mListeners.get(
                    uniqueDisplayId);
            if (listenersForDisplay == null) {
                return;
            }

            listenersForDisplay.remove(listener);

            if (listenersForDisplay.isEmpty()) {
                mListeners.remove(uniqueDisplayId);
            }
        }
    }

    private static final class ValuesContainer<T> {
        private final Map<String, T> mValues = new HashMap<>();

        private void updateValueLocked(String uniqueDisplayId, @Nullable T value) {
            if (value == null) {
                mValues.remove(uniqueDisplayId);
            } else {
                mValues.put(uniqueDisplayId, value);
            }
        }

        private Set<String> getNonGlobalDisplaysLocked() {
            Set<String> keys = new HashSet<>(mValues.keySet());
            keys.remove(GLOBAL_ID);
            return keys;
        }

        private @Nullable T getValueLocked(String displayId) {
            return mValues.getOrDefault(displayId, mValues.get(GLOBAL_ID));
        }
    }
}
+5 −4
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

private val TEST_PLUGIN_TYPE = PluginType(Int::class.java, "test_type")
private val DISPLAY_ID = "display_id"

@SmallTest
class PluginManagerTest {
@@ -62,18 +63,18 @@ class PluginManagerTest {
    fun testSubscribe() {
        val pluginManager = createPluginManager()

        pluginManager.subscribe(TEST_PLUGIN_TYPE, mockListener)
        pluginManager.subscribe(TEST_PLUGIN_TYPE, DISPLAY_ID, mockListener)

        verify(testInjector.mockStorage).addListener(TEST_PLUGIN_TYPE, mockListener)
        verify(testInjector.mockStorage).addListener(TEST_PLUGIN_TYPE, DISPLAY_ID, mockListener)
    }

    @Test
    fun testUnsubscribe() {
        val pluginManager = createPluginManager()

        pluginManager.unsubscribe(TEST_PLUGIN_TYPE, mockListener)
        pluginManager.unsubscribe(TEST_PLUGIN_TYPE, DISPLAY_ID, mockListener)

        verify(testInjector.mockStorage).removeListener(TEST_PLUGIN_TYPE, mockListener)
        verify(testInjector.mockStorage).removeListener(TEST_PLUGIN_TYPE, DISPLAY_ID, mockListener)
    }

    private fun createPluginManager(enabled: Boolean = true): PluginManager {
+62 −13
Original line number Diff line number Diff line
@@ -23,6 +23,8 @@ import org.junit.Test

private val TEST_PLUGIN_TYPE1 = PluginType(String::class.java, "test_type1")
private val TEST_PLUGIN_TYPE2 = PluginType(String::class.java, "test_type2")
private val DISPLAY_ID_1 = "display_1"
private val DISPLAY_ID_2 = "display_2"

@SmallTest
class PluginStorageTest {
@@ -33,9 +35,9 @@ class PluginStorageTest {
    fun testUpdateValue() {
        val type1Value = "value1"
        val testChangeListener = TestPluginChangeListener<String>()
        storage.addListener(TEST_PLUGIN_TYPE1, testChangeListener)
        storage.addListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, testChangeListener)

        storage.updateValue(TEST_PLUGIN_TYPE1, type1Value)
        storage.updateValue(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, type1Value)

        assertThat(testChangeListener.receivedValue).isEqualTo(type1Value)
    }
@@ -44,9 +46,9 @@ class PluginStorageTest {
    fun testAddListener() {
        val type1Value = "value1"
        val testChangeListener = TestPluginChangeListener<String>()
        storage.updateValue(TEST_PLUGIN_TYPE1, type1Value)
        storage.updateValue(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, type1Value)

        storage.addListener(TEST_PLUGIN_TYPE1, testChangeListener)
        storage.addListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, testChangeListener)

        assertThat(testChangeListener.receivedValue).isEqualTo(type1Value)
    }
@@ -55,10 +57,10 @@ class PluginStorageTest {
    fun testRemoveListener() {
        val type1Value = "value1"
        val testChangeListener = TestPluginChangeListener<String>()
        storage.addListener(TEST_PLUGIN_TYPE1, testChangeListener)
        storage.removeListener(TEST_PLUGIN_TYPE1, testChangeListener)
        storage.addListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, testChangeListener)
        storage.removeListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, testChangeListener)

        storage.updateValue(TEST_PLUGIN_TYPE1, type1Value)
        storage.updateValue(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, type1Value)

        assertThat(testChangeListener.receivedValue).isNull()
    }
@@ -68,10 +70,10 @@ class PluginStorageTest {
        val type1Value = "value1"
        val type2Value = "value2"
        val testChangeListener = TestPluginChangeListener<String>()
        storage.updateValue(TEST_PLUGIN_TYPE1, type1Value)
        storage.updateValue(TEST_PLUGIN_TYPE2, type2Value)
        storage.updateValue(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, type1Value)
        storage.updateValue(TEST_PLUGIN_TYPE2, DISPLAY_ID_1, type2Value)

        storage.addListener(TEST_PLUGIN_TYPE1, testChangeListener)
        storage.addListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, testChangeListener)

        assertThat(testChangeListener.receivedValue).isEqualTo(type1Value)
    }
@@ -81,15 +83,62 @@ class PluginStorageTest {
        val type1Value = "value1"
        val testChangeListener1 = TestPluginChangeListener<String>()
        val testChangeListener2 = TestPluginChangeListener<String>()
        storage.addListener(TEST_PLUGIN_TYPE1, testChangeListener1)
        storage.addListener(TEST_PLUGIN_TYPE2, testChangeListener2)
        storage.addListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, testChangeListener1)
        storage.addListener(TEST_PLUGIN_TYPE2, DISPLAY_ID_1, testChangeListener2)

        storage.updateValue(TEST_PLUGIN_TYPE1, type1Value)
        storage.updateValue(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, type1Value)

        assertThat(testChangeListener1.receivedValue).isEqualTo(type1Value)
        assertThat(testChangeListener2.receivedValue).isNull()
    }

    @Test
    fun testUpdateGlobal_noDisplaySpecificValue() {
        val type1Value = "value1"
        val testChangeListener1 = TestPluginChangeListener<String>()
        val testChangeListener2 = TestPluginChangeListener<String>()
        storage.addListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, testChangeListener1)
        storage.addListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_2, testChangeListener2)

        storage.updateGlobalValue(TEST_PLUGIN_TYPE1, type1Value)

        assertThat(testChangeListener1.receivedValue).isEqualTo(type1Value)
        assertThat(testChangeListener2.receivedValue).isEqualTo(type1Value)
    }

    @Test
    fun testUpdateGlobal_withDisplaySpecificValue() {
        val type1Value = "value1"
        val type1GlobalValue = "value1Global"
        val testChangeListener1 = TestPluginChangeListener<String>()
        val testChangeListener2 = TestPluginChangeListener<String>()
        storage.addListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, testChangeListener1)
        storage.addListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_2, testChangeListener2)

        storage.updateValue(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, type1Value)
        storage.updateGlobalValue(TEST_PLUGIN_TYPE1, type1GlobalValue)

        assertThat(testChangeListener1.receivedValue).isEqualTo(type1Value)
        assertThat(testChangeListener2.receivedValue).isEqualTo(type1GlobalValue)
    }

    @Test
    fun testUpdateGlobal_withDisplaySpecificValueRemoved() {
        val type1Value = "value1"
        val type1GlobalValue = "value1Global"
        val testChangeListener1 = TestPluginChangeListener<String>()
        val testChangeListener2 = TestPluginChangeListener<String>()
        storage.addListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, testChangeListener1)
        storage.addListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_2, testChangeListener2)

        storage.updateValue(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, type1Value)
        storage.updateGlobalValue(TEST_PLUGIN_TYPE1, type1GlobalValue)
        storage.updateValue(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, null)

        assertThat(testChangeListener1.receivedValue).isEqualTo(type1GlobalValue)
        assertThat(testChangeListener2.receivedValue).isEqualTo(type1GlobalValue)
    }

    private class TestPluginChangeListener<T> : PluginChangeListener<T> {
        var receivedValue: T? = null