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