diff options
40 files changed, 1322 insertions, 172 deletions
diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 4681d4943256..d6a067d35f9b 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -919,7 +919,7 @@ package android.content.pm { field public int id; field public String lastLoggedInFingerprint; field public long lastLoggedInTime; - field public String name; + field @Nullable public String name; field public boolean partial; field public boolean preCreated; field public int profileBadge; diff --git a/core/java/android/content/pm/UserInfo.java b/core/java/android/content/pm/UserInfo.java index 76e9fcb07f22..d6e13ac90f82 100644 --- a/core/java/android/content/pm/UserInfo.java +++ b/core/java/android/content/pm/UserInfo.java @@ -18,6 +18,7 @@ package android.content.pm; import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.TestApi; import android.annotation.UserIdInt; import android.compat.annotation.UnsupportedAppUsage; @@ -170,7 +171,7 @@ public class UserInfo implements Parcelable { @UnsupportedAppUsage public int serialNumber; @UnsupportedAppUsage - public String name; + public @Nullable String name; @UnsupportedAppUsage public String iconPath; @UnsupportedAppUsage diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index a64e63eacd56..196f2f94120e 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -2179,7 +2179,10 @@ public class UserManager { } } else { UserInfo userInfo = getUserInfo(mUserId); - return userInfo == null ? "" : userInfo.name; + if (userInfo != null && userInfo.name != null) { + return userInfo.name; + } + return ""; } } diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index f0a685ec4d2e..3fee914f2def 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -245,7 +245,7 @@ public class ChooserActivity extends ResolverActivity implements SystemUiDeviceConfigFlags.IS_NEARBY_SHARE_FIRST_TARGET_IN_RANKED_APP, DEFAULT_IS_NEARBY_SHARE_FIRST_TARGET_IN_RANKED_APP); - private static final int DEFAULT_LIST_VIEW_UPDATE_DELAY_MS = 250; + private static final int DEFAULT_LIST_VIEW_UPDATE_DELAY_MS = 125; @VisibleForTesting int mListViewUpdateDelayMs = DeviceConfig.getInt(DeviceConfig.NAMESPACE_SYSTEMUI, diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl index 46b463074383..ef8f2db5ff57 100644 --- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl @@ -88,7 +88,7 @@ interface IStatusBarService in int notificationLocation, boolean modifiedBeforeSending); void onNotificationSettingsViewed(String key); void onNotificationBubbleChanged(String key, boolean isBubble, int flags); - void onBubbleNotificationSuppressionChanged(String key, boolean isNotifSuppressed, boolean isBubbleSuppressed); + void onBubbleMetadataFlagChanged(String key, int flags); void hideCurrentInputMethodForBubbles(); void grantInlineReplyUriPermission(String key, in Uri uri, in UserHandle user, String packageName); oneway void clearInlineReplyUriPermissions(String key); diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index d32acaf1e620..0f328b034f38 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -7056,6 +7056,10 @@ android:permission="android.permission.BIND_JOB_SERVICE"> </service> + <service android:name="com.android.server.notification.ReviewNotificationPermissionsJobService" + android:permission="android.permission.BIND_JOB_SERVICE"> + </service> + <service android:name="com.android.server.pm.PackageManagerShellCommandDataLoader" android:exported="false"> <intent-filter> diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index 82f8a131ae2a..faada1aa03ef 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -777,22 +777,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return null; } - private void updateCallbackIfNecessary() { - updateCallbackIfNecessary(true /* deferCallbackUntilAllActivitiesCreated */); - } - /** * Notifies listeners about changes to split states if necessary. - * - * @param deferCallbackUntilAllActivitiesCreated boolean to indicate whether the split info - * callback should be deferred until all the - * organized activities have been created. */ - private void updateCallbackIfNecessary(boolean deferCallbackUntilAllActivitiesCreated) { + private void updateCallbackIfNecessary() { if (mEmbeddingCallback == null) { return; } - if (deferCallbackUntilAllActivitiesCreated && !allActivitiesCreated()) { + if (!allActivitiesCreated()) { return; } List<SplitInfo> currentSplitStates = getActiveSplitStates(); @@ -848,9 +840,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen for (int i = mTaskContainers.size() - 1; i >= 0; i--) { final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i).mContainers; for (TaskFragmentContainer container : containers) { - if (container.getInfo() == null - || container.getInfo().getActivities().size() - != container.collectActivities().size()) { + if (!container.taskInfoActivityCountMatchesCreated()) { return false; } } @@ -1035,11 +1025,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen && container.getTaskFragmentToken().equals(initialTaskFragmentToken)) { // The onTaskFragmentInfoChanged callback containing this activity has not // reached the client yet, so add the activity to the pending appeared - // activities and send a split info callback to the client before - // {@link Activity#onCreate} is called. + // activities. container.addPendingAppearedActivity(activity); - updateCallbackIfNecessary( - false /* deferCallbackUntilAllActivitiesCreated */); return; } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java index 35981d3af948..26bbcbb937f0 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -145,6 +145,18 @@ class TaskFragmentContainer { return allActivities; } + /** + * Checks if the count of activities from the same process in task fragment info corresponds to + * the ones created and available on the client side. + */ + boolean taskInfoActivityCountMatchesCreated() { + if (mInfo == null) { + return false; + } + return mPendingAppearedActivities.isEmpty() + && mInfo.getActivities().size() == collectActivities().size(); + } + ActivityStack toActivityStack() { return new ActivityStack(collectActivities(), isEmpty()); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java index 227494c04049..31fc6a5be589 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java @@ -71,7 +71,7 @@ public class Bubble implements BubbleViewProvider { private long mLastAccessed; @Nullable - private Bubbles.SuppressionChangedListener mSuppressionListener; + private Bubbles.BubbleMetadataFlagListener mBubbleMetadataFlagListener; /** Whether the bubble should show a dot for the notification indicating updated content. */ private boolean mShowBubbleUpdateDot = true; @@ -192,13 +192,13 @@ public class Bubble implements BubbleViewProvider { @VisibleForTesting(visibility = PRIVATE) public Bubble(@NonNull final BubbleEntry entry, - @Nullable final Bubbles.SuppressionChangedListener listener, + @Nullable final Bubbles.BubbleMetadataFlagListener listener, final Bubbles.PendingIntentCanceledListener intentCancelListener, Executor mainExecutor) { mKey = entry.getKey(); mGroupKey = entry.getGroupKey(); mLocusId = entry.getLocusId(); - mSuppressionListener = listener; + mBubbleMetadataFlagListener = listener; mIntentCancelListener = intent -> { if (mIntent != null) { mIntent.unregisterCancelListener(mIntentCancelListener); @@ -606,8 +606,8 @@ public class Bubble implements BubbleViewProvider { mFlags &= ~Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; } - if (showInShade() != prevShowInShade && mSuppressionListener != null) { - mSuppressionListener.onBubbleNotificationSuppressionChange(this); + if (showInShade() != prevShowInShade && mBubbleMetadataFlagListener != null) { + mBubbleMetadataFlagListener.onBubbleMetadataFlagChanged(this); } } @@ -626,8 +626,8 @@ public class Bubble implements BubbleViewProvider { } else { mFlags &= ~Notification.BubbleMetadata.FLAG_SUPPRESS_BUBBLE; } - if (prevSuppressed != suppressBubble && mSuppressionListener != null) { - mSuppressionListener.onBubbleNotificationSuppressionChange(this); + if (prevSuppressed != suppressBubble && mBubbleMetadataFlagListener != null) { + mBubbleMetadataFlagListener.onBubbleMetadataFlagChanged(this); } } @@ -771,12 +771,17 @@ public class Bubble implements BubbleViewProvider { return isEnabled(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); } - void setShouldAutoExpand(boolean shouldAutoExpand) { + @VisibleForTesting + public void setShouldAutoExpand(boolean shouldAutoExpand) { + boolean prevAutoExpand = shouldAutoExpand(); if (shouldAutoExpand) { enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); } else { disable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); } + if (prevAutoExpand != shouldAutoExpand && mBubbleMetadataFlagListener != null) { + mBubbleMetadataFlagListener.onBubbleMetadataFlagChanged(this); + } } public void setIsBubble(final boolean isBubble) { @@ -799,6 +804,10 @@ public class Bubble implements BubbleViewProvider { return (mFlags & option) != 0; } + public int getFlags() { + return mFlags; + } + @Override public String toString() { return "Bubble{" + mKey + '}'; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 806c395bf395..f407bdcb8852 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -323,7 +323,7 @@ public class BubbleController { public void initialize() { mBubbleData.setListener(mBubbleDataListener); - mBubbleData.setSuppressionChangedListener(this::onBubbleNotificationSuppressionChanged); + mBubbleData.setSuppressionChangedListener(this::onBubbleMetadataFlagChanged); mBubbleData.setPendingIntentCancelledListener(bubble -> { if (bubble.getBubbleIntent() == null) { @@ -554,11 +554,10 @@ public class BubbleController { } @VisibleForTesting - public void onBubbleNotificationSuppressionChanged(Bubble bubble) { + public void onBubbleMetadataFlagChanged(Bubble bubble) { // Make sure NoMan knows suppression state so that anyone querying it can tell. try { - mBarService.onBubbleNotificationSuppressionChanged(bubble.getKey(), - !bubble.showInShade(), bubble.isSuppressed()); + mBarService.onBubbleMetadataFlagChanged(bubble.getKey(), bubble.getFlags()); } catch (RemoteException e) { // Bad things have happened } @@ -1038,7 +1037,15 @@ public class BubbleController { } } else { Bubble bubble = mBubbleData.getOrCreateBubble(notif, null /* persistedBubble */); - inflateAndAdd(bubble, suppressFlyout, showInShade); + if (notif.shouldSuppressNotificationList()) { + // If we're suppressing notifs for DND, we don't want the bubbles to randomly + // expand when DND turns off so flip the flag. + if (bubble.shouldAutoExpand()) { + bubble.setShouldAutoExpand(false); + } + } else { + inflateAndAdd(bubble, suppressFlyout, showInShade); + } } } @@ -1070,7 +1077,8 @@ public class BubbleController { } } - private void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp) { + @VisibleForTesting + public void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp) { // shouldBubbleUp checks canBubble & for bubble metadata boolean shouldBubble = shouldBubbleUp && canLaunchInTaskView(mContext, entry); if (!shouldBubble && mBubbleData.hasAnyBubbleWithKey(entry.getKey())) { @@ -1096,7 +1104,8 @@ public class BubbleController { } } - private void onRankingUpdated(RankingMap rankingMap, + @VisibleForTesting + public void onRankingUpdated(RankingMap rankingMap, HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey) { if (mTmpRanking == null) { mTmpRanking = new NotificationListenerService.Ranking(); @@ -1107,19 +1116,22 @@ public class BubbleController { Pair<BubbleEntry, Boolean> entryData = entryDataByKey.get(key); BubbleEntry entry = entryData.first; boolean shouldBubbleUp = entryData.second; - if (entry != null && !isCurrentProfile( entry.getStatusBarNotification().getUser().getIdentifier())) { return; } - + if (entry != null && (entry.shouldSuppressNotificationList() + || entry.getRanking().isSuspended())) { + shouldBubbleUp = false; + } rankingMap.getRanking(key, mTmpRanking); - boolean isActiveBubble = mBubbleData.hasAnyBubbleWithKey(key); - if (isActiveBubble && !mTmpRanking.canBubble()) { + boolean isActiveOrInOverflow = mBubbleData.hasAnyBubbleWithKey(key); + boolean isActive = mBubbleData.hasBubbleInStackWithKey(key); + if (isActiveOrInOverflow && !mTmpRanking.canBubble()) { // If this entry is no longer allowed to bubble, dismiss with the BLOCKED reason. // This means that the app or channel's ability to bubble has been revoked. mBubbleData.dismissBubbleWithKey(key, DISMISS_BLOCKED); - } else if (isActiveBubble && (!shouldBubbleUp || entry.getRanking().isSuspended())) { + } else if (isActiveOrInOverflow && !shouldBubbleUp) { // If this entry is allowed to bubble, but cannot currently bubble up or is // suspended, dismiss it. This happens when DND is enabled and configured to hide // bubbles, or focus mode is enabled and the app is designated as distracting. @@ -1127,9 +1139,9 @@ public class BubbleController { // notification, so that the bubble will be re-created if shouldBubbleUp returns // true. mBubbleData.dismissBubbleWithKey(key, DISMISS_NO_BUBBLE_UP); - } else if (entry != null && mTmpRanking.isBubble() && !isActiveBubble) { + } else if (entry != null && mTmpRanking.isBubble() && !isActive) { entry.setFlagBubble(true); - onEntryUpdated(entry, shouldBubbleUp && !entry.getRanking().isSuspended()); + onEntryUpdated(entry, shouldBubbleUp); } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java index c98c0e69de15..e4a0fd03860c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java @@ -159,7 +159,7 @@ public class BubbleData { private Listener mListener; @Nullable - private Bubbles.SuppressionChangedListener mSuppressionListener; + private Bubbles.BubbleMetadataFlagListener mBubbleMetadataFlagListener; private Bubbles.PendingIntentCanceledListener mCancelledListener; /** @@ -190,9 +190,8 @@ public class BubbleData { mMaxOverflowBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_overflow); } - public void setSuppressionChangedListener( - Bubbles.SuppressionChangedListener listener) { - mSuppressionListener = listener; + public void setSuppressionChangedListener(Bubbles.BubbleMetadataFlagListener listener) { + mBubbleMetadataFlagListener = listener; } public void setPendingIntentCancelledListener( @@ -311,7 +310,7 @@ public class BubbleData { bubbleToReturn = mPendingBubbles.get(key); } else if (entry != null) { // New bubble - bubbleToReturn = new Bubble(entry, mSuppressionListener, mCancelledListener, + bubbleToReturn = new Bubble(entry, mBubbleMetadataFlagListener, mCancelledListener, mMainExecutor); } else { // Persisted bubble being promoted @@ -1058,6 +1057,22 @@ public class BubbleData { return null; } + /** + * Get a pending bubble with given notification <code>key</code> + * + * @param key notification key + * @return bubble that matches or null + */ + @VisibleForTesting(visibility = PRIVATE) + public Bubble getPendingBubbleWithKey(String key) { + for (Bubble b : mPendingBubbles.values()) { + if (b.getKey().equals(key)) { + return b; + } + } + return null; + } + @VisibleForTesting(visibility = PRIVATE) void setTimeSource(TimeSource timeSource) { mTimeSource = timeSource; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java index 2b2a2f7e35df..c7db8d8d1646 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java @@ -263,10 +263,10 @@ public interface Bubbles { void onBubbleExpandChanged(boolean isExpanding, String key); } - /** Listener to be notified when the flags for notification or bubble suppression changes.*/ - interface SuppressionChangedListener { - /** Called when the notification suppression state of a bubble changes. */ - void onBubbleNotificationSuppressionChange(Bubble bubble); + /** Listener to be notified when the flags on BubbleMetadata have changed. */ + interface BubbleMetadataFlagListener { + /** Called when the flags on BubbleMetadata have changed for the provided bubble. */ + void onBubbleMetadataFlagChanged(Bubble bubble); } /** Listener to be notified when a pending intent has been canceled for a bubble. */ diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java index 169f03e7bc3e..bde94d9d6c29 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java @@ -115,7 +115,7 @@ public class BubbleDataTest extends ShellTestCase { private ArgumentCaptor<BubbleData.Update> mUpdateCaptor; @Mock - private Bubbles.SuppressionChangedListener mSuppressionListener; + private Bubbles.BubbleMetadataFlagListener mBubbleMetadataFlagListener; @Mock private Bubbles.PendingIntentCanceledListener mPendingIntentCanceledListener; @@ -136,30 +136,47 @@ public class BubbleDataTest extends ShellTestCase { mock(NotificationListenerService.Ranking.class); when(ranking.isTextChanged()).thenReturn(true); mEntryInterruptive = createBubbleEntry(1, "interruptive", "package.d", ranking); - mBubbleInterruptive = new Bubble(mEntryInterruptive, mSuppressionListener, null, + mBubbleInterruptive = new Bubble(mEntryInterruptive, mBubbleMetadataFlagListener, null, mMainExecutor); mEntryDismissed = createBubbleEntry(1, "dismissed", "package.d", null); - mBubbleDismissed = new Bubble(mEntryDismissed, mSuppressionListener, null, + mBubbleDismissed = new Bubble(mEntryDismissed, mBubbleMetadataFlagListener, null, mMainExecutor); mEntryLocusId = createBubbleEntry(1, "keyLocus", "package.e", null, new LocusId("locusId1")); - mBubbleLocusId = new Bubble(mEntryLocusId, mSuppressionListener, null, mMainExecutor); + mBubbleLocusId = new Bubble(mEntryLocusId, + mBubbleMetadataFlagListener, + null /* pendingIntentCanceledListener */, + mMainExecutor); - mBubbleA1 = new Bubble(mEntryA1, mSuppressionListener, mPendingIntentCanceledListener, + mBubbleA1 = new Bubble(mEntryA1, + mBubbleMetadataFlagListener, + mPendingIntentCanceledListener, mMainExecutor); - mBubbleA2 = new Bubble(mEntryA2, mSuppressionListener, mPendingIntentCanceledListener, + mBubbleA2 = new Bubble(mEntryA2, + mBubbleMetadataFlagListener, + mPendingIntentCanceledListener, mMainExecutor); - mBubbleA3 = new Bubble(mEntryA3, mSuppressionListener, mPendingIntentCanceledListener, + mBubbleA3 = new Bubble(mEntryA3, + mBubbleMetadataFlagListener, + mPendingIntentCanceledListener, mMainExecutor); - mBubbleB1 = new Bubble(mEntryB1, mSuppressionListener, mPendingIntentCanceledListener, + mBubbleB1 = new Bubble(mEntryB1, + mBubbleMetadataFlagListener, + mPendingIntentCanceledListener, mMainExecutor); - mBubbleB2 = new Bubble(mEntryB2, mSuppressionListener, mPendingIntentCanceledListener, + mBubbleB2 = new Bubble(mEntryB2, + mBubbleMetadataFlagListener, + mPendingIntentCanceledListener, mMainExecutor); - mBubbleB3 = new Bubble(mEntryB3, mSuppressionListener, mPendingIntentCanceledListener, + mBubbleB3 = new Bubble(mEntryB3, + mBubbleMetadataFlagListener, + mPendingIntentCanceledListener, mMainExecutor); - mBubbleC1 = new Bubble(mEntryC1, mSuppressionListener, mPendingIntentCanceledListener, + mBubbleC1 = new Bubble(mEntryC1, + mBubbleMetadataFlagListener, + mPendingIntentCanceledListener, mMainExecutor); mPositioner = new TestableBubblePositioner(mContext, mock(WindowManager.class)); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java index 819a984b4a77..e8f3f69ca64e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java @@ -63,7 +63,7 @@ public class BubbleTest extends ShellTestCase { private Bubble mBubble; @Mock - private Bubbles.SuppressionChangedListener mSuppressionListener; + private Bubbles.BubbleMetadataFlagListener mBubbleMetadataFlagListener; @Before public void setUp() { @@ -81,7 +81,7 @@ public class BubbleTest extends ShellTestCase { when(mNotif.getBubbleMetadata()).thenReturn(metadata); when(mSbn.getKey()).thenReturn("mock"); mBubbleEntry = new BubbleEntry(mSbn, null, true, false, false, false); - mBubble = new Bubble(mBubbleEntry, mSuppressionListener, null, mMainExecutor); + mBubble = new Bubble(mBubbleEntry, mBubbleMetadataFlagListener, null, mMainExecutor); } @Test @@ -144,22 +144,22 @@ public class BubbleTest extends ShellTestCase { } @Test - public void testSuppressionListener_change_notified() { + public void testBubbleMetadataFlagListener_change_notified() { assertThat(mBubble.showInShade()).isTrue(); mBubble.setSuppressNotification(true); assertThat(mBubble.showInShade()).isFalse(); - verify(mSuppressionListener).onBubbleNotificationSuppressionChange(mBubble); + verify(mBubbleMetadataFlagListener).onBubbleMetadataFlagChanged(mBubble); } @Test - public void testSuppressionListener_noChange_doesntNotify() { + public void testBubbleMetadataFlagListener_noChange_doesntNotify() { assertThat(mBubble.showInShade()).isTrue(); mBubble.setSuppressNotification(false); - verify(mSuppressionListener, never()).onBubbleNotificationSuppressionChange(any()); + verify(mBubbleMetadataFlagListener, never()).onBubbleMetadataFlagChanged(any()); } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java index e7f97d25fabe..58b6ad3e51e8 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java @@ -55,8 +55,6 @@ import android.view.WindowManager; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.core.graphics.drawable.IconCompat; -import androidx.mediarouter.media.MediaRouter; -import androidx.mediarouter.media.MediaRouterParams; import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.Utils; @@ -215,9 +213,8 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, } boolean shouldShowLaunchSection() { - MediaRouterParams routerParams = MediaRouter.getInstance(mContext).getRouterParams(); - Log.d(TAG, "try to get routerParams: " + routerParams); - return routerParams != null && !routerParams.isMediaTransferReceiverEnabled(); + // TODO(b/231398073): Implements this when available. + return false; } void setRefreshing(boolean refreshing) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java index ebd610bb0af4..0c9e1ec1ff77 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java @@ -180,13 +180,6 @@ public interface NotificationShadeWindowController extends RemoteInputController default void setRequestTopUi(boolean requestTopUi, String componentTag) {} /** - * Under low light conditions, we might want to increase the display brightness on devices that - * don't have an IR camera. - * @param brightness float from 0 to 1 or {@code LayoutParams.BRIGHTNESS_OVERRIDE_NONE} - */ - default void setFaceAuthDisplayBrightness(float brightness) {} - - /** * If {@link LightRevealScrim} obscures the UI. * @param opaque if the scrim is opaque */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinator.kt index a390e9f9b09d..15ad312b413e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinator.kt @@ -20,11 +20,13 @@ import com.android.systemui.statusbar.notification.collection.ListEntry import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope +import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner import com.android.systemui.statusbar.notification.collection.render.NodeController import com.android.systemui.statusbar.notification.dagger.PeopleHeader +import com.android.systemui.statusbar.notification.icon.ConversationIconManager import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.PeopleNotificationType import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_NON_PERSON @@ -39,12 +41,40 @@ import javax.inject.Inject @CoordinatorScope class ConversationCoordinator @Inject constructor( private val peopleNotificationIdentifier: PeopleNotificationIdentifier, + private val conversationIconManager: ConversationIconManager, @PeopleHeader peopleHeaderController: NodeController ) : Coordinator { + private val promotedEntriesToSummaryOfSameChannel = + mutableMapOf<NotificationEntry, NotificationEntry>() + + private val onBeforeRenderListListener = OnBeforeRenderListListener { _ -> + val unimportantSummaries = promotedEntriesToSummaryOfSameChannel + .mapNotNull { (promoted, summary) -> + val originalGroup = summary.parent + when { + originalGroup == null -> null + originalGroup == promoted.parent -> null + originalGroup.parent == null -> null + originalGroup.summary != summary -> null + originalGroup.children.any { it.channel == summary.channel } -> null + else -> summary.key + } + } + conversationIconManager.setUnimportantConversations(unimportantSummaries) + promotedEntriesToSummaryOfSameChannel.clear() + } + private val notificationPromoter = object : NotifPromoter(TAG) { override fun shouldPromoteToTopLevel(entry: NotificationEntry): Boolean { - return entry.channel?.isImportantConversation == true + val shouldPromote = entry.channel?.isImportantConversation == true + if (shouldPromote) { + val summary = entry.parent?.summary + if (summary != null && entry.channel == summary.channel) { + promotedEntriesToSummaryOfSameChannel[entry] = summary + } + } + return shouldPromote } } @@ -67,6 +97,7 @@ class ConversationCoordinator @Inject constructor( override fun attach(pipeline: NotifPipeline) { pipeline.addPromoter(notificationPromoter) + pipeline.addOnBeforeRenderListListener(onBeforeRenderListListener) } private fun isConversation(entry: ListEntry): Boolean = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java index 34c8044ef0d3..d96590a82547 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java @@ -70,6 +70,8 @@ import com.android.systemui.statusbar.notification.collection.render.GroupMember import com.android.systemui.statusbar.notification.collection.render.NotifGutsViewManager; import com.android.systemui.statusbar.notification.collection.render.NotifShadeEventSource; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; +import com.android.systemui.statusbar.notification.icon.ConversationIconManager; +import com.android.systemui.statusbar.notification.icon.IconManager; import com.android.systemui.statusbar.notification.init.NotificationsController; import com.android.systemui.statusbar.notification.init.NotificationsControllerImpl; import com.android.systemui.statusbar.notification.init.NotificationsControllerStub; @@ -370,6 +372,10 @@ public interface NotificationsModule { /** */ @Binds + ConversationIconManager bindConversationIconManager(IconManager iconManager); + + /** */ + @Binds BindEventManager bindBindEventManagerImpl(BindEventManagerImpl bindEventManagerImpl); /** */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt index 5375ac345e50..d8965418b4c4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt @@ -27,6 +27,7 @@ import android.view.View import android.widget.ImageView import com.android.internal.statusbar.StatusBarIcon import com.android.systemui.R +import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.notification.InflationException import com.android.systemui.statusbar.notification.collection.NotificationEntry @@ -44,11 +45,14 @@ import javax.inject.Inject * TODO: Much of this code was copied whole-sale in order to get it out of NotificationEntry. * Long-term, it should probably live somewhere in the content inflation pipeline. */ +@SysUISingleton class IconManager @Inject constructor( private val notifCollection: CommonNotifCollection, private val launcherApps: LauncherApps, private val iconBuilder: IconBuilder -) { +) : ConversationIconManager { + private var unimportantConversationKeys: Set<String> = emptySet() + fun attach() { notifCollection.addCollectionListener(entryListener) } @@ -63,16 +67,8 @@ class IconManager @Inject constructor( } override fun onRankingApplied() { - // When the sensitivity changes OR when the isImportantConversation status changes, - // we need to update the icons - for (entry in notifCollection.allNotifs) { - val isImportant = isImportantConversation(entry) - if (entry.icons.areIconsAvailable && - isImportant != entry.icons.isImportantConversation) { - updateIconsSafe(entry) - } - entry.icons.isImportantConversation = isImportant - } + // rankings affect whether a conversation is important, which can change the icons + recalculateForImportantConversationChange() } } @@ -80,6 +76,18 @@ class IconManager @Inject constructor( entry -> updateIconsSafe(entry) } + private fun recalculateForImportantConversationChange() { + for (entry in notifCollection.allNotifs) { + val isImportant = isImportantConversation(entry) + if (entry.icons.areIconsAvailable && + isImportant != entry.icons.isImportantConversation + ) { + updateIconsSafe(entry) + } + entry.icons.isImportantConversation = isImportant + } + } + /** * Inflate icon views for each icon variant and assign appropriate icons to them. Stores the * result in [NotificationEntry.getIcons]. @@ -306,8 +314,28 @@ class IconManager @Inject constructor( } private fun isImportantConversation(entry: NotificationEntry): Boolean { - return entry.ranking.channel != null && entry.ranking.channel.isImportantConversation + return entry.ranking.channel != null && + entry.ranking.channel.isImportantConversation && + entry.key !in unimportantConversationKeys + } + + override fun setUnimportantConversations(keys: Collection<String>) { + val newKeys = keys.toSet() + val changed = unimportantConversationKeys != newKeys + unimportantConversationKeys = newKeys + if (changed) { + recalculateForImportantConversationChange() + } } } -private const val TAG = "IconManager"
\ No newline at end of file +private const val TAG = "IconManager" + +interface ConversationIconManager { + /** + * Sets the complete current set of notification keys which should (for the purposes of icon + * presentation) be considered unimportant. This tells the icon manager to remove the avatar + * of a group from which the priority notification has been removed. + */ + fun setUnimportantConversations(keys: Collection<String>) +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt index 5646545dcd23..0ff152380fb8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt @@ -164,12 +164,23 @@ private class KeyguardNotificationVisibilityProviderImpl @Inject constructor( !lockscreenUserManager.shouldShowLockscreenNotifications() -> true // User settings do not allow this notification on the lockscreen, so hide it. userSettingsDisallowNotification(entry) -> true + // if entry is silent, apply custom logic to see if should hide + shouldHideIfEntrySilent(entry) -> true + else -> false + } + + private fun shouldHideIfEntrySilent(entry: ListEntry): Boolean = when { + // Show if high priority (not hidden) + highPriorityProvider.isHighPriority(entry) -> false + // Ambient notifications are hidden always from lock screen + entry.representativeEntry?.isAmbient == true -> true + // [Now notification is silent] + // Hide regardless of parent priority if user wants silent notifs hidden + hideSilentNotificationsOnLockscreen -> true // Parent priority is high enough to be shown on the lockscreen, do not hide. - entry.parent?.let(::priorityExceedsLockscreenShowingThreshold) == true -> false - // Entry priority is high enough to be shown on the lockscreen, do not hide. - priorityExceedsLockscreenShowingThreshold(entry) -> false - // Priority is too low, hide. - else -> true + entry.parent?.let(::shouldHideIfEntrySilent) == false -> false + // Show when silent notifications are allowed on lockscreen + else -> false } private fun userSettingsDisallowNotification(entry: NotificationEntry): Boolean { @@ -193,11 +204,6 @@ private class KeyguardNotificationVisibilityProviderImpl @Inject constructor( } } - private fun priorityExceedsLockscreenShowingThreshold(entry: ListEntry): Boolean = when { - hideSilentNotificationsOnLockscreen -> highPriorityProvider.isHighPriority(entry) - else -> entry.representativeEntry?.ranking?.isAmbient == false - } - private fun readShowSilentNotificationSetting() { val showSilentNotifs = secureSettings.getBoolForUser(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java index 24660b261c51..01aa2ec9bfe6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java @@ -111,7 +111,6 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW private final SysuiColorExtractor mColorExtractor; private final ScreenOffAnimationController mScreenOffAnimationController; - private float mFaceAuthDisplayBrightness = LayoutParams.BRIGHTNESS_OVERRIDE_NONE; /** * Layout params would be aggregated and dispatched all at once if this is > 0. * @@ -266,12 +265,6 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW mScreenBrightnessDoze = value / 255f; } - @Override - public void setFaceAuthDisplayBrightness(float brightness) { - mFaceAuthDisplayBrightness = brightness; - apply(mCurrentState); - } - private void setKeyguardDark(boolean dark) { int vis = mNotificationShadeView.getSystemUiVisibility(); if (dark) { @@ -455,7 +448,9 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW private void applyWindowLayoutParams() { if (mDeferWindowLayoutParams == 0 && mLp != null && mLp.copyFrom(mLpChanged) != 0) { + Trace.beginSection("updateViewLayout"); mWindowManager.updateViewLayout(mNotificationShadeView, mLp); + Trace.endSection(); } } @@ -523,7 +518,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW if (state.mForceDozeBrightness) { mLpChanged.screenBrightness = mScreenBrightnessDoze; } else { - mLpChanged.screenBrightness = mFaceAuthDisplayBrightness; + mLpChanged.screenBrightness = LayoutParams.BRIGHTNESS_OVERRIDE_NONE; } } @@ -572,6 +567,10 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW @Override public void setPanelVisible(boolean visible) { + if (mCurrentState.mPanelVisible == visible + && mCurrentState.mNotificationShadeFocusable == visible) { + return; + } mCurrentState.mPanelVisible = visible; mCurrentState.mNotificationShadeFocusable = visible; apply(mCurrentState); @@ -626,8 +625,14 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW @Override public void setScrimsVisibility(int scrimsVisibility) { + if (scrimsVisibility == mCurrentState.mScrimsVisibility) { + return; + } + boolean wasExpanded = isExpanded(mCurrentState); mCurrentState.mScrimsVisibility = scrimsVisibility; - apply(mCurrentState); + if (wasExpanded != isExpanded(mCurrentState)) { + apply(mCurrentState); + } mScrimsVisibilityListener.accept(scrimsVisibility); } @@ -687,6 +692,9 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW @Override public void setPanelExpanded(boolean isExpanded) { + if (mCurrentState.mPanelExpanded == isExpanded) { + return; + } mCurrentState.mPanelExpanded = isExpanded; apply(mCurrentState); } @@ -703,6 +711,9 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW */ @Override public void setForceDozeBrightness(boolean forceDozeBrightness) { + if (mCurrentState.mForceDozeBrightness == forceDozeBrightness) { + return; + } mCurrentState.mForceDozeBrightness = forceDozeBrightness; apply(mCurrentState); } @@ -841,7 +852,6 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW boolean mLightRevealScrimOpaque; boolean mForceCollapsed; boolean mForceDozeBrightness; - int mFaceAuthDisplayBrightness; boolean mForceUserActivity; boolean mLaunchingActivity; boolean mBackdropShowing; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java index 37517219f103..5a33603d81ce 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java @@ -269,8 +269,12 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene super.onEnd(animation); if (animation.getTypeMask() == WindowInsets.Type.ime()) { mEntry.mRemoteEditImeAnimatingAway = false; - mEntry.mRemoteEditImeVisible = - mEditText.getRootWindowInsets().isVisible(WindowInsets.Type.ime()); + WindowInsets editTextRootWindowInsets = mEditText.getRootWindowInsets(); + if (editTextRootWindowInsets == null) { + Log.w(TAG, "onEnd called on detached view", new Exception()); + } + mEntry.mRemoteEditImeVisible = editTextRootWindowInsets != null + && editTextRootWindowInsets.isVisible(WindowInsets.Type.ime()); if (!mEntry.mRemoteEditImeVisible && !mEditText.mShowImeOnInputConnection) { mController.removeRemoteInput(mEntry, mToken); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java index b45d78d5502d..4b458f5a9123 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java @@ -88,4 +88,7 @@ public class GroupEntryBuilder { return this; } + public static List<NotificationEntry> getRawChildren(GroupEntry groupEntry) { + return groupEntry.getRawChildren(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinatorTest.kt index 7692a05eb5fc..742fcf5e03c3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinatorTest.kt @@ -21,17 +21,22 @@ import android.testing.AndroidTestingRunner import android.testing.TestableLooper import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.collection.GroupEntry +import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection +import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner import com.android.systemui.statusbar.notification.collection.render.NodeController +import com.android.systemui.statusbar.notification.icon.ConversationIconManager import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_IMPORTANT_PERSON import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_PERSON +import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.withArgCaptor import com.google.common.truth.Truth.assertThat import org.junit.Assert.assertFalse @@ -52,8 +57,10 @@ class ConversationCoordinatorTest : SysuiTestCase() { private lateinit var promoter: NotifPromoter private lateinit var peopleSectioner: NotifSectioner private lateinit var peopleComparator: NotifComparator + private lateinit var beforeRenderListListener: OnBeforeRenderListListener @Mock private lateinit var pipeline: NotifPipeline + @Mock private lateinit var conversationIconManager: ConversationIconManager @Mock private lateinit var peopleNotificationIdentifier: PeopleNotificationIdentifier @Mock private lateinit var channel: NotificationChannel @Mock private lateinit var headerController: NodeController @@ -66,7 +73,11 @@ class ConversationCoordinatorTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) - coordinator = ConversationCoordinator(peopleNotificationIdentifier, headerController) + coordinator = ConversationCoordinator( + peopleNotificationIdentifier, + conversationIconManager, + headerController + ) whenever(channel.isImportantConversation).thenReturn(true) coordinator.attach(pipeline) @@ -75,6 +86,9 @@ class ConversationCoordinatorTest : SysuiTestCase() { promoter = withArgCaptor { verify(pipeline).addPromoter(capture()) } + beforeRenderListListener = withArgCaptor { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } peopleSectioner = coordinator.sectioner peopleComparator = peopleSectioner.comparator!! @@ -96,6 +110,25 @@ class ConversationCoordinatorTest : SysuiTestCase() { } @Test + fun testPromotedImportantConversationsMakesSummaryUnimportant() { + val altChildA = NotificationEntryBuilder().setTag("A").build() + val altChildB = NotificationEntryBuilder().setTag("B").build() + val summary = NotificationEntryBuilder().setId(2).setChannel(channel).build() + val groupEntry = GroupEntryBuilder() + .setParent(GroupEntry.ROOT_ENTRY) + .setSummary(summary) + .setChildren(listOf(entry, altChildA, altChildB)) + .build() + assertTrue(promoter.shouldPromoteToTopLevel(entry)) + assertFalse(promoter.shouldPromoteToTopLevel(altChildA)) + assertFalse(promoter.shouldPromoteToTopLevel(altChildB)) + NotificationEntryBuilder.setNewParent(entry, GroupEntry.ROOT_ENTRY) + GroupEntryBuilder.getRawChildren(groupEntry).remove(entry) + beforeRenderListListener.onBeforeRenderList(listOf(entry, groupEntry)) + verify(conversationIconManager).setUnimportantConversations(eq(listOf(summary.key))) + } + + @Test fun testInPeopleSection() { whenever(peopleNotificationIdentifier.getPeopleNotificationType(entry)) .thenReturn(TYPE_PERSON) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java index cf996073f6a0..ed455a349bdc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java @@ -27,6 +27,7 @@ import static com.android.systemui.util.mockito.KotlinMockitoHelpersKt.argThat; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; @@ -228,6 +229,41 @@ public class KeyguardNotificationVisibilityProviderTest extends SysuiTestCase { } @Test + public void hideSilentNotificationsPerUserSettingWithHighPriorityParent() { + when(mKeyguardStateController.isShowing()).thenReturn(true); + mFakeSettings.putBool(Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, true); + mFakeSettings.putBool(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, false); + GroupEntry parent = new GroupEntryBuilder() + .setKey("parent") + .addChild(mEntry) + .setSummary(new NotificationEntryBuilder() + .setUser(new UserHandle(NOTIF_USER_ID)) + .setImportance(IMPORTANCE_LOW) + .build()) + .build(); + mEntry = new NotificationEntryBuilder() + .setUser(new UserHandle(NOTIF_USER_ID)) + .setImportance(IMPORTANCE_LOW) + .setParent(parent) + .build(); + when(mHighPriorityProvider.isHighPriority(any())).thenReturn(false); + assertTrue(mKeyguardNotificationVisibilityProvider.shouldHideNotification(mEntry)); + } + + @Test + public void hideSilentNotificationsPerUserSetting() { + when(mKeyguardStateController.isShowing()).thenReturn(true); + mFakeSettings.putBool(Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, true); + mFakeSettings.putBool(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, false); + mEntry = new NotificationEntryBuilder() + .setUser(new UserHandle(NOTIF_USER_ID)) + .setImportance(IMPORTANCE_LOW) + .build(); + when(mHighPriorityProvider.isHighPriority(any())).thenReturn(false); + assertTrue(mKeyguardNotificationVisibilityProvider.shouldHideNotification(mEntry)); + } + + @Test public void notifyListeners_onSettingChange_zenMode() { when(mKeyguardStateController.isShowing()).thenReturn(true); Consumer<String> listener = mock(Consumer.class); @@ -384,8 +420,8 @@ public class KeyguardNotificationVisibilityProviderTest extends SysuiTestCase { mFakeSettings.putBool(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, false); when(mHighPriorityProvider.isHighPriority(parent)).thenReturn(true); - // THEN don't filter out the entry - assertFalse( + // THEN filter out the entry regardless of parent + assertTrue( mKeyguardNotificationVisibilityProvider.shouldHideNotification(entryWithParent)); // WHEN its parent doesn't exceed threshold to show on lockscreen diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImplTest.java index ddccd834f76e..26199d53a2b4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImplTest.java @@ -30,6 +30,7 @@ import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import android.app.IActivityManager; @@ -103,6 +104,7 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { mNotificationShadeWindowController.setNotificationShadeView(mNotificationShadeWindowView); mNotificationShadeWindowController.attach(); + verify(mWindowManager).addView(eq(mNotificationShadeWindowView), any()); } @Test @@ -174,6 +176,14 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { } @Test + public void setScrimsVisibility_earlyReturn() { + clearInvocations(mWindowManager); + mNotificationShadeWindowController.setScrimsVisibility(ScrimController.TRANSPARENT); + // Abort early if value didn't change + verify(mWindowManager, never()).updateViewLayout(any(), mLayoutParameters.capture()); + } + + @Test public void attach_animatingKeyguardAndSurface_wallpaperVisible() { clearInvocations(mWindowManager); when(mKeyguardViewMediator.isShowingAndNotOccluded()).thenReturn(true); @@ -221,6 +231,8 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { public void setPanelExpanded_notFocusable_altFocusable_whenPanelIsOpen() { mNotificationShadeWindowController.setPanelExpanded(true); clearInvocations(mWindowManager); + mNotificationShadeWindowController.setPanelExpanded(true); + verifyNoMoreInteractions(mWindowManager); mNotificationShadeWindowController.setNotificationShadeFocusable(true); verify(mWindowManager).updateViewLayout(any(), mLayoutParameters.capture()); @@ -287,6 +299,8 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { public void batchApplyWindowLayoutParams_doesNotDispatchEvents() { mNotificationShadeWindowController.setForceDozeBrightness(true); verify(mWindowManager).updateViewLayout(any(), any()); + mNotificationShadeWindowController.setForceDozeBrightness(true); + verifyNoMoreInteractions(mWindowManager); clearInvocations(mWindowManager); mNotificationShadeWindowController.batchApplyWindowLayoutParams(()-> { diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index 193879e5c55c..238a4d37a872 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -57,6 +57,7 @@ import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.LauncherApps; import android.content.pm.PackageManager; +import android.content.pm.UserInfo; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; @@ -70,6 +71,8 @@ import android.service.notification.NotificationListenerService; import android.service.notification.ZenModeConfig; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import android.util.Pair; +import android.util.SparseArray; import android.view.View; import android.view.WindowManager; @@ -147,6 +150,7 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.HashMap; import java.util.List; import java.util.Optional; @@ -1010,7 +1014,7 @@ public class BubblesTest extends SysuiTestCase { assertBubbleNotificationSuppressedFromShade(mBubbleEntry); // Should notify delegate that shade state changed - verify(mBubbleController).onBubbleNotificationSuppressionChanged( + verify(mBubbleController).onBubbleMetadataFlagChanged( mBubbleData.getBubbleInStackWithKey(mRow.getKey())); } @@ -1027,7 +1031,7 @@ public class BubblesTest extends SysuiTestCase { assertBubbleNotificationSuppressedFromShade(mBubbleEntry); // Should notify delegate that shade state changed - verify(mBubbleController).onBubbleNotificationSuppressionChanged( + verify(mBubbleController).onBubbleMetadataFlagChanged( mBubbleData.getBubbleInStackWithKey(mRow.getKey())); } @@ -1447,6 +1451,69 @@ public class BubblesTest extends SysuiTestCase { assertThat(stackView.getVisibility()).isEqualTo(View.VISIBLE); } + @Test + public void testSetShouldAutoExpand_notifiesFlagChanged() { + mEntryListener.onPendingEntryAdded(mRow); + + assertTrue(mBubbleController.hasBubbles()); + Bubble b = mBubbleData.getBubbleInStackWithKey(mBubbleEntry.getKey()); + assertThat(b.shouldAutoExpand()).isFalse(); + + // Set it to the same thing + b.setShouldAutoExpand(false); + + // Verify it doesn't notify + verify(mBubbleController, never()).onBubbleMetadataFlagChanged(any()); + + // Set it to something different + b.setShouldAutoExpand(true); + verify(mBubbleController).onBubbleMetadataFlagChanged(b); + } + + @Test + public void testUpdateBubble_skipsDndSuppressListNotifs() { + mBubbleEntry = new BubbleEntry(mRow.getSbn(), mRow.getRanking(), mRow.isDismissable(), + mRow.shouldSuppressNotificationDot(), true /* DndSuppressNotifFromList */, + mRow.shouldSuppressPeek()); + mBubbleEntry.getBubbleMetadata().setFlags( + Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); + + mBubbleController.updateBubble(mBubbleEntry); + + Bubble b = mBubbleData.getPendingBubbleWithKey(mBubbleEntry.getKey()); + assertThat(b.shouldAutoExpand()).isFalse(); + assertThat(mBubbleData.getBubbleInStackWithKey(mBubbleEntry.getKey())).isNull(); + } + + @Test + public void testOnRankingUpdate_DndSuppressListNotif() { + // It's in the stack + mBubbleController.updateBubble(mBubbleEntry); + assertThat(mBubbleData.hasBubbleInStackWithKey(mBubbleEntry.getKey())).isTrue(); + + // Set current user profile + SparseArray<UserInfo> userInfos = new SparseArray<>(); + userInfos.put(mBubbleEntry.getStatusBarNotification().getUser().getIdentifier(), + mock(UserInfo.class)); + mBubbleController.onCurrentProfilesChanged(userInfos); + + // Send ranking update that the notif is suppressed from the list. + HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey = new HashMap<>(); + mBubbleEntry = new BubbleEntry(mRow.getSbn(), mRow.getRanking(), mRow.isDismissable(), + mRow.shouldSuppressNotificationDot(), true /* DndSuppressNotifFromList */, + mRow.shouldSuppressPeek()); + Pair<BubbleEntry, Boolean> pair = new Pair(mBubbleEntry, true); + entryDataByKey.put(mBubbleEntry.getKey(), pair); + + NotificationListenerService.RankingMap rankingMap = + mock(NotificationListenerService.RankingMap.class); + when(rankingMap.getOrderedKeys()).thenReturn(new String[] { mBubbleEntry.getKey() }); + mBubbleController.onRankingUpdated(rankingMap, entryDataByKey); + + // Should no longer be in the stack + assertThat(mBubbleData.hasBubbleInStackWithKey(mBubbleEntry.getKey())).isFalse(); + } + /** Creates a bubble using the userId and package. */ private Bubble createBubble(int userId, String pkg) { final UserHandle userHandle = new UserHandle(userId); diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java index 02d869172030..dff89e0a5558 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java @@ -50,6 +50,7 @@ import android.content.BroadcastReceiver; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.LauncherApps; +import android.content.pm.UserInfo; import android.hardware.display.AmbientDisplayConfiguration; import android.os.Handler; import android.os.PowerManager; @@ -59,6 +60,8 @@ import android.service.notification.NotificationListenerService; import android.service.notification.ZenModeConfig; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import android.util.Pair; +import android.util.SparseArray; import android.view.View; import android.view.WindowManager; @@ -128,6 +131,7 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.HashMap; import java.util.List; import java.util.Optional; @@ -880,7 +884,7 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase { assertBubbleNotificationSuppressedFromShade(mBubbleEntry); // Should notify delegate that shade state changed - verify(mBubbleController).onBubbleNotificationSuppressionChanged( + verify(mBubbleController).onBubbleMetadataFlagChanged( mBubbleData.getBubbleInStackWithKey(mRow.getKey())); } @@ -897,7 +901,7 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase { assertBubbleNotificationSuppressedFromShade(mBubbleEntry); // Should notify delegate that shade state changed - verify(mBubbleController).onBubbleNotificationSuppressionChanged( + verify(mBubbleController).onBubbleMetadataFlagChanged( mBubbleData.getBubbleInStackWithKey(mRow.getKey())); } @@ -1267,6 +1271,69 @@ public class NewNotifPipelineBubblesTest extends SysuiTestCase { assertThat(stackView.getVisibility()).isEqualTo(View.VISIBLE); } + @Test + public void testSetShouldAutoExpand_notifiesFlagChanged() { + mBubbleController.updateBubble(mBubbleEntry); + + assertTrue(mBubbleController.hasBubbles()); + Bubble b = mBubbleData.getBubbleInStackWithKey(mBubbleEntry.getKey()); + assertThat(b.shouldAutoExpand()).isFalse(); + + // Set it to the same thing + b.setShouldAutoExpand(false); + + // Verify it doesn't notify + verify(mBubbleController, never()).onBubbleMetadataFlagChanged(any()); + + // Set it to something different + b.setShouldAutoExpand(true); + verify(mBubbleController).onBubbleMetadataFlagChanged(b); + } + + @Test + public void testUpdateBubble_skipsDndSuppressListNotifs() { + mBubbleEntry = new BubbleEntry(mRow.getSbn(), mRow.getRanking(), mRow.isDismissable(), + mRow.shouldSuppressNotificationDot(), true /* DndSuppressNotifFromList */, + mRow.shouldSuppressPeek()); + mBubbleEntry.getBubbleMetadata().setFlags( + Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); + + mBubbleController.updateBubble(mBubbleEntry); + + Bubble b = mBubbleData.getPendingBubbleWithKey(mBubbleEntry.getKey()); + assertThat(b.shouldAutoExpand()).isFalse(); + assertThat(mBubbleData.getBubbleInStackWithKey(mBubbleEntry.getKey())).isNull(); + } + + @Test + public void testOnRankingUpdate_DndSuppressListNotif() { + // It's in the stack + mBubbleController.updateBubble(mBubbleEntry); + assertThat(mBubbleData.hasBubbleInStackWithKey(mBubbleEntry.getKey())).isTrue(); + + // Set current user profile + SparseArray<UserInfo> userInfos = new SparseArray<>(); + userInfos.put(mBubbleEntry.getStatusBarNotification().getUser().getIdentifier(), + mock(UserInfo.class)); + mBubbleController.onCurrentProfilesChanged(userInfos); + + // Send ranking update that the notif is suppressed from the list. + HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey = new HashMap<>(); + mBubbleEntry = new BubbleEntry(mRow.getSbn(), mRow.getRanking(), mRow.isDismissable(), + mRow.shouldSuppressNotificationDot(), true /* DndSuppressNotifFromList */, + mRow.shouldSuppressPeek()); + Pair<BubbleEntry, Boolean> pair = new Pair(mBubbleEntry, true); + entryDataByKey.put(mBubbleEntry.getKey(), pair); + + NotificationListenerService.RankingMap rankingMap = + mock(NotificationListenerService.RankingMap.class); + when(rankingMap.getOrderedKeys()).thenReturn(new String[] { mBubbleEntry.getKey() }); + mBubbleController.onRankingUpdated(rankingMap, entryDataByKey); + + // Should no longer be in the stack + assertThat(mBubbleData.hasBubbleInStackWithKey(mBubbleEntry.getKey())).isFalse(); + } + /** * Sets the bubble metadata flags for this entry. These flags are normally set by * NotificationManagerService when the notification is sent, however, these tests do not diff --git a/services/core/java/com/android/server/notification/NotificationDelegate.java b/services/core/java/com/android/server/notification/NotificationDelegate.java index 02f9ceb2d11d..89902f7f8321 100644 --- a/services/core/java/com/android/server/notification/NotificationDelegate.java +++ b/services/core/java/com/android/server/notification/NotificationDelegate.java @@ -51,14 +51,16 @@ public interface NotificationDelegate { void onNotificationSettingsViewed(String key); /** * Called when the state of {@link Notification#FLAG_BUBBLE} is changed. + * + * @param key the notification key + * @param isBubble whether the notification should have {@link Notification#FLAG_BUBBLE} applied + * @param flags the flags to apply to the notification's {@link Notification.BubbleMetadata} */ void onNotificationBubbleChanged(String key, boolean isBubble, int flags); /** - * Called when the state of {@link Notification.BubbleMetadata#FLAG_SUPPRESS_NOTIFICATION} - * or {@link Notification.BubbleMetadata#FLAG_SUPPRESS_BUBBLE} changes. + * Called when the flags on {@link Notification.BubbleMetadata} are changed. */ - void onBubbleNotificationSuppressionChanged(String key, boolean isNotifSuppressed, - boolean isBubbleSuppressed); + void onBubbleMetadataFlagChanged(String key, int flags); /** * Grant permission to read the specified URI to the package associated with the diff --git a/services/core/java/com/android/server/notification/NotificationManagerInternal.java b/services/core/java/com/android/server/notification/NotificationManagerInternal.java index c548e7edc3cf..8a627367c1dc 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerInternal.java +++ b/services/core/java/com/android/server/notification/NotificationManagerInternal.java @@ -42,4 +42,7 @@ public interface NotificationManagerInternal { /** Does the specified package/uid have permission to post notifications? */ boolean areNotificationsEnabledForPackage(String pkg, int uid); + + /** Send a notification to the user prompting them to review their notification permissions. */ + void sendReviewPermissionsNotification(); } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 6078bfc95488..83c576e9259d 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -18,6 +18,7 @@ package com.android.server.notification; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; import static android.app.AppOpsManager.MODE_ALLOWED; +import static android.app.Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; import static android.app.Notification.FLAG_AUTOGROUP_SUMMARY; import static android.app.Notification.FLAG_BUBBLE; import static android.app.Notification.FLAG_FOREGROUND_SERVICE; @@ -274,6 +275,7 @@ import com.android.internal.logging.InstanceIdSequence; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.internal.messages.nano.SystemMessageProto; import com.android.internal.notification.SystemNotificationChannels; import com.android.internal.os.BackgroundThread; import com.android.internal.os.SomeArgs; @@ -442,6 +444,18 @@ public class NotificationManagerService extends SystemService { private static final int NOTIFICATION_INSTANCE_ID_MAX = (1 << 13); + // States for the review permissions notification + static final int REVIEW_NOTIF_STATE_UNKNOWN = -1; + static final int REVIEW_NOTIF_STATE_SHOULD_SHOW = 0; + static final int REVIEW_NOTIF_STATE_USER_INTERACTED = 1; + static final int REVIEW_NOTIF_STATE_DISMISSED = 2; + static final int REVIEW_NOTIF_STATE_RESHOWN = 3; + + // Action strings for review permissions notification + static final String REVIEW_NOTIF_ACTION_REMIND = "REVIEW_NOTIF_ACTION_REMIND"; + static final String REVIEW_NOTIF_ACTION_DISMISS = "REVIEW_NOTIF_ACTION_DISMISS"; + static final String REVIEW_NOTIF_ACTION_CANCELED = "REVIEW_NOTIF_ACTION_CANCELED"; + /** * Apps that post custom toasts in the background will have those blocked. Apps can * still post toasts created with @@ -652,6 +666,9 @@ public class NotificationManagerService extends SystemService { private InstanceIdSequence mNotificationInstanceIdSequence; private Set<String> mMsgPkgsAllowedAsConvos = new HashSet(); + // Broadcast intent receiver for notification permissions review-related intents + private ReviewNotificationPermissionsReceiver mReviewNotificationPermissionsReceiver; + static class Archive { final SparseArray<Boolean> mEnabled; final int mBufferSize; @@ -1413,8 +1430,7 @@ public class NotificationManagerService extends SystemService { } @Override - public void onBubbleNotificationSuppressionChanged(String key, boolean isNotifSuppressed, - boolean isBubbleSuppressed) { + public void onBubbleMetadataFlagChanged(String key, int flags) { synchronized (mNotificationLock) { NotificationRecord r = mNotificationsByKey.get(key); if (r != null) { @@ -1424,17 +1440,12 @@ public class NotificationManagerService extends SystemService { return; } - boolean flagChanged = false; - if (data.isNotificationSuppressed() != isNotifSuppressed) { - flagChanged = true; - data.setSuppressNotification(isNotifSuppressed); - } - if (data.isBubbleSuppressed() != isBubbleSuppressed) { - flagChanged = true; - data.setSuppressBubble(isBubbleSuppressed); - } - if (flagChanged) { + if (flags != data.getFlags()) { + data.setFlags(flags); + // Shouldn't alert again just because of a flag change. r.getNotification().flags |= FLAG_ONLY_ALERT_ONCE; + // Force isAppForeground true here, because for sysui's purposes we + // want to be able to adjust the flag behaviour. mHandler.post( new EnqueueNotificationRunnable(r.getUser().getIdentifier(), r, true /* isAppForeground */, SystemClock.elapsedRealtime())); @@ -2416,6 +2427,11 @@ public class NotificationManagerService extends SystemService { IntentFilter localeChangedFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED); getContext().registerReceiver(mLocaleChangeReceiver, localeChangedFilter); + + mReviewNotificationPermissionsReceiver = new ReviewNotificationPermissionsReceiver(); + getContext().registerReceiver(mReviewNotificationPermissionsReceiver, + ReviewNotificationPermissionsReceiver.getFilter(), + Context.RECEIVER_NOT_EXPORTED); } /** @@ -2709,6 +2725,7 @@ public class NotificationManagerService extends SystemService { mHistoryManager.onBootPhaseAppsCanStart(); registerDeviceConfigChange(); migrateDefaultNAS(); + maybeShowInitialReviewPermissionsNotification(); } else if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) { mSnoozeHelper.scheduleRepostsForPersistedNotifications(System.currentTimeMillis()); } @@ -6336,6 +6353,21 @@ public class NotificationManagerService extends SystemService { public boolean areNotificationsEnabledForPackage(String pkg, int uid) { return areNotificationsEnabledForPackageInt(pkg, uid); } + + @Override + public void sendReviewPermissionsNotification() { + // This method is meant to be called from the JobService upon running the job for this + // notification having been rescheduled; so without checking any other state, it will + // send the notification. + checkCallerIsSystem(); + NotificationManager nm = getContext().getSystemService(NotificationManager.class); + nm.notify(TAG, + SystemMessageProto.SystemMessage.NOTE_REVIEW_NOTIFICATION_PERMISSIONS, + createReviewPermissionsNotification()); + Settings.Global.putInt(getContext().getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_RESHOWN); + } }; int getNumNotificationChannelsForPackage(String pkg, int uid, boolean includeDeleted) { @@ -7145,10 +7177,12 @@ public class NotificationManagerService extends SystemService { && r.getNotification().isBubbleNotification()) || (mReason == REASON_CLICK && r.canBubble() && r.isFlagBubbleRemoved())) { - boolean isBubbleSuppressed = r.getNotification().getBubbleMetadata() != null - && r.getNotification().getBubbleMetadata().isBubbleSuppressed(); - mNotificationDelegate.onBubbleNotificationSuppressionChanged( - r.getKey(), true /* notifSuppressed */, isBubbleSuppressed); + int flags = 0; + if (r.getNotification().getBubbleMetadata() != null) { + flags = r.getNotification().getBubbleMetadata().getFlags(); + } + flags |= FLAG_SUPPRESS_NOTIFICATION; + mNotificationDelegate.onBubbleMetadataFlagChanged(r.getKey(), flags); return; } if ((r.getNotification().flags & mMustHaveFlags) != mMustHaveFlags) { @@ -11608,6 +11642,76 @@ public class NotificationManagerService extends SystemService { out.endTag(null, LOCKSCREEN_ALLOW_SECURE_NOTIFICATIONS_TAG); } + // Creates a notification that informs the user about changes due to the migration to + // use permissions for notifications. + protected Notification createReviewPermissionsNotification() { + int title = R.string.review_notification_settings_title; + int content = R.string.review_notification_settings_text; + + // Tapping on the notification leads to the settings screen for managing app notifications, + // using the intent reserved for system services to indicate it comes from this notification + Intent tapIntent = new Intent(Settings.ACTION_ALL_APPS_NOTIFICATION_SETTINGS_FOR_REVIEW); + Intent remindIntent = new Intent(REVIEW_NOTIF_ACTION_REMIND); + Intent dismissIntent = new Intent(REVIEW_NOTIF_ACTION_DISMISS); + Intent swipeIntent = new Intent(REVIEW_NOTIF_ACTION_CANCELED); + + // Both "remind me" and "dismiss" actions will be actions received by the BroadcastReceiver + final Notification.Action remindMe = new Notification.Action.Builder(null, + getContext().getResources().getString( + R.string.review_notification_settings_remind_me_action), + PendingIntent.getBroadcast( + getContext(), 0, remindIntent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)) + .build(); + final Notification.Action dismiss = new Notification.Action.Builder(null, + getContext().getResources().getString( + R.string.review_notification_settings_dismiss), + PendingIntent.getBroadcast( + getContext(), 0, dismissIntent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)) + .build(); + + return new Notification.Builder(getContext(), SystemNotificationChannels.SYSTEM_CHANGES) + .setSmallIcon(R.drawable.stat_sys_adb) + .setContentTitle(getContext().getResources().getString(title)) + .setContentText(getContext().getResources().getString(content)) + .setContentIntent(PendingIntent.getActivity(getContext(), 0, tapIntent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)) + .setStyle(new Notification.BigTextStyle()) + .setFlag(Notification.FLAG_NO_CLEAR, true) + .setAutoCancel(true) + .addAction(remindMe) + .addAction(dismiss) + .setDeleteIntent(PendingIntent.getBroadcast(getContext(), 0, swipeIntent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)) + .build(); + } + + protected void maybeShowInitialReviewPermissionsNotification() { + int currentState = Settings.Global.getInt(getContext().getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + REVIEW_NOTIF_STATE_UNKNOWN); + + // now check the last known state of the notification -- this determination of whether the + // user is in the correct target audience occurs elsewhere, and will have written the + // REVIEW_NOTIF_STATE_SHOULD_SHOW to indicate it should be shown in the future. + // + // alternatively, if the user has rescheduled the notification (so it has been shown + // again) but not yet interacted with the new notification, then show it again on boot, + // as this state indicates that the user had the notification open before rebooting. + // + // sending the notification here does not record a new state for the notification; + // that will be written by parts of the system further down the line if at any point + // the user interacts with the notification. + if (currentState == REVIEW_NOTIF_STATE_SHOULD_SHOW + || currentState == REVIEW_NOTIF_STATE_RESHOWN) { + NotificationManager nm = getContext().getSystemService(NotificationManager.class); + nm.notify(TAG, + SystemMessageProto.SystemMessage.NOTE_REVIEW_NOTIFICATION_PERMISSIONS, + createReviewPermissionsNotification()); + } + } + /** * Shows a warning on logcat. Shows the toast only once per package. This is to avoid being too * aggressive and annoying the user. diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index 0525b1e33267..ef3c770f125b 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -96,6 +96,10 @@ public class PreferencesHelper implements RankingConfig { private final int XML_VERSION; /** What version to check to do the upgrade for bubbles. */ private static final int XML_VERSION_BUBBLES_UPGRADE = 1; + /** The first xml version with notification permissions enabled. */ + private static final int XML_VERSION_NOTIF_PERMISSION = 3; + /** The first xml version that notifies users to review their notification permissions */ + private static final int XML_VERSION_REVIEW_PERMISSIONS_NOTIFICATION = 4; @VisibleForTesting static final int UNKNOWN_UID = UserHandle.USER_NULL; private static final String NON_BLOCKABLE_CHANNEL_DELIM = ":"; @@ -206,7 +210,7 @@ public class PreferencesHelper implements RankingConfig { mStatsEventBuilderFactory = statsEventBuilderFactory; if (mPermissionHelper.isMigrationEnabled()) { - XML_VERSION = 3; + XML_VERSION = 4; } else { XML_VERSION = 2; } @@ -226,8 +230,16 @@ public class PreferencesHelper implements RankingConfig { final int xmlVersion = parser.getAttributeInt(null, ATT_VERSION, -1); boolean upgradeForBubbles = xmlVersion == XML_VERSION_BUBBLES_UPGRADE; - boolean migrateToPermission = - (xmlVersion < XML_VERSION) && mPermissionHelper.isMigrationEnabled(); + boolean migrateToPermission = (xmlVersion < XML_VERSION_NOTIF_PERMISSION) + && mPermissionHelper.isMigrationEnabled(); + if (xmlVersion < XML_VERSION_REVIEW_PERMISSIONS_NOTIFICATION) { + // make a note that we should show the notification at some point. + // it shouldn't be possible for the user to already have seen it, as the XML version + // would be newer then. + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW); + } ArrayList<PermissionHelper.PackagePermission> pkgPerms = new ArrayList<>(); synchronized (mPackagePreferences) { while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { diff --git a/services/core/java/com/android/server/notification/ReviewNotificationPermissionsJobService.java b/services/core/java/com/android/server/notification/ReviewNotificationPermissionsJobService.java new file mode 100644 index 000000000000..fde45f71a844 --- /dev/null +++ b/services/core/java/com/android/server/notification/ReviewNotificationPermissionsJobService.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.notification; + +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.app.job.JobService; +import android.content.ComponentName; +import android.content.Context; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.LocalServices; + +/** + * JobService implementation for scheduling the notification informing users about + * notification permissions updates and taking them to review their existing permissions. + * @hide + */ +public class ReviewNotificationPermissionsJobService extends JobService { + public static final String TAG = "ReviewNotificationPermissionsJobService"; + + @VisibleForTesting + protected static final int JOB_ID = 225373531; + + /** + * Schedule a new job that will show a notification the specified amount of time in the future. + */ + public static void scheduleJob(Context context, long rescheduleTimeMillis) { + JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); + // if the job already exists for some reason, cancel & reschedule + if (jobScheduler.getPendingJob(JOB_ID) != null) { + jobScheduler.cancel(JOB_ID); + } + ComponentName component = new ComponentName( + context, ReviewNotificationPermissionsJobService.class); + JobInfo newJob = new JobInfo.Builder(JOB_ID, component) + .setPersisted(true) // make sure it'll still get rescheduled after reboot + .setMinimumLatency(rescheduleTimeMillis) // run after specified amount of time + .build(); + jobScheduler.schedule(newJob); + } + + @Override + public boolean onStartJob(JobParameters params) { + // While jobs typically should be run on different threads, this + // job only posts a notification, which is not a long-running operation + // as notification posting is asynchronous. + NotificationManagerInternal nmi = + LocalServices.getService(NotificationManagerInternal.class); + nmi.sendReviewPermissionsNotification(); + + // once the notification is posted, the job is done, so no need to + // keep it alive afterwards + return false; + } + + @Override + public boolean onStopJob(JobParameters params) { + // If we're interrupted for some reason, try again (though this may not + // ever happen due to onStartJob not leaving a job running after being + // called) + return true; + } +} diff --git a/services/core/java/com/android/server/notification/ReviewNotificationPermissionsReceiver.java b/services/core/java/com/android/server/notification/ReviewNotificationPermissionsReceiver.java new file mode 100644 index 000000000000..b99aeac44025 --- /dev/null +++ b/services/core/java/com/android/server/notification/ReviewNotificationPermissionsReceiver.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.notification; + +import android.app.NotificationManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.provider.Settings; +import android.util.Log; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.messages.nano.SystemMessageProto; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; + +/** + * Broadcast receiver for intents that come from the "review notification permissions" notification, + * shown to users who upgrade to T from an earlier OS to inform them of notification setup changes + * and invite them to review their notification permissions. + */ +public class ReviewNotificationPermissionsReceiver extends BroadcastReceiver { + public static final String TAG = "ReviewNotifPermissions"; + static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + // 7 days in millis, as the amount of time to wait before re-sending the notification + private static final long JOB_RESCHEDULE_TIME = 1000 /* millis */ * 60 /* seconds */ + * 60 /* minutes */ * 24 /* hours */ * 7 /* days */; + + static IntentFilter getFilter() { + IntentFilter filter = new IntentFilter(); + filter.addAction(NotificationManagerService.REVIEW_NOTIF_ACTION_REMIND); + filter.addAction(NotificationManagerService.REVIEW_NOTIF_ACTION_DISMISS); + filter.addAction(NotificationManagerService.REVIEW_NOTIF_ACTION_CANCELED); + return filter; + } + + // Cancels the "review notification permissions" notification. + @VisibleForTesting + protected void cancelNotification(Context context) { + NotificationManager nm = context.getSystemService(NotificationManager.class); + if (nm != null) { + nm.cancel(NotificationManagerService.TAG, + SystemMessageProto.SystemMessage.NOTE_REVIEW_NOTIFICATION_PERMISSIONS); + } else { + Slog.w(TAG, "could not cancel notification: NotificationManager not found"); + } + } + + @VisibleForTesting + protected void rescheduleNotification(Context context) { + ReviewNotificationPermissionsJobService.scheduleJob(context, JOB_RESCHEDULE_TIME); + // log if needed + if (DEBUG) { + Slog.d(TAG, "Scheduled review permissions notification for on or after: " + + LocalDateTime.now(ZoneId.systemDefault()) + .plus(JOB_RESCHEDULE_TIME, ChronoUnit.MILLIS)); + } + } + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(NotificationManagerService.REVIEW_NOTIF_ACTION_REMIND)) { + // Reschedule the notification for 7 days in the future + rescheduleNotification(context); + + // note that the user has interacted; no longer needed to show the initial + // notification + Settings.Global.putInt(context.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED); + cancelNotification(context); + } else if (action.equals(NotificationManagerService.REVIEW_NOTIF_ACTION_DISMISS)) { + // user dismissed; write to settings so we don't show ever again + Settings.Global.putInt(context.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_DISMISSED); + cancelNotification(context); + } else if (action.equals(NotificationManagerService.REVIEW_NOTIF_ACTION_CANCELED)) { + // we may get here from the user swiping away the notification, + // or from the notification being canceled in any other way. + // only in the case that the user hasn't interacted with it in + // any other way yet, reschedule + int notifState = Settings.Global.getInt(context.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + /* default */ NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN); + if (notifState == NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW) { + // user hasn't interacted in the past, so reschedule once and then note that the + // user *has* interacted now so we don't re-reschedule if they swipe again + rescheduleNotification(context); + Settings.Global.putInt(context.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED); + } else if (notifState == NotificationManagerService.REVIEW_NOTIF_STATE_RESHOWN) { + // swiping away on a rescheduled notification; mark as interacted and + // don't reschedule again. + Settings.Global.putInt(context.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED); + } + } + } +} diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 0dabff8370ba..bcdf4291ed41 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -1430,6 +1430,8 @@ public class UserManagerService extends IUserManager.Stub { /** * Returns a UserInfo object with the name filled in, for Owner and Guest, or the original * if the name is already set. + * + * Note: Currently, the resulting name can be null if a user was truly created with a null name. */ private UserInfo userWithName(UserInfo orig) { if (orig != null && orig.name == null) { @@ -1638,7 +1640,7 @@ public class UserManagerService extends IUserManager.Stub { } @Override - public String getUserName() { + public @NonNull String getUserName() { final int callingUid = Binder.getCallingUid(); if (!hasQueryOrCreateUsersPermission() && !hasPermissionGranted( @@ -1649,7 +1651,10 @@ public class UserManagerService extends IUserManager.Stub { final int userId = UserHandle.getUserId(callingUid); synchronized (mUsersLock) { UserInfo userInfo = userWithName(getUserInfoLU(userId)); - return userInfo == null ? "" : userInfo.name; + if (userInfo != null && userInfo.name != null) { + return userInfo.name; + } + return ""; } } @@ -4165,7 +4170,7 @@ public class UserManagerService extends IUserManager.Stub { * @return the converted user, or {@code null} if no pre-created user could be converted. */ private @Nullable UserInfo convertPreCreatedUserIfPossible(String userType, - @UserInfoFlag int flags, String name, @Nullable Object token) { + @UserInfoFlag int flags, @Nullable String name, @Nullable Object token) { final UserData preCreatedUserData; synchronized (mUsersLock) { preCreatedUserData = getPreCreatedUserLU(userType); diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index b6855726c122..d48f26332017 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -1650,13 +1650,11 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } @Override - public void onBubbleNotificationSuppressionChanged(String key, boolean isNotifSuppressed, - boolean isBubbleSuppressed) { + public void onBubbleMetadataFlagChanged(String key, int flags) { enforceStatusBarService(); final long identity = Binder.clearCallingIdentity(); try { - mNotificationDelegate.onBubbleNotificationSuppressionChanged(key, isNotifSuppressed, - isBubbleSuppressed); + mNotificationDelegate.onBubbleMetadataFlagChanged(key, flags); } finally { Binder.restoreCallingIdentity(identity); } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 348e015500fe..c0cd7a755e25 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -198,6 +198,7 @@ import com.android.internal.app.IAppOpsService; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.logging.InstanceIdSequence; import com.android.internal.logging.InstanceIdSequenceFake; +import com.android.internal.messages.nano.SystemMessageProto; import com.android.internal.statusbar.NotificationVisibility; import com.android.server.DeviceIdleInternal; import com.android.server.LocalServices; @@ -303,6 +304,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { ActivityManagerInternal mAmi; @Mock private Looper mMainLooper; + @Mock + private NotificationManager mMockNm; @Mock IIntentSender pi1; @@ -405,6 +408,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { LocalServices.removeServiceForTest(PermissionPolicyInternal.class); LocalServices.addService(PermissionPolicyInternal.class, mPermissionPolicyInternal); mContext.addMockSystemService(Context.ALARM_SERVICE, mAlarmManager); + mContext.addMockSystemService(NotificationManager.class, mMockNm); doNothing().when(mContext).sendBroadcastAsUser(any(), any(), any()); @@ -7516,46 +7520,53 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - public void testOnBubbleNotificationSuppressionChanged() throws Exception { + public void testOnBubbleMetadataFlagChanged() throws Exception { setUpPrefsForBubbles(PKG, mUid, true /* global */, BUBBLE_PREFERENCE_ALL /* app */, true /* channel */); - // Bubble notification + // Post a bubble notification NotificationRecord nr = generateMessageBubbleNotifRecord(mTestNotificationChannel, "tag"); - + // Set this so that the bubble can be suppressed + nr.getNotification().getBubbleMetadata().setFlags( + Notification.BubbleMetadata.FLAG_SUPPRESSABLE_BUBBLE); mBinderService.enqueueNotificationWithTag(PKG, PKG, nr.getSbn().getTag(), nr.getSbn().getId(), nr.getSbn().getNotification(), nr.getSbn().getUserId()); waitForIdle(); - // NOT suppressed + // Check the flags Notification n = mBinderService.getActiveNotifications(PKG)[0].getNotification(); assertFalse(n.getBubbleMetadata().isNotificationSuppressed()); + assertFalse(n.getBubbleMetadata().getAutoExpandBubble()); + assertFalse(n.getBubbleMetadata().isBubbleSuppressed()); + assertTrue(n.getBubbleMetadata().isBubbleSuppressable()); // Reset as this is called when the notif is first sent reset(mListeners); - // Test: update suppression to true - mService.mNotificationDelegate.onBubbleNotificationSuppressionChanged(nr.getKey(), true, - false); + // Test: change the flags + int flags = Notification.BubbleMetadata.FLAG_SUPPRESSABLE_BUBBLE; + flags |= Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE; + flags |= Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; + flags |= Notification.BubbleMetadata.FLAG_SUPPRESS_BUBBLE; + mService.mNotificationDelegate.onBubbleMetadataFlagChanged(nr.getKey(), flags); waitForIdle(); // Check n = mBinderService.getActiveNotifications(PKG)[0].getNotification(); - assertTrue(n.getBubbleMetadata().isNotificationSuppressed()); + assertEquals(flags, n.getBubbleMetadata().getFlags()); // Reset to check again reset(mListeners); - // Test: update suppression to false - mService.mNotificationDelegate.onBubbleNotificationSuppressionChanged(nr.getKey(), false, - false); + // Test: clear flags + mService.mNotificationDelegate.onBubbleMetadataFlagChanged(nr.getKey(), 0); waitForIdle(); // Check n = mBinderService.getActiveNotifications(PKG)[0].getNotification(); - assertFalse(n.getBubbleMetadata().isNotificationSuppressed()); + assertEquals(0, n.getBubbleMetadata().getFlags()); } @Test @@ -9294,4 +9305,77 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // the notifyPostedLocked function is called twice. verify(mListeners, times(2)).notifyPostedLocked(any(), any()); } + + @Test + public void testMaybeShowReviewPermissionsNotification_unknown() { + // Set up various possible states of the settings int and confirm whether or not the + // notification is shown as expected + + // Initial state: default/unknown setting, make sure nothing happens + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN); + mService.maybeShowInitialReviewPermissionsNotification(); + verify(mMockNm, never()).notify(anyString(), anyInt(), any(Notification.class)); + } + + @Test + public void testMaybeShowReviewPermissionsNotification_shouldShow() { + // If state is SHOULD_SHOW, it ... should show + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW); + mService.maybeShowInitialReviewPermissionsNotification(); + verify(mMockNm, times(1)).notify(eq(NotificationManagerService.TAG), + eq(SystemMessageProto.SystemMessage.NOTE_REVIEW_NOTIFICATION_PERMISSIONS), + any(Notification.class)); + } + + @Test + public void testMaybeShowReviewPermissionsNotification_alreadyShown() { + // If state is either USER_INTERACTED or DISMISSED, we should not show this on boot + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED); + mService.maybeShowInitialReviewPermissionsNotification(); + + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_DISMISSED); + mService.maybeShowInitialReviewPermissionsNotification(); + + verify(mMockNm, never()).notify(anyString(), anyInt(), any(Notification.class)); + } + + @Test + public void testMaybeShowReviewPermissionsNotification_reshown() { + // If we have re-shown the notification and the user did not subsequently interacted with + // it, then make sure we show when trying on boot + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_RESHOWN); + mService.maybeShowInitialReviewPermissionsNotification(); + verify(mMockNm, times(1)).notify(eq(NotificationManagerService.TAG), + eq(SystemMessageProto.SystemMessage.NOTE_REVIEW_NOTIFICATION_PERMISSIONS), + any(Notification.class)); + } + + @Test + public void testRescheduledReviewPermissionsNotification() { + // when rescheduled, the notification goes through the NotificationManagerInternal service + // this call doesn't need to know anything about previously scheduled state -- if called, + // it should send the notification & write the appropriate int to Settings + mInternalService.sendReviewPermissionsNotification(); + + // Notification should be sent + verify(mMockNm, times(1)).notify(eq(NotificationManagerService.TAG), + eq(SystemMessageProto.SystemMessage.NOTE_REVIEW_NOTIFICATION_PERMISSIONS), + any(Notification.class)); + + // write STATE_RESHOWN to settings + assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_RESHOWN, + Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN)); + } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index 63d7453450d2..6d0895935877 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -289,6 +289,11 @@ public class PreferencesHelperTest extends UiServiceTestCase { .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED) .build(); + + // make sure that the settings for review notification permissions are unset to begin with + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN); } private ByteArrayOutputStream writeXmlAndPurge( @@ -656,6 +661,13 @@ public class PreferencesHelperTest extends UiServiceTestCase { verify(mPermissionHelper).setNotificationPermission(nMr1Expected); verify(mPermissionHelper).setNotificationPermission(oExpected); verify(mPermissionHelper).setNotificationPermission(pExpected); + + // verify that we also write a state for review_permissions_notification to eventually + // show a notification + assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW, + Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN)); } @Test @@ -738,7 +750,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test - public void testReadXml_newXml_noMigration() throws Exception { + public void testReadXml_newXml_noMigration_showPermissionNotification() throws Exception { when(mPermissionHelper.isMigrationEnabled()).thenReturn(true); mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mLogger, mAppOpsManager, mStatsEventBuilderFactory); @@ -786,6 +798,70 @@ public class PreferencesHelperTest extends UiServiceTestCase { compareChannels(idp, mHelper.getNotificationChannel(PKG_P, UID_P, idp.getId(), false)); verify(mPermissionHelper, never()).setNotificationPermission(any()); + + // verify that we do, however, write a state for review_permissions_notification to + // eventually show a notification, since this XML version is older than the notification + assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW, + Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN)); + } + + @Test + public void testReadXml_newXml_noMigration_noPermissionNotification() throws Exception { + when(mPermissionHelper.isMigrationEnabled()).thenReturn(true); + mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mPermissionHelper, mLogger, mAppOpsManager, mStatsEventBuilderFactory); + + String xml = "<ranking version=\"4\">\n" + + "<package name=\"" + PKG_N_MR1 + "\" show_badge=\"true\">\n" + + "<channel id=\"idn\" name=\"name\" importance=\"2\"/>\n" + + "<channel id=\"miscellaneous\" name=\"Uncategorized\" />\n" + + "</package>\n" + + "<package name=\"" + PKG_O + "\" >\n" + + "<channel id=\"ido\" name=\"name2\" importance=\"2\" show_badge=\"true\"/>\n" + + "</package>\n" + + "<package name=\"" + PKG_P + "\" >\n" + + "<channel id=\"idp\" name=\"name3\" importance=\"4\" locked=\"2\" />\n" + + "</package>\n" + + "</ranking>\n"; + NotificationChannel idn = new NotificationChannel("idn", "name", IMPORTANCE_LOW); + idn.setSound(null, new AudioAttributes.Builder() + .setUsage(USAGE_NOTIFICATION) + .setContentType(CONTENT_TYPE_SONIFICATION) + .setFlags(0) + .build()); + idn.setShowBadge(false); + NotificationChannel ido = new NotificationChannel("ido", "name2", IMPORTANCE_LOW); + ido.setShowBadge(true); + ido.setSound(null, new AudioAttributes.Builder() + .setUsage(USAGE_NOTIFICATION) + .setContentType(CONTENT_TYPE_SONIFICATION) + .setFlags(0) + .build()); + NotificationChannel idp = new NotificationChannel("idp", "name3", IMPORTANCE_HIGH); + idp.lockFields(2); + idp.setSound(null, new AudioAttributes.Builder() + .setUsage(USAGE_NOTIFICATION) + .setContentType(CONTENT_TYPE_SONIFICATION) + .setFlags(0) + .build()); + + loadByteArrayXml(xml.getBytes(), true, USER_SYSTEM); + + assertTrue(mHelper.canShowBadge(PKG_N_MR1, UID_N_MR1)); + + assertEquals(idn, mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, idn.getId(), false)); + compareChannels(ido, mHelper.getNotificationChannel(PKG_O, UID_O, ido.getId(), false)); + compareChannels(idp, mHelper.getNotificationChannel(PKG_P, UID_P, idp.getId(), false)); + + verify(mPermissionHelper, never()).setNotificationPermission(any()); + + // this XML is new enough, we should not be attempting to show a notification or anything + assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN, + Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN)); } @Test @@ -903,7 +979,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { ByteArrayOutputStream baos = writeXmlAndPurge( PKG_N_MR1, UID_N_MR1, false, USER_SYSTEM); - String expected = "<ranking version=\"3\">\n" + String expected = "<ranking version=\"4\">\n" + "<package name=\"com.example.o\" show_badge=\"true\" " + "app_user_locked_fields=\"0\" sent_invalid_msg=\"false\" " + "sent_valid_msg=\"false\" user_demote_msg_app=\"false\" uid=\"1111\">\n" @@ -984,7 +1060,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { ByteArrayOutputStream baos = writeXmlAndPurge( PKG_N_MR1, UID_N_MR1, true, USER_SYSTEM); - String expected = "<ranking version=\"3\">\n" + String expected = "<ranking version=\"4\">\n" // Importance 0 because off in permissionhelper + "<package name=\"com.example.o\" importance=\"0\" show_badge=\"true\" " + "app_user_locked_fields=\"0\" sent_invalid_msg=\"false\" " @@ -1067,7 +1143,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { ByteArrayOutputStream baos = writeXmlAndPurge( PKG_N_MR1, UID_N_MR1, true, USER_SYSTEM); - String expected = "<ranking version=\"3\">\n" + String expected = "<ranking version=\"4\">\n" // Importance 0 because off in permissionhelper + "<package name=\"com.example.o\" importance=\"0\" show_badge=\"true\" " + "app_user_locked_fields=\"0\" sent_invalid_msg=\"false\" " @@ -1121,7 +1197,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { ByteArrayOutputStream baos = writeXmlAndPurge( PKG_N_MR1, UID_N_MR1, true, USER_SYSTEM); - String expected = "<ranking version=\"3\">\n" + String expected = "<ranking version=\"4\">\n" // Packages that exist solely in permissionhelper + "<package name=\"" + PKG_P + "\" importance=\"3\" />\n" + "<package name=\"" + PKG_O + "\" importance=\"0\" />\n" diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ReviewNotificationPermissionsJobServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ReviewNotificationPermissionsJobServiceTest.java new file mode 100644 index 000000000000..5a4ce5da676e --- /dev/null +++ b/services/tests/uiservicestests/src/com/android/server/notification/ReviewNotificationPermissionsJobServiceTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.notification; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.testing.AndroidTestingRunner; + +import androidx.test.rule.ServiceTestRule; + +import com.android.server.LocalServices; +import com.android.server.UiServiceTestCase; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; + +@RunWith(AndroidTestingRunner.class) +public class ReviewNotificationPermissionsJobServiceTest extends UiServiceTestCase { + private ReviewNotificationPermissionsJobService mJobService; + private JobParameters mJobParams = new JobParameters(null, + ReviewNotificationPermissionsJobService.JOB_ID, null, null, null, + 0, false, false, null, null, null); + + @Captor + ArgumentCaptor<JobInfo> mJobInfoCaptor; + + @Mock + private JobScheduler mMockJobScheduler; + + @Mock + private NotificationManagerInternal mMockNotificationManagerInternal; + + @Rule + public final ServiceTestRule mServiceRule = new ServiceTestRule(); + + @Before + public void setUp() throws Exception { + mJobService = new ReviewNotificationPermissionsJobService(); + mContext.addMockSystemService(JobScheduler.class, mMockJobScheduler); + + // add NotificationManagerInternal to LocalServices + LocalServices.removeServiceForTest(NotificationManagerInternal.class); + LocalServices.addService(NotificationManagerInternal.class, + mMockNotificationManagerInternal); + } + + @Test + public void testScheduleJob() { + // if asked, the job doesn't currently exist yet + when(mMockJobScheduler.getPendingJob(anyInt())).thenReturn(null); + + final int rescheduleTimeMillis = 350; // arbitrary number + + // attempt to schedule the job + ReviewNotificationPermissionsJobService.scheduleJob(mContext, rescheduleTimeMillis); + verify(mMockJobScheduler, times(1)).schedule(mJobInfoCaptor.capture()); + + // verify various properties of the job that is passed in to the job scheduler + JobInfo jobInfo = mJobInfoCaptor.getValue(); + assertEquals(ReviewNotificationPermissionsJobService.JOB_ID, jobInfo.getId()); + assertEquals(rescheduleTimeMillis, jobInfo.getMinLatencyMillis()); + assertTrue(jobInfo.isPersisted()); // should continue after reboot + assertFalse(jobInfo.isPeriodic()); // one time + } + + @Test + public void testOnStartJob() { + // the job need not be persisted after it does its work, so it'll return + // false + assertFalse(mJobService.onStartJob(mJobParams)); + + // verify that starting the job causes the notification to be sent + verify(mMockNotificationManagerInternal).sendReviewPermissionsNotification(); + } +} diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ReviewNotificationPermissionsReceiverTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ReviewNotificationPermissionsReceiverTest.java new file mode 100644 index 000000000000..12281a742a50 --- /dev/null +++ b/services/tests/uiservicestests/src/com/android/server/notification/ReviewNotificationPermissionsReceiverTest.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.notification; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +import android.content.Context; +import android.content.Intent; +import android.provider.Settings; +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.server.UiServiceTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidTestingRunner.class) +@SmallTest +public class ReviewNotificationPermissionsReceiverTest extends UiServiceTestCase { + + // Simple mock class that just overrides the reschedule and cancel behavior so that it's easy + // to tell whether the receiver has sent requests to either reschedule or cancel the + // notification (or both). + private class MockReviewNotificationPermissionsReceiver + extends ReviewNotificationPermissionsReceiver { + boolean mCanceled = false; + boolean mRescheduled = false; + + @Override + protected void cancelNotification(Context context) { + mCanceled = true; + } + + @Override + protected void rescheduleNotification(Context context) { + mRescheduled = true; + } + } + + private MockReviewNotificationPermissionsReceiver mReceiver; + private Intent mIntent; + + @Before + public void setUp() { + mReceiver = new MockReviewNotificationPermissionsReceiver(); + mIntent = new Intent(); // actions will be set in test cases + } + + @Test + public void testReceive_remindMeLater_firstTime() { + // Test what happens when we receive a "remind me later" intent coming from + // a previously-not-interacted notification + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW); + + // set up Intent action + mIntent.setAction(NotificationManagerService.REVIEW_NOTIF_ACTION_REMIND); + + // Upon receipt of the intent, the following things should happen: + // - notification rescheduled + // - notification explicitly canceled + // - settings state updated to indicate user has interacted + mReceiver.onReceive(mContext, mIntent); + assertTrue(mReceiver.mRescheduled); + assertTrue(mReceiver.mCanceled); + assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED, + Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN)); + } + + @Test + public void testReceive_remindMeLater_laterTimes() { + // Test what happens when we receive a "remind me later" intent coming from + // a previously-interacted notification that has been rescheduled + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_RESHOWN); + + // set up Intent action + mIntent.setAction(NotificationManagerService.REVIEW_NOTIF_ACTION_REMIND); + + // Upon receipt of the intent, the following things should still happen + // regardless of the fact that the user has interacted before: + // - notification rescheduled + // - notification explicitly canceled + // - settings state still indicate user has interacted + mReceiver.onReceive(mContext, mIntent); + assertTrue(mReceiver.mRescheduled); + assertTrue(mReceiver.mCanceled); + assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED, + Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN)); + } + + @Test + public void testReceive_dismiss() { + // Test that dismissing the notification does *not* reschedule the notification, + // does cancel it, and writes that it has been dismissed to settings + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW); + + // set up Intent action + mIntent.setAction(NotificationManagerService.REVIEW_NOTIF_ACTION_DISMISS); + + // send intent, watch what happens + mReceiver.onReceive(mContext, mIntent); + assertFalse(mReceiver.mRescheduled); + assertTrue(mReceiver.mCanceled); + assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_DISMISSED, + Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN)); + } + + @Test + public void testReceive_notificationCanceled_firstSwipe() { + // Test the basic swipe away case: the first time the user swipes the notification + // away, it will not have been interacted with yet, so make sure it's rescheduled + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW); + + // set up Intent action, would be called from notification's delete intent + mIntent.setAction(NotificationManagerService.REVIEW_NOTIF_ACTION_CANCELED); + + // send intent, make sure it gets: + // - rescheduled + // - not explicitly canceled, the notification was already canceled + // - noted that it's been interacted with + mReceiver.onReceive(mContext, mIntent); + assertTrue(mReceiver.mRescheduled); + assertFalse(mReceiver.mCanceled); + assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED, + Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN)); + } + + @Test + public void testReceive_notificationCanceled_secondSwipe() { + // Test the swipe away case for a rescheduled notification: in this case + // it should not be rescheduled anymore + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_RESHOWN); + + // set up Intent action, would be called from notification's delete intent + mIntent.setAction(NotificationManagerService.REVIEW_NOTIF_ACTION_CANCELED); + + // send intent, make sure it gets: + // - not rescheduled on the second+ swipe + // - not explicitly canceled, the notification was already canceled + // - mark as user interacted + mReceiver.onReceive(mContext, mIntent); + assertFalse(mReceiver.mRescheduled); + assertFalse(mReceiver.mCanceled); + assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_USER_INTERACTED, + Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN)); + } + + @Test + public void testReceive_notificationCanceled_fromDismiss() { + // Test that if the notification delete intent is called due to us canceling + // the notification from the receiver, we don't do anything extra + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_DISMISSED); + + // set up Intent action, would be called from notification's delete intent + mIntent.setAction(NotificationManagerService.REVIEW_NOTIF_ACTION_CANCELED); + + // nothing should happen, nothing at all + mReceiver.onReceive(mContext, mIntent); + assertFalse(mReceiver.mRescheduled); + assertFalse(mReceiver.mCanceled); + assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_DISMISSED, + Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, + NotificationManagerService.REVIEW_NOTIF_STATE_UNKNOWN)); + } +} |