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