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