Merge "Fix an ANR caused by the dream overlay status bar." into tm-dev

This commit is contained in:
William Leshner 2022-03-24 22:59:03 +00:00 committed by Android (Google) Code Review
commit 8ffe12bba6
4 changed files with 239 additions and 87 deletions

View File

@ -0,0 +1,116 @@
/*
* Copyright (C) 2022 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.dreams;
import android.annotation.NonNull;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.statusbar.NotificationListener;
import com.android.systemui.statusbar.NotificationListener.NotificationHandler;
import com.android.systemui.statusbar.policy.CallbackController;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.inject.Inject;
/***
* {@link DreamOverlayNotificationCountProvider} provides the current notification count to
* registered callbacks.
*/
@SysUISingleton
public class DreamOverlayNotificationCountProvider
implements CallbackController<DreamOverlayNotificationCountProvider.Callback> {
private final Set<String> mNotificationKeys = new HashSet<>();
private final List<Callback> mCallbacks = new ArrayList<>();
private final NotificationHandler mNotificationHandler = new NotificationHandler() {
@Override
public void onNotificationPosted(
StatusBarNotification sbn, NotificationListenerService.RankingMap rankingMap) {
mNotificationKeys.add(sbn.getKey());
reportNotificationCountChanged();
}
@Override
public void onNotificationRemoved(
StatusBarNotification sbn, NotificationListenerService.RankingMap rankingMap) {
mNotificationKeys.remove(sbn.getKey());
reportNotificationCountChanged();
}
@Override
public void onNotificationRemoved(
StatusBarNotification sbn,
NotificationListenerService.RankingMap rankingMap,
int reason) {
mNotificationKeys.remove(sbn.getKey());
reportNotificationCountChanged();
}
@Override
public void onNotificationRankingUpdate(NotificationListenerService.RankingMap rankingMap) {
}
@Override
public void onNotificationsInitialized() {
}
};
@Inject
public DreamOverlayNotificationCountProvider(
NotificationListener notificationListener) {
notificationListener.addNotificationHandler(mNotificationHandler);
Arrays.stream(notificationListener.getActiveNotifications())
.forEach(sbn -> mNotificationKeys.add(sbn.getKey()));
}
@Override
public void addCallback(@NonNull Callback callback) {
if (!mCallbacks.contains(callback)) {
mCallbacks.add(callback);
callback.onNotificationCountChanged(mNotificationKeys.size());
}
}
@Override
public void removeCallback(@NonNull Callback callback) {
mCallbacks.remove(callback);
}
private void reportNotificationCountChanged() {
final int notificationCount = mNotificationKeys.size();
mCallbacks.forEach(callback -> callback.onNotificationCountChanged(notificationCount));
}
/**
* A callback to be registered with {@link DreamOverlayNotificationCountProvider} to receive
* changes to the current notification count.
*/
public interface Callback {
/**
* Called when the notification count has changed.
* @param count The current notification count.
*/
void onNotificationCountChanged(int count);
}
}

View File

