1 /*
2  * Copyright (C) 2022 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.dreams.complication;
18 
19 import static com.android.systemui.dreams.complication.dagger.ComplicationModule.COMPLICATIONS_FADE_OUT_DELAY;
20 import static com.android.systemui.dreams.complication.dagger.ComplicationModule.COMPLICATIONS_RESTORE_TIMEOUT;
21 
22 import android.util.Log;
23 import android.view.MotionEvent;
24 import android.view.View;
25 
26 import androidx.annotation.Nullable;
27 
28 import com.android.systemui.complication.Complication;
29 import com.android.systemui.dagger.qualifiers.Main;
30 import com.android.systemui.dreams.DreamOverlayStateController;
31 import com.android.systemui.dreams.touch.DreamOverlayTouchMonitor;
32 import com.android.systemui.dreams.touch.DreamTouchHandler;
33 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
34 import com.android.systemui.touch.TouchInsetManager;
35 import com.android.systemui.util.concurrency.DelayableExecutor;
36 
37 import com.google.common.util.concurrent.ListenableFuture;
38 
39 import java.util.ArrayDeque;
40 import java.util.concurrent.ExecutionException;
41 
42 import javax.inject.Inject;
43 import javax.inject.Named;
44 
45 /**
46  * {@link HideComplicationTouchHandler} is responsible for hiding the overlay complications from
47  * visibility whenever there is touch interactions outside the overlay. The overlay interaction
48  * scope includes touches to the complication plus any touch entry region for gestures as specified
49  * to the {@link DreamOverlayTouchMonitor}.
50  *
51  * This {@link DreamTouchHandler} is also responsible for fading in the complications at the end
52  * of the {@link com.android.systemui.dreams.touch.DreamTouchHandler.TouchSession}.
53  */
54 public class HideComplicationTouchHandler implements DreamTouchHandler {
55     private static final String TAG = "HideComplicationHandler";
56     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
57 
58     private final int mRestoreTimeout;
59     private final int mFadeOutDelay;
60     private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
61     private final DelayableExecutor mExecutor;
62     private final DreamOverlayStateController mOverlayStateController;
63     private final TouchInsetManager mTouchInsetManager;
64     private final Complication.VisibilityController mVisibilityController;
65     private boolean mHidden = false;
66     @Nullable
67     private Runnable mHiddenCallback;
68     private final ArrayDeque<Runnable> mCancelCallbacks = new ArrayDeque<>();
69 
70 
71     private final Runnable mRestoreComplications = new Runnable() {
72         @Override
73         public void run() {
74             mVisibilityController.setVisibility(View.VISIBLE);
75             mHidden = false;
76         }
77     };
78 
79     private final Runnable mHideComplications = new Runnable() {
80         @Override
81         public void run() {
82             if (mOverlayStateController.areExitAnimationsRunning()) {
83                 // Avoid interfering with the exit animations.
84                 return;
85             }
86             mVisibilityController.setVisibility(View.INVISIBLE);
87             mHidden = true;
88             if (mHiddenCallback != null) {
89                 mHiddenCallback.run();
90                 mHiddenCallback = null;
91             }
92         }
93     };
94 
95     @Inject
HideComplicationTouchHandler(Complication.VisibilityController visibilityController, @Named(COMPLICATIONS_RESTORE_TIMEOUT) int restoreTimeout, @Named(COMPLICATIONS_FADE_OUT_DELAY) int fadeOutDelay, TouchInsetManager touchInsetManager, StatusBarKeyguardViewManager statusBarKeyguardViewManager, @Main DelayableExecutor executor, DreamOverlayStateController overlayStateController)96     HideComplicationTouchHandler(Complication.VisibilityController visibilityController,
97             @Named(COMPLICATIONS_RESTORE_TIMEOUT) int restoreTimeout,
98             @Named(COMPLICATIONS_FADE_OUT_DELAY) int fadeOutDelay,
99             TouchInsetManager touchInsetManager,
100             StatusBarKeyguardViewManager statusBarKeyguardViewManager,
101             @Main DelayableExecutor executor,
102             DreamOverlayStateController overlayStateController) {
103         mVisibilityController = visibilityController;
104         mRestoreTimeout = restoreTimeout;
105         mFadeOutDelay = fadeOutDelay;
106         mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
107         mTouchInsetManager = touchInsetManager;
108         mExecutor = executor;
109         mOverlayStateController = overlayStateController;
110     }
111 
112     @Override
onSessionStart(TouchSession session)113     public void onSessionStart(TouchSession session) {
114         if (DEBUG) {
115             Log.d(TAG, "onSessionStart");
116         }
117 
118         final boolean bouncerShowing = mStatusBarKeyguardViewManager.isBouncerShowing();
119 
120         // If other sessions are interested in this touch, do not fade out elements.
121         if (session.getActiveSessionCount() > 1 || bouncerShowing
122                 || mOverlayStateController.areExitAnimationsRunning()) {
123             if (DEBUG) {
124                 Log.d(TAG, "not fading. Active session count: " + session.getActiveSessionCount()
125                         + ". Bouncer showing: " + bouncerShowing);
126             }
127             session.pop();
128             return;
129         }
130 
131         session.registerInputListener(ev -> {
132             if (!(ev instanceof MotionEvent)) {
133                 return;
134             }
135 
136             final MotionEvent motionEvent = (MotionEvent) ev;
137 
138             if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
139                 if (DEBUG) {
140                     Log.d(TAG, "ACTION_DOWN received");
141                 }
142 
143                 final ListenableFuture<Boolean> touchCheck = mTouchInsetManager
144                         .checkWithinTouchRegion(Math.round(motionEvent.getX()),
145                                 Math.round(motionEvent.getY()));
146 
147                 touchCheck.addListener(() -> {
148                     try {
149                         if (!touchCheck.get()) {
150                             // Cancel all pending callbacks.
151                             while (!mCancelCallbacks.isEmpty()) mCancelCallbacks.pop().run();
152                             mCancelCallbacks.add(
153                                     mExecutor.executeDelayed(
154                                             mHideComplications, mFadeOutDelay));
155                         } else {
156                             // If a touch occurred inside the dream overlay touch insets, do not
157                             // handle the touch.
158                             session.pop();
159                         }
160                     } catch (InterruptedException | ExecutionException exception) {
161                         Log.e(TAG, "could not check TouchInsetManager:" + exception);
162                     }
163                 }, mExecutor);
164             } else if (motionEvent.getAction() == MotionEvent.ACTION_CANCEL
165                     || motionEvent.getAction() == MotionEvent.ACTION_UP) {
166                 // End session and initiate delayed reappearance of the complications.
167                 session.pop();
168                 runAfterHidden(() -> mCancelCallbacks.add(
169                         mExecutor.executeDelayed(mRestoreComplications,
170                                 mRestoreTimeout)));
171             }
172         });
173     }
174 
175     /**
176      * Triggers a runnable after complications have been hidden. Will override any previously set
177      * runnable currently waiting for hide to happen.
178      */
runAfterHidden(Runnable runnable)179     private void runAfterHidden(Runnable runnable) {
180         mExecutor.execute(() -> {
181             if (mHidden) {
182                 runnable.run();
183             } else {
184                 mHiddenCallback = runnable;
185             }
186         });
187     }
188 }
189