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