resolve merge conflicts of 88c651eab1 to oc-dr1-dev

Bug: 119041698
Test: atest SystemUITests
Change-Id: I9d1547f98cc19111ce2dc8ab2f4e2bf2d3cb0baa
Merged-In: I0680034ed9315aa2c05282524d48faaed066ebd0
This commit is contained in:
Evan Laird
2019-11-14 11:27:07 -05:00
7 changed files with 413 additions and 7 deletions

View File

@@ -0,0 +1,87 @@
/*
* Copyright (C) 2019 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;
import android.annotation.NonNull;
import android.app.Notification;
import android.os.Handler;
import android.os.Looper;
import android.util.ArraySet;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.statusbar.NotificationData;
/**
* Extends the lifetime of foreground notification services such that they show for at least
* five seconds
*/
public class ForegroundServiceLifetimeExtender implements NotificationLifetimeExtender {
private static final String TAG = "FGSLifetimeExtender";
@VisibleForTesting
public static final int MIN_FGS_TIME_MS = 5000;
private NotificationSafeToRemoveCallback mNotificationSafeToRemoveCallback;
private ArraySet<NotificationData.Entry> mManagedEntries = new ArraySet<>();
private Handler mHandler = new Handler(Looper.getMainLooper());
public ForegroundServiceLifetimeExtender() {
}
@Override
public void setCallback(@NonNull NotificationSafeToRemoveCallback callback) {
mNotificationSafeToRemoveCallback = callback;
}
@Override
public boolean shouldExtendLifetime(@NonNull NotificationData.Entry entry) {
if ((entry.notification.getNotification().flags
& Notification.FLAG_FOREGROUND_SERVICE) == 0) {
return false;
}
long currentTime = System.currentTimeMillis();
return currentTime - entry.notification.getPostTime() < MIN_FGS_TIME_MS;
}
@Override
public boolean shouldExtendLifetimeForPendingNotification(
@NonNull NotificationData.Entry entry) {
return shouldExtendLifetime(entry);
}
@Override
public void setShouldManageLifetime(
@NonNull NotificationData.Entry entry, boolean shouldManage) {
android.util.Log.d("FGSExtender", "setShouldManageLifetime " + shouldManage);
if (!shouldManage) {
mManagedEntries.remove(entry);
return;
}
mManagedEntries.add(entry);
Runnable r = () -> {
if (mManagedEntries.contains(entry)) {
mManagedEntries.remove(entry);
if (mNotificationSafeToRemoveCallback != null) {
mNotificationSafeToRemoveCallback.onSafeToRemove(entry.key);
}
}
};
long delayAmt = MIN_FGS_TIME_MS
- (System.currentTimeMillis() - entry.notification.getPostTime());
mHandler.postDelayed(r, delayAmt);
}
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright (C) 2019 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;
import android.annotation.NonNull;
import com.android.systemui.statusbar.NotificationData;
/**
* Interface for anything that may need to keep notifications managed even after
* {@link NotificationListener} removes it. The lifetime extender is in charge of performing the
* callback when the notification is then safe to remove.
*/
public interface NotificationLifetimeExtender {
/**
* Set the handler to callback to when the notification is safe to remove.
*
* @param callback the handler to callback
*/
void setCallback(@NonNull NotificationSafeToRemoveCallback callback);
/**
* Determines whether or not the extender needs the notification kept after removal.
*
* @param entry the entry containing the notification to check
* @return true if the notification lifetime should be extended
*/
boolean shouldExtendLifetime(@NonNull NotificationData.Entry entry);
/**
* It's possible that a notification was canceled before it ever became visible. This callback
* gives lifetime extenders a chance to make sure it shows up. For example if a foreground
* service is canceled too quickly but we still want to make sure a FGS notification shows.
* @param pendingEntry the canceled (but pending) entry
* @return true if the notification lifetime should be extended
*/
default boolean shouldExtendLifetimeForPendingNotification(
@NonNull NotificationData.Entry pendingEntry) {
return false;
}
/**
* Sets whether or not the lifetime should be managed by the extender. In practice, if
* shouldManage is true, this is where the extender starts managing the entry internally and is
* now responsible for calling {@link NotificationSafeToRemoveCallback#onSafeToRemove(String)}
* when the entry is safe to remove. If shouldManage is false, the extender no longer needs to
* worry about it (either because we will be removing it anyway or the entry is no longer
* removed due to an update).
*
* @param entry the entry that needs an extended lifetime
* @param shouldManage true if the extender should manage the entry now, false otherwise
*/
void setShouldManageLifetime(@NonNull NotificationData.Entry entry, boolean shouldManage);
/**
* The callback for when the notification is now safe to remove (i.e. its lifetime has ended).
*/
interface NotificationSafeToRemoveCallback {
/**
* Called when the lifetime extender determines it's safe to remove.
*
* @param key key of the entry that is now safe to remove
*/
void onSafeToRemove(String key);
}
}

View File

@@ -112,6 +112,7 @@ import android.service.vr.IVrManager;
import android.service.vr.IVrStateCallbacks;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.ArrayMap;
import android.util.DisplayMetrics;
import android.util.EventLog;
import android.util.Log;
@@ -206,6 +207,7 @@ import com.android.systemui.statusbar.DismissView;
import com.android.systemui.statusbar.DragDownHelper;
import com.android.systemui.statusbar.EmptyShadeView;
import com.android.systemui.statusbar.ExpandableNotificationRow;
import com.android.systemui.statusbar.ForegroundServiceLifetimeExtender;
import com.android.systemui.statusbar.GestureRecorder;
import com.android.systemui.statusbar.KeyboardShortcuts;
import com.android.systemui.statusbar.KeyguardIndicationController;
@@ -213,6 +215,7 @@ import com.android.systemui.statusbar.NotificationData;
import com.android.systemui.statusbar.NotificationData.Entry;
import com.android.systemui.statusbar.NotificationGuts;
import com.android.systemui.statusbar.NotificationInfo;
import com.android.systemui.statusbar.NotificationLifetimeExtender;
import com.android.systemui.statusbar.NotificationShelf;
import com.android.systemui.statusbar.NotificationSnooze;
import com.android.systemui.statusbar.RemoteInputController;
@@ -745,10 +748,11 @@ public class StatusBar extends SystemUI implements DemoMode,
@Nullable private View mAmbientIndicationContainer;
private String mKeyToRemoveOnGutsClosed;
private SysuiColorExtractor mColorExtractor;
private ForegroundServiceController mForegroundServiceController;
private ScreenLifecycle mScreenLifecycle;
@VisibleForTesting WakefulnessLifecycle mWakefulnessLifecycle;
protected ForegroundServiceController mForegroundServiceController;
private void recycleAllVisibilityObjects(ArraySet<NotificationVisibility> array) {
final int N = array.size();
for (int i = 0 ; i < N; i++) {
@@ -803,6 +807,9 @@ public class StatusBar extends SystemUI implements DemoMode,
mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
mForegroundServiceController = Dependency.get(ForegroundServiceController.class);
mFGSExtender = new ForegroundServiceLifetimeExtender();
mFGSExtender.setCallback(key -> removeNotification(key, mLatestRankingMap));
mDisplay = mWindowManager.getDefaultDisplay();
updateDisplaySize();
@@ -1803,6 +1810,11 @@ public class StatusBar extends SystemUI implements DemoMode,
}
Entry entry = mNotificationData.get(key);
if (entry != null && mFGSExtender.shouldExtendLifetime(entry)) {
extendLifetime(entry, mFGSExtender);
return;
}
if (entry != null && mRemoteInputController.isRemoteInputActive(entry)
&& (entry.row != null && !entry.row.isDismissed())) {
mLatestRankingMap = ranking;
@@ -1841,9 +1853,35 @@ public class StatusBar extends SystemUI implements DemoMode,
}
}
}
// Make sure no lifetime extension is happening anymore
cancelLifetimeExtension(entry);
setAreThereNotifications();
}
/** Lifetime extension keeps entries around after they would've otherwise been canceled */
private void extendLifetime(Entry entry, NotificationLifetimeExtender extender) {
// Cancel any other extender which might be holding on to this notification entry
NotificationLifetimeExtender activeExtender = mRetainedNotifications.get(entry);
if (activeExtender != null && activeExtender != extender) {
activeExtender.setShouldManageLifetime(entry, false);
}
mRetainedNotifications.put(entry, extender);
extender.setShouldManageLifetime(entry, true);
}
/** Tells the current extender (if any) to stop extending the entry's lifetime */
private void cancelLifetimeExtension(NotificationData.Entry entry) {
NotificationLifetimeExtender activeExtender = mRetainedNotifications.remove(entry);
if (activeExtender != null) {
activeExtender.setShouldManageLifetime(entry, false);
}
}
@VisibleForTesting
public Map<Entry, NotificationLifetimeExtender> getRetainedNotificationMap() {
return mRetainedNotifications;
}
/**
* Ensures that the group children are cancelled immediately when the group summary is cancelled
* instead of waiting for the notification manager to send all cancels. Otherwise this could
@@ -3479,6 +3517,17 @@ public class StatusBar extends SystemUI implements DemoMode,
}
}
pw.println(" Lifetime-extended notifications:");
if (mRetainedNotifications.isEmpty()) {
pw.println(" None");
} else {
for (Map.Entry<NotificationData.Entry, NotificationLifetimeExtender> entry
: mRetainedNotifications.entrySet()) {
pw.println(" " + entry.getKey().notification + " retained by "
+ entry.getValue().getClass().getName());
}
}
pw.print(" mInteractingWindows="); pw.println(mInteractingWindows);
pw.print(" mStatusBarWindowState=");
pw.println(windowStateToString(mStatusBarWindowState));
@@ -5622,6 +5671,11 @@ public class StatusBar extends SystemUI implements DemoMode,
protected RemoteInputController mRemoteInputController;
// A lifetime extender that watches for foreground service notifications
@VisibleForTesting protected NotificationLifetimeExtender mFGSExtender;
private final Map<Entry, NotificationLifetimeExtender> mRetainedNotifications =
new ArrayMap<>();
// for heads up notifications
protected HeadsUpManager mHeadsUpManager;
@@ -7288,6 +7342,9 @@ public class StatusBar extends SystemUI implements DemoMode,
Log.w(TAG, "Notification that was kept for guts was updated. " + key);
}
// No need to keep the lifetime extension around if an update comes in for it
cancelLifetimeExtension(entry);
Notification n = notification.getNotification();
mNotificationData.updateRanking(ranking);

View File

@@ -0,0 +1,85 @@
/*
* Copyright (C) 2019 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;
import static com.android.systemui.statusbar.ForegroundServiceLifetimeExtender.MIN_FGS_TIME_MS;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import android.app.Notification;
import android.service.notification.NotificationListenerService.Ranking;
import android.service.notification.StatusBarNotification;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.statusbar.NotificationData.Entry;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ForegroundServiceLifetimeExtenderTest extends SysuiTestCase {
private ForegroundServiceLifetimeExtender mExtender = new ForegroundServiceLifetimeExtender();
private StatusBarNotification mSbn;
private NotificationData.Entry mEntry;
private Notification mNotif;
@Before
public void setup() {
mNotif = new Notification.Builder(mContext, "")
.setSmallIcon(R.drawable.ic_person)
.setContentTitle("Title")
.setContentText("Text")
.build();
mSbn = mock(StatusBarNotification.class);
when(mSbn.getNotification()).thenReturn(mNotif);
mEntry = new NotificationData.Entry(mSbn);
}
/**
* ForegroundServiceLifetimeExtenderTest
*/
@Test
public void testShouldExtendLifetime_should_foreground() {
// Extend the lifetime of a FGS notification iff it has not been visible
// for the minimum time
mNotif.flags |= Notification.FLAG_FOREGROUND_SERVICE;
when(mSbn.getPostTime()).thenReturn(System.currentTimeMillis());
assertTrue(mExtender.shouldExtendLifetime(mEntry));
}
@Test
public void testShouldExtendLifetime_shouldNot_foreground() {
mNotif.flags |= Notification.FLAG_FOREGROUND_SERVICE;
when(mSbn.getPostTime()).thenReturn(System.currentTimeMillis() - MIN_FGS_TIME_MS - 1);
assertFalse(mExtender.shouldExtendLifetime(mEntry));
}
@Test
public void testShouldExtendLifetime_shouldNot_notForeground() {
mNotif.flags = 0;
when(mSbn.getPostTime()).thenReturn(System.currentTimeMillis() - MIN_FGS_TIME_MS - 1);
assertFalse(mExtender.shouldExtendLifetime(mEntry));
}
}

