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