diff --git a/packages/CarSystemUI/src/com/android/systemui/CarSystemUIFactory.java b/packages/CarSystemUI/src/com/android/systemui/CarSystemUIFactory.java index 7039a2c0a9571..3c0a2973ad40f 100644 --- a/packages/CarSystemUI/src/com/android/systemui/CarSystemUIFactory.java +++ b/packages/CarSystemUI/src/com/android/systemui/CarSystemUIFactory.java @@ -21,9 +21,11 @@ import android.content.Context; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.ViewMediatorCallback; import com.android.systemui.car.CarNotificationEntryManager; +import com.android.systemui.car.CarNotificationInterruptionStateProvider; import com.android.systemui.statusbar.car.CarFacetButtonController; import com.android.systemui.statusbar.car.CarStatusBarKeyguardViewManager; import com.android.systemui.statusbar.notification.NotificationEntryManager; +import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.volume.CarVolumeDialogComponent; import com.android.systemui.volume.VolumeDialogComponent; @@ -67,6 +69,12 @@ public class CarSystemUIFactory extends SystemUIFactory { return new CarNotificationEntryManager(context); } + @Override + public NotificationInterruptionStateProvider provideNotificationInterruptionStateProvider( + Context context) { + return new CarNotificationInterruptionStateProvider(context); + } + @Module protected static class ContextHolder { private Context mContext; diff --git a/packages/CarSystemUI/src/com/android/systemui/car/CarNotificationEntryManager.java b/packages/CarSystemUI/src/com/android/systemui/car/CarNotificationEntryManager.java index 0563418e5fb90..323cae0067d8e 100644 --- a/packages/CarSystemUI/src/com/android/systemui/car/CarNotificationEntryManager.java +++ b/packages/CarSystemUI/src/com/android/systemui/car/CarNotificationEntryManager.java @@ -18,7 +18,6 @@ package com.android.systemui.car; import android.content.Context; -import com.android.systemui.statusbar.notification.NotificationData; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; @@ -39,16 +38,4 @@ public class CarNotificationEntryManager extends NotificationEntryManager { // long click listener. return null; } - - @Override - public boolean shouldHeadsUp(NotificationData.Entry entry) { - // Because space is usually constrained in the auto use-case, there should not be a - // pinned notification when the shade has been expanded. Ensure this by not pinning any - // notification if the shade is already opened. - if (!getPresenter().isPresenterFullyCollapsed()) { - return false; - } - - return super.shouldHeadsUp(entry); - } } diff --git a/packages/CarSystemUI/src/com/android/systemui/car/CarNotificationInterruptionStateProvider.java b/packages/CarSystemUI/src/com/android/systemui/car/CarNotificationInterruptionStateProvider.java new file mode 100644 index 0000000000000..62502ef60e96d --- /dev/null +++ b/packages/CarSystemUI/src/com/android/systemui/car/CarNotificationInterruptionStateProvider.java @@ -0,0 +1,42 @@ +/* + * 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 com.android.systemui.car; + +import android.content.Context; + +import com.android.systemui.statusbar.notification.NotificationData; +import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider; + +/** Auto-specific implementation of {@link NotificationInterruptionStateProvider}. */ +public class CarNotificationInterruptionStateProvider extends + NotificationInterruptionStateProvider { + public CarNotificationInterruptionStateProvider(Context context) { + super(context); + } + + @Override + public boolean shouldHeadsUp(NotificationData.Entry entry) { + // Because space is usually constrained in the auto use-case, there should not be a + // pinned notification when the shade has been expanded. Ensure this by not pinning any + // notification if the shade is already opened. + if (!getPresenter().isPresenterFullyCollapsed()) { + return false; + } + + return super.shouldHeadsUp(entry); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java index 445d156d289ca..6b4d07a44d34c 100644 --- a/packages/SystemUI/src/com/android/systemui/Dependency.java +++ b/packages/SystemUI/src/com/android/systemui/Dependency.java @@ -56,6 +56,8 @@ import com.android.systemui.statusbar.StatusBarStateController; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.notification.NotificationData.KeyguardEnvironment; import com.android.systemui.statusbar.notification.NotificationEntryManager; +import com.android.systemui.statusbar.notification.NotificationFilter; +import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider; import com.android.systemui.statusbar.notification.NotificationRowBinder; import com.android.systemui.statusbar.notification.VisualStabilityManager; import com.android.systemui.statusbar.notification.logging.NotificationLogger; @@ -256,6 +258,8 @@ public class Dependency extends SystemUI { @Inject Lazy mNotificationLogger; @Inject Lazy mNotificationViewHierarchyManager; @Inject Lazy mNotificationRowBinder; + @Inject Lazy mNotificationFilter; + @Inject Lazy mNotificationInterruptionStateProvider; @Inject Lazy mKeyguardDismissUtil; @Inject Lazy mSmartReplyController; @Inject Lazy mRemoteInputQuickSettingsDisabler; @@ -425,6 +429,9 @@ public class Dependency extends SystemUI { mProviders.put(NotificationViewHierarchyManager.class, mNotificationViewHierarchyManager::get); mProviders.put(NotificationRowBinder.class, mNotificationRowBinder::get); + mProviders.put(NotificationFilter.class, mNotificationFilter::get); + mProviders.put(NotificationInterruptionStateProvider.class, + mNotificationInterruptionStateProvider::get); mProviders.put(KeyguardDismissUtil.class, mKeyguardDismissUtil::get); mProviders.put(SmartReplyController.class, mSmartReplyController::get); mProviders.put(RemoteInputQuickSettingsDisabler.class, diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java index 7689dfc8d6b75..384a14c70e9c3 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java @@ -42,6 +42,7 @@ import com.android.systemui.statusbar.NotificationLockscreenUserManagerImpl; import com.android.systemui.statusbar.ScrimView; import com.android.systemui.statusbar.notification.NotificationData; import com.android.systemui.statusbar.notification.NotificationEntryManager; +import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.phone.KeyguardBouncer; import com.android.systemui.statusbar.phone.KeyguardEnvironmentImpl; @@ -198,6 +199,13 @@ public class SystemUIFactory { return new NotificationListener(context); } + @Singleton + @Provides + public NotificationInterruptionStateProvider provideNotificationInterruptionStateProvider( + Context context) { + return new NotificationInterruptionStateProvider(context); + } + @Module protected static class ContextHolder { private Context mContext; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationPresenter.java index ee0d1a2d5a51b..c945afd1cb39c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationPresenter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationPresenter.java @@ -15,7 +15,6 @@ */ package com.android.systemui.statusbar; -import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.NotificationRowBinder; import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; @@ -29,7 +28,6 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow */ public interface NotificationPresenter extends ExpandableNotificationRow.OnExpandClickListener, ActivatableNotificationView.OnActivatedListener, - NotificationEntryManager.Callback, NotificationRowBinder.BindRowCallback { /** * Returns true if the presenter is not visible. For example, it may not be necessary to do diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java index 25247470bd889..017cda741e5f9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java @@ -133,7 +133,6 @@ public class NotificationViewHierarchyManager { mAlwaysExpandNonGroupedNotification = res.getBoolean(R.bool.config_alwaysExpandNonGroupedNotifications); mStatusBarStateListener = new StatusBarStateListener(mBubbleController); - mEntryManager.setStatusBarStateListener(mStatusBarStateListener); Dependency.get(StatusBarStateController.class).addCallback(mStatusBarStateListener); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/AlertTransferListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/AlertTransferListener.java deleted file mode 100644 index 13e991ba24312..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/AlertTransferListener.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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 com.android.systemui.statusbar.notification; - -/** - * Listener interface for when NotificationEntryManager needs to tell - * NotificationGroupAlertTransferHelper things. Will eventually grow to be a general-purpose - * listening interface for the NotificationEntryManager. - */ -public interface AlertTransferListener { - /** - * Called when a new notification is posted. At this point, the notification is "pending": its - * views haven't been inflated yet and most of the system pretends like it doesn't exist yet. - */ - void onPendingEntryAdded(NotificationData.Entry entry); - - /** - * Called when an existing notification's views are reinflated (usually due to an update being - * posted to that notification). - */ - void onEntryReinflated(NotificationData.Entry entry); - - /** - * Called when a notification has been removed (either because the user swiped it away or - * because the developer retracted it). - */ - void onEntryRemoved(NotificationData.Entry entry); -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationData.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationData.java index ef7e0fe5791f4..433a994ac0cc2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationData.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationData.java @@ -27,20 +27,15 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_NOTIFICAT import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR; -import android.Manifest; import android.annotation.NonNull; -import android.app.AppGlobals; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.Person; import android.content.Context; -import android.content.pm.IPackageManager; -import android.content.pm.PackageManager; import android.graphics.drawable.Icon; import android.os.Bundle; import android.os.Parcelable; -import android.os.RemoteException; import android.os.SystemClock; import android.service.notification.NotificationListenerService.Ranking; import android.service.notification.NotificationListenerService.RankingMap; @@ -58,17 +53,13 @@ import com.android.internal.statusbar.StatusBarIcon; import com.android.internal.util.ArrayUtils; import com.android.internal.util.ContrastColorUtil; import com.android.systemui.Dependency; -import com.android.systemui.ForegroundServiceController; import com.android.systemui.statusbar.InflationTask; -import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.StatusBarIconView; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationGuts; import com.android.systemui.statusbar.notification.row.NotificationInflater.InflationFlag; import com.android.systemui.statusbar.phone.NotificationGroupManager; -import com.android.systemui.statusbar.phone.ShadeController; -import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.statusbar.policy.HeadsUpManager; import java.io.PrintWriter; @@ -83,14 +74,13 @@ import java.util.Objects; */ public class NotificationData { + private final NotificationFilter mNotificationFilter = Dependency.get(NotificationFilter.class); + /** * These dependencies are late init-ed */ private KeyguardEnvironment mEnvironment; - private ShadeController mShadeController; private NotificationMediaManager mMediaManager; - private ForegroundServiceController mFsc; - private NotificationLockscreenUserManager mUserManager; private HeadsUpManager mHeadsUpManager; @@ -120,6 +110,9 @@ public class NotificationData { @NonNull public List systemGeneratedSmartActions = Collections.emptyList(); public CharSequence[] smartReplies = new CharSequence[0]; + @VisibleForTesting + public int suppressedVisualEffects; + public boolean suspended; private Entry parent; // our parent (if we're in a group) private ArrayList children = new ArrayList(); @@ -183,6 +176,8 @@ public class NotificationData { smartReplies = ranking.getSmartReplies() == null ? new CharSequence[0] : ranking.getSmartReplies().toArray(new CharSequence[0]); + suppressedVisualEffects = ranking.getSuppressedVisualEffects(); + suspended = ranking.isSuspended(); } public void setInterruption() { @@ -625,6 +620,71 @@ public class NotificationData { if (row == null) return true; return row.canViewBeDismissed(); } + + boolean isExemptFromDndVisualSuppression() { + if (isNotificationBlockedByPolicy(notification.getNotification())) { + return false; + } + + if ((notification.getNotification().flags + & Notification.FLAG_FOREGROUND_SERVICE) != 0) { + return true; + } + if (notification.getNotification().isMediaNotification()) { + return true; + } + if (mIsSystemNotification != null && mIsSystemNotification) { + return true; + } + return false; + } + + private boolean shouldSuppressVisualEffect(int effect) { + if (isExemptFromDndVisualSuppression()) { + return false; + } + return (suppressedVisualEffects & effect) != 0; + } + + /** + * Returns whether {@link NotificationManager.Policy#SUPPRESSED_EFFECT_FULL_SCREEN_INTENT} + * is set for this entry. + */ + public boolean shouldSuppressFullScreenIntent() { + return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_FULL_SCREEN_INTENT); + } + + /** + * Returns whether {@link NotificationManager.Policy#SUPPRESSED_EFFECT_PEEK} + * is set for this entry. + */ + public boolean shouldSuppressPeek() { + return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_PEEK); + } + + /** + * Returns whether {@link NotificationManager.Policy#SUPPRESSED_EFFECT_STATUS_BAR} + * is set for this entry. + */ + public boolean shouldSuppressStatusBar() { + return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_STATUS_BAR); + } + + /** + * Returns whether {@link NotificationManager.Policy#SUPPRESSED_EFFECT_AMBIENT} + * is set for this entry. + */ + public boolean shouldSuppressAmbient() { + return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_AMBIENT); + } + + /** + * Returns whether {@link NotificationManager.Policy#SUPPRESSED_EFFECT_NOTIFICATION_LIST} + * is set for this entry. + */ + public boolean shouldSuppressNotificationList() { + return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_NOTIFICATION_LIST); + } } private final ArrayMap mEntries = new ArrayMap<>(); @@ -706,13 +766,6 @@ public class NotificationData { return mEnvironment; } - private ShadeController getShadeController() { - if (mShadeController == null) { - mShadeController = Dependency.get(ShadeController.class); - } - return mShadeController; - } - private NotificationMediaManager getMediaManager() { if (mMediaManager == null) { mMediaManager = Dependency.get(NotificationMediaManager.class); @@ -720,20 +773,6 @@ public class NotificationData { return mMediaManager; } - private ForegroundServiceController getFsc() { - if (mFsc == null) { - mFsc = Dependency.get(ForegroundServiceController.class); - } - return mFsc; - } - - private NotificationLockscreenUserManager getUserManager() { - if (mUserManager == null) { - mUserManager = Dependency.get(NotificationLockscreenUserManager.class); - } - return mUserManager; - } - /** * Returns the sorted list of active notifications (depending on {@link KeyguardEnvironment} * @@ -778,7 +817,7 @@ public class NotificationData { } public Entry remove(String key, RankingMap ranking) { - Entry removed = null; + Entry removed; synchronized (mEntries) { removed = mEntries.remove(key); } @@ -849,62 +888,12 @@ public class NotificationData { return Ranking.VISIBILITY_NO_OVERRIDE; } - public boolean shouldSuppressFullScreenIntent(Entry entry) { - return shouldSuppressVisualEffect(entry, SUPPRESSED_EFFECT_FULL_SCREEN_INTENT); - } - - public boolean shouldSuppressPeek(Entry entry) { - return shouldSuppressVisualEffect(entry, SUPPRESSED_EFFECT_PEEK); - } - - public boolean shouldSuppressStatusBar(Entry entry) { - return shouldSuppressVisualEffect(entry, SUPPRESSED_EFFECT_STATUS_BAR); - } - - public boolean shouldSuppressAmbient(Entry entry) { - return shouldSuppressVisualEffect(entry, SUPPRESSED_EFFECT_AMBIENT); - } - - public boolean shouldSuppressNotificationList(Entry entry) { - return shouldSuppressVisualEffect(entry, SUPPRESSED_EFFECT_NOTIFICATION_LIST); - } - - private boolean shouldSuppressVisualEffect(Entry entry, int effect) { - if (isExemptFromDndVisualSuppression(entry)) { - return false; - } - String key = entry.key; - if (mRankingMap != null) { - getRanking(key, mTmpRanking); - return (mTmpRanking.getSuppressedVisualEffects() & effect) != 0; - } - return false; - } - - protected boolean isExemptFromDndVisualSuppression(Entry entry) { - if (isNotificationBlockedByPolicy(entry.notification.getNotification())) { - return false; - } - - if ((entry.notification.getNotification().flags - & Notification.FLAG_FOREGROUND_SERVICE) != 0) { - return true; - } - if (entry.notification.getNotification().isMediaNotification()) { - return true; - } - if (entry.mIsSystemNotification != null && entry.mIsSystemNotification) { - return true; - } - return false; - } - /** * Categories that are explicitly called out on DND settings screens are always blocked, if * DND has flagged them, even if they are foreground or system notifications that might * otherwise visually bypass DND. */ - protected boolean isNotificationBlockedByPolicy(Notification n) { + private static boolean isNotificationBlockedByPolicy(Notification n) { if (isCategory(CATEGORY_CALL, n) || isCategory(CATEGORY_MESSAGE, n) || isCategory(CATEGORY_ALARM, n) @@ -915,7 +904,7 @@ public class NotificationData { return false; } - private boolean isCategory(String category, Notification n) { + private static boolean isCategory(String category, Notification n) { return Objects.equals(n.category, category); } @@ -1013,7 +1002,7 @@ public class NotificationData { for (int i = 0; i < N; i++) { Entry entry = mEntries.valueAt(i); - if (shouldFilterOut(entry)) { + if (mNotificationFilter.shouldFilterOut(entry)) { continue; } @@ -1024,87 +1013,6 @@ public class NotificationData { Collections.sort(mSortedAndFiltered, mRankingComparator); } - /** - * @return true if this notification should NOT be shown right now - */ - public boolean shouldFilterOut(Entry entry) { - final StatusBarNotification sbn = entry.notification; - if (!(getEnvironment().isDeviceProvisioned() || - showNotificationEvenIfUnprovisioned(sbn))) { - return true; - } - - if (!getEnvironment().isNotificationForCurrentProfiles(sbn)) { - return true; - } - - if (getUserManager().isLockscreenPublicMode(sbn.getUserId()) && - (sbn.getNotification().visibility == Notification.VISIBILITY_SECRET - || getUserManager().shouldHideNotifications(sbn.getUserId()) - || getUserManager().shouldHideNotifications(sbn.getKey()))) { - return true; - } - - if (getShadeController().isDozing() && shouldSuppressAmbient(entry)) { - return true; - } - - if (!getShadeController().isDozing() && shouldSuppressNotificationList(entry)) { - return true; - } - - if (shouldHide(sbn.getKey())) { - return true; - } - - if (!StatusBar.ENABLE_CHILD_NOTIFICATIONS - && mGroupManager.isChildInGroupWithSummary(sbn)) { - return true; - } - - if (getFsc().isDungeonNotification(sbn) - && !getFsc().isDungeonNeededForUser(sbn.getUserId())) { - // this is a foreground-service disclosure for a user that does not need to show one - return true; - } - if (getFsc().isSystemAlertNotification(sbn)) { - final String[] apps = sbn.getNotification().extras.getStringArray( - Notification.EXTRA_FOREGROUND_APPS); - if (apps != null && apps.length >= 1) { - if (!getFsc().isSystemAlertWarningNeeded(sbn.getUserId(), apps[0])) { - return true; - } - } - } - - return false; - } - - // Q: What kinds of notifications should show during setup? - // A: Almost none! Only things coming from packages with permission - // android.permission.NOTIFICATION_DURING_SETUP that also have special "kind" tags marking them - // as relevant for setup (see below). - public static boolean showNotificationEvenIfUnprovisioned(StatusBarNotification sbn) { - return showNotificationEvenIfUnprovisioned(AppGlobals.getPackageManager(), sbn); - } - - @VisibleForTesting - static boolean showNotificationEvenIfUnprovisioned(IPackageManager packageManager, - StatusBarNotification sbn) { - return checkUidPermission(packageManager, Manifest.permission.NOTIFICATION_DURING_SETUP, - sbn.getUid()) == PackageManager.PERMISSION_GRANTED - && sbn.getNotification().extras.getBoolean(Notification.EXTRA_ALLOW_DURING_SETUP); - } - - private static int checkUidPermission(IPackageManager packageManager, String permission, - int uid) { - try { - return packageManager.checkUidPermission(permission, uid); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - public void dump(PrintWriter pw, String indent) { int N = mSortedAndFiltered.size(); pw.print(indent); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryListener.java new file mode 100644 index 0000000000000..361ae8b1920ad --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryListener.java @@ -0,0 +1,75 @@ +/* + * 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 com.android.systemui.statusbar.notification; + +import android.service.notification.StatusBarNotification; + +/** + * Listener interface for changes sent by NotificationEntryManager. + */ +public interface NotificationEntryListener { + /** + * Called when a new notification is posted. At this point, the notification is "pending": its + * views haven't been inflated yet and most of the system pretends like it doesn't exist yet. + */ + default void onPendingEntryAdded(NotificationData.Entry entry) { + } + + /** + * Called when a new entry is created. + */ + default void onNotificationAdded(NotificationData.Entry entry) { + } + + /** + * Called when a notification was updated. + */ + default void onNotificationUpdated(StatusBarNotification notification) { + } + + /** + * Called when an existing notification's views are reinflated (usually due to an update being + * posted to that notification). + */ + default void onEntryReinflated(NotificationData.Entry entry) { + } + + /** + * Called when a notification has been removed (either because the user swiped it away or + * because the developer retracted it). + * + * TODO: combine this with onNotificationRemoved(). + */ + default void onEntryRemoved(NotificationData.Entry entry) { + } + + /** + * Called when a notification was removed. + * + * @param key key of notification that was removed + * @param old StatusBarNotification of the notification before it was removed + */ + default void onNotificationRemoved(String key, StatusBarNotification old) { + } + + /** + * Removes a notification immediately. + * + * TODO: combine this with onNotificationRemoved(). + */ + default void onPerformRemoveNotification(StatusBarNotification statusBarNotification) { + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java index 535ea624dfc27..98ddd6b9cc5cc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java @@ -17,43 +17,30 @@ package com.android.systemui.statusbar.notification; import static com.android.systemui.bubbles.BubbleController.DEBUG_DEMOTE_TO_NOTIF; import static com.android.systemui.statusbar.NotificationRemoteInputManager.FORCE_REMOTE_INPUT_HISTORY; -import static com.android.systemui.statusbar.StatusBarState.SHADE; import static com.android.systemui.statusbar.notification.row.NotificationInflater.FLAG_CONTENT_VIEW_AMBIENT; import static com.android.systemui.statusbar.notification.row.NotificationInflater.FLAG_CONTENT_VIEW_HEADS_UP; import android.annotation.Nullable; import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; import android.content.Context; -import android.database.ContentObserver; -import android.os.Bundle; import android.os.Handler; import android.os.PowerManager; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; -import android.provider.Settings; -import android.service.dreams.DreamService; -import android.service.dreams.IDreamManager; import android.service.notification.NotificationListenerService; import android.service.notification.NotificationStats; import android.service.notification.StatusBarNotification; -import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; -import android.util.EventLog; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.logging.MetricsLogger; import com.android.internal.statusbar.IStatusBarService; import com.android.internal.statusbar.NotificationVisibility; import com.android.systemui.Dependency; import com.android.systemui.Dumpable; -import com.android.systemui.EventLogTags; import com.android.systemui.ForegroundServiceController; -import com.android.systemui.UiOffloadThread; import com.android.systemui.bubbles.BubbleController; import com.android.systemui.statusbar.AlertingNotificationManager; import com.android.systemui.statusbar.AmbientPulseManager; @@ -64,7 +51,6 @@ import com.android.systemui.statusbar.NotificationPresenter; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.NotificationUiAdjustment; import com.android.systemui.statusbar.NotificationUpdateHandler; -import com.android.systemui.statusbar.NotificationViewHierarchyManager; import com.android.systemui.statusbar.notification.NotificationData.KeyguardEnvironment; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationGutsManager; @@ -97,8 +83,6 @@ public class NotificationEntryManager implements BubbleController.BubbleDismissListener { private static final String TAG = "NotificationEntryMgr"; protected static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - private static final boolean ENABLE_HEADS_UP = true; - private static final String SETTING_HEADS_UP_TICKER = "ticker_gets_heads_up"; public static final long RECENTLY_ALERTED_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(30); @@ -109,7 +93,6 @@ public class NotificationEntryManager implements Dependency.get(NotificationGroupManager.class); private final NotificationGutsManager mGutsManager = Dependency.get(NotificationGutsManager.class); - private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class); private final DeviceProvisionedController mDeviceProvisionedController = Dependency.get(DeviceProvisionedController.class); private final VisualStabilityManager mVisualStabilityManager = @@ -119,6 +102,8 @@ public class NotificationEntryManager implements private final AmbientPulseManager mAmbientPulseManager = Dependency.get(AmbientPulseManager.class); private final BubbleController mBubbleController = Dependency.get(BubbleController.class); + private final NotificationInterruptionStateProvider mNotificationInterruptionStateProvider = + Dependency.get(NotificationInterruptionStateProvider.class); // Lazily retrieved dependencies private NotificationRemoteInputManager mRemoteInputManager; @@ -130,23 +115,18 @@ public class NotificationEntryManager implements private final Handler mDeferredNotificationViewUpdateHandler; private Runnable mUpdateNotificationViewsCallback; - protected IDreamManager mDreamManager; protected IStatusBarService mBarService; private NotificationPresenter mPresenter; - private Callback mCallback; + private NotificationEntryListener mCallback; protected PowerManager mPowerManager; private NotificationListenerService.RankingMap mLatestRankingMap; protected HeadsUpManager mHeadsUpManager; protected NotificationData mNotificationData; - private ContentObserver mHeadsUpObserver; - protected boolean mUseHeadsUp = false; - private boolean mDisableNotificationAlerts; protected NotificationListContainer mListContainer; @VisibleForTesting final ArrayList mNotificationLifetimeExtenders = new ArrayList<>(); - private NotificationViewHierarchyManager.StatusBarStateListener mStatusBarStateListener; - @Nullable private AlertTransferListener mAlertTransferListener; + private final List mNotificationEntryListeners = new ArrayList<>(); private final DeviceProvisionedController.DeviceProvisionedListener mDeviceProvisionedListener = @@ -157,11 +137,6 @@ public class NotificationEntryManager implements } }; - public void setDisableNotificationAlerts(boolean disableNotificationAlerts) { - mDisableNotificationAlerts = disableNotificationAlerts; - mHeadsUpObserver.onChange(true); - } - public void destroy() { mDeviceProvisionedController.removeCallback(mDeviceProvisionedListener); } @@ -177,8 +152,6 @@ public class NotificationEntryManager implements pw.println(entry.notification); } } - pw.print(" mUseHeadsUp="); - pw.println(mUseHeadsUp); } public NotificationEntryManager(Context context) { @@ -186,15 +159,14 @@ public class NotificationEntryManager implements mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); mBarService = IStatusBarService.Stub.asInterface( ServiceManager.getService(Context.STATUS_BAR_SERVICE)); - mDreamManager = IDreamManager.Stub.asInterface( - ServiceManager.checkService(DreamService.DREAM_SERVICE)); mBubbleController.setDismissListener(this /* bubbleEventListener */); mNotificationData = new NotificationData(); mDeferredNotificationViewUpdateHandler = new Handler(); } - public void setAlertTransferListener(AlertTransferListener listener) { - mAlertTransferListener = listener; + /** Adds a {@link NotificationEntryListener}. */ + public void addNotificationEntryListener(NotificationEntryListener listener) { + mNotificationEntryListeners.add(listener); } /** @@ -236,7 +208,7 @@ public class NotificationEntryManager implements } public void setUpWithPresenter(NotificationPresenter presenter, - NotificationListContainer listContainer, Callback callback, + NotificationListContainer listContainer, NotificationEntryListener callback, HeadsUpManager headsUpManager) { mPresenter = presenter; mUpdateNotificationViewsCallback = mPresenter::updateNotificationViews; @@ -245,36 +217,6 @@ public class NotificationEntryManager implements mNotificationData.setHeadsUpManager(mHeadsUpManager); mListContainer = listContainer; - mHeadsUpObserver = new ContentObserver(Dependency.get(Dependency.MAIN_HANDLER)) { - @Override - public void onChange(boolean selfChange) { - boolean wasUsing = mUseHeadsUp; - mUseHeadsUp = ENABLE_HEADS_UP && !mDisableNotificationAlerts - && Settings.Global.HEADS_UP_OFF != Settings.Global.getInt( - mContext.getContentResolver(), - Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED, - Settings.Global.HEADS_UP_OFF); - Log.d(TAG, "heads up is " + (mUseHeadsUp ? "enabled" : "disabled")); - if (wasUsing != mUseHeadsUp) { - if (!mUseHeadsUp) { - Log.d(TAG, - "dismissing any existing heads up notification on disable event"); - mHeadsUpManager.releaseAllImmediately(); - } - } - } - }; - - if (ENABLE_HEADS_UP) { - mContext.getContentResolver().registerContentObserver( - Settings.Global.getUriFor(Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED), - true, - mHeadsUpObserver); - mContext.getContentResolver().registerContentObserver( - Settings.Global.getUriFor(SETTING_HEADS_UP_TICKER), true, - mHeadsUpObserver); - } - mNotificationLifetimeExtenders.add(mHeadsUpManager); mNotificationLifetimeExtenders.add(mAmbientPulseManager); mNotificationLifetimeExtenders.add(mGutsManager); @@ -285,20 +227,6 @@ public class NotificationEntryManager implements } mDeviceProvisionedController.addCallback(mDeviceProvisionedListener); - - mHeadsUpObserver.onChange(true); // set up - - getRowBinder().setInterruptionStateProvider(new InterruptionStateProvider() { - @Override - public boolean shouldHeadsUp(NotificationData.Entry entry) { - return NotificationEntryManager.this.shouldHeadsUp(entry); - } - - @Override - public boolean shouldPulse(NotificationData.Entry entry) { - return NotificationEntryManager.this.shouldPulse(entry); - } - }); } public NotificationData getNotificationData() { @@ -322,14 +250,6 @@ public class NotificationEntryManager implements updateNotifications(); } - private boolean shouldSuppressFullScreenIntent(NotificationData.Entry entry) { - if (mPresenter.isDeviceInVrMode()) { - return true; - } - - return mNotificationData.shouldSuppressFullScreenIntent(entry); - } - public void performRemoveNotification(StatusBarNotification n) { final int rank = mNotificationData.getRank(n.getKey()); final int count = mNotificationData.getActiveNotifications().size(); @@ -441,7 +361,7 @@ public class NotificationEntryManager implements if ((inflatedFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) { // Possible for shouldHeadsUp to change between the inflation starting and ending. // If it does and we no longer need to heads up, we should free the view. - if (shouldHeadsUp(entry)) { + if (mNotificationInterruptionStateProvider.shouldHeadsUp(entry)) { mHeadsUpManager.showNotification(entry); // Mark as seen immediately setNotificationShown(entry.notification); @@ -450,7 +370,7 @@ public class NotificationEntryManager implements } } if ((inflatedFlags & FLAG_CONTENT_VIEW_AMBIENT) != 0) { - if (shouldPulse(entry)) { + if (mNotificationInterruptionStateProvider.shouldPulse(entry)) { mAmbientPulseManager.showNotification(entry); } else { entry.freeContentViewWhenSafe(FLAG_CONTENT_VIEW_AMBIENT); @@ -474,8 +394,8 @@ public class NotificationEntryManager implements mVisualStabilityManager.onLowPriorityUpdated(entry); mPresenter.updateNotificationViews(); } - if (mAlertTransferListener != null) { - mAlertTransferListener.onEntryReinflated(entry); + for (NotificationEntryListener listener : mNotificationEntryListeners) { + listener.onEntryReinflated(entry); } } } @@ -492,8 +412,10 @@ public class NotificationEntryManager implements final NotificationData.Entry entry = mNotificationData.get(key); abortExistingInflation(key); - if (mAlertTransferListener != null && entry != null) { - mAlertTransferListener.onEntryRemoved(entry); + if (entry != null) { + for (NotificationEntryListener listener : mNotificationEntryListeners) { + listener.onEntryRemoved(entry); + } } // Attempt to remove notifications from their alert managers (heads up, ambient pulse). @@ -650,51 +572,15 @@ public class NotificationEntryManager implements mNotificationData.updateRanking(rankingMap); NotificationListenerService.Ranking ranking = new NotificationListenerService.Ranking(); rankingMap.getRanking(key, ranking); - NotificationData.Entry shadeEntry = createNotificationEntry(notification, ranking); - boolean isHeadsUped = shouldHeadsUp(shadeEntry); - if (!isHeadsUped && notification.getNotification().fullScreenIntent != null) { - if (shouldSuppressFullScreenIntent(shadeEntry)) { - if (DEBUG) { - Log.d(TAG, "No Fullscreen intent: suppressed by DND: " + key); - } - } else if (mNotificationData.getImportance(key) - < NotificationManager.IMPORTANCE_HIGH) { - if (DEBUG) { - Log.d(TAG, "No Fullscreen intent: not important enough: " - + key); - } - } else { - // Stop screensaver if the notification has a fullscreen intent. - // (like an incoming phone call) - Dependency.get(UiOffloadThread.class).submit(() -> { - try { - mDreamManager.awaken(); - } catch (RemoteException e) { - e.printStackTrace(); - } - }); - - // not immersive & a fullscreen alert should be shown - if (DEBUG) - Log.d(TAG, "Notification has fullScreenIntent; sending fullScreenIntent"); - try { - EventLog.writeEvent(EventLogTags.SYSUI_FULLSCREEN_NOTIFICATION, - key); - notification.getNotification().fullScreenIntent.send(); - shadeEntry.notifyFullScreenIntentLaunched(); - mMetricsLogger.count("note_fullscreen", 1); - } catch (PendingIntent.CanceledException e) { - } - } - } + NotificationData.Entry entry = createNotificationEntry(notification, ranking); abortExistingInflation(key); mForegroundServiceController.addNotification(notification, mNotificationData.getImportance(key)); - mPendingNotifications.put(key, shadeEntry); - if (mAlertTransferListener != null) { - mAlertTransferListener.onPendingEntryAdded(shadeEntry); + mPendingNotifications.put(key, entry); + for (NotificationEntryListener listener : mNotificationEntryListeners) { + listener.onPendingEntryAdded(entry); } } @@ -767,9 +653,11 @@ public class NotificationEntryManager implements boolean alertAgain = alertAgain(entry, entry.notification.getNotification()); if (getShadeController().isDozing()) { - updateAlertState(entry, shouldPulse(entry), alertAgain, mAmbientPulseManager); + updateAlertState(entry, mNotificationInterruptionStateProvider.shouldPulse(entry), + alertAgain, mAmbientPulseManager); } else { - updateAlertState(entry, shouldHeadsUp(entry), alertAgain, mHeadsUpManager); + updateAlertState(entry, mNotificationInterruptionStateProvider.shouldHeadsUp(entry), + alertAgain, mHeadsUpManager); } updateNotifications(); @@ -828,7 +716,7 @@ public class NotificationEntryManager implements // By comparing the old and new UI adjustments, reinflate the view accordingly. for (NotificationData.Entry entry : entries) { - mNotificationRowBinder.onNotificationRankingUpdated( + getRowBinder().onNotificationRankingUpdated( entry, oldImportances.get(entry.key), oldAdjustments.get(entry.key), @@ -851,182 +739,6 @@ public class NotificationEntryManager implements } } - public void setStatusBarStateListener( - NotificationViewHierarchyManager.StatusBarStateListener listener) { - mStatusBarStateListener = listener; - } - - /** - * Whether the notification should peek in from the top and alert the user. - * - * @param entry the entry to check - * @return true if the entry should heads up, false otherwise - */ - @VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED) - public boolean shouldHeadsUp(NotificationData.Entry entry) { - StatusBarNotification sbn = entry.notification; - - if (getShadeController().isDozing()) { - if (DEBUG) { - Log.d(TAG, "No heads up: device is dozing: " + sbn.getKey()); - } - return false; - } - - // TODO: need to changes this, e.g. should still heads up in expanded shade, might want - // message bubble from the bubble to go through heads up path - boolean inShade = mStatusBarStateListener != null - && mStatusBarStateListener.getCurrentState() == SHADE; - if (entry.isBubble() && !entry.isBubbleDismissed() && inShade) { - return false; - } - - if (!canAlertCommon(entry)) { - if (DEBUG) { - Log.d(TAG, "No heads up: notification shouldn't alert: " + sbn.getKey()); - } - return false; - } - - if (!mUseHeadsUp || mPresenter.isDeviceInVrMode()) { - if (DEBUG) { - Log.d(TAG, "No heads up: no huns or vr mode"); - } - return false; - } - - boolean isDreaming = false; - try { - isDreaming = mDreamManager.isDreaming(); - } catch (RemoteException e) { - Log.e(TAG, "Failed to query dream manager.", e); - } - boolean inUse = mPowerManager.isScreenOn() && !isDreaming; - - if (!inUse) { - if (DEBUG) { - Log.d(TAG, "No heads up: not in use: " + sbn.getKey()); - } - return false; - } - - if (mNotificationData.shouldSuppressPeek(entry)) { - if (DEBUG) { - Log.d(TAG, "No heads up: suppressed by DND: " + sbn.getKey()); - } - return false; - } - - if (isSnoozedPackage(sbn)) { - if (DEBUG) { - Log.d(TAG, "No heads up: snoozed package: " + sbn.getKey()); - } - return false; - } - - if (entry.hasJustLaunchedFullScreenIntent()) { - if (DEBUG) { - Log.d(TAG, "No heads up: recent fullscreen: " + sbn.getKey()); - } - return false; - } - - if (mNotificationData.getImportance(sbn.getKey()) < NotificationManager.IMPORTANCE_HIGH) { - if (DEBUG) { - Log.d(TAG, "No heads up: unimportant notification: " + sbn.getKey()); - } - return false; - } - - if (!mCallback.canHeadsUp(entry, sbn)) { - return false; - } - - return true; - } - - /** - * Whether or not the notification should "pulse" on the user's display when the phone is - * dozing. This displays the ambient view of the notification. - * - * @param entry the entry to check - * @return true if the entry should ambient pulse, false otherwise - */ - private boolean shouldPulse(NotificationData.Entry entry) { - StatusBarNotification sbn = entry.notification; - - if (!getShadeController().isDozing()) { - if (DEBUG) { - Log.d(TAG, "No pulsing: not dozing: " + sbn.getKey()); - } - return false; - } - - if (!canAlertCommon(entry)) { - if (DEBUG) { - Log.d(TAG, "No pulsing: notification shouldn't alert: " + sbn.getKey()); - } - return false; - } - - if (mNotificationData.shouldSuppressAmbient(entry)) { - if (DEBUG) { - Log.d(TAG, "No pulsing: ambient effect suppressed: " + sbn.getKey()); - } - return false; - } - - if (mNotificationData.getImportance(sbn.getKey()) - < NotificationManager.IMPORTANCE_DEFAULT) { - if (DEBUG) { - Log.d(TAG, "No pulsing: not important enough: " + sbn.getKey()); - } - return false; - } - - Bundle extras = sbn.getNotification().extras; - CharSequence title = extras.getCharSequence(Notification.EXTRA_TITLE); - CharSequence text = extras.getCharSequence(Notification.EXTRA_TEXT); - if (TextUtils.isEmpty(title) && TextUtils.isEmpty(text)) { - if (DEBUG) { - Log.d(TAG, "No pulsing: title and text are empty: " + sbn.getKey()); - } - return false; - } - - return true; - } - - /** - * Common checks between heads up alerting and ambient pulse alerting. See - * {@link NotificationEntryManager#shouldHeadsUp(NotificationData.Entry)} and - * {@link NotificationEntryManager#shouldPulse(NotificationData.Entry)}. Notifications that - * fail any of these checks should not alert at all. - * - * @param entry the entry to check - * @return true if these checks pass, false if the notification should not alert - */ - protected boolean canAlertCommon(NotificationData.Entry entry) { - StatusBarNotification sbn = entry.notification; - - if (mNotificationData.shouldFilterOut(entry)) { - if (DEBUG) { - Log.d(TAG, "No alerting: filtered notification: " + sbn.getKey()); - } - return false; - } - - // Don't alert notifications that are suppressed due to group alert behavior - if (sbn.isGroup() && sbn.getNotification().suppressAlertingDueToGrouping()) { - if (DEBUG) { - Log.d(TAG, "No alerting: suppressed due to group alert behavior"); - } - return false; - } - - return true; - } - private void setNotificationShown(StatusBarNotification n) { setNotificationsShown(new String[]{n.getKey()}); } @@ -1039,10 +751,6 @@ public class NotificationEntryManager implements } } - private boolean isSnoozedPackage(StatusBarNotification sbn) { - return mHeadsUpManager.isSnoozed(sbn.getPackageName()); - } - /** * Update the entry's alert state and call the appropriate {@link AlertingNotificationManager} * method. @@ -1076,64 +784,4 @@ public class NotificationEntryManager implements public Iterable getPendingNotificationsIterator() { return mPendingNotifications.values(); } - - /** - * Interface for retrieving heads-up and pulsing state for an entry. - */ - public interface InterruptionStateProvider { - /** - * Whether the provided entry should be marked as heads-up when inflated. - */ - boolean shouldHeadsUp(NotificationData.Entry entry); - - /** - * Whether the provided entry should be marked as pulsing (displayed in ambient) when - * inflated. - */ - boolean shouldPulse(NotificationData.Entry entry); - } - - /** - * Callback for NotificationEntryManager. - */ - public interface Callback { - - /** - * Called when a new entry is created. - * - * @param shadeEntry entry that was created - */ - void onNotificationAdded(NotificationData.Entry shadeEntry); - - /** - * Called when a notification was updated. - * - * @param notification notification that was updated - */ - void onNotificationUpdated(StatusBarNotification notification); - - /** - * Called when a notification was removed. - * - * @param key key of notification that was removed - * @param old StatusBarNotification of the notification before it was removed - */ - void onNotificationRemoved(String key, StatusBarNotification old); - - /** - * Removes a notification immediately. - * - * @param statusBarNotification notification that is being removed - */ - void onPerformRemoveNotification(StatusBarNotification statusBarNotification); - - /** - * Returns true if NotificationEntryManager can heads up this notification. - * - * @param entry entry of the notification that might be heads upped - * @param sbn notification that might be heads upped - * @return true if the notification can be heads upped - */ - boolean canHeadsUp(NotificationData.Entry entry, StatusBarNotification sbn); - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationFilter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationFilter.java new file mode 100644 index 0000000000000..5e99c388655f9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationFilter.java @@ -0,0 +1,162 @@ +/* + * 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 com.android.systemui.statusbar.notification; + +import android.Manifest; +import android.app.AppGlobals; +import android.app.Notification; +import android.content.pm.IPackageManager; +import android.content.pm.PackageManager; +import android.os.RemoteException; +import android.service.notification.StatusBarNotification; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.Dependency; +import com.android.systemui.ForegroundServiceController; +import com.android.systemui.statusbar.NotificationLockscreenUserManager; +import com.android.systemui.statusbar.phone.NotificationGroupManager; +import com.android.systemui.statusbar.phone.ShadeController; +import com.android.systemui.statusbar.phone.StatusBar; + +import javax.inject.Inject; +import javax.inject.Singleton; + +/** Component which manages the various reasons a notification might be filtered out. */ +@Singleton +public class NotificationFilter { + + private final NotificationGroupManager mGroupManager = Dependency.get( + NotificationGroupManager.class); + + private NotificationData.KeyguardEnvironment mEnvironment; + private ShadeController mShadeController; + private ForegroundServiceController mFsc; + private NotificationLockscreenUserManager mUserManager; + + @Inject + public NotificationFilter() {} + + private NotificationData.KeyguardEnvironment getEnvironment() { + if (mEnvironment == null) { + mEnvironment = Dependency.get(NotificationData.KeyguardEnvironment.class); + } + return mEnvironment; + } + + private ShadeController getShadeController() { + if (mShadeController == null) { + mShadeController = Dependency.get(ShadeController.class); + } + return mShadeController; + } + + private ForegroundServiceController getFsc() { + if (mFsc == null) { + mFsc = Dependency.get(ForegroundServiceController.class); + } + return mFsc; + } + + private NotificationLockscreenUserManager getUserManager() { + if (mUserManager == null) { + mUserManager = Dependency.get(NotificationLockscreenUserManager.class); + } + return mUserManager; + } + + + /** + * @return true if the provided notification should NOT be shown right now. + */ + public boolean shouldFilterOut(NotificationData.Entry entry) { + final StatusBarNotification sbn = entry.notification; + if (!(getEnvironment().isDeviceProvisioned() + || showNotificationEvenIfUnprovisioned(sbn))) { + return true; + } + + if (!getEnvironment().isNotificationForCurrentProfiles(sbn)) { + return true; + } + + if (getUserManager().isLockscreenPublicMode(sbn.getUserId()) + && (sbn.getNotification().visibility == Notification.VISIBILITY_SECRET + || getUserManager().shouldHideNotifications(sbn.getUserId()) + || getUserManager().shouldHideNotifications(sbn.getKey()))) { + return true; + } + + if (getShadeController().isDozing() && entry.shouldSuppressAmbient()) { + return true; + } + + if (!getShadeController().isDozing() && entry.shouldSuppressNotificationList()) { + return true; + } + + if (entry.suspended) { + return true; + } + + if (!StatusBar.ENABLE_CHILD_NOTIFICATIONS + && mGroupManager.isChildInGroupWithSummary(sbn)) { + return true; + } + + if (getFsc().isDungeonNotification(sbn) + && !getFsc().isDungeonNeededForUser(sbn.getUserId())) { + // this is a foreground-service disclosure for a user that does not need to show one + return true; + } + if (getFsc().isSystemAlertNotification(sbn)) { + final String[] apps = sbn.getNotification().extras.getStringArray( + Notification.EXTRA_FOREGROUND_APPS); + if (apps != null && apps.length >= 1) { + if (!getFsc().isSystemAlertWarningNeeded(sbn.getUserId(), apps[0])) { + return true; + } + } + } + + return false; + } + + // Q: What kinds of notifications should show during setup? + // A: Almost none! Only things coming from packages with permission + // android.permission.NOTIFICATION_DURING_SETUP that also have special "kind" tags marking them + // as relevant for setup (see below). + private static boolean showNotificationEvenIfUnprovisioned(StatusBarNotification sbn) { + return showNotificationEvenIfUnprovisioned(AppGlobals.getPackageManager(), sbn); + } + + @VisibleForTesting + static boolean showNotificationEvenIfUnprovisioned(IPackageManager packageManager, + StatusBarNotification sbn) { + return checkUidPermission(packageManager, Manifest.permission.NOTIFICATION_DURING_SETUP, + sbn.getUid()) == PackageManager.PERMISSION_GRANTED + && sbn.getNotification().extras.getBoolean(Notification.EXTRA_ALLOW_DURING_SETUP); + } + + private static int checkUidPermission(IPackageManager packageManager, String permission, + int uid) { + try { + return packageManager.checkUidPermission(permission, uid); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationInterruptionStateProvider.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationInterruptionStateProvider.java new file mode 100644 index 0000000000000..8bd0e9ad3c8b8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationInterruptionStateProvider.java @@ -0,0 +1,332 @@ +/* + * 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 com.android.systemui.statusbar.notification; + +import static com.android.systemui.statusbar.StatusBarState.SHADE; + +import android.app.Notification; +import android.app.NotificationManager; +import android.content.Context; +import android.database.ContentObserver; +import android.os.Bundle; +import android.os.PowerManager; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.provider.Settings; +import android.service.dreams.DreamService; +import android.service.dreams.IDreamManager; +import android.service.notification.StatusBarNotification; +import android.text.TextUtils; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.Dependency; +import com.android.systemui.statusbar.NotificationPresenter; +import com.android.systemui.statusbar.StatusBarStateController; +import com.android.systemui.statusbar.phone.ShadeController; +import com.android.systemui.statusbar.policy.HeadsUpManager; + +/** + * Provides heads-up and pulsing state for notification entries. + */ +public class NotificationInterruptionStateProvider { + + private static final String TAG = "InterruptionStateProvider"; + private static final boolean DEBUG = false; + private static final boolean ENABLE_HEADS_UP = true; + private static final String SETTING_HEADS_UP_TICKER = "ticker_gets_heads_up"; + + private final StatusBarStateController mStatusBarStateController = + Dependency.get(StatusBarStateController.class); + private final NotificationFilter mNotificationFilter = Dependency.get(NotificationFilter.class); + + private final Context mContext; + private final PowerManager mPowerManager; + private final IDreamManager mDreamManager; + + private NotificationPresenter mPresenter; + private ShadeController mShadeController; + private HeadsUpManager mHeadsUpManager; + private HeadsUpSuppressor mHeadsUpSuppressor; + + private ContentObserver mHeadsUpObserver; + @VisibleForTesting + protected boolean mUseHeadsUp = false; + private boolean mDisableNotificationAlerts; + + public NotificationInterruptionStateProvider(Context context) { + this(context, + (PowerManager) context.getSystemService(Context.POWER_SERVICE), + IDreamManager.Stub.asInterface( + ServiceManager.checkService(DreamService.DREAM_SERVICE))); + } + + @VisibleForTesting + protected NotificationInterruptionStateProvider( + Context context, + PowerManager powerManager, + IDreamManager dreamManager) { + mContext = context; + mPowerManager = powerManager; + mDreamManager = dreamManager; + } + + /** Sets up late-binding dependencies for this component. */ + public void setUpWithPresenter( + NotificationPresenter notificationPresenter, + HeadsUpManager headsUpManager, + HeadsUpSuppressor headsUpSuppressor) { + mPresenter = notificationPresenter; + mHeadsUpManager = headsUpManager; + mHeadsUpSuppressor = headsUpSuppressor; + + mHeadsUpObserver = new ContentObserver(Dependency.get(Dependency.MAIN_HANDLER)) { + @Override + public void onChange(boolean selfChange) { + boolean wasUsing = mUseHeadsUp; + mUseHeadsUp = ENABLE_HEADS_UP && !mDisableNotificationAlerts + && Settings.Global.HEADS_UP_OFF != Settings.Global.getInt( + mContext.getContentResolver(), + Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED, + Settings.Global.HEADS_UP_OFF); + Log.d(TAG, "heads up is " + (mUseHeadsUp ? "enabled" : "disabled")); + if (wasUsing != mUseHeadsUp) { + if (!mUseHeadsUp) { + Log.d(TAG, + "dismissing any existing heads up notification on disable event"); + mHeadsUpManager.releaseAllImmediately(); + } + } + } + }; + + if (ENABLE_HEADS_UP) { + mContext.getContentResolver().registerContentObserver( + Settings.Global.getUriFor(Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED), + true, + mHeadsUpObserver); + mContext.getContentResolver().registerContentObserver( + Settings.Global.getUriFor(SETTING_HEADS_UP_TICKER), true, + mHeadsUpObserver); + } + mHeadsUpObserver.onChange(true); // set up + } + + private ShadeController getShadeController() { + if (mShadeController == null) { + mShadeController = Dependency.get(ShadeController.class); + } + return mShadeController; + } + + /** + * Whether the notification should peek in from the top and alert the user. + * + * @param entry the entry to check + * @return true if the entry should heads up, false otherwise + */ + public boolean shouldHeadsUp(NotificationData.Entry entry) { + StatusBarNotification sbn = entry.notification; + + if (getShadeController().isDozing()) { + if (DEBUG) { + Log.d(TAG, "No heads up: device is dozing: " + sbn.getKey()); + } + return false; + } + + // TODO: need to changes this, e.g. should still heads up in expanded shade, might want + // message bubble from the bubble to go through heads up path + boolean inShade = mStatusBarStateController.getState() == SHADE; + if (entry.isBubble() && !entry.isBubbleDismissed() && inShade) { + return false; + } + + if (!canAlertCommon(entry)) { + if (DEBUG) { + Log.d(TAG, "No heads up: notification shouldn't alert: " + sbn.getKey()); + } + return false; + } + + if (!mUseHeadsUp || mPresenter.isDeviceInVrMode()) { + if (DEBUG) { + Log.d(TAG, "No heads up: no huns or vr mode"); + } + return false; + } + + boolean isDreaming = false; + try { + isDreaming = mDreamManager.isDreaming(); + } catch (RemoteException e) { + Log.e(TAG, "Failed to query dream manager.", e); + } + boolean inUse = mPowerManager.isScreenOn() && !isDreaming; + + if (!inUse) { + if (DEBUG) { + Log.d(TAG, "No heads up: not in use: " + sbn.getKey()); + } + return false; + } + + if (entry.shouldSuppressPeek()) { + if (DEBUG) { + Log.d(TAG, "No heads up: suppressed by DND: " + sbn.getKey()); + } + return false; + } + + if (isSnoozedPackage(sbn)) { + if (DEBUG) { + Log.d(TAG, "No heads up: snoozed package: " + sbn.getKey()); + } + return false; + } + + if (entry.hasJustLaunchedFullScreenIntent()) { + if (DEBUG) { + Log.d(TAG, "No heads up: recent fullscreen: " + sbn.getKey()); + } + return false; + } + + if (entry.importance < NotificationManager.IMPORTANCE_HIGH) { + if (DEBUG) { + Log.d(TAG, "No heads up: unimportant notification: " + sbn.getKey()); + } + return false; + } + + if (!mHeadsUpSuppressor.canHeadsUp(entry, sbn)) { + return false; + } + + return true; + } + + /** + * Whether or not the notification should "pulse" on the user's display when the phone is + * dozing. This displays the ambient view of the notification. + * + * @param entry the entry to check + * @return true if the entry should ambient pulse, false otherwise + */ + public boolean shouldPulse(NotificationData.Entry entry) { + StatusBarNotification sbn = entry.notification; + + if (!getShadeController().isDozing()) { + if (DEBUG) { + Log.d(TAG, "No pulsing: not dozing: " + sbn.getKey()); + } + return false; + } + + if (!canAlertCommon(entry)) { + if (DEBUG) { + Log.d(TAG, "No pulsing: notification shouldn't alert: " + sbn.getKey()); + } + return false; + } + + if (entry.shouldSuppressAmbient()) { + if (DEBUG) { + Log.d(TAG, "No pulsing: ambient effect suppressed: " + sbn.getKey()); + } + return false; + } + + if (entry.importance < NotificationManager.IMPORTANCE_DEFAULT) { + if (DEBUG) { + Log.d(TAG, "No pulsing: not important enough: " + sbn.getKey()); + } + return false; + } + + Bundle extras = sbn.getNotification().extras; + CharSequence title = extras.getCharSequence(Notification.EXTRA_TITLE); + CharSequence text = extras.getCharSequence(Notification.EXTRA_TEXT); + if (TextUtils.isEmpty(title) && TextUtils.isEmpty(text)) { + if (DEBUG) { + Log.d(TAG, "No pulsing: title and text are empty: " + sbn.getKey()); + } + return false; + } + + return true; + } + + /** + * Common checks between heads up alerting and ambient pulse alerting. See + * {@link #shouldHeadsUp(NotificationData.Entry)} and + * {@link #shouldPulse(NotificationData.Entry)}. Notifications that fail any of these checks + * should not alert at all. + * + * @param entry the entry to check + * @return true if these checks pass, false if the notification should not alert + */ + protected boolean canAlertCommon(NotificationData.Entry entry) { + StatusBarNotification sbn = entry.notification; + + if (mNotificationFilter.shouldFilterOut(entry)) { + if (DEBUG) { + Log.d(TAG, "No alerting: filtered notification: " + sbn.getKey()); + } + return false; + } + + // Don't alert notifications that are suppressed due to group alert behavior + if (sbn.isGroup() && sbn.getNotification().suppressAlertingDueToGrouping()) { + if (DEBUG) { + Log.d(TAG, "No alerting: suppressed due to group alert behavior"); + } + return false; + } + + return true; + } + + private boolean isSnoozedPackage(StatusBarNotification sbn) { + return mHeadsUpManager.isSnoozed(sbn.getPackageName()); + } + + /** Sets whether to disable all alerts. */ + public void setDisableNotificationAlerts(boolean disableNotificationAlerts) { + mDisableNotificationAlerts = disableNotificationAlerts; + mHeadsUpObserver.onChange(true); + } + + protected NotificationPresenter getPresenter() { + return mPresenter; + } + + /** A component which can suppress heads-up notifications due to the overall state of the UI. */ + public interface HeadsUpSuppressor { + /** + * Returns false if the provided notification is ineligible for heads-up according to this + * component. + * + * @param entry entry of the notification that might be heads upped + * @param sbn notification that might be heads upped + * @return false if the notification can not be heads upped + */ + boolean canHeadsUp(NotificationData.Entry entry, StatusBarNotification sbn); + + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationRowBinder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationRowBinder.java index 824bd813d5634..b241b8a70b3fa 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationRowBinder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationRowBinder.java @@ -64,6 +64,8 @@ public class NotificationRowBinder { private final NotificationGutsManager mGutsManager = Dependency.get(NotificationGutsManager.class); private final UiOffloadThread mUiOffloadThread = Dependency.get(UiOffloadThread.class); + private final NotificationInterruptionStateProvider mNotificationInterruptionStateProvider = + Dependency.get(NotificationInterruptionStateProvider.class); private final Context mContext; private final IStatusBarService mBarService; @@ -79,7 +81,6 @@ public class NotificationRowBinder { private ExpandableNotificationRow.OnAppOpsClickListener mOnAppOpsClickListener; private BindRowCallback mBindRowCallback; private NotificationClicker mNotificationClicker; - private NotificationEntryManager.InterruptionStateProvider mInterruptionStateProvider; @Inject public NotificationRowBinder(Context context) { @@ -116,11 +117,6 @@ public class NotificationRowBinder { mNotificationClicker = clicker; } - public void setInterruptionStateProvider( - NotificationEntryManager.InterruptionStateProvider interruptionStateProvider) { - mInterruptionStateProvider = interruptionStateProvider; - } - /** * Inflates the views for the given entry (possibly asynchronously). */ @@ -253,10 +249,10 @@ public class NotificationRowBinder { row.setUseIncreasedHeadsUpHeight(useIncreasedHeadsUp); row.setEntry(entry); - if (mInterruptionStateProvider.shouldHeadsUp(entry)) { + if (mNotificationInterruptionStateProvider.shouldHeadsUp(entry)) { row.updateInflationFlag(FLAG_CONTENT_VIEW_HEADS_UP, true /* shouldInflate */); } - if (mInterruptionStateProvider.shouldPulse(entry)) { + if (mNotificationInterruptionStateProvider.shouldPulse(entry)) { row.updateInflationFlag(FLAG_CONTENT_VIEW_AMBIENT, true /* shouldInflate */); } row.setNeedsRedaction( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelper.java index 6732bbeba4933..b1f74c8ab91e9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelper.java @@ -29,8 +29,8 @@ import com.android.systemui.statusbar.AmbientPulseManager.OnAmbientChangedListen import com.android.systemui.statusbar.InflationTask; import com.android.systemui.statusbar.StatusBarStateController; import com.android.systemui.statusbar.StatusBarStateController.StateListener; -import com.android.systemui.statusbar.notification.AlertTransferListener; import com.android.systemui.statusbar.notification.NotificationData.Entry; +import com.android.systemui.statusbar.notification.NotificationEntryListener; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.row.NotificationInflater.AsyncInflationTask; import com.android.systemui.statusbar.notification.row.NotificationInflater.InflationFlag; @@ -95,7 +95,7 @@ public class NotificationGroupAlertTransferHelper implements OnHeadsUpChangedLis // not being up to date. mEntryManager = entryManager; - mEntryManager.setAlertTransferListener(mAlertTransferListener); + mEntryManager.addNotificationEntryListener(mNotificationEntryListener); groupManager.addOnGroupChangeListener(mOnGroupChangeListener); } @@ -186,7 +186,8 @@ public class NotificationGroupAlertTransferHelper implements OnHeadsUpChangedLis } } - private final AlertTransferListener mAlertTransferListener = new AlertTransferListener() { + private final NotificationEntryListener mNotificationEntryListener = + new NotificationEntryListener() { // Called when a new notification has been posted but is not inflated yet. We use this to // see as early as we can if we need to abort a transfer. @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java index 2d5d562746404..e40835fb8fd07 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java @@ -207,7 +207,7 @@ public class NotificationIconAreaController implements DarkReceiver { } // showAmbient == show in shade but not shelf - if (!showAmbient && mEntryManager.getNotificationData().shouldSuppressStatusBar(entry)) { + if (!showAmbient && entry.shouldSuppressStatusBar()) { return false; } 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 1b43f8fad9bdc..008c21d1b936f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java @@ -191,6 +191,7 @@ import com.android.systemui.statusbar.notification.NotificationClicker; import com.android.systemui.statusbar.notification.NotificationData; import com.android.systemui.statusbar.notification.NotificationData.Entry; import com.android.systemui.statusbar.notification.NotificationEntryManager; +import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider; import com.android.systemui.statusbar.notification.NotificationRowBinder; import com.android.systemui.statusbar.notification.VisualStabilityManager; import com.android.systemui.statusbar.notification.logging.NotificationLogger; @@ -377,6 +378,7 @@ public class StatusBar extends SystemUI implements DemoMode, private NotificationGutsManager mGutsManager; protected NotificationLogger mNotificationLogger; protected NotificationEntryManager mEntryManager; + private NotificationInterruptionStateProvider mNotificationInterruptionStateProvider; private NotificationRowBinder mNotificationRowBinder; protected NotificationViewHierarchyManager mViewHierarchyManager; protected ForegroundServiceController mForegroundServiceController; @@ -622,6 +624,8 @@ public class StatusBar extends SystemUI implements DemoMode, mGutsManager = Dependency.get(NotificationGutsManager.class); mMediaManager = Dependency.get(NotificationMediaManager.class); mEntryManager = Dependency.get(NotificationEntryManager.class); + mNotificationInterruptionStateProvider = + Dependency.get(NotificationInterruptionStateProvider.class); mNotificationRowBinder = Dependency.get(NotificationRowBinder.class); mViewHierarchyManager = Dependency.get(NotificationViewHierarchyManager.class); mForegroundServiceController = Dependency.get(ForegroundServiceController.class); @@ -1413,7 +1417,7 @@ public class StatusBar extends SystemUI implements DemoMode, } if ((diff1 & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) != 0) { - mEntryManager.setDisableNotificationAlerts( + mNotificationInterruptionStateProvider.setDisableNotificationAlerts( (state1 & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) != 0); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java index c93d151b0f52d..8d1b911b101f4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java @@ -24,6 +24,7 @@ import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.app.KeyguardManager; import android.app.Notification; +import android.app.NotificationManager; import android.app.PendingIntent; import android.app.TaskStackBuilder; import android.content.Context; @@ -33,15 +34,21 @@ import android.os.Looper; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; +import android.service.dreams.DreamService; +import android.service.dreams.IDreamManager; import android.service.notification.StatusBarNotification; import android.text.TextUtils; +import android.util.EventLog; import android.util.Log; import android.view.RemoteAnimationAdapter; +import com.android.internal.logging.MetricsLogger; import com.android.internal.statusbar.IStatusBarService; import com.android.internal.statusbar.NotificationVisibility; import com.android.internal.widget.LockPatternUtils; import com.android.systemui.Dependency; +import com.android.systemui.EventLogTags; +import com.android.systemui.UiOffloadThread; import com.android.systemui.assist.AssistManager; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.statusbar.CommandQueue; @@ -54,7 +61,9 @@ import com.android.systemui.statusbar.StatusBarStateController; import com.android.systemui.statusbar.notification.ActivityLaunchAnimator; import com.android.systemui.statusbar.notification.NotificationActivityStarter; import com.android.systemui.statusbar.notification.NotificationData; +import com.android.systemui.statusbar.notification.NotificationEntryListener; import com.android.systemui.statusbar.notification.NotificationEntryManager; +import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.policy.HeadsUpUtil; import com.android.systemui.statusbar.policy.KeyguardMonitor; @@ -66,6 +75,7 @@ import com.android.systemui.statusbar.policy.PreviewInflater; public class StatusBarNotificationActivityStarter implements NotificationActivityStarter { private static final String TAG = "NotificationClickHandler"; + protected static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private final AssistManager mAssistManager = Dependency.get(AssistManager.class); private final NotificationGroupManager mGroupManager = @@ -84,6 +94,9 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit Dependency.get(NotificationEntryManager.class); private final StatusBarStateController mStatusBarStateController = Dependency.get(StatusBarStateController.class); + private final NotificationInterruptionStateProvider mNotificationInterruptionStateProvider = + Dependency.get(NotificationInterruptionStateProvider.class); + private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class); private final Context mContext; private final NotificationPanelView mNotificationPanel; @@ -94,6 +107,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit private final ActivityLaunchAnimator mActivityLaunchAnimator; private final IStatusBarService mBarService; private final CommandQueue mCommandQueue; + private final IDreamManager mDreamManager; private boolean mIsCollapsingToShowActivityOverLockscreen; @@ -112,6 +126,15 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit mBarService = IStatusBarService.Stub.asInterface( ServiceManager.getService(Context.STATUS_BAR_SERVICE)); mCommandQueue = getComponent(context, CommandQueue.class); + mDreamManager = IDreamManager.Stub.asInterface( + ServiceManager.checkService(DreamService.DREAM_SERVICE)); + + mEntryManager.addNotificationEntryListener(new NotificationEntryListener() { + @Override + public void onPendingEntryAdded(NotificationData.Entry entry) { + handleFullScreenIntent(entry); + } + }); } /** @@ -322,6 +345,45 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit }, null, false /* afterKeyguardGone */); } + private void handleFullScreenIntent(NotificationData.Entry entry) { + boolean isHeadsUped = mNotificationInterruptionStateProvider.shouldHeadsUp(entry); + if (!isHeadsUped && entry.notification.getNotification().fullScreenIntent != null) { + if (shouldSuppressFullScreenIntent(entry)) { + if (DEBUG) { + Log.d(TAG, "No Fullscreen intent: suppressed by DND: " + entry.key); + } + } else if (entry.importance < NotificationManager.IMPORTANCE_HIGH) { + if (DEBUG) { + Log.d(TAG, "No Fullscreen intent: not important enough: " + entry.key); + } + } else { + // Stop screensaver if the notification has a fullscreen intent. + // (like an incoming phone call) + Dependency.get(UiOffloadThread.class).submit(() -> { + try { + mDreamManager.awaken(); + } catch (RemoteException e) { + e.printStackTrace(); + } + }); + + // not immersive & a fullscreen alert should be shown + if (DEBUG) { + Log.d(TAG, "Notification has fullScreenIntent; sending fullScreenIntent"); + } + try { + EventLog.writeEvent(EventLogTags.SYSUI_FULLSCREEN_NOTIFICATION, + entry.key); + entry.notification.getNotification().fullScreenIntent.send(); + entry.notifyFullScreenIntentLaunched(); + mMetricsLogger.count("note_fullscreen", 1); + } catch (PendingIntent.CanceledException e) { + // ignore + } + } + } + } + @Override public boolean isCollapsingToShowActivityOverLockscreen() { return mIsCollapsingToShowActivityOverLockscreen; @@ -351,6 +413,14 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit || !mActivityLaunchAnimator.isAnimationPending(); } + private boolean shouldSuppressFullScreenIntent(NotificationData.Entry entry) { + if (mPresenter.isDeviceInVrMode()) { + return true; + } + + return entry.shouldSuppressFullScreenIntent(); + } + private void removeNotification(StatusBarNotification notification) { // We have to post it to the UI thread for synchronization Dependency.get(MAIN_HANDLER).post(() -> { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java index c8c9ebe596771..d643f07e28597 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java @@ -57,7 +57,9 @@ import com.android.systemui.statusbar.StatusBarStateController; import com.android.systemui.statusbar.notification.AboveShelfObserver; import com.android.systemui.statusbar.notification.ActivityLaunchAnimator; import com.android.systemui.statusbar.notification.NotificationData.Entry; +import com.android.systemui.statusbar.notification.NotificationEntryListener; import com.android.systemui.statusbar.notification.NotificationEntryManager; +import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider; import com.android.systemui.statusbar.notification.NotificationRowBinder; import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; @@ -91,6 +93,8 @@ public class StatusBarNotificationPresenter implements NotificationPresenter, Dependency.get(NotificationEntryManager.class); private final NotificationRowBinder mNotificationRowBinder = Dependency.get(NotificationRowBinder.class); + private final NotificationInterruptionStateProvider mNotificationInterruptionStateProvider = + Dependency.get(NotificationInterruptionStateProvider.class); private final NotificationMediaManager mMediaManager = Dependency.get(NotificationMediaManager.class); protected AmbientPulseManager mAmbientPulseManager = Dependency.get(AmbientPulseManager.class); @@ -169,10 +173,37 @@ public class StatusBarNotificationPresenter implements NotificationPresenter, NotificationListContainer notifListContainer = (NotificationListContainer) stackScroller; Dependency.get(InitController.class).addPostInitTask(() -> { + NotificationEntryListener notificationEntryListener = new NotificationEntryListener() { + @Override + public void onNotificationAdded(Entry entry) { + // Recalculate the position of the sliding windows and the titles. + mShadeController.updateAreThereNotifications(); + } + + @Override + public void onNotificationUpdated(StatusBarNotification notification) { + mShadeController.updateAreThereNotifications(); + } + + @Override + public void onNotificationRemoved(String key, StatusBarNotification old) { + StatusBarNotificationPresenter.this.onNotificationRemoved(key, old); + } + + @Override + public void onPerformRemoveNotification( + StatusBarNotification statusBarNotification) { + StatusBarNotificationPresenter.this.onPerformRemoveNotification(); + } + }; + mViewHierarchyManager.setUpWithPresenter(this, notifListContainer); - mEntryManager.setUpWithPresenter(this, notifListContainer, this, mHeadsUpManager); + mEntryManager.setUpWithPresenter( + this, notifListContainer, notificationEntryListener, mHeadsUpManager); mNotificationRowBinder.setUpWithPresenter(this, notifListContainer, mHeadsUpManager, mEntryManager, this); + mNotificationInterruptionStateProvider.setUpWithPresenter( + this, mHeadsUpManager, this::canHeadsUp); mLockscreenUserManager.setUpWithPresenter(this); mMediaManager.setUpWithPresenter(this); Dependency.get(NotificationGutsManager.class).setUpWithPresenter(this, @@ -222,10 +253,9 @@ public class StatusBarNotificationPresenter implements NotificationPresenter, || mActivityLaunchAnimator.isAnimationRunning(); } - @Override - public void onPerformRemoveNotification(StatusBarNotification n) { + private void onPerformRemoveNotification() { if (mNotificationPanel.hasPulsingNotifications() && - !mAmbientPulseManager.hasNotifications()) { + !mAmbientPulseManager.hasNotifications()) { // We were showing a pulse for a notification, but no notifications are pulsing anymore. // Finish the pulse. mDozeScrimController.pulseOutNow(); @@ -249,18 +279,6 @@ public class StatusBarNotificationPresenter implements NotificationPresenter, mNotificationPanel.updateNotificationViews(); } - @Override - public void onNotificationAdded(Entry shadeEntry) { - // Recalculate the position of the sliding windows and the titles. - mShadeController.updateAreThereNotifications(); - } - - @Override - public void onNotificationUpdated(StatusBarNotification notification) { - mShadeController.updateAreThereNotifications(); - } - - @Override public void onNotificationRemoved(String key, StatusBarNotification old) { if (SPEW) Log.d(TAG, "removeNotification key=" + key + " old=" + old); @@ -282,7 +300,6 @@ public class StatusBarNotificationPresenter implements NotificationPresenter, return !mEntryManager.getNotificationData().getActiveNotifications().isEmpty(); } - @Override public boolean canHeadsUp(Entry entry, StatusBarNotification sbn) { if (mShadeController.isDozing()) { return false; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java index f8ff583162cf9..894ef3dde7792 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java @@ -26,6 +26,7 @@ import android.testing.TestableLooper; import com.android.systemui.Dependency; import com.android.systemui.InitController; import com.android.systemui.SysuiTestCase; +import com.android.systemui.statusbar.notification.NotificationEntryListener; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.logging.NotificationLogger; import com.android.systemui.statusbar.notification.row.NotificationGutsManager; @@ -55,7 +56,8 @@ import org.mockito.MockitoAnnotations; public class NonPhoneDependencyTest extends SysuiTestCase { @Mock private NotificationPresenter mPresenter; @Mock private NotificationListContainer mListContainer; - @Mock private NotificationEntryManager.Callback mEntryManagerCallback; + @Mock + private NotificationEntryListener mEntryManagerCallback; @Mock private HeadsUpManager mHeadsUpManager; @Mock private RemoteInputController.Delegate mDelegate; @Mock private NotificationRemoteInputManager.Callback mRemoteInputManagerCallback; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationDataTest.java index def7513bc7dd5..871ff8998c204 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationDataTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationDataTest.java @@ -29,8 +29,6 @@ import static junit.framework.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -50,7 +48,6 @@ import android.service.notification.NotificationListenerService; import android.service.notification.NotificationListenerService.Ranking; import android.service.notification.SnoozeCriterion; import android.service.notification.StatusBarNotification; -import android.support.test.annotation.UiThreadTest; import android.support.test.filters.SmallTest; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -105,6 +102,7 @@ public class NotificationDataTest extends SysuiTestCase { com.android.systemui.util.Assert.sMainLooper = TestableLooper.get(this).getLooper(); MockitoAnnotations.initMocks(this); when(mMockStatusBarNotification.getUid()).thenReturn(UID_NORMAL); + when(mMockStatusBarNotification.cloneLight()).thenReturn(mMockStatusBarNotification); when(mMockPackageManager.checkUidPermission( eq(Manifest.permission.NOTIFICATION_DURING_SETUP), @@ -128,41 +126,6 @@ public class NotificationDataTest extends SysuiTestCase { Dependency.get(InitController.class).executePostInitTasks(); } - @Test - @UiThreadTest - public void testShowNotificationEvenIfUnprovisioned_FalseIfNoExtra() { - initStatusBarNotification(false); - when(mMockStatusBarNotification.getUid()).thenReturn(UID_ALLOW_DURING_SETUP); - - assertFalse( - NotificationData.showNotificationEvenIfUnprovisioned( - mMockPackageManager, - mMockStatusBarNotification)); - } - - @Test - @UiThreadTest - public void testShowNotificationEvenIfUnprovisioned_FalseIfNoPermission() { - initStatusBarNotification(true); - - assertFalse( - NotificationData.showNotificationEvenIfUnprovisioned( - mMockPackageManager, - mMockStatusBarNotification)); - } - - @Test - @UiThreadTest - public void testShowNotificationEvenIfUnprovisioned_TrueIfHasPermissionAndExtra() { - initStatusBarNotification(true); - when(mMockStatusBarNotification.getUid()).thenReturn(UID_ALLOW_DURING_SETUP); - - assertTrue( - NotificationData.showNotificationEvenIfUnprovisioned( - mMockPackageManager, - mMockStatusBarNotification)); - } - @Test public void testChannelSetWhenAdded() { mNotificationData.add(mRow.getEntry()); @@ -229,76 +192,6 @@ public class NotificationDataTest extends SysuiTestCase { .mActiveAppOps.contains(OP_ACCEPT_HANDOVER)); } - @Test - public void testSuppressSystemAlertNotification() { - when(mFsc.isSystemAlertWarningNeeded(anyInt(), anyString())).thenReturn(false); - when(mFsc.isSystemAlertNotification(any())).thenReturn(true); - StatusBarNotification sbn = mRow.getEntry().notification; - Bundle bundle = new Bundle(); - bundle.putStringArray(Notification.EXTRA_FOREGROUND_APPS, new String[] {"something"}); - sbn.getNotification().extras = bundle; - - assertTrue(mNotificationData.shouldFilterOut(mRow.getEntry())); - } - - @Test - public void testDoNotSuppressSystemAlertNotification() { - StatusBarNotification sbn = mRow.getEntry().notification; - Bundle bundle = new Bundle(); - bundle.putStringArray(Notification.EXTRA_FOREGROUND_APPS, new String[] {"something"}); - sbn.getNotification().extras = bundle; - - when(mFsc.isSystemAlertWarningNeeded(anyInt(), anyString())).thenReturn(true); - when(mFsc.isSystemAlertNotification(any())).thenReturn(true); - - assertFalse(mNotificationData.shouldFilterOut(mRow.getEntry())); - - when(mFsc.isSystemAlertWarningNeeded(anyInt(), anyString())).thenReturn(true); - when(mFsc.isSystemAlertNotification(any())).thenReturn(false); - - assertFalse(mNotificationData.shouldFilterOut(mRow.getEntry())); - - when(mFsc.isSystemAlertWarningNeeded(anyInt(), anyString())).thenReturn(false); - when(mFsc.isSystemAlertNotification(any())).thenReturn(false); - - assertFalse(mNotificationData.shouldFilterOut(mRow.getEntry())); - } - - @Test - public void testDoNotSuppressMalformedSystemAlertNotification() { - when(mFsc.isSystemAlertWarningNeeded(anyInt(), anyString())).thenReturn(true); - - // missing extra - assertFalse(mNotificationData.shouldFilterOut(mRow.getEntry())); - - StatusBarNotification sbn = mRow.getEntry().notification; - Bundle bundle = new Bundle(); - bundle.putStringArray(Notification.EXTRA_FOREGROUND_APPS, new String[] {}); - sbn.getNotification().extras = bundle; - - // extra missing values - assertFalse(mNotificationData.shouldFilterOut(mRow.getEntry())); - } - - @Test - public void testShouldFilterHiddenNotifications() { - initStatusBarNotification(false); - // setup - when(mFsc.isSystemAlertWarningNeeded(anyInt(), anyString())).thenReturn(false); - when(mFsc.isSystemAlertNotification(any())).thenReturn(false); - - // test should filter out hidden notifications: - // hidden - when(mMockStatusBarNotification.getKey()).thenReturn(TEST_HIDDEN_NOTIFICATION_KEY); - NotificationData.Entry entry = new NotificationData.Entry(mMockStatusBarNotification); - assertTrue(mNotificationData.shouldFilterOut(entry)); - - // not hidden - when(mMockStatusBarNotification.getKey()).thenReturn("not hidden"); - entry = new NotificationData.Entry(mMockStatusBarNotification); - assertFalse(mNotificationData.shouldFilterOut(entry)); - } - @Test public void testGetNotificationsForCurrentUser_shouldFilterNonCurrentUserNotifications() throws Exception { @@ -325,9 +218,10 @@ public class NotificationDataTest extends SysuiTestCase { Notification n = mMockStatusBarNotification.getNotification(); n.flags = Notification.FLAG_FOREGROUND_SERVICE; NotificationData.Entry entry = new NotificationData.Entry(mMockStatusBarNotification); + mNotificationData.add(entry); - assertTrue(mNotificationData.isExemptFromDndVisualSuppression(entry)); - assertFalse(mNotificationData.shouldSuppressAmbient(entry)); + assertTrue(entry.isExemptFromDndVisualSuppression()); + assertFalse(entry.shouldSuppressAmbient()); } @Test @@ -341,9 +235,10 @@ public class NotificationDataTest extends SysuiTestCase { n = nb.build(); when(mMockStatusBarNotification.getNotification()).thenReturn(n); NotificationData.Entry entry = new NotificationData.Entry(mMockStatusBarNotification); + mNotificationData.add(entry); - assertTrue(mNotificationData.isExemptFromDndVisualSuppression(entry)); - assertFalse(mNotificationData.shouldSuppressAmbient(entry)); + assertTrue(entry.isExemptFromDndVisualSuppression()); + assertFalse(entry.shouldSuppressAmbient()); } @Test @@ -353,9 +248,10 @@ public class NotificationDataTest extends SysuiTestCase { TEST_EXEMPT_DND_VISUAL_SUPPRESSION_KEY); NotificationData.Entry entry = new NotificationData.Entry(mMockStatusBarNotification); entry.mIsSystemNotification = true; + mNotificationData.add(entry); - assertTrue(mNotificationData.isExemptFromDndVisualSuppression(entry)); - assertFalse(mNotificationData.shouldSuppressAmbient(entry)); + assertTrue(entry.isExemptFromDndVisualSuppression()); + assertFalse(entry.shouldSuppressAmbient()); } @Test @@ -365,31 +261,33 @@ public class NotificationDataTest extends SysuiTestCase { TEST_EXEMPT_DND_VISUAL_SUPPRESSION_KEY); NotificationData.Entry entry = new NotificationData.Entry(mMockStatusBarNotification); entry.mIsSystemNotification = true; + mNotificationData.add(entry); + when(mMockStatusBarNotification.getNotification()).thenReturn( new Notification.Builder(mContext, "").setCategory(CATEGORY_CALL).build()); - assertFalse(mNotificationData.isExemptFromDndVisualSuppression(entry)); - assertTrue(mNotificationData.shouldSuppressAmbient(entry)); + assertFalse(entry.isExemptFromDndVisualSuppression()); + assertTrue(entry.shouldSuppressAmbient()); when(mMockStatusBarNotification.getNotification()).thenReturn( new Notification.Builder(mContext, "").setCategory(CATEGORY_REMINDER).build()); - assertFalse(mNotificationData.isExemptFromDndVisualSuppression(entry)); + assertFalse(entry.isExemptFromDndVisualSuppression()); when(mMockStatusBarNotification.getNotification()).thenReturn( new Notification.Builder(mContext, "").setCategory(CATEGORY_ALARM).build()); - assertFalse(mNotificationData.isExemptFromDndVisualSuppression(entry)); + assertFalse(entry.isExemptFromDndVisualSuppression()); when(mMockStatusBarNotification.getNotification()).thenReturn( new Notification.Builder(mContext, "").setCategory(CATEGORY_EVENT).build()); - assertFalse(mNotificationData.isExemptFromDndVisualSuppression(entry)); + assertFalse(entry.isExemptFromDndVisualSuppression()); when(mMockStatusBarNotification.getNotification()).thenReturn( new Notification.Builder(mContext, "").setCategory(CATEGORY_MESSAGE).build()); - assertFalse(mNotificationData.isExemptFromDndVisualSuppression(entry)); + assertFalse(entry.isExemptFromDndVisualSuppression()); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java index 8fe91cd5e82a0..701ea7d3e2fe4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java @@ -103,7 +103,8 @@ public class NotificationEntryManagerTest extends SysuiTestCase { @Mock private KeyguardEnvironment mEnvironment; @Mock private ExpandableNotificationRow mRow; @Mock private NotificationListContainer mListContainer; - @Mock private NotificationEntryManager.Callback mCallback; + @Mock + private NotificationEntryListener mCallback; @Mock private NotificationRowBinder.BindRowCallback mBindCallback; @Mock private HeadsUpManager mHeadsUpManager; @@ -137,7 +138,6 @@ public class NotificationEntryManagerTest extends SysuiTestCase { super(context); mBarService = barService; mCountDownLatch = new CountDownLatch(1); - mUseHeadsUp = true; } @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationFilterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationFilterTest.java new file mode 100644 index 0000000000000..da8bc01dc29da --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationFilterTest.java @@ -0,0 +1,212 @@ +/* + * 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 com.android.systemui.statusbar.notification; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.Manifest; +import android.app.Notification; +import android.content.pm.IPackageManager; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.service.notification.StatusBarNotification; +import android.support.test.annotation.UiThreadTest; +import android.support.test.filters.SmallTest; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.testing.TestableLooper.RunWithLooper; + +import com.android.systemui.ForegroundServiceController; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.statusbar.NotificationTestHelper; +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; +import com.android.systemui.statusbar.phone.NotificationGroupManager; +import com.android.systemui.statusbar.phone.ShadeController; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@RunWithLooper +public class NotificationFilterTest extends SysuiTestCase { + + private static final int UID_NORMAL = 123; + private static final int UID_ALLOW_DURING_SETUP = 456; + private static final String TEST_HIDDEN_NOTIFICATION_KEY = "testHiddenNotificationKey"; + + private final StatusBarNotification mMockStatusBarNotification = + mock(StatusBarNotification.class); + + @Mock + ForegroundServiceController mFsc; + @Mock + NotificationData.KeyguardEnvironment mEnvironment; + private final IPackageManager mMockPackageManager = mock(IPackageManager.class); + + private NotificationFilter mNotificationFilter; + private ExpandableNotificationRow mRow; + + @Before + public void setUp() throws Exception { + com.android.systemui.util.Assert.sMainLooper = TestableLooper.get(this).getLooper(); + MockitoAnnotations.initMocks(this); + when(mMockStatusBarNotification.getUid()).thenReturn(UID_NORMAL); + + when(mMockPackageManager.checkUidPermission( + eq(Manifest.permission.NOTIFICATION_DURING_SETUP), + eq(UID_NORMAL))) + .thenReturn(PackageManager.PERMISSION_DENIED); + when(mMockPackageManager.checkUidPermission( + eq(Manifest.permission.NOTIFICATION_DURING_SETUP), + eq(UID_ALLOW_DURING_SETUP))) + .thenReturn(PackageManager.PERMISSION_GRANTED); + mDependency.injectTestDependency(ForegroundServiceController.class, mFsc); + mDependency.injectTestDependency(NotificationGroupManager.class, + new NotificationGroupManager()); + mDependency.injectMockDependency(ShadeController.class); + mDependency.injectTestDependency(NotificationData.KeyguardEnvironment.class, mEnvironment); + when(mEnvironment.isDeviceProvisioned()).thenReturn(true); + when(mEnvironment.isNotificationForCurrentProfiles(any())).thenReturn(true); + mRow = new NotificationTestHelper(getContext()).createRow(); + mNotificationFilter = new NotificationFilter(); + } + + @Test + @UiThreadTest + public void testShowNotificationEvenIfUnprovisioned_FalseIfNoExtra() { + initStatusBarNotification(false); + when(mMockStatusBarNotification.getUid()).thenReturn(UID_ALLOW_DURING_SETUP); + + assertFalse( + NotificationFilter.showNotificationEvenIfUnprovisioned( + mMockPackageManager, + mMockStatusBarNotification)); + } + + @Test + @UiThreadTest + public void testShowNotificationEvenIfUnprovisioned_FalseIfNoPermission() { + initStatusBarNotification(true); + + assertFalse( + NotificationFilter.showNotificationEvenIfUnprovisioned( + mMockPackageManager, + mMockStatusBarNotification)); + } + + @Test + @UiThreadTest + public void testShowNotificationEvenIfUnprovisioned_TrueIfHasPermissionAndExtra() { + initStatusBarNotification(true); + when(mMockStatusBarNotification.getUid()).thenReturn(UID_ALLOW_DURING_SETUP); + + assertTrue( + NotificationFilter.showNotificationEvenIfUnprovisioned( + mMockPackageManager, + mMockStatusBarNotification)); + } + + @Test + public void testSuppressSystemAlertNotification() { + when(mFsc.isSystemAlertWarningNeeded(anyInt(), anyString())).thenReturn(false); + when(mFsc.isSystemAlertNotification(any())).thenReturn(true); + StatusBarNotification sbn = mRow.getEntry().notification; + Bundle bundle = new Bundle(); + bundle.putStringArray(Notification.EXTRA_FOREGROUND_APPS, new String[]{"something"}); + sbn.getNotification().extras = bundle; + + assertTrue(mNotificationFilter.shouldFilterOut(mRow.getEntry())); + } + + @Test + public void testDoNotSuppressSystemAlertNotification() { + StatusBarNotification sbn = mRow.getEntry().notification; + Bundle bundle = new Bundle(); + bundle.putStringArray(Notification.EXTRA_FOREGROUND_APPS, new String[]{"something"}); + sbn.getNotification().extras = bundle; + + when(mFsc.isSystemAlertWarningNeeded(anyInt(), anyString())).thenReturn(true); + when(mFsc.isSystemAlertNotification(any())).thenReturn(true); + + assertFalse(mNotificationFilter.shouldFilterOut(mRow.getEntry())); + + when(mFsc.isSystemAlertWarningNeeded(anyInt(), anyString())).thenReturn(true); + when(mFsc.isSystemAlertNotification(any())).thenReturn(false); + + assertFalse(mNotificationFilter.shouldFilterOut(mRow.getEntry())); + + when(mFsc.isSystemAlertWarningNeeded(anyInt(), anyString())).thenReturn(false); + when(mFsc.isSystemAlertNotification(any())).thenReturn(false); + + assertFalse(mNotificationFilter.shouldFilterOut(mRow.getEntry())); + } + + @Test + public void testDoNotSuppressMalformedSystemAlertNotification() { + when(mFsc.isSystemAlertWarningNeeded(anyInt(), anyString())).thenReturn(true); + + // missing extra + assertFalse(mNotificationFilter.shouldFilterOut(mRow.getEntry())); + + StatusBarNotification sbn = mRow.getEntry().notification; + Bundle bundle = new Bundle(); + bundle.putStringArray(Notification.EXTRA_FOREGROUND_APPS, new String[]{}); + sbn.getNotification().extras = bundle; + + // extra missing values + assertFalse(mNotificationFilter.shouldFilterOut(mRow.getEntry())); + } + + @Test + public void testShouldFilterHiddenNotifications() { + initStatusBarNotification(false); + // setup + when(mFsc.isSystemAlertWarningNeeded(anyInt(), anyString())).thenReturn(false); + when(mFsc.isSystemAlertNotification(any())).thenReturn(false); + + // test should filter out hidden notifications: + // hidden + NotificationData.Entry entry = new NotificationData.Entry(mMockStatusBarNotification); + entry.suspended = true; + assertTrue(mNotificationFilter.shouldFilterOut(entry)); + + // not hidden + entry = new NotificationData.Entry(mMockStatusBarNotification); + entry.suspended = false; + assertFalse(mNotificationFilter.shouldFilterOut(entry)); + } + + private void initStatusBarNotification(boolean allowDuringSetup) { + Bundle bundle = new Bundle(); + bundle.putBoolean(Notification.EXTRA_ALLOW_DURING_SETUP, allowDuringSetup); + Notification notification = new Notification.Builder(mContext, "test") + .addExtras(bundle) + .build(); + when(mMockStatusBarNotification.getNotification()).thenReturn(notification); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index e65e806da93a8..b4f99c4ed1566 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -126,7 +126,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { mPowerManager = new PowerManager(mContext, powerManagerService, Handler.createAsync(Looper.myLooper())); - mEntryManager = new TestableNotificationEntryManager(mDreamManager, mPowerManager, + mEntryManager = new TestableNotificationEntryManager(mPowerManager, mContext); mDependency.injectTestDependency(NotificationEntryManager.class, mEntryManager); Dependency.get(InitController.class).executePostInitTasks(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelperTest.java index ee39e10b41655..490288efd40fc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelperTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelperTest.java @@ -32,9 +32,9 @@ import android.testing.TestableLooper; import com.android.systemui.SysuiTestCase; import com.android.systemui.statusbar.AmbientPulseManager; -import com.android.systemui.statusbar.notification.AlertTransferListener; import com.android.systemui.statusbar.notification.NotificationData; import com.android.systemui.statusbar.notification.NotificationData.Entry; +import com.android.systemui.statusbar.notification.NotificationEntryListener; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.policy.HeadsUpManager; @@ -61,8 +61,9 @@ public class NotificationGroupAlertTransferHelperTest extends SysuiTestCase { private AmbientPulseManager mAmbientPulseManager; private HeadsUpManager mHeadsUpManager; @Mock private NotificationEntryManager mNotificationEntryManager; - @Captor private ArgumentCaptor mListenerCaptor; - private AlertTransferListener mAlertTransferListener; + @Captor + private ArgumentCaptor mListenerCaptor; + private NotificationEntryListener mNotificationEntryListener; private final HashMap mPendingEntries = new HashMap<>(); private final NotificationGroupTestHelper mGroupTestHelper = new NotificationGroupTestHelper(mContext); @@ -85,8 +86,8 @@ public class NotificationGroupAlertTransferHelperTest extends SysuiTestCase { mGroupAlertTransferHelper.setHeadsUpManager(mHeadsUpManager); mGroupAlertTransferHelper.bind(mNotificationEntryManager, mGroupManager); - verify(mNotificationEntryManager).setAlertTransferListener(mListenerCaptor.capture()); - mAlertTransferListener = mListenerCaptor.getValue(); + verify(mNotificationEntryManager).addNotificationEntryListener(mListenerCaptor.capture()); + mNotificationEntryListener = mListenerCaptor.getValue(); mHeadsUpManager.addListener(mGroupAlertTransferHelper); mAmbientPulseManager.addListener(mGroupAlertTransferHelper); } @@ -121,7 +122,7 @@ public class NotificationGroupAlertTransferHelperTest extends SysuiTestCase { // Add second child notification so that summary is no longer suppressed. mPendingEntries.put(childEntry2.key, childEntry2); - mAlertTransferListener.onPendingEntryAdded(childEntry2); + mNotificationEntryListener.onPendingEntryAdded(childEntry2); mGroupManager.onEntryAdded(childEntry2); // The alert state should transfer back to the summary as there is now more than one @@ -148,7 +149,7 @@ public class NotificationGroupAlertTransferHelperTest extends SysuiTestCase { // Add second child notification so that summary is no longer suppressed. mPendingEntries.put(childEntry2.key, childEntry2); - mAlertTransferListener.onPendingEntryAdded(childEntry2); + mNotificationEntryListener.onPendingEntryAdded(childEntry2); mGroupManager.onEntryAdded(childEntry2); // Dozing changed so no reason to re-alert summary. @@ -186,7 +187,7 @@ public class NotificationGroupAlertTransferHelperTest extends SysuiTestCase { when(childEntry.getRow().isInflationFlagSet(mHeadsUpManager.getContentFlag())) .thenReturn(true); - mAlertTransferListener.onEntryReinflated(childEntry); + mNotificationEntryListener.onEntryReinflated(childEntry); // Alert is immediately removed from summary, and we show child as its content is inflated. assertFalse(mHeadsUpManager.isAlerting(summaryEntry.key)); @@ -210,13 +211,13 @@ public class NotificationGroupAlertTransferHelperTest extends SysuiTestCase { // Add second child notification so that summary is no longer suppressed. mPendingEntries.put(childEntry2.key, childEntry2); - mAlertTransferListener.onPendingEntryAdded(childEntry2); + mNotificationEntryListener.onPendingEntryAdded(childEntry2); mGroupManager.onEntryAdded(childEntry2); // Child entry finishes its inflation. when(childEntry.getRow().isInflationFlagSet(mHeadsUpManager.getContentFlag())) .thenReturn(true); - mAlertTransferListener.onEntryReinflated(childEntry); + mNotificationEntryListener.onEntryReinflated(childEntry); verify(childEntry.getRow(), times(1)).freeContentViewWhenSafe(mHeadsUpManager .getContentFlag()); @@ -236,7 +237,7 @@ public class NotificationGroupAlertTransferHelperTest extends SysuiTestCase { mGroupManager.onEntryAdded(summaryEntry); mGroupManager.onEntryAdded(childEntry); - mAlertTransferListener.onEntryRemoved(childEntry); + mNotificationEntryListener.onEntryRemoved(childEntry); assertFalse(mGroupAlertTransferHelper.isAlertTransferPending(childEntry)); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java index e5620a5520d5e..c584d026f62be 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.phone; import static android.app.NotificationManager.IMPORTANCE_HIGH; +import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; @@ -91,7 +92,10 @@ import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.StatusBarStateController; import com.android.systemui.statusbar.notification.NotificationData; import com.android.systemui.statusbar.notification.NotificationData.Entry; +import com.android.systemui.statusbar.notification.NotificationEntryListener; import com.android.systemui.statusbar.notification.NotificationEntryManager; +import com.android.systemui.statusbar.notification.NotificationFilter; +import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider; import com.android.systemui.statusbar.notification.VisualStabilityManager; import com.android.systemui.statusbar.notification.logging.NotificationLogger; import com.android.systemui.statusbar.notification.row.NotificationGutsManager; @@ -129,6 +133,8 @@ public class StatusBarTest extends SysuiTestCase { @Mock private ArrayList mNotificationList; @Mock private BiometricUnlockController mBiometricUnlockController; @Mock private NotificationData mNotificationData; + @Mock + private NotificationInterruptionStateProvider.HeadsUpSuppressor mHeadsUpSuppressor; // Mock dependencies: @Mock private NotificationViewHierarchyManager mViewHierarchyManager; @@ -141,13 +147,17 @@ public class StatusBarTest extends SysuiTestCase { @Mock private StatusBarStateController mStatusBarStateController; @Mock private DeviceProvisionedController mDeviceProvisionedController; @Mock private NotificationPresenter mNotificationPresenter; - @Mock private NotificationEntryManager.Callback mCallback; + @Mock + private NotificationEntryListener mCallback; @Mock private BubbleController mBubbleController; + @Mock + private NotificationFilter mNotificationFilter; private TestableStatusBar mStatusBar; private FakeMetricsLogger mMetricsLogger; private PowerManager mPowerManager; private TestableNotificationEntryManager mEntryManager; + private TestableNotificationInterruptionStateProvider mNotificationInterruptionStateProvider; private NotificationLogger mNotificationLogger; private CommandQueue mCommandQueue; @@ -168,6 +178,17 @@ public class StatusBarTest extends SysuiTestCase { mDependency.injectTestDependency(DeviceProvisionedController.class, mDeviceProvisionedController); mDependency.injectMockDependency(BubbleController.class); + mDependency.injectTestDependency(NotificationFilter.class, mNotificationFilter); + + IPowerManager powerManagerService = mock(IPowerManager.class); + mPowerManager = new PowerManager(mContext, powerManagerService, + Handler.createAsync(Looper.myLooper())); + + mNotificationInterruptionStateProvider = + new TestableNotificationInterruptionStateProvider(mContext, mPowerManager, + mDreamManager); + mDependency.injectTestDependency(NotificationInterruptionStateProvider.class, + mNotificationInterruptionStateProvider); mContext.addMockSystemService(TrustManager.class, mock(TrustManager.class)); mContext.addMockSystemService(FingerprintManager.class, mock(FingerprintManager.class)); @@ -178,10 +199,6 @@ public class StatusBarTest extends SysuiTestCase { mNotificationLogger = new NotificationLogger(); DozeLog.traceDozing(mContext, false /* dozing */); - IPowerManager powerManagerService = mock(IPowerManager.class); - mPowerManager = new PowerManager(mContext, powerManagerService, - Handler.createAsync(Looper.myLooper())); - mCommandQueue = mock(CommandQueue.class); when(mCommandQueue.asBinder()).thenReturn(new Binder()); mContext.putComponent(CommandQueue.class, mCommandQueue); @@ -205,7 +222,10 @@ public class StatusBarTest extends SysuiTestCase { return null; }).when(mStatusBarKeyguardViewManager).addAfterKeyguardGoneRunnable(any()); - mEntryManager = new TestableNotificationEntryManager(mDreamManager, mPowerManager, mContext); + mNotificationInterruptionStateProvider.setUpWithPresenter(mNotificationPresenter, + mHeadsUpManager, mHeadsUpSuppressor); + + mEntryManager = new TestableNotificationEntryManager(mPowerManager, mContext); when(mRemoteInputManager.getController()).thenReturn(mRemoteInputController); mStatusBar = new TestableStatusBar(mStatusBarKeyguardViewManager, mUnlockMethodCache, mKeyguardIndicationController, mStackScroller, mHeadsUpManager, @@ -362,11 +382,9 @@ public class StatusBarTest extends SysuiTestCase { public void testShouldHeadsUp_nonSuppressedGroupSummary() throws Exception { when(mPowerManager.isScreenOn()).thenReturn(true); when(mHeadsUpManager.isSnoozed(anyString())).thenReturn(false); - when(mNotificationData.shouldSuppressStatusBar(any())).thenReturn(false); - when(mNotificationData.shouldFilterOut(any())).thenReturn(false); + when(mNotificationFilter.shouldFilterOut(any())).thenReturn(false); when(mDreamManager.isDreaming()).thenReturn(false); - when(mNotificationData.getImportance(any())).thenReturn(IMPORTANCE_HIGH); - when(mCallback.canHeadsUp(any(), any())).thenReturn(true); + when(mHeadsUpSuppressor.canHeadsUp(any(), any())).thenReturn(true); Notification n = new Notification.Builder(getContext(), "a") .setGroup("a") @@ -376,19 +394,18 @@ public class StatusBarTest extends SysuiTestCase { StatusBarNotification sbn = new StatusBarNotification("a", "a", 0, "a", 0, 0, n, UserHandle.of(0), null, 0); NotificationData.Entry entry = new NotificationData.Entry(sbn); + entry.importance = IMPORTANCE_HIGH; - assertTrue(mEntryManager.shouldHeadsUp(entry)); + assertTrue(mNotificationInterruptionStateProvider.shouldHeadsUp(entry)); } @Test public void testShouldHeadsUp_suppressedGroupSummary() throws Exception { when(mPowerManager.isScreenOn()).thenReturn(true); when(mHeadsUpManager.isSnoozed(anyString())).thenReturn(false); - when(mNotificationData.shouldSuppressStatusBar(any())).thenReturn(false); - when(mNotificationData.shouldFilterOut(any())).thenReturn(false); + when(mNotificationFilter.shouldFilterOut(any())).thenReturn(false); when(mDreamManager.isDreaming()).thenReturn(false); - when(mNotificationData.getImportance(any())).thenReturn(IMPORTANCE_HIGH); - when(mCallback.canHeadsUp(any(), any())).thenReturn(true); + when(mHeadsUpSuppressor.canHeadsUp(any(), any())).thenReturn(true); Notification n = new Notification.Builder(getContext(), "a") .setGroup("a") @@ -398,46 +415,44 @@ public class StatusBarTest extends SysuiTestCase { StatusBarNotification sbn = new StatusBarNotification("a", "a", 0, "a", 0, 0, n, UserHandle.of(0), null, 0); NotificationData.Entry entry = new NotificationData.Entry(sbn); + entry.importance = IMPORTANCE_HIGH; - assertFalse(mEntryManager.shouldHeadsUp(entry)); + assertFalse(mNotificationInterruptionStateProvider.shouldHeadsUp(entry)); } @Test public void testShouldHeadsUp_suppressedHeadsUp() throws Exception { when(mPowerManager.isScreenOn()).thenReturn(true); when(mHeadsUpManager.isSnoozed(anyString())).thenReturn(false); - when(mNotificationData.shouldFilterOut(any())).thenReturn(false); + when(mNotificationFilter.shouldFilterOut(any())).thenReturn(false); when(mDreamManager.isDreaming()).thenReturn(false); - when(mNotificationData.getImportance(any())).thenReturn(IMPORTANCE_HIGH); - when(mCallback.canHeadsUp(any(), any())).thenReturn(true); - - when(mNotificationData.shouldSuppressPeek(any())).thenReturn(true); + when(mHeadsUpSuppressor.canHeadsUp(any(), any())).thenReturn(true); Notification n = new Notification.Builder(getContext(), "a").build(); StatusBarNotification sbn = new StatusBarNotification("a", "a", 0, "a", 0, 0, n, UserHandle.of(0), null, 0); NotificationData.Entry entry = new NotificationData.Entry(sbn); + entry.suppressedVisualEffects = SUPPRESSED_EFFECT_PEEK; + entry.importance = IMPORTANCE_HIGH; - assertFalse(mEntryManager.shouldHeadsUp(entry)); + assertFalse(mNotificationInterruptionStateProvider.shouldHeadsUp(entry)); } @Test public void testShouldHeadsUp_noSuppressedHeadsUp() throws Exception { when(mPowerManager.isScreenOn()).thenReturn(true); when(mHeadsUpManager.isSnoozed(anyString())).thenReturn(false); - when(mNotificationData.shouldFilterOut(any())).thenReturn(false); + when(mNotificationFilter.shouldFilterOut(any())).thenReturn(false); when(mDreamManager.isDreaming()).thenReturn(false); - when(mNotificationData.getImportance(any())).thenReturn(IMPORTANCE_HIGH); - when(mCallback.canHeadsUp(any(), any())).thenReturn(true); - - when(mNotificationData.shouldSuppressPeek(any())).thenReturn(false); + when(mHeadsUpSuppressor.canHeadsUp(any(), any())).thenReturn(true); Notification n = new Notification.Builder(getContext(), "a").build(); StatusBarNotification sbn = new StatusBarNotification("a", "a", 0, "a", 0, 0, n, UserHandle.of(0), null, 0); NotificationData.Entry entry = new NotificationData.Entry(sbn); + entry.importance = IMPORTANCE_HIGH; - assertTrue(mEntryManager.shouldHeadsUp(entry)); + assertTrue(mNotificationInterruptionStateProvider.shouldHeadsUp(entry)); } @Test @@ -725,20 +740,29 @@ public class StatusBarTest extends SysuiTestCase { public static class TestableNotificationEntryManager extends NotificationEntryManager { - public TestableNotificationEntryManager(IDreamManager dreamManager, - PowerManager powerManager, Context context) { + public TestableNotificationEntryManager(PowerManager powerManager, Context context) { super(context); - mDreamManager = dreamManager; mPowerManager = powerManager; } public void setUpForTest(NotificationPresenter presenter, NotificationListContainer listContainer, - Callback callback, + NotificationEntryListener callback, HeadsUpManagerPhone headsUpManager, NotificationData notificationData) { super.setUpWithPresenter(presenter, listContainer, callback, headsUpManager); mNotificationData = notificationData; + } + } + + public static class TestableNotificationInterruptionStateProvider extends + NotificationInterruptionStateProvider { + + public TestableNotificationInterruptionStateProvider( + Context context, + PowerManager powerManager, + IDreamManager dreamManager) { + super(context, powerManager, dreamManager); mUseHeadsUp = true; } }