blob: b4ccb567504a186a6088892bcbab10e8b2ac1b15 [file] [log] [blame]
Mady Mellor4b80b102016-01-22 08:03:58 -08001/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
Rohan Shah20790b82018-07-02 17:21:04 -070014 * limitations under the License
Mady Mellor4b80b102016-01-22 08:03:58 -080015 */
16
Rohan Shah20790b82018-07-02 17:21:04 -070017package com.android.systemui.statusbar.notification.row;
Mady Mellor4b80b102016-01-22 08:03:58 -080018
Julia Reynolds789bf3f2019-05-24 11:09:02 -040019import static android.provider.Settings.Secure.SHOW_NOTIFICATION_SNOOZE;
20
Aaron Heuckroth266cb342018-09-07 14:52:04 -040021import static com.android.systemui.SwipeHelper.SWIPED_FAR_ENOUGH_SIZE_FRACTION;
22
Mady Mellor4b80b102016-01-22 08:03:58 -080023import android.animation.Animator;
24import android.animation.AnimatorListenerAdapter;
25import android.animation.ValueAnimator;
Julia Reynoldsb5867452018-02-28 16:31:35 -050026import android.annotation.Nullable;
Mady Mellor4c197602017-04-10 17:57:52 -070027import android.app.Notification;
Mady Mellor4b80b102016-01-22 08:03:58 -080028import android.content.Context;
Mady Mellor3fd273e2016-03-15 21:08:14 -070029import android.content.res.Resources;
Evan Lairde55c6012019-03-13 12:54:37 -040030import android.graphics.Point;
Mady Mellor43c2cd12016-12-12 21:05:13 -080031import android.graphics.drawable.Drawable;
Mady Mellor95d743c2017-01-10 12:05:27 -080032import android.os.Handler;
Selim Cinek0f66a4c2017-04-28 19:26:28 -070033import android.os.Looper;
Julia Reynolds789bf3f2019-05-24 11:09:02 -040034import android.provider.Settings;
Mady Mellor4c197602017-04-10 17:57:52 -070035import android.service.notification.StatusBarNotification;
Gus Prevasb79fff62018-12-07 14:38:42 -050036import android.util.ArrayMap;
Mady Mellor43c2cd12016-12-12 21:05:13 -080037import android.view.LayoutInflater;
Mady Mellor4b80b102016-01-22 08:03:58 -080038import android.view.View;
Mady Mellor95d743c2017-01-10 12:05:27 -080039import android.view.ViewGroup;
Mady Mellor4b80b102016-01-22 08:03:58 -080040import android.widget.FrameLayout;
Mady Mellor95d743c2017-01-10 12:05:27 -080041import android.widget.FrameLayout.LayoutParams;
Mady Mellor4b80b102016-01-22 08:03:58 -080042
Aaron Heuckroth266cb342018-09-07 14:52:04 -040043import com.android.internal.annotations.VisibleForTesting;
Gus Prevaseb61ce12018-11-07 16:57:40 -050044import com.android.systemui.Interpolators;
45import com.android.systemui.R;
46import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
47import com.android.systemui.statusbar.AlphaOptimizedImageView;
48import com.android.systemui.statusbar.notification.row.NotificationGuts.GutsContent;
49import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
50
51import java.util.ArrayList;
52import java.util.List;
Gus Prevasb79fff62018-12-07 14:38:42 -050053import java.util.Map;
Aaron Heuckroth266cb342018-09-07 14:52:04 -040054
Mady Mellord9c2232c2017-04-04 18:45:30 -070055public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnClickListener,
56 ExpandableNotificationRow.LayoutListener {
Mady Mellor4b80b102016-01-22 08:03:58 -080057
Mady Mellor55744252017-04-10 10:05:17 -070058 private static final boolean DEBUG = false;
59 private static final String TAG = "swipe";
60
Mady Mellor55744252017-04-10 10:05:17 -070061 // Notification must be swiped at least this fraction of a single menu item to show menu
62 private static final float SWIPED_FAR_ENOUGH_MENU_FRACTION = 0.25f;
63 private static final float SWIPED_FAR_ENOUGH_MENU_UNCLEARABLE_FRACTION = 0.15f;
64
65 // When the menu is displayed, the notification must be swiped within this fraction of a single
66 // menu item to snap back to menu (else it will cover the menu or it'll be dismissed)
67 private static final float SWIPED_BACK_ENOUGH_TO_COVER_FRACTION = 0.2f;
68
Aaron Heuckroth266cb342018-09-07 14:52:04 -040069 private static final int ICON_ALPHA_ANIM_DURATION = 200;
70 private static final long SHOW_MENU_DELAY = 60;
71
Mady Mellor4b80b102016-01-22 08:03:58 -080072 private ExpandableNotificationRow mParent;
Mady Mellor95d743c2017-01-10 12:05:27 -080073
74 private Context mContext;
75 private FrameLayout mMenuContainer;
Gus Prevas5a70a4e2018-11-26 17:16:05 -050076 private NotificationMenuItem mInfoItem;
Julia Reynoldsb5867452018-02-28 16:31:35 -050077 private MenuItem mAppOpsItem;
Mady Mellorb3a6aed2018-05-11 13:24:07 -070078 private MenuItem mSnoozeItem;
Gus Prevaseb61ce12018-11-07 16:57:40 -050079 private ArrayList<MenuItem> mLeftMenuItems;
80 private ArrayList<MenuItem> mRightMenuItems;
Gus Prevasb79fff62018-12-07 14:38:42 -050081 private final Map<View, MenuItem> mMenuItemsByView = new ArrayMap<>();
Mady Mellor95d743c2017-01-10 12:05:27 -080082 private OnMenuEventListener mMenuListener;
Gus Prevasa18dc572019-01-14 16:11:22 -050083 private boolean mDismissRtl;
84 private boolean mIsForeground;
Evan Laird30b9b162019-04-24 15:22:24 -040085 private final boolean mIsUsingBidirectionalSwipe;
Mady Mellor4b80b102016-01-22 08:03:58 -080086
87 private ValueAnimator mFadeAnimator;
Mady Mellor95d743c2017-01-10 12:05:27 -080088 private boolean mAnimating;
89 private boolean mMenuFadedIn;
90
91 private boolean mOnLeft;
92 private boolean mIconsPlaced;
93
94 private boolean mDismissing;
95 private boolean mSnapping;
96 private float mTranslation;
Mady Mellorcdd57182016-04-12 19:00:17 -070097
Mady Mellor761cde12017-01-10 11:36:39 -080098 private int[] mIconLocation = new int[2];
Mady Mellorb53bc272016-02-11 18:28:23 -080099 private int[] mParentLocation = new int[2];
Mady Mellor43c2cd12016-12-12 21:05:13 -0800100
Aaron Heuckroth266cb342018-09-07 14:52:04 -0400101 private int mHorizSpaceForIcon = -1;
Mady Mellor81a9d482017-05-30 12:51:01 -0700102 private int mVertSpaceForIcons = -1;
103 private int mIconPadding = -1;
Selim Cinek515b2032017-11-15 10:20:19 -0800104 private int mSidePadding;
Mady Mellor43c2cd12016-12-12 21:05:13 -0800105
106 private float mAlpha = 0f;
Mady Mellor4b80b102016-01-22 08:03:58 -0800107
Mady Mellor95d743c2017-01-10 12:05:27 -0800108 private CheckForDrag mCheckForDrag;
109 private Handler mHandler;
110
Aaron Heuckroth266cb342018-09-07 14:52:04 -0400111 private boolean mMenuSnapped;
Mady Mellor95d743c2017-01-10 12:05:27 -0800112 private boolean mMenuSnappedOnLeft;
113 private boolean mShouldShowMenu;
114
Mady Mellor89e15ec2017-06-28 17:08:21 -0700115 private boolean mIsUserTouching;
Mady Mellor95d743c2017-01-10 12:05:27 -0800116
Mady Mellor761cde12017-01-10 11:36:39 -0800117 public NotificationMenuRow(Context context) {
Evan Laird30b9b162019-04-24 15:22:24 -0400118 //TODO: (b/131242807) not using bidirectional swipe for now
119 this(context, false);
120 }
121
122 // Only needed for testing until we want to turn bidirectional swipe back on
123 @VisibleForTesting
124 NotificationMenuRow(Context context, boolean isUsingBidirectionalSwipe) {
Mady Mellor95d743c2017-01-10 12:05:27 -0800125 mContext = context;
Mady Mellor81a9d482017-05-30 12:51:01 -0700126 mShouldShowMenu = context.getResources().getBoolean(R.bool.config_showNotificationGear);
Selim Cinek0f66a4c2017-04-28 19:26:28 -0700127 mHandler = new Handler(Looper.getMainLooper());
Gus Prevaseb61ce12018-11-07 16:57:40 -0500128 mLeftMenuItems = new ArrayList<>();
129 mRightMenuItems = new ArrayList<>();
Evan Laird30b9b162019-04-24 15:22:24 -0400130 mIsUsingBidirectionalSwipe = isUsingBidirectionalSwipe;
Mady Mellor4b80b102016-01-22 08:03:58 -0800131 }
132
Mady Mellor95d743c2017-01-10 12:05:27 -0800133 @Override
134 public ArrayList<MenuItem> getMenuItems(Context context) {
Gus Prevaseb61ce12018-11-07 16:57:40 -0500135 return mOnLeft ? mLeftMenuItems : mRightMenuItems;
Mady Mellor43c2cd12016-12-12 21:05:13 -0800136 }
137
Mady Mellor95d743c2017-01-10 12:05:27 -0800138 @Override
139 public MenuItem getLongpressMenuItem(Context context) {
Mady Mellor4c197602017-04-10 17:57:52 -0700140 return mInfoItem;
Mady Mellor04d7a0f2017-01-25 13:16:03 -0800141 }
142
Mady Mellor95d743c2017-01-10 12:05:27 -0800143 @Override
Julia Reynoldsb5867452018-02-28 16:31:35 -0500144 public MenuItem getAppOpsMenuItem(Context context) {
145 return mAppOpsItem;
146 }
147
148 @Override
Mady Mellorb3a6aed2018-05-11 13:24:07 -0700149 public MenuItem getSnoozeMenuItem(Context context) {
150 return mSnoozeItem;
151 }
152
Aaron Heuckroth266cb342018-09-07 14:52:04 -0400153 @VisibleForTesting
154 protected ExpandableNotificationRow getParent() {
155 return mParent;
156 }
157
158 @VisibleForTesting
159 protected boolean isMenuOnLeft() {
160 return mOnLeft;
161 }
162
163 @VisibleForTesting
164 protected boolean isMenuSnappedOnLeft() {
165 return mMenuSnappedOnLeft;
166 }
167
168 @VisibleForTesting
169 protected boolean isMenuSnapped() {
170 return mMenuSnapped;
171 }
172
173 @VisibleForTesting
174 protected boolean isDismissing() {
175 return mDismissing;
176 }
177
178 @VisibleForTesting
179 protected boolean isSnapping() {
180 return mSnapping;
Mady Mellor95d743c2017-01-10 12:05:27 -0800181 }
182
183 @Override
184 public void setMenuClickListener(OnMenuEventListener listener) {
185 mMenuListener = listener;
186 }
187
188 @Override
Mady Mellor4ab28202017-06-06 11:42:50 -0700189 public void createMenu(ViewGroup parent, StatusBarNotification sbn) {
Mady Mellor95d743c2017-01-10 12:05:27 -0800190 mParent = (ExpandableNotificationRow) parent;
Gus Prevasbd348a82018-11-12 17:14:49 -0500191 createMenuViews(true /* resetState */,
192 sbn != null && (sbn.getNotification().flags & Notification.FLAG_FOREGROUND_SERVICE)
193 != 0);
Mady Mellor43c2cd12016-12-12 21:05:13 -0800194 }
195
Mady Mellor95d743c2017-01-10 12:05:27 -0800196 @Override
197 public boolean isMenuVisible() {
198 return mAlpha > 0;
Mady Mellor43c2cd12016-12-12 21:05:13 -0800199 }
200
Aaron Heuckroth266cb342018-09-07 14:52:04 -0400201 @VisibleForTesting
202 protected boolean isUserTouching() {
203 return mIsUserTouching;
204 }
205
206 @Override
207 public boolean shouldShowMenu() {
208 return mShouldShowMenu;
209 }
210
Mady Mellor95d743c2017-01-10 12:05:27 -0800211 @Override
212 public View getMenuView() {
213 return mMenuContainer;
214 }
215
Aaron Heuckroth266cb342018-09-07 14:52:04 -0400216 @VisibleForTesting
217 protected float getTranslation() {
218 return mTranslation;
219 }
220
Mady Mellor95d743c2017-01-10 12:05:27 -0800221 @Override
222 public void resetMenu() {
223 resetState(true);
224 }
225
Mady Mellor4c197602017-04-10 17:57:52 -0700226 @Override
Aaron Heuckroth266cb342018-09-07 14:52:04 -0400227 public void onTouchEnd() {
228 mIsUserTouching = false;
229 }
230
231 @Override
Mady Mellor4ab28202017-06-06 11:42:50 -0700232 public void onNotificationUpdated(StatusBarNotification sbn) {
Mady Mellor4c197602017-04-10 17:57:52 -0700233 if (mMenuContainer == null) {
234 // Menu hasn't been created yet, no need to do anything.
235 return;
236 }
Gus Prevasbd348a82018-11-12 17:14:49 -0500237 createMenuViews(!isMenuVisible() /* resetState */,
238 (sbn.getNotification().flags & Notification.FLAG_FOREGROUND_SERVICE) != 0);
Mady Mellor4c197602017-04-10 17:57:52 -0700239 }
240
Mady Mellord9c2232c2017-04-04 18:45:30 -0700241 @Override
242 public void onConfigurationChanged() {
243 mParent.setLayoutListener(this);
244 }
245
246 @Override
247 public void onLayout() {
248 mIconsPlaced = false; // Force icons to be re-placed
249 setMenuLocation();
250 mParent.removeListener();
251 }
252
Gus Prevasbd348a82018-11-12 17:14:49 -0500253 private void createMenuViews(boolean resetState, final boolean isForeground) {
Gus Prevasa18dc572019-01-14 16:11:22 -0500254 mIsForeground = isForeground;
255
Mady Mellor81a9d482017-05-30 12:51:01 -0700256 final Resources res = mContext.getResources();
257 mHorizSpaceForIcon = res.getDimensionPixelSize(R.dimen.notification_menu_icon_size);
258 mVertSpaceForIcons = res.getDimensionPixelSize(R.dimen.notification_min_height);
Gus Prevaseb61ce12018-11-07 16:57:40 -0500259 mLeftMenuItems.clear();
260 mRightMenuItems.clear();
Julia Reynolds789bf3f2019-05-24 11:09:02 -0400261
262 boolean showSnooze = Settings.Secure.getInt(mContext.getContentResolver(),
263 SHOW_NOTIFICATION_SNOOZE, 0) == 1;
264
Mady Mellor81a9d482017-05-30 12:51:01 -0700265 // Construct the menu items based on the notification
Julia Reynolds789bf3f2019-05-24 11:09:02 -0400266 if (!isForeground && showSnooze) {
267 // Only show snooze for non-foreground notifications, and if the setting is on
Gus Prevasbd348a82018-11-12 17:14:49 -0500268 mSnoozeItem = createSnoozeItem(mContext);
Mady Mellor4c197602017-04-10 17:57:52 -0700269 }
Julia Reynoldsb5867452018-02-28 16:31:35 -0500270 mAppOpsItem = createAppOpsItem(mContext);
Evan Laird30b9b162019-04-24 15:22:24 -0400271 if (mIsUsingBidirectionalSwipe) {
Gus Prevascaed15c2019-01-18 14:19:51 -0500272 mInfoItem = createInfoItem(mContext, !mParent.getEntry().isHighPriority());
Gus Prevasbd348a82018-11-12 17:14:49 -0500273 } else {
Gus Prevas5a70a4e2018-11-26 17:16:05 -0500274 mInfoItem = createInfoItem(mContext);
275 }
276
Evan Laird30b9b162019-04-24 15:22:24 -0400277 if (!mIsUsingBidirectionalSwipe) {
Julia Reynolds789bf3f2019-05-24 11:09:02 -0400278 if (!isForeground && showSnooze) {
Gus Prevas5a70a4e2018-11-26 17:16:05 -0500279 mRightMenuItems.add(mSnoozeItem);
280 }
281 mRightMenuItems.add(mInfoItem);
282 mRightMenuItems.add(mAppOpsItem);
283 mLeftMenuItems.addAll(mRightMenuItems);
284 } else {
Gus Prevasa18dc572019-01-14 16:11:22 -0500285 ArrayList<MenuItem> menuItems = mDismissRtl ? mLeftMenuItems : mRightMenuItems;
286 menuItems.add(mInfoItem);
Gus Prevasbd348a82018-11-12 17:14:49 -0500287 }
Julia Reynoldsb5867452018-02-28 16:31:35 -0500288
Gus Prevaseb61ce12018-11-07 16:57:40 -0500289 populateMenuViews();
Mady Mellor98d8bdb2017-05-19 11:46:07 -0700290 if (resetState) {
291 resetState(false /* notify */);
292 } else {
293 mIconsPlaced = false;
294 setMenuLocation();
Mady Mellor89e15ec2017-06-28 17:08:21 -0700295 if (!mIsUserTouching) {
Aaron Heuckroth266cb342018-09-07 14:52:04 -0400296 onSnapOpen();
Mady Mellor89e15ec2017-06-28 17:08:21 -0700297 }
Mady Mellor98d8bdb2017-05-19 11:46:07 -0700298 }
Mady Mellor4c197602017-04-10 17:57:52 -0700299 }
300
Gus Prevaseb61ce12018-11-07 16:57:40 -0500301 private void populateMenuViews() {
302 if (mMenuContainer != null) {
303 mMenuContainer.removeAllViews();
Gus Prevasb79fff62018-12-07 14:38:42 -0500304 mMenuItemsByView.clear();
Gus Prevaseb61ce12018-11-07 16:57:40 -0500305 } else {
306 mMenuContainer = new FrameLayout(mContext);
307 }
308 List<MenuItem> menuItems = mOnLeft ? mLeftMenuItems : mRightMenuItems;
309 for (int i = 0; i < menuItems.size(); i++) {
310 addMenuView(menuItems.get(i), mMenuContainer);
311 }
312 }
313
Mady Mellor95d743c2017-01-10 12:05:27 -0800314 private void resetState(boolean notify) {
Mady Mellor761cde12017-01-10 11:36:39 -0800315 setMenuAlpha(0f);
316 mIconsPlaced = false;
317 mMenuFadedIn = false;
Mady Mellorb53bc272016-02-11 18:28:23 -0800318 mAnimating = false;
Mady Mellor3a5e8dd2016-03-12 00:13:23 +0000319 mSnapping = false;
320 mDismissing = false;
Aaron Heuckroth266cb342018-09-07 14:52:04 -0400321 mMenuSnapped = false;
Mady Mellor95d743c2017-01-10 12:05:27 -0800322 setMenuLocation();
323 if (mMenuListener != null && notify) {
324 mMenuListener.onMenuReset(mParent);
Mady Mellor3a5e8dd2016-03-12 00:13:23 +0000325 }
Mady Mellorb53bc272016-02-11 18:28:23 -0800326 }
327
Mady Mellor95d743c2017-01-10 12:05:27 -0800328 @Override
Aaron Heuckroth266cb342018-09-07 14:52:04 -0400329 public void onTouchMove(float delta) {
330 mSnapping = false;
331
332 if (!isTowardsMenu(delta) && isMenuLocationChange()) {
333 // Don't consider it "snapped" if location has changed.
334 mMenuSnapped = false;
335
336 // Changed directions, make sure we check to fade in icon again.
337 if (!mHandler.hasCallbacks(mCheckForDrag)) {
338 // No check scheduled, set null to schedule a new one.
Mady Mellor95d743c2017-01-10 12:05:27 -0800339 mCheckForDrag = null;
Mady Mellor95d743c2017-01-10 12:05:27 -0800340 } else {
Aaron Heuckroth266cb342018-09-07 14:52:04 -0400341 // Check scheduled, reset alpha and update location; check will fade it in
342 setMenuAlpha(0f);
343 setMenuLocation();
Mady Mellor95d743c2017-01-10 12:05:27 -0800344 }
Mady Mellor95d743c2017-01-10 12:05:27 -0800345 }
Aaron Heuckroth266cb342018-09-07 14:52:04 -0400346 if (mShouldShowMenu
347 && !NotificationStackScrollLayout.isPinnedHeadsUp(getParent())
348 && !mParent.areGutsExposed()
Selim Cinekc3fec682019-06-06 18:11:07 -0700349 && !mParent.showingPulsing()
Aaron Heuckroth266cb342018-09-07 14:52:04 -0400350 && (mCheckForDrag == null || !mHandler.hasCallbacks(mCheckForDrag))) {
351 // Only show the menu if we're not a heads up view and guts aren't exposed.
352 mCheckForDrag = new CheckForDrag();
353 mHandler.postDelayed(mCheckForDrag, SHOW_MENU_DELAY);
Mady Mellor55744252017-04-10 10:05:17 -0700354 }
Mady Mellor4b80b102016-01-22 08:03:58 -0800355 }
356
Aaron Heuckroth266cb342018-09-07 14:52:04 -0400357 @VisibleForTesting
358 protected void beginDrag() {
359 mSnapping = false;
360 if (mFadeAnimator != null) {
361 mFadeAnimator.cancel();
362 }
363 mHandler.removeCallbacks(mCheckForDrag);
364 mCheckForDrag = null;
365 mIsUserTouching = true;
Mady Mellor95d743c2017-01-10 12:05:27 -0800366 }
367
Aaron Heuckroth266cb342018-09-07 14:52:04 -0400368 @Override
369 public void onTouchStart() {
370 beginDrag();
371 }
372
373 @Override
374 public void onSnapOpen() {
375 mMenuSnapped = true;
376 mMenuSnappedOnLeft = isMenuOnLeft();
Gus Prevas03323792018-11-21 10:16:08 -0500377 if (mAlpha == 0f && mParent != null) {
378 fadeInMenu(mParent.getWidth());
379 }
Aaron Heuckroth266cb342018-09-07 14:52:04 -0400380 if (mMenuListener != null) {
381 mMenuListener.onMenuShown(getParent());
382 }
383 }
384
385 @Override
386 public void onSnapClosed() {
Mady Mellor89e15ec2017-06-28 17:08:21 -0700387 cancelDrag();
Aaron Heuckroth266cb342018-09-07 14:52:04 -0400388 mMenuSnapped = false;
Mady Mellor95d743c2017-01-10 12:05:27 -0800389 mSnapping = true;
Mady Mellor95d743c2017-01-10 12:05:27 -0800390 }
391
Aaron Heuckroth266cb342018-09-07 14:52:04 -0400392 @Override
393 public void onDismiss() {
Mady Mellor89e15ec2017-06-28 17:08:21 -0700394 cancelDrag();
Aaron Heuckroth266cb342018-09-07 14:52:04 -0400395 mMenuSnapped = false;
Mady Mellor89e15ec2017-06-28 17:08:21 -0700396 mDismissing = true;
Mady Mellor89e15ec2017-06-28 17:08:21 -0700397 }
398
Aaron Heuckroth266cb342018-09-07 14:52:04 -0400399 @VisibleForTesting
400 protected void cancelDrag() {
Mady Mellor7a5b2b62017-04-14 18:53:45 -0700401 if (mFadeAnimator != null) {
402 mFadeAnimator.cancel();
403 }
404 mHandler.removeCallbacks(mCheckForDrag);
Mady Mellor95d743c2017-01-10 12:05:27 -0800405 }
406
Aaron Heuckroth266cb342018-09-07 14:52:04 -0400407 @VisibleForTesting
408 protected float getMinimumSwipeDistance() {
409 final float multiplier = getParent().canViewBeDismissed()
Mady Mellor55744252017-04-10 10:05:17 -0700410 ? SWIPED_FAR_ENOUGH_MENU_FRACTION
411 : SWIPED_FAR_ENOUGH_MENU_UNCLEARABLE_FRACTION;
Aaron Heuckroth266cb342018-09-07 14:52:04 -0400412 return mHorizSpaceForIcon * multiplier;
413 }
414
415 @VisibleForTesting
416 protected float getMaximumSwipeDistance() {
417 return mHorizSpaceForIcon * SWIPED_BACK_ENOUGH_TO_COVER_FRACTION;
Mady Mellor95d743c2017-01-10 12:05:27 -0800418 }
419
420 /**
421 * Returns whether the gesture is towards the menu location or not.
422 */
Aaron Heuckroth266cb342018-09-07 14:52:04 -0400423 @Override
424 public boolean isTowardsMenu(float movement) {
Mady Mellor95d743c2017-01-10 12:05:27 -0800425 return isMenuVisible()
Aaron Heuckroth266cb342018-09-07 14:52:04 -0400426 && ((isMenuOnLeft() && movement <= 0)
427 || (!isMenuOnLeft() && movement >= 0));
Mady Mellor95d743c2017-01-10 12:05:27 -0800428 }
429
430 @Override
Mady Mellor3fd273e2016-03-15 21:08:14 -0700431 public void setAppName(String appName) {
Mady Mellor95d743c2017-01-10 12:05:27 -0800432 if (appName == null) {
433 return;
434 }
Gus Prevaseb61ce12018-11-07 16:57:40 -0500435 setAppName(appName, mLeftMenuItems);
436 setAppName(appName, mRightMenuItems);
437 }
438
439 private void setAppName(String appName,
440 ArrayList<MenuItem> menuItems) {
Mady Mellor95d743c2017-01-10 12:05:27 -0800441 Resources res = mContext.getResources();
Gus Prevaseb61ce12018-11-07 16:57:40 -0500442 final int count = menuItems.size();
Mady Mellor43c2cd12016-12-12 21:05:13 -0800443 for (int i = 0; i < count; i++) {
Gus Prevaseb61ce12018-11-07 16:57:40 -0500444 MenuItem item = menuItems.get(i);
Mady Mellor43c2cd12016-12-12 21:05:13 -0800445 String description = String.format(
446 res.getString(R.string.notification_menu_accessibility),
Mady Mellor95d743c2017-01-10 12:05:27 -0800447 appName, item.getContentDescription());
448 View menuView = item.getMenuView();
449 if (menuView != null) {
450 menuView.setContentDescription(description);
451 }
Mady Mellor43c2cd12016-12-12 21:05:13 -0800452 }
Mady Mellor3fd273e2016-03-15 21:08:14 -0700453 }
454
Mady Mellor95d743c2017-01-10 12:05:27 -0800455 @Override
Aaron Heuckroth266cb342018-09-07 14:52:04 -0400456 public void onParentHeightUpdate() {
Gus Prevaseb61ce12018-11-07 16:57:40 -0500457 if (mParent == null
458 || (mLeftMenuItems.isEmpty() && mRightMenuItems.isEmpty())
459 || mMenuContainer == null) {
Mady Mellor95d743c2017-01-10 12:05:27 -0800460 return;
461 }
Mady Mellor54540972017-06-07 11:55:36 -0700462 int parentHeight = mParent.getActualHeight();
Mady Mellor95d743c2017-01-10 12:05:27 -0800463 float translationY;
464 if (parentHeight < mVertSpaceForIcons) {
465 translationY = (parentHeight / 2) - (mHorizSpaceForIcon / 2);
Mady Mellor4b80b102016-01-22 08:03:58 -0800466 } else {
Mady Mellor95d743c2017-01-10 12:05:27 -0800467 translationY = (mVertSpaceForIcons - mHorizSpaceForIcon) / 2;
Mady Mellor4b80b102016-01-22 08:03:58 -0800468 }
Mady Mellor95d743c2017-01-10 12:05:27 -0800469 mMenuContainer.setTranslationY(translationY);
Mady Mellor4b80b102016-01-22 08:03:58 -0800470 }
471
Mady Mellor95d743c2017-01-10 12:05:27 -0800472 @Override
Aaron Heuckroth266cb342018-09-07 14:52:04 -0400473 public void onParentTranslationUpdate(float translation) {
Mady Mellor95d743c2017-01-10 12:05:27 -0800474 mTranslation = translation;
Mady Mellor761cde12017-01-10 11:36:39 -0800475 if (mAnimating || !mMenuFadedIn) {
476 // Don't adjust when animating, or if the menu hasn't been shown yet.
Mady Mellor4b80b102016-01-22 08:03:58 -0800477 return;
478 }
Mady Mellor95d743c2017-01-10 12:05:27 -0800479 final float fadeThreshold = mParent.getWidth() * 0.3f;
480 final float absTrans = Math.abs(translation);
Mady Mellor4b80b102016-01-22 08:03:58 -0800481 float desiredAlpha = 0;
Mady Mellor3a5e8dd2016-03-12 00:13:23 +0000482 if (absTrans == 0) {
483 desiredAlpha = 0;
484 } else if (absTrans <= fadeThreshold) {
Mady Mellor4b80b102016-01-22 08:03:58 -0800485 desiredAlpha = 1;
486 } else {
Mady Mellor95d743c2017-01-10 12:05:27 -0800487 desiredAlpha = 1 - ((absTrans - fadeThreshold) / (mParent.getWidth() - fadeThreshold));
Mady Mellor4b80b102016-01-22 08:03:58 -0800488 }
Mady Mellor761cde12017-01-10 11:36:39 -0800489 setMenuAlpha(desiredAlpha);
Mady Mellor4b80b102016-01-22 08:03:58 -0800490 }
491
Mady Mellor95d743c2017-01-10 12:05:27 -0800492 @Override
493 public void onClick(View v) {
494 if (mMenuListener == null) {
495 // Nothing to do
496 return;
497 }
498 v.getLocationOnScreen(mIconLocation);
499 mParent.getLocationOnScreen(mParentLocation);
Gus Prevaseb61ce12018-11-07 16:57:40 -0500500 final int centerX = mHorizSpaceForIcon / 2;
Mady Mellor95d743c2017-01-10 12:05:27 -0800501 final int centerY = v.getHeight() / 2;
502 final int x = mIconLocation[0] - mParentLocation[0] + centerX;
503 final int y = mIconLocation[1] - mParentLocation[1] + centerY;
Gus Prevasb79fff62018-12-07 14:38:42 -0500504 if (mMenuItemsByView.containsKey(v)) {
505 mMenuListener.onMenuClicked(mParent, x, y, mMenuItemsByView.get(v));
Aaron Heuckroth266cb342018-09-07 14:52:04 -0400506 }
Mady Mellor95d743c2017-01-10 12:05:27 -0800507 }
508
509 private boolean isMenuLocationChange() {
510 boolean onLeft = mTranslation > mIconPadding;
511 boolean onRight = mTranslation < -mIconPadding;
Aaron Heuckroth266cb342018-09-07 14:52:04 -0400512 if ((isMenuOnLeft() && onRight) || (!isMenuOnLeft() && onLeft)) {
Mady Mellor95d743c2017-01-10 12:05:27 -0800513 return true;
514 }
515 return false;
516 }
517
518 private void setMenuLocation() {
519 boolean showOnLeft = mTranslation > 0;
Aaron Heuckroth266cb342018-09-07 14:52:04 -0400520 if ((mIconsPlaced && showOnLeft == isMenuOnLeft()) || isSnapping() || mMenuContainer == null
Mady Mellord9c2232c2017-04-04 18:45:30 -0700521 || !mMenuContainer.isAttachedToWindow()) {
Mady Mellor95d743c2017-01-10 12:05:27 -0800522 // Do nothing
523 return;
524 }
Gus Prevaseb61ce12018-11-07 16:57:40 -0500525 boolean wasOnLeft = mOnLeft;
526 mOnLeft = showOnLeft;
527 if (wasOnLeft != showOnLeft) {
528 populateMenuViews();
529 }
Mady Mellor95d743c2017-01-10 12:05:27 -0800530 final int count = mMenuContainer.getChildCount();
Mady Mellor95d743c2017-01-10 12:05:27 -0800531 for (int i = 0; i < count; i++) {
532 final View v = mMenuContainer.getChildAt(i);
Selim Cinekb95fd182017-12-21 13:03:32 -0800533 final float left = i * mHorizSpaceForIcon;
534 final float right = mParent.getWidth() - (mHorizSpaceForIcon * (i + 1));
Mady Mellord9c2232c2017-04-04 18:45:30 -0700535 v.setX(showOnLeft ? left : right);
Mady Mellor95d743c2017-01-10 12:05:27 -0800536 }
Mady Mellor95d743c2017-01-10 12:05:27 -0800537 mIconsPlaced = true;
538 }
539
Aaron Heuckroth266cb342018-09-07 14:52:04 -0400540 @VisibleForTesting
541 protected void setMenuAlpha(float alpha) {
Mady Mellor95d743c2017-01-10 12:05:27 -0800542 mAlpha = alpha;
543 if (mMenuContainer == null) {
544 return;
545 }
546 if (alpha == 0) {
547 mMenuFadedIn = false; // Can fade in again once it's gone.
548 mMenuContainer.setVisibility(View.INVISIBLE);
549 } else {
550 mMenuContainer.setVisibility(View.VISIBLE);
551 }
552 final int count = mMenuContainer.getChildCount();
553 for (int i = 0; i < count; i++) {
554 mMenuContainer.getChildAt(i).setAlpha(mAlpha);
555 }
556 }
557
558 /**
559 * Returns the horizontal space in pixels required to display the menu.
560 */
Aaron Heuckroth266cb342018-09-07 14:52:04 -0400561 @VisibleForTesting
562 protected int getSpaceForMenu() {
Mady Mellor95d743c2017-01-10 12:05:27 -0800563 return mHorizSpaceForIcon * mMenuContainer.getChildCount();
564 }
565
566 private final class CheckForDrag implements Runnable {
567 @Override
568 public void run() {
569 final float absTransX = Math.abs(mTranslation);
570 final float bounceBackToMenuWidth = getSpaceForMenu();
571 final float notiThreshold = mParent.getWidth() * 0.4f;
572 if ((!isMenuVisible() || isMenuLocationChange())
573 && absTransX >= bounceBackToMenuWidth * 0.4
574 && absTransX < notiThreshold) {
575 fadeInMenu(notiThreshold);
576 }
577 }
578 }
579
580 private void fadeInMenu(final float notiThreshold) {
Mady Mellor3a5e8dd2016-03-12 00:13:23 +0000581 if (mDismissing || mAnimating) {
582 return;
583 }
Mady Mellor95d743c2017-01-10 12:05:27 -0800584 if (isMenuLocationChange()) {
Mady Mellor761cde12017-01-10 11:36:39 -0800585 setMenuAlpha(0f);
Mady Mellor3a5e8dd2016-03-12 00:13:23 +0000586 }
Mady Mellor95d743c2017-01-10 12:05:27 -0800587 final float transX = mTranslation;
588 final boolean fromLeft = mTranslation > 0;
589 setMenuLocation();
Mady Mellor43c2cd12016-12-12 21:05:13 -0800590 mFadeAnimator = ValueAnimator.ofFloat(mAlpha, 1);
Mady Mellor4b80b102016-01-22 08:03:58 -0800591 mFadeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
592 @Override
593 public void onAnimationUpdate(ValueAnimator animation) {
594 final float absTrans = Math.abs(transX);
595
Mady Mellor95d743c2017-01-10 12:05:27 -0800596 boolean pastMenu = (fromLeft && transX <= notiThreshold)
Mady Mellor4b80b102016-01-22 08:03:58 -0800597 || (!fromLeft && absTrans <= notiThreshold);
Mady Mellor95d743c2017-01-10 12:05:27 -0800598 if (pastMenu && !mMenuFadedIn) {
Mady Mellor761cde12017-01-10 11:36:39 -0800599 setMenuAlpha((float) animation.getAnimatedValue());
Mady Mellor4b80b102016-01-22 08:03:58 -0800600 }
601 }
602 });
603 mFadeAnimator.addListener(new AnimatorListenerAdapter() {
604 @Override
Mady Mellor4b80b102016-01-22 08:03:58 -0800605 public void onAnimationStart(Animator animation) {
Mady Mellor4b80b102016-01-22 08:03:58 -0800606 mAnimating = true;
607 }
608
609 @Override
Mady Mellor3a5e8dd2016-03-12 00:13:23 +0000610 public void onAnimationCancel(Animator animation) {
611 // TODO should animate back to 0f from current alpha
Mady Mellor761cde12017-01-10 11:36:39 -0800612 setMenuAlpha(0f);
Mady Mellor3a5e8dd2016-03-12 00:13:23 +0000613 }
614
615 @Override
Mady Mellor4b80b102016-01-22 08:03:58 -0800616 public void onAnimationEnd(Animator animation) {
Mady Mellor4b80b102016-01-22 08:03:58 -0800617 mAnimating = false;
Mady Mellor761cde12017-01-10 11:36:39 -0800618 mMenuFadedIn = mAlpha == 1;
Mady Mellor4b80b102016-01-22 08:03:58 -0800619 }
620 });
621 mFadeAnimator.setInterpolator(Interpolators.ALPHA_IN);
Mady Mellor43c2cd12016-12-12 21:05:13 -0800622 mFadeAnimator.setDuration(ICON_ALPHA_ANIM_DURATION);
Mady Mellor4b80b102016-01-22 08:03:58 -0800623 mFadeAnimator.start();
624 }
625
Mady Mellorcdd57182016-04-12 19:00:17 -0700626 @Override
Mady Mellor95d743c2017-01-10 12:05:27 -0800627 public void setMenuItems(ArrayList<MenuItem> items) {
628 // Do nothing we use our own for now.
629 // TODO -- handle / allow custom menu items!
Mady Mellorcdd57182016-04-12 19:00:17 -0700630 }
631
Evan Lairde55c6012019-03-13 12:54:37 -0400632 @Override
633 public boolean shouldShowGutsOnSnapOpen() {
Evan Laird30b9b162019-04-24 15:22:24 -0400634 return mIsUsingBidirectionalSwipe;
Evan Lairde55c6012019-03-13 12:54:37 -0400635 }
636
637 @Override
638 public MenuItem menuItemToExposeOnSnap() {
Evan Laird30b9b162019-04-24 15:22:24 -0400639 return mIsUsingBidirectionalSwipe ? mInfoItem : null;
Evan Lairde55c6012019-03-13 12:54:37 -0400640 }
641
642 @Override
643 public Point getRevealAnimationOrigin() {
644 View v = mInfoItem.getMenuView();
645 int menuX = v.getLeft() + v.getPaddingLeft() + (v.getWidth() / 2);
646 int menuY = v.getTop() + v.getPaddingTop() + (v.getHeight() / 2);
647 if (isMenuOnLeft()) {
648 return new Point(menuX, menuY);
649 } else {
650 menuX = mParent.getRight() - menuX;
651 return new Point(menuX, menuY);
652 }
653 }
654
Gus Prevasbd348a82018-11-12 17:14:49 -0500655 static MenuItem createSnoozeItem(Context context) {
Mady Mellor95d743c2017-01-10 12:05:27 -0800656 Resources res = context.getResources();
Mady Mellor95d743c2017-01-10 12:05:27 -0800657 NotificationSnooze content = (NotificationSnooze) LayoutInflater.from(context)
658 .inflate(R.layout.notification_snooze, null, false);
659 String snoozeDescription = res.getString(R.string.notification_menu_snooze_description);
660 MenuItem snooze = new NotificationMenuItem(context, snoozeDescription, content,
661 R.drawable.ic_snooze);
Mady Mellor4c197602017-04-10 17:57:52 -0700662 return snooze;
663 }
Mady Mellor95d743c2017-01-10 12:05:27 -0800664
Gus Prevas5a70a4e2018-11-26 17:16:05 -0500665 static NotificationMenuItem createInfoItem(Context context) {
Mady Mellor4c197602017-04-10 17:57:52 -0700666 Resources res = context.getResources();
667 String infoDescription = res.getString(R.string.notification_menu_gear_description);
668 NotificationInfo infoContent = (NotificationInfo) LayoutInflater.from(context).inflate(
Mady Mellor95d743c2017-01-10 12:05:27 -0800669 R.layout.notification_info, null, false);
Gus Prevas5a70a4e2018-11-26 17:16:05 -0500670 return new NotificationMenuItem(context, infoDescription, infoContent,
671 R.drawable.ic_settings);
672 }
673
674 static NotificationMenuItem createInfoItem(Context context, boolean isCurrentlySilent) {
675 Resources res = context.getResources();
676 String infoDescription = res.getString(R.string.notification_menu_gear_description);
677 NotificationInfo infoContent = (NotificationInfo) LayoutInflater.from(context).inflate(
678 R.layout.notification_info, null, false);
679 int iconResId = isCurrentlySilent
Gus Prevas25d1c2f2018-12-07 14:44:34 -0500680 ? R.drawable.ic_notifications_silence
681 : R.drawable.ic_notifications_alert;
Gus Prevas5a70a4e2018-11-26 17:16:05 -0500682 return new NotificationMenuItem(context, infoDescription, infoContent, iconResId);
Mady Mellor95d743c2017-01-10 12:05:27 -0800683 }
684
Gus Prevasbd348a82018-11-12 17:14:49 -0500685 static MenuItem createAppOpsItem(Context context) {
Julia Reynoldsb5867452018-02-28 16:31:35 -0500686 AppOpsInfo appOpsContent = (AppOpsInfo) LayoutInflater.from(context).inflate(
687 R.layout.app_ops_info, null, false);
688 MenuItem info = new NotificationMenuItem(context, null, appOpsContent,
689 -1 /*don't show in slow swipe menu */);
690 return info;
691 }
692
Mady Mellor95d743c2017-01-10 12:05:27 -0800693 private void addMenuView(MenuItem item, ViewGroup parent) {
694 View menuView = item.getMenuView();
695 if (menuView != null) {
Gus Prevaseb61ce12018-11-07 16:57:40 -0500696 menuView.setAlpha(mAlpha);
Mady Mellor95d743c2017-01-10 12:05:27 -0800697 parent.addView(menuView);
698 menuView.setOnClickListener(this);
699 FrameLayout.LayoutParams lp = (LayoutParams) menuView.getLayoutParams();
Aaron Heuckroth266cb342018-09-07 14:52:04 -0400700 lp.width = mHorizSpaceForIcon;
701 lp.height = mHorizSpaceForIcon;
Mady Mellor95d743c2017-01-10 12:05:27 -0800702 menuView.setLayoutParams(lp);
Mady Mellor4b80b102016-01-22 08:03:58 -0800703 }
Gus Prevasb79fff62018-12-07 14:38:42 -0500704 mMenuItemsByView.put(menuView, item);
Mady Mellor95d743c2017-01-10 12:05:27 -0800705 }
706
Aaron Heuckroth266cb342018-09-07 14:52:04 -0400707 @VisibleForTesting
708 /**
709 * Determine the minimum offset below which the menu should snap back closed.
710 */
711 protected float getSnapBackThreshold() {
712 return getSpaceForMenu() - getMaximumSwipeDistance();
713 }
714
715 /**
716 * Determine the maximum offset above which the parent notification should be dismissed.
717 * @return
718 */
719 @VisibleForTesting
720 protected float getDismissThreshold() {
721 return getParent().getWidth() * SWIPED_FAR_ENOUGH_SIZE_FRACTION;
722 }
723
724 @Override
725 public boolean isWithinSnapMenuThreshold() {
726 float translation = getTranslation();
727 float snapBackThreshold = getSnapBackThreshold();
728 float targetRight = getDismissThreshold();
729 return isMenuOnLeft()
730 ? translation > snapBackThreshold && translation < targetRight
731 : translation < -snapBackThreshold && translation > -targetRight;
732 }
733
734 @Override
735 public boolean isSwipedEnoughToShowMenu() {
736 final float minimumSwipeDistance = getMinimumSwipeDistance();
737 final float translation = getTranslation();
738 return isMenuVisible() && (isMenuOnLeft() ?
739 translation > minimumSwipeDistance
740 : translation < -minimumSwipeDistance);
741 }
742
743 @Override
744 public int getMenuSnapTarget() {
745 return isMenuOnLeft() ? getSpaceForMenu() : -getSpaceForMenu();
746 }
747
748 @Override
749 public boolean shouldSnapBack() {
750 float translation = getTranslation();
751 float targetLeft = getSnapBackThreshold();
752 return isMenuOnLeft() ? translation < targetLeft : translation > -targetLeft;
753 }
754
755 @Override
756 public boolean isSnappedAndOnSameSide() {
757 return isMenuSnapped() && isMenuVisible()
758 && isMenuSnappedOnLeft() == isMenuOnLeft();
759 }
760
761 @Override
762 public boolean canBeDismissed() {
763 return getParent().canViewBeDismissed();
764 }
765
Gus Prevasa18dc572019-01-14 16:11:22 -0500766 @Override
767 public void setDismissRtl(boolean dismissRtl) {
768 mDismissRtl = dismissRtl;
769 if (mMenuContainer != null) {
770 createMenuViews(true, mIsForeground);
771 }
772 }
773
Mady Mellor95d743c2017-01-10 12:05:27 -0800774 public static class NotificationMenuItem implements MenuItem {
775 View mMenuView;
776 GutsContent mGutsContent;
777 String mContentDescription;
778
Julia Reynoldsb5867452018-02-28 16:31:35 -0500779 /**
780 * Add a new 'guts' panel. If iconResId < 0 it will not appear in the slow swipe menu
781 * but can still be exposed via other affordances.
782 */
Gus Prevasbd348a82018-11-12 17:14:49 -0500783 public NotificationMenuItem(Context context, String contentDescription, GutsContent content,
784 int iconResId) {
Mady Mellor95d743c2017-01-10 12:05:27 -0800785 Resources res = context.getResources();
786 int padding = res.getDimensionPixelSize(R.dimen.notification_menu_icon_padding);
787 int tint = res.getColor(R.color.notification_gear_color);
Julia Reynoldsb5867452018-02-28 16:31:35 -0500788 if (iconResId >= 0) {
789 AlphaOptimizedImageView iv = new AlphaOptimizedImageView(context);
790 iv.setPadding(padding, padding, padding, padding);
791 Drawable icon = context.getResources().getDrawable(iconResId);
792 iv.setImageDrawable(icon);
793 iv.setColorFilter(tint);
794 iv.setAlpha(1f);
795 mMenuView = iv;
796 }
Gus Prevasbd348a82018-11-12 17:14:49 -0500797 mContentDescription = contentDescription;
Mady Mellor95d743c2017-01-10 12:05:27 -0800798 mGutsContent = content;
Mady Mellor43c2cd12016-12-12 21:05:13 -0800799 }
Mady Mellorf0625802016-02-11 18:03:48 -0800800
Mady Mellor95d743c2017-01-10 12:05:27 -0800801 @Override
Julia Reynoldsb5867452018-02-28 16:31:35 -0500802 @Nullable
Mady Mellor95d743c2017-01-10 12:05:27 -0800803 public View getMenuView() {
804 return mMenuView;
Mady Mellor3a5e8dd2016-03-12 00:13:23 +0000805 }
Mady Mellor3a5e8dd2016-03-12 00:13:23 +0000806
Mady Mellor95d743c2017-01-10 12:05:27 -0800807 @Override
808 public View getGutsView() {
809 return mGutsContent.getContentView();
Mady Mellorf0625802016-02-11 18:03:48 -0800810 }
Mady Mellor43c2cd12016-12-12 21:05:13 -0800811
Mady Mellor95d743c2017-01-10 12:05:27 -0800812 @Override
813 public String getContentDescription() {
814 return mContentDescription;
815 }
Mady Mellor43c2cd12016-12-12 21:05:13 -0800816 }
Mady Mellor95d743c2017-01-10 12:05:27 -0800817}