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