summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/app/NotificationManager.java170
-rw-r--r--core/java/android/app/admin/DevicePolicyManager.java18
-rw-r--r--core/java/com/android/internal/notification/SystemNotificationChannels.java8
-rw-r--r--core/res/res/values/strings.xml17
-rw-r--r--core/res/res/values/symbols.xml8
-rw-r--r--core/tests/coretests/src/android/app/NotificationManagerTest.java213
-rw-r--r--core/tests/coretests/src/com/android/internal/notification/SystemNotificationChannelsTest.java5
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt34
-rw-r--r--packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt30
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManagerTest.kt9
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt10
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinator.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NotifPipelineInitializer.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnAfterRenderListListener.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt49
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewRenderer.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManager.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/data/model/NotifStats.kt36
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java9
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java1
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinatorTest.kt9
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt27
-rw-r--r--proto/src/system_messages.proto4
-rw-r--r--services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java18
-rw-r--r--services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java356
-rw-r--r--services/core/java/com/android/server/notification/NotificationManagerService.java1
-rw-r--r--services/core/java/com/android/server/notification/PreferencesHelper.java93
-rw-r--r--services/core/java/com/android/server/vibrator/BasicToPwleSegmentAdapter.java5
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java187
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java253
-rw-r--r--services/tests/vibrator/src/com/android/server/vibrator/BasicToPwleSegmentAdapterTest.java158
39 files changed, 1589 insertions, 205 deletions
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index 08bd854525ec..aede8aa70ede 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -17,6 +17,7 @@
package android.app;
import static android.Manifest.permission.POST_NOTIFICATIONS;
+import static android.app.NotificationChannel.DEFAULT_CHANNEL_ID;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.service.notification.Flags.notificationClassification;
@@ -50,6 +51,7 @@ import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
+import android.os.IpcDataCache;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
@@ -71,6 +73,8 @@ import android.util.LruCache;
import android.util.Slog;
import android.util.proto.ProtoOutputStream;
+import com.android.internal.annotations.VisibleForTesting;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.time.InstantSource;
@@ -1202,12 +1206,20 @@ public class NotificationManager {
* package (see {@link Context#createPackageContext(String, int)}).</p>
*/
public NotificationChannel getNotificationChannel(String channelId) {
- INotificationManager service = service();
- try {
- return service.getNotificationChannel(mContext.getOpPackageName(),
- mContext.getUserId(), mContext.getPackageName(), channelId);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ if (Flags.nmBinderPerfCacheChannels()) {
+ return getChannelFromList(channelId,
+ mNotificationChannelListCache.query(new NotificationChannelQuery(
+ mContext.getOpPackageName(),
+ mContext.getPackageName(),
+ mContext.getUserId())));
+ } else {
+ INotificationManager service = service();
+ try {
+ return service.getNotificationChannel(mContext.getOpPackageName(),
+ mContext.getUserId(), mContext.getPackageName(), channelId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
}
@@ -1222,13 +1234,21 @@ public class NotificationManager {
*/
public @Nullable NotificationChannel getNotificationChannel(@NonNull String channelId,
@NonNull String conversationId) {
- INotificationManager service = service();
- try {
- return service.getConversationNotificationChannel(mContext.getOpPackageName(),
- mContext.getUserId(), mContext.getPackageName(), channelId, true,
- conversationId);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ if (Flags.nmBinderPerfCacheChannels()) {
+ return getConversationChannelFromList(channelId, conversationId,
+ mNotificationChannelListCache.query(new NotificationChannelQuery(
+ mContext.getOpPackageName(),
+ mContext.getPackageName(),
+ mContext.getUserId())));
+ } else {
+ INotificationManager service = service();
+ try {
+ return service.getConversationNotificationChannel(mContext.getOpPackageName(),
+ mContext.getUserId(), mContext.getPackageName(), channelId, true,
+ conversationId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
}
@@ -1241,15 +1261,62 @@ public class NotificationManager {
* {@link Context#createPackageContext(String, int)}).</p>
*/
public List<NotificationChannel> getNotificationChannels() {
- INotificationManager service = service();
- try {
- return service.getNotificationChannels(mContext.getOpPackageName(),
- mContext.getPackageName(), mContext.getUserId()).getList();
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ if (Flags.nmBinderPerfCacheChannels()) {
+ return mNotificationChannelListCache.query(new NotificationChannelQuery(
+ mContext.getOpPackageName(),
+ mContext.getPackageName(),
+ mContext.getUserId()));
+ } else {
+ INotificationManager service = service();
+ try {
+ return service.getNotificationChannels(mContext.getOpPackageName(),
+ mContext.getPackageName(), mContext.getUserId()).getList();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
}
+ // channel list assumed to be associated with the appropriate package & user id already.
+ private static NotificationChannel getChannelFromList(String channelId,
+ List<NotificationChannel> channels) {
+ if (channels == null) {
+ return null;
+ }
+ if (channelId == null) {
+ channelId = DEFAULT_CHANNEL_ID;
+ }
+ for (NotificationChannel channel : channels) {
+ if (channelId.equals(channel.getId())) {
+ return channel;
+ }
+ }
+ return null;
+ }
+
+ private static NotificationChannel getConversationChannelFromList(String channelId,
+ String conversationId, List<NotificationChannel> channels) {
+ if (channels == null) {
+ return null;
+ }
+ if (channelId == null) {
+ channelId = DEFAULT_CHANNEL_ID;
+ }
+ if (conversationId == null) {
+ return getChannelFromList(channelId, channels);
+ }
+ NotificationChannel parent = null;
+ for (NotificationChannel channel : channels) {
+ if (conversationId.equals(channel.getConversationId())
+ && channelId.equals(channel.getParentChannelId())) {
+ return channel;
+ } else if (channelId.equals(channel.getId())) {
+ parent = channel;
+ }
+ }
+ return parent;
+ }
+
/**
* Deletes the given notification channel.
*
@@ -1328,6 +1395,71 @@ public class NotificationManager {
}
}
+ private static final String NOTIFICATION_CHANNEL_CACHE_API = "getNotificationChannel";
+ private static final String NOTIFICATION_CHANNEL_LIST_CACHE_NAME = "getNotificationChannels";
+ private static final int NOTIFICATION_CHANNEL_CACHE_SIZE = 10;
+
+ private final IpcDataCache.QueryHandler<NotificationChannelQuery, List<NotificationChannel>>
+ mNotificationChannelListQueryHandler = new IpcDataCache.QueryHandler<>() {
+ @Override
+ public List<NotificationChannel> apply(NotificationChannelQuery query) {
+ INotificationManager service = service();
+ try {
+ return service.getNotificationChannels(query.callingPkg,
+ query.targetPkg, query.userId).getList();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public boolean shouldBypassCache(@NonNull NotificationChannelQuery query) {
+ // Other locations should also not be querying the cache in the first place if
+ // the flag is not enabled, but this is an extra precaution.
+ if (!Flags.nmBinderPerfCacheChannels()) {
+ Log.wtf(TAG,
+ "shouldBypassCache called when nm_binder_perf_cache_channels off");
+ return true;
+ }
+ return false;
+ }
+ };
+
+ private final IpcDataCache<NotificationChannelQuery, List<NotificationChannel>>
+ mNotificationChannelListCache =
+ new IpcDataCache<>(NOTIFICATION_CHANNEL_CACHE_SIZE, IpcDataCache.MODULE_SYSTEM,
+ NOTIFICATION_CHANNEL_CACHE_API, NOTIFICATION_CHANNEL_LIST_CACHE_NAME,
+ mNotificationChannelListQueryHandler);
+
+ private record NotificationChannelQuery(
+ String callingPkg,
+ String targetPkg,
+ int userId) {}
+
+ /**
+ * @hide
+ */
+ public static void invalidateNotificationChannelCache() {
+ if (Flags.nmBinderPerfCacheChannels()) {
+ IpcDataCache.invalidateCache(IpcDataCache.MODULE_SYSTEM,
+ NOTIFICATION_CHANNEL_CACHE_API);
+ } else {
+ // if we are here, we have failed to flag something
+ Log.wtf(TAG, "invalidateNotificationChannelCache called without flag");
+ }
+ }
+
+ /**
+ * For testing only: running tests with a cache requires marking the cache's property for
+ * testing, as test APIs otherwise cannot invalidate the cache. This must be called after
+ * calling PropertyInvalidatedCache.setTestMode(true).
+ * @hide
+ */
+ @VisibleForTesting
+ public void setChannelCacheToTestMode() {
+ mNotificationChannelListCache.testPropertyName();
+ }
+
/**
* @hide
*/
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index a2fddb045179..c50452157d74 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -4354,20 +4354,24 @@ public class DevicePolicyManager {
}
/**
- * Indicates that app functions are not controlled by policy.
+ * Indicates that {@link android.app.appfunctions.AppFunctionManager} is not controlled by
+ * policy.
*
* <p>If no admin set this policy, it means appfunctions are enabled.
*/
@FlaggedApi(android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER)
public static final int APP_FUNCTIONS_NOT_CONTROLLED_BY_POLICY = 0;
- /** Indicates that app functions are controlled and disabled by a policy. */
+ /** Indicates that {@link android.app.appfunctions.AppFunctionManager} is controlled and
+ * disabled by policy, i.e. no apps in the current user are allowed to expose app functions.
+ */
@FlaggedApi(android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER)
public static final int APP_FUNCTIONS_DISABLED = 1;
/**
- * Indicates that app functions are controlled and disabled by a policy for cross profile
- * interactions only.
+ * Indicates that {@link android.app.appfunctions.AppFunctionManager} is controlled and
+ * disabled by a policy for cross profile interactions only, i.e. app functions exposed by apps
+ * in the current user can only be invoked within the same user.
*
* <p>This is different from {@link #APP_FUNCTIONS_DISABLED} in that it only disables cross
* profile interactions (even if the caller has permissions required to interact across users).
@@ -4388,7 +4392,9 @@ public class DevicePolicyManager {
public @interface AppFunctionsPolicy {}
/**
- * Sets the app functions policy which controls app functions operations on the device.
+ * Sets the {@link android.app.appfunctions.AppFunctionManager} policy which controls app
+ * functions operations on the device. An app function is a piece of functionality that apps
+ * expose to the system for cross-app orchestration.
*
* <p>This function can only be called by a device owner, a profile owner or holders of the
* permission {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_APP_FUNCTIONS}.
@@ -4414,7 +4420,7 @@ public class DevicePolicyManager {
}
/**
- * Returns the current app functions policy.
+ * Returns the current {@link android.app.appfunctions.AppFunctionManager} policy.
*
* <p>The returned policy will be the current resolved policy rather than the policy set by the
* calling admin.
diff --git a/core/java/com/android/internal/notification/SystemNotificationChannels.java b/core/java/com/android/internal/notification/SystemNotificationChannels.java
index 7a025f16b63a..972c2ea403e0 100644
--- a/core/java/com/android/internal/notification/SystemNotificationChannels.java
+++ b/core/java/com/android/internal/notification/SystemNotificationChannels.java
@@ -68,6 +68,7 @@ public class SystemNotificationChannels {
@Deprecated public static final String SYSTEM_CHANGES_DEPRECATED = "SYSTEM_CHANGES";
public static final String SYSTEM_CHANGES = "SYSTEM_CHANGES_ALERTS";
public static final String ACCESSIBILITY_MAGNIFICATION = "ACCESSIBILITY_MAGNIFICATION";
+ public static final String ACCESSIBILITY_HEARING_DEVICE = "ACCESSIBILITY_HEARING_DEVICE";
public static final String ACCESSIBILITY_SECURITY_POLICY = "ACCESSIBILITY_SECURITY_POLICY";
public static final String ABUSIVE_BACKGROUND_APPS = "ABUSIVE_BACKGROUND_APPS";
@@ -210,6 +211,13 @@ public class SystemNotificationChannels {
newFeaturePrompt.setBlockable(true);
channelsList.add(newFeaturePrompt);
+ final NotificationChannel accessibilityHearingDeviceChannel = new NotificationChannel(
+ ACCESSIBILITY_HEARING_DEVICE,
+ context.getString(R.string.notification_channel_accessibility_hearing_device),
+ NotificationManager.IMPORTANCE_HIGH);
+ accessibilityHearingDeviceChannel.setBlockable(true);
+ channelsList.add(accessibilityHearingDeviceChannel);
+
final NotificationChannel accessibilitySecurityPolicyChannel = new NotificationChannel(
ACCESSIBILITY_SECURITY_POLICY,
context.getString(R.string.notification_channel_accessibility_security_policy),
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index f6dd3f2ed029..debc5e9a0dce 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -884,6 +884,10 @@
<string name="notification_channel_accessibility_magnification">Magnification</string>
<!-- Text shown when viewing channel settings for notifications related to accessibility
+ hearing device. [CHAR_LIMIT=NONE]-->
+ <string name="notification_channel_accessibility_hearing_device">Hearing device</string>
+
+ <!-- Text shown when viewing channel settings for notifications related to accessibility
security policy. [CHAR_LIMIT=NONE]-->
<string name="notification_channel_accessibility_security_policy">Accessibility usage</string>
@@ -4994,6 +4998,19 @@
<!-- Text used to describe system navigation features, shown within a UI allowing a user to assign system magnification features to the Accessibility button in the navigation bar. -->
<string name="accessibility_magnification_chooser_text">Magnification</string>
+ <!-- Notification title for switching input to the phone's microphone. It will be shown when making a phone call with the hearing device. [CHAR LIMIT=none] -->
+ <string name="hearing_device_switch_phone_mic_notification_title">Switch to phone mic?</string>
+ <!-- Notification title for switching input to the hearing device's microphone. It will be shown when making a phone call with the hearing device. [CHAR LIMIT=none] -->
+ <string name="hearing_device_switch_hearing_mic_notification_title">Switch to hearing aid mic?</string>
+ <!-- Notification content for switching input to the phone's microphone. It will be shown when making a phone call with the hearing device. [CHAR LIMIT=none] -->
+ <string name="hearing_device_switch_phone_mic_notification_text">For better sound or if your hearing aid battery is low. This only switches your mic during the call.</string>
+ <!-- Notification content for switching input to the hearing device's microphone. It will be shown when making a phone call with the hearing device. [CHAR LIMIT=none] -->
+ <string name="hearing_device_switch_hearing_mic_notification_text">You can use your hearing aid microphone for hands-free calling. This only switches your mic during the call.</string>
+ <!-- Notification action button. Click it will switch the input between phone's microphone and hearing device's microphone. It will be shown when making a phone call with the hearing device. [CHAR LIMIT=none] -->
+ <string name="hearing_device_notification_switch_button">Switch</string>
+ <!-- Notification action button. Click it will open the bluetooth device details page for this hearing device. It will be shown when making a phone call with the hearing device. [CHAR LIMIT=none] -->
+ <string name="hearing_device_notification_settings_button">Settings</string>
+
<!-- Text spoken when the current user is switched if accessibility is enabled. [CHAR LIMIT=none] -->
<string name="user_switched">Current user <xliff:g id="name" example="Bob">%1$s</xliff:g>.</string>
<!-- Message shown when switching to a user [CHAR LIMIT=none] -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index c5b764c6578d..f89ca44cce30 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3843,6 +3843,13 @@
<java-symbol type="string" name="reduce_bright_colors_feature_name" />
<java-symbol type="string" name="one_handed_mode_feature_name" />
+ <java-symbol type="string" name="hearing_device_switch_phone_mic_notification_title" />
+ <java-symbol type="string" name="hearing_device_switch_hearing_mic_notification_title" />
+ <java-symbol type="string" name="hearing_device_switch_phone_mic_notification_text" />
+ <java-symbol type="string" name="hearing_device_switch_hearing_mic_notification_text" />
+ <java-symbol type="string" name="hearing_device_notification_switch_button" />
+ <java-symbol type="string" name="hearing_device_notification_settings_button" />
+
<!-- com.android.internal.widget.RecyclerView -->
<java-symbol type="id" name="item_touch_helper_previous_elevation"/>
<java-symbol type="dimen" name="item_touch_helper_max_drag_scroll_per_frame"/>
@@ -4030,6 +4037,7 @@
<java-symbol type="string" name="notification_channel_heavy_weight_app" />
<java-symbol type="string" name="notification_channel_system_changes" />
<java-symbol type="string" name="notification_channel_accessibility_magnification" />
+ <java-symbol type="string" name="notification_channel_accessibility_hearing_device" />
<java-symbol type="string" name="notification_channel_accessibility_security_policy" />
<java-symbol type="string" name="notification_channel_display" />
<java-symbol type="string" name="time_zone_change_notification_title" />
diff --git a/core/tests/coretests/src/android/app/NotificationManagerTest.java b/core/tests/coretests/src/android/app/NotificationManagerTest.java
index 6538ce85457c..3d6e1225bd92 100644
--- a/core/tests/coretests/src/android/app/NotificationManagerTest.java
+++ b/core/tests/coretests/src/android/app/NotificationManagerTest.java
@@ -16,6 +16,8 @@
package android.app;
+import static com.google.common.truth.Truth.assertThat;
+
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
@@ -25,8 +27,12 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.pm.ParceledListSlice;
+import android.os.UserHandle;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import android.platform.test.flag.junit.SetFlagsRule;
@@ -35,6 +41,7 @@ import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -42,6 +49,7 @@ import org.junit.runner.RunWith;
import java.time.Instant;
import java.time.InstantSource;
+import java.util.List;
@RunWith(AndroidJUnit4.class)
@SmallTest
@@ -50,14 +58,24 @@ public class NotificationManagerTest {
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
- private Context mContext;
private NotificationManagerWithMockService mNotificationManager;
private final FakeClock mClock = new FakeClock();
+ private PackageTestableContext mContext;
+
@Before
public void setUp() {
- mContext = ApplicationProvider.getApplicationContext();
+ mContext = new PackageTestableContext(ApplicationProvider.getApplicationContext());
mNotificationManager = new NotificationManagerWithMockService(mContext, mClock);
+
+ // Caches must be in test mode in order to be used in tests.
+ PropertyInvalidatedCache.setTestMode(true);
+ mNotificationManager.setChannelCacheToTestMode();
+ }
+
+ @After
+ public void tearDown() {
+ PropertyInvalidatedCache.setTestMode(false);
}
@Test
@@ -243,12 +261,161 @@ public class NotificationManagerTest {
anyInt(), any(), anyInt());
}
+ @Test
+ @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+ public void getNotificationChannel_cachedUntilInvalidated() throws Exception {
+ // Invalidate the cache first because the cache won't do anything until then
+ NotificationManager.invalidateNotificationChannelCache();
+
+ // It doesn't matter what the returned contents are, as long as we return a channel.
+ // This setup must set up getNotificationChannels(), as that's the method called.
+ when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(),
+ anyInt())).thenReturn(new ParceledListSlice<>(List.of(exampleChannel())));
+
+ // ask for the same channel 100 times without invalidating the cache
+ for (int i = 0; i < 100; i++) {
+ NotificationChannel unused = mNotificationManager.getNotificationChannel("id");
+ }
+
+ // invalidate the cache; then ask again
+ NotificationManager.invalidateNotificationChannelCache();
+ NotificationChannel unused = mNotificationManager.getNotificationChannel("id");
+
+ verify(mNotificationManager.mBackendService, times(2))
+ .getNotificationChannels(any(), any(), anyInt());
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+ public void getNotificationChannel_sameApp_oneCall() throws Exception {
+ NotificationManager.invalidateNotificationChannelCache();
+
+ NotificationChannel c1 = new NotificationChannel("id1", "name1",
+ NotificationManager.IMPORTANCE_DEFAULT);
+ NotificationChannel c2 = new NotificationChannel("id2", "name2",
+ NotificationManager.IMPORTANCE_NONE);
+
+ when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(),
+ anyInt())).thenReturn(new ParceledListSlice<>(List.of(c1, c2)));
+
+ assertThat(mNotificationManager.getNotificationChannel("id1")).isEqualTo(c1);
+ assertThat(mNotificationManager.getNotificationChannel("id2")).isEqualTo(c2);
+ assertThat(mNotificationManager.getNotificationChannel("id3")).isNull();
+
+ verify(mNotificationManager.mBackendService, times(1))
+ .getNotificationChannels(any(), any(), anyInt());
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+ public void getNotificationChannels_cachedUntilInvalidated() throws Exception {
+ NotificationManager.invalidateNotificationChannelCache();
+ when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(),
+ anyInt())).thenReturn(new ParceledListSlice<>(List.of(exampleChannel())));
+
+ // ask for channels 100 times without invalidating the cache
+ for (int i = 0; i < 100; i++) {
+ List<NotificationChannel> unused = mNotificationManager.getNotificationChannels();
+ }
+
+ // invalidate the cache; then ask again
+ NotificationManager.invalidateNotificationChannelCache();
+ List<NotificationChannel> res = mNotificationManager.getNotificationChannels();
+
+ verify(mNotificationManager.mBackendService, times(2))
+ .getNotificationChannels(any(), any(), anyInt());
+ assertThat(res).containsExactlyElementsIn(List.of(exampleChannel()));
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+ public void getNotificationChannel_channelAndConversationLookup() throws Exception {
+ NotificationManager.invalidateNotificationChannelCache();
+
+ // Full list of channels: c1; conv1 = child of c1; c2 is unrelated
+ NotificationChannel c1 = new NotificationChannel("id", "name",
+ NotificationManager.IMPORTANCE_DEFAULT);
+ NotificationChannel conv1 = new NotificationChannel("", "name_conversation",
+ NotificationManager.IMPORTANCE_DEFAULT);
+ conv1.setConversationId("id", "id_conversation");
+ NotificationChannel c2 = new NotificationChannel("other", "name2",
+ NotificationManager.IMPORTANCE_DEFAULT);
+
+ when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(), anyInt()))
+ .thenReturn(new ParceledListSlice<>(List.of(c1, conv1, c2)));
+
+ // Lookup for channel c1 and c2: returned as expected
+ assertThat(mNotificationManager.getNotificationChannel("id")).isEqualTo(c1);
+ assertThat(mNotificationManager.getNotificationChannel("other")).isEqualTo(c2);
+
+ // Lookup for conv1 should return conv1
+ assertThat(mNotificationManager.getNotificationChannel("id", "id_conversation")).isEqualTo(
+ conv1);
+
+ // Lookup for a different conversation channel that doesn't exist, whose parent channel id
+ // is "id", should return c1
+ assertThat(mNotificationManager.getNotificationChannel("id", "nonexistent")).isEqualTo(c1);
+
+ // Lookup of a nonexistent channel is null
+ assertThat(mNotificationManager.getNotificationChannel("id3")).isNull();
+
+ // All of that should have been one call to getNotificationChannels()
+ verify(mNotificationManager.mBackendService, times(1))
+ .getNotificationChannels(any(), any(), anyInt());
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+ public void getNotificationChannel_differentPackages() throws Exception {
+ NotificationManager.invalidateNotificationChannelCache();
+ final String pkg1 = "one";
+ final String pkg2 = "two";
+ final int userId = 0;
+ final int userId1 = 1;
+
+ // multiple channels with the same ID, but belonging to different packages/users
+ NotificationChannel channel1 = new NotificationChannel("id", "name1",
+ NotificationManager.IMPORTANCE_DEFAULT);
+ NotificationChannel channel2 = channel1.copy();
+ channel2.setName("name2");
+ NotificationChannel channel3 = channel1.copy();
+ channel3.setName("name3");
+
+ when(mNotificationManager.mBackendService.getNotificationChannels(any(), eq(pkg1),
+ eq(userId))).thenReturn(new ParceledListSlice<>(List.of(channel1)));
+ when(mNotificationManager.mBackendService.getNotificationChannels(any(), eq(pkg2),
+ eq(userId))).thenReturn(new ParceledListSlice<>(List.of(channel2)));
+ when(mNotificationManager.mBackendService.getNotificationChannels(any(), eq(pkg1),
+ eq(userId1))).thenReturn(new ParceledListSlice<>(List.of(channel3)));
+
+ // set our context to pretend to be from package 1 and userId 0
+ mContext.setParameters(pkg1, pkg1, userId);
+ assertThat(mNotificationManager.getNotificationChannel("id")).isEqualTo(channel1);
+
+ // now package 2
+ mContext.setParameters(pkg2, pkg2, userId);
+ assertThat(mNotificationManager.getNotificationChannel("id")).isEqualTo(channel2);
+
+ // now pkg1 for a different user
+ mContext.setParameters(pkg1, pkg1, userId1);
+ assertThat(mNotificationManager.getNotificationChannel("id")).isEqualTo(channel3);
+
+ // Those should have been three different calls
+ verify(mNotificationManager.mBackendService, times(3))
+ .getNotificationChannels(any(), any(), anyInt());
+ }
+
private Notification exampleNotification() {
return new Notification.Builder(mContext, "channel")
.setSmallIcon(android.R.drawable.star_big_on)
.build();
}
+ private NotificationChannel exampleChannel() {
+ return new NotificationChannel("id", "channel_name",
+ NotificationManager.IMPORTANCE_DEFAULT);
+ }
+
private static class NotificationManagerWithMockService extends NotificationManager {
private final INotificationManager mBackendService;
@@ -264,6 +431,48 @@ public class NotificationManagerTest {
}
}
+ // Helper context wrapper class where we can control just the return values of getPackageName,
+ // getOpPackageName, and getUserId (used in getNotificationChannels).
+ private static class PackageTestableContext extends ContextWrapper {
+ private String mPackage;
+ private String mOpPackage;
+ private Integer mUserId;
+
+ PackageTestableContext(Context base) {
+ super(base);
+ }
+
+ void setParameters(String packageName, String opPackageName, int userId) {
+ mPackage = packageName;
+ mOpPackage = opPackageName;
+ mUserId = userId;
+ }
+
+ @Override
+ public String getPackageName() {
+ if (mPackage != null) return mPackage;
+ return super.getPackageName();
+ }
+
+ @Override
+ public String getOpPackageName() {
+ if (mOpPackage != null) return mOpPackage;
+ return super.getOpPackageName();
+ }
+
+ @Override
+ public int getUserId() {
+ if (mUserId != null) return mUserId;
+ return super.getUserId();
+ }
+
+ @Override
+ public UserHandle getUser() {
+ if (mUserId != null) return UserHandle.of(mUserId);
+ return super.getUser();
+ }
+ }
+
private static class FakeClock implements InstantSource {
private long mNowMillis = 441644400000L;
diff --git a/core/tests/coretests/src/com/android/internal/notification/SystemNotificationChannelsTest.java b/core/tests/coretests/src/com/android/internal/notification/SystemNotificationChannelsTest.java
index 0bf406c970f2..2bd3f4df9435 100644
--- a/core/tests/coretests/src/com/android/internal/notification/SystemNotificationChannelsTest.java
+++ b/core/tests/coretests/src/com/android/internal/notification/SystemNotificationChannelsTest.java
@@ -17,6 +17,7 @@
package com.android.internal.notification;
import static com.android.internal.notification.SystemNotificationChannels.ABUSIVE_BACKGROUND_APPS;
+import static com.android.internal.notification.SystemNotificationChannels.ACCESSIBILITY_HEARING_DEVICE;
import static com.android.internal.notification.SystemNotificationChannels.ACCESSIBILITY_MAGNIFICATION;
import static com.android.internal.notification.SystemNotificationChannels.ACCESSIBILITY_SECURITY_POLICY;
import static com.android.internal.notification.SystemNotificationChannels.ACCOUNT;
@@ -90,8 +91,8 @@ public class SystemNotificationChannelsTest {
DEVELOPER_IMPORTANT, UPDATES, NETWORK_STATUS, NETWORK_ALERTS,
NETWORK_AVAILABLE, VPN, DEVICE_ADMIN, ALERTS, RETAIL_MODE, USB,
FOREGROUND_SERVICE, HEAVY_WEIGHT_APP, SYSTEM_CHANGES,
- ACCESSIBILITY_MAGNIFICATION, ACCESSIBILITY_SECURITY_POLICY,
- ABUSIVE_BACKGROUND_APPS);
+ ACCESSIBILITY_MAGNIFICATION, ACCESSIBILITY_HEARING_DEVICE,
+ ACCESSIBILITY_SECURITY_POLICY, ABUSIVE_BACKGROUND_APPS);
}
@Test
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt b/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt
index 5f1f588bb2b5..c7d6e8aed3b4 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt
@@ -34,6 +34,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScrollModifierNode
import androidx.compose.ui.input.pointer.AwaitPointerEventScope
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.PointerId
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.PointerInputScope
@@ -168,6 +169,12 @@ private class NestedDraggableNode(
CompositionLocalConsumerModifierNode,
OrientationAware {
private val nestedScrollDispatcher = NestedScrollDispatcher()
+ private var trackWheelScroll: SuspendingPointerInputModifierNode? = null
+ set(value) {
+ field?.let { undelegate(it) }
+ field = value?.also { delegate(it) }
+ }
+
private var trackDownPositionDelegate: SuspendingPointerInputModifierNode? = null
set(value) {
field?.let { undelegate(it) }
@@ -189,6 +196,7 @@ private class NestedDraggableNode(
* This is use to track the started position of a drag started on a nested scrollable.
*/
private var lastFirstDown: Offset? = null
+ private var lastEventWasScrollWheel: Boolean = false
/** The pointers currently down, in order of which they were done and mapping to their type. */
private val pointersDown = linkedMapOf<PointerId, PointerType>()
@@ -218,8 +226,11 @@ private class NestedDraggableNode(
nestedScrollController?.ensureOnDragStoppedIsCalled()
nestedScrollController = null
- if (!enabled && trackDownPositionDelegate != null) {
+ if (!enabled && trackWheelScroll != null) {
+ check(trackDownPositionDelegate != null)
check(detectDragsDelegate != null)
+
+ trackWheelScroll = null
trackDownPositionDelegate = null
detectDragsDelegate = null
}
@@ -232,17 +243,22 @@ private class NestedDraggableNode(
) {
if (!enabled) return
- if (trackDownPositionDelegate == null) {
+ if (trackWheelScroll == null) {
+ check(trackDownPositionDelegate == null)
check(detectDragsDelegate == null)
+
+ trackWheelScroll = SuspendingPointerInputModifierNode { trackWheelScroll() }
trackDownPositionDelegate = SuspendingPointerInputModifierNode { trackDownPosition() }
detectDragsDelegate = SuspendingPointerInputModifierNode { detectDrags() }
}
+ checkNotNull(trackWheelScroll).onPointerEvent(pointerEvent, pass, bounds)
checkNotNull(trackDownPositionDelegate).onPointerEvent(pointerEvent, pass, bounds)
checkNotNull(detectDragsDelegate).onPointerEvent(pointerEvent, pass, bounds)
}
override fun onCancelPointerInput() {
+ trackWheelScroll?.onCancelPointerInput()
trackDownPositionDelegate?.onCancelPointerInput()
detectDragsDelegate?.onCancelPointerInput()
}
@@ -457,6 +473,13 @@ private class NestedDraggableNode(
* ===============================
*/
+ private suspend fun PointerInputScope.trackWheelScroll() {
+ awaitEachGesture {
+ val event = awaitPointerEvent(pass = PointerEventPass.Initial)
+ lastEventWasScrollWheel = event.type == PointerEventType.Scroll
+ }
+ }
+
private suspend fun PointerInputScope.trackDownPosition() {
awaitEachGesture {
try {
@@ -501,7 +524,12 @@ private class NestedDraggableNode(
}
val sign = offset.sign
- if (nestedScrollController == null && draggable.shouldConsumeNestedScroll(sign)) {
+ if (
+ nestedScrollController == null &&
+ // TODO(b/388231324): Remove this.
+ !lastEventWasScrollWheel &&
+ draggable.shouldConsumeNestedScroll(sign)
+ ) {
val startedPosition = checkNotNull(lastFirstDown) { "lastFirstDown is not set" }
// TODO(b/382665591): Ensure that there is at least one pointer down.
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt
index 7f70e97411f4..f9cf495d9d9f 100644
--- a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt
@@ -18,6 +18,8 @@ package com.android.compose.gesture
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.rememberScrollableState
+import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
@@ -35,6 +37,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.PointerType
import androidx.compose.ui.platform.LocalViewConfiguration
+import androidx.compose.ui.test.ScrollWheel
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onRoot
@@ -710,6 +713,33 @@ class NestedDraggableTest(override val orientation: Orientation) : OrientationAw
rule.onRoot().performTouchInput { down(center) }
}
+ @Test
+ // TODO(b/388231324): Remove this.
+ fun nestedScrollWithMouseWheelIsIgnored() {
+ val draggable = TestDraggable()
+ val touchSlop =
+ rule.setContentWithTouchSlop {
+ Box(
+ Modifier.fillMaxSize()
+ .nestedDraggable(draggable, orientation)
+ .scrollable(rememberScrollableState { 0f }, orientation)
+ )
+ }
+
+ rule.onRoot().performMouseInput {
+ enter(center)
+ scroll(
+ touchSlop + 1f,
+ when (orientation) {
+ Orientation.Horizontal -> ScrollWheel.Horizontal
+ Orientation.Vertical -> ScrollWheel.Vertical
+ },
+ )
+ }
+
+ assertThat(draggable.onDragStartedCalled).isFalse()
+ }
+
private fun ComposeContentTestRule.setContentWithTouchSlop(
content: @Composable () -> Unit
): Float {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManagerTest.kt
index 912633c874ed..e6fbc725af04 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManagerTest.kt
@@ -101,7 +101,6 @@ class RenderStageManagerTest : SysuiTestCase() {
// VERIFY that the renderer is not queried for group or row controllers
inOrder(spyViewRenderer).apply {
verify(spyViewRenderer, times(1)).onRenderList(any())
- verify(spyViewRenderer, times(1)).getStackController()
verify(spyViewRenderer, never()).getGroupController(any())
verify(spyViewRenderer, never()).getRowController(any())
verify(spyViewRenderer, times(1)).onDispatchComplete()
@@ -121,7 +120,6 @@ class RenderStageManagerTest : SysuiTestCase() {
// VERIFY that the renderer is queried once per group/entry
inOrder(spyViewRenderer).apply {
verify(spyViewRenderer, times(1)).onRenderList(any())
- verify(spyViewRenderer, times(1)).getStackController()
verify(spyViewRenderer, times(2)).getGroupController(any())
verify(spyViewRenderer, times(8)).getRowController(any())
verify(spyViewRenderer, times(1)).onDispatchComplete()
@@ -144,7 +142,6 @@ class RenderStageManagerTest : SysuiTestCase() {
// VERIFY that the renderer is queried once per group/entry
inOrder(spyViewRenderer).apply {
verify(spyViewRenderer, times(1)).onRenderList(any())
- verify(spyViewRenderer, times(1)).getStackController()
verify(spyViewRenderer, times(2)).getGroupController(any())
verify(spyViewRenderer, times(8)).getRowController(any())
verify(spyViewRenderer, times(1)).onDispatchComplete()
@@ -162,7 +159,7 @@ class RenderStageManagerTest : SysuiTestCase() {
onRenderListListener.onRenderList(listWith2Groups8Entries())
// VERIFY that the listeners are invoked once per group and once per entry
- verify(onAfterRenderListListener, times(1)).onAfterRenderList(any(), any())
+ verify(onAfterRenderListListener, times(1)).onAfterRenderList(any())
verify(onAfterRenderGroupListener, times(2)).onAfterRenderGroup(any(), any())
verify(onAfterRenderEntryListener, times(8)).onAfterRenderEntry(any(), any())
verifyNoMoreInteractions(
@@ -182,7 +179,7 @@ class RenderStageManagerTest : SysuiTestCase() {
onRenderListListener.onRenderList(listOf())
// VERIFY that the stack listener is invoked once but other listeners are not
- verify(onAfterRenderListListener, times(1)).onAfterRenderList(any(), any())
+ verify(onAfterRenderListListener, times(1)).onAfterRenderList(any())
verify(onAfterRenderGroupListener, never()).onAfterRenderGroup(any(), any())
verify(onAfterRenderEntryListener, never()).onAfterRenderEntry(any(), any())
verifyNoMoreInteractions(
@@ -203,8 +200,6 @@ class RenderStageManagerTest : SysuiTestCase() {
private class FakeNotifViewRenderer : NotifViewRenderer {
override fun onRenderList(notifList: List<ListEntry>) {}
- override fun getStackController(): NotifStackController = mock()
-
override fun getGroupController(group: GroupEntry): NotifGroupController = mock()
override fun getRowController(entry: NotificationEntry): NotifRowController = mock()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
index 54ce88b40c11..83c61507a506 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
@@ -26,7 +26,7 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
-import com.android.systemui.statusbar.notification.collection.render.NotifStats
+import com.android.systemui.statusbar.notification.data.model.NotifStats
import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
@@ -275,7 +275,6 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() {
activeNotificationListRepository.notifStats.value =
NotifStats(
- numActiveNotifs = 2,
hasNonClearableAlertingNotifs = false,
hasClearableAlertingNotifs = true,
hasNonClearableSilentNotifs = false,
@@ -293,7 +292,6 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() {
activeNotificationListRepository.notifStats.value =
NotifStats(
- numActiveNotifs = 2,
hasNonClearableAlertingNotifs = false,
hasClearableAlertingNotifs = false,
hasNonClearableSilentNotifs = false,
@@ -311,7 +309,6 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() {
activeNotificationListRepository.notifStats.value =
NotifStats(
- numActiveNotifs = 0,
hasNonClearableAlertingNotifs = false,
hasClearableAlertingNotifs = false,
hasNonClearableSilentNotifs = false,
@@ -329,7 +326,6 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() {
activeNotificationListRepository.notifStats.value =
NotifStats(
- numActiveNotifs = 2,
hasNonClearableAlertingNotifs = false,
hasClearableAlertingNotifs = false,
hasNonClearableSilentNotifs = false,
@@ -347,7 +343,6 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() {
activeNotificationListRepository.notifStats.value =
NotifStats(
- numActiveNotifs = 2,
hasNonClearableAlertingNotifs = true,
hasClearableAlertingNotifs = false,
hasNonClearableSilentNotifs = true,
@@ -365,7 +360,6 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() {
activeNotificationListRepository.notifStats.value =
NotifStats(
- numActiveNotifs = 2,
hasNonClearableAlertingNotifs = false,
hasClearableAlertingNotifs = true,
hasNonClearableSilentNotifs = false,
@@ -383,7 +377,6 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() {
activeNotificationListRepository.notifStats.value =
NotifStats(
- numActiveNotifs = 2,
hasNonClearableAlertingNotifs = false,
hasClearableAlertingNotifs = false,
hasNonClearableSilentNotifs = true,
@@ -401,7 +394,6 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() {
activeNotificationListRepository.notifStats.value =
NotifStats(
- numActiveNotifs = 2,
hasNonClearableAlertingNotifs = false,
hasClearableAlertingNotifs = false,
hasNonClearableSilentNotifs = false,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
index 06b1c432955a..b3a60b052d08 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
@@ -37,7 +37,7 @@ import com.android.systemui.power.shared.model.WakefulnessState
import com.android.systemui.res.R
import com.android.systemui.shade.shadeTestUtil
import com.android.systemui.shared.settings.data.repository.fakeSecureSettingsRepository
-import com.android.systemui.statusbar.notification.collection.render.NotifStats
+import com.android.systemui.statusbar.notification.data.model.NotifStats
import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
import com.android.systemui.statusbar.notification.footer.shared.NotifRedesignFooter
@@ -115,7 +115,6 @@ class FooterViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
activeNotificationListRepository.notifStats.value =
NotifStats(
- numActiveNotifs = 2,
hasNonClearableAlertingNotifs = false,
hasClearableAlertingNotifs = true,
hasNonClearableSilentNotifs = false,
@@ -133,7 +132,6 @@ class FooterViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
activeNotificationListRepository.notifStats.value =
NotifStats(
- numActiveNotifs = 2,
hasNonClearableAlertingNotifs = false,
hasClearableAlertingNotifs = false,
hasNonClearableSilentNotifs = false,
@@ -151,7 +149,6 @@ class FooterViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
activeNotificationListRepository.notifStats.value =
NotifStats(
- numActiveNotifs = 2,
hasNonClearableAlertingNotifs = false,
hasClearableAlertingNotifs = true,
hasNonClearableSilentNotifs = false,
@@ -183,7 +180,6 @@ class FooterViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
// AND there are clearable notifications
activeNotificationListRepository.notifStats.value =
NotifStats(
- numActiveNotifs = 2,
hasNonClearableAlertingNotifs = false,
hasClearableAlertingNotifs = true,
hasNonClearableSilentNotifs = false,
@@ -217,7 +213,6 @@ class FooterViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
// AND there are clearable notifications
activeNotificationListRepository.notifStats.value =
NotifStats(
- numActiveNotifs = 2,
hasNonClearableAlertingNotifs = false,
hasClearableAlertingNotifs = true,
hasNonClearableSilentNotifs = false,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinator.kt
index 90212ed5b5f7..034a4fd2af72 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinator.kt
@@ -36,7 +36,7 @@ class DataStoreCoordinator
internal constructor(private val notifLiveDataStoreImpl: NotifLiveDataStoreImpl) : CoreCoordinator {
override fun attach(pipeline: NotifPipeline) {
- pipeline.addOnAfterRenderListListener { entries, _ -> onAfterRenderList(entries) }
+ pipeline.addOnAfterRenderListListener { entries -> onAfterRenderList(entries) }
}
override fun dumpPipeline(d: PipelineDumper) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
index d4d3cdf42fb1..1cb2366a16fe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
@@ -23,8 +23,7 @@ import com.android.systemui.statusbar.notification.collection.ListEntry
import com.android.systemui.statusbar.notification.collection.NotifPipeline
import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManagerImpl
-import com.android.systemui.statusbar.notification.collection.render.NotifStackController
-import com.android.systemui.statusbar.notification.collection.render.NotifStats
+import com.android.systemui.statusbar.notification.data.model.NotifStats
import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.RenderNotificationListInteractor
import com.android.systemui.statusbar.notification.stack.BUCKET_SILENT
@@ -51,8 +50,7 @@ internal constructor(
groupExpansionManagerImpl.attach(pipeline)
}
- // TODO: b/293167744 - Remove controller param.
- private fun onAfterRenderList(entries: List<ListEntry>, controller: NotifStackController) =
+ private fun onAfterRenderList(entries: List<ListEntry>) =
traceSection("StackCoordinator.onAfterRenderList") {
val notifStats = calculateNotifStats(entries)
activeNotificationsInteractor.setNotifStats(notifStats)
@@ -84,7 +82,6 @@ internal constructor(
}
}
return NotifStats(
- numActiveNotifs = entries.size,
hasNonClearableAlertingNotifs = hasNonClearableAlertingNotifs,
hasClearableAlertingNotifs = hasClearableAlertingNotifs,
hasNonClearableSilentNotifs = hasNonClearableSilentNotifs,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NotifPipelineInitializer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NotifPipelineInitializer.java
index a34d033afcaa..c58b3febe54b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NotifPipelineInitializer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NotifPipelineInitializer.java
@@ -33,7 +33,6 @@ import com.android.systemui.statusbar.notification.collection.ShadeListBuilder;
import com.android.systemui.statusbar.notification.collection.coalescer.GroupCoalescer;
import com.android.systemui.statusbar.notification.collection.coordinator.NotifCoordinators;
import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl;
-import com.android.systemui.statusbar.notification.collection.render.NotifStackController;
import com.android.systemui.statusbar.notification.collection.render.RenderStageManager;
import com.android.systemui.statusbar.notification.collection.render.ShadeViewManager;
import com.android.systemui.statusbar.notification.collection.render.ShadeViewManagerFactory;
@@ -89,8 +88,7 @@ public class NotifPipelineInitializer implements Dumpable, PipelineDumpable {
public void initialize(
NotificationListener notificationService,
NotificationRowBinderImpl rowBinder,
- NotificationListContainer listContainer,
- NotifStackController stackController) {
+ NotificationListContainer listContainer) {
mDumpManager.registerDumpable("NotifPipeline", this);
mNotificationService = notificationService;
@@ -102,7 +100,7 @@ public class NotifPipelineInitializer implements Dumpable, PipelineDumpable {
mNotifPluggableCoordinators.attach(mPipelineWrapper);
// Wire up pipeline
- mShadeViewManager = mShadeViewManagerFactory.create(listContainer, stackController);
+ mShadeViewManager = mShadeViewManagerFactory.create(listContainer);
mShadeViewManager.attach(mRenderStageManager);
mRenderStageManager.attach(mListBuilder);
mListBuilder.attach(mNotifCollection);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnAfterRenderListListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnAfterRenderListListener.java
index b5a0f7ae169d..ac450c03b850 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnAfterRenderListListener.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnAfterRenderListListener.java
@@ -20,7 +20,6 @@ import androidx.annotation.NonNull;
import com.android.systemui.statusbar.notification.collection.ListEntry;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
-import com.android.systemui.statusbar.notification.collection.render.NotifStackController;
import java.util.List;
@@ -31,9 +30,6 @@ public interface OnAfterRenderListListener {
*
* @param entries The current list of top-level entries. Note that this is a live view into the
* current list and will change whenever the pipeline is rerun.
- * @param controller An object for setting state on the shade.
*/
- void onAfterRenderList(
- @NonNull List<ListEntry> entries,
- @NonNull NotifStackController controller);
+ void onAfterRenderList(@NonNull List<ListEntry> entries);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt
deleted file mode 100644
index a37937a6c495..000000000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * 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.statusbar.notification.collection.render
-
-import javax.inject.Inject
-
-/** An interface by which the pipeline can make updates to the notification root view. */
-interface NotifStackController {
- /** Provides stats about the list of notifications attached to the shade */
- fun setNotifStats(stats: NotifStats)
-}
-
-/** Data provided to the NotificationRootController whenever the pipeline runs */
-data class NotifStats(
- // TODO(b/293167744): The count can be removed from here when we remove the FooterView flag.
- val numActiveNotifs: Int,
- val hasNonClearableAlertingNotifs: Boolean,
- val hasClearableAlertingNotifs: Boolean,
- val hasNonClearableSilentNotifs: Boolean,
- val hasClearableSilentNotifs: Boolean
-) {
- companion object {
- @JvmStatic val empty = NotifStats(0, false, false, false, false)
- }
-}
-
-/**
- * An implementation of NotifStackController which provides default, no-op implementations of each
- * method. This is used by ArcSystemUI so that that implementation can opt-in to overriding methods,
- * rather than forcing us to add no-op implementations in their implementation every time a method
- * is added.
- */
-open class DefaultNotifStackController @Inject constructor() : NotifStackController {
- override fun setNotifStats(stats: NotifStats) {}
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewRenderer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewRenderer.kt
index 410b78b9d3bf..8284022c7270 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewRenderer.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewRenderer.kt
@@ -37,12 +37,6 @@ interface NotifViewRenderer {
fun onRenderList(notifList: List<ListEntry>)
/**
- * Provides an interface for the pipeline to update the overall shade. This will be called at
- * most once for each time [onRenderList] is called.
- */
- fun getStackController(): NotifStackController
-
- /**
* Provides an interface for the pipeline to update individual groups. This will be called at
* most once for each group in the most recent call to [onRenderList].
*/
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManager.kt
index 9d3b098fa966..21e68376031c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManager.kt
@@ -50,7 +50,7 @@ class RenderStageManager @Inject constructor() : PipelineDumpable {
traceSection("RenderStageManager.onRenderList") {
val viewRenderer = viewRenderer ?: return
viewRenderer.onRenderList(notifList)
- dispatchOnAfterRenderList(viewRenderer, notifList)
+ dispatchOnAfterRenderList(notifList)
dispatchOnAfterRenderGroups(viewRenderer, notifList)
dispatchOnAfterRenderEntries(viewRenderer, notifList)
viewRenderer.onDispatchComplete()
@@ -85,15 +85,9 @@ class RenderStageManager @Inject constructor() : PipelineDumpable {
dump("onAfterRenderEntryListeners", onAfterRenderEntryListeners)
}
- private fun dispatchOnAfterRenderList(
- viewRenderer: NotifViewRenderer,
- entries: List<ListEntry>,
- ) {
+ private fun dispatchOnAfterRenderList(entries: List<ListEntry>) {
traceSection("RenderStageManager.dispatchOnAfterRenderList") {
- val stackController = viewRenderer.getStackController()
- onAfterRenderListListeners.forEach { listener ->
- listener.onAfterRenderList(entries, stackController)
- }
+ onAfterRenderListListeners.forEach { listener -> listener.onAfterRenderList(entries) }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt
index 3c838e5b707e..72316bf14c9a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt
@@ -41,7 +41,6 @@ class ShadeViewManager
constructor(
@ShadeDisplayAware context: Context,
@Assisted listContainer: NotificationListContainer,
- @Assisted private val stackController: NotifStackController,
mediaContainerController: MediaContainerController,
featureManager: NotificationSectionsFeatureManager,
sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider,
@@ -83,8 +82,6 @@ constructor(
}
}
- override fun getStackController(): NotifStackController = stackController
-
override fun getGroupController(group: GroupEntry): NotifGroupController =
viewBarn.requireGroupController(group.requireSummary)
@@ -95,8 +92,5 @@ constructor(
@AssistedFactory
interface ShadeViewManagerFactory {
- fun create(
- listContainer: NotificationListContainer,
- stackController: NotifStackController,
- ): ShadeViewManager
+ fun create(listContainer: NotificationListContainer): ShadeViewManager
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/model/NotifStats.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/model/NotifStats.kt
new file mode 100644
index 000000000000..d7fd7025a94f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/model/NotifStats.kt
@@ -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.systemui.statusbar.notification.data.model
+
+/** Information about the current list of notifications. */
+data class NotifStats(
+ val hasNonClearableAlertingNotifs: Boolean,
+ val hasClearableAlertingNotifs: Boolean,
+ val hasNonClearableSilentNotifs: Boolean,
+ val hasClearableSilentNotifs: Boolean,
+) {
+ companion object {
+ @JvmStatic
+ val empty =
+ NotifStats(
+ hasNonClearableAlertingNotifs = false,
+ hasClearableAlertingNotifs = false,
+ hasNonClearableSilentNotifs = false,
+ hasClearableSilentNotifs = false,
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt
index 2b9e49372a63..70f06ebe8468 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt
@@ -16,7 +16,7 @@
package com.android.systemui.statusbar.notification.data.repository
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.statusbar.notification.collection.render.NotifStats
+import com.android.systemui.statusbar.notification.data.model.NotifStats
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore.Key
import com.android.systemui.statusbar.notification.shared.ActiveNotificationEntryModel
import com.android.systemui.statusbar.notification.shared.ActiveNotificationGroupModel
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
index 6b93ee1c435e..0c040c855368 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
@@ -18,7 +18,7 @@ package com.android.systemui.statusbar.notification.domain.interactor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
-import com.android.systemui.statusbar.notification.collection.render.NotifStats
+import com.android.systemui.statusbar.notification.data.model.NotifStats
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
import com.android.systemui.statusbar.notification.shared.ActiveNotificationGroupModel
import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt
index 2c5d9c2e449b..3c2051f0b153 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt
@@ -20,7 +20,6 @@ import android.service.notification.StatusBarNotification
import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption
import com.android.systemui.statusbar.NotificationPresenter
import com.android.systemui.statusbar.notification.NotificationActivityStarter
-import com.android.systemui.statusbar.notification.collection.render.NotifStackController
import com.android.systemui.statusbar.notification.stack.NotificationListContainer
/**
@@ -33,7 +32,6 @@ interface NotificationsController {
fun initialize(
presenter: NotificationPresenter,
listContainer: NotificationListContainer,
- stackController: NotifStackController,
notificationActivityStarter: NotificationActivityStarter,
)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
index ea6a60bd7a1c..0a9899e88d24 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
@@ -34,7 +34,6 @@ import com.android.systemui.statusbar.notification.collection.inflation.Notifica
import com.android.systemui.statusbar.notification.collection.init.NotifPipelineInitializer
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
-import com.android.systemui.statusbar.notification.collection.render.NotifStackController
import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder
import com.android.systemui.statusbar.notification.logging.NotificationLogger
import com.android.systemui.statusbar.notification.row.NotifBindPipelineInitializer
@@ -76,7 +75,6 @@ constructor(
override fun initialize(
presenter: NotificationPresenter,
listContainer: NotificationListContainer,
- stackController: NotifStackController,
notificationActivityStarter: NotificationActivityStarter,
) {
notificationListener.registerAsSystemService()
@@ -101,7 +99,7 @@ constructor(
notifPipelineInitializer
.get()
- .initialize(notificationListener, notificationRowBinder, listContainer, stackController)
+ .initialize(notificationListener, notificationRowBinder, listContainer)
targetSdkResolver.initialize(notifPipeline.get())
notificationsMediaManager.setUpWithPresenter(presenter)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt
index 148b3f021643..92d96f9e899b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt
@@ -21,7 +21,6 @@ import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.Snoo
import com.android.systemui.statusbar.NotificationListener
import com.android.systemui.statusbar.NotificationPresenter
import com.android.systemui.statusbar.notification.NotificationActivityStarter
-import com.android.systemui.statusbar.notification.collection.render.NotifStackController
import com.android.systemui.statusbar.notification.stack.NotificationListContainer
import javax.inject.Inject
@@ -35,7 +34,6 @@ constructor(private val notificationListener: NotificationListener) : Notificati
override fun initialize(
presenter: NotificationPresenter,
listContainer: NotificationListContainer,
- stackController: NotifStackController,
notificationActivityStarter: NotificationActivityStarter,
) {
// Always connect the listener even if notification-handling is disabled. Being a listener
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index b892bebb3120..e752e6581421 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -103,9 +103,7 @@ import com.android.systemui.statusbar.notification.collection.notifcollection.Di
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
import com.android.systemui.statusbar.notification.collection.provider.NotificationDismissibilityProvider;
import com.android.systemui.statusbar.notification.collection.provider.VisibilityLocationProviderDelegator;
-import com.android.systemui.statusbar.notification.collection.render.DefaultNotifStackController;
import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
-import com.android.systemui.statusbar.notification.collection.render.NotifStackController;
import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
import com.android.systemui.statusbar.notification.headsup.HeadsUpManager;
import com.android.systemui.statusbar.notification.headsup.HeadsUpNotificationViewControllerEmptyImpl;
@@ -211,9 +209,6 @@ public class NotificationStackScrollLayoutController implements Dumpable {
private final NotificationListContainerImpl mNotificationListContainer =
new NotificationListContainerImpl();
- // TODO: b/293167744 - Remove this.
- private final NotifStackController mNotifStackController =
- new DefaultNotifStackController();
@VisibleForTesting
final View.OnAttachStateChangeListener mOnAttachStateChangeListener =
@@ -1469,10 +1464,6 @@ public class NotificationStackScrollLayoutController implements Dumpable {
return mNotificationListContainer;
}
- public NotifStackController getNotifStackController() {
- return mNotifStackController;
- }
-
public void resetCheckSnoozeLeavebehind() {
mView.resetCheckSnoozeLeavebehind();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 3d6cd7e49dfe..b146b92ed110 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -1492,7 +1492,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces {
mNotificationsController.initialize(
mPresenterLazy.get(),
mNotifListContainer,
- mStackScrollerController.getNotifStackController(),
mNotificationActivityStarterLazy.get());
mWindowRootViewVisibilityInteractor.setUp(mPresenterLazy.get(), mNotificationsController);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinatorTest.kt
index a3c518128b47..f31d49094ac4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinatorTest.kt
@@ -26,7 +26,6 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection
import com.android.systemui.statusbar.notification.collection.listbuilder.OnAfterRenderListListener
-import com.android.systemui.statusbar.notification.collection.render.NotifStackController
import com.android.systemui.util.mockito.withArgCaptor
import com.google.common.truth.Truth.assertThat
import org.junit.Before
@@ -48,7 +47,6 @@ class DataStoreCoordinatorTest : SysuiTestCase() {
private val pipeline: NotifPipeline = mock()
private val notifLiveDataStoreImpl: NotifLiveDataStoreImpl = mock()
- private val stackController: NotifStackController = mock()
private val section: NotifSection = mock()
@Before
@@ -63,7 +61,7 @@ class DataStoreCoordinatorTest : SysuiTestCase() {
@Test
fun testUpdateDataStore_withOneEntry() {
- afterRenderListListener.onAfterRenderList(listOf(entry), stackController)
+ afterRenderListListener.onAfterRenderList(listOf(entry))
verify(notifLiveDataStoreImpl).setActiveNotifList(eq(listOf(entry)))
verifyNoMoreInteractions(notifLiveDataStoreImpl)
}
@@ -86,8 +84,7 @@ class DataStoreCoordinatorTest : SysuiTestCase() {
.setSection(section)
.build(),
notificationEntry("baz", 1),
- ),
- stackController,
+ )
)
val list: List<NotificationEntry> = withArgCaptor {
verify(notifLiveDataStoreImpl).setActiveNotifList(capture())
@@ -111,7 +108,7 @@ class DataStoreCoordinatorTest : SysuiTestCase() {
@Test
fun testUpdateDataStore_withZeroEntries_whenNewPipelineEnabled() {
- afterRenderListListener.onAfterRenderList(listOf(), stackController)
+ afterRenderListListener.onAfterRenderList(listOf())
verify(notifLiveDataStoreImpl).setActiveNotifList(eq(listOf()))
verifyNoMoreInteractions(notifLiveDataStoreImpl)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt
index 77bac59b9dcd..97e99b95f80e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt
@@ -28,8 +28,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntryB
import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection
import com.android.systemui.statusbar.notification.collection.listbuilder.OnAfterRenderListListener
import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManagerImpl
-import com.android.systemui.statusbar.notification.collection.render.NotifStackController
-import com.android.systemui.statusbar.notification.collection.render.NotifStats
+import com.android.systemui.statusbar.notification.data.model.NotifStats
import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.RenderNotificationListInteractor
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
@@ -43,7 +42,6 @@ import org.junit.runner.RunWith
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
-import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever
@SmallTest
@@ -61,7 +59,6 @@ class StackCoordinatorTest : SysuiTestCase() {
private val sensitiveNotificationProtectionController:
SensitiveNotificationProtectionController =
mock()
- private val stackController: NotifStackController = mock()
private val section: NotifSection = mock()
private val row: ExpandableNotificationRow = mock()
@@ -87,25 +84,23 @@ class StackCoordinatorTest : SysuiTestCase() {
@Test
fun testSetRenderedListOnInteractor() {
- afterRenderListListener.onAfterRenderList(listOf(entry), stackController)
+ afterRenderListListener.onAfterRenderList(listOf(entry))
verify(renderListInteractor).setRenderedList(eq(listOf(entry)))
}
@Test
fun testSetNotificationStats_clearableAlerting() {
whenever(section.bucket).thenReturn(BUCKET_ALERTING)
- afterRenderListListener.onAfterRenderList(listOf(entry), stackController)
+ afterRenderListListener.onAfterRenderList(listOf(entry))
verify(activeNotificationsInteractor)
.setNotifStats(
NotifStats(
- 1,
hasNonClearableAlertingNotifs = false,
hasClearableAlertingNotifs = true,
hasNonClearableSilentNotifs = false,
hasClearableSilentNotifs = false,
)
)
- verifyNoMoreInteractions(stackController)
}
@Test
@@ -113,35 +108,31 @@ class StackCoordinatorTest : SysuiTestCase() {
fun testSetNotificationStats_isSensitiveStateActive_nonClearableAlerting() {
whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true)
whenever(section.bucket).thenReturn(BUCKET_ALERTING)
- afterRenderListListener.onAfterRenderList(listOf(entry), stackController)
+ afterRenderListListener.onAfterRenderList(listOf(entry))
verify(activeNotificationsInteractor)
.setNotifStats(
NotifStats(
- 1,
hasNonClearableAlertingNotifs = true,
hasClearableAlertingNotifs = false,
hasNonClearableSilentNotifs = false,
hasClearableSilentNotifs = false,
)
)
- verifyNoMoreInteractions(stackController)
}
@Test
fun testSetNotificationStats_clearableSilent() {
whenever(section.bucket).thenReturn(BUCKET_SILENT)
- afterRenderListListener.onAfterRenderList(listOf(entry), stackController)
+ afterRenderListListener.onAfterRenderList(listOf(entry))
verify(activeNotificationsInteractor)
.setNotifStats(
NotifStats(
- 1,
hasNonClearableAlertingNotifs = false,
hasClearableAlertingNotifs = false,
hasNonClearableSilentNotifs = false,
hasClearableSilentNotifs = true,
)
)
- verifyNoMoreInteractions(stackController)
}
@Test
@@ -149,35 +140,31 @@ class StackCoordinatorTest : SysuiTestCase() {
fun testSetNotificationStats_isSensitiveStateActive_nonClearableSilent() {
whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true)
whenever(section.bucket).thenReturn(BUCKET_SILENT)
- afterRenderListListener.onAfterRenderList(listOf(entry), stackController)
+ afterRenderListListener.onAfterRenderList(listOf(entry))
verify(activeNotificationsInteractor)
.setNotifStats(
NotifStats(
- 1,
hasNonClearableAlertingNotifs = false,
hasClearableAlertingNotifs = false,
hasNonClearableSilentNotifs = true,
hasClearableSilentNotifs = false,
)
)
- verifyNoMoreInteractions(stackController)
}
@Test
fun testSetNotificationStats_nonClearableRedacted() {
entry.setSensitive(true, true)
whenever(section.bucket).thenReturn(BUCKET_ALERTING)
- afterRenderListListener.onAfterRenderList(listOf(entry), stackController)
+ afterRenderListListener.onAfterRenderList(listOf(entry))
verify(activeNotificationsInteractor)
.setNotifStats(
NotifStats(
- 1,
hasNonClearableAlertingNotifs = true,
hasClearableAlertingNotifs = false,
hasNonClearableSilentNotifs = false,
hasClearableSilentNotifs = false,
)
)
- verifyNoMoreInteractions(stackController)
}
}
diff --git a/proto/src/system_messages.proto b/proto/src/system_messages.proto
index 648990588d29..3a38152825c9 100644
--- a/proto/src/system_messages.proto
+++ b/proto/src/system_messages.proto
@@ -420,5 +420,9 @@ message SystemMessage {
// Notify the user that accessibility floating menu is hidden.
// Package: com.android.systemui
NOTE_A11Y_FLOATING_MENU_HIDDEN = 1009;
+
+ // Notify the hearing aid user that input device can be changed to builtin device or hearing device.
+ // Package: android
+ NOTE_HEARING_DEVICE_INPUT_SWITCH = 1012;
}
}
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 37d045bf6422..6cd1f721d215 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -413,6 +413,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
private SparseArray<SurfaceControl> mA11yOverlayLayers = new SparseArray<>();
private final FlashNotificationsController mFlashNotificationsController;
+ private final HearingDevicePhoneCallNotificationController mHearingDeviceNotificationController;
private final UserManagerInternal mUmi;
private AccessibilityUserState getCurrentUserStateLocked() {
@@ -569,6 +570,12 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
// TODO(b/255426725): not used on tests
mVisibleBgUserIds = null;
mInputManager = context.getSystemService(InputManager.class);
+ if (com.android.settingslib.flags.Flags.hearingDevicesInputRoutingControl()) {
+ mHearingDeviceNotificationController = new HearingDevicePhoneCallNotificationController(
+ context);
+ } else {
+ mHearingDeviceNotificationController = null;
+ }
init();
}
@@ -618,6 +625,12 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
} else {
mVisibleBgUserIds = null;
}
+ if (com.android.settingslib.flags.Flags.hearingDevicesInputRoutingControl()) {
+ mHearingDeviceNotificationController = new HearingDevicePhoneCallNotificationController(
+ context);
+ } else {
+ mHearingDeviceNotificationController = null;
+ }
init();
}
@@ -630,6 +643,11 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
if (enableTalkbackAndMagnifierKeyGestures()) {
mInputManager.registerKeyGestureEventHandler(mKeyGestureEventHandler);
}
+ if (com.android.settingslib.flags.Flags.hearingDevicesInputRoutingControl()) {
+ if (mHearingDeviceNotificationController != null) {
+ mHearingDeviceNotificationController.startListenForCallState();
+ }
+ }
disableAccessibilityMenuToMigrateIfNeeded();
}
diff --git a/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java b/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java
new file mode 100644
index 000000000000..d06daf5db127
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java
@@ -0,0 +1,356 @@
+/*
+ * 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.accessibility;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothUuid;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.media.MediaRecorder;
+import android.os.Bundle;
+import android.telephony.TelephonyCallback;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.internal.R;
+import com.android.internal.messages.nano.SystemMessageProto;
+import com.android.internal.notification.SystemNotificationChannels;
+import com.android.internal.util.ArrayUtils;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+
+/**
+ * A controller class to handle notification for hearing device during phone calls.
+ */
+public class HearingDevicePhoneCallNotificationController {
+
+ private final TelephonyManager mTelephonyManager;
+ private final TelephonyCallback mTelephonyListener;
+ private final Executor mCallbackExecutor;
+
+ public HearingDevicePhoneCallNotificationController(@NonNull Context context) {
+ mTelephonyListener = new CallStateListener(context);
+ mTelephonyManager = context.getSystemService(TelephonyManager.class);
+ mCallbackExecutor = Executors.newSingleThreadExecutor();
+ }
+
+ @VisibleForTesting
+ HearingDevicePhoneCallNotificationController(@NonNull Context context,
+ TelephonyCallback telephonyCallback) {
+ mTelephonyListener = telephonyCallback;
+ mTelephonyManager = context.getSystemService(TelephonyManager.class);
+ mCallbackExecutor = context.getMainExecutor();
+ }
+
+ /**
+ * Registers a telephony callback to listen for call state changed to handle notification for
+ * hearing device during phone calls.
+ */
+ public void startListenForCallState() {
+ mTelephonyManager.registerTelephonyCallback(mCallbackExecutor, mTelephonyListener);
+ }
+
+ /**
+ * A telephony callback listener to listen to call state changes and show/dismiss notification
+ */
+ @VisibleForTesting
+ static class CallStateListener extends TelephonyCallback implements
+ TelephonyCallback.CallStateListener {
+
+ private static final String TAG =
+ "HearingDevice_CallStateListener";
+ private static final String ACTION_SWITCH_TO_BUILTIN_MIC =
+ "com.android.server.accessibility.hearingdevice.action.SWITCH_TO_BUILTIN_MIC";
+ private static final String ACTION_SWITCH_TO_HEARING_MIC =
+ "com.android.server.accessibility.hearingdevice.action.SWITCH_TO_HEARING_MIC";
+ private static final String ACTION_BLUETOOTH_DEVICE_DETAILS =
+ "com.android.settings.BLUETOOTH_DEVICE_DETAIL_SETTINGS";
+ private static final String KEY_BLUETOOTH_ADDRESS = "device_address";
+ private static final String EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args";
+ private static final int MICROPHONE_SOURCE_VOICE_COMMUNICATION =
+ MediaRecorder.AudioSource.VOICE_COMMUNICATION;
+ private static final AudioDeviceAttributes BUILTIN_MIC = new AudioDeviceAttributes(
+ AudioDeviceAttributes.ROLE_INPUT, AudioDeviceInfo.TYPE_BUILTIN_MIC, "");
+
+ private final Context mContext;
+ private NotificationManager mNotificationManager;
+ private AudioManager mAudioManager;
+ private BroadcastReceiver mHearingDeviceActionReceiver;
+ private BluetoothDevice mHearingDevice;
+ private boolean mIsNotificationShown = false;
+
+ CallStateListener(@NonNull Context context) {
+ mContext = context;
+ }
+
+ @Override
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public void onCallStateChanged(int state) {
+ // NotificationManagerService and AudioService are all initialized after
+ // AccessibilityManagerService.
+ // Can not get them in constructor. Need to get these services until callback is
+ // triggered.
+ mNotificationManager = mContext.getSystemService(NotificationManager.class);
+ mAudioManager = mContext.getSystemService(AudioManager.class);
+ if (mNotificationManager == null || mAudioManager == null) {
+ Log.w(TAG, "NotificationManager or AudioManager is not prepare yet.");
+ return;
+ }
+
+ if (state == TelephonyManager.CALL_STATE_IDLE) {
+ dismissNotificationIfNeeded();
+
+ if (mHearingDevice != null) {
+ // reset to its original status
+ setMicrophonePreferredForCalls(mHearingDevice.isMicrophonePreferredForCalls());
+ }
+ mHearingDevice = null;
+ }
+ if (state == TelephonyManager.CALL_STATE_OFFHOOK) {
+ mHearingDevice = getSupportedInputHearingDeviceInfo(
+ mAudioManager.getAvailableCommunicationDevices());
+ if (mHearingDevice != null) {
+ showNotificationIfNeeded();
+ }
+ }
+ }
+
+ private void showNotificationIfNeeded() {
+ if (mIsNotificationShown) {
+ return;
+ }
+
+ showNotification(mHearingDevice.isMicrophonePreferredForCalls());
+ mIsNotificationShown = true;
+ }
+
+ private void dismissNotificationIfNeeded() {
+ if (!mIsNotificationShown) {
+ return;
+ }
+
+ dismissNotification();
+ mIsNotificationShown = false;
+ }
+
+ private void showNotification(boolean useRemoteMicrophone) {
+ mNotificationManager.notify(
+ SystemMessageProto.SystemMessage.NOTE_HEARING_DEVICE_INPUT_SWITCH,
+ createSwitchInputNotification(useRemoteMicrophone));
+ registerReceiverIfNeeded();
+ }
+
+ private void dismissNotification() {
+ unregisterReceiverIfNeeded();
+ mNotificationManager.cancel(
+ SystemMessageProto.SystemMessage.NOTE_HEARING_DEVICE_INPUT_SWITCH);
+ }
+
+ private BluetoothDevice getSupportedInputHearingDeviceInfo(List<AudioDeviceInfo> infoList) {
+ final BluetoothAdapter bluetoothAdapter = mContext.getSystemService(
+ BluetoothManager.class).getAdapter();
+ if (bluetoothAdapter == null) {
+ return null;
+ }
+ if (!isHapClientSupported()) {
+ return null;
+ }
+
+ final Set<String> inputDeviceAddress = Arrays.stream(
+ mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).map(
+ AudioDeviceInfo::getAddress).collect(Collectors.toSet());
+
+ //TODO: b/370812132 - Need to update if TYPE_LEA_HEARING_AID is added
+ final AudioDeviceInfo hearingDeviceInfo = infoList.stream()
+ .filter(info -> info.getType() == AudioDeviceInfo.TYPE_BLE_HEADSET)
+ .filter(info -> inputDeviceAddress.contains(info.getAddress()))
+ .filter(info -> isHapClientDevice(bluetoothAdapter, info))
+ .findAny()
+ .orElse(null);
+
+ return (hearingDeviceInfo != null) ? bluetoothAdapter.getRemoteDevice(
+ hearingDeviceInfo.getAddress()) : null;
+ }
+
+ @VisibleForTesting
+ boolean isHapClientDevice(BluetoothAdapter bluetoothAdapter, AudioDeviceInfo info) {
+ BluetoothDevice device = bluetoothAdapter.getRemoteDevice(info.getAddress());
+ return ArrayUtils.contains(device.getUuids(), BluetoothUuid.HAS);
+ }
+
+ @VisibleForTesting
+ boolean isHapClientSupported() {
+ return BluetoothAdapter.getDefaultAdapter().getSupportedProfiles().contains(
+ BluetoothProfile.HAP_CLIENT);
+ }
+
+ private Notification createSwitchInputNotification(boolean useRemoteMicrophone) {
+ return new Notification.Builder(mContext,
+ SystemNotificationChannels.ACCESSIBILITY_HEARING_DEVICE)
+ .setContentTitle(getSwitchInputTitle(useRemoteMicrophone))
+ .setContentText(getSwitchInputMessage(useRemoteMicrophone))
+ .setSmallIcon(R.drawable.ic_settings_24dp)
+ .setColor(mContext.getResources().getColor(
+ com.android.internal.R.color.system_notification_accent_color))
+ .setLocalOnly(true)
+ .setCategory(Notification.CATEGORY_SYSTEM)
+ .setContentIntent(createPendingIntent(ACTION_BLUETOOTH_DEVICE_DETAILS))
+ .setActions(buildSwitchInputAction(useRemoteMicrophone),
+ buildOpenSettingsAction())
+ .build();
+ }
+
+ private Notification.Action buildSwitchInputAction(boolean useRemoteMicrophone) {
+ return useRemoteMicrophone
+ ? new Notification.Action.Builder(null,
+ mContext.getString(R.string.hearing_device_notification_switch_button),
+ createPendingIntent(ACTION_SWITCH_TO_BUILTIN_MIC)).build()
+ : new Notification.Action.Builder(null,
+ mContext.getString(R.string.hearing_device_notification_switch_button),
+ createPendingIntent(ACTION_SWITCH_TO_HEARING_MIC)).build();
+ }
+
+ private Notification.Action buildOpenSettingsAction() {
+ return new Notification.Action.Builder(null,
+ mContext.getString(R.string.hearing_device_notification_settings_button),
+ createPendingIntent(ACTION_BLUETOOTH_DEVICE_DETAILS)).build();
+ }
+
+ private PendingIntent createPendingIntent(String action) {
+ final Intent intent = new Intent(action);
+
+ switch (action) {
+ case ACTION_SWITCH_TO_BUILTIN_MIC, ACTION_SWITCH_TO_HEARING_MIC -> {
+ intent.setPackage(mContext.getPackageName());
+ return PendingIntent.getBroadcast(mContext, /* requestCode = */ 0, intent,
+ PendingIntent.FLAG_IMMUTABLE);
+ }
+ case ACTION_BLUETOOTH_DEVICE_DETAILS -> {
+ Bundle bundle = new Bundle();
+ bundle.putString(KEY_BLUETOOTH_ADDRESS, mHearingDevice.getAddress());
+ intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, bundle);
+ intent.addFlags(
+ Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ return PendingIntent.getActivity(mContext, /* requestCode = */ 0, intent,
+ PendingIntent.FLAG_IMMUTABLE);
+ }
+ }
+ return null;
+ }
+
+ private void setMicrophonePreferredForCalls(boolean useRemoteMicrophone) {
+ if (useRemoteMicrophone) {
+ switchToHearingMic();
+ } else {
+ switchToBuiltinMic();
+ }
+ }
+
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ private void switchToBuiltinMic() {
+ mAudioManager.clearPreferredDevicesForCapturePreset(
+ MICROPHONE_SOURCE_VOICE_COMMUNICATION);
+ mAudioManager.setPreferredDeviceForCapturePreset(MICROPHONE_SOURCE_VOICE_COMMUNICATION,
+ BUILTIN_MIC);
+ }
+
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ private void switchToHearingMic() {
+ // clear config to let audio manager to determine next priority device. We can assume
+ // user connects to hearing device here, so next priority device should be hearing
+ // device.
+ mAudioManager.clearPreferredDevicesForCapturePreset(
+ MICROPHONE_SOURCE_VOICE_COMMUNICATION);
+ }
+
+ private void registerReceiverIfNeeded() {
+ if (mHearingDeviceActionReceiver != null) {
+ return;
+ }
+ mHearingDeviceActionReceiver = new HearingDeviceActionReceiver();
+ final IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(ACTION_SWITCH_TO_BUILTIN_MIC);
+ intentFilter.addAction(ACTION_SWITCH_TO_HEARING_MIC);
+ mContext.registerReceiver(mHearingDeviceActionReceiver, intentFilter,
+ Manifest.permission.MANAGE_ACCESSIBILITY, null, Context.RECEIVER_NOT_EXPORTED);
+ }
+
+ private void unregisterReceiverIfNeeded() {
+ if (mHearingDeviceActionReceiver == null) {
+ return;
+ }
+ mContext.unregisterReceiver(mHearingDeviceActionReceiver);
+ mHearingDeviceActionReceiver = null;
+ }
+
+ private CharSequence getSwitchInputTitle(boolean useRemoteMicrophone) {
+ return useRemoteMicrophone
+ ? mContext.getString(
+ R.string.hearing_device_switch_phone_mic_notification_title)
+ : mContext.getString(
+ R.string.hearing_device_switch_hearing_mic_notification_title);
+ }
+
+ private CharSequence getSwitchInputMessage(boolean useRemoteMicrophone) {
+ return useRemoteMicrophone
+ ? mContext.getString(
+ R.string.hearing_device_switch_phone_mic_notification_text)
+ : mContext.getString(
+ R.string.hearing_device_switch_hearing_mic_notification_text);
+ }
+
+ private class HearingDeviceActionReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (TextUtils.isEmpty(action)) {
+ return;
+ }
+
+ if (ACTION_SWITCH_TO_BUILTIN_MIC.equals(action)) {
+ switchToBuiltinMic();
+ showNotification(/* useRemoteMicrophone= */ false);
+ } else if (ACTION_SWITCH_TO_HEARING_MIC.equals(action)) {
+ switchToHearingMic();
+ showNotification(/* useRemoteMicrophone= */ true);
+ }
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 9567c818fa18..dd9741ce9ca1 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -3162,6 +3162,7 @@ public class NotificationManagerService extends SystemService {
mAssistants.onBootPhaseAppsCanStart();
mConditionProviders.onBootPhaseAppsCanStart();
mHistoryManager.onBootPhaseAppsCanStart();
+ mPreferencesHelper.onBootPhaseAppsCanStart();
migrateDefaultNAS();
maybeShowInitialReviewPermissionsNotification();
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 15377d6b269a..9d25d18df87c 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -82,7 +82,6 @@ import android.text.format.DateUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.IntArray;
-import android.util.Log;
import android.util.Pair;
import android.util.Slog;
import android.util.SparseBooleanArray;
@@ -272,6 +271,15 @@ public class PreferencesHelper implements RankingConfig {
updateMediaNotificationFilteringEnabled();
}
+ void onBootPhaseAppsCanStart() {
+ // IpcDataCaches must be invalidated once data becomes available, as queries will only
+ // begin to be cached after the first invalidation signal. At this point, we know about all
+ // notification channels.
+ if (android.app.Flags.nmBinderPerfCacheChannels()) {
+ invalidateNotificationChannelCache();
+ }
+ }
+
public void readXml(TypedXmlPullParser parser, boolean forRestore, int userId)
throws XmlPullParserException, IOException {
int type = parser.getEventType();
@@ -531,12 +539,14 @@ public class PreferencesHelper implements RankingConfig {
private PackagePreferences getOrCreatePackagePreferencesLocked(String pkg,
@UserIdInt int userId, int uid, int importance, int priority, int visibility,
boolean showBadge, int bubblePreference, long creationTime) {
+ boolean created = false;
final String key = packagePreferencesKey(pkg, uid);
PackagePreferences
r = (uid == UNKNOWN_UID)
? mRestoredWithoutUids.get(unrestoredPackageKey(pkg, userId))
: mPackagePreferences.get(key);
if (r == null) {
+ created = true;
r = new PackagePreferences();
r.pkg = pkg;
r.uid = uid;
@@ -572,6 +582,9 @@ public class PreferencesHelper implements RankingConfig {
mRestoredWithoutUids.remove(unrestoredPackageKey(pkg, userId));
}
}
+ if (android.app.Flags.nmBinderPerfCacheChannels() && created) {
+ invalidateNotificationChannelCache();
+ }
return r;
}
@@ -664,6 +677,9 @@ public class PreferencesHelper implements RankingConfig {
}
NotificationChannel channel = new NotificationChannel(channelId, label, IMPORTANCE_LOW);
p.channels.put(channelId, channel);
+ if (android.app.Flags.nmBinderPerfCacheChannels()) {
+ invalidateNotificationChannelCache();
+ }
return channel;
}
@@ -1208,6 +1224,10 @@ public class PreferencesHelper implements RankingConfig {
updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
}
+ if (android.app.Flags.nmBinderPerfCacheChannels() && needsPolicyFileChange) {
+ invalidateNotificationChannelCache();
+ }
+
return needsPolicyFileChange;
}
@@ -1229,6 +1249,9 @@ public class PreferencesHelper implements RankingConfig {
}
channel.unlockFields(USER_LOCKED_IMPORTANCE);
}
+ if (android.app.Flags.nmBinderPerfCacheChannels()) {
+ invalidateNotificationChannelCache();
+ }
}
@@ -1301,6 +1324,9 @@ public class PreferencesHelper implements RankingConfig {
updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
}
if (changed) {
+ if (android.app.Flags.nmBinderPerfCacheChannels()) {
+ invalidateNotificationChannelCache();
+ }
updateConfig();
}
}
@@ -1537,6 +1563,10 @@ public class PreferencesHelper implements RankingConfig {
if (channelBypassedDnd) {
updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
}
+
+ if (android.app.Flags.nmBinderPerfCacheChannels() && deletedChannel) {
+ invalidateNotificationChannelCache();
+ }
return deletedChannel;
}
@@ -1566,6 +1596,9 @@ public class PreferencesHelper implements RankingConfig {
}
r.channels.remove(channelId);
}
+ if (android.app.Flags.nmBinderPerfCacheChannels()) {
+ invalidateNotificationChannelCache();
+ }
}
@Override
@@ -1576,13 +1609,18 @@ public class PreferencesHelper implements RankingConfig {
if (r == null) {
return;
}
+ boolean deleted = false;
int N = r.channels.size() - 1;
for (int i = N; i >= 0; i--) {
String key = r.channels.keyAt(i);
if (!DEFAULT_CHANNEL_ID.equals(key)) {
r.channels.remove(key);
+ deleted = true;
}
}
+ if (android.app.Flags.nmBinderPerfCacheChannels() && deleted) {
+ invalidateNotificationChannelCache();
+ }
}
}
@@ -1613,6 +1651,9 @@ public class PreferencesHelper implements RankingConfig {
}
}
}
+ if (android.app.Flags.nmBinderPerfCacheChannels()) {
+ invalidateNotificationChannelCache();
+ }
}
public void updateDefaultApps(int userId, ArraySet<String> toRemove,
@@ -1642,6 +1683,9 @@ public class PreferencesHelper implements RankingConfig {
}
}
}
+ if (android.app.Flags.nmBinderPerfCacheChannels()) {
+ invalidateNotificationChannelCache();
+ }
}
public NotificationChannelGroup getNotificationChannelGroupWithChannels(String pkg,
@@ -1757,6 +1801,9 @@ public class PreferencesHelper implements RankingConfig {
if (groupBypassedDnd) {
updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
}
+ if (android.app.Flags.nmBinderPerfCacheChannels() && deletedChannels.size() > 0) {
+ invalidateNotificationChannelCache();
+ }
return deletedChannels;
}
@@ -1902,8 +1949,13 @@ public class PreferencesHelper implements RankingConfig {
}
}
}
- if (!deletedChannelIds.isEmpty() && mCurrentUserHasChannelsBypassingDnd) {
- updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+ if (!deletedChannelIds.isEmpty()) {
+ if (mCurrentUserHasChannelsBypassingDnd) {
+ updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+ }
+ if (android.app.Flags.nmBinderPerfCacheChannels()) {
+ invalidateNotificationChannelCache();
+ }
}
return deletedChannelIds;
}
@@ -2196,6 +2248,11 @@ public class PreferencesHelper implements RankingConfig {
PackagePreferences prefs = getOrCreatePackagePreferencesLocked(sourcePkg, sourceUid);
prefs.delegate = new Delegate(delegatePkg, delegateUid, true);
}
+ if (android.app.Flags.nmBinderPerfCacheChannels()) {
+ // If package delegates change, then which packages can get what channel information
+ // also changes, so we need to clear the cache.
+ invalidateNotificationChannelCache();
+ }
}
/**
@@ -2208,6 +2265,9 @@ public class PreferencesHelper implements RankingConfig {
prefs.delegate.mEnabled = false;
}
}
+ if (android.app.Flags.nmBinderPerfCacheChannels()) {
+ invalidateNotificationChannelCache();
+ }
}
/**
@@ -2811,18 +2871,24 @@ public class PreferencesHelper implements RankingConfig {
public void onUserRemoved(int userId) {
synchronized (mLock) {
+ boolean removed = false;
int N = mPackagePreferences.size();
for (int i = N - 1; i >= 0; i--) {
PackagePreferences PackagePreferences = mPackagePreferences.valueAt(i);
if (UserHandle.getUserId(PackagePreferences.uid) == userId) {
mPackagePreferences.removeAt(i);
+ removed = true;
}
}
+ if (android.app.Flags.nmBinderPerfCacheChannels() && removed) {
+ invalidateNotificationChannelCache();
+ }
}
}
protected void onLocaleChanged(Context context, int userId) {
synchronized (mLock) {
+ boolean updated = false;
int N = mPackagePreferences.size();
for (int i = 0; i < N; i++) {
PackagePreferences PackagePreferences = mPackagePreferences.valueAt(i);
@@ -2833,10 +2899,14 @@ public class PreferencesHelper implements RankingConfig {
DEFAULT_CHANNEL_ID).setName(
context.getResources().getString(
R.string.default_notification_channel_label));
+ updated = true;
}
// TODO (b/346396459): Localize all reserved channels
}
}
+ if (android.app.Flags.nmBinderPerfCacheChannels() && updated) {
+ invalidateNotificationChannelCache();
+ }
}
}
@@ -2884,7 +2954,7 @@ public class PreferencesHelper implements RankingConfig {
channel.getAudioAttributes().getUsage());
if (Settings.System.DEFAULT_NOTIFICATION_URI.equals(
restoredUri)) {
- Log.w(TAG,
+ Slog.w(TAG,
"Could not restore sound: " + uri + " for channel: "
+ channel);
}
@@ -2922,6 +2992,9 @@ public class PreferencesHelper implements RankingConfig {
if (updated) {
updateConfig();
+ if (android.app.Flags.nmBinderPerfCacheChannels()) {
+ invalidateNotificationChannelCache();
+ }
}
return updated;
}
@@ -2939,6 +3012,9 @@ public class PreferencesHelper implements RankingConfig {
p.priority = DEFAULT_PRIORITY;
p.visibility = DEFAULT_VISIBILITY;
p.showBadge = DEFAULT_SHOW_BADGE;
+ if (android.app.Flags.nmBinderPerfCacheChannels()) {
+ invalidateNotificationChannelCache();
+ }
}
}
}
@@ -3123,6 +3199,9 @@ public class PreferencesHelper implements RankingConfig {
}
}
}
+ if (android.app.Flags.nmBinderPerfCacheChannels()) {
+ invalidateNotificationChannelCache();
+ }
}
public void migrateNotificationPermissions(List<UserInfo> users) {
@@ -3154,6 +3233,12 @@ public class PreferencesHelper implements RankingConfig {
mRankingHandler.requestSort();
}
+ @VisibleForTesting
+ // Utility method for overriding in tests to confirm that the cache gets cleared.
+ protected void invalidateNotificationChannelCache() {
+ NotificationManager.invalidateNotificationChannelCache();
+ }
+
private static String packagePreferencesKey(String pkg, int uid) {
return pkg + "|" + uid;
}
diff --git a/services/core/java/com/android/server/vibrator/BasicToPwleSegmentAdapter.java b/services/core/java/com/android/server/vibrator/BasicToPwleSegmentAdapter.java
index 54ae047a2858..0b676ff7d590 100644
--- a/services/core/java/com/android/server/vibrator/BasicToPwleSegmentAdapter.java
+++ b/services/core/java/com/android/server/vibrator/BasicToPwleSegmentAdapter.java
@@ -100,6 +100,11 @@ final class BasicToPwleSegmentAdapter implements VibrationSegmentsAdapter {
}
VibratorInfo.FrequencyProfile frequencyProfile = info.getFrequencyProfile();
+ if (frequencyProfile.isEmpty()) {
+ // The frequency profile has an invalid frequency range, so keep the segments unchanged.
+ return repeatIndex;
+ }
+
float[] frequenciesHz = frequencyProfile.getFrequenciesHz();
float[] accelerationsGs = frequencyProfile.getOutputAccelerationsGs();
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java
new file mode 100644
index 000000000000..efea21428937
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java
@@ -0,0 +1,187 @@
+/*
+ * 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.accessibility;
+
+import static android.Manifest.permission.BLUETOOTH_PRIVILEGED;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.Application;
+import android.app.Instrumentation;
+import android.app.NotificationManager;
+import android.bluetooth.BluetoothAdapter;
+import android.content.Context;
+import android.media.AudioDeviceInfo;
+import android.media.AudioDevicePort;
+import android.media.AudioManager;
+import android.telephony.TelephonyCallback;
+import android.telephony.TelephonyManager;
+
+import androidx.annotation.NonNull;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.messages.nano.SystemMessageProto;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Tests for the {@link HearingDevicePhoneCallNotificationController}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class HearingDevicePhoneCallNotificationControllerTest {
+ @Rule
+ public MockitoRule mockito = MockitoJUnit.rule();
+
+ private static final String TEST_ADDRESS = "55:66:77:88:99:AA";
+
+ private final Application mApplication = ApplicationProvider.getApplicationContext();
+ @Spy
+ private final Context mContext = mApplication.getApplicationContext();
+ private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
+
+ @Mock
+ private TelephonyManager mTelephonyManager;
+ @Mock
+ private NotificationManager mNotificationManager;
+ @Mock
+ private AudioManager mAudioManager;
+ private HearingDevicePhoneCallNotificationController mController;
+ private TestCallStateListener mTestCallStateListener;
+
+ @Before
+ public void setUp() {
+ mInstrumentation.getUiAutomation().adoptShellPermissionIdentity(BLUETOOTH_PRIVILEGED);
+ when(mContext.getSystemService(TelephonyManager.class)).thenReturn(mTelephonyManager);
+ when(mContext.getSystemService(NotificationManager.class)).thenReturn(mNotificationManager);
+ when(mContext.getSystemService(AudioManager.class)).thenReturn(mAudioManager);
+
+ mTestCallStateListener = new TestCallStateListener(mContext);
+ mController = new HearingDevicePhoneCallNotificationController(mContext,
+ mTestCallStateListener);
+ mController.startListenForCallState();
+ }
+
+ @Test
+ public void startListenForCallState_callbackNotNull() {
+ Mockito.reset(mTelephonyManager);
+ mController = new HearingDevicePhoneCallNotificationController(mContext);
+ ArgumentCaptor<TelephonyCallback> listenerCaptor = ArgumentCaptor.forClass(
+ TelephonyCallback.class);
+
+ mController.startListenForCallState();
+
+ verify(mTelephonyManager).registerTelephonyCallback(any(Executor.class),
+ listenerCaptor.capture());
+ TelephonyCallback callback = listenerCaptor.getValue();
+ assertThat(callback).isNotNull();
+ }
+
+ @Test
+ public void onCallStateChanged_stateOffHook_hapDevice_showNotification() {
+ AudioDeviceInfo hapDeviceInfo = createAudioDeviceInfo(TEST_ADDRESS,
+ AudioManager.DEVICE_OUT_BLE_HEADSET);
+ when(mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(
+ new AudioDeviceInfo[]{hapDeviceInfo});
+ when(mAudioManager.getAvailableCommunicationDevices()).thenReturn(List.of(hapDeviceInfo));
+
+ mTestCallStateListener.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK);
+
+ verify(mNotificationManager).notify(
+ eq(SystemMessageProto.SystemMessage.NOTE_HEARING_DEVICE_INPUT_SWITCH), any());
+ }
+
+ @Test
+ public void onCallStateChanged_stateOffHook_a2dpDevice_noNotification() {
+ AudioDeviceInfo a2dpDeviceInfo = createAudioDeviceInfo(TEST_ADDRESS,
+ AudioManager.DEVICE_OUT_BLUETOOTH_A2DP);
+ when(mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(
+ new AudioDeviceInfo[]{a2dpDeviceInfo});
+ when(mAudioManager.getAvailableCommunicationDevices()).thenReturn(List.of(a2dpDeviceInfo));
+
+ mTestCallStateListener.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK);
+
+ verify(mNotificationManager, never()).notify(
+ eq(SystemMessageProto.SystemMessage.NOTE_HEARING_DEVICE_INPUT_SWITCH), any());
+ }
+
+ @Test
+ public void onCallStateChanged_stateOffHookThenIdle_hapDeviceInfo_cancelNotification() {
+ AudioDeviceInfo hapDeviceInfo = createAudioDeviceInfo(TEST_ADDRESS,
+ AudioManager.DEVICE_OUT_BLE_HEADSET);
+ when(mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(
+ new AudioDeviceInfo[]{hapDeviceInfo});
+ when(mAudioManager.getAvailableCommunicationDevices()).thenReturn(List.of(hapDeviceInfo));
+
+ mTestCallStateListener.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK);
+ mTestCallStateListener.onCallStateChanged(TelephonyManager.CALL_STATE_IDLE);
+
+ verify(mNotificationManager).cancel(
+ eq(SystemMessageProto.SystemMessage.NOTE_HEARING_DEVICE_INPUT_SWITCH));
+ }
+
+ private AudioDeviceInfo createAudioDeviceInfo(String address, int type) {
+ AudioDevicePort audioDevicePort = mock(AudioDevicePort.class);
+ doReturn(type).when(audioDevicePort).type();
+ doReturn(address).when(audioDevicePort).address();
+ doReturn("testDevice").when(audioDevicePort).name();
+
+ return new AudioDeviceInfo(audioDevicePort);
+ }
+
+ /**
+ * For easier testing for CallStateListener, override methods that contain final object.
+ */
+ private static class TestCallStateListener extends
+ HearingDevicePhoneCallNotificationController.CallStateListener {
+
+ TestCallStateListener(@NonNull Context context) {
+ super(context);
+ }
+
+ @Override
+ boolean isHapClientSupported() {
+ return true;
+ }
+
+ @Override
+ boolean isHapClientDevice(BluetoothAdapter bluetoothAdapter, AudioDeviceInfo info) {
+ return TEST_ADDRESS.equals(info.getAddress());
+ }
+ }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index 8e79514c875e..8cc233b7594b 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -164,6 +164,7 @@ import com.android.os.AtomsProto.PackageNotificationChannelPreferences;
import com.android.os.AtomsProto.PackageNotificationPreferences;
import com.android.server.UiServiceTestCase;
import com.android.server.notification.PermissionHelper.PackagePermission;
+import com.android.server.uri.UriGrantsManagerInternal;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
@@ -179,6 +180,9 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
@@ -199,9 +203,6 @@ import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadLocalRandom;
-import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
-import platform.test.runner.parameterized.Parameters;
-
@SmallTest
@RunWith(ParameterizedAndroidJunit4.class)
@EnableFlags(FLAG_PERSIST_INCOMPLETE_RESTORE_DATA)
@@ -239,9 +240,10 @@ public class PreferencesHelperTest extends UiServiceTestCase {
private NotificationManager.Policy mTestNotificationPolicy;
- private PreferencesHelper mHelper;
- // fresh object for testing xml reading
- private PreferencesHelper mXmlHelper;
+ private TestPreferencesHelper mHelper;
+ // fresh object for testing xml reading; also TestPreferenceHelper in order to avoid interacting
+ // with real IpcDataCaches
+ private TestPreferencesHelper mXmlHelper;
private AudioAttributes mAudioAttributes;
private NotificationChannelLoggerFake mLogger = new NotificationChannelLoggerFake();
@@ -378,10 +380,10 @@ public class PreferencesHelperTest extends UiServiceTestCase {
when(mUserProfiles.getCurrentProfileIds()).thenReturn(currentProfileIds);
when(mClock.millis()).thenReturn(System.currentTimeMillis());
- mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
+ mHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
mUgmInternal, false, mClock);
- mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
+ mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
mUgmInternal, false, mClock);
resetZenModeHelper();
@@ -793,7 +795,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
@Test
public void testReadXml_oldXml_migrates() throws Exception {
- mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
+ mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock);
@@ -929,7 +931,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
@Test
public void testReadXml_newXml_noMigration_showPermissionNotification() throws Exception {
- mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
+ mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock);
@@ -988,7 +990,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
@Test
public void testReadXml_newXml_permissionNotificationOff() throws Exception {
- mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
+ mHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
mUgmInternal, /* showReviewPermissionsNotification= */ false, mClock);
@@ -1047,7 +1049,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
@Test
public void testReadXml_newXml_noMigration_noPermissionNotification() throws Exception {
- mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
+ mHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock);
@@ -1641,7 +1643,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
serializer.flush();
// simulate load after reboot
- mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
+ mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
mUgmInternal, false, mClock);
loadByteArrayXml(baos.toByteArray(), false, USER_ALL);
@@ -1696,7 +1698,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
Duration.ofDays(2).toMillis() + System.currentTimeMillis());
// simulate load after reboot
- mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
+ mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
mUgmInternal, false, mClock);
loadByteArrayXml(xml.getBytes(), false, USER_ALL);
@@ -1774,10 +1776,10 @@ public class PreferencesHelperTest extends UiServiceTestCase {
when(contentResolver.getResourceId(ANDROID_RES_SOUND_URI)).thenReturn(resId).thenThrow(
new FileNotFoundException("")).thenReturn(resId);
- mHelper = new PreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper,
+ mHelper = new TestPreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
mUgmInternal, false, mClock);
- mXmlHelper = new PreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper,
+ mXmlHelper = new TestPreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
mUgmInternal, false, mClock);
@@ -6573,4 +6575,223 @@ public class PreferencesHelperTest extends UiServiceTestCase {
mHelper.setCanBePromoted(PKG_P, UID_P, false, false);
assertThat(mHelper.canBePromoted(PKG_P, UID_P)).isTrue();
}
+
+ @Test
+ @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+ public void testInvalidateChannelCache_invalidateOnCreationAndChange() {
+ mHelper.resetCacheInvalidation();
+ NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_DEFAULT);
+ mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1,
+ false);
+
+ // new channel should invalidate the cache.
+ assertThat(mHelper.hasCacheBeenInvalidated()).isTrue();
+
+ // when the channel data is updated, should invalidate the cache again after that.
+ mHelper.resetCacheInvalidation();
+ NotificationChannel newChannel = channel.copy();
+ newChannel.setName("new name");
+ newChannel.setImportance(IMPORTANCE_HIGH);
+ mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, newChannel, true, UID_N_MR1, false);
+ assertThat(mHelper.hasCacheBeenInvalidated()).isTrue();
+
+ // also for conversations
+ mHelper.resetCacheInvalidation();
+ String parentId = "id";
+ String convId = "conversation";
+ NotificationChannel conv = new NotificationChannel(
+ String.format(CONVERSATION_CHANNEL_ID_FORMAT, parentId, convId), "conversation",
+ IMPORTANCE_DEFAULT);
+ conv.setConversationId(parentId, convId);
+ mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, conv, true, false, UID_N_MR1,
+ false);
+ assertThat(mHelper.hasCacheBeenInvalidated()).isTrue();
+
+ mHelper.resetCacheInvalidation();
+ NotificationChannel newConv = conv.copy();
+ newConv.setName("changed");
+ mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, newConv, true, UID_N_MR1, false);
+ assertThat(mHelper.hasCacheBeenInvalidated()).isTrue();
+ }
+
+ @Test
+ @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+ public void testInvalidateChannelCache_invalidateOnDelete() {
+ NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_DEFAULT);
+ mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1,
+ false);
+
+ // ignore any invalidations up until now
+ mHelper.resetCacheInvalidation();
+
+ mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, "id", UID_N_MR1, false);
+ assertThat(mHelper.hasCacheBeenInvalidated()).isTrue();
+
+ // recreate channel and now permanently delete
+ mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1,
+ false);
+ mHelper.resetCacheInvalidation();
+ mHelper.permanentlyDeleteNotificationChannel(PKG_N_MR1, UID_N_MR1, "id");
+ assertThat(mHelper.hasCacheBeenInvalidated()).isTrue();
+ }
+
+ @Test
+ @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+ public void testInvalidateChannelCache_noInvalidationWhenNoChange() {
+ NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_DEFAULT);
+ mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1,
+ false);
+
+ // ignore any invalidations up until now
+ mHelper.resetCacheInvalidation();
+
+ // newChannel, same as the old channel
+ NotificationChannel newChannel = channel.copy();
+ mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, newChannel, true, false, UID_N_MR1,
+ false);
+ mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, newChannel, true, UID_N_MR1, false);
+
+ // because there were no effective changes, we should not see any cache invalidations
+ assertThat(mHelper.hasCacheBeenInvalidated()).isFalse();
+
+ // deletions of a nonexistent channel also don't change anything
+ mHelper.resetCacheInvalidation();
+ mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, "nonexistent", UID_N_MR1, false);
+ assertThat(mHelper.hasCacheBeenInvalidated()).isFalse();
+ }
+
+ @Test
+ @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+ public void testInvalidateCache_multipleUsersAndPackages() {
+ // Setup: create channels for:
+ // pkg O, user
+ // pkg O, work (same channel ID, different user)
+ // pkg N_MR1, user
+ // pkg N_MR1, user, conversation child of above
+ String p2u1ConvId = String.format(CONVERSATION_CHANNEL_ID_FORMAT, "p2", "conv");
+ NotificationChannel p1u1 = new NotificationChannel("p1", "p1u1", IMPORTANCE_DEFAULT);
+ NotificationChannel p1u2 = new NotificationChannel("p1", "p1u2", IMPORTANCE_DEFAULT);
+ NotificationChannel p2u1 = new NotificationChannel("p2", "p2u1", IMPORTANCE_DEFAULT);
+ NotificationChannel p2u1Conv = new NotificationChannel(p2u1ConvId, "p2u1 conv",
+ IMPORTANCE_DEFAULT);
+ p2u1Conv.setConversationId("p2", "conv");
+
+ mHelper.createNotificationChannel(PKG_O, UID_O, p1u1, true,
+ false, UID_O, false);
+ mHelper.createNotificationChannel(PKG_O, UID_O + UserHandle.PER_USER_RANGE, p1u2, true,
+ false, UID_O + UserHandle.PER_USER_RANGE, false);
+ mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, p2u1, true,
+ false, UID_N_MR1, false);
+ mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, p2u1Conv, true,
+ false, UID_N_MR1, false);
+ mHelper.resetCacheInvalidation();
+
+ // Update to an existent channel, with a change: should invalidate
+ NotificationChannel p1u1New = p1u1.copy();
+ p1u1New.setName("p1u1 new");
+ mHelper.updateNotificationChannel(PKG_O, UID_O, p1u1New, true, UID_O, false);
+ assertThat(mHelper.hasCacheBeenInvalidated()).isTrue();
+
+ // Do it again, but no change for this user
+ mHelper.resetCacheInvalidation();
+ mHelper.updateNotificationChannel(PKG_O, UID_O, p1u1New.copy(), true, UID_O, false);
+ assertThat(mHelper.hasCacheBeenInvalidated()).isFalse();
+
+ // Delete conversations, but for a package without those conversations
+ mHelper.resetCacheInvalidation();
+ mHelper.deleteConversations(PKG_O, UID_O, Set.of(p2u1Conv.getConversationId()), UID_O,
+ false);
+ assertThat(mHelper.hasCacheBeenInvalidated()).isFalse();
+
+ // Now delete conversations for the right package
+ mHelper.resetCacheInvalidation();
+ mHelper.deleteConversations(PKG_N_MR1, UID_N_MR1, Set.of(p2u1Conv.getConversationId()),
+ UID_N_MR1, false);
+ assertThat(mHelper.hasCacheBeenInvalidated()).isTrue();
+ }
+
+ @Test
+ @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+ public void testInvalidateCache_userRemoved() throws Exception {
+ NotificationChannel c1 = new NotificationChannel("id1", "name1", IMPORTANCE_DEFAULT);
+ int uid1 = UserHandle.getUid(1, 1);
+ setUpPackageWithUid("pkg1", uid1);
+ mHelper.createNotificationChannel("pkg1", uid1, c1, true, false, uid1, false);
+ mHelper.resetCacheInvalidation();
+
+ // delete user 1; should invalidate cache
+ mHelper.onUserRemoved(1);
+ assertThat(mHelper.hasCacheBeenInvalidated()).isTrue();
+ }
+
+ @Test
+ @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+ public void testInvalidateCache_packagesChanged() {
+ NotificationChannel channel1 =
+ new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH);
+ mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false,
+ UID_N_MR1, false);
+
+ // package deleted: expect cache invalidation
+ mHelper.resetCacheInvalidation();
+ mHelper.onPackagesChanged(true, USER_SYSTEM, new String[]{PKG_N_MR1},
+ new int[]{UID_N_MR1});
+ assertThat(mHelper.hasCacheBeenInvalidated()).isTrue();
+
+ // re-created: expect cache invalidation again
+ mHelper.resetCacheInvalidation();
+ mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false,
+ UID_N_MR1, false);
+ mHelper.onPackagesChanged(false, USER_SYSTEM, new String[]{PKG_N_MR1},
+ new int[]{UID_N_MR1});
+ assertThat(mHelper.hasCacheBeenInvalidated()).isTrue();
+ }
+
+ @Test
+ @DisableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+ public void testInvalidateCache_flagOff_neverTouchesCache() {
+ // Do a bunch of channel-changing operations.
+ NotificationChannel channel =
+ new NotificationChannel("id", "name1", NotificationManager.IMPORTANCE_HIGH);
+ mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false,
+ UID_N_MR1, false);
+
+ NotificationChannel copy = channel.copy();
+ copy.setName("name2");
+ mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, copy, true, UID_N_MR1, false);
+ mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, "id", UID_N_MR1, false);
+
+ assertThat(mHelper.hasCacheBeenInvalidated()).isFalse();
+ }
+
+ // Test version of PreferencesHelper whose only functional difference is that it does not
+ // interact with the real IpcDataCache, and instead tracks whether or not the cache has been
+ // invalidated since creation or the last reset.
+ private static class TestPreferencesHelper extends PreferencesHelper {
+ private boolean mCacheInvalidated = false;
+
+ TestPreferencesHelper(Context context, PackageManager pm, RankingHandler rankingHandler,
+ ZenModeHelper zenHelper, PermissionHelper permHelper, PermissionManager permManager,
+ NotificationChannelLogger notificationChannelLogger,
+ AppOpsManager appOpsManager, ManagedServices.UserProfiles userProfiles,
+ UriGrantsManagerInternal ugmInternal,
+ boolean showReviewPermissionsNotification, Clock clock) {
+ super(context, pm, rankingHandler, zenHelper, permHelper, permManager,
+ notificationChannelLogger, appOpsManager, userProfiles, ugmInternal,
+ showReviewPermissionsNotification, clock);
+ }
+
+ @Override
+ protected void invalidateNotificationChannelCache() {
+ mCacheInvalidated = true;
+ }
+
+ boolean hasCacheBeenInvalidated() {
+ return mCacheInvalidated;
+ }
+
+ void resetCacheInvalidation() {
+ mCacheInvalidated = false;
+ }
+ }
}
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/BasicToPwleSegmentAdapterTest.java b/services/tests/vibrator/src/com/android/server/vibrator/BasicToPwleSegmentAdapterTest.java
new file mode 100644
index 000000000000..09f573cd1ee0
--- /dev/null
+++ b/services/tests/vibrator/src/com/android/server/vibrator/BasicToPwleSegmentAdapterTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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.vibrator;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.hardware.vibrator.IVibrator;
+import android.os.VibratorInfo;
+import android.os.vibrator.BasicPwleSegment;
+import android.os.vibrator.Flags;
+import android.os.vibrator.PwleSegment;
+import android.os.vibrator.StepSegment;
+import android.os.vibrator.VibrationEffectSegment;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.IntStream;
+
+public class BasicToPwleSegmentAdapterTest {
+
+ private static final float TEST_RESONANT_FREQUENCY = 150;
+ private static final float[] TEST_FREQUENCIES =
+ new float[]{90f, 120f, 150f, 60f, 30f, 210f, 270f, 300f, 240f, 180f};
+ private static final float[] TEST_OUTPUT_ACCELERATIONS =
+ new float[]{1.2f, 1.8f, 2.4f, 0.6f, 0.1f, 2.2f, 1.0f, 0.5f, 1.9f, 3.0f};
+
+ private static final VibratorInfo.FrequencyProfile TEST_FREQUENCY_PROFILE =
+ new VibratorInfo.FrequencyProfile(TEST_RESONANT_FREQUENCY, TEST_FREQUENCIES,
+ TEST_OUTPUT_ACCELERATIONS);
+
+ private static final VibratorInfo.FrequencyProfile EMPTY_FREQUENCY_PROFILE =
+ new VibratorInfo.FrequencyProfile(TEST_RESONANT_FREQUENCY, null, null);
+
+ private BasicToPwleSegmentAdapter mAdapter;
+
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+ @Before
+ public void setUp() throws Exception {
+ mAdapter = new BasicToPwleSegmentAdapter();
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testBasicPwleSegments_withFeatureFlagDisabled_returnsOriginalSegments() {
+ List<VibrationEffectSegment> segments = new ArrayList<>(Arrays.asList(
+ // startIntensity, endIntensity, startSharpness, endSharpness, duration
+ new BasicPwleSegment(0.2f, 0.8f, 0.2f, 0.4f, 20),
+ new BasicPwleSegment(0.8f, 0.2f, 0.4f, 0.5f, 100),
+ new BasicPwleSegment(0.2f, 0.65f, 0.5f, 0.5f, 50)));
+ List<VibrationEffectSegment> originalSegments = new ArrayList<>(segments);
+
+ VibratorInfo vibratorInfo = createVibratorInfo(
+ TEST_FREQUENCY_PROFILE, IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2);
+
+ assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ -1))
+ .isEqualTo(-1);
+ assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ 1))
+ .isEqualTo(1);
+
+ assertThat(segments).isEqualTo(originalSegments);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testBasicPwleSegments_noPwleCapability_returnsOriginalSegments() {
+ List<VibrationEffectSegment> segments = new ArrayList<>(Arrays.asList(
+ // startIntensity, endIntensity, startSharpness, endSharpness, duration
+ new BasicPwleSegment(0.2f, 0.8f, 0.2f, 0.4f, 20),
+ new BasicPwleSegment(0.8f, 0.2f, 0.4f, 0.5f, 100),
+ new BasicPwleSegment(0.2f, 0.65f, 0.5f, 0.5f, 50)));
+ List<VibrationEffectSegment> originalSegments = new ArrayList<>(segments);
+
+ VibratorInfo vibratorInfo = createVibratorInfo(TEST_FREQUENCY_PROFILE);
+
+ assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ -1))
+ .isEqualTo(-1);
+ assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ 1))
+ .isEqualTo(1);
+
+ assertThat(segments).isEqualTo(originalSegments);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testBasicPwleSegments_invalidFrequencyProfile_returnsOriginalSegments() {
+ List<VibrationEffectSegment> segments = new ArrayList<>(Arrays.asList(
+ // startIntensity, endIntensity, startSharpness, endSharpness, duration
+ new BasicPwleSegment(0.2f, 0.8f, 0.2f, 0.4f, 20),
+ new BasicPwleSegment(0.8f, 0.2f, 0.4f, 0.5f, 100),
+ new BasicPwleSegment(0.2f, 0.65f, 0.5f, 0.5f, 50)));
+ List<VibrationEffectSegment> originalSegments = new ArrayList<>(segments);
+ VibratorInfo vibratorInfo = createVibratorInfo(
+ EMPTY_FREQUENCY_PROFILE, IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2);
+
+ assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ -1))
+ .isEqualTo(-1);
+ assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ 1))
+ .isEqualTo(1);
+
+ assertThat(segments).isEqualTo(originalSegments);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testBasicPwleSegments_withPwleCapability_adaptSegmentsCorrectly() {
+ List<VibrationEffectSegment> segments = new ArrayList<>(Arrays.asList(
+ new StepSegment(/* amplitude= */ 1, /* frequencyHz= */ 40f, /* duration= */ 100),
+ // startIntensity, endIntensity, startSharpness, endSharpness, duration
+ new BasicPwleSegment(0.0f, 1.0f, 0.0f, 1.0f, 100),
+ new BasicPwleSegment(0.0f, 1.0f, 0.0f, 1.0f, 100),
+ new BasicPwleSegment(0.0f, 1.0f, 0.0f, 1.0f, 100)));
+ List<VibrationEffectSegment> expectedSegments = Arrays.asList(
+ new StepSegment(/* amplitude= */ 1, /* frequencyHz= */ 40f, /* duration= */ 100),
+ // startAmplitude, endAmplitude, startFrequencyHz, endFrequencyHz, duration
+ new PwleSegment(0.0f, 1.0f, 30.0f, 300.0f, 100),
+ new PwleSegment(0.0f, 1.0f, 30.0f, 300.0f, 100),
+ new PwleSegment(0.0f, 1.0f, 30.0f, 300.0f, 100));
+ VibratorInfo vibratorInfo = createVibratorInfo(
+ TEST_FREQUENCY_PROFILE, IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2);
+
+ assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ 1))
+ .isEqualTo(1);
+
+ assertThat(segments).isEqualTo(expectedSegments);
+ }
+
+ private static VibratorInfo createVibratorInfo(VibratorInfo.FrequencyProfile frequencyProfile,
+ int... capabilities) {
+ return new VibratorInfo.Builder(0)
+ .setCapabilities(IntStream.of(capabilities).reduce((a, b) -> a | b).orElse(0))
+ .setFrequencyProfile(frequencyProfile)
+ .build();
+ }
+}