diff options
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 + } + } +} |