diff options
3 files changed, 359 insertions, 1 deletions
diff --git a/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewController.java b/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewController.java index 1738091d14c9..1eead62c042a 100644 --- a/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewController.java +++ b/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewController.java @@ -48,17 +48,21 @@ import com.android.systemui.car.CarServiceProvider; import com.android.systemui.car.window.OverlayPanelViewController; import com.android.systemui.car.window.OverlayViewGlobalStateController; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.dagger.qualifiers.UiBackground; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.FlingAnimationUtils; import com.android.systemui.statusbar.StatusBarState; +import java.util.concurrent.Executor; + import javax.inject.Inject; import javax.inject.Singleton; /** View controller for the notification panel. */ @Singleton -public class NotificationPanelViewController extends OverlayPanelViewController { +public class NotificationPanelViewController extends OverlayPanelViewController + implements CommandQueue.Callbacks { private static final boolean DEBUG = true; private static final String TAG = "NotificationPanelViewController"; @@ -68,12 +72,14 @@ public class NotificationPanelViewController extends OverlayPanelViewController private final CarServiceProvider mCarServiceProvider; private final IStatusBarService mBarService; private final CommandQueue mCommandQueue; + private final Executor mUiBgExecutor; private final NotificationDataManager mNotificationDataManager; private final CarUxRestrictionManagerWrapper mCarUxRestrictionManagerWrapper; private final CarNotificationListener mCarNotificationListener; private final NotificationClickHandlerFactory mNotificationClickHandlerFactory; private final StatusBarStateController mStatusBarStateController; private final boolean mEnableHeadsUpNotificationWhenNotificationShadeOpen; + private final NotificationVisibilityLogger mNotificationVisibilityLogger; private float mInitialBackgroundAlpha; private float mBackgroundAlphaDiff; @@ -98,6 +104,7 @@ public class NotificationPanelViewController extends OverlayPanelViewController @Main Resources resources, OverlayViewGlobalStateController overlayViewGlobalStateController, FlingAnimationUtils.Builder flingAnimationUtilsBuilder, + @UiBackground Executor uiBgExecutor, /* Other things */ CarServiceProvider carServiceProvider, @@ -110,6 +117,7 @@ public class NotificationPanelViewController extends OverlayPanelViewController CarUxRestrictionManagerWrapper carUxRestrictionManagerWrapper, CarNotificationListener carNotificationListener, NotificationClickHandlerFactory notificationClickHandlerFactory, + NotificationVisibilityLogger notificationVisibilityLogger, /* Things that need to be replaced */ StatusBarStateController statusBarStateController @@ -121,12 +129,15 @@ public class NotificationPanelViewController extends OverlayPanelViewController mCarServiceProvider = carServiceProvider; mBarService = barService; mCommandQueue = commandQueue; + mUiBgExecutor = uiBgExecutor; mNotificationDataManager = notificationDataManager; mCarUxRestrictionManagerWrapper = carUxRestrictionManagerWrapper; mCarNotificationListener = carNotificationListener; mNotificationClickHandlerFactory = notificationClickHandlerFactory; mStatusBarStateController = statusBarStateController; + mNotificationVisibilityLogger = notificationVisibilityLogger; + mCommandQueue.addCallback(this); // Notification background setup. mInitialBackgroundAlpha = (float) mResources.getInteger( R.integer.config_initialNotificationBackgroundAlpha) / 100; @@ -151,12 +162,36 @@ public class NotificationPanelViewController extends OverlayPanelViewController .config_enableHeadsUpNotificationWhenNotificationShadeOpen); } + // CommandQueue.Callbacks + + @Override + public void animateExpandNotificationsPanel() { + if (!isPanelExpanded()) { + toggle(); + } + } + + @Override + public void animateCollapsePanels(int flags, boolean force) { + if (isPanelExpanded()) { + toggle(); + } + } + + // OverlayViewController + @Override protected void onFinishInflate() { reinflate(); } @Override + protected void hideInternal() { + super.hideInternal(); + mNotificationVisibilityLogger.stop(); + } + + @Override protected boolean shouldShowNavigationBar() { return true; } @@ -197,6 +232,11 @@ public class NotificationPanelViewController extends OverlayPanelViewController mUnseenCountUpdateListener.onUnseenCountUpdate( mNotificationDataManager.getUnseenNotificationCount()); } + mCarNotificationListener.setNotificationsShown( + mNotificationDataManager.getSeenNotifications()); + // This logs both when the notification panel is expanded and when the notification + // panel is scrolled. + mNotificationVisibilityLogger.log(isPanelExpanded()); }); mNotificationClickHandlerFactory.setNotificationDataManager(mNotificationDataManager); @@ -332,6 +372,8 @@ public class NotificationPanelViewController extends OverlayPanelViewController mNotificationDataManager.clearAll(); } + // OverlayPanelViewController + @Override protected boolean shouldAnimateCollapsePanel() { return true; @@ -364,6 +406,30 @@ public class NotificationPanelViewController extends OverlayPanelViewController } @Override + protected void onPanelVisible(boolean visible) { + super.onPanelVisible(visible); + mUiBgExecutor.execute(() -> { + try { + if (visible) { + // When notification panel is open even just a bit, we want to clear + // notification effects. + boolean clearNotificationEffects = + mStatusBarStateController.getState() != StatusBarState.KEYGUARD; + mBarService.onPanelRevealed(clearNotificationEffects, + mNotificationDataManager.getVisibleNotifications().size()); + } else { + mBarService.onPanelHidden(); + } + } catch (RemoteException ex) { + // Won't fail unless the world has ended. + Log.e(TAG, String.format( + "Unable to notify StatusBarService of panel visibility: %s", visible)); + } + }); + + } + + @Override protected void onPanelExpanded(boolean expand) { super.onPanelExpanded(expand); @@ -373,6 +439,9 @@ public class NotificationPanelViewController extends OverlayPanelViewController } clearNotificationEffects(); } + if (!expand) { + mNotificationVisibilityLogger.log(isPanelExpanded()); + } } /** diff --git a/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationVisibilityLogger.java b/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationVisibilityLogger.java new file mode 100644 index 000000000000..44c819711bd2 --- /dev/null +++ b/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationVisibilityLogger.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2020 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.car.notification; + +import android.os.RemoteException; +import android.util.ArraySet; +import android.util.Log; + +import com.android.car.notification.AlertEntry; +import com.android.car.notification.NotificationDataManager; +import com.android.internal.statusbar.IStatusBarService; +import com.android.internal.statusbar.NotificationVisibility; +import com.android.systemui.dagger.qualifiers.UiBackground; + +import java.util.Set; +import java.util.concurrent.Executor; + +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * Handles notification logging, in particular, logging which notifications are visible and which + * are not. + */ +@Singleton +public class NotificationVisibilityLogger { + + private static final String TAG = "NotificationVisibilityLogger"; + + private final ArraySet<NotificationVisibility> mCurrentlyVisible = new ArraySet<>(); + private final ArraySet<NotificationVisibility> mNewlyVisible = new ArraySet<>(); + private final ArraySet<NotificationVisibility> mPreviouslyVisible = new ArraySet<>(); + private final ArraySet<NotificationVisibility> mTmpCurrentlyVisible = new ArraySet<>(); + + private final IStatusBarService mBarService; + private final Executor mUiBgExecutor; + private final NotificationDataManager mNotificationDataManager; + + private boolean mIsVisible; + + private final Runnable mVisibilityReporter = new Runnable() { + + @Override + public void run() { + if (mIsVisible) { + int count = mNotificationDataManager.getVisibleNotifications().size(); + for (AlertEntry alertEntry : mNotificationDataManager.getVisibleNotifications()) { + NotificationVisibility visObj = NotificationVisibility.obtain( + alertEntry.getKey(), + /* rank= */ -1, + count, + mIsVisible, + NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA); + mTmpCurrentlyVisible.add(visObj); + if (!mCurrentlyVisible.contains(visObj)) { + mNewlyVisible.add(visObj); + } + } + } + mPreviouslyVisible.addAll(mCurrentlyVisible); + mPreviouslyVisible.removeAll(mTmpCurrentlyVisible); + onNotificationVisibilityChanged(mNewlyVisible, mPreviouslyVisible); + + recycleAllVisibilityObjects(mCurrentlyVisible); + mCurrentlyVisible.addAll(mTmpCurrentlyVisible); + + recycleAllVisibilityObjects(mPreviouslyVisible); + recycleAllVisibilityObjects(mNewlyVisible); + recycleAllVisibilityObjects(mTmpCurrentlyVisible); + } + }; + + @Inject + public NotificationVisibilityLogger( + @UiBackground Executor uiBgExecutor, + IStatusBarService barService, + NotificationDataManager notificationDataManager) { + mUiBgExecutor = uiBgExecutor; + mBarService = barService; + mNotificationDataManager = notificationDataManager; + } + + /** Triggers a visibility report update to be sent to StatusBarService. */ + public void log(boolean isVisible) { + mIsVisible = isVisible; + mUiBgExecutor.execute(mVisibilityReporter); + } + + /** Stops logging, clearing all visibility objects. */ + public void stop() { + recycleAllVisibilityObjects(mCurrentlyVisible); + } + + /** + * Notify StatusBarService of change in notifications' visibility. + */ + private void onNotificationVisibilityChanged( + Set<NotificationVisibility> newlyVisible, Set<NotificationVisibility> noLongerVisible) { + if (newlyVisible.isEmpty() && noLongerVisible.isEmpty()) { + return; + } + + try { + mBarService.onNotificationVisibilityChanged( + cloneVisibilitiesAsArr(newlyVisible), cloneVisibilitiesAsArr(noLongerVisible)); + } catch (RemoteException e) { + // Won't fail unless the world has ended. + Log.e(TAG, "Failed to notify StatusBarService of notification visibility change"); + } + } + + /** + * Clears array and recycles NotificationVisibility objects for reuse. + */ + private static void recycleAllVisibilityObjects(ArraySet<NotificationVisibility> array) { + for (int i = 0; i < array.size(); i++) { + array.valueAt(i).recycle(); + } + array.clear(); + } + + /** + * Converts Set of NotificationVisibility objects to primitive array. + */ + private static NotificationVisibility[] cloneVisibilitiesAsArr(Set<NotificationVisibility> c) { + NotificationVisibility[] array = new NotificationVisibility[c.size()]; + int i = 0; + for (NotificationVisibility nv : c) { + if (nv != null) { + array[i] = nv.clone(); + } + i++; + } + return array; + } +} diff --git a/packages/CarSystemUI/tests/src/com/android/systemui/car/notification/NotificationVisibilityLoggerTest.java b/packages/CarSystemUI/tests/src/com/android/systemui/car/notification/NotificationVisibilityLoggerTest.java new file mode 100644 index 000000000000..89dac58cd2a7 --- /dev/null +++ b/packages/CarSystemUI/tests/src/com/android/systemui/car/notification/NotificationVisibilityLoggerTest.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2020 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.car.notification; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Notification; +import android.os.RemoteException; +import android.os.UserHandle; +import android.service.notification.StatusBarNotification; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import androidx.test.filters.SmallTest; + +import com.android.car.notification.AlertEntry; +import com.android.car.notification.NotificationDataManager; +import com.android.internal.statusbar.IStatusBarService; +import com.android.internal.statusbar.NotificationVisibility; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.util.concurrency.FakeExecutor; +import com.android.systemui.util.time.FakeSystemClock; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Collections; + +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +@SmallTest +public class NotificationVisibilityLoggerTest extends SysuiTestCase { + + private static final String PKG = "package_1"; + private static final String OP_PKG = "OpPackage"; + private static final int ID = 1; + private static final String TAG = "Tag"; + private static final int UID = 2; + private static final int INITIAL_PID = 3; + private static final String CHANNEL_ID = "CHANNEL_ID"; + private static final String CONTENT_TITLE = "CONTENT_TITLE"; + private static final String OVERRIDE_GROUP_KEY = "OVERRIDE_GROUP_KEY"; + private static final long POST_TIME = 12345L; + private static final UserHandle USER_HANDLE = new UserHandle(12); + + @Mock + private IStatusBarService mBarService; + @Mock + private NotificationDataManager mNotificationDataManager; + + private NotificationVisibilityLogger mNotificationVisibilityLogger; + private FakeExecutor mUiBgExecutor; + private AlertEntry mMessageNotification; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(/* testClass= */this); + + mUiBgExecutor = new FakeExecutor(new FakeSystemClock()); + Notification.Builder mNotificationBuilder1 = new Notification.Builder(mContext, CHANNEL_ID) + .setContentTitle(CONTENT_TITLE); + mMessageNotification = new AlertEntry(new StatusBarNotification(PKG, OP_PKG, + ID, TAG, UID, INITIAL_PID, mNotificationBuilder1.build(), USER_HANDLE, + OVERRIDE_GROUP_KEY, POST_TIME)); + + when(mNotificationDataManager.getVisibleNotifications()).thenReturn( + Collections.singletonList(mMessageNotification)); + + mNotificationVisibilityLogger = new NotificationVisibilityLogger( + mUiBgExecutor, mBarService, mNotificationDataManager); + } + + @Test + public void log_notifiesStatusBarService() throws RemoteException { + mNotificationVisibilityLogger.log(/* isVisible= */ true); + mUiBgExecutor.runNextReady(); + + verify(mBarService).onNotificationVisibilityChanged( + any(NotificationVisibility[].class), any(NotificationVisibility[].class)); + } + + @Test + public void log_isVisibleIsTrue_notifiesOfNewlyVisibleItems() throws RemoteException { + ArgumentCaptor<NotificationVisibility[]> newlyVisibleCaptor = + ArgumentCaptor.forClass(NotificationVisibility[].class); + ArgumentCaptor<NotificationVisibility[]> previouslyVisibleCaptor = + ArgumentCaptor.forClass(NotificationVisibility[].class); + + mNotificationVisibilityLogger.log(/* isVisible= */ true); + mUiBgExecutor.runNextReady(); + + verify(mBarService).onNotificationVisibilityChanged( + newlyVisibleCaptor.capture(), previouslyVisibleCaptor.capture()); + assertThat(newlyVisibleCaptor.getValue().length).isEqualTo(1); + assertThat(previouslyVisibleCaptor.getValue().length).isEqualTo(0); + } + + @Test + public void log_isVisibleIsFalse_notifiesOfPreviouslyVisibleItems() throws RemoteException { + ArgumentCaptor<NotificationVisibility[]> newlyVisibleCaptor = + ArgumentCaptor.forClass(NotificationVisibility[].class); + ArgumentCaptor<NotificationVisibility[]> previouslyVisibleCaptor = + ArgumentCaptor.forClass(NotificationVisibility[].class); + mNotificationVisibilityLogger.log(/* isVisible= */ true); + mUiBgExecutor.runNextReady(); + reset(mBarService); + + mNotificationVisibilityLogger.log(/* isVisible= */ false); + mUiBgExecutor.runNextReady(); + + verify(mBarService).onNotificationVisibilityChanged( + newlyVisibleCaptor.capture(), previouslyVisibleCaptor.capture()); + assertThat(previouslyVisibleCaptor.getValue().length).isEqualTo(1); + assertThat(newlyVisibleCaptor.getValue().length).isEqualTo(0); + } +} |