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