@ -27,16 +27,12 @@ import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.os.UserHandle;
import android.provider.Settings;
import android.service.notification.NotificationListenerService.RankingMap;
import android.service.notification.StatusBarNotification;
import android.text.format.DateFormat;
import android.util.PluralsMessageFormatter;
import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dreams.dagger.DreamOverlayComponent;
import com.android.systemui.statusbar.NotificationListener;
import com.android.systemui.statusbar.NotificationListener.NotificationHandler;
import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController;
import com.android.systemui.statusbar.policy.NextAlarmController;
import com.android.systemui.statusbar.policy.ZenModeController;
@ -62,7 +58,7 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve
private final Resources mResources;
private final DateFormatUtil mDateFormatUtil;
private final IndividualSensorPrivacyController mSensorPrivacyController;
private final NotificationListener mNotificationListener;
private final DreamOverlayNotificationCountProvider mDreamOverlayNotificationCountProvider;
private final ZenModeController mZenModeController;
private final Executor mMainExecutor;
@ -96,35 +92,6 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve
private final NextAlarmController.NextAlarmChangeCallback mNextAlarmCallback =
nextAlarm -> updateAlarmStatusIcon();
private final NotificationHandler mNotificationHandler = new NotificationHandler() {
@Override
public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
updateNotificationsStatusIcon();
}
@Override
public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap) {
updateNotificationsStatusIcon();
}
@Override
public void onNotificationRemoved(
StatusBarNotification sbn,
RankingMap rankingMap,
int reason) {
updateNotificationsStatusIcon();
}
@Override
public void onNotificationRankingUpdate(RankingMap rankingMap) {
}
@Override
public void onNotificationsInitialized() {
updateNotificationsStatusIcon();
}
};
private final ZenModeController.Callback mZenModeCallback = new ZenModeController.Callback() {
@Override
public void onZenChanged(int zen) {
@ -132,6 +99,14 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve
}
};
private final DreamOverlayNotificationCountProvider.Callback mNotificationCountCallback =
notificationCount -> showIcon(
DreamOverlayStatusBarView.STATUS_ICON_NOTIFICATIONS,
notificationCount > 0,
notificationCount > 0
? buildNotificationsContentDescription(notificationCount)
: null);
@Inject
public DreamOverlayStatusBarViewController(
DreamOverlayStatusBarView view,
@ -143,7 +118,7 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve
NextAlarmController nextAlarmController,
DateFormatUtil dateFormatUtil,
IndividualSensorPrivacyController sensorPrivacyController,
NotificationListener notificationListener,
DreamOverlayNotificationCountProvider dreamOverlayNotificationCountProvider,
ZenModeController zenModeController) {
super(view);
mResources = resources;
@ -154,20 +129,14 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve
mNextAlarmController = nextAlarmController;
mDateFormatUtil = dateFormatUtil;
mSensorPrivacyController = sensorPrivacyController;
mNotificationListener = notificationListener;
mDreamOverlayNotificationCountProvider = dreamOverlayNotificationCountProvider;
mZenModeController = zenModeController;
// Handlers can be added to NotificationListener, but apparently they can't be removed. So
// add the handler here in the constructor rather than in onViewAttached to avoid confusion.
mNotificationListener.addNotificationHandler(mNotificationHandler);
}
@Override
protected void onViewAttached() {
mIsAttached = true;
updateNotificationsStatusIcon();
mConnectivityManager.registerNetworkCallback(mNetworkRequest, mNetworkCallback);
updateWifiUnavailableStatusIcon();
@ -180,6 +149,7 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve
mZenModeController.addCallback(mZenModeCallback);
updatePriorityModeStatusIcon();
mDreamOverlayNotificationCountProvider.addCallback(mNotificationCountCallback);
mTouchInsetSession.addViewToTracking(mView);
}
@ -189,6 +159,7 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve
mSensorPrivacyController.removeCallback(mSensorCallback);
mNextAlarmController.removeCallback(mNextAlarmCallback);
mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
mDreamOverlayNotificationCountProvider.removeCallback(mNotificationCountCallback);
mTouchInsetSession.clear();
mIsAttached = false;
@ -231,24 +202,6 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve
micBlocked && cameraBlocked);
}
private void updateNotificationsStatusIcon() {
if (mView == null) {
// It is possible for this method to be called before the view is attached, which makes
// null-checking necessary.
return;
}
final StatusBarNotification[] notifications =
mNotificationListener.getActiveNotifications();
final int notificationCount = notifications != null ? notifications.length : 0;
showIcon(
DreamOverlayStatusBarView.STATUS_ICON_NOTIFICATIONS,
notificationCount > 0,
notificationCount > 0
? buildNotificationsContentDescription(notificationCount)
: null);
}
private String buildNotificationsContentDescription(int notificationCount) {
return PluralsMessageFormatter.format(
mResources,

View File

@ -0,0 +1,85 @@
/*
* Copyright (C) 2022 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.dreams;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.testing.AndroidTestingRunner;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.statusbar.NotificationListener;
import com.android.systemui.statusbar.NotificationListener.NotificationHandler;
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;
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class DreamOverlayNotificationCountProviderTest extends SysuiTestCase {
@Mock
NotificationListener mNotificationListener;
@Mock
DreamOverlayNotificationCountProvider.Callback mCallback;
@Mock
StatusBarNotification mNotification1;
@Mock
StatusBarNotification mNotification2;
@Mock
NotificationListenerService.RankingMap mRankingMap;
private DreamOverlayNotificationCountProvider mProvider;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
when(mNotification1.getKey()).thenReturn("key1");
when(mNotification2.getKey()).thenReturn("key2");
final StatusBarNotification[] notifications = {mNotification1};
when(mNotificationListener.getActiveNotifications()).thenReturn(notifications);
mProvider = new DreamOverlayNotificationCountProvider(mNotificationListener);
mProvider.addCallback(mCallback);
}
@Test
public void testPostingNotificationCallsCallbackWithNotificationCount() {
final ArgumentCaptor<NotificationHandler> handlerArgumentCaptor =
ArgumentCaptor.forClass(NotificationHandler.class);
verify(mNotificationListener).addNotificationHandler(handlerArgumentCaptor.capture());
handlerArgumentCaptor.getValue().onNotificationPosted(mNotification2, mRankingMap);
verify(mCallback).onNotificationCountChanged(2);
}
@Test
public void testRemovingNotificationCallsCallbackWithZeroNotificationCount() {
final ArgumentCaptor<NotificationHandler> handlerArgumentCaptor =
ArgumentCaptor.forClass(NotificationHandler.class);
verify(mNotificationListener).addNotificationHandler(handlerArgumentCaptor.capture());
handlerArgumentCaptor.getValue().onNotificationRemoved(mNotification1, mRankingMap);
verify(mCallback).onNotificationCountChanged(0);
}
}

View File

@ -31,15 +31,12 @@ import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.provider.Settings;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.testing.AndroidTestingRunner;
import androidx.test.filters.SmallTest;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.statusbar.NotificationListener;
import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController;
import com.android.systemui.statusbar.policy.NextAlarmController;
import com.android.systemui.statusbar.policy.ZenModeController;
@ -84,13 +81,9 @@ public class DreamOverlayStatusBarViewControllerTest extends SysuiTestCase {
@Mock
IndividualSensorPrivacyController mSensorPrivacyController;
@Mock
StatusBarNotification mStatusBarNotification;
@Mock
NotificationListenerService.RankingMap mRankingMap;
@Mock
NotificationListener mNotificationListener;
@Mock
ZenModeController mZenModeController;
@Mock
DreamOverlayNotificationCountProvider mDreamOverlayNotificationCountProvider;
private final Executor mMainExecutor = Runnable::run;
@ -113,7 +106,7 @@ public class DreamOverlayStatusBarViewControllerTest extends SysuiTestCase {
mNextAlarmController,
mDateFormatUtil,
mSensorPrivacyController,
mNotificationListener,
mDreamOverlayNotificationCountProvider,
mZenModeController);
}
@ -123,6 +116,7 @@ public class DreamOverlayStatusBarViewControllerTest extends SysuiTestCase {
verify(mNextAlarmController).addCallback(any());
verify(mSensorPrivacyController).addCallback(any());
verify(mZenModeController).addCallback(any());
verify(mDreamOverlayNotificationCountProvider).addCallback(any());
}
@Test
@ -202,17 +196,26 @@ public class DreamOverlayStatusBarViewControllerTest extends SysuiTestCase {
@Test
public void testOnViewAttachedShowsNotificationsIconWhenNotificationsExist() {
StatusBarNotification[] notifications = { mStatusBarNotification };
when(mNotificationListener.getActiveNotifications()).thenReturn(notifications);
mController.onViewAttached();
final ArgumentCaptor<DreamOverlayNotificationCountProvider.Callback> callbackCapture =
ArgumentCaptor.forClass(DreamOverlayNotificationCountProvider.Callback.class);
verify(mDreamOverlayNotificationCountProvider).addCallback(callbackCapture.capture());
callbackCapture.getValue().onNotificationCountChanged(1);
verify(mView).showIcon(
eq(DreamOverlayStatusBarView.STATUS_ICON_NOTIFICATIONS), eq(true), any());
}
@Test
public void testOnViewAttachedHidesNotificationsIconWhenNoNotificationsExist() {
when(mNotificationListener.getActiveNotifications()).thenReturn(null);
mController.onViewAttached();
final ArgumentCaptor<DreamOverlayNotificationCountProvider.Callback> callbackCapture =
ArgumentCaptor.forClass(DreamOverlayNotificationCountProvider.Callback.class);
verify(mDreamOverlayNotificationCountProvider).addCallback(callbackCapture.capture());
callbackCapture.getValue().onNotificationCountChanged(0);
verify(mView).showIcon(
eq(DreamOverlayStatusBarView.STATUS_ICON_NOTIFICATIONS), eq(false), isNull());
}
@ -248,6 +251,7 @@ public class DreamOverlayStatusBarViewControllerTest extends SysuiTestCase {
verify(mNextAlarmController).removeCallback(any());
verify(mSensorPrivacyController).removeCallback(any());
verify(mZenModeController).removeCallback(any());
verify(mDreamOverlayNotificationCountProvider).removeCallback(any());
}
@Test
@ -309,13 +313,10 @@ public class DreamOverlayStatusBarViewControllerTest extends SysuiTestCase {
public void testNotificationsIconShownWhenNotificationAdded() {
mController.onViewAttached();
StatusBarNotification[] notifications = { mStatusBarNotification };
when(mNotificationListener.getActiveNotifications()).thenReturn(notifications);
final ArgumentCaptor<NotificationListener.NotificationHandler> callbackCapture =
ArgumentCaptor.forClass(NotificationListener.NotificationHandler.class);
verify(mNotificationListener).addNotificationHandler(callbackCapture.capture());
callbackCapture.getValue().onNotificationPosted(mStatusBarNotification, mRankingMap);
final ArgumentCaptor<DreamOverlayNotificationCountProvider.Callback> callbackCapture =
ArgumentCaptor.forClass(DreamOverlayNotificationCountProvider.Callback.class);
verify(mDreamOverlayNotificationCountProvider).addCallback(callbackCapture.capture());
callbackCapture.getValue().onNotificationCountChanged(1);
verify(mView).showIcon(
eq(DreamOverlayStatusBarView.STATUS_ICON_NOTIFICATIONS), eq(true), any());
@ -323,15 +324,12 @@ public class DreamOverlayStatusBarViewControllerTest extends SysuiTestCase {
@Test
public void testNotificationsIconHiddenWhenLastNotificationRemoved() {
StatusBarNotification[] notifications = { mStatusBarNotification };
when(mNotificationListener.getActiveNotifications()).thenReturn(notifications)
.thenReturn(null);
mController.onViewAttached();
final ArgumentCaptor<NotificationListener.NotificationHandler> callbackCapture =
ArgumentCaptor.forClass(NotificationListener.NotificationHandler.class);
verify(mNotificationListener).addNotificationHandler(callbackCapture.capture());
callbackCapture.getValue().onNotificationPosted(mStatusBarNotification, mRankingMap);
final ArgumentCaptor<DreamOverlayNotificationCountProvider.Callback> callbackCapture =
ArgumentCaptor.forClass(DreamOverlayNotificationCountProvider.Callback.class);
verify(mDreamOverlayNotificationCountProvider).addCallback(callbackCapture.capture());
callbackCapture.getValue().onNotificationCountChanged(0);
verify(mView).showIcon(
eq(DreamOverlayStatusBarView.STATUS_ICON_NOTIFICATIONS), eq(false), any());