diff options
| author | 2018-08-14 16:59:33 -0400 | |
|---|---|---|
| committer | 2018-08-20 18:59:31 +0000 | |
| commit | 6a63d1bfb5229cf30b9c2d0d49cd6ea2c834f8a6 (patch) | |
| tree | a43e42133fbc7fd24e1546e314802efc6d0c0cb3 | |
| parent | 93def8053d757210f259c1c4551f01b8a8c38fd6 (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
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); |