summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/com/android/car/notification/CarHeadsUpNotificationManager.java87
-rw-r--r--src/com/android/car/notification/CarHeadsUpNotificationQueue.java12
-rw-r--r--src/com/android/car/notification/CarNotificationItemTouchListener.java17
-rw-r--r--src/com/android/car/notification/CarNotificationListener.java22
-rw-r--r--src/com/android/car/notification/DismissAnimationHelper.java10
-rw-r--r--src/com/android/car/notification/HeadsUpEntry.java7
-rw-r--r--src/com/android/car/notification/HeadsUpNotificationOnTouchListener.java316
-rw-r--r--src/com/android/car/notification/headsup/CarHeadsUpNotificationAppContainer.java10
-rw-r--r--src/com/android/car/notification/template/CarNotificationActionsView.java8
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);
}
}