1 /* 2 * Copyright (C) 2018 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.quickstep; 18 19 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; 20 import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS; 21 import static android.view.Surface.ROTATION_0; 22 23 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP; 24 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT; 25 import static com.android.window.flags.Flags.enableDesktopWindowingMode; 26 27 import android.app.ActivityOptions; 28 import android.graphics.Bitmap; 29 import android.graphics.Color; 30 import android.graphics.Rect; 31 import android.os.Handler; 32 import android.os.Looper; 33 import android.os.RemoteException; 34 import android.provider.Settings; 35 import android.util.Log; 36 import android.view.View; 37 import android.view.WindowInsets; 38 import android.view.WindowManagerGlobal; 39 import android.window.SplashScreen; 40 41 import androidx.annotation.Nullable; 42 43 import com.android.launcher3.DeviceProfile; 44 import com.android.launcher3.Flags; 45 import com.android.launcher3.R; 46 import com.android.launcher3.logging.StatsLogManager.LauncherEvent; 47 import com.android.launcher3.model.WellbeingModel; 48 import com.android.launcher3.popup.SystemShortcut; 49 import com.android.launcher3.popup.SystemShortcut.AppInfo; 50 import com.android.launcher3.util.InstantAppResolver; 51 import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption; 52 import com.android.launcher3.views.ActivityContext; 53 import com.android.quickstep.orientation.RecentsPagedOrientationHandler; 54 import com.android.quickstep.util.RecentsOrientedState; 55 import com.android.quickstep.views.GroupedTaskView; 56 import com.android.quickstep.views.RecentsView; 57 import com.android.quickstep.views.RecentsViewContainer; 58 import com.android.quickstep.views.TaskThumbnailViewDeprecated; 59 import com.android.quickstep.views.TaskView; 60 import com.android.quickstep.views.TaskView.TaskContainer; 61 import com.android.systemui.shared.recents.model.Task; 62 import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecCompat; 63 import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecsFuture; 64 import com.android.systemui.shared.recents.view.RecentsTransition; 65 import com.android.systemui.shared.system.ActivityManagerWrapper; 66 67 import java.util.Collections; 68 import java.util.List; 69 import java.util.function.Function; 70 import java.util.stream.Collectors; 71 72 /** 73 * Represents a system shortcut that can be shown for a recent task. Appears as a single entry in 74 * the dropdown menu that shows up when you tap an app icon in Overview. 75 */ 76 public interface TaskShortcutFactory { 77 @Nullable getShortcuts(RecentsViewContainer container, TaskContainer taskContainer)78 default List<SystemShortcut> getShortcuts(RecentsViewContainer container, 79 TaskContainer taskContainer) { 80 return null; 81 } 82 83 /** 84 * Returns {@code true} if it should be shown for grouped task; {@code false} otherwise. 85 */ showForGroupedTask()86 default boolean showForGroupedTask() { 87 return false; 88 } 89 90 /** 91 * Returns {@code true} if it should be shown for desktop task; {@code false} otherwise. 92 */ showForDesktopTask()93 default boolean showForDesktopTask() { 94 return false; 95 } 96 97 /** @return a singleton list if the provided shortcut is non-null, null otherwise */ 98 @Nullable createSingletonShortcutList(@ullable SystemShortcut shortcut)99 default List<SystemShortcut> createSingletonShortcutList(@Nullable SystemShortcut shortcut) { 100 if (shortcut != null) { 101 return Collections.singletonList(shortcut); 102 } 103 return null; 104 } 105 106 TaskShortcutFactory APP_INFO = new TaskShortcutFactory() { 107 @Override 108 public List<SystemShortcut> getShortcuts(RecentsViewContainer container, 109 TaskContainer taskContainer) { 110 TaskView taskView = taskContainer.getTaskView(); 111 int actionId = taskContainer.getStagePosition() == STAGE_POSITION_BOTTOM_OR_RIGHT 112 ? R.id.action_app_info_bottom_right 113 : R.id.action_app_info_top_left; 114 115 AppInfo.SplitAccessibilityInfo accessibilityInfo = 116 new AppInfo.SplitAccessibilityInfo(taskView.containsMultipleTasks(), 117 TaskUtils.getTitle(taskView.getContext(), taskContainer.getTask()), 118 actionId 119 ); 120 return Collections.singletonList(new AppInfo(container, taskContainer.getItemInfo(), 121 taskView, accessibilityInfo)); 122 } 123 124 @Override 125 public boolean showForGroupedTask() { 126 return true; 127 } 128 }; 129 130 class SplitSelectSystemShortcut extends SystemShortcut { 131 private final TaskView mTaskView; 132 private final SplitPositionOption mSplitPositionOption; 133 SplitSelectSystemShortcut(RecentsViewContainer container, TaskView taskView, SplitPositionOption option)134 public SplitSelectSystemShortcut(RecentsViewContainer container, TaskView taskView, 135 SplitPositionOption option) { 136 super(option.iconResId, option.textResId, container, taskView.getFirstItemInfo(), 137 taskView); 138 mTaskView = taskView; 139 mSplitPositionOption = option; 140 } 141 142 @Override onClick(View view)143 public void onClick(View view) { 144 mTaskView.initiateSplitSelect(mSplitPositionOption); 145 } 146 } 147 148 /** 149 * A menu item, "Save app pair", that allows the user to preserve the current app combination as 150 * one persistent icon on the Home screen, allowing for quick split screen launching. 151 */ 152 class SaveAppPairSystemShortcut extends SystemShortcut<RecentsViewContainer> { 153 private final GroupedTaskView mTaskView; 154 155 SaveAppPairSystemShortcut(RecentsViewContainer container, GroupedTaskView taskView, int iconResId)156 public SaveAppPairSystemShortcut(RecentsViewContainer container, GroupedTaskView taskView, 157 int iconResId) { 158 super(iconResId, R.string.save_app_pair, container, taskView.getFirstItemInfo(), 159 taskView); 160 mTaskView = taskView; 161 } 162 163 @Override onClick(View view)164 public void onClick(View view) { 165 dismissTaskMenuView(); 166 ((RecentsView) mTarget.getOverviewPanel()) 167 .getSplitSelectController().getAppPairsController().saveAppPair(mTaskView); 168 } 169 } 170 171 class FreeformSystemShortcut extends SystemShortcut<RecentsViewContainer> { 172 private static final String TAG = "FreeformSystemShortcut"; 173 174 private Handler mHandler; 175 176 private final RecentsView mRecentsView; 177 private final TaskThumbnailViewDeprecated mThumbnailView; 178 private final TaskView mTaskView; 179 private final LauncherEvent mLauncherEvent; 180 FreeformSystemShortcut(int iconRes, int textRes, RecentsViewContainer container, TaskContainer taskContainer, LauncherEvent launcherEvent)181 public FreeformSystemShortcut(int iconRes, int textRes, RecentsViewContainer container, 182 TaskContainer taskContainer, LauncherEvent launcherEvent) { 183 super(iconRes, textRes, container, taskContainer.getItemInfo(), 184 taskContainer.getTaskView()); 185 mLauncherEvent = launcherEvent; 186 mHandler = new Handler(Looper.getMainLooper()); 187 mTaskView = taskContainer.getTaskView(); 188 mRecentsView = container.getOverviewPanel(); 189 mThumbnailView = taskContainer.getThumbnailViewDeprecated(); 190 } 191 192 @Override onClick(View view)193 public void onClick(View view) { 194 dismissTaskMenuView(); 195 RecentsView rv = mTarget.getOverviewPanel(); 196 rv.switchToScreenshot(() -> { 197 rv.finishRecentsAnimation(true /* toRecents */, false /* shouldPip */, () -> { 198 mTarget.returnToHomescreen(); 199 rv.getHandler().post(this::startActivity); 200 }); 201 }); 202 } 203 startActivity()204 private void startActivity() { 205 final Task.TaskKey taskKey = mTaskView.getFirstTask().key; 206 final int taskId = taskKey.id; 207 final ActivityOptions options = makeLaunchOptions(mTarget); 208 if (options != null) { 209 options.setSplashScreenStyle(SplashScreen.SPLASH_SCREEN_STYLE_ICON); 210 } 211 if (options != null 212 && ActivityManagerWrapper.getInstance().startActivityFromRecents(taskId, 213 options)) { 214 final Runnable animStartedListener = () -> { 215 // Hide the task view and wait for the window to be resized 216 // TODO: Consider animating in launcher and do an in-place start activity 217 // afterwards 218 mRecentsView.setIgnoreResetTask(taskId); 219 mTaskView.setAlpha(0f); 220 }; 221 222 final int[] position = new int[2]; 223 mThumbnailView.getLocationOnScreen(position); 224 final int width = (int) (mThumbnailView.getWidth() * mTaskView.getScaleX()); 225 final int height = (int) (mThumbnailView.getHeight() * mTaskView.getScaleY()); 226 final Rect taskBounds = new Rect(position[0], position[1], 227 position[0] + width, position[1] + height); 228 229 // Take the thumbnail of the task without a scrim and apply it back after 230 float alpha = mThumbnailView.getDimAlpha(); 231 mThumbnailView.setDimAlpha(0); 232 Bitmap thumbnail = RecentsTransition.drawViewIntoHardwareBitmap( 233 taskBounds.width(), taskBounds.height(), mThumbnailView, 1f, 234 Color.BLACK); 235 mThumbnailView.setDimAlpha(alpha); 236 237 AppTransitionAnimationSpecsFuture future = 238 new AppTransitionAnimationSpecsFuture(mHandler) { 239 @Override 240 public List<AppTransitionAnimationSpecCompat> composeSpecs() { 241 return Collections.singletonList( 242 new AppTransitionAnimationSpecCompat( 243 taskId, thumbnail, taskBounds)); 244 } 245 }; 246 overridePendingAppTransitionMultiThumbFuture( 247 future, animStartedListener, mHandler, true /* scaleUp */, 248 taskKey.displayId); 249 mTarget.getStatsLogManager().logger().withItemInfo(mTaskView.getFirstItemInfo()) 250 .log(mLauncherEvent); 251 } 252 } 253 254 /** 255 * Overrides a pending app transition. 256 */ overridePendingAppTransitionMultiThumbFuture( AppTransitionAnimationSpecsFuture animationSpecFuture, Runnable animStartedCallback, Handler animStartedCallbackHandler, boolean scaleUp, int displayId)257 private void overridePendingAppTransitionMultiThumbFuture( 258 AppTransitionAnimationSpecsFuture animationSpecFuture, Runnable animStartedCallback, 259 Handler animStartedCallbackHandler, boolean scaleUp, int displayId) { 260 try { 261 WindowManagerGlobal.getWindowManagerService() 262 .overridePendingAppTransitionMultiThumbFuture( 263 animationSpecFuture.getFuture(), 264 RecentsTransition.wrapStartedListener(animStartedCallbackHandler, 265 animStartedCallback), scaleUp, displayId); 266 } catch (RemoteException e) { 267 Log.w(TAG, "Failed to override pending app transition (multi-thumbnail future): ", 268 e); 269 } 270 } 271 makeLaunchOptions(RecentsViewContainer container)272 private ActivityOptions makeLaunchOptions(RecentsViewContainer container) { 273 ActivityOptions activityOptions = ActivityOptions.makeBasic(); 274 activityOptions.setLaunchWindowingMode(WINDOWING_MODE_FREEFORM); 275 // Arbitrary bounds only because freeform is in dev mode right now 276 final View decorView = container.getWindow().getDecorView(); 277 final WindowInsets insets = decorView.getRootWindowInsets(); 278 final Rect r = new Rect(0, 0, decorView.getWidth() / 2, decorView.getHeight() / 2); 279 r.offsetTo(insets.getSystemWindowInsetLeft() + 50, 280 insets.getSystemWindowInsetTop() + 50); 281 activityOptions.setLaunchBounds(r); 282 return activityOptions; 283 } 284 } 285 286 /** 287 * Does NOT add split options in the following scenarios: 288 * * 1. Taskbar is not present AND aren't at least 2 tasks in overview to show split options for 289 * * 2. Split isn't supported by the task itself (non resizable activity) 290 * * 3. We aren't currently in multi-window 291 * * 4. The taskView to show split options for is the focused task AND we haven't started 292 * * scrolling in overview (if we haven't scrolled, there's a split overview action button so 293 * * we don't need this menu option) 294 */ 295 TaskShortcutFactory SPLIT_SELECT = new TaskShortcutFactory() { 296 @Override 297 public List<SystemShortcut> getShortcuts(RecentsViewContainer container, 298 TaskContainer taskContainer) { 299 DeviceProfile deviceProfile = container.getDeviceProfile(); 300 final Task task = taskContainer.getTask(); 301 final int intentFlags = task.key.baseIntent.getFlags(); 302 final TaskView taskView = taskContainer.getTaskView(); 303 final RecentsView recentsView = taskView.getRecentsView(); 304 final RecentsPagedOrientationHandler orientationHandler = 305 recentsView.getPagedOrientationHandler(); 306 307 boolean notEnoughTasksToSplit = 308 !deviceProfile.isTaskbarPresent && recentsView.getTaskViewCount() < 2; 309 boolean isTaskSplitNotSupported = !task.isDockable || 310 (intentFlags & FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0; 311 boolean hideForExistingMultiWindow = container.getDeviceProfile().isMultiWindowMode; 312 boolean isFocusedTask = deviceProfile.isTablet && taskView.isFocusedTask(); 313 boolean isTaskInExpectedScrollPosition = 314 recentsView.isTaskInExpectedScrollPosition(recentsView.indexOfChild(taskView)); 315 316 if (notEnoughTasksToSplit || isTaskSplitNotSupported || hideForExistingMultiWindow 317 || (isFocusedTask && isTaskInExpectedScrollPosition)) { 318 return null; 319 } 320 321 return orientationHandler.getSplitPositionOptions(deviceProfile) 322 .stream() 323 .map((Function<SplitPositionOption, SystemShortcut>) option -> 324 new SplitSelectSystemShortcut(container, taskView, option)) 325 .collect(Collectors.toList()); 326 } 327 }; 328 329 TaskShortcutFactory SAVE_APP_PAIR = new TaskShortcutFactory() { 330 @Nullable 331 @Override 332 public List<SystemShortcut> getShortcuts(RecentsViewContainer container, 333 TaskContainer taskContainer) { 334 DeviceProfile deviceProfile = container.getDeviceProfile(); 335 final TaskView taskView = taskContainer.getTaskView(); 336 final RecentsView recentsView = taskView.getRecentsView(); 337 boolean isLargeTileFocusedTask = deviceProfile.isTablet && taskView.isFocusedTask(); 338 boolean isInExpectedScrollPosition = 339 recentsView.isTaskInExpectedScrollPosition(recentsView.indexOfChild(taskView)); 340 boolean shouldShowActionsButtonInstead = 341 isLargeTileFocusedTask && isInExpectedScrollPosition; 342 343 // No "save app pair" menu item if: 344 // - we are in 3p launcher 345 // - the Overview Actions Button should be visible 346 // - the task view is not a valid save-able split pair 347 if (!recentsView.supportsAppPairs() 348 || shouldShowActionsButtonInstead 349 || !recentsView.getSplitSelectController().getAppPairsController() 350 .canSaveAppPair(taskView)) { 351 return null; 352 } 353 354 int iconResId = deviceProfile.isLeftRightSplit 355 ? R.drawable.ic_save_app_pair_left_right 356 : R.drawable.ic_save_app_pair_up_down; 357 358 return Collections.singletonList( 359 new SaveAppPairSystemShortcut(container, 360 (GroupedTaskView) taskView, iconResId)); 361 } 362 363 @Override 364 public boolean showForGroupedTask() { 365 return true; 366 } 367 }; 368 369 TaskShortcutFactory FREE_FORM = new TaskShortcutFactory() { 370 @Override 371 public List<SystemShortcut> getShortcuts(RecentsViewContainer container, 372 TaskContainer taskContainer) { 373 final Task task = taskContainer.getTask(); 374 if (!task.isDockable) { 375 return null; 376 } 377 if (!isAvailable(container)) { 378 return null; 379 } 380 381 return Collections.singletonList(new FreeformSystemShortcut( 382 R.drawable.ic_caption_desktop_button_foreground, 383 R.string.recent_task_option_freeform, container, taskContainer, 384 LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP)); 385 } 386 387 private boolean isAvailable(RecentsViewContainer container) { 388 return Settings.Global.getInt( 389 container.asContext().getContentResolver(), 390 Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, 0) != 0 391 && !enableDesktopWindowingMode(); 392 } 393 }; 394 395 TaskShortcutFactory PIN = new TaskShortcutFactory() { 396 @Override 397 public List<SystemShortcut> getShortcuts(RecentsViewContainer container, 398 TaskContainer taskContainer) { 399 if (!SystemUiProxy.INSTANCE.get(container.asContext()).isActive()) { 400 return null; 401 } 402 if (!ActivityManagerWrapper.getInstance().isScreenPinningEnabled()) { 403 return null; 404 } 405 if (ActivityManagerWrapper.getInstance().isLockToAppActive()) { 406 // We shouldn't be able to pin while an app is locked. 407 return null; 408 } 409 return Collections.singletonList(new PinSystemShortcut(container, taskContainer)); 410 } 411 }; 412 413 class PinSystemShortcut extends SystemShortcut<RecentsViewContainer> { 414 415 private static final String TAG = "PinSystemShortcut"; 416 417 private final TaskView mTaskView; 418 PinSystemShortcut(RecentsViewContainer target, TaskContainer taskContainer)419 public PinSystemShortcut(RecentsViewContainer target, 420 TaskContainer taskContainer) { 421 super(R.drawable.ic_pin, R.string.recent_task_option_pin, target, 422 taskContainer.getItemInfo(), taskContainer.getTaskView()); 423 mTaskView = taskContainer.getTaskView(); 424 } 425 426 @Override onClick(View view)427 public void onClick(View view) { 428 if (mTaskView.launchTaskAnimated() != null) { 429 SystemUiProxy.INSTANCE.get(mTarget.asContext()).startScreenPinning( 430 mTaskView.getFirstTask().key.id); 431 } 432 dismissTaskMenuView(); 433 mTarget.getStatsLogManager().logger().withItemInfo(mTaskView.getFirstItemInfo()) 434 .log(LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_PIN_TAP); 435 } 436 } 437 438 TaskShortcutFactory INSTALL = new TaskShortcutFactory() { 439 @Override 440 public List<SystemShortcut> getShortcuts(RecentsViewContainer container, 441 TaskContainer taskContainer) { 442 Task t = taskContainer.getTask(); 443 return InstantAppResolver.newInstance(container.asContext()).isInstantApp( 444 t.getTopComponent().getPackageName(), t.getKey().userId) 445 ? Collections.singletonList(new SystemShortcut.Install(container, 446 taskContainer.getItemInfo(), taskContainer.getTaskView())) 447 : null; 448 } 449 }; 450 451 TaskShortcutFactory WELLBEING = new TaskShortcutFactory() { 452 @Override 453 public List<SystemShortcut> getShortcuts(RecentsViewContainer container, 454 TaskContainer taskContainer) { 455 SystemShortcut<ActivityContext> wellbeingShortcut = 456 WellbeingModel.SHORTCUT_FACTORY.getShortcut(container, 457 taskContainer.getItemInfo(), taskContainer.getTaskView()); 458 return createSingletonShortcutList(wellbeingShortcut); 459 } 460 }; 461 462 TaskShortcutFactory SCREENSHOT = new TaskShortcutFactory() { 463 @Override 464 public List<SystemShortcut> getShortcuts(RecentsViewContainer container, 465 TaskContainer taskContainer) { 466 boolean isTablet = container.getDeviceProfile().isTablet; 467 boolean isGridOnlyOverview = isTablet && Flags.enableGridOnlyOverview(); 468 // Extra conditions if it's not grid-only overview 469 if (!isGridOnlyOverview) { 470 RecentsOrientedState orientedState = taskContainer.getTaskView().getOrientedState(); 471 boolean isFakeLandscape = !orientedState.isRecentsActivityRotationAllowed() 472 && orientedState.getTouchRotation() != ROTATION_0; 473 if (!isFakeLandscape) { 474 return null; 475 } 476 } 477 478 SystemShortcut screenshotShortcut = taskContainer.getOverlay().getScreenshotShortcut( 479 container, taskContainer.getItemInfo(), taskContainer.getTaskView()); 480 return createSingletonShortcutList(screenshotShortcut); 481 } 482 483 @Override 484 public boolean showForDesktopTask() { 485 return true; 486 } 487 }; 488 489 TaskShortcutFactory MODAL = new TaskShortcutFactory() { 490 @Override 491 public List<SystemShortcut> getShortcuts(RecentsViewContainer container, 492 TaskContainer taskContainer) { 493 boolean isTablet = container.getDeviceProfile().isTablet; 494 boolean isGridOnlyOverview = isTablet && Flags.enableGridOnlyOverview(); 495 if (!isGridOnlyOverview) { 496 return null; 497 } 498 499 SystemShortcut modalStateSystemShortcut = 500 taskContainer.getOverlay().getModalStateSystemShortcut( 501 taskContainer.getItemInfo(), taskContainer.getTaskView()); 502 return createSingletonShortcutList(modalStateSystemShortcut); 503 } 504 }; 505 } 506