1 /*
2  * Copyright (C) 2019 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 static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
20 
21 import android.app.StatusBarManager;
22 import android.graphics.RectF;
23 import android.hardware.display.AmbientDisplayConfiguration;
24 import android.media.AudioManager;
25 import android.media.session.MediaSessionLegacyHelper;
26 import android.os.SystemClock;
27 import android.os.UserHandle;
28 import android.provider.Settings;
29 import android.view.GestureDetector;
30 import android.view.InputDevice;
31 import android.view.KeyEvent;
32 import android.view.MotionEvent;
33 import android.view.View;
34 import android.view.ViewGroup;
35 
36 import com.android.internal.annotations.VisibleForTesting;
37 import com.android.systemui.ExpandHelper;
38 import com.android.systemui.R;
39 import com.android.systemui.dock.DockManager;
40 import com.android.systemui.doze.DozeLog;
41 import com.android.systemui.plugins.FalsingManager;
42 import com.android.systemui.shared.plugins.PluginManager;
43 import com.android.systemui.statusbar.CommandQueue;
44 import com.android.systemui.statusbar.DragDownHelper;
45 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
46 import com.android.systemui.statusbar.NotificationShadeDepthController;
47 import com.android.systemui.statusbar.PulseExpansionHandler;
48 import com.android.systemui.statusbar.SuperStatusBarViewFactory;
49 import com.android.systemui.statusbar.SysuiStatusBarStateController;
50 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
51 import com.android.systemui.statusbar.notification.NotificationEntryManager;
52 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
53 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
54 import com.android.systemui.statusbar.policy.KeyguardStateController;
55 import com.android.systemui.tuner.TunerService;
56 import com.android.systemui.util.InjectionInflationController;
57 
58 import java.io.FileDescriptor;
59 import java.io.PrintWriter;
60 
61 import javax.inject.Inject;
62 
63 /**
64  * Controller for {@link NotificationShadeWindowView}.
65  */
66 public class NotificationShadeWindowViewController {
67     private final InjectionInflationController mInjectionInflationController;
68     private final NotificationWakeUpCoordinator mCoordinator;
69     private final PulseExpansionHandler mPulseExpansionHandler;
70     private final DynamicPrivacyController mDynamicPrivacyController;
71     private final KeyguardBypassController mBypassController;
72     private final PluginManager mPluginManager;
73     private final FalsingManager mFalsingManager;
74     private final TunerService mTunerService;
75     private final NotificationLockscreenUserManager mNotificationLockscreenUserManager;
76     private final NotificationEntryManager mNotificationEntryManager;
77     private final KeyguardStateController mKeyguardStateController;
78     private final SysuiStatusBarStateController mStatusBarStateController;
79     private final DozeLog mDozeLog;
80     private final DozeParameters mDozeParameters;
81     private final CommandQueue mCommandQueue;
82     private final NotificationShadeWindowView mView;
83     private final ShadeController mShadeController;
84     private final NotificationShadeDepthController mDepthController;
85 
86     private GestureDetector mGestureDetector;
87     private View mBrightnessMirror;
88     private boolean mTouchActive;
89     private boolean mTouchCancelled;
90     private boolean mExpandAnimationPending;
91     private boolean mExpandAnimationRunning;
92     private NotificationStackScrollLayout mStackScrollLayout;
93     private PhoneStatusBarView mStatusBarView;
94     private PhoneStatusBarTransitions mBarTransitions;
95     private StatusBar mService;
96     private NotificationShadeWindowController mNotificationShadeWindowController;
97     private DragDownHelper mDragDownHelper;
98     private boolean mDoubleTapEnabled;
99     private boolean mSingleTapEnabled;
100     private boolean mExpandingBelowNotch;
101     private final DockManager mDockManager;
102     private final NotificationPanelViewController mNotificationPanelViewController;
103     private final SuperStatusBarViewFactory mStatusBarViewFactory;
104 
105     // Used for determining view / touch intersection
106     private int[] mTempLocation = new int[2];
107     private RectF mTempRect = new RectF();
108     private boolean mIsTrackingBarGesture = false;
109 
110     @Inject
NotificationShadeWindowViewController( InjectionInflationController injectionInflationController, NotificationWakeUpCoordinator coordinator, PulseExpansionHandler pulseExpansionHandler, DynamicPrivacyController dynamicPrivacyController, KeyguardBypassController bypassController, FalsingManager falsingManager, PluginManager pluginManager, TunerService tunerService, NotificationLockscreenUserManager notificationLockscreenUserManager, NotificationEntryManager notificationEntryManager, KeyguardStateController keyguardStateController, SysuiStatusBarStateController statusBarStateController, DozeLog dozeLog, DozeParameters dozeParameters, CommandQueue commandQueue, ShadeController shadeController, DockManager dockManager, NotificationShadeDepthController depthController, NotificationShadeWindowView notificationShadeWindowView, NotificationPanelViewController notificationPanelViewController, SuperStatusBarViewFactory statusBarViewFactory)111     public NotificationShadeWindowViewController(
112             InjectionInflationController injectionInflationController,
113             NotificationWakeUpCoordinator coordinator,
114             PulseExpansionHandler pulseExpansionHandler,
115             DynamicPrivacyController dynamicPrivacyController,
116             KeyguardBypassController bypassController,
117             FalsingManager falsingManager,
118             PluginManager pluginManager,
119             TunerService tunerService,
120             NotificationLockscreenUserManager notificationLockscreenUserManager,
121             NotificationEntryManager notificationEntryManager,
122             KeyguardStateController keyguardStateController,
123             SysuiStatusBarStateController statusBarStateController,
124             DozeLog dozeLog,
125             DozeParameters dozeParameters,
126             CommandQueue commandQueue,
127             ShadeController shadeController,
128             DockManager dockManager,
129             NotificationShadeDepthController depthController,
130             NotificationShadeWindowView notificationShadeWindowView,
131             NotificationPanelViewController notificationPanelViewController,
132             SuperStatusBarViewFactory statusBarViewFactory) {
133         mInjectionInflationController = injectionInflationController;
134         mCoordinator = coordinator;
135         mPulseExpansionHandler = pulseExpansionHandler;
136         mDynamicPrivacyController = dynamicPrivacyController;
137         mBypassController = bypassController;
138         mFalsingManager = falsingManager;
139         mPluginManager = pluginManager;
140         mTunerService = tunerService;
141         mNotificationLockscreenUserManager = notificationLockscreenUserManager;
142         mNotificationEntryManager = notificationEntryManager;
143         mKeyguardStateController = keyguardStateController;
144         mStatusBarStateController = statusBarStateController;
145         mDozeLog = dozeLog;
146         mDozeParameters = dozeParameters;
147         mCommandQueue = commandQueue;
148         mView = notificationShadeWindowView;
149         mShadeController = shadeController;
150         mDockManager = dockManager;
151         mNotificationPanelViewController = notificationPanelViewController;
152         mDepthController = depthController;
153         mStatusBarViewFactory = statusBarViewFactory;
154 
155         // This view is not part of the newly inflated expanded status bar.
156         mBrightnessMirror = mView.findViewById(R.id.brightness_mirror);
157     }
158 
159     /** Inflates the {@link R.layout#status_bar_expanded} layout and sets it up. */
setupExpandedStatusBar()160     public void setupExpandedStatusBar() {
161         mStackScrollLayout = mView.findViewById(R.id.notification_stack_scroller);
162 
163         TunerService.Tunable tunable = (key, newValue) -> {
164             AmbientDisplayConfiguration configuration =
165                     new AmbientDisplayConfiguration(mView.getContext());
166             switch (key) {
167                 case Settings.Secure.DOZE_DOUBLE_TAP_GESTURE:
168                     mDoubleTapEnabled = configuration.doubleTapGestureEnabled(
169                             UserHandle.USER_CURRENT);
170                     break;
171                 case Settings.Secure.DOZE_TAP_SCREEN_GESTURE:
172                     mSingleTapEnabled = configuration.tapGestureEnabled(UserHandle.USER_CURRENT);
173             }
174         };
175         mTunerService.addTunable(tunable,
176                 Settings.Secure.DOZE_DOUBLE_TAP_GESTURE,
177                 Settings.Secure.DOZE_TAP_SCREEN_GESTURE);
178 
179         GestureDetector.SimpleOnGestureListener gestureListener =
180                 new GestureDetector.SimpleOnGestureListener() {
181                     @Override
182                     public boolean onSingleTapConfirmed(MotionEvent e) {
183                         if (mSingleTapEnabled && !mDockManager.isDocked()) {
184                             mService.wakeUpIfDozing(
185                                     SystemClock.uptimeMillis(), mView, "SINGLE_TAP");
186                             return true;
187                         }
188                         return false;
189                     }
190 
191                     @Override
192                     public boolean onDoubleTap(MotionEvent e) {
193                         if (mDoubleTapEnabled || mSingleTapEnabled) {
194                             mService.wakeUpIfDozing(
195                                     SystemClock.uptimeMillis(), mView, "DOUBLE_TAP");
196                             return true;
197                         }
198                         return false;
199                     }
200                 };
201         mGestureDetector = new GestureDetector(mView.getContext(), gestureListener);
202 
203         mView.setInteractionEventHandler(new NotificationShadeWindowView.InteractionEventHandler() {
204             @Override
205             public Boolean handleDispatchTouchEvent(MotionEvent ev) {
206                 boolean isDown = ev.getActionMasked() == MotionEvent.ACTION_DOWN;
207                 boolean isUp = ev.getActionMasked() == MotionEvent.ACTION_UP;
208                 boolean isCancel = ev.getActionMasked() == MotionEvent.ACTION_CANCEL;
209 
210                 boolean expandingBelowNotch = mExpandingBelowNotch;
211                 if (isUp || isCancel) {
212                     mExpandingBelowNotch = false;
213                 }
214 
215                 // Reset manual touch dispatch state here but make sure the UP/CANCEL event still
216                 // gets
217                 // delivered.
218 
219                 if (!isCancel && mService.shouldIgnoreTouch()) {
220                     return false;
221                 }
222                 if (isDown && mNotificationPanelViewController.isFullyCollapsed()) {
223                     mNotificationPanelViewController.startExpandLatencyTracking();
224                 }
225                 if (isDown) {
226                     setTouchActive(true);
227                     mTouchCancelled = false;
228                 } else if (ev.getActionMasked() == MotionEvent.ACTION_UP
229                         || ev.getActionMasked() == MotionEvent.ACTION_CANCEL) {
230                     setTouchActive(false);
231                 }
232                 if (mTouchCancelled || mExpandAnimationRunning || mExpandAnimationPending) {
233                     return false;
234                 }
235                 mFalsingManager.onTouchEvent(ev, mView.getWidth(), mView.getHeight());
236                 mGestureDetector.onTouchEvent(ev);
237                 if (mBrightnessMirror != null
238                         && mBrightnessMirror.getVisibility() == View.VISIBLE) {
239                     // Disallow new pointers while the brightness mirror is visible. This is so that
240                     // you can't touch anything other than the brightness slider while the mirror is
241                     // showing and the rest of the panel is transparent.
242                     if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) {
243                         return false;
244                     }
245                 }
246                 if (isDown) {
247                     mStackScrollLayout.closeControlsIfOutsideTouch(ev);
248                 }
249                 if (mStatusBarStateController.isDozing()) {
250                     mService.mDozeScrimController.extendPulse();
251                 }
252                 // In case we start outside of the view bounds (below the status bar), we need to
253                 // dispatch
254                 // the touch manually as the view system can't accommodate for touches outside of
255                 // the
256                 // regular view bounds.
257                 if (isDown && ev.getY() >= mView.getBottom()) {
258                     mExpandingBelowNotch = true;
259                     expandingBelowNotch = true;
260                 }
261                 if (expandingBelowNotch) {
262                     return mStatusBarView.dispatchTouchEvent(ev);
263                 }
264 
265                 if (!mIsTrackingBarGesture && isDown
266                         && mNotificationPanelViewController.isFullyCollapsed()) {
267                     float x = ev.getRawX();
268                     float y = ev.getRawY();
269                     if (isIntersecting(mStatusBarView, x, y)) {
270                         if (mService.isSameStatusBarState(WINDOW_STATE_SHOWING)) {
271                             mIsTrackingBarGesture = true;
272                             return mStatusBarView.dispatchTouchEvent(ev);
273                         } else { // it's hidden or hiding, don't send to notification shade.
274                             return true;
275                         }
276                     }
277                 } else if (mIsTrackingBarGesture) {
278                     final boolean sendToNotification = mStatusBarView.dispatchTouchEvent(ev);
279                     if (isUp || isCancel) {
280                         mIsTrackingBarGesture = false;
281                     }
282                     return sendToNotification;
283                 }
284 
285                 return null;
286             }
287 
288             @Override
289             public boolean shouldInterceptTouchEvent(MotionEvent ev) {
290                 if (mStatusBarStateController.isDozing() && !mService.isPulsing()
291                         && !mDockManager.isDocked()) {
292                     // Capture all touch events in always-on.
293                     return true;
294                 }
295                 boolean intercept = false;
296                 if (mNotificationPanelViewController.isFullyExpanded()
297                         && mDragDownHelper.isDragDownEnabled()
298                         && !mService.isBouncerShowing()
299                         && !mStatusBarStateController.isDozing()) {
300                     intercept = mDragDownHelper.onInterceptTouchEvent(ev);
301                 }
302 
303                 return intercept;
304 
305             }
306 
307             @Override
308             public void didIntercept(MotionEvent ev) {
309                 MotionEvent cancellation = MotionEvent.obtain(ev);
310                 cancellation.setAction(MotionEvent.ACTION_CANCEL);
311                 mStackScrollLayout.onInterceptTouchEvent(cancellation);
312                 mNotificationPanelViewController.getView().onInterceptTouchEvent(cancellation);
313                 cancellation.recycle();
314             }
315 
316             @Override
317             public boolean handleTouchEvent(MotionEvent ev) {
318                 boolean handled = false;
319                 if (mStatusBarStateController.isDozing()) {
320                     handled = !mService.isPulsing();
321                 }
322                 if ((mDragDownHelper.isDragDownEnabled() && !handled)
323                         || mDragDownHelper.isDraggingDown()) {
324                     // we still want to finish our drag down gesture when locking the screen
325                     handled = mDragDownHelper.onTouchEvent(ev);
326                 }
327 
328                 return handled;
329             }
330 
331             @Override
332             public void didNotHandleTouchEvent(MotionEvent ev) {
333                 final int action = ev.getActionMasked();
334                 if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
335                     mService.setInteracting(StatusBarManager.WINDOW_STATUS_BAR, false);
336                 }
337             }
338 
339             @Override
340             public boolean interceptMediaKey(KeyEvent event) {
341                 return mService.interceptMediaKey(event);
342             }
343 
344             @Override
345             public boolean dispatchKeyEvent(KeyEvent event) {
346                 boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
347                 switch (event.getKeyCode()) {
348                     case KeyEvent.KEYCODE_BACK:
349                         if (!down) {
350                             mService.onBackPressed();
351                         }
352                         return true;
353                     case KeyEvent.KEYCODE_MENU:
354                         if (!down) {
355                             return mService.onMenuPressed();
356                         }
357                         break;
358                     case KeyEvent.KEYCODE_SPACE:
359                         if (!down) {
360                             return mService.onSpacePressed();
361                         }
362                         break;
363                     case KeyEvent.KEYCODE_VOLUME_DOWN:
364                     case KeyEvent.KEYCODE_VOLUME_UP:
365                         if (mStatusBarStateController.isDozing()) {
366                             MediaSessionLegacyHelper.getHelper(mView.getContext())
367                                     .sendVolumeKeyEvent(
368                                             event, AudioManager.USE_DEFAULT_STREAM_TYPE, true);
369                             return true;
370                         }
371                         break;
372                 }
373                 return false;
374             }
375         });
376 
377         mView.setOnHierarchyChangeListener(new ViewGroup.OnHierarchyChangeListener() {
378             @Override
379             public void onChildViewAdded(View parent, View child) {
380                 if (child.getId() == R.id.brightness_mirror) {
381                     mBrightnessMirror = child;
382                 }
383             }
384 
385             @Override
386             public void onChildViewRemoved(View parent, View child) {
387             }
388         });
389 
390         ExpandHelper.Callback expandHelperCallback = mStackScrollLayout.getExpandHelperCallback();
391         DragDownHelper.DragDownCallback dragDownCallback = mStackScrollLayout.getDragDownCallback();
392         setDragDownHelper(
393                 new DragDownHelper(
394                         mView.getContext(), mView, expandHelperCallback,
395                         dragDownCallback, mFalsingManager));
396 
397         mDepthController.setRoot(mView);
398         mNotificationPanelViewController.addExpansionListener(mDepthController);
399     }
400 
getView()401     public NotificationShadeWindowView getView() {
402         return mView;
403     }
404 
setTouchActive(boolean touchActive)405     public void setTouchActive(boolean touchActive) {
406         mTouchActive = touchActive;
407     }
408 
cancelCurrentTouch()409     public void cancelCurrentTouch() {
410         if (mTouchActive) {
411             final long now = SystemClock.uptimeMillis();
412             MotionEvent event = MotionEvent.obtain(now, now,
413                     MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
414             event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
415             mView.dispatchTouchEvent(event);
416             event.recycle();
417             mTouchCancelled = true;
418         }
419     }
420 
dump(FileDescriptor fd, PrintWriter pw, String[] args)421     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
422         pw.print("  mExpandAnimationPending=");
423         pw.println(mExpandAnimationPending);
424         pw.print("  mExpandAnimationRunning=");
425         pw.println(mExpandAnimationRunning);
426         pw.print("  mTouchCancelled=");
427         pw.println(mTouchCancelled);
428         pw.print("  mTouchActive=");
429         pw.println(mTouchActive);
430     }
431 
setExpandAnimationPending(boolean pending)432     public void setExpandAnimationPending(boolean pending) {
433         if (mExpandAnimationPending != pending) {
434             mExpandAnimationPending = pending;
435             mNotificationShadeWindowController
436                     .setLaunchingActivity(mExpandAnimationPending | mExpandAnimationRunning);
437         }
438     }
439 
setExpandAnimationRunning(boolean running)440     public void setExpandAnimationRunning(boolean running) {
441         if (mExpandAnimationRunning != running) {
442             mExpandAnimationRunning = running;
443             mNotificationShadeWindowController
444                     .setLaunchingActivity(mExpandAnimationPending | mExpandAnimationRunning);
445         }
446     }
447 
cancelExpandHelper()448     public void cancelExpandHelper() {
449         if (mStackScrollLayout != null) {
450             mStackScrollLayout.cancelExpandHelper();
451         }
452     }
453 
getBarTransitions()454     public PhoneStatusBarTransitions getBarTransitions() {
455         return mBarTransitions;
456     }
457 
setStatusBarView(PhoneStatusBarView statusBarView)458     public void setStatusBarView(PhoneStatusBarView statusBarView) {
459         mStatusBarView = statusBarView;
460         if (statusBarView != null && mStatusBarViewFactory != null) {
461             mBarTransitions = new PhoneStatusBarTransitions(
462                     statusBarView,
463                     mStatusBarViewFactory.getStatusBarWindowView()
464                             .findViewById(R.id.status_bar_container));
465         }
466     }
467 
setService(StatusBar statusBar, NotificationShadeWindowController controller)468     public void setService(StatusBar statusBar, NotificationShadeWindowController controller) {
469         mService = statusBar;
470         mNotificationShadeWindowController = controller;
471     }
472 
473     @VisibleForTesting
setDragDownHelper(DragDownHelper dragDownHelper)474     void setDragDownHelper(DragDownHelper dragDownHelper) {
475         mDragDownHelper = dragDownHelper;
476     }
477 
isIntersecting(View view, float x, float y)478     private boolean isIntersecting(View view, float x, float y) {
479         mTempLocation = view.getLocationOnScreen();
480         mTempRect.set(mTempLocation[0], mTempLocation[1], mTempLocation[0] + view.getWidth(),
481                 mTempLocation[1] + view.getHeight());
482         return mTempRect.contains(x, y);
483     }
484 }
485