Merge "[Notif] Add Blocking helper to swipe" into pi-dev

This commit is contained in:
TreeHugger Robot
2018-03-27 19:19:40 +00:00
committed by Android (Google) Code Review
17 changed files with 1199 additions and 338 deletions

View File

@@ -31,6 +31,7 @@ import com.android.systemui.classifier.FalsingManager;
import com.android.systemui.keyguard.DismissCallbackRegistry;
import com.android.systemui.qs.QSTileHost;
import com.android.systemui.statusbar.KeyguardIndicationController;
import com.android.systemui.statusbar.NotificationBlockingHelperManager;
import com.android.systemui.statusbar.NotificationEntryManager;
import com.android.systemui.statusbar.NotificationGutsManager;
import com.android.systemui.statusbar.NotificationListener;
@@ -130,6 +131,8 @@ public class SystemUIFactory {
providers.put(NotificationGroupManager.class, NotificationGroupManager::new);
providers.put(NotificationMediaManager.class, () -> new NotificationMediaManager(context));
providers.put(NotificationGutsManager.class, () -> new NotificationGutsManager(context));
providers.put(NotificationBlockingHelperManager.class,
() -> new NotificationBlockingHelperManager(context));
providers.put(NotificationRemoteInputManager.class,
() -> new NotificationRemoteInputManager(context));
providers.put(SmartReplyConstants.class,

View File

@@ -16,6 +16,7 @@
package com.android.systemui.statusbar;
import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE;
import static com.android.systemui.statusbar.notification.ActivityLaunchAnimator.ExpandAnimationParameters;
import static com.android.systemui.statusbar.notification.NotificationInflater.InflationCallback;
@@ -34,7 +35,6 @@ import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.util.ArraySet;
import android.util.AttributeSet;
@@ -127,6 +127,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
private boolean mHasUserChangedExpansion;
/** If {@link #mHasUserChangedExpansion}, has the user expanded this row */
private boolean mUserExpanded;
/** Whether the blocking helper is showing on this notification (even if dismissed) */
private boolean mIsBlockingHelperShowing;
/**
* Has this notification been expanded while it was pinned
@@ -400,8 +402,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
updateIconVisibilities();
updateShelfIconColor();
showBlockingHelper(mEntry.userSentiment ==
NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE);
showBlockingHelperButton(mEntry.userSentiment == USER_SENTIMENT_NEGATIVE);
updateRippleAllowed();
}
@@ -594,6 +595,13 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
return mNotificationParent != null;
}
/**
* @return whether this notification is the only child in the group summary
*/
public boolean isOnlyChildInGroup() {
return mGroupManager.isOnlyChildInGroup(getStatusBarNotification());
}
public ExpandableNotificationRow getNotificationParent() {
return mNotificationParent;
}
@@ -1150,11 +1158,31 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
return mGroupParentWhenDismissed;
}
/**
* Dismisses the notification with the option of showing the blocking helper in-place if we have
* a negative user sentiment.
*
* @param fromAccessibility whether this dismiss is coming from an accessibility action
* @return whether a blocking helper is shown in this row
*/
public boolean performDismissWithBlockingHelper(boolean fromAccessibility) {
NotificationBlockingHelperManager manager =
Dependency.get(NotificationBlockingHelperManager.class);
boolean isBlockingHelperShown = manager.perhapsShowBlockingHelper(this, mMenuRow);
// Continue with dismiss since we don't want the blocking helper to be directly associated
// with a certain notification.
performDismiss(fromAccessibility);
return isBlockingHelperShown;
}
public void performDismiss(boolean fromAccessibility) {
if (mGroupManager.isOnlyChildInGroup(getStatusBarNotification())) {
if (isOnlyChildInGroup()) {
ExpandableNotificationRow groupSummary =
mGroupManager.getLogicalGroupSummary(getStatusBarNotification());
if (groupSummary.isClearable()) {
// If this is the only child in the group, dismiss the group, but don't try to show
// the blocking helper affordance!
groupSummary.performDismiss(fromAccessibility);
}
}
@@ -1166,6 +1194,14 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
}
public void setBlockingHelperShowing(boolean isBlockingHelperShowing) {
mIsBlockingHelperShowing = isBlockingHelperShowing;
}
public boolean isBlockingHelperShowing() {
return mIsBlockingHelperShowing;
}
public void setOnDismissRunnable(Runnable onDismissRunnable) {
mOnDismissRunnable = onDismissRunnable;
}
@@ -1390,7 +1426,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
requestLayout();
}
public void showBlockingHelper(boolean show) {
public void showBlockingHelperButton(boolean show) {
mHelperButton.setVisibility(show ? View.VISIBLE : View.GONE);
}
@@ -1423,7 +1459,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
mPrivateLayout = (NotificationContentView) findViewById(R.id.expanded);
mLayouts = new NotificationContentView[] {mPrivateLayout, mPublicLayout};
final NotificationGutsManager gutsMan = Dependency.get(NotificationGutsManager.class);
mHelperButton = findViewById(R.id.helper);
mHelperButton.setOnClickListener(view -> {
doLongClickCallback();
@@ -2526,7 +2561,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
switch (action) {
case AccessibilityNodeInfo.ACTION_DISMISS:
performDismiss(true /* fromAccessibility */);
performDismissWithBlockingHelper(true /* fromAccessibility */);
return true;
case AccessibilityNodeInfo.ACTION_COLLAPSE:
case AccessibilityNodeInfo.ACTION_EXPAND:

View File

@@ -0,0 +1,137 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.systemui.statusbar;
import android.content.Context;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
import com.android.systemui.Dependency;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE;
/**
* Manager for the notification blocking helper - tracks and helps create the blocking helper
* affordance.
*/
public class NotificationBlockingHelperManager {
/** Enables debug logging and always makes the blocking helper show up after a dismiss. */
private static final boolean DEBUG = false;
private static final String TAG = "BlockingHelper";
private final Context mContext;
/** Row that the blocking helper will be shown in (via {@link NotificationGuts}. */
private ExpandableNotificationRow mBlockingHelperRow;
/**
* Whether the notification shade/stack is expanded - used to determine blocking helper
* eligibility.
*/
private boolean mIsShadeExpanded;
public NotificationBlockingHelperManager(Context context) {
mContext = context;
}
/**
* Potentially shows the blocking helper, represented via the {@link NotificationInfo} menu
* item, in the current row if user sentiment is negative.
*
* @param row row to render the blocking helper in
* @param menuRow menu used to generate the {@link NotificationInfo} view that houses the
* blocking helper UI
* @return whether we're showing a blocking helper in the given notification row
*/
boolean perhapsShowBlockingHelper(
ExpandableNotificationRow row, NotificationMenuRowPlugin menuRow) {
int numChildren = row.getNumberOfNotificationChildren();
// We only show the blocking helper if:
// - The dismissed row is a valid group (>1 or 0 children) or the only child in the group
// - The notification shade is fully expanded (guarantees we're not touching a HUN).
// - User sentiment is negative
if (DEBUG
|| row.getEntry().userSentiment == USER_SENTIMENT_NEGATIVE
&& mIsShadeExpanded
&& (!row.isChildInGroup() || row.isOnlyChildInGroup())) {
// Dismiss any current blocking helper before continuing forward (only one can be shown
// at a given time).
dismissCurrentBlockingHelper();
if (DEBUG) {
Log.d(TAG, "Manager.perhapsShowBlockingHelper: Showing new blocking helper");
}
NotificationGutsManager manager = Dependency.get(NotificationGutsManager.class);
// Enable blocking helper on the row before moving forward so everything in the guts is
// correctly prepped.
mBlockingHelperRow = row;
mBlockingHelperRow.setBlockingHelperShowing(true);
// We don't care about the touch origin (x, y) since we're opening guts without any
// explicit user interaction.
manager.openGuts(mBlockingHelperRow, 0, 0, menuRow.getLongpressMenuItem(mContext));
return true;
}
return false;
}
/**
* Dismiss the currently showing blocking helper, if any, through a notification update.
*
* @return whether the blocking helper was dismissed
*/
boolean dismissCurrentBlockingHelper() {
if (!isBlockingHelperRowNull()) {
if (DEBUG) {
Log.d(TAG, "Manager.dismissCurrentBlockingHelper: Dismissing current helper");
}
if (!mBlockingHelperRow.isBlockingHelperShowing()) {
Log.e(TAG, "Manager.dismissCurrentBlockingHelper: "
+ "Non-null row is not showing a blocking helper");
}
mBlockingHelperRow.setBlockingHelperShowing(false);
if (mBlockingHelperRow.isAttachedToWindow()) {
Dependency.get(NotificationEntryManager.class).updateNotifications();
}
mBlockingHelperRow = null;
return true;
}
return false;
}
/**
* Update the expansion status of the notification shade/stack.
*
* @param expandedHeight how much the shade is expanded ({code 0} indicating it's collapsed)
*/
public void setNotificationShadeExpanded(float expandedHeight) {
mIsShadeExpanded = expandedHeight > 0.0f;
}
@VisibleForTesting
boolean isBlockingHelperRowNull() {
return mBlockingHelperRow == null;
}
@VisibleForTesting
void setBlockingHelperRowForTest(ExpandableNotificationRow blockingHelperRowForTest) {
mBlockingHelperRow = blockingHelperRowForTest;
}
}

View File

@@ -30,6 +30,7 @@ import android.view.ViewAnimationUtils;
import android.view.accessibility.AccessibilityEvent;
import android.widget.FrameLayout;
import com.android.systemui.Dependency;
import com.android.systemui.Interpolators;
import com.android.systemui.R;
import com.android.systemui.statusbar.stack.StackStateAnimator;
@@ -189,8 +190,12 @@ public class NotificationGuts extends FrameLayout {
}
public void openControls(
int x, int y, boolean needsFalsingProtection, @Nullable Runnable onAnimationEnd) {
animateOpen(x, y, onAnimationEnd);
boolean shouldDoCircularReveal,
int x,
int y,
boolean needsFalsingProtection,
@Nullable Runnable onAnimationEnd) {
animateOpen(shouldDoCircularReveal, x, y, onAnimationEnd);
setExposed(true /* exposed */, needsFalsingProtection);
}
@@ -204,7 +209,20 @@ public class NotificationGuts extends FrameLayout {
}
}
/**
* Closes any exposed guts/views.
*
* @param x x coordinate to animate the close circular reveal with
* @param y y coordinate to animate the close circular reveal with
* @param save whether the state should be saved
* @param force whether the guts should be force-closed regardless of state.
*/
public void closeControls(int x, int y, boolean save, boolean force) {
// First try to dismiss any blocking helper.
boolean wasBlockingHelperDismissed =
Dependency.get(NotificationBlockingHelperManager.class)
.dismissCurrentBlockingHelper();
if (getWindowToken() == null) {
if (mClosedListener != null) {
mClosedListener.onGutsClosed(this);
@@ -212,8 +230,12 @@ public class NotificationGuts extends FrameLayout {
return;
}
if (mGutsContent == null || !mGutsContent.handleCloseControls(save, force)) {
animateClose(x, y);
if (mGutsContent == null
|| !mGutsContent.handleCloseControls(save, force)
|| wasBlockingHelperDismissed) {
// We only want to do a circular reveal if we're not showing the blocking helper.
animateClose(x, y, !wasBlockingHelperDismissed /* shouldDoCircularReveal */);
setExposed(false, mNeedsFalsingProtection);
if (mClosedListener != null) {
mClosedListener.onGutsClosed(this);
@@ -221,47 +243,58 @@ public class NotificationGuts extends FrameLayout {
}
}
private void animateOpen(int x, int y, @Nullable Runnable onAnimationEnd) {
final double horz = Math.max(getWidth() - x, x);
final double vert = Math.max(getHeight() - y, y);
final float r = (float) Math.hypot(horz, vert);
final Animator a
= ViewAnimationUtils.createCircularReveal(this, x, y, 0, r);
a.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
a.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
a.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
if (onAnimationEnd != null) {
onAnimationEnd.run();
}
}
});
a.start();
/** Animates in the guts view via either a fade or a circular reveal. */
private void animateOpen(
boolean shouldDoCircularReveal, int x, int y, @Nullable Runnable onAnimationEnd) {
if (shouldDoCircularReveal) {
double horz = Math.max(getWidth() - x, x);
double vert = Math.max(getHeight() - y, y);
float r = (float) Math.hypot(horz, vert);
// Circular reveal originating at (x, y)
Animator a = ViewAnimationUtils.createCircularReveal(this, x, y, 0, r);
a.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
a.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
a.addListener(new AnimateOpenListener(onAnimationEnd));
a.start();
} else {
// Fade in content
this.setAlpha(0f);
this.animate()
.alpha(1f)
.setDuration(StackStateAnimator.ANIMATION_DURATION_BLOCKING_HELPER_FADE)
.setInterpolator(Interpolators.ALPHA_IN)
.setListener(new AnimateOpenListener(onAnimationEnd))
.start();
}
}
private void animateClose(int x, int y) {
if (x == -1 || y == -1) {
x = (getLeft() + getRight()) / 2;
y = (getTop() + getHeight() / 2);
}
final double horz = Math.max(getWidth() - x, x);
final double vert = Math.max(getHeight() - y, y);
final float r = (float) Math.hypot(horz, vert);
final Animator a = ViewAnimationUtils.createCircularReveal(this,
x, y, r, 0);
a.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
a.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
a.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
setVisibility(View.GONE);
/** Animates out the guts view via either a fade or a circular reveal. */
private void animateClose(int x, int y, boolean shouldDoCircularReveal) {
if (shouldDoCircularReveal) {
// Circular reveal originating at (x, y)
if (x == -1 || y == -1) {
x = (getLeft() + getRight()) / 2;
y = (getTop() + getHeight() / 2);
}
});
a.start();
double horz = Math.max(getWidth() - x, x);
double vert = Math.max(getHeight() - y, y);
float r = (float) Math.hypot(horz, vert);
Animator a = ViewAnimationUtils.createCircularReveal(this,
x, y, r, 0);
a.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
a.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
a.addListener(new AnimateCloseListener(this /* view */));
a.start();
} else {
// Fade in the blocking helper.
this.animate()
.alpha(0f)
.setDuration(StackStateAnimator.ANIMATION_DURATION_BLOCKING_HELPER_FADE)
.setInterpolator(Interpolators.ALPHA_OUT)
.setListener(new AnimateCloseListener(this /* view */))
.start();
}
}
public void setActualHeight(int actualHeight) {
@@ -336,4 +369,36 @@ public class NotificationGuts extends FrameLayout {
public boolean isLeavebehind() {
return mGutsContent != null && mGutsContent.isLeavebehind();
}
/** Listener for animations executed in {@link #animateOpen(boolean, int, int, Runnable)}. */
private static class AnimateOpenListener extends AnimatorListenerAdapter {
final Runnable mOnAnimationEnd;
private AnimateOpenListener(Runnable onAnimationEnd) {
mOnAnimationEnd = onAnimationEnd;
}
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
if (mOnAnimationEnd != null) {
mOnAnimationEnd.run();
}
}
}
/** Listener for animations executed in {@link #animateClose(int, int, boolean)}. */
private static class AnimateCloseListener extends AnimatorListenerAdapter {
final View mView;
private AnimateCloseListener(View view) {
mView = view;
}
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mView.setVisibility(View.GONE);
}
}
}

View File

@@ -21,7 +21,6 @@ import static android.app.AppOpsManager.OP_SYSTEM_ALERT_WINDOW;
import static android.service.notification.NotificationListenerService.Ranking
.USER_SENTIMENT_NEGATIVE;
import android.app.AppOpsManager;
import android.app.INotificationManager;
import android.app.NotificationChannel;
import android.content.Context;
@@ -34,6 +33,7 @@ import android.os.ServiceManager;
import android.os.UserHandle;
import android.provider.Settings;
import android.service.notification.StatusBarNotification;
import android.support.annotation.VisibleForTesting;
import android.util.ArraySet;
import android.util.Log;
import android.view.HapticFeedbackConstants;
@@ -119,22 +119,6 @@ public class NotificationGutsManager implements Dumpable {
bindGuts(row);
}
private void saveAndCloseNotificationMenu(
ExpandableNotificationRow row, NotificationGuts guts, View done) {
guts.resetFalsingCheck();
int[] rowLocation = new int[2];
int[] doneLocation = new int[2];
row.getLocationOnScreen(rowLocation);
done.getLocationOnScreen(doneLocation);
final int centerX = done.getWidth() / 2;
final int centerY = done.getHeight() / 2;
final int x = doneLocation[0] - rowLocation[0] + centerX;
final int y = doneLocation[1] - rowLocation[1] + centerY;
closeAndSaveGuts(false /* removeLeavebehind */, false /* force */,
true /* removeControls */, x, y, true /* resetMenu */);
}
/**
* Sends an intent to open the app settings for a particular package and optional
* channel.
@@ -174,12 +158,12 @@ public class NotificationGutsManager implements Dumpable {
private void bindGuts(final ExpandableNotificationRow row,
NotificationMenuRowPlugin.MenuItem item) {
StatusBarNotification sbn = row.getStatusBarNotification();
row.inflateGuts();
row.setGutsView(item);
final StatusBarNotification sbn = row.getStatusBarNotification();
row.setTag(sbn.getPackageName());
final NotificationGuts guts = row.getGuts();
guts.setClosedListener((NotificationGuts g) -> {
row.getGuts().setClosedListener((NotificationGuts g) -> {
if (!g.willBeRemoved() && !row.isRemoved()) {
mListContainer.onHeightChanged(
row, !mPresenter.isPresenterFullyCollapsed() /* needsAnimation */);
@@ -197,87 +181,143 @@ public class NotificationGutsManager implements Dumpable {
View gutsView = item.getGutsView();
if (gutsView instanceof NotificationSnooze) {
NotificationSnooze snoozeGuts = (NotificationSnooze) gutsView;
snoozeGuts.setSnoozeListener(mListContainer.getSwipeActionHelper());
snoozeGuts.setStatusBarNotification(sbn);
snoozeGuts.setSnoozeOptions(row.getEntry().snoozeCriteria);
guts.setHeightChangedListener((NotificationGuts g) -> {
mListContainer.onHeightChanged(row, row.isShown() /* needsAnimation */);
});
initializeSnoozeView(row, (NotificationSnooze) gutsView);
} else if (gutsView instanceof AppOpsInfo) {
initializeAppOpsInfo(row, (AppOpsInfo) gutsView);
} else if (gutsView instanceof NotificationInfo) {
initializeNotificationInfo(row, (NotificationInfo) gutsView);
}
}
if (gutsView instanceof AppOpsInfo) {
AppOpsInfo info = (AppOpsInfo) gutsView;
final UserHandle userHandle = sbn.getUser();
PackageManager pmUser = StatusBar.getPackageManagerForUser(mContext,
userHandle.getIdentifier());
final AppOpsInfo.OnSettingsClickListener onSettingsClick = (View v,
String pkg, int uid, ArraySet<Integer> ops) -> {
mMetricsLogger.action(MetricsProto.MetricsEvent.ACTION_OPS_GUTS_SETTINGS);
guts.resetFalsingCheck();
startAppOpsSettingsActivity(pkg, uid, ops, row);
};
if (!row.getEntry().mActiveAppOps.isEmpty()) {
info.bindGuts(pmUser, onSettingsClick, sbn, row.getEntry().mActiveAppOps);
}
/**
* Sets up the {@link NotificationSnooze} inside the notification row's guts.
*
* @param row view to set up the guts for
* @param notificationSnoozeView view to set up/bind within {@code row}
*/
private void initializeSnoozeView(
final ExpandableNotificationRow row,
NotificationSnooze notificationSnoozeView) {
NotificationGuts guts = row.getGuts();
StatusBarNotification sbn = row.getStatusBarNotification();
notificationSnoozeView.setSnoozeListener(mListContainer.getSwipeActionHelper());
notificationSnoozeView.setStatusBarNotification(sbn);
notificationSnoozeView.setSnoozeOptions(row.getEntry().snoozeCriteria);
guts.setHeightChangedListener((NotificationGuts g) -> {
mListContainer.onHeightChanged(row, row.isShown() /* needsAnimation */);
});
}
/**
* Sets up the {@link AppOpsInfo} inside the notification row's guts.
*
* @param row view to set up the guts for
* @param appOpsInfoView view to set up/bind within {@code row}
*/
private void initializeAppOpsInfo(
final ExpandableNotificationRow row,
AppOpsInfo appOpsInfoView) {
NotificationGuts guts = row.getGuts();
StatusBarNotification sbn = row.getStatusBarNotification();
UserHandle userHandle = sbn.getUser();
PackageManager pmUser = StatusBar.getPackageManagerForUser(mContext,
userHandle.getIdentifier());
AppOpsInfo.OnSettingsClickListener onSettingsClick =
(View v, String pkg, int uid, ArraySet<Integer> ops) -> {
mMetricsLogger.action(MetricsProto.MetricsEvent.ACTION_OPS_GUTS_SETTINGS);
guts.resetFalsingCheck();
startAppOpsSettingsActivity(pkg, uid, ops, row);
};
if (!row.getEntry().mActiveAppOps.isEmpty()) {
appOpsInfoView.bindGuts(pmUser, onSettingsClick, sbn, row.getEntry().mActiveAppOps);
}
}
if (gutsView instanceof NotificationInfo) {
final UserHandle userHandle = sbn.getUser();
PackageManager pmUser = StatusBar.getPackageManagerForUser(mContext,
userHandle.getIdentifier());
final INotificationManager iNotificationManager = INotificationManager.Stub.asInterface(
ServiceManager.getService(Context.NOTIFICATION_SERVICE));
final String pkg = sbn.getPackageName();
NotificationInfo info = (NotificationInfo) gutsView;
// Settings link is only valid for notifications that specify a user, unless this is the
// system user.
NotificationInfo.OnSettingsClickListener onSettingsClick = null;
if (!userHandle.equals(UserHandle.ALL)
|| mLockscreenUserManager.getCurrentUserId() == UserHandle.USER_SYSTEM) {
onSettingsClick = (View v, NotificationChannel channel, int appUid) -> {
mMetricsLogger.action(MetricsProto.MetricsEvent.ACTION_NOTE_INFO);
/**
* Sets up the {@link NotificationInfo} inside the notification row's guts.
*
* @param row view to set up the guts for
* @param notificationInfoView view to set up/bind within {@code row}
*/
@VisibleForTesting
void initializeNotificationInfo(
final ExpandableNotificationRow row,
NotificationInfo notificationInfoView) {
NotificationGuts guts = row.getGuts();
StatusBarNotification sbn = row.getStatusBarNotification();
String packageName = sbn.getPackageName();
// Settings link is only valid for notifications that specify a non-system user
NotificationInfo.OnSettingsClickListener onSettingsClick = null;
UserHandle userHandle = sbn.getUser();
PackageManager pmUser = StatusBar.getPackageManagerForUser(
mContext, userHandle.getIdentifier());
INotificationManager iNotificationManager = INotificationManager.Stub.asInterface(
ServiceManager.getService(Context.NOTIFICATION_SERVICE));
final NotificationInfo.OnAppSettingsClickListener onAppSettingsClick =
(View v, Intent intent) -> {
mMetricsLogger.action(MetricsProto.MetricsEvent.ACTION_APP_NOTE_SETTINGS);
guts.resetFalsingCheck();
mOnSettingsClickListener.onClick(sbn.getKey());
startAppNotificationSettingsActivity(pkg, appUid, channel, row);
mPresenter.startNotificationGutsIntent(intent, sbn.getUid(), row);
};
}
final NotificationInfo.OnAppSettingsClickListener onAppSettingsClick = (View v,
Intent intent) -> {
mMetricsLogger.action(MetricsProto.MetricsEvent.ACTION_APP_NOTE_SETTINGS);
guts.resetFalsingCheck();
mPresenter.startNotificationGutsIntent(intent, sbn.getUid(), row);
};
final View.OnClickListener onDoneClick = (View v) -> {
saveAndCloseNotificationMenu(row, guts, v);
};
boolean isForBlockingHelper = row.isBlockingHelperShowing();
ArraySet<NotificationChannel> channels = new ArraySet<>();
channels.add(row.getEntry().channel);
if (row.isSummaryWithChildren()) {
// If this is a summary, then add in the children notification channels for the
// same user and pkg.
final List<ExpandableNotificationRow> childrenRows = row.getNotificationChildren();
final int numChildren = childrenRows.size();
for (int i = 0; i < numChildren; i++) {
final ExpandableNotificationRow childRow = childrenRows.get(i);
final NotificationChannel childChannel = childRow.getEntry().channel;
final StatusBarNotification childSbn = childRow.getStatusBarNotification();
if (childSbn.getUser().equals(userHandle) &&
childSbn.getPackageName().equals(pkg)) {
channels.add(childChannel);
}
if (!userHandle.equals(UserHandle.ALL)
|| mLockscreenUserManager.getCurrentUserId() == UserHandle.USER_SYSTEM) {
onSettingsClick = (View v, NotificationChannel channel, int appUid) -> {
mMetricsLogger.action(MetricsProto.MetricsEvent.ACTION_NOTE_INFO);
guts.resetFalsingCheck();
mOnSettingsClickListener.onClick(sbn.getKey());
startAppNotificationSettingsActivity(packageName, appUid, channel, row);
};
}
try {
notificationInfoView.bindNotification(
pmUser,
iNotificationManager,
packageName,
row.getEntry().channel,
getNumNotificationChannels(row, packageName, userHandle),
sbn,
mCheckSaveListener,
onSettingsClick,
onAppSettingsClick,
mNonBlockablePkgs,
isForBlockingHelper,
row.getEntry().userSentiment == USER_SENTIMENT_NEGATIVE);
} catch (RemoteException e) {
Log.e(TAG, e.toString());
}
}
/**
* @return the number of channels covered by the notification row (including its children if
* it's a summary notification).
*/
private int getNumNotificationChannels(
ExpandableNotificationRow row, String packageName, UserHandle userHandle) {
ArraySet<NotificationChannel> channels = new ArraySet<>();
channels.add(row.getEntry().channel);
// If this is a summary, then add in the children notification channels for the
// same user and pkg.
if (row.isSummaryWithChildren()) {
final List<ExpandableNotificationRow> childrenRows = row.getNotificationChildren();
final int numChildren = childrenRows.size();
for (int i = 0; i < numChildren; i++) {
final ExpandableNotificationRow childRow = childrenRows.get(i);
final NotificationChannel childChannel = childRow.getEntry().channel;
final StatusBarNotification childSbn = childRow.getStatusBarNotification();
if (childSbn.getUser().equals(userHandle) &&
childSbn.getPackageName().equals(packageName)) {
channels.add(childChannel);
}
}
try {
info.bindNotification(pmUser, iNotificationManager, pkg, row.getEntry().channel,
channels.size(), sbn, mCheckSaveListener, onSettingsClick,
onAppSettingsClick, mNonBlockablePkgs,
row.getEntry().userSentiment == USER_SENTIMENT_NEGATIVE);
} catch (RemoteException e) {
Log.e(TAG, e.toString());
}
}
return channels.size();
}
/**
@@ -312,37 +352,42 @@ public class NotificationGutsManager implements Dumpable {
}
/**
* Opens guts on the given ExpandableNotificationRow |v|.
* Opens guts on the given ExpandableNotificationRow {@code view}. This handles opening guts for
* the normal half-swipe and long-press use cases via a circular reveal. When the blocking
* helper needs to be shown on the row, this will skip the circular reveal.
*
* @param v ExpandableNotificationRow to open guts on
* @param view ExpandableNotificationRow to open guts on
* @param x x coordinate of origin of circular reveal
* @param y y coordinate of origin of circular reveal
* @param item MenuItem the guts should display
* @param menuItem MenuItem the guts should display
* @return true if guts was opened
*/
public boolean openGuts(View v, int x, int y,
NotificationMenuRowPlugin.MenuItem item) {
if (!(v instanceof ExpandableNotificationRow)) {
boolean openGuts(
View view,
int x,
int y,
NotificationMenuRowPlugin.MenuItem menuItem) {
if (!(view instanceof ExpandableNotificationRow)) {
return false;
}
if (v.getWindowToken() == null) {
if (view.getWindowToken() == null) {
Log.e(TAG, "Trying to show notification guts, but not attached to window");
return false;
}
final ExpandableNotificationRow row = (ExpandableNotificationRow) v;
final ExpandableNotificationRow row = (ExpandableNotificationRow) view;
if (row.isDark()) {
return false;
}
v.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
if (row.areGutsExposed()) {
closeAndSaveGuts(false /* removeLeavebehind */, false /* force */,
true /* removeControls */, -1 /* x */, -1 /* y */,
true /* resetMenu */);
return false;
}
bindGuts(row, item);
bindGuts(row, menuItem);
NotificationGuts guts = row.getGuts();
// Assume we are a status_bar_notification_row
@@ -372,15 +417,18 @@ public class NotificationGutsManager implements Dumpable {
final boolean needsFalsingProtection =
(mPresenter.isPresenterLocked() &&
!mAccessibilityManager.isTouchExplorationEnabled());
guts.openControls(x, y, needsFalsingProtection, () -> {
// Move the notification view back over the menu
row.resetTranslation();
});
guts.openControls(
!row.isBlockingHelperShowing(),
x,
y,
needsFalsingProtection,
row::resetTranslation);
row.closeRemoteInput();
mListContainer.onHeightChanged(row, true /* needsAnimation */);
mNotificationGutsExposed = guts;
mGutsMenuItem = item;
mGutsMenuItem = menuItem;
}
});
return true;

View File

@@ -46,9 +46,11 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.settingslib.Utils;
import com.android.systemui.Dependency;
import com.android.systemui.Interpolators;
import com.android.systemui.R;
@@ -81,11 +83,12 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
private OnSettingsClickListener mOnSettingsClickListener;
private OnAppSettingsClickListener mAppSettingsClickListener;
private NotificationGuts mGutsContainer;
/** Whether this view is being shown as part of the blocking helper */
private boolean mIsForBlockingHelper;
private boolean mNegativeUserSentiment;
private OnClickListener mOnKeepShowing = v -> {
closeControls(v);
};
private OnClickListener mOnKeepShowing = this::closeControls;
private OnClickListener mOnStopMinNotifications = v -> {
swapContent(false);
@@ -114,7 +117,9 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
void onClick(View v, Intent intent);
}
public void bindNotification(final PackageManager pm,
@VisibleForTesting
void bindNotification(
final PackageManager pm,
final INotificationManager iNotificationManager,
final String pkg,
final NotificationChannel notificationChannel,
@@ -127,20 +132,24 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
throws RemoteException {
bindNotification(pm, iNotificationManager, pkg, notificationChannel, numChannels, sbn,
checkSaveListener, onSettingsClick, onAppSettingsClick, nonBlockablePkgs,
false /* negative sentiment */);
false /* isBlockingHelper */,
false /* isUserSentimentNegative */);
}
public void bindNotification(final PackageManager pm,
final INotificationManager iNotificationManager,
final String pkg,
final NotificationChannel notificationChannel,
final int numChannels,
final StatusBarNotification sbn,
final CheckSaveListener checkSaveListener,
final OnSettingsClickListener onSettingsClick,
final OnAppSettingsClickListener onAppSettingsClick,
final Set<String> nonBlockablePkgs,
boolean negativeUserSentiment) throws RemoteException {
public void bindNotification(
PackageManager pm,
INotificationManager iNotificationManager,
String pkg,
NotificationChannel notificationChannel,
int numChannels,
StatusBarNotification sbn,
CheckSaveListener checkSaveListener,
OnSettingsClickListener onSettingsClick,
OnAppSettingsClickListener onAppSettingsClick,
Set<String> nonBlockablePkgs,
boolean isForBlockingHelper,
boolean isUserSentimentNegative)
throws RemoteException {
mINotificationManager = iNotificationManager;
mPkg = pkg;
mNumNotificationChannels = numChannels;
@@ -152,9 +161,10 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
mOnSettingsClickListener = onSettingsClick;
mSingleNotificationChannel = notificationChannel;
mStartingUserImportance = mChosenImportance = mSingleNotificationChannel.getImportance();
mNegativeUserSentiment = negativeUserSentiment;
mNegativeUserSentiment = isUserSentimentNegative;
mIsForeground =
(mSbn.getNotification().flags & Notification.FLAG_FOREGROUND_SERVICE) != 0;
mIsForBlockingHelper = isForBlockingHelper;
int numTotalChannels = mINotificationManager.getNumNotificationChannelsForPackage(
pkg, mAppUid, false /* includeDeleted */);
@@ -294,12 +304,14 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
}
private void bindButtons() {
// Set up stay-in-notification actions
View block = findViewById(R.id.block);
block.setOnClickListener(mOnStopMinNotifications);
TextView keep = findViewById(R.id.keep);
keep.setOnClickListener(mOnKeepShowing);
findViewById(R.id.undo).setOnClickListener(mOnUndo);
View minimize = findViewById(R.id.minimize);
findViewById(R.id.undo).setOnClickListener(mOnUndo);
block.setOnClickListener(mOnStopMinNotifications);
keep.setOnClickListener(mOnKeepShowing);
minimize.setOnClickListener(mOnStopMinNotifications);
if (mNonblockable) {
@@ -314,7 +326,7 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
minimize.setVisibility(GONE);
}
// app settings link
// Set up app settings link
TextView settingsLinkView = findViewById(R.id.app_settings);
Intent settingsIntent = getAppSettingsIntent(mPm, mPkg, mSingleNotificationChannel,
mSbn.getId(), mSbn.getTag());
@@ -421,16 +433,23 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
return intent;
}
private void closeControls(View v) {
int[] parentLoc = new int[2];
int[] targetLoc = new int[2];
mGutsContainer.getLocationOnScreen(parentLoc);
v.getLocationOnScreen(targetLoc);
final int centerX = v.getWidth() / 2;
final int centerY = v.getHeight() / 2;
final int x = targetLoc[0] - parentLoc[0] + centerX;
final int y = targetLoc[1] - parentLoc[1] + centerY;
mGutsContainer.closeControls(x, y, true /* save */, false /* force */);
@VisibleForTesting
void closeControls(View v) {
if (mIsForBlockingHelper) {
NotificationBlockingHelperManager manager =
Dependency.get(NotificationBlockingHelperManager.class);
manager.dismissCurrentBlockingHelper();
} else {
int[] parentLoc = new int[2];
int[] targetLoc = new int[2];
mGutsContainer.getLocationOnScreen(parentLoc);
v.getLocationOnScreen(targetLoc);
final int centerX = v.getWidth() / 2;
final int centerY = v.getHeight() / 2;
final int x = targetLoc[0] - parentLoc[0] + centerX;
final int y = targetLoc[1] - parentLoc[1] + centerY;
mGutsContainer.closeControls(x, y, true /* save */, false /* force */);
}
}
@Override

View File

@@ -227,7 +227,6 @@ public class NotificationShelf extends ActivatableNotificationView implements
expandAmount = Math.min(1.0f, expandAmount);
}
// find the first view that doesn't overlap with the shelf
int notificationIndex = 0;
int notGoneIndex = 0;
int colorOfViewBeforeLast = NO_COLOR;
boolean backgroundForceHidden = false;
@@ -247,13 +246,15 @@ public class NotificationShelf extends ActivatableNotificationView implements
int baseZHeight = mAmbientState.getBaseZHeight();
int backgroundTop = 0;
float firstElementRoundness = 0.0f;
while (notificationIndex < mHostLayout.getChildCount()) {
ExpandableView child = (ExpandableView) mHostLayout.getChildAt(notificationIndex);
notificationIndex++;
for (int i = 0; i < mHostLayout.getChildCount(); i++) {
ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i);
if (!(child instanceof ExpandableNotificationRow)
|| child.getVisibility() == GONE) {
continue;
}
ExpandableNotificationRow row = (ExpandableNotificationRow) child;
float notificationClipEnd;
boolean aboveShelf = ViewState.getFinalTranslationZ(row) > baseZHeight
@@ -315,6 +316,9 @@ public class NotificationShelf extends ActivatableNotificationView implements
notGoneIndex++;
previousColor = ownColorUntinted;
}
clipTransientViews();
setBackgroundTop(backgroundTop);
setFirstElementRoundness(firstElementRoundness);
mShelfIcons.setSpeedBumpIndex(mAmbientState.getSpeedBumpIndex());
@@ -337,6 +341,25 @@ public class NotificationShelf extends ActivatableNotificationView implements
}
}
/**
* Clips transient views to the top of the shelf - Transient views are only used for
* disappearing views/animations and need to be clipped correctly by the shelf to ensure they
* don't show underneath the notification stack when something is animating and the user
* swipes quickly.
*/
private void clipTransientViews() {
for (int i = 0; i < mHostLayout.getTransientViewCount(); i++) {
View transientView = mHostLayout.getTransientView(i);
if (transientView instanceof ExpandableNotificationRow) {
ExpandableNotificationRow transientRow = (ExpandableNotificationRow) transientView;
updateNotificationClipHeight(transientRow, getTranslationY());
} else {
Log.e(TAG, "NotificationShelf.clipTransientViews(): "
+ "Trying to clip non-row transient view");
}
}
}
private void setFirstElementRoundness(float firstElementRoundness) {
if (mFirstElementRoundness != firstElementRoundness) {
mFirstElementRoundness = firstElementRoundness;

View File

@@ -125,24 +125,30 @@ public class NotificationViewHierarchyManager {
}
ArrayList<ExpandableNotificationRow> toRemove = new ArrayList<>();
ArrayList<ExpandableNotificationRow> viewsToRemove = new ArrayList<>();
for (int i=0; i< mListContainer.getContainerChildCount(); i++) {
View child = mListContainer.getContainerChildAt(i);
if (!toShow.contains(child) && child instanceof ExpandableNotificationRow) {
toRemove.add((ExpandableNotificationRow) child);
ExpandableNotificationRow row = (ExpandableNotificationRow) child;
// Blocking helper is effectively a detached view. Don't bother removing it from the
// layout.
if (!row.isBlockingHelperShowing()) {
viewsToRemove.add((ExpandableNotificationRow) child);
}
}
}
for (ExpandableNotificationRow remove : toRemove) {
if (mGroupManager.isChildInGroupWithSummary(remove.getStatusBarNotification())) {
for (ExpandableNotificationRow viewToRemove : viewsToRemove) {
if (mGroupManager.isChildInGroupWithSummary(viewToRemove.getStatusBarNotification())) {
// we are only transferring this notification to its parent, don't generate an
// animation
mListContainer.setChildTransferInProgress(true);
}
if (remove.isSummaryWithChildren()) {
remove.removeAllChildren();
if (viewToRemove.isSummaryWithChildren()) {
viewToRemove.removeAllChildren();
}
mListContainer.removeContainerView(remove);
mListContainer.removeContainerView(viewToRemove);
mListContainer.setChildTransferInProgress(false);
}
@@ -168,6 +174,10 @@ public class NotificationViewHierarchyManager {
// We don't care about non-notification views.
continue;
}
if (((ExpandableNotificationRow) child).isBlockingHelperShowing()) {
// Don't count/reorder notifications that are showing the blocking helper!
continue;
}
ExpandableNotificationRow targetChild = toShow.get(j);
if (child != targetChild) {
@@ -340,7 +350,7 @@ public class NotificationViewHierarchyManager {
}
}
row.showBlockingHelper(entry.userSentiment ==
row.showBlockingHelperButton(entry.userSentiment ==
NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE);
row.showAppOpsIcons(entry.mActiveAppOps);

View File

@@ -169,7 +169,7 @@ public class NotificationGroupManager implements OnHeadsUpChangedListener {
if (group.suppressed) {
handleSuppressedSummaryHeadsUpped(group.summary);
}
if (!mIsUpdatingUnchangedGroup) {
if (!mIsUpdatingUnchangedGroup && mListener != null) {
mListener.onGroupsChanged();
}
}

View File

@@ -70,6 +70,7 @@ import android.widget.ScrollView;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.settingslib.Utils;
import com.android.systemui.Dependency;
import com.android.systemui.ExpandHelper;
import com.android.systemui.Interpolators;
import com.android.systemui.R;
@@ -83,6 +84,7 @@ import com.android.systemui.statusbar.EmptyShadeView;
import com.android.systemui.statusbar.ExpandableNotificationRow;
import com.android.systemui.statusbar.ExpandableView;
import com.android.systemui.statusbar.FooterView;
import com.android.systemui.statusbar.NotificationBlockingHelperManager;
import com.android.systemui.statusbar.NotificationData;
import com.android.systemui.statusbar.NotificationGuts;
import com.android.systemui.statusbar.NotificationListContainer;
@@ -448,6 +450,13 @@ public class NotificationStackScrollLayout extends ViewGroup
mRoundnessManager.setOnRoundingChangedCallback(this::invalidate);
addOnExpandedHeightListener(mRoundnessManager::setExpanded);
// Blocking helper manager wants to know the expanded state, update as well.
NotificationBlockingHelperManager blockingHelperManager =
Dependency.get(NotificationBlockingHelperManager.class);
addOnExpandedHeightListener((height, unused) -> {
blockingHelperManager.setNotificationShadeExpanded(height);
});
updateWillNotDraw();
mBackgroundPaint.setAntiAlias(true);
if (DEBUG) {
@@ -1038,45 +1047,63 @@ public class NotificationStackScrollLayout extends ViewGroup
mQsContainer = qsContainer;
}
/**
* Handles cleanup after the given {@code view} has been fully swiped out (including
* re-invoking dismiss logic in case the notification has not made its way out yet).
*/
@Override
public void onChildDismissed(View v) {
ExpandableNotificationRow row = (ExpandableNotificationRow) v;
public void onChildDismissed(View view) {
ExpandableNotificationRow row = (ExpandableNotificationRow) view;
if (!row.isDismissed()) {
handleChildDismissed(v);
handleChildViewDismissed(view);
}
ViewGroup transientContainer = row.getTransientContainer();
if (transientContainer != null) {
transientContainer.removeTransientView(v);
transientContainer.removeTransientView(view);
}
}
private void handleChildDismissed(View v) {
/**
* Starts up notification dismiss and tells the notification, if any, to remove itself from
* layout.
*
* @param view view (e.g. notification) to dismiss from the layout
*/
private void handleChildViewDismissed(View view) {
if (mDismissAllInProgress) {
return;
}
boolean isBlockingHelperShown = false;
setSwipingInProgress(false);
if (mDragAnimPendingChildren.contains(v)) {
// We start the swipe and finish it in the same frame, we don't want any animation
// for the drag
mDragAnimPendingChildren.remove(v);
if (mDragAnimPendingChildren.contains(view)) {
// We start the swipe and finish it in the same frame; we don't want a drag animation.
mDragAnimPendingChildren.remove(view);
}
mSwipedOutViews.add(v);
mAmbientState.onDragFinished(v);
mAmbientState.onDragFinished(view);
updateContinuousShadowDrawing();
if (v instanceof ExpandableNotificationRow) {
ExpandableNotificationRow row = (ExpandableNotificationRow) v;
if (view instanceof ExpandableNotificationRow) {
ExpandableNotificationRow row = (ExpandableNotificationRow) view;
if (row.isHeadsUp()) {
mHeadsUpManager.addSwipedOutNotification(row.getStatusBarNotification().getKey());
}
}
if (v instanceof ExpandableNotificationRow) {
((ExpandableNotificationRow) v).performDismiss(false /* fromAccessibility */);
isBlockingHelperShown =
row.performDismissWithBlockingHelper(false /* fromAccessibility */);
}
if (!isBlockingHelperShown) {
mSwipedOutViews.add(view);
}
mFalsingManager.onNotificationDismissed();
if (mFalsingManager.shouldEnforceBouncer()) {
mStatusBar.executeRunnableDismissingKeyguard(null, null /* cancelAction */,
false /* dismissShade */, true /* afterKeyguardGone */, false /* deferred */);
mStatusBar.executeRunnableDismissingKeyguard(
null,
null /* cancelAction */,
false /* dismissShade */,
true /* afterKeyguardGone */,
false /* deferred */);
}
}
@@ -2729,9 +2756,8 @@ public class NotificationStackScrollLayout extends ViewGroup
updateScrollStateForRemovedChild(expandableView);
boolean animationGenerated = generateRemoveAnimation(child);
if (animationGenerated) {
if (!mSwipedOutViews.contains(child)) {
container.getOverlay().add(child);
} else if (Math.abs(expandableView.getTranslation()) != expandableView.getWidth()) {
if (!mSwipedOutViews.contains(child)
|| Math.abs(expandableView.getTranslation()) != expandableView.getWidth()) {
container.addTransientView(child, 0);
expandableView.setTransientContainer(container);
}
@@ -4618,7 +4644,7 @@ public class NotificationStackScrollLayout extends ViewGroup
if (mIsExpanded) {
// We don't want to quick-dismiss when it's a heads up as this might lead to closing
// of the panel early.
handleChildDismissed(view);
handleChildViewDismissed(view);
}
mStatusBar.getGutsManager().closeAndSaveGuts(true /* removeLeavebehind */,
false /* force */, false /* removeControls */, -1 /* x */, -1 /* y */,

View File

@@ -51,6 +51,7 @@ public class StackStateAnimator {
= (int) (ANIMATION_DURATION_HEADS_UP_APPEAR
* HeadsUpAppearInterpolator.getFractionUntilOvershoot());
public static final int ANIMATION_DURATION_HEADS_UP_DISAPPEAR = 300;
public static final int ANIMATION_DURATION_BLOCKING_HELPER_FADE = 240;
public static final int ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING = 80;
public static final int ANIMATION_DELAY_PER_ELEMENT_MANUAL = 32;
public static final int ANIMATION_DELAY_PER_ELEMENT_GO_TO_FULL_SHADE = 48;
@@ -79,6 +80,7 @@ public class StackStateAnimator {
private ValueAnimator mBottomOverScrollAnimator;
private int mHeadsUpAppearHeightBottom;
private boolean mShadeExpanded;
private ArrayList<ExpandableView> mTransientViewsToRemove = new ArrayList<>();
private NotificationShelf mShelf;
private float mStatusBarIconLocation;
private int[] mTmpLocation = new int[2];
@@ -333,6 +335,12 @@ public class StackStateAnimator {
private void onAnimationFinished() {
mHostLayout.onChildAnimationFinished();
for (ExpandableView transientViewsToRemove : mTransientViewsToRemove) {
transientViewsToRemove.getTransientContainer()
.removeTransientView(transientViewsToRemove);
}
mTransientViewsToRemove.clear();
}
/**
@@ -362,7 +370,7 @@ public class StackStateAnimator {
} else if (event.animationType ==
NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE) {
if (changingView.getVisibility() != View.VISIBLE) {
removeFromOverlay(changingView);
removeTransientView(changingView);
continue;
}
@@ -402,8 +410,7 @@ public class StackStateAnimator {
0, new Runnable() {
@Override
public void run() {
// remove the temporary overlay
removeFromOverlay(changingView);
removeTransientView(changingView);
}
}, null);
} else if (event.animationType ==
@@ -494,6 +501,12 @@ public class StackStateAnimator {
}
}
private static void removeTransientView(ExpandableView viewToRemove) {
if (viewToRemove.getTransientContainer() != null) {
viewToRemove.getTransientContainer().removeTransientView(viewToRemove);
}
}
public static void removeFromOverlay(View changingView) {
ViewGroup parent = (ViewGroup) changingView.getParent();
if (parent != null) {

View File

@@ -16,6 +16,8 @@
package com.android.systemui.statusbar;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
@@ -40,8 +42,13 @@ import com.android.systemui.statusbar.stack.NotificationChildrenContainer;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import java.util.function.Consumer;
@@ -49,52 +56,59 @@ import java.util.function.Consumer;
@RunWith(AndroidJUnit4.class)
public class ExpandableNotificationRowTest extends SysuiTestCase {
private ExpandableNotificationRow mGroup;
private ExpandableNotificationRow mGroupRow;
private NotificationTestHelper mNotificationTestHelper;
boolean mHeadsUpAnimatingAway = false;
@Rule public MockitoRule mockito = MockitoJUnit.rule();
@Mock private NotificationBlockingHelperManager mBlockingHelperManager;
@Before
public void setUp() throws Exception {
mNotificationTestHelper = new NotificationTestHelper(mContext);
mGroup = mNotificationTestHelper.createGroup();
mGroup.setHeadsUpAnimatingAwayListener(
mGroupRow = mNotificationTestHelper.createGroup();
mGroupRow.setHeadsUpAnimatingAwayListener(
animatingAway -> mHeadsUpAnimatingAway = animatingAway);
mDependency.injectTestDependency(
NotificationBlockingHelperManager.class,
mBlockingHelperManager);
}
@Test
public void testGroupSummaryNotShowingIconWhenPublic() {
mGroup.setSensitive(true, true);
mGroup.setHideSensitiveForIntrinsicHeight(true);
Assert.assertTrue(mGroup.isSummaryWithChildren());
Assert.assertFalse(mGroup.isShowingIcon());
mGroupRow.setSensitive(true, true);
mGroupRow.setHideSensitiveForIntrinsicHeight(true);
assertTrue(mGroupRow.isSummaryWithChildren());
assertFalse(mGroupRow.isShowingIcon());
}
@Test
public void testNotificationHeaderVisibleWhenAnimating() {
mGroup.setSensitive(true, true);
mGroup.setHideSensitive(true, false, 0, 0);
mGroup.setHideSensitive(false, true, 0, 0);
Assert.assertTrue(mGroup.getChildrenContainer().getVisibleHeader().getVisibility()
mGroupRow.setSensitive(true, true);
mGroupRow.setHideSensitive(true, false, 0, 0);
mGroupRow.setHideSensitive(false, true, 0, 0);
assertTrue(mGroupRow.getChildrenContainer().getVisibleHeader().getVisibility()
== View.VISIBLE);
}
@Test
public void testUserLockedResetEvenWhenNoChildren() {
mGroup.setUserLocked(true);
mGroup.removeAllChildren();
mGroup.setUserLocked(false);
Assert.assertFalse("The childrencontainer should not be userlocked but is, the state "
+ "seems out of sync.", mGroup.getChildrenContainer().isUserLocked());
mGroupRow.setUserLocked(true);
mGroupRow.removeAllChildren();
mGroupRow.setUserLocked(false);
assertFalse("The childrencontainer should not be userlocked but is, the state "
+ "seems out of sync.", mGroupRow.getChildrenContainer().isUserLocked());
}
@Test
public void testReinflatedOnDensityChange() {
mGroup.setUserLocked(true);
mGroup.removeAllChildren();
mGroup.setUserLocked(false);
mGroupRow.setUserLocked(true);
mGroupRow.removeAllChildren();
mGroupRow.setUserLocked(false);
NotificationChildrenContainer mockContainer = mock(NotificationChildrenContainer.class);
mGroup.setChildrenContainer(mockContainer);
mGroup.onDensityOrFontScaleChanged();
mGroupRow.setChildrenContainer(mockContainer);
mGroupRow.onDensityOrFontScaleChanged();
verify(mockContainer).reInflateViews(any(), any());
}
@@ -151,38 +165,38 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
@Test
public void testClickSound() throws Exception {
Assert.assertTrue("Should play sounds by default.", mGroup.isSoundEffectsEnabled());
mGroup.setDark(true /* dark */, false /* fade */, 0 /* delay */);
mGroup.setSecureStateProvider(()-> false);
Assert.assertFalse("Shouldn't play sounds when dark and trusted.",
mGroup.isSoundEffectsEnabled());
mGroup.setSecureStateProvider(()-> true);
Assert.assertTrue("Should always play sounds when not trusted.",
mGroup.isSoundEffectsEnabled());
assertTrue("Should play sounds by default.", mGroupRow.isSoundEffectsEnabled());
mGroupRow.setDark(true /* dark */, false /* fade */, 0 /* delay */);
mGroupRow.setSecureStateProvider(()-> false);
assertFalse("Shouldn't play sounds when dark and trusted.",
mGroupRow.isSoundEffectsEnabled());
mGroupRow.setSecureStateProvider(()-> true);
assertTrue("Should always play sounds when not trusted.",
mGroupRow.isSoundEffectsEnabled());
}
@Test
public void testSetDismissed_longPressListenerRemoved() {
ExpandableNotificationRow.LongPressListener listener =
mock(ExpandableNotificationRow.LongPressListener.class);
mGroup.setLongPressListener(listener);
mGroup.doLongClickCallback(0,0);
verify(listener, times(1)).onLongPress(eq(mGroup), eq(0), eq(0),
mGroupRow.setLongPressListener(listener);
mGroupRow.doLongClickCallback(0,0);
verify(listener, times(1)).onLongPress(eq(mGroupRow), eq(0), eq(0),
any(NotificationMenuRowPlugin.MenuItem.class));
reset(listener);
mGroup.setDismissed(true);
mGroup.doLongClickCallback(0,0);
verify(listener, times(0)).onLongPress(eq(mGroup), eq(0), eq(0),
mGroupRow.setDismissed(true);
mGroupRow.doLongClickCallback(0,0);
verify(listener, times(0)).onLongPress(eq(mGroupRow), eq(0), eq(0),
any(NotificationMenuRowPlugin.MenuItem.class));
}
@Test
public void testShowAppOps_noHeader() {
// public notification is custom layout - no header
mGroup.setSensitive(true, true);
mGroup.setAppOpsOnClickListener(null);
mGroup.showAppOpsIcons(null);
mGroupRow.setSensitive(true, true);
mGroupRow.setAppOpsOnClickListener(null);
mGroupRow.showAppOpsIcons(null);
}
@Test
@@ -190,17 +204,17 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
NotificationHeaderView mockHeader = mock(NotificationHeaderView.class);
NotificationContentView publicLayout = mock(NotificationContentView.class);
mGroup.setPublicLayout(publicLayout);
mGroupRow.setPublicLayout(publicLayout);
NotificationContentView privateLayout = mock(NotificationContentView.class);
mGroup.setPrivateLayout(privateLayout);
mGroupRow.setPrivateLayout(privateLayout);
NotificationChildrenContainer mockContainer = mock(NotificationChildrenContainer.class);
when(mockContainer.getNotificationChildCount()).thenReturn(1);
when(mockContainer.getHeaderView()).thenReturn(mockHeader);
mGroup.setChildrenContainer(mockContainer);
mGroupRow.setChildrenContainer(mockContainer);
ArraySet<Integer> ops = new ArraySet<>();
ops.add(AppOpsManager.OP_ANSWER_PHONE_CALLS);
mGroup.showAppOpsIcons(ops);
mGroupRow.showAppOpsIcons(ops);
verify(mockHeader, times(1)).showAppOpsIcons(ops);
verify(privateLayout, times(1)).showAppOpsIcons(ops);
@@ -214,17 +228,50 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
ExpandableNotificationRow.OnAppOpsClickListener.class);
View view = mock(View.class);
mGroup.setAppOpsOnClickListener(l);
mGroupRow.setAppOpsOnClickListener(l);
mGroup.getAppOpsOnClickListener().onClick(view);
mGroupRow.getAppOpsOnClickListener().onClick(view);
verify(l, times(1)).onClick(any(), anyInt(), anyInt(), any());
}
@Test
public void testHeadsUpAnimatingAwayListener() {
mGroup.setHeadsUpAnimatingAway(true);
mGroupRow.setHeadsUpAnimatingAway(true);
Assert.assertEquals(true, mHeadsUpAnimatingAway);
mGroup.setHeadsUpAnimatingAway(false);
mGroupRow.setHeadsUpAnimatingAway(false);
Assert.assertEquals(false, mHeadsUpAnimatingAway);
}
@Test
public void testPerformDismissWithBlockingHelper_falseWhenBlockingHelperIsntShown() {
when(mBlockingHelperManager.perhapsShowBlockingHelper(
eq(mGroupRow), any(NotificationMenuRowPlugin.class))).thenReturn(false);
assertFalse(
mGroupRow.performDismissWithBlockingHelper(false /* fromAccessibility */));
}
@Test
public void testPerformDismissWithBlockingHelper_doesntPerformOnGroupSummary() {
ExpandableNotificationRow childRow = mGroupRow.getChildrenContainer().getViewAtPosition(0);
when(mBlockingHelperManager.perhapsShowBlockingHelper(eq(childRow), any(NotificationMenuRowPlugin.class)))
.thenReturn(true);
assertTrue(
childRow.performDismissWithBlockingHelper(false /* fromAccessibility */));
verify(mBlockingHelperManager, times(1))
.perhapsShowBlockingHelper(eq(childRow), any(NotificationMenuRowPlugin.class));
verify(mBlockingHelperManager, times(0))
.perhapsShowBlockingHelper(eq(mGroupRow), any(NotificationMenuRowPlugin.class));
}
@Test
public void testIsBlockingHelperShowing_isCorrectlyUpdated() {
mGroupRow.setBlockingHelperShowing(true);
assertTrue(mGroupRow.isBlockingHelperShowing());
mGroupRow.setBlockingHelperShowing(false);
assertFalse(mGroupRow.isBlockingHelperShowing());
}
}

View File

@@ -0,0 +1,214 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.systemui.statusbar;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
import android.content.Context;
import android.support.test.filters.SmallTest;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.View;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE;
import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEUTRAL;
import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_POSITIVE;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Tests for {@link NotificationBlockingHelperManager}.
*/
@SmallTest
@org.junit.runner.RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class NotificationBlockingHelperManagerTest extends SysuiTestCase {
private NotificationBlockingHelperManager mBlockingHelperManager;
private NotificationTestHelper mHelper;
@Rule public MockitoRule mockito = MockitoJUnit.rule();
@Mock private NotificationGutsManager mGutsManager;
@Mock private NotificationEntryManager mEntryManager;
@Mock private NotificationMenuRow mMenuRow;
@Mock private NotificationMenuRowPlugin.MenuItem mMenuItem;
@Before
public void setUp() {
mBlockingHelperManager = new NotificationBlockingHelperManager(mContext);
mHelper = new NotificationTestHelper(mContext);
when(mGutsManager.openGuts(
any(View.class),
anyInt(),
anyInt(),
any(NotificationMenuRowPlugin.MenuItem.class)))
.thenReturn(true);
mDependency.injectTestDependency(NotificationGutsManager.class, mGutsManager);
mDependency.injectTestDependency(NotificationEntryManager.class, mEntryManager);
when(mMenuRow.getLongpressMenuItem(any(Context.class))).thenReturn(mMenuItem);
}
@Test
public void testDismissCurrentBlockingHelper_nullBlockingHelperRow() {
// By default, this shouldn't dismiss (no pointers/vars set up!)
assertFalse(mBlockingHelperManager.dismissCurrentBlockingHelper());
assertTrue(mBlockingHelperManager.isBlockingHelperRowNull());
}
@Test
public void testDismissCurrentBlockingHelper_withDetachedBlockingHelperRow() throws Exception {
ExpandableNotificationRow row = spy(mHelper.createRow());
row.setBlockingHelperShowing(true);
when(row.isAttachedToWindow()).thenReturn(false);
mBlockingHelperManager.setBlockingHelperRowForTest(row);
assertTrue(mBlockingHelperManager.dismissCurrentBlockingHelper());
assertTrue(mBlockingHelperManager.isBlockingHelperRowNull());
verify(mEntryManager, times(0)).updateNotifications();
}
@Test
public void testDismissCurrentBlockingHelper_withAttachedBlockingHelperRow() throws Exception {
ExpandableNotificationRow row = spy(mHelper.createRow());
row.setBlockingHelperShowing(true);
when(row.isAttachedToWindow()).thenReturn(true);
mBlockingHelperManager.setBlockingHelperRowForTest(row);
assertTrue(mBlockingHelperManager.dismissCurrentBlockingHelper());
assertTrue(mBlockingHelperManager.isBlockingHelperRowNull());
verify(mEntryManager).updateNotifications();
}
@Test
public void testPerhapsShowBlockingHelper_shown() throws Exception {
ExpandableNotificationRow row = mHelper.createRow();
row.getEntry().userSentiment = USER_SENTIMENT_NEGATIVE;
mBlockingHelperManager.setNotificationShadeExpanded(1f);
assertTrue(mBlockingHelperManager.perhapsShowBlockingHelper(row, mMenuRow));
verify(mGutsManager).openGuts(row, 0, 0, mMenuItem);
}
@Test
public void testPerhapsShowBlockingHelper_shownForLargeGroup() throws Exception {
ExpandableNotificationRow groupRow = mHelper.createGroup(10);
groupRow.getEntry().userSentiment = USER_SENTIMENT_NEGATIVE;
mBlockingHelperManager.setNotificationShadeExpanded(1f);
assertTrue(mBlockingHelperManager.perhapsShowBlockingHelper(groupRow, mMenuRow));
verify(mGutsManager).openGuts(groupRow, 0, 0, mMenuItem);
}
@Test
public void testPerhapsShowBlockingHelper_shownForOnlyChildNotification()
throws Exception {
ExpandableNotificationRow groupRow = mHelper.createGroup(1);
// Explicitly get the children container & call getViewAtPosition on it instead of the row
// as other factors such as view expansion may cause us to get the parent row back instead
// of the child row.
ExpandableNotificationRow childRow = groupRow.getChildrenContainer().getViewAtPosition(0);
childRow.getEntry().userSentiment = USER_SENTIMENT_NEGATIVE;
mBlockingHelperManager.setNotificationShadeExpanded(1f);
assertTrue(mBlockingHelperManager.perhapsShowBlockingHelper(childRow, mMenuRow));
verify(mGutsManager).openGuts(childRow, 0, 0, mMenuItem);
}
@Test
public void testPerhapsShowBlockingHelper_notShownDueToNeutralUserSentiment() throws Exception {
ExpandableNotificationRow row = mHelper.createRow();
row.getEntry().userSentiment = USER_SENTIMENT_NEUTRAL;
mBlockingHelperManager.setNotificationShadeExpanded(1f);
assertFalse(mBlockingHelperManager.perhapsShowBlockingHelper(row, mMenuRow));
}
@Test
public void testPerhapsShowBlockingHelper_notShownDueToPositiveUserSentiment()
throws Exception {
ExpandableNotificationRow row = mHelper.createRow();
row.getEntry().userSentiment = USER_SENTIMENT_POSITIVE;
mBlockingHelperManager.setNotificationShadeExpanded(1f);
assertFalse(mBlockingHelperManager.perhapsShowBlockingHelper(row, mMenuRow));
}
@Test
public void testPerhapsShowBlockingHelper_notShownDueToShadeVisibility() throws Exception {
ExpandableNotificationRow row = mHelper.createRow();
row.getEntry().userSentiment = USER_SENTIMENT_NEGATIVE;
// Hide the shade
mBlockingHelperManager.setNotificationShadeExpanded(0f);
assertFalse(mBlockingHelperManager.perhapsShowBlockingHelper(row, mMenuRow));
}
@Test
public void testPerhapsShowBlockingHelper_notShownAsNotificationIsInMultipleChildGroup()
throws Exception {
ExpandableNotificationRow groupRow = mHelper.createGroup(2);
// Explicitly get the children container & call getViewAtPosition on it instead of the row
// as other factors such as view expansion may cause us to get the parent row back instead
// of the child row.
ExpandableNotificationRow childRow = groupRow.getChildrenContainer().getViewAtPosition(0);
childRow.getEntry().userSentiment = USER_SENTIMENT_NEGATIVE;
mBlockingHelperManager.setNotificationShadeExpanded(1f);
assertFalse(mBlockingHelperManager.perhapsShowBlockingHelper(childRow, mMenuRow));
}
@Test
public void testBlockingHelperShowAndDismiss() throws Exception{
ExpandableNotificationRow row = spy(mHelper.createRow());
row.getEntry().userSentiment = USER_SENTIMENT_NEGATIVE;
when(row.isAttachedToWindow()).thenReturn(true);
mBlockingHelperManager.setNotificationShadeExpanded(1f);
// Show check
assertTrue(mBlockingHelperManager.perhapsShowBlockingHelper(row, mMenuRow));
verify(mGutsManager).openGuts(row, 0, 0, mMenuItem);
// Dismiss check
assertTrue(mBlockingHelperManager.dismissCurrentBlockingHelper());
assertTrue(mBlockingHelperManager.isBlockingHelperRowNull());
verify(mEntryManager).updateNotifications();
}
}

View File

@@ -20,6 +20,7 @@ import static android.app.AppOpsManager.OP_CAMERA;
import static android.app.AppOpsManager.OP_RECORD_AUDIO;
import static android.app.AppOpsManager.OP_SYSTEM_ALERT_WINDOW;
import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE;
import static junit.framework.Assert.assertNotNull;
import static org.junit.Assert.assertEquals;
@@ -27,6 +28,8 @@ import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -34,31 +37,24 @@ import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.times;
import android.app.INotificationManager;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Binder;
import android.os.Handler;
import android.os.Looper;
import android.os.UserHandle;
import android.provider.Settings;
import android.service.notification.StatusBarNotification;
import android.support.test.filters.SmallTest;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.util.ArraySet;
import android.view.LayoutInflater;
import android.view.View;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
import com.android.systemui.statusbar.NotificationData;
import com.android.systemui.statusbar.NotificationGuts;
import com.android.systemui.statusbar.NotificationTestHelper;
import com.android.systemui.statusbar.notification.NotificationInflater;
import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
import org.junit.Before;
@@ -70,15 +66,18 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoRule;
import org.mockito.junit.MockitoJUnit;
import java.util.HashSet;
import java.util.Set;
/**
* Tests for {@link NotificationGutsManager}.
*/
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class NotificationGutsManagerTest extends SysuiTestCase {
private static final String TEST_CHANNEL_ID = "NotificationManagerServiceTestChannelId";
private final String mPackageName = mContext.getPackageName();
private final int mUid = Binder.getCallingUid();
private NotificationChannel mTestNotificationChannel = new NotificationChannel(
TEST_CHANNEL_ID, TEST_CHANNEL_ID, NotificationManager.IMPORTANCE_DEFAULT);
private TestableLooper mTestableLooper;
@@ -87,7 +86,6 @@ public class NotificationGutsManagerTest extends SysuiTestCase {
private NotificationGutsManager mGutsManager;
@Rule public MockitoRule mockito = MockitoJUnit.rule();
@Mock private NotificationLockscreenUserManager mLockscreenUserManager;
@Mock private NotificationPresenter mPresenter;
@Mock private NotificationEntryManager mEntryManager;
@Mock private NotificationStackScrollLayout mStackScroller;
@@ -118,7 +116,12 @@ public class NotificationGutsManagerTest extends SysuiTestCase {
});
// Test doesn't support animation since the guts view is not attached.
doNothing().when(guts).openControls(anyInt(), anyInt(), anyBoolean(), any(Runnable.class));
doNothing().when(guts).openControls(
eq(true) /* shouldDoCircularReveal */,
anyInt(),
anyInt(),
anyBoolean(),
any(Runnable.class));
ExpandableNotificationRow realRow = createTestNotificationRow();
NotificationMenuRowPlugin.MenuItem menuItem = createTestMenuItem(realRow);
@@ -130,7 +133,12 @@ public class NotificationGutsManagerTest extends SysuiTestCase {
mGutsManager.openGuts(row, 0, 0, menuItem);
assertEquals(View.INVISIBLE, guts.getVisibility());
mTestableLooper.processAllMessages();
verify(guts).openControls(anyInt(), anyInt(), anyBoolean(), any(Runnable.class));
verify(guts).openControls(
eq(true),
anyInt(),
anyInt(),
anyBoolean(),
any(Runnable.class));
assertEquals(View.VISIBLE, guts.getVisibility());
mGutsManager.closeAndSaveGuts(false, false, false, 0, 0, false);
@@ -148,7 +156,12 @@ public class NotificationGutsManagerTest extends SysuiTestCase {
});
// Test doesn't support animation since the guts view is not attached.
doNothing().when(guts).openControls(anyInt(), anyInt(), anyBoolean(), any(Runnable.class));
doNothing().when(guts).openControls(
eq(true) /* shouldDoCircularReveal */,
anyInt(),
anyInt(),
anyBoolean(),
any(Runnable.class));
ExpandableNotificationRow realRow = createTestNotificationRow();
NotificationMenuRowPlugin.MenuItem menuItem = createTestMenuItem(realRow);
@@ -160,7 +173,12 @@ public class NotificationGutsManagerTest extends SysuiTestCase {
mGutsManager.openGuts(row, 0, 0, menuItem);
mTestableLooper.processAllMessages();
verify(guts).openControls(anyInt(), anyInt(), anyBoolean(), any(Runnable.class));
verify(guts).openControls(
eq(true),
anyInt(),
anyInt(),
anyBoolean(),
any(Runnable.class));
row.onDensityOrFontScaleChanged();
mGutsManager.onDensityOrFontScaleChanged(row);
@@ -247,6 +265,56 @@ public class NotificationGutsManagerTest extends SysuiTestCase {
assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, captor.getValue().getAction());
}
@Test
public void testInitializeNotificationInfoView_showBlockingHelper() throws Exception {
NotificationInfo notificationInfoView = mock(NotificationInfo.class);
ExpandableNotificationRow row = mHelper.createRow();
row.setBlockingHelperShowing(true);
row.getEntry().userSentiment = USER_SENTIMENT_NEGATIVE;
StatusBarNotification statusBarNotification = row.getStatusBarNotification();
mGutsManager.initializeNotificationInfo(row, notificationInfoView);
verify(notificationInfoView).bindNotification(
any(PackageManager.class),
any(INotificationManager.class),
eq(statusBarNotification.getPackageName()),
isNull(),
anyInt(),
eq(statusBarNotification),
any(NotificationInfo.CheckSaveListener.class),
any(NotificationInfo.OnSettingsClickListener.class),
any(NotificationInfo.OnAppSettingsClickListener.class),
any(),
eq(true) /* isForBlockingHelper */,
eq(true) /* isUserSentimentNegative */);
}
@Test
public void testInitializeNotificationInfoView_dontShowBlockingHelper() throws Exception {
NotificationInfo notificationInfoView = mock(NotificationInfo.class);
ExpandableNotificationRow row = mHelper.createRow();
row.setBlockingHelperShowing(false);
row.getEntry().userSentiment = USER_SENTIMENT_NEGATIVE;
StatusBarNotification statusBarNotification = row.getStatusBarNotification();
mGutsManager.initializeNotificationInfo(row, notificationInfoView);
verify(notificationInfoView).bindNotification(
any(PackageManager.class),
any(INotificationManager.class),
eq(statusBarNotification.getPackageName()),
isNull(),
anyInt(),
eq(statusBarNotification),
any(NotificationInfo.CheckSaveListener.class),
any(NotificationInfo.OnSettingsClickListener.class),
any(NotificationInfo.OnAppSettingsClickListener.class),
any(),
eq(false) /* isForBlockingHelper */,
eq(true) /* isUserSentimentNegative */);
}
////////////////////////////////////////////////////////////////////////////////////////////////
// Utility methods:

View File

@@ -67,9 +67,13 @@ import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import java.util.ArrayList;
import java.util.Collections;
@@ -87,14 +91,21 @@ public class NotificationInfoTest extends SysuiTestCase {
private static final String TEST_CHANNEL_NAME = "TEST CHANNEL NAME";
private NotificationInfo mNotificationInfo;
private final INotificationManager mMockINotificationManager = mock(INotificationManager.class);
private final PackageManager mMockPackageManager = mock(PackageManager.class);
private NotificationChannel mNotificationChannel;
private NotificationChannel mDefaultNotificationChannel;
private StatusBarNotification mSbn;
@Rule public MockitoRule mockito = MockitoJUnit.rule();
@Mock private INotificationManager mMockINotificationManager;
@Mock private PackageManager mMockPackageManager;
@Mock private NotificationBlockingHelperManager mBlockingHelperManager;
@Before
public void setUp() throws Exception {
mDependency.injectTestDependency(
NotificationBlockingHelperManager.class,
mBlockingHelperManager);
// Inflate the layout
final LayoutInflater layoutInflater = LayoutInflater.from(mContext);
mNotificationInfo = (NotificationInfo) layoutInflater.inflate(R.layout.notification_info,
@@ -311,7 +322,7 @@ public class NotificationInfoTest extends SysuiTestCase {
public void testbindNotification_BlockingHelper() throws Exception {
mNotificationInfo.bindNotification(mMockPackageManager, mMockINotificationManager,
TEST_PACKAGE_NAME, mNotificationChannel, 1, mSbn, null, null,
null, null, true);
null, null, false, true);
final TextView view = mNotificationInfo.findViewById(R.id.block_prompt);
assertEquals(View.VISIBLE, view.getVisibility());
assertEquals(mContext.getString(R.string.inline_blocking_helper), view.getText());
@@ -385,6 +396,27 @@ public class NotificationInfoTest extends SysuiTestCase {
assertEquals(IMPORTANCE_UNSPECIFIED, mNotificationChannel.getImportance());
}
@Test
public void testCloseControls_blockingHelperDismissedIfShown() throws Exception {
mNotificationInfo.bindNotification(
mMockPackageManager,
mMockINotificationManager,
TEST_PACKAGE_NAME,
mNotificationChannel,
1 /* numChannels */,
mSbn,
null /* checkSaveListener */,
null /* onSettingsClick */,
null /* onAppSettingsClick */,
null /* nonBlockablePkgs */,
true /* isForBlockingHelper */,
false /* isUserSentimentNegative */);
mNotificationInfo.closeControls(mNotificationInfo);
verify(mBlockingHelperManager).dismissCurrentBlockingHelper();
}
@Test
public void testNonBlockableAppDoesNotBecomeBlocked() throws Exception {
mNotificationChannel.setImportance(IMPORTANCE_LOW);

View File

@@ -16,6 +16,7 @@
package com.android.systemui.statusbar;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.Instrumentation;
import android.app.Notification;
@@ -23,33 +24,32 @@ import android.content.Context;
import android.os.UserHandle;
import android.service.notification.StatusBarNotification;
import android.support.test.InstrumentationRegistry;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.widget.FrameLayout;
import android.widget.RemoteViews;
import com.android.systemui.R;
import com.android.systemui.statusbar.notification.AboveShelfChangedListener;
import com.android.systemui.statusbar.notification.AboveShelfObserver;
import com.android.systemui.statusbar.notification.InflationException;
import com.android.systemui.statusbar.notification.NotificationInflaterTest;
import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.policy.HeadsUpManager;
/**
* A helper class to create {@link ExpandableNotificationRow}
* A helper class to create {@link ExpandableNotificationRow} (for both individual and group
* notifications).
*/
public class NotificationTestHelper {
static final String PKG = "com.android.systemui";
static final int UID = 1000;
private static final String GROUP_KEY = "gruKey";
private final Context mContext;
private final Instrumentation mInstrumentation;
private int mId;
private final NotificationGroupManager mGroupManager = new NotificationGroupManager();
private ExpandableNotificationRow mRow;
private InflationException mException;
private HeadsUpManager mHeadsUpManager;
protected static final String PKG = "com.android.systemui";
protected static final int UID = 1000;
public NotificationTestHelper(Context context) {
mContext = context;
@@ -57,57 +57,125 @@ public class NotificationTestHelper {
mHeadsUpManager = new HeadsUpManagerPhone(mContext, null, mGroupManager, null, null);
}
public ExpandableNotificationRow createRow() throws Exception {
return createRow(PKG, UID);
}
public ExpandableNotificationRow createRow(String pkg, int uid) throws Exception {
return createRow(pkg, uid, false /* isGroupSummary */, null /* groupKey */);
}
public ExpandableNotificationRow createRow(Notification notification) throws Exception {
return generateRow(notification, PKG, UID, false /* isGroupRow */);
}
/**
* Returns an {@link ExpandableNotificationRow} group with the given number of child
* notifications.
*/
public ExpandableNotificationRow createGroup(int numChildren) throws Exception {
ExpandableNotificationRow row = createGroupSummary(GROUP_KEY);
for (int i = 0; i < numChildren; i++) {
ExpandableNotificationRow childRow = createGroupChild(GROUP_KEY);
row.addChildNotification(childRow);
}
return row;
}
/** Returns a group notification with 2 child notifications. */
public ExpandableNotificationRow createGroup() throws Exception {
return createGroup(2);
}
private ExpandableNotificationRow createGroupSummary(String groupkey) throws Exception {
return createRow(PKG, UID, true /* isGroupSummary */, groupkey);
}
private ExpandableNotificationRow createGroupChild(String groupkey) throws Exception {
return createRow(PKG, UID, false /* isGroupSummary */, groupkey);
}
/**
* Creates a notification row with the given details.
*
* @param pkg package used for creating a {@link StatusBarNotification}
* @param uid uid used for creating a {@link StatusBarNotification}
* @param isGroupSummary whether the notification row is a group summary
* @param groupKey the group key for the notification group used across notifications
* @return a row with that's either a standalone notification or a group notification if the
* groupKey is non-null
* @throws Exception
*/
private ExpandableNotificationRow createRow(
String pkg,
int uid,
boolean isGroupSummary,
@Nullable String groupKey)
throws Exception {
Notification publicVersion = new Notification.Builder(mContext).setSmallIcon(
R.drawable.ic_person)
.setCustomContentView(new RemoteViews(mContext.getPackageName(),
R.layout.custom_view_dark))
.build();
Notification notification = new Notification.Builder(mContext).setSmallIcon(
R.drawable.ic_person)
.setContentTitle("Title")
.setContentText("Text")
.setPublicVersion(publicVersion)
.build();
return createRow(notification, pkg, uid);
Notification.Builder notificationBuilder =
new Notification.Builder(mContext)
.setSmallIcon(R.drawable.ic_person)
.setContentTitle("Title")
.setContentText("Text")
.setPublicVersion(publicVersion);
// Group notification setup
if (isGroupSummary) {
notificationBuilder.setGroupSummary(true);
}
if (!TextUtils.isEmpty(groupKey)) {
notificationBuilder.setGroup(groupKey);
}
return generateRow(notificationBuilder.build(), pkg, uid, !TextUtils.isEmpty(groupKey));
}
public ExpandableNotificationRow createRow() throws Exception {
return createRow(PKG, UID);
}
public ExpandableNotificationRow createRow(Notification notification) throws Exception {
return createRow(notification, PKG, UID);
}
public ExpandableNotificationRow createRow(Notification notification, String pkg, int uid)
private ExpandableNotificationRow generateRow(
Notification notification,
String pkg,
int uid,
boolean isGroupRow)
throws Exception {
LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
mContext.LAYOUT_INFLATER_SERVICE);
mInstrumentation.runOnMainSync(() -> {
mRow = (ExpandableNotificationRow) inflater.inflate(
R.layout.status_bar_notification_row,
null, false);
});
mInstrumentation.runOnMainSync(() ->
mRow = (ExpandableNotificationRow) inflater.inflate(
R.layout.status_bar_notification_row,
null /* root */,
false /* attachToRoot */)
);
ExpandableNotificationRow row = mRow;
row.setGroupManager(mGroupManager);
row.setHeadsUpManager(mHeadsUpManager);
row.setAboveShelfChangedListener(aboveShelf -> {});
UserHandle mUser = UserHandle.of(ActivityManager.getCurrentUser());
StatusBarNotification sbn = new StatusBarNotification(pkg, pkg, mId++, null, uid,
2000, notification, mUser, null, System.currentTimeMillis());
StatusBarNotification sbn = new StatusBarNotification(
pkg,
pkg,
mId++,
null /* tag */,
uid,
2000 /* initialPid */,
notification,
mUser,
null /* overrideGroupKey */,
System.currentTimeMillis());
NotificationData.Entry entry = new NotificationData.Entry(sbn);
entry.row = row;
entry.createIcons(mContext, sbn);
NotificationInflaterTest.runThenWaitForInflation(() -> row.updateNotification(entry),
NotificationInflaterTest.runThenWaitForInflation(
() -> row.updateNotification(entry),
row.getNotificationInflater());
return row;
}
public ExpandableNotificationRow createGroup() throws Exception {
ExpandableNotificationRow row = createRow();
row.addChildNotification(createRow());
row.addChildNotification(createRow());
// This would be done as part of onAsyncInflationFinished, but we skip large amounts of
// the callback chain, so we need to make up for not adding it to the group manager
// here.
mGroupManager.onEntryAdded(entry);
return row;
}
}

View File

@@ -16,7 +16,12 @@
package com.android.systemui.statusbar.stack;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -25,33 +30,73 @@ import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.view.View;
import com.android.systemui.ExpandHelper;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.TestableDependency;
import com.android.systemui.statusbar.EmptyShadeView;
import com.android.systemui.statusbar.FooterView;
import com.android.systemui.statusbar.NotificationBlockingHelperManager;
import com.android.systemui.statusbar.NotificationShelf;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.phone.ScrimController;
import com.android.systemui.statusbar.phone.StatusBar;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
/**
* Tests for {@link NotificationStackScrollLayout}.
*/
@SmallTest
@RunWith(AndroidJUnit4.class)
public class NotificationStackScrollLayoutTest extends SysuiTestCase {
private NotificationStackScrollLayout mStackScroller;
private StatusBar mBar;
@Rule public MockitoRule mockito = MockitoJUnit.rule();
@Mock private StatusBar mBar;
@Mock private HeadsUpManagerPhone mHeadsUpManager;
@Mock private NotificationBlockingHelperManager mBlockingHelperManager;
@Mock private NotificationGroupManager mGroupManager;
@Mock private ExpandHelper mExpandHelper;
@Mock private EmptyShadeView mEmptyShadeView;
@Before
@UiThreadTest
public void setUp() throws Exception {
// Inject dependencies before initializing the layout
mDependency.injectTestDependency(
NotificationBlockingHelperManager.class,
mBlockingHelperManager);
NotificationShelf notificationShelf = spy(new NotificationShelf(getContext(), null));
mStackScroller = new NotificationStackScrollLayout(getContext());
mBar = mock(StatusBar.class);
mStackScroller.setShelf(notificationShelf);
mStackScroller.setStatusBar(mBar);
mStackScroller.setScrimController(mock(ScrimController.class));
mStackScroller.setHeadsUpManager(mHeadsUpManager);
mStackScroller.setGroupManager(mGroupManager);
mStackScroller.setEmptyShadeView(mEmptyShadeView);
// Stub out functionality that isn't necessary to test.
doNothing().when(mBar)
.executeRunnableDismissingKeyguard(any(Runnable.class),
any(Runnable.class),
anyBoolean(),
anyBoolean(),
anyBoolean());
doNothing().when(mGroupManager).collapseAllGroups();
doNothing().when(mExpandHelper).cancelImmediately();
doNothing().when(notificationShelf).setAnimationsEnabled(anyBoolean());
}
@Test
@@ -75,26 +120,34 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase {
@Test
public void updateEmptyView_dndSuppressing() {
EmptyShadeView view = mock(EmptyShadeView.class);
mStackScroller.setEmptyShadeView(view);
when(view.willBeGone()).thenReturn(true);
when(mEmptyShadeView.willBeGone()).thenReturn(true);
when(mBar.areNotificationsHidden()).thenReturn(true);
mStackScroller.updateEmptyShadeView(true);
verify(view).setText(R.string.dnd_suppressing_shade_text);
verify(mEmptyShadeView).setText(R.string.dnd_suppressing_shade_text);
}
@Test
public void updateEmptyView_dndNotSuppressing() {
EmptyShadeView view = mock(EmptyShadeView.class);
mStackScroller.setEmptyShadeView(view);
when(view.willBeGone()).thenReturn(true);
mStackScroller.setEmptyShadeView(mEmptyShadeView);
when(mEmptyShadeView.willBeGone()).thenReturn(true);
when(mBar.areNotificationsHidden()).thenReturn(false);
mStackScroller.updateEmptyShadeView(true);
verify(view).setText(R.string.empty_shade_text);
verify(mEmptyShadeView).setText(R.string.empty_shade_text);
}
@Test
@UiThreadTest
public void testSetExpandedHeight_blockingHelperManagerReceivedCallbacks() {
mStackScroller.setExpandedHeight(0f);
verify(mBlockingHelperManager).setNotificationShadeExpanded(0f);
reset(mBlockingHelperManager);
mStackScroller.setExpandedHeight(100f);
verify(mBlockingHelperManager).setNotificationShadeExpanded(100f);
}
@Test