1 /* 2 * Copyright (C) 2021 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; 17 18 import static android.app.ActivityTaskManager.INVALID_TASK_ID; 19 20 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT; 21 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION; 22 import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_APP; 23 import static com.android.quickstep.OverviewCommandHelper.TYPE_HIDE; 24 25 import android.content.Intent; 26 import android.graphics.drawable.BitmapDrawable; 27 import android.view.MotionEvent; 28 import android.view.View; 29 import android.window.RemoteTransition; 30 31 import androidx.annotation.CallSuper; 32 import androidx.annotation.NonNull; 33 import androidx.annotation.Nullable; 34 35 import com.android.launcher3.LauncherState; 36 import com.android.launcher3.Utilities; 37 import com.android.launcher3.model.data.ItemInfo; 38 import com.android.launcher3.model.data.ItemInfoWithIcon; 39 import com.android.launcher3.popup.SystemShortcut; 40 import com.android.launcher3.util.DisplayController; 41 import com.android.launcher3.util.SplitConfigurationOptions; 42 import com.android.quickstep.OverviewCommandHelper; 43 import com.android.quickstep.util.GroupTask; 44 import com.android.quickstep.util.TISBindHelper; 45 import com.android.quickstep.views.RecentsView; 46 import com.android.quickstep.views.TaskView; 47 import com.android.quickstep.views.TaskView.TaskContainer; 48 import com.android.systemui.shared.recents.model.Task; 49 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags; 50 51 import java.io.PrintWriter; 52 import java.util.Collections; 53 import java.util.stream.Stream; 54 55 /** 56 * Base class for providing different taskbar UI 57 */ 58 public class TaskbarUIController { 59 public static final TaskbarUIController DEFAULT = new TaskbarUIController(); 60 61 // Initialized in init. 62 protected TaskbarControllers mControllers; 63 64 @CallSuper init(TaskbarControllers taskbarControllers)65 protected void init(TaskbarControllers taskbarControllers) { 66 mControllers = taskbarControllers; 67 } 68 69 @CallSuper onDestroy()70 protected void onDestroy() { 71 mControllers = null; 72 } 73 isTaskbarTouchable()74 protected boolean isTaskbarTouchable() { 75 return true; 76 } 77 78 /** 79 * This should only be called by TaskbarStashController so that a TaskbarUIController can 80 * disable stashing. All other controllers should use 81 * {@link TaskbarStashController#supportsVisualStashing()} as the source of truth. 82 */ supportsVisualStashing()83 public boolean supportsVisualStashing() { 84 return true; 85 } 86 onStashedInAppChanged()87 protected void onStashedInAppChanged() { } 88 89 /** 90 * Called when taskbar icon layout bounds change. 91 */ onIconLayoutBoundsChanged()92 protected void onIconLayoutBoundsChanged() { } 93 getTaskbarUIControllerName()94 protected String getTaskbarUIControllerName() { 95 return "TaskbarUIController"; 96 } 97 98 /** Called when an icon is launched. */ 99 @CallSuper onTaskbarIconLaunched(ItemInfo item)100 public void onTaskbarIconLaunched(ItemInfo item) { 101 // When launching from Taskbar, e.g. from Overview, set FLAG_IN_APP immediately instead of 102 // waiting for onPause, to reduce potential visual noise during the app open transition. 103 if (mControllers.taskbarStashController == null) return; 104 mControllers.taskbarStashController.updateStateForFlag(FLAG_IN_APP, true); 105 mControllers.taskbarStashController.applyState(); 106 } 107 getRootView()108 public View getRootView() { 109 return mControllers.taskbarActivityContext.getDragLayer(); 110 } 111 112 /** 113 * Called when swiping from the bottom nav region in fully gestural mode. 114 * @param inProgress True if the animation started, false if we just settled on an end target. 115 */ setSystemGestureInProgress(boolean inProgress)116 public void setSystemGestureInProgress(boolean inProgress) { 117 mControllers.taskbarStashController.setSystemGestureInProgress(inProgress); 118 } 119 120 /** 121 * Manually closes the overlay window. 122 */ hideOverlayWindow()123 public void hideOverlayWindow() { 124 if (!DisplayController.isTransientTaskbar(mControllers.taskbarActivityContext) 125 || mControllers.taskbarAllAppsController.isOpen()) { 126 mControllers.taskbarOverlayController.hideWindow(); 127 } 128 } 129 130 /** 131 * User expands PiP to full-screen (or split-screen) mode, try to hide the Taskbar. 132 */ onExpandPip()133 public void onExpandPip() { 134 if (mControllers != null) { 135 final TaskbarStashController stashController = mControllers.taskbarStashController; 136 stashController.updateStateForFlag(FLAG_IN_APP, true); 137 stashController.applyState(); 138 } 139 } 140 141 /** 142 * SysUI flags updated, see QuickStepContract.SYSUI_STATE_* values. 143 */ updateStateForSysuiFlags(@ystemUiStateFlags long sysuiFlags)144 public void updateStateForSysuiFlags(@SystemUiStateFlags long sysuiFlags) { 145 } 146 147 /** 148 * Returns {@code true} iff taskbar is stashed. 149 */ isTaskbarStashed()150 public boolean isTaskbarStashed() { 151 return mControllers.taskbarStashController.isStashed(); 152 } 153 154 /** 155 * Returns {@code true} iff taskbar All Apps is open. 156 */ isTaskbarAllAppsOpen()157 public boolean isTaskbarAllAppsOpen() { 158 return mControllers.taskbarAllAppsController.isOpen(); 159 } 160 161 /** 162 * Called at the end of the swipe gesture on Transient taskbar. 163 */ startTranslationSpring()164 public void startTranslationSpring() { 165 mControllers.taskbarActivityContext.startTranslationSpring(); 166 } 167 168 /** 169 * @param ev MotionEvent in screen coordinates. 170 * @return Whether any Taskbar item could handle the given MotionEvent if given the chance. 171 */ isEventOverAnyTaskbarItem(MotionEvent ev)172 public boolean isEventOverAnyTaskbarItem(MotionEvent ev) { 173 return mControllers.taskbarViewController.isEventOverAnyItem(ev) 174 || mControllers.navbarButtonsViewController.isEventOverAnyItem(ev); 175 } 176 177 /** Checks if the given {@link MotionEvent} is over the bubble bar stash handle. */ isEventOverBubbleBarStashHandle(MotionEvent ev)178 public boolean isEventOverBubbleBarStashHandle(MotionEvent ev) { 179 return mControllers.bubbleControllers.map( 180 bubbleControllers -> 181 bubbleControllers.bubbleStashController.isEventOverStashHandle(ev)) 182 .orElse(false); 183 } 184 185 /** 186 * Returns true if icons should be aligned to hotseat in the current transition. 187 */ isIconAlignedWithHotseat()188 public boolean isIconAlignedWithHotseat() { 189 return false; 190 } 191 192 /** 193 * Returns true if hotseat icons are on top of view hierarchy when aligned in the current state. 194 */ isHotseatIconOnTopWhenAligned()195 public boolean isHotseatIconOnTopWhenAligned() { 196 return true; 197 } 198 199 /** Returns {@code true} if Taskbar is currently within overview. */ isInOverviewUi()200 protected boolean isInOverviewUi() { 201 return false; 202 } 203 204 /** Returns {@code true} if Home All Apps available instead of Taskbar All Apps. */ canToggleHomeAllApps()205 protected boolean canToggleHomeAllApps() { 206 return false; 207 } 208 209 @CallSuper dumpLogs(String prefix, PrintWriter pw)210 protected void dumpLogs(String prefix, PrintWriter pw) { 211 pw.println(String.format( 212 "%sTaskbarUIController: using an instance of %s", 213 prefix, 214 getTaskbarUIControllerName())); 215 } 216 217 /** 218 * Returns RecentsView. Overwritten in LauncherTaskbarUIController and 219 * FallbackTaskbarUIController with Launcher-specific implementations. Returns null for other 220 * UI controllers (like DesktopTaskbarUIController) that don't have a RecentsView. 221 */ getRecentsView()222 public @Nullable RecentsView getRecentsView() { 223 return null; 224 } 225 startSplitSelection(SplitConfigurationOptions.SplitSelectSource splitSelectSource)226 public void startSplitSelection(SplitConfigurationOptions.SplitSelectSource splitSelectSource) { 227 RecentsView recentsView = getRecentsView(); 228 if (recentsView == null) { 229 return; 230 } 231 232 recentsView.getSplitSelectController().findLastActiveTasksAndRunCallback( 233 Collections.singletonList(splitSelectSource.itemInfo.getComponentKey()), 234 false /* findExactPairMatch */, 235 foundTasks -> { 236 @Nullable Task foundTask = foundTasks[0]; 237 splitSelectSource.alreadyRunningTaskId = foundTask == null 238 ? INVALID_TASK_ID 239 : foundTask.key.id; 240 splitSelectSource.animateCurrentTaskDismissal = foundTask != null; 241 recentsView.initiateSplitSelect(splitSelectSource); 242 } 243 ); 244 } 245 246 /** 247 * Uses the clicked Taskbar icon to launch a second app for splitscreen. 248 */ triggerSecondAppForSplit(ItemInfoWithIcon info, Intent intent, View startingView)249 public void triggerSecondAppForSplit(ItemInfoWithIcon info, Intent intent, View startingView) { 250 RecentsView recents = getRecentsView(); 251 recents.getSplitSelectController().findLastActiveTasksAndRunCallback( 252 Collections.singletonList(info.getComponentKey()), 253 false /* findExactPairMatch */, 254 foundTasks -> { 255 @Nullable Task foundTask = foundTasks[0]; 256 if (foundTask != null) { 257 TaskView foundTaskView = recents.getTaskViewByTaskId(foundTask.key.id); 258 // TODO (b/266482558): This additional null check is needed because there 259 // are times when our Tasks list doesn't match our TaskViews list (like when 260 // a tile is removed during {@link RecentsView#applyLoadPlan()}. A clearer 261 // state management system is in the works so that we don't need to rely on 262 // null checks as much. See comments at ag/21152798. 263 if (foundTaskView != null) { 264 // There is already a running app of this type, use that as second app. 265 // Get index of task (0 or 1), in case it's a GroupedTaskView 266 TaskContainer taskContainer = 267 foundTaskView.getTaskContainerById(foundTask.key.id); 268 recents.confirmSplitSelect( 269 foundTaskView, 270 foundTask, 271 taskContainer.getIconView().getDrawable(), 272 taskContainer.getThumbnailViewDeprecated(), 273 taskContainer.getThumbnailViewDeprecated().getThumbnail(), 274 null /* intent */, 275 null /* user */, 276 info); 277 return; 278 } 279 } 280 281 // No running app of that type, create a new instance as second app. 282 recents.confirmSplitSelect( 283 null /* containerTaskView */, 284 null /* task */, 285 new BitmapDrawable(info.bitmap.icon), 286 startingView, 287 null /* thumbnail */, 288 intent, 289 info.user, 290 info); 291 } 292 ); 293 } 294 295 /** 296 * Opens the Keyboard Quick Switch View. 297 * 298 * This will set the focus to the first task from the right (from the left in RTL) 299 */ openQuickSwitchView()300 public void openQuickSwitchView() { 301 mControllers.keyboardQuickSwitchController.openQuickSwitchView(); 302 } 303 304 /** 305 * Launches the focused task and closes the Keyboard Quick Switch View. 306 * 307 * If the overlay or view are closed, or the overview task is focused, then Overview is 308 * launched. If the overview task is launched, then the first hidden task is focused. 309 * 310 * @return the index of what task should be focused in ; -1 iff Overview shouldn't be launched 311 */ launchFocusedTask()312 public int launchFocusedTask() { 313 int focusedTaskIndex = mControllers.keyboardQuickSwitchController.launchFocusedTask(); 314 mControllers.keyboardQuickSwitchController.closeQuickSwitchView(); 315 return focusedTaskIndex; 316 } 317 318 /** 319 * Launches the given task in split-screen. 320 */ launchSplitTasks( @onNull GroupTask groupTask, @Nullable RemoteTransition remoteTransition)321 public void launchSplitTasks( 322 @NonNull GroupTask groupTask, @Nullable RemoteTransition remoteTransition) { } 323 324 /** 325 * Returns the matching view (if any) in the taskbar. 326 * @param view The view to match. 327 */ findMatchingView(View view)328 public @Nullable View findMatchingView(View view) { 329 if (!(view.getTag() instanceof ItemInfo)) { 330 return null; 331 } 332 ItemInfo info = (ItemInfo) view.getTag(); 333 if (info.container != CONTAINER_HOTSEAT && info.container != CONTAINER_HOTSEAT_PREDICTION) { 334 return null; 335 } 336 337 // Taskbar has the same items as the hotseat and we can use screenId to find the match. 338 int screenId = info.screenId; 339 View[] views = mControllers.taskbarViewController.getIconViews(); 340 for (int i = views.length - 1; i >= 0; --i) { 341 if (views[i] != null 342 && views[i].getTag() instanceof ItemInfo 343 && ((ItemInfo) views[i].getTag()).screenId == screenId) { 344 return views[i]; 345 } 346 } 347 return null; 348 } 349 350 /** 351 * Callback for when launcher state transition completes after user swipes to home. 352 * @param finalState The final state of the transition. 353 */ onStateTransitionCompletedAfterSwipeToHome(LauncherState finalState)354 public void onStateTransitionCompletedAfterSwipeToHome(LauncherState finalState) { 355 // Overridden 356 } 357 358 /** 359 * Refreshes the resumed state of this ui controller. 360 */ refreshResumedState()361 public void refreshResumedState() {} 362 363 /** 364 * Returns a stream of split screen menu options appropriate to the device. 365 */ getSplitMenuOptions()366 Stream<SystemShortcut.Factory<BaseTaskbarContext>> getSplitMenuOptions() { 367 return Utilities 368 .getSplitPositionOptions(mControllers.taskbarActivityContext.getDeviceProfile()) 369 .stream() 370 .map(mControllers.taskbarPopupController::createSplitShortcutFactory); 371 } 372 373 /** Adjusts the hotseat for the bubble bar. */ adjustHotseatForBubbleBar(boolean isBubbleBarVisible)374 public void adjustHotseatForBubbleBar(boolean isBubbleBarVisible) {} 375 376 @Nullable getTISBindHelper()377 protected TISBindHelper getTISBindHelper() { 378 return null; 379 } 380 381 /** 382 * Launches the focused task in the Keyboard Quick Switch view through the OverviewCommandHelper 383 * <p> 384 * Use this helper method when the focused task may be the overview task. 385 */ launchKeyboardFocusedTask()386 public void launchKeyboardFocusedTask() { 387 TISBindHelper tisBindHelper = getTISBindHelper(); 388 if (tisBindHelper == null) { 389 return; 390 } 391 OverviewCommandHelper overviewCommandHelper = tisBindHelper.getOverviewCommandHelper(); 392 if (overviewCommandHelper == null) { 393 return; 394 } 395 overviewCommandHelper.addCommand(TYPE_HIDE); 396 } 397 398 /** 399 * Adjusts the taskbar based on the visibility of the launcher. 400 * @param isVisible True if launcher is visible, false otherwise. 401 */ onLauncherVisibilityChanged(boolean isVisible)402 public void onLauncherVisibilityChanged(boolean isVisible) { 403 mControllers.taskbarStashController.updateStateForFlag(FLAG_IN_APP, !isVisible); 404 mControllers.taskbarStashController.applyState(); 405 } 406 407 /** 408 * Request for UI controller to ignore animations for the next callback for the end of recents 409 * animation 410 */ setSkipNextRecentsAnimEnd()411 public void setSkipNextRecentsAnimEnd() { 412 // Overridden 413 } 414 415 /** 416 * Sets whether the user is going home based on the current gesture. 417 */ setUserIsGoingHome(boolean isGoingHome)418 public void setUserIsGoingHome(boolean isGoingHome) { 419 mControllers.taskbarStashController.setUserIsGoingHome(isGoingHome); 420 } 421 } 422