diff options
Diffstat (limited to 'src')
9 files changed, 368 insertions, 121 deletions
diff --git a/src/com/android/car/notification/CarHeadsUpNotificationManager.java b/src/com/android/car/notification/CarHeadsUpNotificationManager.java index 32943695..2d6a4935 100644 --- a/src/com/android/car/notification/CarHeadsUpNotificationManager.java +++ b/src/com/android/car/notification/CarHeadsUpNotificationManager.java @@ -25,8 +25,6 @@ import static com.android.car.assist.client.CarAssistUtils.isCarCompatibleMessag import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; -import android.annotation.NonNull; -import android.annotation.Nullable; import android.app.ActivityTaskManager; import android.app.KeyguardManager; import android.app.Notification; @@ -43,6 +41,8 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewTreeObserver; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.annotation.VisibleForTesting; @@ -56,6 +56,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledThreadPoolExecutor; /** @@ -108,12 +109,13 @@ public class CarHeadsUpNotificationManager private final KeyguardManager mKeyguardManager; private final PreprocessingManager mPreprocessingManager; private final LayoutInflater mInflater; - private final CarHeadsUpNotificationContainer mHunContainer; + @VisibleForTesting + final CarHeadsUpNotificationContainer mHunContainer; private final CarHeadsUpNotificationQueue.CarHeadsUpNotificationQueueCallback mCarHeadsUpNotificationQueueCallback; // key for the map is the statusbarnotification key - private final Map<String, HeadsUpEntry> mActiveHeadsUpNotifications = new HashMap<>(); + private final Map<String, HeadsUpEntry> mActiveHeadsUpNotifications = new ConcurrentHashMap<>(); private final List<OnHeadsUpNotificationStateChange> mNotificationStateChangeListeners = new ArrayList<>(); private final Map<HeadsUpEntry, @@ -282,6 +284,18 @@ public class CarHeadsUpNotificationManager mCarHeadsUpNotificationQueue.releaseQueue(); } + /** + * Clears all local cached variables and gracefully removes any heads up notification views if + * present. + */ + public void clearCache() { + mCarHeadsUpNotificationQueue.clearCache(); + for (AlertEntry alertEntry : mActiveHeadsUpNotifications.values()) { + resetHeadsUpEntry(alertEntry); + removeHeadsUpEntry(alertEntry, getHeadsUpView(alertEntry)); + } + } + private void scheduleRemoveHeadsUp(AlertEntry alertEntry) { HeadsUpEntry currentActiveHeadsUpNotification = getActiveHeadsUpEntry(alertEntry); @@ -477,7 +491,11 @@ public class CarHeadsUpNotificationManager // Add swipe gesture View cardView = notificationView.findViewById(R.id.card_view); cardView.setOnTouchListener(new HeadsUpNotificationOnTouchListener(cardView, - isHeadsUpDismissible(alertEntry), () -> resetView(alertEntry))); + isHeadsUpDismissible(alertEntry), () -> { + resetHeadsUpEntry(alertEntry); + removeHeadsUpEntry(alertEntry, getHeadsUpView(alertEntry)); + handleHeadsUpNotificationStateChanged(alertEntry, HeadsUpState.DISMISSED); + })); // Add dismiss button listener View dismissButton = notificationView.findViewById( @@ -574,57 +592,57 @@ public class CarHeadsUpNotificationManager */ private void dismissHun(AlertEntry alertEntry) { Log.d(TAG, "clearViews for Heads Up Notification: "); - if (!isActiveHun(alertEntry)) { - // View can also be removed when swiped away. - return; - } - // Get the current notification to perform animations and remove it immediately from the - // active notification maps and cancel all other call backs if any. - HeadsUpEntry currentHeadsUpNotification = getActiveHeadsUpEntry(alertEntry); - // view could already be in the process of being dismissed - if (currentHeadsUpNotification.mIsDismissing) { - return; - } - currentHeadsUpNotification.mIsDismissing = true; - currentHeadsUpNotification.getHandler().removeCallbacksAndMessages(null); - resetViewTreeListenersEntry(currentHeadsUpNotification); - View view = currentHeadsUpNotification.getNotificationView(); + resetHeadsUpEntry(alertEntry); + boolean isTaggedToBeRemoved = isActiveHun(alertEntry) + && getActiveHeadsUpEntry(alertEntry).mShouldRemove; + View view = getHeadsUpView(alertEntry); AnimatorSet animatorSet = mAnimationHelper.getAnimateOutAnimator(mContext, view); animatorSet.setTarget(view); animatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - mHunContainer.removeNotification(view); - // Remove HUN after the animation ends to prevent accidental touch on the card // triggering another remove call. - mActiveHeadsUpNotifications.remove(alertEntry.getKey()); + removeHeadsUpEntry(alertEntry, view); handleHeadsUpNotificationStateChanged(alertEntry, - currentHeadsUpNotification.mShouldRemove ? HeadsUpState.REMOVED_BY_SENDER + isTaggedToBeRemoved ? HeadsUpState.REMOVED_BY_SENDER : HeadsUpState.DISMISSED); } }); animatorSet.start(); } - /** - * Removes the view for the active heads up notification and also removes the HUN from the map - * of active Notifications. - */ - private void resetView(AlertEntry alertEntry) { + private void resetHeadsUpEntry(@NonNull AlertEntry alertEntry) { if (!isActiveHun(alertEntry)) { return; } HeadsUpEntry currentHeadsUpNotification = getActiveHeadsUpEntry(alertEntry); + // view could already be in the process of being dismissed + if (currentHeadsUpNotification.mIsDismissing) { + return; + } + currentHeadsUpNotification.mIsDismissing = true; currentHeadsUpNotification.getHandler().removeCallbacksAndMessages(null); - mHunContainer.removeNotification(currentHeadsUpNotification.getNotificationView()); - mActiveHeadsUpNotifications.remove(alertEntry.getKey()); - handleHeadsUpNotificationStateChanged(alertEntry, HeadsUpState.DISMISSED); resetViewTreeListenersEntry(currentHeadsUpNotification); } + @Nullable + private View getHeadsUpView(@NonNull AlertEntry alertEntry) { + if (!isActiveHun(alertEntry)) { + return null; + } + return getActiveHeadsUpEntry(alertEntry).getNotificationView(); + } + + private void removeHeadsUpEntry(@NonNull AlertEntry alertEntry, @Nullable View view) { + if (view != null) { + mHunContainer.removeNotification(view); + } + mActiveHeadsUpNotifications.remove(alertEntry.getKey()); + } + /** * Helper method that determines whether a notification should show as a heads-up. * @@ -774,4 +792,9 @@ public class CarHeadsUpNotificationManager void setCarHeadsUpNotificationQueue(CarHeadsUpNotificationQueue carHeadsUpNotificationQueue) { mCarHeadsUpNotificationQueue = carHeadsUpNotificationQueue; } + + @VisibleForTesting + void addActiveHeadsUpNotification(HeadsUpEntry headsUpEntry) { + mActiveHeadsUpNotifications.put(headsUpEntry.getKey(), headsUpEntry); + } } diff --git a/src/com/android/car/notification/CarHeadsUpNotificationQueue.java b/src/com/android/car/notification/CarHeadsUpNotificationQueue.java index 9fe4d83d..dfede430 100644 --- a/src/com/android/car/notification/CarHeadsUpNotificationQueue.java +++ b/src/com/android/car/notification/CarHeadsUpNotificationQueue.java @@ -331,6 +331,18 @@ public class CarHeadsUpNotificationQueue implements } /** + * Clears all local cached variables and cancels scheduled executor tasks. + */ + public void clearCache() { + mPriorityQueue.clear(); + mKeyToAlertEntryMap.clear(); + mThrottledDisplays.clear(); + if (mScheduledFuture != null) { + mScheduledFuture.cancel(/* mayInterruptIfRunning= */ true); + } + } + + /** * Callback to communicate status of HUN. */ public interface CarHeadsUpNotificationQueueCallback { diff --git a/src/com/android/car/notification/CarNotificationItemTouchListener.java b/src/com/android/car/notification/CarNotificationItemTouchListener.java index 908baaa2..8bfa3403 100644 --- a/src/com/android/car/notification/CarNotificationItemTouchListener.java +++ b/src/com/android/car/notification/CarNotificationItemTouchListener.java @@ -50,6 +50,11 @@ import java.util.concurrent.TimeUnit; * and resistant swiping for undismissible notifications. */ public class CarNotificationItemTouchListener extends RecyclerView.SimpleOnItemTouchListener { + /** + * The unit of velocity in milliseconds. A value of 1 means "pixels per millisecond", + * 1000 means "pixels per 1000 milliseconds (1 second)". + */ + private static final int PIXELS_PER_SECOND = (int) TimeUnit.SECONDS.toMillis(1); private static final String TAG = "CarNotificationItemTouchListener"; private static final boolean DEBUG = Build.IS_ENG || Build.IS_USERDEBUG; @@ -145,9 +150,10 @@ public class CarNotificationItemTouchListener extends RecyclerView.SimpleOnItemT mErrorFactorMultiplier = res.getFloat(R.dimen.error_factor_multiplier); mFlingPercentageOfWidthToDismiss = - res.getFloat(R.dimen.fling_percentage_of_width_to_dismiss); + res.getFloat(R.dimen.fling_percentage_of_max_translation_to_dismiss); - mPercentageOfWidthToDismiss = res.getFloat(R.dimen.percentage_of_width_to_dismiss); + mPercentageOfWidthToDismiss = + res.getFloat(R.dimen.percentage_of_max_translation_to_dismiss); mMinVelocityForSwipeDirection = res.getInteger(R.integer.min_velocity_for_swipe_direction_detection); @@ -291,8 +297,7 @@ public class CarNotificationItemTouchListener extends RecyclerView.SimpleOnItemT return false; } - View innerNotificationList = mViewHolder.itemView - .requireViewById(R.id.notification_list); + View innerNotificationList = mViewHolder.itemView.requireViewById(R.id.notification_list); int[] screenXY = {0, 0}; innerNotificationList.getLocationOnScreen(screenXY); int top = screenXY[1]; @@ -337,9 +342,7 @@ public class CarNotificationItemTouchListener extends RecyclerView.SimpleOnItemT break; } - mVelocityTracker.computeCurrentVelocity( - (int) TimeUnit.SECONDS.toMillis(1) /* pixels/second */, - mMaximumFlingVelocity); + mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND, mMaximumFlingVelocity); float velocityX = getLastComputedXVelocity(); float translationX = mViewHolder.getSwipeTranslationX(); diff --git a/src/com/android/car/notification/CarNotificationListener.java b/src/com/android/car/notification/CarNotificationListener.java index 0cc28edb..3d1b3dc9 100644 --- a/src/com/android/car/notification/CarNotificationListener.java +++ b/src/com/android/car/notification/CarNotificationListener.java @@ -15,7 +15,6 @@ */ package com.android.car.notification; -import android.annotation.Nullable; import android.app.NotificationManager; import android.content.ComponentName; import android.content.Context; @@ -31,6 +30,7 @@ import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; import android.util.Log; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.car.notification.headsup.CarHeadsUpNotificationAppContainer; @@ -61,6 +61,7 @@ public class CarNotificationListener extends NotificationListenerService impleme private CarHeadsUpNotificationManager mHeadsUpManager; private NotificationDataManager mNotificationDataManager; private boolean mIsNotificationPanelVisible; + private boolean mIsListenerConnected; /** * Map that contains all the active notifications that are not currently HUN. These @@ -250,10 +251,12 @@ public class CarNotificationListener extends NotificationListenerService impleme mActiveNotifications = Stream.of(getActiveNotifications()).collect( Collectors.toConcurrentMap(StatusBarNotification::getKey, AlertEntry::new)); mRankingMap = super.getCurrentRanking(); + mIsListenerConnected = true; } @Override public void onListenerDisconnected() { + mIsListenerConnected = false; } public void setHandler(Handler handler) { @@ -261,6 +264,17 @@ public class CarNotificationListener extends NotificationListenerService impleme } /** + * Clears all local cached variables. + * Note: This is a blocking call so should not execute any long-running or time-consuming tasks + * like storing cache. + */ + public void clearCache() { + mHeadsUpManager.clearCache(); + mNotificationDataManager.clearAll(); + mActiveNotifications.clear(); + } + + /** * Called when Notification Panel's visibility changes. */ public void onVisibilityChanged(boolean isVisible) { @@ -300,7 +314,6 @@ public class CarNotificationListener extends NotificationListenerService impleme || sbn.getUser().getIdentifier() == UserHandle.USER_ALL); } - @Override public void onStateChange(AlertEntry alertEntry, CarHeadsUpNotificationManager.HeadsUpState headsUpState) { @@ -342,4 +355,9 @@ public class CarNotificationListener extends NotificationListenerService impleme mRankingMap.getRanking(alertEntry.getKey(), ranking); return ranking.getImportance() > NotificationManager.IMPORTANCE_LOW; } + + @VisibleForTesting + boolean getIsListenerConnected() { + return mIsListenerConnected; + } } diff --git a/src/com/android/car/notification/DismissAnimationHelper.java b/src/com/android/car/notification/DismissAnimationHelper.java index 2275f6da..6eb3ff6a 100644 --- a/src/com/android/car/notification/DismissAnimationHelper.java +++ b/src/com/android/car/notification/DismissAnimationHelper.java @@ -58,11 +58,11 @@ class DismissAnimationHelper { } /** - * The percentage of the view holder's width a non-dismissible view holder is allow to translate + * The percentage of the max translation a non-dismissible view holder is allow to translate * during a swipe gesture. As gesture's delta x distance grows the view holder should translate * asymptotically to this amount. */ - private final float mMaxPercentageOfWidthWithResistance; + private final float mMaxPercentageOfMaxTranslationWithResistance; /** * The callback indicating the supplied view has been dismissed. @@ -78,8 +78,8 @@ class DismissAnimationHelper { DismissAnimationHelper(Context context, DismissCallback callbacks) { mCallBacks = callbacks; - mMaxPercentageOfWidthWithResistance = - context.getResources().getFloat(R.dimen.max_percentage_of_width_with_resistance); + mMaxPercentageOfMaxTranslationWithResistance = context.getResources().getFloat( + R.dimen.max_percentage_of_max_translation_with_resistance); } /** Animate the dismissal of the given item. The velocityX is assumed to be 0. */ @@ -150,7 +150,7 @@ class DismissAnimationHelper { int swipeDirection = moveDeltaX > 0 ? Direction.RIGHT : Direction.LEFT; int width = viewHolder.itemView.getWidth(); - float maxSwipeDistanceWithResistance = mMaxPercentageOfWidthWithResistance * width; + float maxSwipeDistanceWithResistance = mMaxPercentageOfMaxTranslationWithResistance * width; if (Math.abs(moveDeltaX) >= width) { // If deltaX is too large, constrain to // maxScrollDistanceWithResistance. diff --git a/src/com/android/car/notification/HeadsUpEntry.java b/src/com/android/car/notification/HeadsUpEntry.java index 42eb4144..4a24152b 100644 --- a/src/com/android/car/notification/HeadsUpEntry.java +++ b/src/com/android/car/notification/HeadsUpEntry.java @@ -33,9 +33,16 @@ public class HeadsUpEntry extends AlertEntry { private View mNotificationView; private CarNotificationBaseViewHolder mCarNotificationBaseViewHolder; + // Signifies that this notification was NOT flagged with Notification.FLAG_ONLY_ALERT_ONCE boolean mIsAlertAgain; + + // Signifies that this notification is to be shown as Heads Up Notification for the first time boolean mIsNewHeadsUp; + + // Signifies that this notification is in process of being dismissed boolean mIsDismissing; + + // Signifies that the sender marked this notification to be removed boolean mShouldRemove; HeadsUpEntry(StatusBarNotification statusBarNotification) { diff --git a/src/com/android/car/notification/HeadsUpNotificationOnTouchListener.java b/src/com/android/car/notification/HeadsUpNotificationOnTouchListener.java index a9701093..c31d973e 100644 --- a/src/com/android/car/notification/HeadsUpNotificationOnTouchListener.java +++ b/src/com/android/car/notification/HeadsUpNotificationOnTouchListener.java @@ -18,44 +18,76 @@ package com.android.car.notification; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; +import android.content.res.Resources; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewPropertyAnimator; +import android.view.ViewTreeObserver; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.concurrent.TimeUnit; /** * OnTouchListener that enables swipe-to-dismiss gesture on heads-up notifications. */ class HeadsUpNotificationOnTouchListener implements View.OnTouchListener { + // todo(b/301474982): converge common logic in this and CarNotificationItemTouchListener class. + private static final int INITIAL_TRANSLATION_X = 0; + private static final int INITIAL_TRANSLATION_Y = 0; + private static final float MAXIMUM_ALPHA = 1f; + private static final float MINIMUM_ALPHA = 0f; /** - * Minimum velocity to initiate a fling, as measured in pixels per second. + * Factor by which view's alpha decreases based on the translation in the direction of dismiss. + * Example: If set to 1f, the view will be invisible when it has translated the maximum possible + * translation, similarly for 2f, view will be invisible halfway. */ - private static final int MINIMUM_FLING_VELOCITY = 2000; + private static final float ALPHA_FADE_FACTOR_MULTIPLIER = 2f; /** + * The unit of velocity in milliseconds. A value of 1 means "pixels per millisecond", + * 1000 means "pixels per 1000 milliseconds (1 second)". + */ + private static final int PIXELS_PER_SECOND = (int) TimeUnit.SECONDS.toMillis(1); + private final View mView; + private final DismissCallbacks mCallbacks; + private final Axis mDismissAxis; + /** * Distance a touch can wander before we think the user is scrolling in pixels. */ - private static final int TOUCH_SLOP = 20; - + private final int mTouchSlop; + private final boolean mDismissOnSwipe; /** * The proportion which view has to be swiped before it dismisses. */ - private static final float THRESHOLD = 0.3f; - + private final float mPercentageOfMaxTransaltionToDismiss; /** - * The unit of velocity in milliseconds. A value of 1 means "pixels per millisecond", - * 1000 means "pixels per 1000 milliseconds (1 second)". + * The minimum velocity in pixel per second the swipe gesture to initiate a dismiss action. */ - private static final int VELOCITY_UNITS = 1000; - - private final View mView; - private final DismissCallbacks mCallbacks; - + private final int mMinimumFlingVelocity; + /** + * The cap on velocity in pixel per second a swipe gesture is calculated to have. + */ + private final int mMaximumFlingVelocity; + /** + * The transaltion that a view can have. To set change value of + * {@code R.dimen.max_translation_headsup} to a non zero value. If set to zero, the view's + * dimensions(height/width) will be used instead. + */ + private float mMaxTranslation; + /** + * Distance by which a view should be translated by to be considered dismissed. Can be + * configured by setting {@code R.dimen.percentage_of_max_translation_to_dismiss} + */ + private float mDismissDelta; private VelocityTracker mVelocityTracker; private float mDownX; + private float mDownY; private boolean mSwiping; private int mSwipingSlop; - private float mTranslationX; - private boolean mDismissOnSwipe = true; + private float mTranslation; /** * The callback indicating the supplied view has been dismissed. @@ -64,24 +96,68 @@ class HeadsUpNotificationOnTouchListener implements View.OnTouchListener { void onDismiss(); } + private enum Axis { + HORIZONTAL, VERTICAL; + + public Axis getOppositeAxis() { + switch (this) { + case VERTICAL: + return HORIZONTAL; + default: + return VERTICAL; + } + } + } + HeadsUpNotificationOnTouchListener(View view, boolean dismissOnSwipe, DismissCallbacks callbacks) { mView = view; mCallbacks = callbacks; mDismissOnSwipe = dismissOnSwipe; + Resources res = view.getContext().getResources(); + mDismissAxis = res.getBoolean(R.bool.config_isHeadsUpNotificationDismissibleVertically) + ? Axis.VERTICAL : Axis.HORIZONTAL; + mTouchSlop = res.getDimensionPixelSize(R.dimen.touch_slop); + mPercentageOfMaxTransaltionToDismiss = + res.getFloat(R.dimen.percentage_of_max_translation_to_dismiss); + mMaxTranslation = res.getDimension(R.dimen.max_translation_headsup); + if (mMaxTranslation != 0) { + mDismissDelta = mMaxTranslation * mPercentageOfMaxTransaltionToDismiss; + } else { + mView.getViewTreeObserver().addOnGlobalLayoutListener( + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + mView.getViewTreeObserver().removeOnGlobalLayoutListener(this); + if (mDismissAxis == Axis.VERTICAL) { + mMaxTranslation = view.getHeight(); + } else { + mMaxTranslation = view.getWidth(); + } + mDismissDelta = mMaxTranslation * mPercentageOfMaxTransaltionToDismiss; + } + }); + } + ViewConfiguration viewConfiguration = ViewConfiguration.get(view.getContext()); + mMaximumFlingVelocity = viewConfiguration.getScaledMaximumFlingVelocity(); + mMinimumFlingVelocity = viewConfiguration.getScaledMinimumFlingVelocity(); } @Override public boolean onTouch(View view, MotionEvent motionEvent) { - motionEvent.offsetLocation(mTranslationX, /* deltaY= */ 0); - int viewWidth = mView.getWidth(); + if (mDismissAxis == Axis.VERTICAL) { + motionEvent.offsetLocation(INITIAL_TRANSLATION_X, /* deltaY= */ mTranslation); + } else { + motionEvent.offsetLocation(/* deltaX= */ mTranslation, INITIAL_TRANSLATION_Y); + } switch (motionEvent.getActionMasked()) { case MotionEvent.ACTION_DOWN: { mDownX = motionEvent.getRawX(); - mVelocityTracker = VelocityTracker.obtain(); + mDownY = motionEvent.getRawY(); + mVelocityTracker = obtainVelocityTracker(); mVelocityTracker.addMovement(motionEvent); - return false; + break; } case MotionEvent.ACTION_UP: { @@ -89,38 +165,29 @@ class HeadsUpNotificationOnTouchListener implements View.OnTouchListener { return false; } - float deltaX = motionEvent.getRawX() - mDownX; mVelocityTracker.addMovement(motionEvent); - mVelocityTracker.computeCurrentVelocity(VELOCITY_UNITS); - float velocityX = mVelocityTracker.getXVelocity(); - float absVelocityX = Math.abs(velocityX); - float absVelocityY = Math.abs(mVelocityTracker.getYVelocity()); - boolean dismiss = false; - boolean dismissRight = false; - if (Math.abs(deltaX) > viewWidth * THRESHOLD) { + mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND, mMaximumFlingVelocity); + float deltaInDismissAxis = + getDeltaInAxis(mDownX, mDownY, motionEvent, mDismissAxis); + boolean shouldBeDismissed = false; + boolean dismissInPositiveDirection = false; + if (Math.abs(deltaInDismissAxis) > mDismissDelta) { // dismiss when the movement is more than the defined threshold. - dismiss = true; - dismissRight = deltaX > 0; - } else if (MINIMUM_FLING_VELOCITY <= absVelocityX - && absVelocityY < absVelocityX - && mSwiping) { + shouldBeDismissed = true; + dismissInPositiveDirection = deltaInDismissAxis > 0; + } else if (mSwiping && isFlingEnoughForDismiss(mVelocityTracker, mDismissAxis) + && isFlingInSameDirectionAsDelta( + deltaInDismissAxis, mVelocityTracker, mDismissAxis)) { // dismiss when the velocity is more than the defined threshold. // dismiss only if flinging in the same direction as dragging. - dismiss = (velocityX < 0) == (deltaX < 0); - dismissRight = mVelocityTracker.getXVelocity() > 0; + shouldBeDismissed = true; + dismissInPositiveDirection = + getVelocityInAxis(mVelocityTracker, mDismissAxis) > 0; } - if (dismiss && mDismissOnSwipe) { + + if (shouldBeDismissed && mDismissOnSwipe) { mCallbacks.onDismiss(); - mView.animate() - .translationX(dismissRight ? viewWidth : -viewWidth) - .alpha(0) - .setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mView.setAlpha(1f); - mView.setTranslationX(0); - } - }); + animateDismissInAxis(mView, mDismissAxis, dismissInPositiveDirection); } else if (mSwiping) { animateToCenter(); } @@ -143,44 +210,33 @@ class HeadsUpNotificationOnTouchListener implements View.OnTouchListener { } mVelocityTracker.addMovement(motionEvent); - float deltaX = motionEvent.getRawX() - mDownX; - if (Math.abs(deltaX) > TOUCH_SLOP) { + float deltaInDismissAxis = + getDeltaInAxis(mDownX, mDownY, motionEvent, mDismissAxis); + if (Math.abs(deltaInDismissAxis) > mTouchSlop) { mSwiping = true; - mSwipingSlop = (deltaX > 0 ? TOUCH_SLOP : -TOUCH_SLOP); - mView.getParent().requestDisallowInterceptTouchEvent(true); - - // prevent onClickListener being triggered when moving. - MotionEvent cancelEvent = MotionEvent.obtain(motionEvent); - cancelEvent.setAction(MotionEvent.ACTION_CANCEL | - (motionEvent.getActionIndex() << - MotionEvent.ACTION_POINTER_INDEX_SHIFT)); - mView.onTouchEvent(cancelEvent); - cancelEvent.recycle(); + mSwipingSlop = (deltaInDismissAxis > 0 ? mTouchSlop : -mTouchSlop); + disallowAndCancelTouchEvents(mView, motionEvent); } if (mSwiping) { - mTranslationX = deltaX; - mView.setTranslationX(deltaX - mSwipingSlop); - if (!mDismissOnSwipe) { - return true; + mTranslation = deltaInDismissAxis; + moveView(mView, + /* translation= */ deltaInDismissAxis - mSwipingSlop, mDismissAxis); + if (mDismissOnSwipe) { + mView.setAlpha(getAlphaForDismissingView(mTranslation, mMaxTranslation)); } - mView.setAlpha(Math.max(0f, Math.min(1f, - 1f - 2f * Math.abs(deltaX) / viewWidth))); return true; } } - - default: { - return false; - } } return false; } private void animateToCenter() { mView.animate() - .translationX(0) - .alpha(1) + .translationX(INITIAL_TRANSLATION_X) + .translationY(INITIAL_TRANSLATION_Y) + .alpha(MAXIMUM_ALPHA) .setListener(null); } @@ -189,8 +245,128 @@ class HeadsUpNotificationOnTouchListener implements View.OnTouchListener { mVelocityTracker.recycle(); } mVelocityTracker = null; - mTranslationX = 0; + mTranslation = 0; mDownX = 0; + mDownY = 0; mSwiping = false; } + + private void resetView(View view) { + view.setTranslationX(INITIAL_TRANSLATION_X); + view.setTranslationY(INITIAL_TRANSLATION_Y); + view.setAlpha(MAXIMUM_ALPHA); + } + + private float getDeltaInAxis( + float downX, float downY, MotionEvent motionEvent, Axis dismissAxis) { + switch (dismissAxis) { + case VERTICAL: + return motionEvent.getRawY() - downY; + default: + return motionEvent.getRawX() - downX; + } + } + + private void disallowAndCancelTouchEvents(View view, MotionEvent motionEvent) { + view.getParent().requestDisallowInterceptTouchEvent(true); + + // prevent onClickListener being triggered when moving. + MotionEvent cancelEvent = obtainMotionEvent(motionEvent); + cancelEvent.setAction(MotionEvent.ACTION_CANCEL + | (motionEvent.getActionIndex() + << MotionEvent.ACTION_POINTER_INDEX_SHIFT)); + view.onTouchEvent(cancelEvent); + cancelEvent.recycle(); + } + + private void moveView(View view, float translation, Axis dismissAxis) { + if (dismissAxis == Axis.VERTICAL) { + view.setTranslationY(translation); + } else { + view.setTranslationX(translation); + } + } + + private float getAlphaForDismissingView(float translation, float maxTranslation) { + float fractionMoved = Math.abs(translation) / Math.abs(maxTranslation); + // min is required to avoid value greater than MAXIMUM_ALPHA + float alphaBasedOnTranslation = Math.min(MAXIMUM_ALPHA, + MAXIMUM_ALPHA - (ALPHA_FADE_FACTOR_MULTIPLIER * fractionMoved)); + // max is required to avoid alpha values less than min + return Math.max(MINIMUM_ALPHA, alphaBasedOnTranslation); + } + + private boolean isFlingEnoughForDismiss(VelocityTracker velocityTracker, Axis axis) { + float velocityInDismissingDirection = getVelocityInAxis(velocityTracker, axis); + float velocityInOppositeDirection = + getVelocityInAxis(velocityTracker, axis.getOppositeAxis()); + boolean isMoreFlingInDismissAxis = + Math.abs(velocityInDismissingDirection) > Math.abs(velocityInOppositeDirection); + return mMinimumFlingVelocity <= Math.abs(velocityInDismissingDirection) + && isMoreFlingInDismissAxis; + } + + private float getVelocityInAxis(VelocityTracker velocityTracker, Axis axis) { + switch (axis) { + case VERTICAL: + return velocityTracker.getYVelocity(); + default: + return velocityTracker.getXVelocity(); + } + } + + private boolean isFlingInSameDirectionAsDelta(float delta, VelocityTracker velocityTracker, + Axis axis) { + float velocityInDismissingDirection = getVelocityInAxis(velocityTracker, axis); + boolean isVelocityInPositiveDirection = velocityInDismissingDirection > 0; + boolean isDeltaInPositiveDirection = delta > 0; + return isVelocityInPositiveDirection == isDeltaInPositiveDirection; + } + + private void animateDismissInAxis(View view, Axis axis, boolean dismissInPositiveDirection) { + float dismissTranslation = dismissInPositiveDirection ? mMaxTranslation : -mMaxTranslation; + ViewPropertyAnimator animator = view.animate(); + if (axis == Axis.VERTICAL) { + animator.translationY(dismissTranslation); + } else { + animator.translationX(dismissTranslation); + } + animator.alpha(MINIMUM_ALPHA).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + resetView(mView); + } + }).start(); + } + + /** + * Should be overridden in test to not access static obtain method. + */ + @VisibleForTesting + MotionEvent obtainMotionEvent(MotionEvent motionEvent) { + return MotionEvent.obtain(motionEvent); + } + + /** + * Should be overridden in test to not access static obtain method. + */ + @VisibleForTesting + VelocityTracker obtainVelocityTracker() { + return VelocityTracker.obtain(); + } + + @VisibleForTesting + int getMinimumFlingVelocity() { + return mMinimumFlingVelocity; + } + + @VisibleForTesting + int getTouchSlop() { + return mTouchSlop; + } + + @VisibleForTesting + float getPercentageOfMaxTransaltionToDismiss() { + return mPercentageOfMaxTransaltionToDismiss; + } } diff --git a/src/com/android/car/notification/headsup/CarHeadsUpNotificationAppContainer.java b/src/com/android/car/notification/headsup/CarHeadsUpNotificationAppContainer.java index b3ccc8e6..f7448fbe 100644 --- a/src/com/android/car/notification/headsup/CarHeadsUpNotificationAppContainer.java +++ b/src/com/android/car/notification/headsup/CarHeadsUpNotificationAppContainer.java @@ -17,6 +17,7 @@ package com.android.car.notification.headsup; import android.content.Context; +import android.content.res.Resources; import android.graphics.PixelFormat; import android.view.Gravity; import android.view.WindowManager; @@ -29,8 +30,6 @@ import com.android.car.notification.R; * Used to attach HUNs views to window. */ public class CarHeadsUpNotificationAppContainer extends CarHeadsUpNotificationContainer { - private static final String TAG = "CarHUNContainerApp"; - public CarHeadsUpNotificationAppContainer(Context context) { super(context, context.getSystemService(WindowManager.class)); @@ -38,16 +37,17 @@ public class CarHeadsUpNotificationAppContainer extends CarHeadsUpNotificationCo @Override protected WindowManager.LayoutParams getWindowManagerLayoutParams() { + Resources resources = getContext().getResources(); WindowManager.LayoutParams wrapperParams = new WindowManager.LayoutParams( - WindowManager.LayoutParams.MATCH_PARENT, - WindowManager.LayoutParams.WRAP_CONTENT, + resources.getDimensionPixelSize(R.dimen.headsup_container_width), + resources.getDimensionPixelSize(R.dimen.headsup_container_height), // This type allows covering status bar and receiving touch input WindowManager.LayoutParams.TYPE_SYSTEM_ERROR, WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT); wrapperParams.gravity = getShowHunOnBottom() ? Gravity.BOTTOM : Gravity.TOP; - wrapperParams.y = (int) getContext().getResources().getDimension( + wrapperParams.y = resources.getDimensionPixelSize( R.dimen.headsup_notification_window_y_offset); return wrapperParams; } diff --git a/src/com/android/car/notification/template/CarNotificationActionsView.java b/src/com/android/car/notification/template/CarNotificationActionsView.java index d9aebb05..22a436cd 100644 --- a/src/com/android/car/notification/template/CarNotificationActionsView.java +++ b/src/com/android/car/notification/template/CarNotificationActionsView.java @@ -70,6 +70,10 @@ public class CarNotificationActionsView extends LinearLayout implements private final Drawable mActionButtonBackground; private final Drawable mCallButtonBackground; private final Drawable mDeclineButtonBackground; + @ColorInt + private final int mCallButtonTextColor; + @ColorInt + private final int mDeclineButtonTextColor; private final Drawable mUnmuteButtonBackground; private final String mReplyButtonText; private final String mPlayButtonText; @@ -130,6 +134,8 @@ public class CarNotificationActionsView extends LinearLayout implements mDeclineButtonBackground.setColorFilter( new PorterDuffColorFilter(mContext.getColor(R.color.call_decline_button), PorterDuff.Mode.SRC_IN)); + mCallButtonTextColor = mContext.getColor(R.color.call_accept_button_text); + mDeclineButtonTextColor = mContext.getColor(R.color.call_decline_button_text); mUnmuteButtonBackground = mContext.getDrawable(R.drawable.call_action_button_background); mUnmuteButtonBackground.setColorFilter( new PorterDuffColorFilter(mContext.getColor(R.color.unmute_button), @@ -227,6 +233,8 @@ public class CarNotificationActionsView extends LinearLayout implements if (mIsCategoryCall) { mActionButtons.get(0).setBackground(mCallButtonBackground); mActionButtons.get(1).setBackground(mDeclineButtonBackground); + mActionButtons.get(0).setTextColor(mCallButtonTextColor); + mActionButtons.get(1).setTextColor(mDeclineButtonTextColor); } } |