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 package com.android.launcher3.taskbar.overlay; 17 18 import static android.view.KeyEvent.ACTION_UP; 19 import static android.view.KeyEvent.KEYCODE_BACK; 20 import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION; 21 22 import android.content.Context; 23 import android.graphics.Insets; 24 import android.media.permission.SafeCloseable; 25 import android.view.KeyEvent; 26 import android.view.MotionEvent; 27 import android.view.View; 28 import android.view.ViewTreeObserver; 29 import android.view.WindowInsets; 30 31 import androidx.annotation.NonNull; 32 33 import com.android.app.viewcapture.SettingsAwareViewCapture; 34 import com.android.launcher3.AbstractFloatingView; 35 import com.android.launcher3.testing.TestLogging; 36 import com.android.launcher3.testing.shared.TestProtocol; 37 import com.android.launcher3.util.DisplayController; 38 import com.android.launcher3.util.TouchController; 39 import com.android.launcher3.views.BaseDragLayer; 40 41 import java.util.List; 42 import java.util.concurrent.CopyOnWriteArrayList; 43 44 /** Root drag layer for the Taskbar overlay window. */ 45 public class TaskbarOverlayDragLayer extends 46 BaseDragLayer<TaskbarOverlayContext> implements 47 ViewTreeObserver.OnComputeInternalInsetsListener { 48 49 private SafeCloseable mViewCaptureCloseable; 50 private final List<OnClickListener> mOnClickListeners = new CopyOnWriteArrayList<>(); 51 private final TouchController mClickListenerTouchController = new TouchController() { 52 @Override 53 public boolean onControllerTouchEvent(MotionEvent ev) { 54 if (ev.getActionMasked() == MotionEvent.ACTION_UP) { 55 for (OnClickListener listener : mOnClickListeners) { 56 listener.onClick(TaskbarOverlayDragLayer.this); 57 } 58 } 59 return false; 60 } 61 62 @Override 63 public boolean onControllerInterceptTouchEvent(MotionEvent ev) { 64 for (int i = 0; i < getChildCount(); i++) { 65 if (isEventOverView(getChildAt(i), ev)) { 66 return false; 67 } 68 } 69 return true; 70 } 71 }; 72 TaskbarOverlayDragLayer(Context context)73 TaskbarOverlayDragLayer(Context context) { 74 super(context, null, 1); 75 setClipChildren(false); 76 recreateControllers(); 77 } 78 79 @Override onAttachedToWindow()80 protected void onAttachedToWindow() { 81 super.onAttachedToWindow(); 82 getViewTreeObserver().addOnComputeInternalInsetsListener(this); 83 mViewCaptureCloseable = SettingsAwareViewCapture.getInstance(getContext()) 84 .startCapture(getRootView(), ".TaskbarOverlay"); 85 } 86 87 @Override onDetachedFromWindow()88 protected void onDetachedFromWindow() { 89 super.onDetachedFromWindow(); 90 getViewTreeObserver().removeOnComputeInternalInsetsListener(this); 91 mViewCaptureCloseable.close(); 92 } 93 94 @Override recreateControllers()95 public void recreateControllers() { 96 mControllers = mOnClickListeners.isEmpty() 97 ? new TouchController[]{mActivity.getDragController()} 98 : new TouchController[] { 99 mActivity.getDragController(), mClickListenerTouchController}; 100 } 101 102 @Override dispatchTouchEvent(MotionEvent ev)103 public boolean dispatchTouchEvent(MotionEvent ev) { 104 TestLogging.recordMotionEvent(TestProtocol.SEQUENCE_MAIN, "Touch event", ev); 105 return super.dispatchTouchEvent(ev); 106 } 107 108 @Override dispatchKeyEvent(KeyEvent event)109 public boolean dispatchKeyEvent(KeyEvent event) { 110 if (event.getAction() == ACTION_UP && event.getKeyCode() == KEYCODE_BACK) { 111 AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mActivity); 112 if (topView != null && topView.canHandleBack()) { 113 topView.onBackInvoked(); 114 return true; 115 } 116 } 117 return super.dispatchKeyEvent(event); 118 } 119 120 @Override onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo)121 public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { 122 if (mActivity.isAnySystemDragInProgress()) { 123 inoutInfo.touchableRegion.setEmpty(); 124 inoutInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION); 125 } 126 } 127 128 @Override onApplyWindowInsets(WindowInsets insets)129 public WindowInsets onApplyWindowInsets(WindowInsets insets) { 130 insets = updateInsetsDueToStashing(insets); 131 setInsets(insets.getInsets(WindowInsets.Type.systemBars()).toRect()); 132 return insets; 133 } 134 135 @Override onViewRemoved(View child)136 public void onViewRemoved(View child) { 137 super.onViewRemoved(child); 138 mActivity.getOverlayController().maybeCloseWindow(); 139 } 140 141 /** 142 * Adds the given callback to clicks to this drag layer. 143 * <p> 144 * Clicks are only accepted on this drag layer if they fall within this drag layer's bounds and 145 * outside the bounds of all child views. 146 * <p> 147 * If the click falls within the bounds of a child view, then this callback does not run and 148 * that child can optionally handle it. 149 */ addOnClickListener(@onNull OnClickListener listener)150 private void addOnClickListener(@NonNull OnClickListener listener) { 151 boolean wasEmpty = mOnClickListeners.isEmpty(); 152 mOnClickListeners.add(listener); 153 if (wasEmpty) { 154 recreateControllers(); 155 } 156 } 157 158 /** 159 * Removes the given on click callback. 160 * <p> 161 * No-op if the callback was never added. 162 */ removeOnClickListener(@onNull OnClickListener listener)163 private void removeOnClickListener(@NonNull OnClickListener listener) { 164 boolean wasEmpty = mOnClickListeners.isEmpty(); 165 mOnClickListeners.remove(listener); 166 if (!wasEmpty && mOnClickListeners.isEmpty()) { 167 recreateControllers(); 168 } 169 } 170 171 /** 172 * Queues the given callback on the next click on this drag layer. 173 * <p> 174 * Once run, this callback is immediately removed. 175 */ runOnClickOnce(@onNull OnClickListener listener)176 public void runOnClickOnce(@NonNull OnClickListener listener) { 177 addOnClickListener(new OnClickListener() { 178 @Override 179 public void onClick(View v) { 180 listener.onClick(v); 181 removeOnClickListener(this); 182 } 183 }); 184 } 185 186 /** 187 * Taskbar automatically stashes when opening all apps, but we don't report the insets as 188 * changing to avoid moving the underlying app. But internally, the apps view should still 189 * layout according to the stashed insets rather than the unstashed insets. So this method 190 * does two things: 191 * 1) Sets navigationBars bottom inset to stashedHeight. 192 * 2) Sets tappableInsets bottom inset to 0. 193 */ updateInsetsDueToStashing(WindowInsets oldInsets)194 private WindowInsets updateInsetsDueToStashing(WindowInsets oldInsets) { 195 if (!DisplayController.isTransientTaskbar(mActivity)) { 196 return oldInsets; 197 } 198 WindowInsets.Builder updatedInsetsBuilder = new WindowInsets.Builder(oldInsets); 199 200 Insets oldNavInsets = oldInsets.getInsets(WindowInsets.Type.navigationBars()); 201 Insets newNavInsets = Insets.of(oldNavInsets.left, oldNavInsets.top, oldNavInsets.right, 202 mActivity.getStashedTaskbarHeight()); 203 updatedInsetsBuilder.setInsets(WindowInsets.Type.navigationBars(), newNavInsets); 204 205 Insets oldTappableInsets = oldInsets.getInsets(WindowInsets.Type.tappableElement()); 206 Insets newTappableInsets = Insets.of(oldTappableInsets.left, oldTappableInsets.top, 207 oldTappableInsets.right, 0); 208 updatedInsetsBuilder.setInsets(WindowInsets.Type.tappableElement(), newTappableInsets); 209 210 return updatedInsetsBuilder.build(); 211 } 212 } 213