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.view.Surface.ROTATION_0; 20 21 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE; 22 import static com.android.quickstep.views.OverviewActionsView.DISABLED_NO_THUMBNAIL; 23 import static com.android.quickstep.views.OverviewActionsView.DISABLED_ROTATED; 24 25 import android.annotation.SuppressLint; 26 import android.app.ActivityManager; 27 import android.content.Context; 28 import android.graphics.Insets; 29 import android.graphics.Matrix; 30 import android.graphics.Rect; 31 import android.os.Build; 32 import android.view.View; 33 import android.widget.Toast; 34 35 import androidx.annotation.NonNull; 36 import androidx.annotation.RequiresApi; 37 38 import com.android.launcher3.BaseActivity; 39 import com.android.launcher3.BaseDraggingActivity; 40 import com.android.launcher3.DeviceProfile; 41 import com.android.launcher3.R; 42 import com.android.launcher3.config.FeatureFlags; 43 import com.android.launcher3.model.data.ItemInfo; 44 import com.android.launcher3.model.data.WorkspaceItemInfo; 45 import com.android.launcher3.popup.SystemShortcut; 46 import com.android.launcher3.touch.PagedOrientationHandler; 47 import com.android.launcher3.util.ResourceBasedOverride; 48 import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption; 49 import com.android.quickstep.TaskShortcutFactory.SplitSelectSystemShortcut; 50 import com.android.quickstep.util.RecentsOrientedState; 51 import com.android.quickstep.views.OverviewActionsView; 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.model.ThumbnailData; 58 import com.android.systemui.shared.system.ActivityManagerWrapper; 59 60 import java.util.ArrayList; 61 import java.util.List; 62 63 /** 64 * Factory class to create and add an overlays on the TaskView 65 */ 66 public class TaskOverlayFactory implements ResourceBasedOverride { 67 getEnabledShortcuts(TaskView taskView, DeviceProfile deviceProfile, TaskIdAttributeContainer taskContainer)68 public static List<SystemShortcut> getEnabledShortcuts(TaskView taskView, 69 DeviceProfile deviceProfile, TaskIdAttributeContainer taskContainer) { 70 final ArrayList<SystemShortcut> shortcuts = new ArrayList<>(); 71 final BaseDraggingActivity activity = BaseActivity.fromContext(taskView.getContext()); 72 boolean hasMultipleTasks = taskView.getTaskIds()[1] != -1; 73 for (TaskShortcutFactory menuOption : MENU_OPTIONS) { 74 if (hasMultipleTasks && !menuOption.showForSplitscreen()) { 75 continue; 76 } 77 78 SystemShortcut shortcut = menuOption.getShortcut(activity, taskContainer); 79 if (shortcut == null) { 80 continue; 81 } 82 83 if (menuOption == TaskShortcutFactory.SPLIT_SCREEN && 84 FeatureFlags.ENABLE_SPLIT_SELECT.get()) { 85 addSplitOptions(shortcuts, activity, taskView, deviceProfile); 86 } else { 87 shortcuts.add(shortcut); 88 } 89 } 90 RecentsOrientedState orientedState = taskView.getRecentsView().getPagedViewOrientedState(); 91 boolean canLauncherRotate = orientedState.isRecentsActivityRotationAllowed(); 92 boolean isInLandscape = orientedState.getTouchRotation() != ROTATION_0; 93 94 // Add overview actions to the menu when in in-place rotate landscape mode. 95 if (!canLauncherRotate && isInLandscape) { 96 // Add screenshot action to task menu. 97 SystemShortcut screenshotShortcut = TaskShortcutFactory.SCREENSHOT 98 .getShortcut(activity, taskContainer); 99 if (screenshotShortcut != null) { 100 shortcuts.add(screenshotShortcut); 101 } 102 103 // Add modal action only if display orientation is the same as the device orientation. 104 if (orientedState.getDisplayRotation() == ROTATION_0) { 105 SystemShortcut modalShortcut = TaskShortcutFactory.MODAL 106 .getShortcut(activity, taskContainer); 107 if (modalShortcut != null) { 108 shortcuts.add(modalShortcut); 109 } 110 } 111 } 112 return shortcuts; 113 } 114 115 116 /** 117 * Does NOT add split options in the following scenarios: 118 * * The taskView to add split options is already showing split screen tasks 119 * * There aren't at least 2 tasks in overview to show split options for 120 * * Device is in "Lock task mode" 121 * * The taskView to show split options for is the focused task AND we haven't started 122 * scrolling in overview (if we haven't scrolled, there's a split overview action button so 123 * we don't need this menu option) 124 */ addSplitOptions(List<SystemShortcut> outShortcuts, BaseDraggingActivity activity, TaskView taskView, DeviceProfile deviceProfile)125 private static void addSplitOptions(List<SystemShortcut> outShortcuts, 126 BaseDraggingActivity activity, TaskView taskView, DeviceProfile deviceProfile) { 127 RecentsView recentsView = taskView.getRecentsView(); 128 PagedOrientationHandler orientationHandler = recentsView.getPagedOrientationHandler(); 129 int[] taskViewTaskIds = taskView.getTaskIds(); 130 boolean taskViewHasMultipleTasks = taskViewTaskIds[0] != -1 && 131 taskViewTaskIds[1] != -1; 132 boolean notEnoughTasksToSplit = recentsView.getTaskViewCount() < 2; 133 boolean isFocusedTask = deviceProfile.overviewShowAsGrid && taskView.isFocusedTask(); 134 boolean isTaskInExpectedScrollPosition = 135 recentsView.isTaskInExpectedScrollPosition(recentsView.indexOfChild(taskView)); 136 ActivityManager activityManager = 137 (ActivityManager) taskView.getContext().getSystemService(Context.ACTIVITY_SERVICE); 138 boolean isLockTaskMode = activityManager.isInLockTaskMode(); 139 140 if (taskViewHasMultipleTasks || notEnoughTasksToSplit || isLockTaskMode || 141 (isFocusedTask && isTaskInExpectedScrollPosition)) { 142 return; 143 } 144 145 List<SplitPositionOption> positions = 146 orientationHandler.getSplitPositionOptions(deviceProfile); 147 for (SplitPositionOption option : positions) { 148 outShortcuts.add(new SplitSelectSystemShortcut(activity, taskView, option)); 149 } 150 } 151 152 public TaskOverlay createOverlay(TaskThumbnailView thumbnailView) { 153 return new TaskOverlay(thumbnailView); 154 } 155 156 /** 157 * Subclasses can attach any system listeners in this method, must be paired with 158 * {@link #removeListeners()} 159 */ 160 public void initListeners() { } 161 162 /** 163 * Subclasses should remove any system listeners in this method, must be paired with 164 * {@link #initListeners()} 165 */ 166 public void removeListeners() { } 167 168 /** Note that these will be shown in order from top to bottom, if available for the task. */ 169 private static final TaskShortcutFactory[] MENU_OPTIONS = new TaskShortcutFactory[]{ 170 TaskShortcutFactory.APP_INFO, 171 TaskShortcutFactory.SPLIT_SCREEN, 172 TaskShortcutFactory.PIN, 173 TaskShortcutFactory.INSTALL, 174 TaskShortcutFactory.FREE_FORM, 175 TaskShortcutFactory.WELLBEING 176 }; 177 178 /** 179 * Overlay on each task handling Overview Action Buttons. 180 */ 181 public static class TaskOverlay<T extends OverviewActionsView> { 182 183 protected final Context mApplicationContext; 184 protected final TaskThumbnailView mThumbnailView; 185 186 private T mActionsView; 187 protected ImageActionsApi mImageApi; 188 189 protected TaskOverlay(TaskThumbnailView taskThumbnailView) { 190 mApplicationContext = taskThumbnailView.getContext().getApplicationContext(); 191 mThumbnailView = taskThumbnailView; 192 mImageApi = new ImageActionsApi( 193 mApplicationContext, mThumbnailView::getThumbnail); 194 } 195 196 protected T getActionsView() { 197 if (mActionsView == null) { 198 mActionsView = BaseActivity.fromContext(mThumbnailView.getContext()).findViewById( 199 R.id.overview_actions_view); 200 } 201 return mActionsView; 202 } 203 204 /** 205 * Called when the current task is interactive for the user 206 */ 207 public void initOverlay(Task task, ThumbnailData thumbnail, Matrix matrix, 208 boolean rotated) { 209 getActionsView().updateDisabledFlags(DISABLED_NO_THUMBNAIL, thumbnail == null); 210 211 if (thumbnail != null) { 212 getActionsView().updateDisabledFlags(DISABLED_ROTATED, rotated); 213 boolean isAllowedByPolicy = mThumbnailView.isRealSnapshot(); 214 getActionsView().setCallbacks(new OverlayUICallbacksImpl(isAllowedByPolicy, task)); 215 } 216 } 217 218 /** 219 * End rendering live tile in Overview. 220 * 221 * @param callback callback to run, after switching to screenshot 222 */ 223 public void endLiveTileMode(@NonNull Runnable callback) { 224 if (ENABLE_QUICKSTEP_LIVE_TILE.get()) { 225 RecentsView recentsView = mThumbnailView.getTaskView().getRecentsView(); 226 recentsView.switchToScreenshot( 227 () -> recentsView.finishRecentsAnimation(true /* toRecents */, 228 false /* shouldPip */, callback)); 229 } else { callback.run()230 callback.run(); 231 } 232 } 233 234 /** 235 * Called to save screenshot of the task thumbnail. 236 */ 237 @SuppressLint("NewApi") saveScreenshot(Task task)238 protected void saveScreenshot(Task task) { 239 if (mThumbnailView.isRealSnapshot()) { 240 mImageApi.saveScreenshot(mThumbnailView.getThumbnail(), 241 getTaskSnapshotBounds(), getTaskSnapshotInsets(), task.key); 242 } else { 243 showBlockedByPolicyMessage(); 244 } 245 } 246 enterSplitSelect()247 private void enterSplitSelect() { 248 RecentsView overviewPanel = mThumbnailView.getTaskView().getRecentsView(); 249 overviewPanel.initiateSplitSelect(mThumbnailView.getTaskView()); 250 } 251 252 /** 253 * Called when the overlay is no longer used. 254 */ reset()255 public void reset() { 256 } 257 258 /** 259 * Called when the system wants to reset the modal visuals. 260 */ resetModalVisuals()261 public void resetModalVisuals() { 262 } 263 264 /** 265 * Gets the modal state system shortcut. 266 */ getModalStateSystemShortcut(WorkspaceItemInfo itemInfo)267 public SystemShortcut getModalStateSystemShortcut(WorkspaceItemInfo itemInfo) { 268 return null; 269 } 270 271 /** 272 * Sets full screen progress to the task overlay. 273 */ setFullscreenProgress(float progress)274 public void setFullscreenProgress(float progress) { 275 } 276 277 /** 278 * Gets the system shortcut for the screenshot that will be added to the task menu. 279 */ getScreenshotShortcut(BaseDraggingActivity activity, ItemInfo iteminfo)280 public SystemShortcut getScreenshotShortcut(BaseDraggingActivity activity, 281 ItemInfo iteminfo) { 282 return new ScreenshotSystemShortcut(activity, iteminfo); 283 } 284 /** 285 * Gets the task snapshot as it is displayed on the screen. 286 * 287 * @return the bounds of the snapshot in screen coordinates. 288 */ getTaskSnapshotBounds()289 public Rect getTaskSnapshotBounds() { 290 int[] location = new int[2]; 291 mThumbnailView.getLocationOnScreen(location); 292 293 return new Rect(location[0], location[1], mThumbnailView.getWidth() + location[0], 294 mThumbnailView.getHeight() + location[1]); 295 } 296 297 /** 298 * Gets the insets that the snapshot is drawn with. 299 * 300 * @return the insets in screen coordinates. 301 */ 302 @RequiresApi(api = Build.VERSION_CODES.Q) getTaskSnapshotInsets()303 public Insets getTaskSnapshotInsets() { 304 return mThumbnailView.getScaledInsets(); 305 } 306 307 /** 308 * Called when the device rotated. 309 */ updateOrientationState(RecentsOrientedState state)310 public void updateOrientationState(RecentsOrientedState state) { 311 } 312 showBlockedByPolicyMessage()313 protected void showBlockedByPolicyMessage() { 314 Toast.makeText( 315 mThumbnailView.getContext(), 316 R.string.blocked_by_policy, 317 Toast.LENGTH_LONG).show(); 318 } 319 320 private class ScreenshotSystemShortcut extends SystemShortcut { 321 322 private final BaseDraggingActivity mActivity; 323 ScreenshotSystemShortcut(BaseDraggingActivity activity, ItemInfo itemInfo)324 ScreenshotSystemShortcut(BaseDraggingActivity activity, ItemInfo itemInfo) { 325 super(R.drawable.ic_screenshot, R.string.action_screenshot, activity, itemInfo); 326 mActivity = activity; 327 } 328 329 @Override onClick(View view)330 public void onClick(View view) { 331 saveScreenshot(mThumbnailView.getTaskView().getTask()); 332 dismissTaskMenuView(mActivity); 333 } 334 } 335 336 protected class OverlayUICallbacksImpl implements OverlayUICallbacks { 337 protected final boolean mIsAllowedByPolicy; 338 protected final Task mTask; 339 OverlayUICallbacksImpl(boolean isAllowedByPolicy, Task task)340 public OverlayUICallbacksImpl(boolean isAllowedByPolicy, Task task) { 341 mIsAllowedByPolicy = isAllowedByPolicy; 342 mTask = task; 343 } 344 345 @SuppressLint("NewApi") onScreenshot()346 public void onScreenshot() { 347 endLiveTileMode(() -> saveScreenshot(mTask)); 348 } 349 onSplit()350 public void onSplit() { 351 endLiveTileMode(TaskOverlay.this::enterSplitSelect); 352 } 353 } 354 } 355 356 /** 357 * Callbacks the Ui can generate. This is the only way for a Ui to call methods on the 358 * controller. 359 */ 360 public interface OverlayUICallbacks { 361 /** User has indicated they want to screenshot the current task. */ 362 void onScreenshot(); 363 364 /** User wants to start split screen with current app. */ 365 void onSplit(); 366 } 367 } 368