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 com.android.launcher3.Flags.enableRefactorTaskThumbnail; 20 import static com.android.quickstep.views.OverviewActionsView.DISABLED_NO_THUMBNAIL; 21 import static com.android.quickstep.views.OverviewActionsView.DISABLED_ROTATED; 22 23 import android.annotation.SuppressLint; 24 import android.content.Context; 25 import android.graphics.Bitmap; 26 import android.graphics.Insets; 27 import android.graphics.Matrix; 28 import android.graphics.Rect; 29 import android.graphics.RectF; 30 import android.os.Build; 31 import android.view.View; 32 33 import androidx.annotation.NonNull; 34 import androidx.annotation.Nullable; 35 import androidx.annotation.RequiresApi; 36 37 import com.android.launcher3.BaseActivity; 38 import com.android.launcher3.R; 39 import com.android.launcher3.model.data.ItemInfo; 40 import com.android.launcher3.model.data.WorkspaceItemInfo; 41 import com.android.launcher3.popup.SystemShortcut; 42 import com.android.launcher3.util.ResourceBasedOverride; 43 import com.android.launcher3.views.ActivityContext; 44 import com.android.launcher3.views.Snackbar; 45 import com.android.quickstep.recents.domain.usecase.ThumbnailPosition; 46 import com.android.quickstep.util.RecentsOrientedState; 47 import com.android.quickstep.views.DesktopTaskView; 48 import com.android.quickstep.views.GroupedTaskView; 49 import com.android.quickstep.views.OverviewActionsView; 50 import com.android.quickstep.views.RecentsView; 51 import com.android.quickstep.views.RecentsViewContainer; 52 import com.android.quickstep.views.TaskContainer; 53 import com.android.quickstep.views.TaskView; 54 import com.android.systemui.shared.recents.model.Task; 55 import com.android.systemui.shared.recents.model.ThumbnailData; 56 57 import java.util.ArrayList; 58 import java.util.List; 59 60 /** 61 * Factory class to create and add an overlays on the TaskView 62 */ 63 public class TaskOverlayFactory implements ResourceBasedOverride { 64 getEnabledShortcuts(TaskView taskView, TaskContainer taskContainer)65 public static List<SystemShortcut> getEnabledShortcuts(TaskView taskView, 66 TaskContainer taskContainer) { 67 final ArrayList<SystemShortcut> shortcuts = new ArrayList<>(); 68 final RecentsViewContainer container = 69 RecentsViewContainer.containerFromContext(taskView.getContext()); 70 for (TaskShortcutFactory menuOption : MENU_OPTIONS) { 71 if (taskView instanceof GroupedTaskView && !menuOption.showForGroupedTask()) { 72 continue; 73 } 74 if (taskView instanceof DesktopTaskView && !menuOption.showForDesktopTask()) { 75 continue; 76 } 77 78 List<SystemShortcut> menuShortcuts = menuOption.getShortcuts(container, taskContainer); 79 if (menuShortcuts == null) { 80 continue; 81 } 82 shortcuts.addAll(menuShortcuts); 83 } 84 return shortcuts; 85 } 86 87 /** Creates a {@link TaskOverlay} associated with the provide {@link TaskContainer}. */ createOverlay(TaskContainer taskContainer)88 public TaskOverlay<?> createOverlay(TaskContainer taskContainer) { 89 return new TaskOverlay<>(taskContainer); 90 } 91 92 /** 93 * Subclasses can attach any system listeners in this method, must be paired with 94 * {@link #removeListeners()} 95 */ initListeners()96 public void initListeners() { 97 } 98 99 /** 100 * Subclasses should remove any system listeners in this method, must be paired with 101 * {@link #initListeners()} 102 */ removeListeners()103 public void removeListeners() { 104 } 105 106 /** 107 * Clears any active state outside of the TaskOverlay lifecycle which might have built 108 * up over time 109 */ clearAllActiveState()110 public void clearAllActiveState() { } 111 112 /** Note that these will be shown in order from top to bottom, if available for the task. */ 113 private static final TaskShortcutFactory[] MENU_OPTIONS = new TaskShortcutFactory[]{ 114 TaskShortcutFactory.APP_INFO, 115 TaskShortcutFactory.SPLIT_SELECT, 116 TaskShortcutFactory.PIN, 117 TaskShortcutFactory.INSTALL, 118 TaskShortcutFactory.FREE_FORM, 119 DesktopSystemShortcut.Companion.createFactory(), 120 ExternalDisplaySystemShortcut.Companion.createFactory(), 121 AspectRatioSystemShortcut.Companion.createFactory(), 122 TaskShortcutFactory.WELLBEING, 123 TaskShortcutFactory.SAVE_APP_PAIR, 124 TaskShortcutFactory.SCREENSHOT, 125 TaskShortcutFactory.MODAL, 126 TaskShortcutFactory.CLOSE, 127 }; 128 129 /** 130 * Overlay on each task handling Overview Action Buttons. 131 */ 132 public static class TaskOverlay<T extends OverviewActionsView> { 133 134 protected final Context mApplicationContext; 135 protected final TaskContainer mTaskContainer; 136 137 private T mActionsView; 138 protected ImageActionsApi mImageApi; 139 private ThumbnailData mThumbnailData = null; 140 TaskOverlay(TaskContainer taskContainer)141 protected TaskOverlay(TaskContainer taskContainer) { 142 mApplicationContext = taskContainer.getTaskView().getContext().getApplicationContext(); 143 mTaskContainer = taskContainer; 144 mImageApi = new ImageActionsApi(mApplicationContext, this::getThumbnail); 145 } 146 setThumbnailState(@ullable ThumbnailData thumbnailData)147 public void setThumbnailState(@Nullable ThumbnailData thumbnailData) { 148 mThumbnailData = thumbnailData; 149 } 150 getThumbnail()151 protected @Nullable Bitmap getThumbnail() { 152 if (enableRefactorTaskThumbnail()) { 153 return mThumbnailData == null ? null : mThumbnailData.getThumbnail(); 154 } else { 155 return mTaskContainer.getThumbnailViewDeprecated().getThumbnail(); 156 } 157 } 158 /** 159 * Returns whether the snapshot is real. If the device is locked for the user of the task, 160 * the snapshot used will be an app-theme generated snapshot instead of a real snapshot. 161 */ isRealSnapshot()162 protected boolean isRealSnapshot() { 163 if (enableRefactorTaskThumbnail()) { 164 if (mThumbnailData == null) return false; 165 166 return mThumbnailData.isRealSnapshot && !mTaskContainer.getTask().isLocked; 167 } else { 168 return mTaskContainer.getThumbnailViewDeprecated().isRealSnapshot(); 169 } 170 } 171 172 /** 173 * Returns whether the snapshot is rotated compared to the current task orientation. 174 */ isThumbnailRotationDifferentFromTask()175 public boolean isThumbnailRotationDifferentFromTask() { 176 if (enableRefactorTaskThumbnail()) { 177 ThumbnailPosition thumbnailPosition = mTaskContainer.getThumbnailPosition(); 178 return thumbnailPosition != null && thumbnailPosition.isRotated(); 179 } 180 181 return mTaskContainer.getThumbnailViewDeprecated() 182 .isThumbnailRotationDifferentFromTask(); 183 } 184 getActionsView()185 protected T getActionsView() { 186 if (mActionsView == null) { 187 mActionsView = (T) RecentsViewContainer.containerFromContext( 188 mTaskContainer.getTaskView().getContext()).getActionsView(); 189 } 190 return mActionsView; 191 } 192 getTaskView()193 public TaskView getTaskView() { 194 return mTaskContainer.getTaskView(); 195 } 196 getSnapshotView()197 public View getSnapshotView() { 198 return mTaskContainer.getSnapshotView(); 199 } 200 201 /** 202 * Called when the current task is interactive for the user 203 */ initOverlay(Task task, @Nullable Bitmap thumbnail, Matrix matrix, boolean rotated)204 public void initOverlay(Task task, @Nullable Bitmap thumbnail, Matrix matrix, 205 boolean rotated) { 206 if (!enableRefactorTaskThumbnail()) { 207 getActionsView().updateDisabledFlags(DISABLED_NO_THUMBNAIL, thumbnail == null); 208 } 209 210 if (thumbnail != null) { 211 if (!enableRefactorTaskThumbnail()) { 212 getActionsView().updateDisabledFlags(DISABLED_ROTATED, rotated); 213 } 214 getActionsView().setCallbacks(new OverlayUICallbacksImpl(isRealSnapshot(), task)); 215 } 216 } 217 218 /** 219 * End rendering live tile in Overview. 220 * 221 * @param callback callback to run, after switching to screenshot 222 */ endLiveTileMode(@onNull Runnable callback)223 public void endLiveTileMode(@NonNull Runnable callback) { 224 RecentsView recentsView = 225 mTaskContainer.getTaskView().getRecentsView(); 226 // Task has already been dismissed 227 if (recentsView == null) return; 228 recentsView.switchToScreenshot( 229 () -> recentsView.finishRecentsAnimation(true /* toRecents */, 230 false /* shouldPip */, callback)); 231 } 232 233 /** 234 * Called to save screenshot of the task thumbnail. 235 */ 236 @SuppressLint("NewApi") saveScreenshot(Task task)237 protected void saveScreenshot(Task task) { 238 if (isRealSnapshot()) { 239 mImageApi.saveScreenshot(getThumbnail(), 240 getTaskSnapshotBounds(), getTaskSnapshotInsets(), task.key); 241 } else { 242 showBlockedByPolicyMessage(); 243 } 244 } 245 enterSplitSelect()246 protected void enterSplitSelect() { 247 RecentsView overviewPanel = mTaskContainer.getTaskView().getRecentsView(); 248 // Task has already been dismissed 249 if (overviewPanel == null) return; 250 overviewPanel.initiateSplitSelect(mTaskContainer); 251 } 252 saveAppPair()253 protected void saveAppPair() { 254 GroupedTaskView taskView = (GroupedTaskView) mTaskContainer.getTaskView(); 255 taskView.getRecentsView().getSplitSelectController().getAppPairsController() 256 .saveAppPair(taskView); 257 } 258 259 /** 260 * Called when the overlay is no longer used. 261 */ reset()262 public void reset() { 263 } 264 265 /** 266 * Called when the system wants to reset the modal visuals. 267 */ resetModalVisuals()268 public void resetModalVisuals() { 269 } 270 271 /** 272 * Gets the modal state system shortcut. 273 */ getModalStateSystemShortcut(WorkspaceItemInfo itemInfo, View original)274 public SystemShortcut getModalStateSystemShortcut(WorkspaceItemInfo itemInfo, 275 View original) { 276 return null; 277 } 278 279 /** 280 * Sets full screen progress to the task overlay. 281 */ setFullscreenProgress(float progress)282 public void setFullscreenProgress(float progress) { 283 } 284 285 /** 286 * Gets the system shortcut for the screenshot that will be added to the task menu. 287 */ getScreenshotShortcut(RecentsViewContainer container, ItemInfo iteminfo, View originalView)288 public SystemShortcut getScreenshotShortcut(RecentsViewContainer container, 289 ItemInfo iteminfo, View originalView) { 290 return new ScreenshotSystemShortcut(container, iteminfo, originalView); 291 } 292 293 /** 294 * Gets the task snapshot as it is displayed on the screen. 295 * 296 * @return the bounds of the snapshot in screen coordinates. 297 */ getTaskSnapshotBounds()298 public Rect getTaskSnapshotBounds() { 299 int[] location = new int[2]; 300 mTaskContainer.getSnapshotView().getLocationOnScreen(location); 301 302 return new Rect(location[0], location[1], 303 mTaskContainer.getSnapshotView().getWidth() + location[0], 304 mTaskContainer.getSnapshotView().getHeight() + location[1]); 305 } 306 307 /** 308 * Gets the insets that the snapshot is drawn with. 309 * 310 * @return the insets in screen coordinates. 311 */ 312 @RequiresApi(api = Build.VERSION_CODES.Q) getTaskSnapshotInsets()313 public Insets getTaskSnapshotInsets() { 314 Bitmap thumbnail = getThumbnail(); 315 if (thumbnail == null) { 316 return Insets.NONE; 317 } 318 319 RectF bitmapRect = new RectF( 320 0, 321 0, 322 thumbnail.getWidth(), 323 thumbnail.getHeight()); 324 View snapshotView = mTaskContainer.getSnapshotView(); 325 RectF viewRect = new RectF(0, 0, snapshotView.getMeasuredWidth(), 326 snapshotView.getMeasuredHeight()); 327 328 // The position helper matrix tells us how to transform the bitmap to fit the view, the 329 // inverse tells us where the view would be in the bitmaps coordinates. The insets are 330 // the difference between the bitmap bounds and the projected view bounds. 331 Matrix boundsToBitmapSpace = new Matrix(); 332 Matrix thumbnailMatrix; 333 if (enableRefactorTaskThumbnail()) { 334 if (mTaskContainer.getThumbnailPosition() != null) { 335 thumbnailMatrix = mTaskContainer.getThumbnailPosition().getMatrix(); 336 } else { 337 thumbnailMatrix = Matrix.IDENTITY_MATRIX; 338 } 339 } else { 340 thumbnailMatrix = mTaskContainer.getThumbnailViewDeprecated().getThumbnailMatrix(); 341 } 342 thumbnailMatrix.invert(boundsToBitmapSpace); 343 RectF boundsInBitmapSpace = new RectF(); 344 boundsToBitmapSpace.mapRect(boundsInBitmapSpace, viewRect); 345 346 RecentsViewContainer container = RecentsViewContainer.containerFromContext( 347 getTaskView().getContext()); 348 int bottomInset = container.getDeviceProfile().isTablet 349 ? Math.round(bitmapRect.bottom - boundsInBitmapSpace.bottom) : 0; 350 return Insets.of(0, 0, 0, bottomInset); 351 } 352 353 /** 354 * Called when the device rotated. 355 */ updateOrientationState(RecentsOrientedState state)356 public void updateOrientationState(RecentsOrientedState state) { 357 } 358 showBlockedByPolicyMessage()359 protected void showBlockedByPolicyMessage() { 360 ActivityContext activityContext = ActivityContext.lookupContext( 361 mTaskContainer.getTaskView().getContext()); 362 String message = activityContext.getStringCache() != null 363 ? activityContext.getStringCache().disabledByAdminMessage 364 : mTaskContainer.getTaskView().getContext().getString( 365 R.string.blocked_by_policy); 366 367 Snackbar.show(BaseActivity.fromContext( 368 mTaskContainer.getTaskView().getContext()), message, null); 369 } 370 371 /** Called when the snapshot has updated its full screen drawing parameters. */ setFullscreenParams(FullscreenDrawParams fullscreenParams)372 public void setFullscreenParams(FullscreenDrawParams fullscreenParams) {} 373 374 /** Sets visibility for the overlay associated elements. */ setVisibility(int visibility)375 public void setVisibility(int visibility) {} 376 377 /** See {@link View#addChildrenForAccessibility(ArrayList)} */ addChildForAccessibility(ArrayList<View> outChildren)378 public void addChildForAccessibility(ArrayList<View> outChildren) {} 379 380 private class ScreenshotSystemShortcut extends SystemShortcut { 381 ScreenshotSystemShortcut(RecentsViewContainer container, ItemInfo itemInfo, View originalView)382 ScreenshotSystemShortcut(RecentsViewContainer container, ItemInfo itemInfo, 383 View originalView) { 384 super(R.drawable.ic_screenshot, R.string.action_screenshot, container, itemInfo, 385 originalView); 386 } 387 388 @Override onClick(View view)389 public void onClick(View view) { 390 saveScreenshot(mTaskContainer.getTask()); 391 dismissTaskMenuView(); 392 } 393 } 394 395 protected class OverlayUICallbacksImpl implements OverlayUICallbacks { 396 protected final boolean mIsAllowedByPolicy; 397 protected final Task mTask; 398 OverlayUICallbacksImpl(boolean isAllowedByPolicy, Task task)399 public OverlayUICallbacksImpl(boolean isAllowedByPolicy, Task task) { 400 mIsAllowedByPolicy = isAllowedByPolicy; 401 mTask = task; 402 } 403 404 @SuppressLint("NewApi") onScreenshot()405 public void onScreenshot() { 406 endLiveTileMode(() -> saveScreenshot(mTask)); 407 } 408 onSplit()409 public void onSplit() { 410 endLiveTileMode(TaskOverlay.this::enterSplitSelect); 411 } 412 onSaveAppPair()413 public void onSaveAppPair() { 414 endLiveTileMode(TaskOverlay.this::saveAppPair); 415 } 416 } 417 } 418 419 /** 420 * Callbacks the Ui can generate. This is the only way for a Ui to call methods on the 421 * controller. 422 */ 423 public interface OverlayUICallbacks { 424 /** User has indicated they want to screenshot the current task. */ onScreenshot()425 void onScreenshot(); 426 427 /** User wants to start split screen with current app. */ onSplit()428 void onSplit(); 429 430 /** User wants to save an app pair with current group of apps. */ onSaveAppPair()431 void onSaveAppPair(); 432 } 433 } 434