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