[SystemUI] Add notification to window feature.
Start apps intent through notification drag and drop. When user click the notification, its app activity was launched as a new task, but through this notification to window feature, user can launch app activity to popup(or split) window. Test: When get a HeadsUp notification > Long press it > open GUTS > drag beyond threshold Test: > start drag and drop > launch app as popup or split. Test: Open quick panel > long press any notification > open GUTS > drag beyond threshold Test: > start drag and drop > launch app as popup or split. Design doc : https://docs.google.com/document/d/1nr1-Wj3DLvOMZGP8X6hCekk7pbB2furvHML-gYormdc/edit?ts=6090accb#heading=h.1t3h5sf Bug: b/187095469 Signed-off-by: e.min <e.min@samsung.com> Change-Id: Id7cd114e1afb7f484a4fa3db615a82d92d01a645 Merged-In: Id7cd114e1afb7f484a4fa3db615a82d92d01a645
This commit is contained in:
@@ -671,4 +671,7 @@
|
||||
1 - Override the setting to always bypass keyguard
|
||||
2 - Override the setting to never bypass keyguard -->
|
||||
<integer name="config_face_unlock_bypass_override">0</integer>
|
||||
|
||||
<!-- Flag to activate notification to contents feature -->
|
||||
<bool name="config_notificationToContents">false</bool>
|
||||
</resources>
|
||||
|
||||
@@ -1591,4 +1591,6 @@
|
||||
<!-- The padding between the icon and the text. -->
|
||||
<dimen name="ongoing_call_chip_icon_text_padding">4dp</dimen>
|
||||
<dimen name="ongoing_call_chip_corner_radius">28dp</dimen>
|
||||
|
||||
<dimen name="drag_and_drop_icon_size">70dp</dimen>
|
||||
</resources>
|
||||
|
||||
@@ -25,6 +25,8 @@ import android.animation.ValueAnimator;
|
||||
import android.animation.ValueAnimator.AnimatorUpdateListener;
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.RectF;
|
||||
import android.os.Handler;
|
||||
@@ -73,6 +75,9 @@ public class SwipeHelper implements Gefingerpoken {
|
||||
private final FlingAnimationUtils mFlingAnimationUtils;
|
||||
private float mPagingTouchSlop;
|
||||
private final float mSlopMultiplier;
|
||||
private int mTouchSlop;
|
||||
private float mTouchSlopMultiplier;
|
||||
|
||||
private final Callback mCallback;
|
||||
private final int mSwipeDirection;
|
||||
private final VelocityTracker mVelocityTracker;
|
||||
@@ -105,6 +110,10 @@ public class SwipeHelper implements Gefingerpoken {
|
||||
final int y = (int) mDownLocation[1] - mViewOffset[1];
|
||||
mTouchedView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
|
||||
((ExpandableNotificationRow) mTouchedView).doLongClickCallback(x, y);
|
||||
|
||||
if (isAvailableToDragAndDrop(mTouchedView)) {
|
||||
mCallback.onLongPressSent(mTouchedView);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -126,6 +135,8 @@ public class SwipeHelper implements Gefingerpoken {
|
||||
mVelocityTracker = VelocityTracker.obtain();
|
||||
mPagingTouchSlop = viewConfiguration.getScaledPagingTouchSlop();
|
||||
mSlopMultiplier = viewConfiguration.getScaledAmbiguousGestureMultiplier();
|
||||
mTouchSlop = viewConfiguration.getScaledTouchSlop();
|
||||
mTouchSlopMultiplier = viewConfiguration.getAmbiguousGestureMultiplier();
|
||||
|
||||
// Extra long-press!
|
||||
mLongPressTimeout = (long) (ViewConfiguration.getLongPressTimeout() * 1.5f);
|
||||
@@ -297,7 +308,9 @@ public class SwipeHelper implements Gefingerpoken {
|
||||
mIsSwiping = false;
|
||||
mSnappingChild = false;
|
||||
mLongPressSent = false;
|
||||
mCallback.onLongPressSent(null);
|
||||
mVelocityTracker.clear();
|
||||
cancelLongPress();
|
||||
mTouchedView = mCallback.getChildAtPosition(ev);
|
||||
|
||||
if (mTouchedView != null) {
|
||||
@@ -349,6 +362,7 @@ public class SwipeHelper implements Gefingerpoken {
|
||||
mIsSwiping = false;
|
||||
mTouchedView = null;
|
||||
mLongPressSent = false;
|
||||
mCallback.onLongPressSent(null);
|
||||
mMenuRowIntercepting = false;
|
||||
cancelLongPress();
|
||||
if (captured) return true;
|
||||
@@ -593,11 +607,7 @@ public class SwipeHelper implements Gefingerpoken {
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent ev) {
|
||||
if (mLongPressSent && !mMenuRowIntercepting) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!mIsSwiping && !mMenuRowIntercepting) {
|
||||
if (!mIsSwiping && !mMenuRowIntercepting && !mLongPressSent) {
|
||||
if (mCallback.getChildAtPosition(ev) != null) {
|
||||
// We are dragging directly over a card, make sure that we also catch the gesture
|
||||
// even if nobody else wants the touch event.
|
||||
@@ -623,30 +633,40 @@ public class SwipeHelper implements Gefingerpoken {
|
||||
if (absDelta >= getFalsingThreshold()) {
|
||||
mTouchAboveFalsingThreshold = true;
|
||||
}
|
||||
// don't let items that can't be dismissed be dragged more than
|
||||
// maxScrollDistance
|
||||
if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissedInDirection(
|
||||
mTouchedView,
|
||||
delta > 0)) {
|
||||
float size = getSize(mTouchedView);
|
||||
float maxScrollDistance = MAX_SCROLL_SIZE_FRACTION * size;
|
||||
if (absDelta >= size) {
|
||||
delta = delta > 0 ? maxScrollDistance : -maxScrollDistance;
|
||||
} else {
|
||||
int startPosition = mCallback.getConstrainSwipeStartPosition();
|
||||
if (absDelta > startPosition) {
|
||||
int signedStartPosition =
|
||||
(int) (startPosition * Math.signum(delta));
|
||||
delta = signedStartPosition
|
||||
+ maxScrollDistance * (float) Math.sin(
|
||||
((delta - signedStartPosition) / size) * (Math.PI / 2));
|
||||
|
||||
if (mLongPressSent) {
|
||||
if (absDelta >= getTouchSlop(ev)) {
|
||||
if (mTouchedView instanceof ExpandableNotificationRow) {
|
||||
((ExpandableNotificationRow) mTouchedView)
|
||||
.doDragCallback(ev.getX(), ev.getY());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// don't let items that can't be dismissed be dragged more than
|
||||
// maxScrollDistance
|
||||
if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissedInDirection(
|
||||
mTouchedView,
|
||||
delta > 0)) {
|
||||
float size = getSize(mTouchedView);
|
||||
float maxScrollDistance = MAX_SCROLL_SIZE_FRACTION * size;
|
||||
if (absDelta >= size) {
|
||||
delta = delta > 0 ? maxScrollDistance : -maxScrollDistance;
|
||||
} else {
|
||||
int startPosition = mCallback.getConstrainSwipeStartPosition();
|
||||
if (absDelta > startPosition) {
|
||||
int signedStartPosition =
|
||||
(int) (startPosition * Math.signum(delta));
|
||||
delta = signedStartPosition
|
||||
+ maxScrollDistance * (float) Math.sin(
|
||||
((delta - signedStartPosition) / size) * (Math.PI / 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setTranslation(mTouchedView, mTranslation + delta);
|
||||
updateSwipeProgressFromOffset(mTouchedView, mCanCurrViewBeDimissed);
|
||||
onMoveUpdate(mTouchedView, ev, mTranslation + delta, delta);
|
||||
setTranslation(mTouchedView, mTranslation + delta);
|
||||
updateSwipeProgressFromOffset(mTouchedView, mCanCurrViewBeDimissed);
|
||||
onMoveUpdate(mTouchedView, ev, mTranslation + delta, delta);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
@@ -747,6 +767,29 @@ public class SwipeHelper implements Gefingerpoken {
|
||||
mIsSwiping = false;
|
||||
}
|
||||
|
||||
private float getTouchSlop(MotionEvent event) {
|
||||
// Adjust the touch slop if another gesture may be being performed.
|
||||
return event.getClassification() == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE
|
||||
? mTouchSlop * mTouchSlopMultiplier
|
||||
: mTouchSlop;
|
||||
}
|
||||
|
||||
private boolean isAvailableToDragAndDrop(View v) {
|
||||
if (v.getResources().getBoolean(R.bool.config_notificationToContents)) {
|
||||
if (v instanceof ExpandableNotificationRow) {
|
||||
ExpandableNotificationRow enr = (ExpandableNotificationRow) v;
|
||||
boolean canBubble = enr.getEntry().canBubble();
|
||||
Notification notif = enr.getEntry().getSbn().getNotification();
|
||||
PendingIntent dragIntent = notif.contentIntent != null ? notif.contentIntent
|
||||
: notif.fullScreenIntent;
|
||||
if (dragIntent != null && dragIntent.isActivity() && !canBubble) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
View getChildAtPosition(MotionEvent ev);
|
||||
|
||||
@@ -770,6 +813,13 @@ public class SwipeHelper implements Gefingerpoken {
|
||||
|
||||
void onDragCancelled(View v);
|
||||
|
||||
/**
|
||||
* Called when the child is long pressed and available to start drag and drop.
|
||||
*
|
||||
* @param v the view that was long pressed.
|
||||
*/
|
||||
void onLongPressSent(View v);
|
||||
|
||||
/**
|
||||
* Called when the child is snapped to a position.
|
||||
*
|
||||
|
||||
@@ -20,6 +20,7 @@ import android.content.Intent;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
import android.view.View;
|
||||
|
||||
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
|
||||
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
|
||||
|
||||
/**
|
||||
@@ -37,6 +38,9 @@ public interface NotificationActivityStarter {
|
||||
/** Called when the user clicks "Manage" or "History" in the Shade. */
|
||||
void startHistoryIntent(View view, boolean showHistory);
|
||||
|
||||
/** Called when the user succeed to drop notification to proper target view. */
|
||||
void onDragSuccess(NotificationEntry entry);
|
||||
|
||||
default boolean isCollapsingToShowActivityOverLockscreen() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,14 @@ public final class NotificationClicker implements View.OnClickListener {
|
||||
private final Optional<Bubbles> mBubblesOptional;
|
||||
private final NotificationActivityStarter mNotificationActivityStarter;
|
||||
|
||||
private ExpandableNotificationRow.OnDragSuccessListener mOnDragSuccessListener =
|
||||
new ExpandableNotificationRow.OnDragSuccessListener() {
|
||||
@Override
|
||||
public void onDragSuccess(NotificationEntry entry) {
|
||||
mNotificationActivityStarter.onDragSuccess(entry);
|
||||
}
|
||||
};
|
||||
|
||||
private NotificationClicker(
|
||||
NotificationClickerLogger logger,
|
||||
Optional<StatusBar> statusBarOptional,
|
||||
@@ -111,8 +119,10 @@ public final class NotificationClicker implements View.OnClickListener {
|
||||
if (notification.contentIntent != null || notification.fullScreenIntent != null
|
||||
|| row.getEntry().isBubble()) {
|
||||
row.setOnClickListener(this);
|
||||
row.setOnDragSuccessListener(mOnDragSuccessListener);
|
||||
} else {
|
||||
row.setOnClickListener(null);
|
||||
row.setOnDragSuccessListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.RectF;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.MathUtils;
|
||||
@@ -139,6 +140,8 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView
|
||||
private boolean mIsHeadsUpAnimation;
|
||||
private int mHeadsUpAddStartLocation;
|
||||
private float mHeadsUpLocation;
|
||||
/* In order to track headsup longpress coorindate. */
|
||||
protected Point mTargetPoint;
|
||||
private boolean mIsAppearing;
|
||||
private boolean mDismissed;
|
||||
private boolean mRefocusOnDismiss;
|
||||
@@ -568,8 +571,19 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView
|
||||
final int actualHeight = getActualHeight();
|
||||
float bottom = actualHeight * interpolatedFraction;
|
||||
|
||||
setOutlineRect(0, mAppearAnimationTranslation, getWidth(),
|
||||
bottom + mAppearAnimationTranslation);
|
||||
if (mTargetPoint != null) {
|
||||
int width = getWidth();
|
||||
float fraction = 1 - mAppearAnimationFraction;
|
||||
|
||||
setOutlineRect(mTargetPoint.x * fraction,
|
||||
mAnimationTranslationY
|
||||
+ (mAnimationTranslationY - mTargetPoint.y) * fraction,
|
||||
width - (width - mTargetPoint.x) * fraction,
|
||||
actualHeight - (actualHeight - mTargetPoint.y) * fraction);
|
||||
} else {
|
||||
setOutlineRect(0, mAppearAnimationTranslation, getWidth(),
|
||||
bottom + mAppearAnimationTranslation);
|
||||
}
|
||||
}
|
||||
|
||||
private float getInterpolatedAppearAnimationFraction() {
|
||||
|
||||
@@ -37,6 +37,7 @@ import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.drawable.AnimatedVectorDrawable;
|
||||
import android.graphics.drawable.AnimationDrawable;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
@@ -260,6 +261,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
|
||||
// Use #setLongPressPosition to optionally assign positional data with the long press.
|
||||
private LongPressListener mLongPressListener;
|
||||
|
||||
private ExpandableNotificationRowDragController mDragController;
|
||||
|
||||
private boolean mGroupExpansionChanging;
|
||||
|
||||
/**
|
||||
@@ -331,6 +334,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
|
||||
}
|
||||
};
|
||||
private OnClickListener mOnClickListener;
|
||||
private OnDragSuccessListener mOnDragSuccessListener;
|
||||
private boolean mHeadsupDisappearRunning;
|
||||
private View mChildAfterViewWhenDismissed;
|
||||
private View mGroupParentWhenDismissed;
|
||||
@@ -1083,6 +1087,10 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
|
||||
mLongPressListener = longPressListener;
|
||||
}
|
||||
|
||||
public void setDragController(ExpandableNotificationRowDragController dragController) {
|
||||
mDragController = dragController;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnClickListener(@Nullable OnClickListener l) {
|
||||
super.setOnClickListener(l);
|
||||
@@ -1329,6 +1337,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
|
||||
public void dismiss(boolean refocusOnDismiss) {
|
||||
super.dismiss(refocusOnDismiss);
|
||||
setLongPressListener(null);
|
||||
setDragController(null);
|
||||
mGroupParentWhenDismissed = mNotificationParent;
|
||||
mChildAfterViewWhenDismissed = null;
|
||||
mEntry.getIcons().getStatusBarIcon().setDismissed();
|
||||
@@ -1637,6 +1646,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
|
||||
}
|
||||
onHeightReset();
|
||||
requestLayout();
|
||||
|
||||
setTargetPoint(null);
|
||||
}
|
||||
|
||||
public void showFeedbackIcon(boolean show, Pair<Integer, Integer> resIds) {
|
||||
@@ -1727,6 +1738,29 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
|
||||
mTranslateableViews.remove(mGutsStub);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called once when starting drag motion after opening notification guts,
|
||||
* in case of notification that has {@link android.app.Notification#contentIntent}
|
||||
* and it is to start an activity.
|
||||
*/
|
||||
public void doDragCallback(float x, float y) {
|
||||
if (mDragController != null) {
|
||||
setTargetPoint(new Point((int) x, (int) y));
|
||||
mDragController.startDragAndDrop(this);
|
||||
}
|
||||
}
|
||||
|
||||
public void setOnDragSuccessListener(OnDragSuccessListener listener) {
|
||||
mOnDragSuccessListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a notification is dropped on proper target window.
|
||||
*/
|
||||
public void dragAndDropSuccess() {
|
||||
mOnDragSuccessListener.onDragSuccess(getEntry());
|
||||
}
|
||||
|
||||
private void doLongClickCallback() {
|
||||
doLongClickCallback(getWidth() / 2, getHeight() / 2);
|
||||
}
|
||||
@@ -3254,6 +3288,16 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
|
||||
boolean onLongPress(View v, int x, int y, MenuItem item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when notification drag and drop is finished successfully.
|
||||
*/
|
||||
public interface OnDragSuccessListener {
|
||||
/**
|
||||
* @param entry NotificationEntry that succeed to drop on proper target window.
|
||||
*/
|
||||
void onDragSuccess(NotificationEntry entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent to View.OnClickListener with coordinates
|
||||
*/
|
||||
@@ -3321,4 +3365,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setTargetPoint(Point p) {
|
||||
mTargetPoint = p;
|
||||
}
|
||||
public Point getTargetPoint() {
|
||||
return mTargetPoint;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.classifier.FalsingCollector;
|
||||
import com.android.systemui.plugins.FalsingManager;
|
||||
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
|
||||
@@ -85,6 +86,8 @@ public class ExpandableNotificationRowController implements NodeController {
|
||||
private final PeopleNotificationIdentifier mPeopleNotificationIdentifier;
|
||||
private final Optional<BubblesManager> mBubblesManagerOptional;
|
||||
|
||||
private final ExpandableNotificationRowDragController mDragController;
|
||||
|
||||
@Inject
|
||||
public ExpandableNotificationRowController(
|
||||
ExpandableNotificationRow view,
|
||||
@@ -109,7 +112,8 @@ public class ExpandableNotificationRowController implements NodeController {
|
||||
FalsingManager falsingManager,
|
||||
FalsingCollector falsingCollector,
|
||||
PeopleNotificationIdentifier peopleNotificationIdentifier,
|
||||
Optional<BubblesManager> bubblesManagerOptional) {
|
||||
Optional<BubblesManager> bubblesManagerOptional,
|
||||
ExpandableNotificationRowDragController dragController) {
|
||||
mView = view;
|
||||
mListContainer = listContainer;
|
||||
mActivatableNotificationViewController = activatableNotificationViewController;
|
||||
@@ -134,6 +138,7 @@ public class ExpandableNotificationRowController implements NodeController {
|
||||
mFalsingCollector = falsingCollector;
|
||||
mPeopleNotificationIdentifier = peopleNotificationIdentifier;
|
||||
mBubblesManagerOptional = bubblesManagerOptional;
|
||||
mDragController = dragController;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -164,6 +169,10 @@ public class ExpandableNotificationRowController implements NodeController {
|
||||
);
|
||||
mView.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
|
||||
if (mAllowLongPress) {
|
||||
if (mView.getResources().getBoolean(R.bool.config_notificationToContents)) {
|
||||
mView.setDragController(mDragController);
|
||||
}
|
||||
|
||||
mView.setLongPressListener((v, x, y, item) -> {
|
||||
if (mView.isSummaryWithChildren()) {
|
||||
mView.expandNotification();
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package com.android.systemui.statusbar.notification.row;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipDescription;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
import android.util.Log;
|
||||
import android.view.DragEvent;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.android.systemui.Dependency;
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.statusbar.CommandQueue;
|
||||
import com.android.systemui.statusbar.phone.ShadeController;
|
||||
import com.android.systemui.statusbar.policy.HeadsUpManager;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
/**
|
||||
* Controller for Notification to window.
|
||||
*/
|
||||
public class ExpandableNotificationRowDragController {
|
||||
private static final String TAG = ExpandableNotificationRowDragController.class.getSimpleName();
|
||||
private int mIconSize;
|
||||
|
||||
private final Context mContext;
|
||||
private final HeadsUpManager mHeadsUpManager;
|
||||
|
||||
@Inject
|
||||
public ExpandableNotificationRowDragController(Context context,
|
||||
HeadsUpManager headsUpManager) {
|
||||
mContext = context;
|
||||
mHeadsUpManager = headsUpManager;
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
mIconSize = mContext.getResources().getDimensionPixelSize(R.dimen.drag_and_drop_icon_size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when drag event beyond the touchslop,
|
||||
* and start drag and drop.
|
||||
*
|
||||
* @param view notification that was long pressed and started to drag and drop.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public void startDragAndDrop(View view) {
|
||||
ExpandableNotificationRow enr = null;
|
||||
if (view instanceof ExpandableNotificationRow) {
|
||||
enr = (ExpandableNotificationRow) view;
|
||||
}
|
||||
|
||||
StatusBarNotification sn = enr.getEntry().getSbn();
|
||||
Notification notification = sn.getNotification();
|
||||
final PendingIntent contentIntent = notification.contentIntent != null
|
||||
? notification.contentIntent
|
||||
: notification.fullScreenIntent;
|
||||
Bitmap iconBitmap = getBitmapFromDrawable(
|
||||
getPkgIcon(enr.getEntry().getSbn().getPackageName()));
|
||||
|
||||
final ImageView snapshot = new ImageView(mContext);
|
||||
snapshot.setImageBitmap(iconBitmap);
|
||||
snapshot.layout(0, 0, mIconSize, mIconSize);
|
||||
|
||||
ClipDescription clipDescription = new ClipDescription("Drag And Drop",
|
||||
new String[]{ClipDescription.MIMETYPE_APPLICATION_ACTIVITY});
|
||||
Intent dragIntent = new Intent();
|
||||
dragIntent.putExtra("android.intent.extra.PENDING_INTENT", contentIntent);
|
||||
dragIntent.putExtra(Intent.EXTRA_USER, android.os.Process.myUserHandle());
|
||||
ClipData.Item item = new ClipData.Item(dragIntent);
|
||||
ClipData dragData = new ClipData(clipDescription, item);
|
||||
View.DragShadowBuilder myShadow = new View.DragShadowBuilder(snapshot);
|
||||
view.setOnDragListener(getDraggedViewDragListener());
|
||||
view.startDragAndDrop(dragData, myShadow, null, View.DRAG_FLAG_GLOBAL);
|
||||
}
|
||||
|
||||
|
||||
private Drawable getPkgIcon(String pkgName) {
|
||||
Drawable pkgicon = null;
|
||||
PackageManager pm = mContext.getPackageManager();
|
||||
ApplicationInfo info;
|
||||
try {
|
||||
info = pm.getApplicationInfo(
|
||||
pkgName,
|
||||
PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||
| PackageManager.MATCH_DISABLED_COMPONENTS
|
||||
| PackageManager.MATCH_DIRECT_BOOT_UNAWARE
|
||||
| PackageManager.MATCH_DIRECT_BOOT_AWARE);
|
||||
if (info != null) {
|
||||
pkgicon = pm.getApplicationIcon(info);
|
||||
} else {
|
||||
Log.d(TAG, " application info is null ");
|
||||
pkgicon = pm.getDefaultActivityIcon();
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
Log.d(TAG, "can not find package with : " + pkgName);
|
||||
pkgicon = pm.getDefaultActivityIcon();
|
||||
}
|
||||
|
||||
return pkgicon;
|
||||
}
|
||||
|
||||
private Bitmap getBitmapFromDrawable(@NonNull Drawable drawable) {
|
||||
final Bitmap bmp = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
|
||||
drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
|
||||
final Canvas canvas = new Canvas(bmp);
|
||||
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
|
||||
drawable.draw(canvas);
|
||||
return bmp;
|
||||
}
|
||||
|
||||
private View.OnDragListener getDraggedViewDragListener() {
|
||||
return (view, dragEvent) -> {
|
||||
switch (dragEvent.getAction()) {
|
||||
case DragEvent.ACTION_DRAG_STARTED:
|
||||
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
|
||||
if (view instanceof ExpandableNotificationRow) {
|
||||
ExpandableNotificationRow enr = (ExpandableNotificationRow) view;
|
||||
if (enr.isPinned()) {
|
||||
mHeadsUpManager.releaseAllImmediately();
|
||||
} else {
|
||||
Dependency.get(ShadeController.class).animateCollapsePanels(
|
||||
CommandQueue.FLAG_EXCLUDE_NONE, true /* force */);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
case DragEvent.ACTION_DRAG_ENDED:
|
||||
if (dragEvent.getResult()) {
|
||||
if (view instanceof ExpandableNotificationRow) {
|
||||
ExpandableNotificationRow enr = (ExpandableNotificationRow) view;
|
||||
enr.dragAndDropSuccess();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -183,6 +183,8 @@ public class NotificationStackScrollLayoutController {
|
||||
private int mBarState;
|
||||
private HeadsUpAppearanceController mHeadsUpAppearanceController;
|
||||
|
||||
private View mLongPressedView;
|
||||
|
||||
private final NotificationListContainerImpl mNotificationListContainer =
|
||||
new NotificationListContainerImpl();
|
||||
|
||||
@@ -491,6 +493,11 @@ public class NotificationStackScrollLayoutController {
|
||||
return child;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongPressSent(View v) {
|
||||
mLongPressedView = v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBeginDrag(View v) {
|
||||
mFalsingCollector.onNotificationStartDismissing();
|
||||
@@ -1425,6 +1432,10 @@ public class NotificationStackScrollLayoutController {
|
||||
return mDynamicPrivacyController.isInLockedDownShade();
|
||||
}
|
||||
|
||||
public boolean isLongPressInProgress() {
|
||||
return mLongPressedView != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the dimmed state for all of the notification views.
|
||||
*/
|
||||
@@ -1679,17 +1690,23 @@ public class NotificationStackScrollLayoutController {
|
||||
mView.handleEmptySpaceClick(ev);
|
||||
|
||||
NotificationGuts guts = mNotificationGutsManager.getExposedGuts();
|
||||
|
||||
boolean longPressWantsIt = false;
|
||||
if (mLongPressedView != null) {
|
||||
longPressWantsIt = mSwipeHelper.onInterceptTouchEvent(ev);
|
||||
}
|
||||
boolean expandWantsIt = false;
|
||||
if (!mSwipeHelper.isSwiping()
|
||||
if (mLongPressedView == null && !mSwipeHelper.isSwiping()
|
||||
&& !mView.getOnlyScrollingInThisMotion() && guts == null) {
|
||||
expandWantsIt = mView.getExpandHelper().onInterceptTouchEvent(ev);
|
||||
}
|
||||
boolean scrollWantsIt = false;
|
||||
if (!mSwipeHelper.isSwiping() && !mView.isExpandingNotification()) {
|
||||
if (mLongPressedView == null && !mSwipeHelper.isSwiping()
|
||||
&& !mView.isExpandingNotification()) {
|
||||
scrollWantsIt = mView.onInterceptTouchEventScroll(ev);
|
||||
}
|
||||
boolean swipeWantsIt = false;
|
||||
if (!mView.isBeingDragged()
|
||||
if (mLongPressedView == null && !mView.isBeingDragged()
|
||||
&& !mView.isExpandingNotification()
|
||||
&& !mView.getExpandedInThisMotion()
|
||||
&& !mView.getOnlyScrollingInThisMotion()
|
||||
@@ -1717,7 +1734,7 @@ public class NotificationStackScrollLayoutController {
|
||||
InteractionJankMonitor.getInstance().begin(mView,
|
||||
CUJ_NOTIFICATION_SHADE_SCROLL_FLING);
|
||||
}
|
||||
return swipeWantsIt || scrollWantsIt || expandWantsIt;
|
||||
return swipeWantsIt || scrollWantsIt || expandWantsIt || longPressWantsIt;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1726,11 +1743,15 @@ public class NotificationStackScrollLayoutController {
|
||||
boolean isCancelOrUp = ev.getActionMasked() == MotionEvent.ACTION_CANCEL
|
||||
|| ev.getActionMasked() == MotionEvent.ACTION_UP;
|
||||
mView.handleEmptySpaceClick(ev);
|
||||
boolean longPressWantsIt = false;
|
||||
if (guts != null && mLongPressedView != null) {
|
||||
longPressWantsIt = mSwipeHelper.onTouchEvent(ev);
|
||||
}
|
||||
boolean expandWantsIt = false;
|
||||
boolean onlyScrollingInThisMotion = mView.getOnlyScrollingInThisMotion();
|
||||
boolean expandingNotification = mView.isExpandingNotification();
|
||||
if (mView.getIsExpanded() && !mSwipeHelper.isSwiping() && !onlyScrollingInThisMotion
|
||||
&& guts == null) {
|
||||
if (mLongPressedView == null && mView.getIsExpanded()
|
||||
&& !mSwipeHelper.isSwiping() && !onlyScrollingInThisMotion && guts == null) {
|
||||
ExpandHelper expandHelper = mView.getExpandHelper();
|
||||
if (isCancelOrUp) {
|
||||
expandHelper.onlyObserveMovements(false);
|
||||
@@ -1744,12 +1765,12 @@ public class NotificationStackScrollLayoutController {
|
||||
}
|
||||
}
|
||||
boolean scrollerWantsIt = false;
|
||||
if (mView.isExpanded() && !mSwipeHelper.isSwiping() && !expandingNotification
|
||||
&& !mView.getDisallowScrollingInThisMotion()) {
|
||||
if (mLongPressedView == null && mView.isExpanded() && !mSwipeHelper.isSwiping()
|
||||
&& !expandingNotification && !mView.getDisallowScrollingInThisMotion()) {
|
||||
scrollerWantsIt = mView.onScrollTouch(ev);
|
||||
}
|
||||
boolean horizontalSwipeWantsIt = false;
|
||||
if (!mView.isBeingDragged()
|
||||
if (mLongPressedView == null && !mView.isBeingDragged()
|
||||
&& !expandingNotification
|
||||
&& !mView.getExpandedInThisMotion()
|
||||
&& !onlyScrollingInThisMotion
|
||||
@@ -1775,7 +1796,7 @@ public class NotificationStackScrollLayoutController {
|
||||
mView.setCheckForLeaveBehind(true);
|
||||
}
|
||||
traceJankOnTouchEvent(ev.getActionMasked(), scrollerWantsIt);
|
||||
return horizontalSwipeWantsIt || scrollerWantsIt || expandWantsIt;
|
||||
return horizontalSwipeWantsIt || scrollerWantsIt || expandWantsIt || longPressWantsIt;
|
||||
}
|
||||
|
||||
private void traceJankOnTouchEvent(int action, boolean scrollerWantsIt) {
|
||||
|
||||
@@ -433,6 +433,7 @@ public class StackStateAnimator {
|
||||
if (row.isDismissed()) {
|
||||
needsAnimation = false;
|
||||
}
|
||||
|
||||
NotificationEntry entry = row.getEntry();
|
||||
StatusBarIconView icon = entry.getIcons().getStatusBarIcon();
|
||||
final StatusBarIconView centeredIcon = entry.getIcons().getCenteredIcon();
|
||||
@@ -442,7 +443,8 @@ public class StackStateAnimator {
|
||||
if (icon.getParent() != null) {
|
||||
icon.getLocationOnScreen(mTmpLocation);
|
||||
float iconPosition = mTmpLocation[0] - icon.getTranslationX()
|
||||
+ ViewState.getFinalTranslationX(icon) + icon.getWidth() * 0.25f;
|
||||
+ ViewState.getFinalTranslationX(icon)
|
||||
+ icon.getWidth() * 0.25f;
|
||||
mHostLayout.getLocationOnScreen(mTmpLocation);
|
||||
targetLocation = iconPosition - mTmpLocation[0];
|
||||
}
|
||||
|
||||
@@ -3928,7 +3928,9 @@ public class NotificationPanelViewController extends PanelViewController {
|
||||
if (mStatusBar.isBouncerShowing()) {
|
||||
return true;
|
||||
}
|
||||
if (mBar.panelEnabled() && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) {
|
||||
if (mBar.panelEnabled()
|
||||
&& !mNotificationStackScrollLayoutController.isLongPressInProgress()
|
||||
&& mHeadsUpTouchHelper.onInterceptTouchEvent(event)) {
|
||||
mMetricsLogger.count(COUNTER_PANEL_OPEN, 1);
|
||||
mMetricsLogger.count(COUNTER_PANEL_OPEN_PEEK, 1);
|
||||
return true;
|
||||
@@ -3980,6 +3982,7 @@ public class NotificationPanelViewController extends PanelViewController {
|
||||
return true;
|
||||
}
|
||||
if (mListenForHeadsUp && !mHeadsUpTouchHelper.isTrackingHeadsUp()
|
||||
&& !mNotificationStackScrollLayoutController.isLongPressInProgress()
|
||||
&& mHeadsUpTouchHelper.onInterceptTouchEvent(event)) {
|
||||
mMetricsLogger.count(COUNTER_PANEL_OPEN_PEEK, 1);
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ import com.android.systemui.statusbar.notification.collection.render.GroupMember
|
||||
import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider;
|
||||
import com.android.systemui.statusbar.notification.logging.NotificationLogger;
|
||||
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
|
||||
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowDragController;
|
||||
import com.android.systemui.statusbar.notification.row.OnUserInteractionCallback;
|
||||
import com.android.systemui.statusbar.policy.HeadsUpUtil;
|
||||
import com.android.systemui.statusbar.policy.KeyguardStateController;
|
||||
@@ -402,6 +403,53 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit
|
||||
mIsCollapsingToShowActivityOverLockscreen = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a notification is dropped on proper target window.
|
||||
* Intent that is included in this entry notification,
|
||||
* will be sent by {@link ExpandableNotificationRowDragController}
|
||||
*
|
||||
* @param entry notification entry that is dropped.
|
||||
*/
|
||||
@Override
|
||||
public void onDragSuccess(NotificationEntry entry) {
|
||||
// this method is not responsible for intent sending.
|
||||
// will focus follow operation only after drag-and-drop that notification.
|
||||
NotificationVisibility.NotificationLocation location =
|
||||
NotificationLogger.getNotificationLocation(entry);
|
||||
final NotificationVisibility nv = NotificationVisibility.obtain(entry.getKey(),
|
||||
entry.getRanking().getRank(), getVisibleNotificationsCount(), true, location);
|
||||
|
||||
// retrieve the group summary to remove with this entry before we tell NMS the
|
||||
// notification was clicked to avoid a race condition
|
||||
final boolean shouldAutoCancel = shouldAutoCancel(entry.getSbn());
|
||||
final NotificationEntry summaryToRemove = shouldAutoCancel
|
||||
? mOnUserInteractionCallback.getGroupSummaryToDismiss(entry) : null;
|
||||
|
||||
String notificationKey = entry.getKey();
|
||||
// inform NMS that the notification was clicked
|
||||
mClickNotifier.onNotificationClick(notificationKey, nv);
|
||||
|
||||
if (shouldAutoCancel || mRemoteInputManager.isNotificationKeptForRemoteInputHistory(
|
||||
notificationKey)) {
|
||||
// Immediately remove notification from visually showing.
|
||||
// We have to post the removal to the UI thread for synchronization.
|
||||
mMainThreadHandler.post(() -> {
|
||||
final Runnable removeNotification = () ->
|
||||
mOnUserInteractionCallback.onDismiss(
|
||||
entry, REASON_CLICK, summaryToRemove);
|
||||
if (mPresenter.isCollapsing()) {
|
||||
// To avoid lags we're only performing the remove
|
||||
// after the shade is collapsed
|
||||
mShadeController.addPostCollapseAction(removeNotification);
|
||||
} else {
|
||||
removeNotification.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
mIsCollapsingToShowActivityOverLockscreen = false;
|
||||
}
|
||||
|
||||
private void expandBubbleStackOnMainThread(NotificationEntry entry) {
|
||||
if (!mBubblesManagerOptional.isPresent()) {
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.systemui.statusbar.notification.row;
|
||||
|
||||
import static android.view.DragEvent.ACTION_DRAG_STARTED;
|
||||
|
||||
import android.content.Context;
|
||||
import android.testing.AndroidTestingRunner;
|
||||
import android.testing.TestableLooper;
|
||||
import android.testing.TestableLooper.RunWithLooper;
|
||||
import android.view.DragEvent;
|
||||
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.systemui.SysuiTestCase;
|
||||
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
|
||||
import com.android.systemui.statusbar.phone.ShadeController;
|
||||
import com.android.systemui.statusbar.policy.HeadsUpManager;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner.class)
|
||||
@RunWithLooper
|
||||
public class ExpandableNotificationRowDragControllerTest extends SysuiTestCase {
|
||||
|
||||
private ExpandableNotificationRow mRow;
|
||||
private ExpandableNotificationRow mGroupRow;
|
||||
private ExpandableNotificationRowDragController mController;
|
||||
private NotificationTestHelper mNotificationTestHelper;
|
||||
|
||||
private NotificationGutsManager mGutsManager = mock(NotificationGutsManager.class);
|
||||
private HeadsUpManager mHeadsUpManager = mock(HeadsUpManager.class);
|
||||
private NotificationMenuRow mMenuRow = mock(NotificationMenuRow.class);
|
||||
private NotificationMenuRowPlugin.MenuItem mMenuItem =
|
||||
mock(NotificationMenuRowPlugin.MenuItem.class);
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
allowTestableLooperAsMainThread();
|
||||
|
||||
mDependency.injectMockDependency(ShadeController.class);
|
||||
|
||||
mNotificationTestHelper = new NotificationTestHelper(
|
||||
mContext,
|
||||
mDependency,
|
||||
TestableLooper.get(this));
|
||||
mRow = mNotificationTestHelper.createRow();
|
||||
mGroupRow = mNotificationTestHelper.createGroup(4);
|
||||
when(mMenuRow.getLongpressMenuItem(any(Context.class))).thenReturn(mMenuItem);
|
||||
|
||||
mController = new ExpandableNotificationRowDragController(mContext, mHeadsUpManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoStartDragHeadsUpNotif_startDragAndDrop() throws Exception {
|
||||
ExpandableNotificationRowDragController controller = createSpyController();
|
||||
mRow.setDragController(controller);
|
||||
mRow.setHeadsUp(true);
|
||||
mRow.setPinned(true);
|
||||
|
||||
mRow.doLongClickCallback(0, 0);
|
||||
mRow.doDragCallback(0, 0);
|
||||
verify(controller).startDragAndDrop(mRow);
|
||||
|
||||
// Simulate the drag start
|
||||
mRow.dispatchDragEvent(DragEvent.obtain(ACTION_DRAG_STARTED, 0, 0, 0, 0, null, null, null,
|
||||
null, null, false));
|
||||
verify(mHeadsUpManager, times(1)).releaseAllImmediately();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoStartDragNotif() throws Exception {
|
||||
ExpandableNotificationRowDragController controller = createSpyController();
|
||||
mRow.setDragController(controller);
|
||||
|
||||
mDependency.get(ShadeController.class).instantExpandNotificationsPanel();
|
||||
mRow.doDragCallback(0, 0);
|
||||
verify(controller).startDragAndDrop(mRow);
|
||||
|
||||
// Simulate the drag start
|
||||
mRow.dispatchDragEvent(DragEvent.obtain(ACTION_DRAG_STARTED, 0, 0, 0, 0, null, null, null,
|
||||
null, null, false));
|
||||
verify(mDependency.get(ShadeController.class)).animateCollapsePanels(0, true);
|
||||
}
|
||||
|
||||
private ExpandableNotificationRowDragController createSpyController() {
|
||||
return spy(mController);
|
||||
}
|
||||
}
|
||||
@@ -265,7 +265,8 @@ public class NotificationEntryManagerInflationTest extends SysuiTestCase {
|
||||
new FalsingManagerFake(),
|
||||
new FalsingCollectorFake(),
|
||||
mPeopleNotificationIdentifier,
|
||||
Optional.of(mock(BubblesManager.class))
|
||||
Optional.of(mock(BubblesManager.class)),
|
||||
mock(ExpandableNotificationRowDragController.class)
|
||||
));
|
||||
|
||||
when(mNotificationRowComponentBuilder.activatableNotificationView(any()))
|
||||
|
||||
Reference in New Issue
Block a user