• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 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
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.statusbar.notification.headsup;
18 
19 import android.annotation.Nullable;
20 import android.content.Context;
21 import android.os.RemoteException;
22 import android.view.MotionEvent;
23 import android.view.ViewConfiguration;
24 
25 import com.android.internal.statusbar.IStatusBarService;
26 import com.android.systemui.Gefingerpoken;
27 import com.android.systemui.scene.shared.flag.SceneContainerFlag;
28 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
29 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
30 import com.android.systemui.statusbar.notification.row.ExpandableView;
31 
32 /**
33  * A helper class to handle touches on the heads-up views.
34  */
35 public class HeadsUpTouchHelper implements Gefingerpoken {
36 
37     private final HeadsUpManager mHeadsUpManager;
38     private final IStatusBarService mStatusBarService;
39     private final Callback mCallback;
40     private int mTrackingPointer;
41     private final float mTouchSlop;
42     private float mInitialTouchX;
43     private float mInitialTouchY;
44     private boolean mTouchingHeadsUpView;
45     private boolean mTrackingHeadsUp;
46     private boolean mCollapseSnoozes;
47     private final HeadsUpNotificationViewController mPanel;
48     private ExpandableNotificationRow mPickedChild;
49 
HeadsUpTouchHelper(HeadsUpManager headsUpManager, IStatusBarService statusBarService, Callback callback, HeadsUpNotificationViewController notificationPanelView)50     public HeadsUpTouchHelper(HeadsUpManager headsUpManager,
51             IStatusBarService statusBarService,
52             Callback callback,
53             HeadsUpNotificationViewController notificationPanelView) {
54         mHeadsUpManager = headsUpManager;
55         mStatusBarService = statusBarService;
56         mCallback = callback;
57         mPanel = notificationPanelView;
58         Context context = mCallback.getContext();
59         final ViewConfiguration configuration = ViewConfiguration.get(context);
60         mTouchSlop = configuration.getScaledTouchSlop();
61     }
62 
isTrackingHeadsUp()63     public boolean isTrackingHeadsUp() {
64         return mTrackingHeadsUp;
65     }
66 
67     @Override
onInterceptTouchEvent(MotionEvent event)68     public boolean onInterceptTouchEvent(MotionEvent event) {
69         if (!mTouchingHeadsUpView && event.getActionMasked() != MotionEvent.ACTION_DOWN) {
70             return false;
71         }
72         int pointerIndex = event.findPointerIndex(mTrackingPointer);
73         if (pointerIndex < 0) {
74             pointerIndex = 0;
75             mTrackingPointer = event.getPointerId(pointerIndex);
76         }
77         final float x = event.getX(pointerIndex);
78         final float y = event.getY(pointerIndex);
79         switch (event.getActionMasked()) {
80             case MotionEvent.ACTION_DOWN:
81                 mInitialTouchY = y;
82                 mInitialTouchX = x;
83                 setTrackingHeadsUp(false);
84                 ExpandableView child = mCallback.getChildAtRawPosition(x, y);
85                 mTouchingHeadsUpView = false;
86                 if (child instanceof ExpandableNotificationRow) {
87                     ExpandableNotificationRow pickedChild = (ExpandableNotificationRow) child;
88                     mTouchingHeadsUpView = !mCallback.isExpanded()
89                             && pickedChild.isHeadsUp() && pickedChild.isPinned();
90                     if (mTouchingHeadsUpView) {
91                         mPickedChild = pickedChild;
92                     }
93                 } else if (child == null && !mCallback.isExpanded()) {
94                     // We might touch above the visible heads up child, but then we still would
95                     // like to capture it.
96                     NotificationEntry topEntry = mHeadsUpManager.getTopEntry();
97                     if (topEntry != null && topEntry.isRowPinned()) {
98                         mPickedChild = topEntry.getRow();
99                         mTouchingHeadsUpView = true;
100                     }
101                 }
102                 break;
103             case MotionEvent.ACTION_POINTER_UP:
104                 final int upPointer = event.getPointerId(event.getActionIndex());
105                 if (mTrackingPointer == upPointer) {
106                     // gesture is ongoing, find a new pointer to track
107                     final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
108                     mTrackingPointer = event.getPointerId(newIndex);
109                     mInitialTouchX = event.getX(newIndex);
110                     mInitialTouchY = event.getY(newIndex);
111                 }
112                 break;
113 
114             case MotionEvent.ACTION_MOVE:
115                 final float h = y - mInitialTouchY;
116                 if (mTouchingHeadsUpView && Math.abs(h) > mTouchSlop
117                         && Math.abs(h) > Math.abs(x - mInitialTouchX)) {
118                     if (!SceneContainerFlag.isEnabled()) {
119                         setTrackingHeadsUp(true);
120                         mCollapseSnoozes = h < 0;
121                         mInitialTouchX = x;
122                         mInitialTouchY = y;
123                         int startHeight = (int) (mPickedChild.getActualHeight()
124                                 + mPickedChild.getTranslationY());
125                         mPanel.setHeadsUpDraggingStartingHeight(startHeight);
126                         mPanel.startExpand(x, y, true /* startTracking */, startHeight);
127 
128                         // This call needs to be after the expansion start otherwise we will get a
129                         // flicker of one frame as it's not expanded yet.
130                         mHeadsUpManager.unpinAll(true);
131 
132                         clearNotificationEffects();
133                         endMotion();
134                     }
135                     return true;
136                 }
137                 break;
138 
139             case MotionEvent.ACTION_CANCEL:
140             case MotionEvent.ACTION_UP:
141                 if (mPickedChild != null && mTouchingHeadsUpView) {
142                     // We may swallow this click if the heads up just came in.
143                     if (mHeadsUpManager.shouldSwallowClick(
144                             mPickedChild.getKey())) {
145                         endMotion();
146                         return true;
147                     }
148                 }
149                 endMotion();
150                 break;
151         }
152         return false;
153     }
154 
155     private void setTrackingHeadsUp(boolean tracking) {
156         mTrackingHeadsUp = tracking;
157         mHeadsUpManager.setTrackingHeadsUp(tracking);
158         mPanel.setTrackedHeadsUp(tracking ? mPickedChild : null);
159     }
160 
161     public void notifyFling(boolean collapse) {
162         if (collapse && mCollapseSnoozes) {
163             mHeadsUpManager.snooze();
164         }
165         mCollapseSnoozes = false;
166     }
167 
168     @Override
169     public boolean onTouchEvent(MotionEvent event) {
170         if (SceneContainerFlag.isEnabled()) {
171             int pointerIndex = event.findPointerIndex(mTrackingPointer);
172             if (pointerIndex < 0) {
173                 pointerIndex = 0;
174                 mTrackingPointer = event.getPointerId(pointerIndex);
175             }
176             final float x = event.getX(pointerIndex);
177             final float y = event.getY(pointerIndex);
178             switch (event.getActionMasked()) {
179                 case MotionEvent.ACTION_POINTER_UP:
180                     final int upPointer = event.getPointerId(event.getActionIndex());
181                     if (mTrackingPointer == upPointer) {
182                         // gesture is ongoing, find a new pointer to track
183                         final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
184                         mTrackingPointer = event.getPointerId(newIndex);
185                         mInitialTouchX = event.getX(newIndex);
186                         mInitialTouchY = event.getY(newIndex);
187                     }
188                     break;
189                 case MotionEvent.ACTION_MOVE:
190                     final float h = y - mInitialTouchY;
191                     if (mTouchingHeadsUpView && Math.abs(h) > mTouchSlop
192                             && Math.abs(h) > Math.abs(x - mInitialTouchX)) {
193                         setTrackingHeadsUp(true);
194                         mCollapseSnoozes = h < 0;
195                         mInitialTouchX = x;
196                         mInitialTouchY = y;
197                         int startHeight = (int) (mPickedChild.getActualHeight()
198                                 + mPickedChild.getTranslationY());
199                         mPanel.setHeadsUpDraggingStartingHeight(startHeight);
200                         mPanel.startExpand(x, y, true /* startTracking */, startHeight);
201 
202                         clearNotificationEffects();
203                         endMotion();
204                         return true;
205                     }
206                     break;
207                 case MotionEvent.ACTION_CANCEL:
208                 case MotionEvent.ACTION_UP:
209                     if (mPickedChild != null && mTouchingHeadsUpView) {
210                         // We may swallow this click if the heads up just came in.
211                         if (mHeadsUpManager.shouldSwallowClick(
212                                 mPickedChild.getKey())) {
213                             endMotion();
214                             setTrackingHeadsUp(false);
215                             return true;
216                         }
217                     }
218                     endMotion();
219                     setTrackingHeadsUp(false);
220                     return false;
221             }
222             return false;
223         } else {
224             if (!mTrackingHeadsUp) {
225                 return false;
226             }
227             switch (event.getActionMasked()) {
228                 case MotionEvent.ACTION_UP:
229                 case MotionEvent.ACTION_CANCEL:
230                     endMotion();
231                     setTrackingHeadsUp(false);
232                     break;
233             }
234             return true;
235         }
236     }
237 
endMotion()238     private void endMotion() {
239         mTrackingPointer = -1;
240         mPickedChild = null;
241         mTouchingHeadsUpView = false;
242     }
243 
clearNotificationEffects()244     private void clearNotificationEffects() {
245         try {
246             mStatusBarService.clearNotificationEffects();
247         } catch (RemoteException e) {
248             // Won't fail unless the world has ended.
249         }
250     }
251 
252     public interface Callback {
253         ExpandableView getChildAtRawPosition(float touchX, float touchY);
254         boolean isExpanded();
255         Context getContext();
256     }
257 
258     /** The controller for a view that houses heads up notifications. */
259     public interface HeadsUpNotificationViewController {
260         /** Called when a HUN is dragged to indicate the starting height for shade motion. */
261         void setHeadsUpDraggingStartingHeight(int startHeight);
262 
263         /** Sets notification that is being expanded. */
264         void setTrackedHeadsUp(@Nullable ExpandableNotificationRow expandableNotificationRow);
265 
266         /** Called when a MotionEvent is about to trigger expansion. */
267         void startExpand(float newX, float newY, boolean startTracking, float expandedHeight);
268     }
269 }
270