View File

@@ -18,6 +18,8 @@ package com.android.systemui.statusbar.phone;
import static android.app.NotificationManager.IMPORTANCE_HIGH;
import static com.android.systemui.statusbar.ForegroundServiceLifetimeExtender.MIN_FGS_TIME_MS;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import static junit.framework.TestCase.fail;
@@ -37,6 +39,7 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verify;
import android.app.ActivityManager;
import android.app.Notification;
import android.app.trust.TrustManager;
import android.hardware.fingerprint.FingerprintManager;
@@ -49,6 +52,7 @@ import android.os.PowerManager;
import android.os.RemoteException;
import android.os.UserHandle;
import android.service.notification.StatusBarNotification;
import android.service.notification.NotificationListenerService.RankingMap;
import android.support.test.filters.SmallTest;
import android.support.test.metricshelper.MetricsAsserts;
import android.support.test.runner.AndroidJUnit4;
@@ -64,6 +68,11 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.logging.testing.FakeMetricsLogger;
import com.android.internal.statusbar.IStatusBarService;
import com.android.keyguard.KeyguardHostView.OnDismissAction;
import com.android.systemui.ForegroundServiceController;
import com.android.systemui.R;
import com.android.systemui.statusbar.ForegroundServiceLifetimeExtender;
import com.android.systemui.statusbar.NotificationLifetimeExtender;
import com.android.systemui.statusbar.RemoteInputController;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.recents.misc.SystemServicesProxy;
@@ -83,12 +92,18 @@ import org.junit.runner.RunWith;
import java.io.ByteArrayOutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Map;
import junit.framework.Assert;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@RunWithLooper
public class StatusBarTest extends SysuiTestCase {
private static final String TEST_PACKAGE_NAME = "test";
private static final int TEST_UID = 123;
StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
UnlockMethodCache mUnlockMethodCache;
KeyguardIndicationController mKeyguardIndicationController;
@@ -101,8 +116,12 @@ public class StatusBarTest extends SysuiTestCase {
SystemServicesProxy mSystemServicesProxy;
NotificationPanelView mNotificationPanelView;
IStatusBarService mBarService;
RemoteInputController mRemoteInputController;
ForegroundServiceController mForegroundServiceController;
ArrayList<Entry> mNotificationList;
private DisplayMetrics mDisplayMetrics = new DisplayMetrics();
private ForegroundServiceLifetimeExtender mFGSExtender =
new ForegroundServiceLifetimeExtender();
@Before
public void setup() throws Exception {
@@ -120,6 +139,8 @@ public class StatusBarTest extends SysuiTestCase {
when(mNotificationPanelView.getLayoutParams()).thenReturn(new LayoutParams(0, 0));
mNotificationList = mock(ArrayList.class);
IPowerManager powerManagerService = mock(IPowerManager.class);
mRemoteInputController = mock(RemoteInputController.class);
mForegroundServiceController = mock(ForegroundServiceController.class);
HandlerThread handlerThread = new HandlerThread("TestThread");
handlerThread.start();
mPowerManager = new PowerManager(mContext, powerManagerService,
@@ -128,11 +149,12 @@ public class StatusBarTest extends SysuiTestCase {
mBarService = mock(IStatusBarService.class);
mDependency.injectTestDependency(MetricsLogger.class, mMetricsLogger);
mFGSExtender.setCallback(key -> mStatusBar.removeNotification(key, mock(RankingMap.class)));
mStatusBar = new TestableStatusBar(mStatusBarKeyguardViewManager, mUnlockMethodCache,
mKeyguardIndicationController, mStackScroller, mHeadsUpManager,
mNotificationData, mPowerManager, mSystemServicesProxy, mNotificationPanelView,
mBarService);
mStatusBar.mContext = mContext;
mBarService, mFGSExtender, mRemoteInputController, mForegroundServiceController);
doAnswer(invocation -> {
OnDismissAction onDismissAction = (OnDismissAction) invocation.getArguments()[0];
onDismissAction.onDismiss();
@@ -410,6 +432,53 @@ public class StatusBarTest extends SysuiTestCase {
assertTrue(mStatusBar.shouldPeek(entry, sbn));
}
@Test
public void testForegroundServiceNotificationKeptForFiveSeconds() throws Exception {
RankingMap rm = mock(RankingMap.class);
// sbn posted "just now"
Notification n = new Notification.Builder(mContext, "")
.setSmallIcon(R.drawable.ic_person)
.setContentTitle("Title")
.setContentText("Text")
.build();
n.flags |= Notification.FLAG_FOREGROUND_SERVICE;
StatusBarNotification sbn =
new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID,
0, n, new UserHandle(ActivityManager.getCurrentUser()), null,
System.currentTimeMillis());
NotificationData.Entry entry = new NotificationData.Entry(sbn);
when(mNotificationData.get(any())).thenReturn(entry);
mStatusBar.removeNotification(sbn.getKey(), rm);
Map<NotificationData.Entry, NotificationLifetimeExtender> map =
mStatusBar.getRetainedNotificationMap();
Assert.assertTrue(map.containsKey(entry));
}
@Test
public void testForegroundServiceNotification_notRetainedIfShownForFiveSeconds()
throws Exception {
RankingMap rm = mock(RankingMap.class);
// sbn posted "more than 5 seconds ago"
Notification n = new Notification.Builder(mContext, "")
.setSmallIcon(R.drawable.ic_person)
.setContentTitle("Title")
.setContentText("Text")
.build();
n.flags |= Notification.FLAG_FOREGROUND_SERVICE;
StatusBarNotification sbn =
new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID,
0, n, new UserHandle(ActivityManager.getCurrentUser()), null,
System.currentTimeMillis() - MIN_FGS_TIME_MS - 1);
NotificationData.Entry entry = new NotificationData.Entry(sbn);
when(mNotificationData.get(any())).thenReturn(entry);
mStatusBar.removeNotification(sbn.getKey(), rm);
Map<NotificationData.Entry, NotificationLifetimeExtender> map =
mStatusBar.getRetainedNotificationMap();
Assert.assertFalse(map.containsKey(entry));
}
@Test
public void testLogHidden() {
@@ -489,7 +558,8 @@ public class StatusBarTest extends SysuiTestCase {
UnlockMethodCache unlock, KeyguardIndicationController key,
NotificationStackScrollLayout stack, HeadsUpManager hum, NotificationData nd,
PowerManager pm, SystemServicesProxy ssp, NotificationPanelView panelView,
IStatusBarService barService) {
IStatusBarService barService, ForegroundServiceLifetimeExtender fgsExtender,
RemoteInputController ric, ForegroundServiceController fsc) {
mStatusBarKeyguardViewManager = man;
mUnlockMethodCache = unlock;
mKeyguardIndicationController = key;
@@ -501,6 +571,9 @@ public class StatusBarTest extends SysuiTestCase {
mSystemServicesProxy = ssp;
mNotificationPanel = panelView;
mBarService = barService;
mFGSExtender = fgsExtender;
mRemoteInputController = ric;
mForegroundServiceController = fsc;
mWakefulnessLifecycle = createAwakeWakefulnessLifecycle();
}
@@ -511,8 +584,9 @@ public class StatusBarTest extends SysuiTestCase {
return wakefulnessLifecycle;
}
public void setBarStateForTest(int state) {
mState = state;
}
}
}
}

View File

@@ -4442,8 +4442,15 @@ public class NotificationManagerService extends SystemService {
userId, mustHaveFlags, mustNotHaveFlags, reason, listenerName);
synchronized (mNotificationLock) {
// Look for the notification, searching both the posted and enqueued lists.
NotificationRecord r = findNotificationLocked(pkg, tag, id, userId);
// If the notification is currently enqueued, repost this runnable so it has a
// chance to notify listeners
if ((findNotificationByListLocked(
mEnqueuedNotifications, pkg, tag, id, userId)) != null) {
mHandler.post(this);
}
// Look for the notification in the posted list, since we already checked enq
NotificationRecord r = findNotificationByListLocked(
mNotificationList, pkg, tag, id, userId);
if (r != null) {
// The notification was found, check if it should be removed.

View File

@@ -336,6 +336,21 @@ public class NotificationManagerServiceTest extends NotificationTestCase {
assertEquals(0, mNotificationManagerService.getNotificationRecordCount());
}
@Test
public void testCancelImmediatelyAfterEnqueueNotifiesListeners_ForegroundServiceFlag()
throws Exception {
final StatusBarNotification sbn = generateNotificationRecord(null).sbn;
sbn.getNotification().flags =
Notification.FLAG_ONGOING_EVENT | Notification.FLAG_FOREGROUND_SERVICE;
mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag",
sbn.getId(), sbn.getNotification(), sbn.getUserId());
mBinderService.cancelNotificationWithTag(PKG, "tag", sbn.getId(), sbn.getUserId());
waitForIdle();
verify(mNotificationListeners, times(1)).notifyPostedLocked(any(), any());
verify(mNotificationListeners, times(1)).notifyRemovedLocked(any(), anyInt());
}
@Test
public void testCancelNotificationWhilePostedAndEnqueued() throws Exception {
mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", 0,