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.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
19 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_CONSUME_IME_INSETS;
20 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
21 
22 import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
23 import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE;
24 import static com.android.launcher3.LauncherState.ALL_APPS;
25 
26 import android.annotation.SuppressLint;
27 import android.content.Context;
28 import android.graphics.PixelFormat;
29 import android.view.Gravity;
30 import android.view.MotionEvent;
31 import android.view.WindowManager;
32 import android.view.WindowManager.LayoutParams;
33 
34 import androidx.annotation.Nullable;
35 
36 import com.android.launcher3.AbstractFloatingView;
37 import com.android.launcher3.DeviceProfile;
38 import com.android.launcher3.taskbar.TaskbarActivityContext;
39 import com.android.launcher3.taskbar.TaskbarControllers;
40 import com.android.systemui.shared.system.TaskStackChangeListener;
41 import com.android.systemui.shared.system.TaskStackChangeListeners;
42 
43 import java.util.Optional;
44 
45 /**
46  * Handles the Taskbar overlay window lifecycle.
47  * <p>
48  * Overlays need to be inflated in a separate window so that have the correct hierarchy. For
49  * instance, they need to be below the notification tray. If there are multiple overlays open, the
50  * same window is used.
51  */
52 public final class TaskbarOverlayController {
53 
54     private static final String WINDOW_TITLE = "Taskbar Overlay";
55 
56     private final TaskbarActivityContext mTaskbarContext;
57     private final Context mWindowContext;
58     private final TaskbarOverlayProxyView mProxyView;
59     private final LayoutParams mLayoutParams;
60 
61     private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() {
62         @Override
63         public void onTaskMovedToFront(int taskId) {
64             // New front task will be below existing overlay, so move out of the way.
65             hideWindowOnTaskStackChange();
66         }
67 
68         @Override
69         public void onTaskStackChanged() {
70             // The other callbacks are insufficient for All Apps, because there are many cases where
71             // it can relaunch the same task already behind it. However, this callback needs to be a
72             // no-op when only EDU is shown, because going between the EDU steps invokes this
73             // callback.
74             if (mControllers.getSharedState() != null
75                     && mControllers.getSharedState().allAppsVisible) {
76                 hideWindowOnTaskStackChange();
77             }
78         }
79 
80         private void hideWindowOnTaskStackChange() {
81             // A task was launched while overlay window was open, so stash Taskbar.
82             mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
83             hideWindow();
84         }
85     };
86 
87     private DeviceProfile mLauncherDeviceProfile;
88     private @Nullable TaskbarOverlayContext mOverlayContext;
89     private TaskbarControllers mControllers; // Initialized in init.
90 
TaskbarOverlayController( TaskbarActivityContext taskbarContext, DeviceProfile launcherDeviceProfile)91     public TaskbarOverlayController(
92             TaskbarActivityContext taskbarContext, DeviceProfile launcherDeviceProfile) {
93         mTaskbarContext = taskbarContext;
94         mWindowContext = mTaskbarContext.createWindowContext(TYPE_APPLICATION_OVERLAY, null);
95         mProxyView = new TaskbarOverlayProxyView();
96         mLayoutParams = createLayoutParams();
97         mLauncherDeviceProfile = launcherDeviceProfile;
98     }
99 
100     /** Initialize the controller. */
init(TaskbarControllers controllers)101     public void init(TaskbarControllers controllers) {
102         mControllers = controllers;
103     }
104 
105     /**
106      * Creates a window for Taskbar overlays, if it does not already exist. Returns the window
107      * context for the current overlay window.
108      */
requestWindow()109     public TaskbarOverlayContext requestWindow() {
110         if (mOverlayContext == null) {
111             mOverlayContext = new TaskbarOverlayContext(
112                     mWindowContext, mTaskbarContext, mControllers);
113         }
114 
115         if (!mProxyView.isOpen()) {
116             mProxyView.show();
117             Optional.ofNullable(mOverlayContext.getSystemService(WindowManager.class))
118                     .ifPresent(m -> m.addView(mOverlayContext.getDragLayer(), mLayoutParams));
119             TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener);
120         }
121 
122         return mOverlayContext;
123     }
124 
125     /** Hides the current overlay window with animation. */
hideWindow()126     public void hideWindow() {
127         mProxyView.close(true);
128     }
129 
130     /**
131      * Removes the overlay window from the hierarchy, if all floating views are closed and there is
132      * no system drag operation in progress.
133      * <p>
134      * This method should be called after an exit animation finishes, if applicable.
135      */
maybeCloseWindow()136     void maybeCloseWindow() {
137         if (!canCloseWindow()) return;
138         mProxyView.close(false);
139         onDestroy();
140     }
141 
142     @SuppressLint("WrongConstant")
canCloseWindow()143     private boolean canCloseWindow() {
144         if (mOverlayContext == null) return true;
145         if (AbstractFloatingView.hasOpenView(mOverlayContext, TYPE_ALL)) return false;
146         return !mOverlayContext.getDragController().isSystemDragInProgress();
147     }
148 
149     /** Destroys the controller and any overlay window if present. */
onDestroy()150     public void onDestroy() {
151         TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener);
152         Optional.ofNullable(mOverlayContext)
153                 .map(c -> c.getSystemService(WindowManager.class))
154                 .ifPresent(m -> m.removeViewImmediate(mOverlayContext.getDragLayer()));
155         mOverlayContext = null;
156     }
157 
158     /** The current device profile for the overlay window. */
getLauncherDeviceProfile()159     public DeviceProfile getLauncherDeviceProfile() {
160         return mLauncherDeviceProfile;
161     }
162 
163     /** Updates {@link DeviceProfile} instance for Taskbar's overlay window. */
updateLauncherDeviceProfile(DeviceProfile dp)164     public void updateLauncherDeviceProfile(DeviceProfile dp) {
165         mLauncherDeviceProfile = dp;
166         Optional.ofNullable(mOverlayContext).ifPresent(c -> {
167             AbstractFloatingView.closeAllOpenViewsExcept(c, false, TYPE_REBIND_SAFE);
168             c.dispatchDeviceProfileChanged();
169         });
170     }
171 
172     /** The default open duration for overlays. */
getOpenDuration()173     public int getOpenDuration() {
174         return ALL_APPS.getTransitionDuration(mTaskbarContext, true);
175     }
176 
177     /** The default close duration for overlays. */
getCloseDuration()178     public int getCloseDuration() {
179         return ALL_APPS.getTransitionDuration(mTaskbarContext, false);
180     }
181 
182     @SuppressLint("WrongConstant")
createLayoutParams()183     private LayoutParams createLayoutParams() {
184         LayoutParams layoutParams = new LayoutParams(
185                 TYPE_APPLICATION_OVERLAY,
186                 LayoutParams.FLAG_SPLIT_TOUCH,
187                 PixelFormat.TRANSLUCENT);
188         layoutParams.setTitle(WINDOW_TITLE);
189         layoutParams.gravity = Gravity.BOTTOM;
190         layoutParams.packageName = mTaskbarContext.getPackageName();
191         layoutParams.setFitInsetsTypes(0); // Handled by container view.
192         layoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
193         layoutParams.setSystemApplicationOverlay(true);
194         layoutParams.privateFlags = PRIVATE_FLAG_CONSUME_IME_INSETS;
195         return layoutParams;
196     }
197 
198     /**
199      * Proxy view connecting taskbar drag layer to the overlay window.
200      *
201      * Overlays are in a separate window and has its own drag layer, but this proxy lets its views
202      * behave as though they are in the taskbar drag layer. For instance, when the taskbar closes
203      * all {@link AbstractFloatingView} instances, the overlay window will also close.
204      */
205     private class TaskbarOverlayProxyView extends AbstractFloatingView {
206 
TaskbarOverlayProxyView()207         private TaskbarOverlayProxyView() {
208             super(mTaskbarContext, null);
209         }
210 
show()211         private void show() {
212             mIsOpen = true;
213             mTaskbarContext.getDragLayer().addView(this);
214         }
215 
216         @Override
handleClose(boolean animate)217         protected void handleClose(boolean animate) {
218             if (!mIsOpen) return;
219             mTaskbarContext.getDragLayer().removeView(this);
220             Optional.ofNullable(mOverlayContext).ifPresent(c -> {
221                 if (canCloseWindow()) {
222                     onDestroy(); // Window is already ready to be destroyed.
223                 } else {
224                     // Close window's AFVs before destroying it. Its drag layer will attempt to
225                     // close the proxy view again once its children are removed.
226                     closeAllOpenViews(c, animate);
227                 }
228             });
229         }
230 
231         @Override
isOfType(int type)232         protected boolean isOfType(int type) {
233             return (type & TYPE_TASKBAR_OVERLAY_PROXY) != 0;
234         }
235 
236         @Override
onControllerInterceptTouchEvent(MotionEvent ev)237         public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
238             return false;
239         }
240     }
241 }
242