summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Julia Reynolds <juliacr@google.com> 2018-08-14 16:59:33 -0400
committer Julia Reynolds <juliacr@google.com> 2018-08-20 18:59:31 +0000
commit6a63d1bfb5229cf30b9c2d0d49cd6ea2c834f8a6 (patch)
treea43e42133fbc7fd24e1546e314802efc6d0c0cb3
parent93def8053d757210f259c1c4551f01b8a8c38fd6 (diff)
The quietening round 3: aging
Move notifications into the min bucket after they've been seen (plus a variable delay based on type). Test: ExtServicesUnitTests Bug: 111475013 Change-Id: Id577162d063dc1b0ad370f66af7a503e294c5b65
-rw-r--r--core/java/android/service/notification/INotificationListener.aidl2
-rw-r--r--core/java/android/service/notification/NotificationAssistantService.java24
-rw-r--r--core/java/android/service/notification/NotificationListenerService.java6
-rw-r--r--packages/ExtServices/src/android/ext/services/notification/AgingHelper.java172
-rw-r--r--packages/ExtServices/src/android/ext/services/notification/Assistant.java84
-rw-r--r--packages/ExtServices/src/android/ext/services/notification/NotificationEntry.java9
-rw-r--r--packages/ExtServices/tests/src/android/ext/services/notification/AgingHelperTest.java153
-rw-r--r--packages/ExtServices/tests/src/android/ext/services/notification/AssistantTest.java27
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java28
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java1
-rw-r--r--services/core/java/com/android/server/notification/NotificationManagerService.java70
11 files changed, 544 insertions, 32 deletions
diff --git a/core/java/android/service/notification/INotificationListener.aidl b/core/java/android/service/notification/INotificationListener.aidl
index 2dff7161a068..d8bd00281ba5 100644
--- a/core/java/android/service/notification/INotificationListener.aidl
+++ b/core/java/android/service/notification/INotificationListener.aidl
@@ -18,6 +18,7 @@ package android.service.notification;
import android.app.NotificationChannel;
import android.app.NotificationChannelGroup;
+import android.content.pm.ParceledListSlice;
import android.os.UserHandle;
import android.service.notification.NotificationStats;
import android.service.notification.IStatusBarNotificationHolder;
@@ -45,4 +46,5 @@ oneway interface INotificationListener
// assistants only
void onNotificationEnqueuedWithChannel(in IStatusBarNotificationHolder notificationHolder, in NotificationChannel channel);
void onNotificationSnoozedUntilContext(in IStatusBarNotificationHolder notificationHolder, String snoozeCriterionId);
+ void onNotificationsSeen(in List<String> keys);
}
diff --git a/core/java/android/service/notification/NotificationAssistantService.java b/core/java/android/service/notification/NotificationAssistantService.java
index 3853fc54773f..3b820ca1e165 100644
--- a/core/java/android/service/notification/NotificationAssistantService.java
+++ b/core/java/android/service/notification/NotificationAssistantService.java
@@ -149,6 +149,14 @@ public abstract class NotificationAssistantService extends NotificationListenerS
}
/**
+ * Implement this to know when a user has seen notifications, as triggered by
+ * {@link #setNotificationsShown(String[])}.
+ */
+ public void onNotificationsSeen(List<String> keys) {
+
+ }
+
+ /**
* Updates a notification. N.B. this won’t cause
* an existing notification to alert, but might allow a future update to
* this notification to alert.
@@ -236,11 +244,20 @@ public abstract class NotificationAssistantService extends NotificationListenerS
mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_SNOOZED,
args).sendToTarget();
}
+
+ @Override
+ public void onNotificationsSeen(List<String> keys) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = keys;
+ mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATIONS_SEEN,
+ args).sendToTarget();
+ }
}
private final class MyHandler extends Handler {
public static final int MSG_ON_NOTIFICATION_ENQUEUED = 1;
public static final int MSG_ON_NOTIFICATION_SNOOZED = 2;
+ public static final int MSG_ON_NOTIFICATIONS_SEEN = 3;
public MyHandler(Looper looper) {
super(looper, null, false);
@@ -275,6 +292,13 @@ public abstract class NotificationAssistantService extends NotificationListenerS
onNotificationSnoozedUntilContext(sbn, snoozeCriterionId);
break;
}
+ case MSG_ON_NOTIFICATIONS_SEEN: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ List<String> keys = (List<String>) args.arg1;
+ args.recycle();
+ onNotificationsSeen(keys);
+ break;
+ }
}
}
}
diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java
index 98da5694456d..1b588f470971 100644
--- a/core/java/android/service/notification/NotificationListenerService.java
+++ b/core/java/android/service/notification/NotificationListenerService.java
@@ -1332,6 +1332,12 @@ public abstract class NotificationListenerService extends Service {
}
@Override
+ public void onNotificationsSeen(List<String> keys)
+ throws RemoteException {
+ // no-op in the listener
+ }
+
+ @Override
public void onNotificationSnoozedUntilContext(
IStatusBarNotificationHolder notificationHolder, String snoozeCriterionId)
throws RemoteException {
diff --git a/packages/ExtServices/src/android/ext/services/notification/AgingHelper.java b/packages/ExtServices/src/android/ext/services/notification/AgingHelper.java
new file mode 100644
index 000000000000..5782ea100070
--- /dev/null
+++ b/packages/ExtServices/src/android/ext/services/notification/AgingHelper.java
@@ -0,0 +1,172 @@
+/**
+ * Copyright (C) 2018 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 android.ext.services.notification;
+
+import static android.app.NotificationManager.IMPORTANCE_MIN;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.ext.services.notification.NotificationCategorizer.Category;
+import android.net.Uri;
+import android.util.ArraySet;
+import android.util.Slog;
+
+import java.util.Set;
+
+public class AgingHelper {
+ private final static String TAG = "AgingHelper";
+ private final boolean DEBUG = false;
+
+ private static final String AGING_ACTION = AgingHelper.class.getSimpleName() + ".EVALUATE";
+ private static final int REQUEST_CODE_AGING = 1;
+ private static final String AGING_SCHEME = "aging";
+ private static final String EXTRA_KEY = "key";
+ private static final String EXTRA_CATEGORY = "category";
+
+ private static final int HOUR_MS = 1000 * 60 * 60;
+ private static final int TWO_HOURS_MS = 2 * HOUR_MS;
+
+ private Context mContext;
+ private NotificationCategorizer mNotificationCategorizer;
+ private AlarmManager mAm;
+ private Callback mCallback;
+
+ // The set of keys we've scheduled alarms for
+ private Set<String> mAging = new ArraySet<>();
+
+ public AgingHelper(Context context, NotificationCategorizer categorizer, Callback callback) {
+ mNotificationCategorizer = categorizer;
+ mContext = context;
+ mAm = mContext.getSystemService(AlarmManager.class);
+ mCallback = callback;
+
+ IntentFilter filter = new IntentFilter(AGING_ACTION);
+ filter.addDataScheme(AGING_SCHEME);
+ mContext.registerReceiver(mBroadcastReceiver, filter);
+ }
+
+ // NAS lifecycle methods
+
+ public void onNotificationSeen(NotificationEntry entry) {
+ // user has strong opinions about this notification. we can't down rank it, so don't bother.
+ if (entry.getChannel().isImportanceLocked()) {
+ return;
+ }
+
+ @Category int category = mNotificationCategorizer.getCategory(entry);
+
+ // already very low
+ if (category == NotificationCategorizer.CATEGORY_MIN) {
+ return;
+ }
+
+ if (entry.hasSeen()) {
+ if (category == NotificationCategorizer.CATEGORY_ONGOING
+ || category > NotificationCategorizer.CATEGORY_REMINDER) {
+ scheduleAging(entry.getSbn().getKey(), category, TWO_HOURS_MS);
+ } else {
+ scheduleAging(entry.getSbn().getKey(), category, HOUR_MS);
+ }
+
+ mAging.add(entry.getSbn().getKey());
+ }
+ }
+
+ public void onNotificationPosted(NotificationEntry entry) {
+ cancelAging(entry.getSbn().getKey());
+ }
+
+ public void onNotificationRemoved(String key) {
+ cancelAging(key);
+ }
+
+ public void onDestroy() {
+ mContext.unregisterReceiver(mBroadcastReceiver);
+ }
+
+ // Aging
+
+ private void scheduleAging(String key, @Category int category, long duration) {
+ if (mAging.contains(key)) {
+ // already scheduled. Don't reset aging just because the user saw the noti again.
+ return;
+ }
+ final PendingIntent pi = createPendingIntent(key, category);
+ long time = System.currentTimeMillis() + duration;
+ if (DEBUG) Slog.d(TAG, "Scheduling evaluate for " + key + " in ms: " + duration);
+ mAm.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, time, pi);
+ }
+
+ private void cancelAging(String key) {
+ final PendingIntent pi = createPendingIntent(key);
+ mAm.cancel(pi);
+ mAging.remove(key);
+ }
+
+ private Intent createBaseIntent(String key) {
+ return new Intent(AGING_ACTION)
+ .setData(new Uri.Builder().scheme(AGING_SCHEME).appendPath(key).build());
+ }
+
+ private Intent createAgingIntent(String key, @Category int category) {
+ Intent intent = createBaseIntent(key);
+ intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
+ .putExtra(EXTRA_CATEGORY, category)
+ .putExtra(EXTRA_KEY, key);
+ return intent;
+ }
+
+ private PendingIntent createPendingIntent(String key, @Category int category) {
+ return PendingIntent.getBroadcast(mContext,
+ REQUEST_CODE_AGING,
+ createAgingIntent(key, category),
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+
+ private PendingIntent createPendingIntent(String key) {
+ return PendingIntent.getBroadcast(mContext,
+ REQUEST_CODE_AGING,
+ createBaseIntent(key),
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+
+ private void demote(String key, @Category int category) {
+ int newImportance = IMPORTANCE_MIN;
+ // TODO: Change "aged" importance based on category
+ mCallback.sendAdjustment(key, newImportance);
+ }
+
+ protected interface Callback {
+ void sendAdjustment(String key, int newImportance);
+ }
+
+ private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (DEBUG) {
+ Slog.d(TAG, "Reposting notification");
+ }
+ if (AGING_ACTION.equals(intent.getAction())) {
+ demote(intent.getStringExtra(EXTRA_KEY), intent.getIntExtra(EXTRA_CATEGORY,
+ NotificationCategorizer.CATEGORY_EVERYTHING_ELSE));
+ }
+ }
+ };
+}
diff --git a/packages/ExtServices/src/android/ext/services/notification/Assistant.java b/packages/ExtServices/src/android/ext/services/notification/Assistant.java
index a8ecec3db213..f0f31fbded6f 100644
--- a/packages/ExtServices/src/android/ext/services/notification/Assistant.java
+++ b/packages/ExtServices/src/android/ext/services/notification/Assistant.java
@@ -18,23 +18,28 @@ package android.ext.services.notification;
import static android.app.NotificationManager.IMPORTANCE_LOW;
import static android.app.NotificationManager.IMPORTANCE_MIN;
+import static android.service.notification.Adjustment.KEY_IMPORTANCE;
import static android.service.notification.NotificationListenerService.Ranking
.USER_SENTIMENT_NEGATIVE;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityThread;
+import android.app.AlarmManager;
import android.app.INotificationManager;
import android.app.Notification;
import android.app.NotificationChannel;
import android.content.ContentResolver;
import android.content.Context;
+import android.content.pm.IPackageManager;
import android.database.ContentObserver;
+import android.ext.services.notification.AgingHelper.Callback;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
+import android.os.UserHandle;
import android.os.storage.StorageManager;
import android.provider.Settings;
import android.service.notification.Adjustment;
@@ -64,6 +69,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
/**
@@ -89,15 +95,17 @@ public class Assistant extends NotificationAssistantService {
private int mStreakLimit;
private SmartActionsHelper mSmartActionsHelper;
private NotificationCategorizer mNotificationCategorizer;
+ private AgingHelper mAgingHelper;
// key : impressions tracker
// TODO: prune deleted channels and apps
- final ArrayMap<String, ChannelImpressions> mkeyToImpressions = new ArrayMap<>();
- // SBN key : channel id
- ArrayMap<String, String> mLiveNotifications = new ArrayMap<>();
+ private final ArrayMap<String, ChannelImpressions> mkeyToImpressions = new ArrayMap<>();
+ // SBN key : entry
+ protected ArrayMap<String, NotificationEntry> mLiveNotifications = new ArrayMap<>();
private Ranking mFakeRanking = null;
private AtomicFile mFile = null;
+ private IPackageManager mPackageManager;
protected SettingsObserver mSettingsObserver;
public Assistant() {
@@ -108,9 +116,13 @@ public class Assistant extends NotificationAssistantService {
super.onCreate();
// Contexts are correctly hooked up by the creation step, which is required for the observer
// to be hooked up/initialized.
+ mPackageManager = ActivityThread.getPackageManager();
mSettingsObserver = new SettingsObserver(mHandler);
mSmartActionsHelper = new SmartActionsHelper();
mNotificationCategorizer = new NotificationCategorizer();
+ mAgingHelper = new AgingHelper(getContext(),
+ mNotificationCategorizer,
+ new AgingCallback());
}
private void loadFile() {
@@ -157,7 +169,7 @@ public class Assistant extends NotificationAssistantService {
}
}
- private void saveFile() throws IOException {
+ private void saveFile() {
AsyncTask.execute(() -> {
final FileOutputStream stream;
try {
@@ -200,6 +212,9 @@ public class Assistant extends NotificationAssistantService {
public Adjustment onNotificationEnqueued(StatusBarNotification sbn,
NotificationChannel channel) {
if (DEBUG) Log.i(TAG, "ENQUEUED " + sbn.getKey() + " on " + channel.getId());
+ if (!isForCurrentUser(sbn)) {
+ return null;
+ }
NotificationEntry entry = new NotificationEntry(
ActivityThread.getPackageManager(), sbn, channel);
ArrayList<Notification.Action> actions =
@@ -222,7 +237,7 @@ public class Assistant extends NotificationAssistantService {
signals.putCharSequenceArrayList(Adjustment.KEY_SMART_REPLIES, smartReplies);
}
if (mNotificationCategorizer.shouldSilence(entry)) {
- signals.putInt(Adjustment.KEY_IMPORTANCE, IMPORTANCE_LOW);
+ signals.putInt(KEY_IMPORTANCE, IMPORTANCE_LOW);
}
return new Adjustment(
@@ -237,8 +252,13 @@ public class Assistant extends NotificationAssistantService {
public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
if (DEBUG) Log.i(TAG, "POSTED " + sbn.getKey());
try {
+ if (!isForCurrentUser(sbn)) {
+ return;
+ }
Ranking ranking = getRanking(sbn.getKey(), rankingMap);
if (ranking != null && ranking.getChannel() != null) {
+ NotificationEntry entry = new NotificationEntry(mPackageManager,
+ sbn, ranking.getChannel());
String key = getKey(
sbn.getPackageName(), sbn.getUserId(), ranking.getChannel().getId());
ChannelImpressions ci = mkeyToImpressions.getOrDefault(key,
@@ -248,7 +268,8 @@ public class Assistant extends NotificationAssistantService {
sbn.getPackageName(), sbn.getKey(), sbn.getUserId()));
}
mkeyToImpressions.put(key, ci);
- mLiveNotifications.put(sbn.getKey(), ranking.getChannel().getId());
+ mLiveNotifications.put(sbn.getKey(), entry);
+ mAgingHelper.onNotificationPosted(entry);
}
} catch (Throwable e) {
Log.e(TAG, "Error occurred processing post", e);
@@ -259,8 +280,11 @@ public class Assistant extends NotificationAssistantService {
public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap,
NotificationStats stats, int reason) {
try {
+ if (!isForCurrentUser(sbn)) {
+ return;
+ }
boolean updatedImpressions = false;
- String channelId = mLiveNotifications.remove(sbn.getKey());
+ String channelId = mLiveNotifications.remove(sbn.getKey()).getChannel().getId();
String key = getKey(sbn.getPackageName(), sbn.getUserId(), channelId);
synchronized (mkeyToImpressions) {
ChannelImpressions ci = mkeyToImpressions.getOrDefault(key,
@@ -302,6 +326,22 @@ public class Assistant extends NotificationAssistantService {
}
@Override
+ public void onNotificationsSeen(List<String> keys) {
+ if (keys == null) {
+ return;
+ }
+
+ for (String key : keys) {
+ NotificationEntry entry = mLiveNotifications.get(key);
+
+ if (entry != null) {
+ entry.setSeen();
+ mAgingHelper.onNotificationSeen(entry);
+ }
+ }
+ }
+
+ @Override
public void onListenerConnected() {
if (DEBUG) Log.i(TAG, "CONNECTED");
try {
@@ -318,6 +358,17 @@ public class Assistant extends NotificationAssistantService {
}
}
+ @Override
+ public void onListenerDisconnected() {
+ if (mAgingHelper != null) {
+ mAgingHelper.onDestroy();
+ }
+ }
+
+ private boolean isForCurrentUser(StatusBarNotification sbn) {
+ return sbn != null && sbn.getUserId() == UserHandle.myUserId();
+ }
+
protected String getKey(String pkg, int userId, String channelId) {
return pkg + "|" + userId + "|" + channelId;
}
@@ -361,6 +412,11 @@ public class Assistant extends NotificationAssistantService {
}
@VisibleForTesting
+ public void setPackageManager(IPackageManager pm) {
+ mPackageManager = pm;
+ }
+
+ @VisibleForTesting
public ChannelImpressions getImpressions(String key) {
synchronized (mkeyToImpressions) {
return mkeyToImpressions.get(key);
@@ -380,6 +436,20 @@ public class Assistant extends NotificationAssistantService {
return impressions;
}
+ protected final class AgingCallback implements Callback {
+ @Override
+ public void sendAdjustment(String key, int newImportance) {
+ NotificationEntry entry = mLiveNotifications.get(key);
+ if (entry != null) {
+ Bundle bundle = new Bundle();
+ bundle.putInt(KEY_IMPORTANCE, newImportance);
+ Adjustment adjustment = new Adjustment(entry.getSbn().getPackageName(), key, bundle,
+ "aging", entry.getSbn().getUserId());
+ adjustNotification(adjustment);
+ }
+ }
+ }
+
/**
* Observer for updates on blocking helper threshold values.
*/
diff --git a/packages/ExtServices/src/android/ext/services/notification/NotificationEntry.java b/packages/ExtServices/src/android/ext/services/notification/NotificationEntry.java
index cdc09906cb82..8fee822f11c0 100644
--- a/packages/ExtServices/src/android/ext/services/notification/NotificationEntry.java
+++ b/packages/ExtServices/src/android/ext/services/notification/NotificationEntry.java
@@ -50,6 +50,7 @@ public class NotificationEntry {
private AudioAttributes mAttributes;
private NotificationChannel mChannel;
private int mImportance;
+ private boolean mSeen;
public NotificationEntry(IPackageManager packageManager, StatusBarNotification sbn,
NotificationChannel channel) {
@@ -198,6 +199,14 @@ public class NotificationEntry {
return false;
}
+ public void setSeen() {
+ mSeen = true;
+ }
+
+ public boolean hasSeen() {
+ return mSeen;
+ }
+
public StatusBarNotification getSbn() {
return mSbn;
}
diff --git a/packages/ExtServices/tests/src/android/ext/services/notification/AgingHelperTest.java b/packages/ExtServices/tests/src/android/ext/services/notification/AgingHelperTest.java
new file mode 100644
index 000000000000..b023b364a19b
--- /dev/null
+++ b/packages/ExtServices/tests/src/android/ext/services/notification/AgingHelperTest.java
@@ -0,0 +1,153 @@
+/**
+ * Copyright (C) 2018 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 android.ext.services.notification;
+
+import static android.app.NotificationManager.IMPORTANCE_HIGH;
+import static android.app.NotificationManager.IMPORTANCE_MIN;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AlarmManager;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.PendingIntent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.os.Build;
+import android.os.Process;
+import android.os.UserHandle;
+import android.service.notification.StatusBarNotification;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+import android.testing.TestableContext;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+public class AgingHelperTest {
+ private String mPkg = "pkg";
+ private int mUid = 2018;
+
+ @Rule
+ public final TestableContext mContext =
+ new TestableContext(InstrumentationRegistry.getTargetContext(), null);
+
+ @Mock
+ private NotificationCategorizer mCategorizer;
+ @Mock
+ private AlarmManager mAlarmManager;
+ @Mock
+ private IPackageManager mPackageManager;
+ @Mock
+ private AgingHelper.Callback mCallback;
+
+ private AgingHelper mAgingHelper;
+
+ private StatusBarNotification generateSbn(String channelId) {
+ Notification n = new Notification.Builder(mContext, channelId)
+ .setContentTitle("foo")
+ .build();
+
+ return new StatusBarNotification(mPkg, mPkg, 0, "tag", mUid, mUid, n,
+ UserHandle.SYSTEM, null, 0);
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mPkg = mContext.getPackageName();
+ mUid = Process.myUid();
+
+ ApplicationInfo info = mock(ApplicationInfo.class);
+ when(mPackageManager.getApplicationInfo(anyString(), anyInt(), anyInt()))
+ .thenReturn(info);
+ info.targetSdkVersion = Build.VERSION_CODES.P;
+
+ mContext.addMockSystemService(AlarmManager.class, mAlarmManager);
+
+ mAgingHelper = new AgingHelper(mContext, mCategorizer, mCallback);
+ }
+
+ @Test
+ public void testNoSnoozingOnPost() {
+ NotificationChannel channel = new NotificationChannel("", "", IMPORTANCE_HIGH);
+ StatusBarNotification sbn = generateSbn(channel.getId());
+ NotificationEntry entry = new NotificationEntry(mPackageManager, sbn, channel);
+
+
+ mAgingHelper.onNotificationPosted(entry);
+ verify(mAlarmManager, never()).setExactAndAllowWhileIdle(anyInt(), anyLong(), any());
+ }
+
+ @Test
+ public void testPostResetsSnooze() {
+ NotificationChannel channel = new NotificationChannel("", "", IMPORTANCE_HIGH);
+ StatusBarNotification sbn = generateSbn(channel.getId());
+ NotificationEntry entry = new NotificationEntry(mPackageManager, sbn, channel);
+
+
+ mAgingHelper.onNotificationPosted(entry);
+ verify(mAlarmManager, times(1)).cancel(any(PendingIntent.class));
+ }
+
+ @Test
+ public void testSnoozingOnSeen() {
+ NotificationChannel channel = new NotificationChannel("", "", IMPORTANCE_HIGH);
+ StatusBarNotification sbn = generateSbn(channel.getId());
+ NotificationEntry entry = new NotificationEntry(mPackageManager, sbn, channel);
+ entry.setSeen();
+ when(mCategorizer.getCategory(entry)).thenReturn(NotificationCategorizer.CATEGORY_PEOPLE);
+
+ mAgingHelper.onNotificationSeen(entry);
+ verify(mAlarmManager, times(1)).setExactAndAllowWhileIdle(anyInt(), anyLong(), any());
+ }
+
+ @Test
+ public void testNoSnoozingOnSeenUserLocked() {
+ NotificationChannel channel = new NotificationChannel("", "", IMPORTANCE_HIGH);
+ channel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE);
+ StatusBarNotification sbn = generateSbn(channel.getId());
+ NotificationEntry entry = new NotificationEntry(mPackageManager, sbn, channel);
+ when(mCategorizer.getCategory(entry)).thenReturn(NotificationCategorizer.CATEGORY_PEOPLE);
+
+ mAgingHelper.onNotificationSeen(entry);
+ verify(mAlarmManager, never()).setExactAndAllowWhileIdle(anyInt(), anyLong(), any());
+ }
+
+ @Test
+ public void testNoSnoozingOnSeenAlreadyLow() {
+ NotificationEntry entry = mock(NotificationEntry.class);
+ when(entry.getChannel()).thenReturn(new NotificationChannel("", "", IMPORTANCE_HIGH));
+ when(entry.getImportance()).thenReturn(IMPORTANCE_MIN);
+
+ mAgingHelper.onNotificationSeen(entry);
+ verify(mAlarmManager, never()).setExactAndAllowWhileIdle(anyInt(), anyLong(), any());
+ }
+}
diff --git a/packages/ExtServices/tests/src/android/ext/services/notification/AssistantTest.java b/packages/ExtServices/tests/src/android/ext/services/notification/AssistantTest.java
index bb68bc2b875e..2820232cdb38 100644
--- a/packages/ExtServices/tests/src/android/ext/services/notification/AssistantTest.java
+++ b/packages/ExtServices/tests/src/android/ext/services/notification/AssistantTest.java
@@ -21,6 +21,8 @@ import static android.app.NotificationManager.IMPORTANCE_LOW;
import static android.app.NotificationManager.IMPORTANCE_MIN;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
@@ -33,6 +35,9 @@ import android.app.Notification;
import android.app.NotificationChannel;
import android.content.ContentResolver;
import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.os.Build;
import android.os.UserHandle;
import android.provider.Settings;
import android.service.notification.Adjustment;
@@ -80,6 +85,8 @@ public class AssistantTest extends ServiceTestCase<Assistant> {
@Mock INotificationManager mNoMan;
@Mock AtomicFile mFile;
+ @Mock
+ IPackageManager mPackageManager;
Assistant mAssistant;
Application mApplication;
@@ -113,6 +120,11 @@ public class AssistantTest extends ServiceTestCase<Assistant> {
mAssistant = getService();
mAssistant.setNoMan(mNoMan);
mAssistant.setFile(mFile);
+ mAssistant.setPackageManager(mPackageManager);
+ ApplicationInfo info = mock(ApplicationInfo.class);
+ when(mPackageManager.getApplicationInfo(anyString(), anyInt(), anyInt()))
+ .thenReturn(info);
+ info.targetSdkVersion = Build.VERSION_CODES.P;
when(mFile.startWrite()).thenReturn(mock(FileOutputStream.class));
}
@@ -439,4 +451,19 @@ public class AssistantTest extends ServiceTestCase<Assistant> {
// With the new threshold, the blocking helper should be triggered.
assertEquals(true, ci.shouldTriggerBlock());
}
+
+ @Test
+ public void testTrimLiveNotifications() {
+ StatusBarNotification sbn = generateSbn(PKG1, UID1, P1C1, "no", null);
+ mAssistant.setFakeRanking(generateRanking(sbn, P1C1));
+
+ mAssistant.onNotificationPosted(sbn, mock(RankingMap.class));
+
+ assertTrue(mAssistant.mLiveNotifications.containsKey(sbn.getKey()));
+
+ mAssistant.onNotificationRemoved(
+ sbn, mock(RankingMap.class), new NotificationStats(), 0);
+
+ assertFalse(mAssistant.mLiveNotifications.containsKey(sbn.getKey()));
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
index 767b07f4e7f3..e96e176cc503 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
@@ -62,6 +62,8 @@ public class NotificationLogger {
protected IStatusBarService mBarService;
private long mLastVisibilityReportUptimeMs;
private NotificationListContainer mListContainer;
+ private Object mDozingLock = new Object();
+ private boolean mDozing;
protected final OnChildLocationsChangedListener mNotificationLocationsChangedListener =
new OnChildLocationsChangedListener() {
@@ -174,6 +176,12 @@ public class NotificationLogger {
mNotificationLocationsChangedListener.onChildLocationsChanged();
}
+ public void setDozing(boolean dozing) {
+ synchronized (mDozingLock) {
+ mDozing = dozing;
+ }
+ }
+
private void logNotificationVisibilityChanges(
Collection<NotificationVisibility> newlyVisible,
Collection<NotificationVisibility> noLongerVisible) {
@@ -190,19 +198,25 @@ public class NotificationLogger {
// Ignore.
}
- final int N = newlyVisible.size();
+ final int N = newlyVisibleAr.length;
if (N > 0) {
String[] newlyVisibleKeyAr = new String[N];
for (int i = 0; i < N; i++) {
newlyVisibleKeyAr[i] = newlyVisibleAr[i].key;
}
- // TODO: Call NotificationEntryManager to do this, once it exists.
- // TODO: Consider not catching all runtime exceptions here.
- try {
- mNotificationListener.setNotificationsShown(newlyVisibleKeyAr);
- } catch (RuntimeException e) {
- Log.d(TAG, "failed setNotificationsShown: ", e);
+ synchronized (mDozingLock) {
+ // setNotificationsShown should only be called if we are confident that
+ // the user has seen the notification, aka not when ambient display is on
+ if (!mDozing) {
+ // TODO: Call NotificationEntryManager to do this, once it exists.
+ // TODO: Consider not catching all runtime exceptions here.
+ try {
+ mNotificationListener.setNotificationsShown(newlyVisibleKeyAr);
+ } catch (RuntimeException e) {
+ Log.d(TAG, "failed setNotificationsShown: ", e);
+ }
+ }
}
}
recycleAllVisibilityObjects(newlyVisibleAr);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index cd0255b0edf4..3701eafeb5eb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -3924,6 +3924,7 @@ public class StatusBar extends SystemUI implements DemoMode,
mDozeScrimController.setDozing(mDozing);
mKeyguardIndicationController.setDozing(mDozing);
mNotificationPanel.setDozing(mDozing, animate);
+ mNotificationLogger.setDozing(mDozing);
updateQsExpansionEnabled();
Trace.endSection();
}
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index f0743192d65e..dd3e2d42ef88 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -72,7 +72,6 @@ import static android.service.notification.NotificationListenerService.REASON_UN
import static android.service.notification.NotificationListenerService.REASON_USER_STOPPED;
import static android.service.notification.NotificationListenerService.TRIM_FULL;
import static android.service.notification.NotificationListenerService.TRIM_LIGHT;
-import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.LayoutParams.TYPE_TOAST;
import static com.android.server.utils.PriorityDump.PRIORITY_ARG;
@@ -2694,24 +2693,30 @@ public class NotificationManagerService extends SystemService {
try {
synchronized (mNotificationLock) {
final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token);
- if (keys != null) {
- final int N = keys.length;
- for (int i = 0; i < N; i++) {
- NotificationRecord r = mNotificationsByKey.get(keys[i]);
- if (r == null) continue;
- final int userId = r.sbn.getUserId();
- if (userId != info.userid && userId != UserHandle.USER_ALL &&
- !mUserProfiles.isCurrentProfile(userId)) {
- throw new SecurityException("Disallowed call from listener: "
- + info.service);
- }
- if (!r.isSeen()) {
- if (DBG) Slog.d(TAG, "Marking notification as seen " + keys[i]);
- reportSeen(r);
- r.setSeen();
- maybeRecordInterruptionLocked(r);
- }
+ if (keys == null) {
+ return;
+ }
+ ArrayList<NotificationRecord> seen = new ArrayList<>();
+ final int n = keys.length;
+ for (int i = 0; i < n; i++) {
+ NotificationRecord r = mNotificationsByKey.get(keys[i]);
+ if (r == null) continue;
+ final int userId = r.sbn.getUserId();
+ if (userId != info.userid && userId != UserHandle.USER_ALL
+ && !mUserProfiles.isCurrentProfile(userId)) {
+ throw new SecurityException("Disallowed call from listener: "
+ + info.service);
}
+ seen.add(r);
+ if (!r.isSeen()) {
+ if (DBG) Slog.d(TAG, "Marking notification as seen " + keys[i]);
+ reportSeen(r);
+ r.setSeen();
+ maybeRecordInterruptionLocked(r);
+ }
+ }
+ if (!seen.isEmpty()) {
+ mAssistants.onNotificationsSeenLocked(seen);
}
}
} finally {
@@ -6556,6 +6561,35 @@ public class NotificationManagerService extends SystemService {
rebindServices(true);
}
+ protected void onNotificationsSeenLocked(ArrayList<NotificationRecord> records) {
+ // There should be only one, but it's a list, so while we enforce
+ // singularity elsewhere, we keep it general here, to avoid surprises.
+ for (final ManagedServiceInfo info : NotificationAssistants.this.getServices()) {
+ ArrayList<String> keys = new ArrayList<>(records.size());
+ for (NotificationRecord r : records) {
+ boolean sbnVisible = isVisibleToListener(r.sbn, info)
+ && info.isSameUser(r.getUserId());
+ if (sbnVisible) {
+ keys.add(r.getKey());
+ }
+ }
+
+ if (!keys.isEmpty()) {
+ mHandler.post(() -> notifySeen(info, keys));
+ }
+ }
+ }
+
+ private void notifySeen(final ManagedServiceInfo info,
+ final ArrayList<String> keys) {
+ final INotificationListener assistant = (INotificationListener) info.service;
+ try {
+ assistant.onNotificationsSeen(keys);
+ } catch (RemoteException ex) {
+ Log.e(TAG, "unable to notify assistant (seen): " + assistant, ex);
+ }
+ }
+
public void onNotificationEnqueued(final NotificationRecord r) {
final StatusBarNotification sbn = r.sbn;
TrimCache trimCache = new TrimCache(sbn);