summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/res/res/values/config_display.xml2
-rw-r--r--core/res/res/values/symbols.xml1
-rw-r--r--services/core/java/com/android/server/display/DisplayManagerService.java12
-rw-r--r--services/core/java/com/android/server/display/plugin/Plugin.java44
-rw-r--r--services/core/java/com/android/server/display/plugin/PluginManager.java141
-rw-r--r--services/core/java/com/android/server/display/plugin/PluginStorage.java134
-rw-r--r--services/core/java/com/android/server/display/plugin/PluginType.java48
-rw-r--r--services/core/java/com/android/server/display/plugin/PluginsProvider.java36
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/plugin/PluginManagerTest.kt97
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/plugin/PluginStorageTest.kt100
10 files changed, 612 insertions, 3 deletions
diff --git a/core/res/res/values/config_display.xml b/core/res/res/values/config_display.xml
index 2e66060926fd..c458d0e9a3c0 100644
--- a/core/res/res/values/config_display.xml
+++ b/core/res/res/values/config_display.xml
@@ -29,5 +29,7 @@
<!-- Whether even dimmer feature is enabled. -->
<bool name="config_evenDimmerEnabled">false</bool>
+ <!-- Jar file path to look for PluginProvider -->
+ <string name="config_pluginsProviderJarPath"/>
</resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index badb98686fb2..55204531ba0e 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -5582,6 +5582,7 @@
<!-- DisplayManager configs. -->
<java-symbol type="bool" name="config_evenDimmerEnabled" />
+ <java-symbol type="string" name="config_pluginsProviderJarPath" />
<java-symbol type="bool" name="config_watchlistUseFileHashesCache" />
<java-symbol type="string" name="config_defaultContextualSearchPackageName" />
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 5a2610b00772..bb5ab15c2122 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -177,6 +177,7 @@ import com.android.server.display.feature.DisplayManagerFlags;
import com.android.server.display.layout.Layout;
import com.android.server.display.mode.DisplayModeDirector;
import com.android.server.display.notifications.DisplayNotificationManager;
+import com.android.server.display.plugin.PluginManager;
import com.android.server.display.utils.DebugUtils;
import com.android.server.display.utils.SensorUtils;
import com.android.server.input.InputManagerInternal;
@@ -583,6 +584,7 @@ public final class DisplayManagerService extends SystemService {
private final DisplayNotificationManager mDisplayNotificationManager;
private final ExternalDisplayStatsService mExternalDisplayStatsService;
+ private final PluginManager mPluginManager;
// Manages the relative placement of extended displays
@Nullable
@@ -669,6 +671,7 @@ public final class DisplayManagerService extends SystemService {
} else {
mDisplayTopologyCoordinator = null;
}
+ mPluginManager = new PluginManager(mContext, mFlags);
}
public void setupSchedulerPolicies() {
@@ -739,6 +742,7 @@ public final class DisplayManagerService extends SystemService {
mLogicalDisplayMapper.onBootCompleted();
mDisplayNotificationManager.onBootCompleted();
mExternalDisplayPolicy.onBootCompleted();
+ mPluginManager.onBootCompleted();
}
}
@@ -3543,6 +3547,9 @@ public final class DisplayManagerService extends SystemService {
SparseArray<DisplayPowerController> displayPowerControllersLocal = new SparseArray<>();
int displayPowerControllerCount;
+ IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
+ ipw.increaseIndent();
+
synchronized (mSyncRoot) {
brightnessTrackerLocal = mBrightnessTracker;
@@ -3590,9 +3597,6 @@ public final class DisplayManagerService extends SystemService {
pw.println(" Display SdrBrightness=" + brightnessPair.sdrBrightness);
}
- IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
- ipw.increaseIndent();
-
pw.println();
pw.println("Display Adapters: size=" + mDisplayAdapters.size());
pw.println("------------------------");
@@ -3655,6 +3659,8 @@ public final class DisplayManagerService extends SystemService {
pw.println();
mDisplayTopologyCoordinator.dump(pw);
}
+ pw.println();
+ mPluginManager.dump(ipw);
pw.println();
mFlags.dump(pw);
diff --git a/services/core/java/com/android/server/display/plugin/Plugin.java b/services/core/java/com/android/server/display/plugin/Plugin.java
new file mode 100644
index 000000000000..1bef4641b9ad
--- /dev/null
+++ b/services/core/java/com/android/server/display/plugin/Plugin.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2024 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.server.display.plugin;
+
+import com.android.tools.r8.keepanno.annotations.KeepForApi;
+
+import java.io.PrintWriter;
+
+/**
+ * Interface that OEMs should implement in order to inject custom code to system process.
+ * Communication between OEM Plugin and Framework is implemented via {@link PluginStorage}.
+ * OEM Plugin pushes values to PluginStorage, that are picked up by
+ * {@link PluginManager.PluginChangeListener}, implemented on Framework side.
+ * Avoid calling heavy operations in constructor - it will be called during boot and will
+ * negatively impact boot time. Use onBootComplete and separate thread for long running operations.
+ */
+@KeepForApi
+public interface Plugin {
+
+ /**
+ * Called when device boot completed
+ */
+ void onBootCompleted();
+
+ /**
+ * Print the object's state and debug information into the given stream.
+ */
+ void dump(PrintWriter pw);
+}
+
diff --git a/services/core/java/com/android/server/display/plugin/PluginManager.java b/services/core/java/com/android/server/display/plugin/PluginManager.java
new file mode 100644
index 000000000000..d4099975cafa
--- /dev/null
+++ b/services/core/java/com/android/server/display/plugin/PluginManager.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2024 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.server.display.plugin;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.SystemServerClassLoaderFactory;
+import com.android.server.display.feature.DisplayManagerFlags;
+
+import dalvik.system.PathClassLoader;
+
+import java.io.PrintWriter;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Responsible for loading Plugins. Plugins and PluginSupplier are loaded from
+ * standalone system jar.
+ * Plugin manager will look for PROVIDER_IMPL_CLASS in configured jar.
+ * After device booted, PluginManager will delegate this call to each Plugin
+ */
+public class PluginManager {
+ private static final String PROVIDER_IMPL_CLASS =
+ "com.android.server.display.plugin.PluginsProviderImpl";
+ private static final String TAG = "PluginManager";
+
+ private final DisplayManagerFlags mFlags;
+ private final PluginStorage mPluginStorage;
+ private final List<Plugin> mPlugins;
+
+ public PluginManager(Context context, DisplayManagerFlags flags) {
+ this(context, flags, new Injector());
+ }
+
+ @VisibleForTesting
+ PluginManager(Context context, DisplayManagerFlags flags, Injector injector) {
+ mFlags = flags;
+ mPluginStorage = injector.getPluginStorage();
+ if (mFlags.isPluginManagerEnabled()) {
+ mPlugins = Collections.unmodifiableList(injector.loadPlugins(context, mPluginStorage));
+ Slog.d(TAG, "loaded Plugins:" + mPlugins);
+ } else {
+ mPlugins = List.of();
+ Slog.d(TAG, "PluginManager disabled");
+ }
+ }
+
+ /**
+ * Forwards boot completed event to Plugins
+ */
+ public void onBootCompleted() {
+ mPlugins.forEach(Plugin::onBootCompleted);
+ }
+
+ /**
+ * Adds change listener for particular plugin type
+ */
+ public <T> void subscribe(PluginType<T> type, PluginChangeListener<T> listener) {
+ mPluginStorage.addListener(type, listener);
+ }
+
+ /**
+ * Removes change listener
+ */
+ public <T> void unsubscribe(PluginType<T> type, PluginChangeListener<T> listener) {
+ mPluginStorage.removeListener(type, listener);
+ }
+
+ /**
+ * Print the object's state and debug information into the given stream.
+ */
+ public void dump(PrintWriter pw) {
+ pw.println("PluginManager:");
+ mPluginStorage.dump(pw);
+ for (Plugin plugin : mPlugins) {
+ plugin.dump(pw);
+ }
+ }
+
+ /**
+ * Listens for changes in PluginStorage for a particular type
+ * @param <T> plugin value type
+ */
+ public interface PluginChangeListener<T> {
+ /**
+ * Called when Plugin value changed
+ */
+ void onChanged(@Nullable T value);
+ }
+
+ static class Injector {
+ PluginStorage getPluginStorage() {
+ return new PluginStorage();
+ }
+
+ List<Plugin> loadPlugins(Context context, PluginStorage storage) {
+ String providerJarPath = context
+ .getString(com.android.internal.R.string.config_pluginsProviderJarPath);
+ Slog.d(TAG, "loading plugins from:" + providerJarPath);
+ if (TextUtils.isEmpty(providerJarPath)) {
+ return List.of();
+ }
+ try {
+ PathClassLoader pathClassLoader =
+ SystemServerClassLoaderFactory.getOrCreateClassLoader(
+ providerJarPath, getClass().getClassLoader(), false);
+ @SuppressWarnings("PrivateApi")
+ Class<? extends PluginsProvider> cp = pathClassLoader.loadClass(PROVIDER_IMPL_CLASS)
+ .asSubclass(PluginsProvider.class);
+ PluginsProvider provider = cp.getDeclaredConstructor().newInstance();
+ return provider.getPlugins(context, storage);
+ } catch (ClassNotFoundException e) {
+ Slog.e(TAG, "loading failed: " + PROVIDER_IMPL_CLASS + " is not found in"
+ + providerJarPath, e);
+ } catch (InvocationTargetException | InstantiationException | IllegalAccessException
+ | NoSuchMethodException e) {
+ Slog.e(TAG, "Class instantiation failed", e);
+ }
+ return List.of();
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/display/plugin/PluginStorage.java b/services/core/java/com/android/server/display/plugin/PluginStorage.java
new file mode 100644
index 000000000000..2bcea777e681
--- /dev/null
+++ b/services/core/java/com/android/server/display/plugin/PluginStorage.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2024 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.server.display.plugin;
+
+import android.annotation.Nullable;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.tools.r8.keepanno.annotations.KeepForApi;
+
+import java.io.PrintWriter;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Stores values pushed by Plugins and forwards them to corresponding listener.
+ */
+public class PluginStorage {
+ private static final String TAG = "PluginStorage";
+
+ private final Object mLock = new Object();
+ @GuardedBy("mLock")
+ private final Map<PluginType<?>, Object> mValues = new HashMap<>();
+ @GuardedBy("mLock")
+ private final Map<PluginType<?>, ListenersContainer<?>> mListeners = new HashMap<>();
+
+ /**
+ * Updates value in storage and forwards it to corresponding listeners.
+ * Should be called by OEM Plugin implementation in order to provide communicate with Framework
+ */
+ @KeepForApi
+ public <T> void updateValue(PluginType<T> type, @Nullable T value) {
+ Slog.d(TAG, "updateValue, type=" + type.mName + "; value=" + value);
+ Set<PluginManager.PluginChangeListener<T>> localListeners;
+ synchronized (mLock) {
+ mValues.put(type, value);
+ ListenersContainer<T> container = getListenersContainerForTypeLocked(type);
+ localListeners = new LinkedHashSet<>(container.mListeners);
+ }
+ Slog.d(TAG, "updateValue, notifying listeners=" + localListeners);
+ localListeners.forEach(l -> l.onChanged(value));
+ }
+
+ /**
+ * 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 value = null;
+ synchronized (mLock) {
+ ListenersContainer<T> container = getListenersContainerForTypeLocked(type);
+ if (container.mListeners.add(listener)) {
+ value = getValueForTypeLocked(type);
+ }
+ }
+ if (value != null) {
+ listener.onChanged(value);
+ }
+ }
+
+ /**
+ * Removes listener
+ */
+ <T> void removeListener(PluginType<T> type, PluginManager.PluginChangeListener<T> listener) {
+ synchronized (mLock) {
+ ListenersContainer<T> container = getListenersContainerForTypeLocked(type);
+ container.mListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Print the object's state and debug information into the given stream.
+ */
+ void dump(PrintWriter pw) {
+ Map<PluginType<?>, Object> localValues;
+ @SuppressWarnings("rawtypes")
+ Map<PluginType, Set> localListeners = new HashMap<>();
+ synchronized (mLock) {
+ localValues = new HashMap<>(mValues);
+ mListeners.forEach((type, container) -> localListeners.put(type, container.mListeners));
+ }
+ pw.println("PluginStorage:");
+ pw.println("values=" + localValues);
+ pw.println("listeners=" + localListeners);
+ }
+
+ @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) {
+ ListenersContainer<?> container = mListeners.get(type);
+ if (container == null) {
+ ListenersContainer<T> lc = new ListenersContainer<>();
+ mListeners.put(type, lc);
+ return lc;
+ } else {
+ return (ListenersContainer<T>) container;
+ }
+ }
+
+ private static final class ListenersContainer<T> {
+ private final Set<PluginManager.PluginChangeListener<T>> mListeners = new LinkedHashSet<>();
+ }
+}
diff --git a/services/core/java/com/android/server/display/plugin/PluginType.java b/services/core/java/com/android/server/display/plugin/PluginType.java
new file mode 100644
index 000000000000..fb60833d259e
--- /dev/null
+++ b/services/core/java/com/android/server/display/plugin/PluginType.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2024 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.server.display.plugin;
+
+import com.android.internal.annotations.Keep;
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Represent customisation entry point to Framework. OEM and Framework team should define
+ * new PluginTypes together, after that, Framework team can integrate listener and OEM team
+ * create Plugin implementation
+ *
+ * @param <T> type of plugin value
+ */
+@Keep
+public class PluginType<T> {
+
+ final Class<T> mType;
+ final String mName;
+
+ @VisibleForTesting
+ PluginType(Class<T> type, String name) {
+ mType = type;
+ mName = name;
+ }
+
+ @Override
+ public String toString() {
+ return "PluginType{"
+ + "mType=" + mType
+ + ", mName=" + mName
+ + '}';
+ }
+}
diff --git a/services/core/java/com/android/server/display/plugin/PluginsProvider.java b/services/core/java/com/android/server/display/plugin/PluginsProvider.java
new file mode 100644
index 000000000000..9ad85f67bc8b
--- /dev/null
+++ b/services/core/java/com/android/server/display/plugin/PluginsProvider.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 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.server.display.plugin;
+
+import android.annotation.NonNull;
+import android.content.Context;
+
+import com.android.tools.r8.keepanno.annotations.KeepForApi;
+
+import java.util.List;
+
+/**
+ * Interface that OEMs should implement in order to supply Plugins to PluginManager
+ */
+@KeepForApi
+public interface PluginsProvider {
+ /**
+ * Provides list of Plugins to PluginManager
+ */
+ @NonNull
+ List<Plugin> getPlugins(Context context, PluginStorage storage);
+}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/plugin/PluginManagerTest.kt b/services/tests/displayservicetests/src/com/android/server/display/plugin/PluginManagerTest.kt
new file mode 100644
index 000000000000..01061f16c279
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/plugin/PluginManagerTest.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2024 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.server.display.plugin
+
+import android.content.Context
+import androidx.test.filters.SmallTest
+import com.android.server.display.feature.DisplayManagerFlags
+import com.android.server.display.plugin.PluginManager.PluginChangeListener
+
+import org.junit.Test
+
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+private val TEST_PLUGIN_TYPE = PluginType(Int::class.java, "test_type")
+
+@SmallTest
+class PluginManagerTest {
+
+ private val mockContext = mock<Context>()
+ private val mockFlags = mock<DisplayManagerFlags>()
+ private val mockListener = mock<PluginChangeListener<Int>>()
+ private val testInjector = TestInjector()
+
+ @Test
+ fun testBootCompleted_enabledPluginManager() {
+ val pluginManager = createPluginManager()
+
+ pluginManager.onBootCompleted()
+
+ verify(testInjector.mockPlugin1).onBootCompleted()
+ verify(testInjector.mockPlugin2).onBootCompleted()
+ }
+
+ @Test
+ fun testBootCompleted_disabledPluginManager() {
+ val pluginManager = createPluginManager(false)
+
+ pluginManager.onBootCompleted()
+
+ verify(testInjector.mockPlugin1, never()).onBootCompleted()
+ verify(testInjector.mockPlugin2, never()).onBootCompleted()
+ }
+
+ @Test
+ fun testSubscribe() {
+ val pluginManager = createPluginManager()
+
+ pluginManager.subscribe(TEST_PLUGIN_TYPE, mockListener)
+
+ verify(testInjector.mockStorage).addListener(TEST_PLUGIN_TYPE, mockListener)
+ }
+
+ @Test
+ fun testUnsubscribe() {
+ val pluginManager = createPluginManager()
+
+ pluginManager.unsubscribe(TEST_PLUGIN_TYPE, mockListener)
+
+ verify(testInjector.mockStorage).removeListener(TEST_PLUGIN_TYPE, mockListener)
+ }
+
+ private fun createPluginManager(enabled: Boolean = true): PluginManager {
+ whenever(mockFlags.isPluginManagerEnabled).thenReturn(enabled)
+ return PluginManager(mockContext, mockFlags, testInjector)
+ }
+
+ private class TestInjector : PluginManager.Injector() {
+ val mockStorage = mock<PluginStorage>()
+ val mockPlugin1 = mock<Plugin>()
+ val mockPlugin2 = mock<Plugin>()
+
+ override fun getPluginStorage(): PluginStorage {
+ return mockStorage
+ }
+
+ override fun loadPlugins(context: Context?, storage: PluginStorage?): List<Plugin> {
+ return listOf(mockPlugin1, mockPlugin2)
+ }
+ }
+}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/plugin/PluginStorageTest.kt b/services/tests/displayservicetests/src/com/android/server/display/plugin/PluginStorageTest.kt
new file mode 100644
index 000000000000..218e34134e93
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/plugin/PluginStorageTest.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2024 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.server.display.plugin
+
+import androidx.test.filters.SmallTest
+import com.android.server.display.plugin.PluginManager.PluginChangeListener
+import com.google.common.truth.Truth.assertThat
+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")
+
+@SmallTest
+class PluginStorageTest {
+
+ val storage = PluginStorage()
+
+ @Test
+ fun testUpdateValue() {
+ val type1Value = "value1"
+ val testChangeListener = TestPluginChangeListener<String>()
+ storage.addListener(TEST_PLUGIN_TYPE1, testChangeListener)
+
+ storage.updateValue(TEST_PLUGIN_TYPE1, type1Value)
+
+ assertThat(testChangeListener.receivedValue).isEqualTo(type1Value)
+ }
+
+ @Test
+ fun testAddListener() {
+ val type1Value = "value1"
+ val testChangeListener = TestPluginChangeListener<String>()
+ storage.updateValue(TEST_PLUGIN_TYPE1, type1Value)
+
+ storage.addListener(TEST_PLUGIN_TYPE1, testChangeListener)
+
+ assertThat(testChangeListener.receivedValue).isEqualTo(type1Value)
+ }
+
+ @Test
+ fun testRemoveListener() {
+ val type1Value = "value1"
+ val testChangeListener = TestPluginChangeListener<String>()
+ storage.addListener(TEST_PLUGIN_TYPE1, testChangeListener)
+ storage.removeListener(TEST_PLUGIN_TYPE1, testChangeListener)
+
+ storage.updateValue(TEST_PLUGIN_TYPE1, type1Value)
+
+ assertThat(testChangeListener.receivedValue).isNull()
+ }
+
+ @Test
+ fun testAddListener_multipleValues() {
+ val type1Value = "value1"
+ val type2Value = "value2"
+ val testChangeListener = TestPluginChangeListener<String>()
+ storage.updateValue(TEST_PLUGIN_TYPE1, type1Value)
+ storage.updateValue(TEST_PLUGIN_TYPE2, type2Value)
+
+ storage.addListener(TEST_PLUGIN_TYPE1, testChangeListener)
+
+ assertThat(testChangeListener.receivedValue).isEqualTo(type1Value)
+ }
+
+ @Test
+ fun testUpdateValue_multipleListeners() {
+ val type1Value = "value1"
+ val testChangeListener1 = TestPluginChangeListener<String>()
+ val testChangeListener2 = TestPluginChangeListener<String>()
+ storage.addListener(TEST_PLUGIN_TYPE1, testChangeListener1)
+ storage.addListener(TEST_PLUGIN_TYPE2, testChangeListener2)
+
+ storage.updateValue(TEST_PLUGIN_TYPE1, type1Value)
+
+ assertThat(testChangeListener1.receivedValue).isEqualTo(type1Value)
+ assertThat(testChangeListener2.receivedValue).isNull()
+ }
+
+ private class TestPluginChangeListener<T> : PluginChangeListener<T> {
+ var receivedValue: T? = null
+
+ override fun onChanged(value: T?) {
+ receivedValue = value
+ }
+ }
+}