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.Display.DEFAULT_DISPLAY; 20 21 import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS; 22 import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_SELECTIONS; 23 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP; 24 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_SPLIT_SCREEN_TAP; 25 import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.TAP; 26 27 import android.app.Activity; 28 import android.app.ActivityOptions; 29 import android.graphics.Bitmap; 30 import android.graphics.Color; 31 import android.graphics.Rect; 32 import android.os.Handler; 33 import android.os.Looper; 34 import android.view.View; 35 36 import com.android.launcher3.BaseDraggingActivity; 37 import com.android.launcher3.DeviceProfile; 38 import com.android.launcher3.R; 39 import com.android.launcher3.logging.StatsLogManager.LauncherEvent; 40 import com.android.launcher3.model.WellbeingModel; 41 import com.android.launcher3.popup.SystemShortcut; 42 import com.android.launcher3.popup.SystemShortcut.AppInfo; 43 import com.android.launcher3.userevent.nano.LauncherLogProto; 44 import com.android.launcher3.util.Executors; 45 import com.android.launcher3.util.InstantAppResolver; 46 import com.android.quickstep.views.RecentsView; 47 import com.android.quickstep.views.TaskThumbnailView; 48 import com.android.quickstep.views.TaskView; 49 import com.android.systemui.shared.recents.model.Task; 50 import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecCompat; 51 import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecsFuture; 52 import com.android.systemui.shared.recents.view.RecentsTransition; 53 import com.android.systemui.shared.system.ActivityCompat; 54 import com.android.systemui.shared.system.ActivityManagerWrapper; 55 import com.android.systemui.shared.system.ActivityOptionsCompat; 56 import com.android.systemui.shared.system.WindowManagerWrapper; 57 58 import java.util.Collections; 59 import java.util.List; 60 import java.util.function.Consumer; 61 62 /** 63 * Represents a system shortcut that can be shown for a recent task. 64 */ 65 public interface TaskShortcutFactory { 66 getShortcut(BaseDraggingActivity activity, TaskView view)67 SystemShortcut getShortcut(BaseDraggingActivity activity, TaskView view); 68 69 TaskShortcutFactory APP_INFO = (activity, view) -> new AppInfo(activity, view.getItemInfo()); 70 71 abstract class MultiWindowFactory implements TaskShortcutFactory { 72 73 private final int mIconRes; 74 private final int mTextRes; 75 private final LauncherEvent mLauncherEvent; 76 MultiWindowFactory(int iconRes, int textRes, LauncherEvent launcherEvent)77 MultiWindowFactory(int iconRes, int textRes, LauncherEvent launcherEvent) { 78 mIconRes = iconRes; 79 mTextRes = textRes; 80 mLauncherEvent = launcherEvent; 81 } 82 isAvailable(BaseDraggingActivity activity, int displayId)83 protected abstract boolean isAvailable(BaseDraggingActivity activity, int displayId); makeLaunchOptions(Activity activity)84 protected abstract ActivityOptions makeLaunchOptions(Activity activity); onActivityStarted(BaseDraggingActivity activity)85 protected abstract boolean onActivityStarted(BaseDraggingActivity activity); 86 87 @Override getShortcut(BaseDraggingActivity activity, TaskView taskView)88 public SystemShortcut getShortcut(BaseDraggingActivity activity, TaskView taskView) { 89 final Task task = taskView.getTask(); 90 if (!task.isDockable) { 91 return null; 92 } 93 if (!isAvailable(activity, task.key.displayId)) { 94 return null; 95 } 96 return new MultiWindowSystemShortcut(mIconRes, mTextRes, activity, taskView, this, 97 mLauncherEvent); 98 } 99 } 100 101 class MultiWindowSystemShortcut extends SystemShortcut { 102 103 private Handler mHandler; 104 105 private final RecentsView mRecentsView; 106 private final TaskThumbnailView mThumbnailView; 107 private final TaskView mTaskView; 108 private final MultiWindowFactory mFactory; 109 private final LauncherEvent mLauncherEvent; 110 MultiWindowSystemShortcut(int iconRes, int textRes, BaseDraggingActivity activity, TaskView taskView, MultiWindowFactory factory, LauncherEvent launcherEvent)111 public MultiWindowSystemShortcut(int iconRes, int textRes, BaseDraggingActivity activity, 112 TaskView taskView, MultiWindowFactory factory, LauncherEvent launcherEvent) { 113 super(iconRes, textRes, activity, taskView.getItemInfo()); 114 mLauncherEvent = launcherEvent; 115 mHandler = new Handler(Looper.getMainLooper()); 116 mTaskView = taskView; 117 mRecentsView = activity.getOverviewPanel(); 118 mThumbnailView = taskView.getThumbnail(); 119 mFactory = factory; 120 } 121 122 @Override onClick(View view)123 public void onClick(View view) { 124 Task.TaskKey taskKey = mTaskView.getTask().key; 125 final int taskId = taskKey.id; 126 127 final View.OnLayoutChangeListener onLayoutChangeListener = 128 new View.OnLayoutChangeListener() { 129 @Override 130 public void onLayoutChange(View v, int l, int t, int r, int b, 131 int oldL, int oldT, int oldR, int oldB) { 132 mTaskView.getRootView().removeOnLayoutChangeListener(this); 133 mRecentsView.clearIgnoreResetTask(taskId); 134 135 // Start animating in the side pages once launcher has been resized 136 mRecentsView.dismissTask(mTaskView, false, false); 137 } 138 }; 139 140 final DeviceProfile.OnDeviceProfileChangeListener onDeviceProfileChangeListener = 141 new DeviceProfile.OnDeviceProfileChangeListener() { 142 @Override 143 public void onDeviceProfileChanged(DeviceProfile dp) { 144 mTarget.removeOnDeviceProfileChangeListener(this); 145 if (dp.isMultiWindowMode) { 146 mTaskView.getRootView().addOnLayoutChangeListener( 147 onLayoutChangeListener); 148 } 149 } 150 }; 151 152 dismissTaskMenuView(mTarget); 153 154 ActivityOptions options = mFactory.makeLaunchOptions(mTarget); 155 if (options != null 156 && ActivityManagerWrapper.getInstance().startActivityFromRecents(taskId, 157 options)) { 158 if (!mFactory.onActivityStarted(mTarget)) { 159 return; 160 } 161 // Add a device profile change listener to kick off animating the side tasks 162 // once we enter multiwindow mode and relayout 163 mTarget.addOnDeviceProfileChangeListener(onDeviceProfileChangeListener); 164 165 final Runnable animStartedListener = () -> { 166 // Hide the task view and wait for the window to be resized 167 // TODO: Consider animating in launcher and do an in-place start activity 168 // afterwards 169 mRecentsView.setIgnoreResetTask(taskId); 170 mTaskView.setAlpha(0f); 171 }; 172 173 final int[] position = new int[2]; 174 mThumbnailView.getLocationOnScreen(position); 175 final int width = (int) (mThumbnailView.getWidth() * mTaskView.getScaleX()); 176 final int height = (int) (mThumbnailView.getHeight() * mTaskView.getScaleY()); 177 final Rect taskBounds = new Rect(position[0], position[1], 178 position[0] + width, position[1] + height); 179 180 // Take the thumbnail of the task without a scrim and apply it back after 181 float alpha = mThumbnailView.getDimAlpha(); 182 mThumbnailView.setDimAlpha(0); 183 Bitmap thumbnail = RecentsTransition.drawViewIntoHardwareBitmap( 184 taskBounds.width(), taskBounds.height(), mThumbnailView, 1f, 185 Color.BLACK); 186 mThumbnailView.setDimAlpha(alpha); 187 188 AppTransitionAnimationSpecsFuture future = 189 new AppTransitionAnimationSpecsFuture(mHandler) { 190 @Override 191 public List<AppTransitionAnimationSpecCompat> composeSpecs() { 192 return Collections.singletonList(new AppTransitionAnimationSpecCompat( 193 taskId, thumbnail, taskBounds)); 194 } 195 }; 196 WindowManagerWrapper.getInstance().overridePendingAppTransitionMultiThumbFuture( 197 future, animStartedListener, mHandler, true /* scaleUp */, 198 taskKey.displayId); 199 mTarget.getStatsLogManager().logger().withItemInfo(mTaskView.getItemInfo()) 200 .log(mLauncherEvent); 201 } 202 } 203 } 204 205 TaskShortcutFactory SPLIT_SCREEN = new MultiWindowFactory(R.drawable.ic_split_screen, 206 R.string.recent_task_option_split_screen, LAUNCHER_SYSTEM_SHORTCUT_SPLIT_SCREEN_TAP) { 207 208 @Override 209 protected boolean isAvailable(BaseDraggingActivity activity, int displayId) { 210 // Don't show menu-item if already in multi-window and the task is from 211 // the secondary display. 212 // TODO(b/118266305): Temporarily disable splitscreen for secondary display while new 213 // implementation is enabled 214 return !activity.getDeviceProfile().isMultiWindowMode 215 && (displayId == -1 || displayId == DEFAULT_DISPLAY); 216 } 217 218 @Override 219 protected ActivityOptions makeLaunchOptions(Activity activity) { 220 final ActivityCompat act = new ActivityCompat(activity); 221 final int navBarPosition = WindowManagerWrapper.getInstance().getNavBarPosition( 222 act.getDisplayId()); 223 if (navBarPosition == WindowManagerWrapper.NAV_BAR_POS_INVALID) { 224 return null; 225 } 226 boolean dockTopOrLeft = navBarPosition != WindowManagerWrapper.NAV_BAR_POS_LEFT; 227 return ActivityOptionsCompat.makeSplitScreenOptions(dockTopOrLeft); 228 } 229 230 @Override 231 protected boolean onActivityStarted(BaseDraggingActivity activity) { 232 SystemUiProxy.INSTANCE.get(activity).onSplitScreenInvoked(); 233 activity.getUserEventDispatcher().logActionOnControl(TAP, 234 LauncherLogProto.ControlType.SPLIT_SCREEN_TARGET); 235 return true; 236 } 237 }; 238 239 TaskShortcutFactory FREE_FORM = new MultiWindowFactory(R.drawable.ic_split_screen, 240 R.string.recent_task_option_freeform, LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP) { 241 242 @Override 243 protected boolean isAvailable(BaseDraggingActivity activity, int displayId) { 244 return ActivityManagerWrapper.getInstance().supportsFreeformMultiWindow(activity); 245 } 246 247 @Override 248 protected ActivityOptions makeLaunchOptions(Activity activity) { 249 ActivityOptions activityOptions = ActivityOptionsCompat.makeFreeformOptions(); 250 // Arbitrary bounds only because freeform is in dev mode right now 251 Rect r = new Rect(50, 50, 200, 200); 252 activityOptions.setLaunchBounds(r); 253 return activityOptions; 254 } 255 256 @Override 257 protected boolean onActivityStarted(BaseDraggingActivity activity) { 258 activity.returnToHomescreen(); 259 return true; 260 } 261 }; 262 263 TaskShortcutFactory PIN = (activity, tv) -> { 264 if (!SystemUiProxy.INSTANCE.get(activity).isActive()) { 265 return null; 266 } 267 if (!ActivityManagerWrapper.getInstance().isScreenPinningEnabled()) { 268 return null; 269 } 270 if (ActivityManagerWrapper.getInstance().isLockToAppActive()) { 271 // We shouldn't be able to pin while an app is locked. 272 return null; 273 } 274 return new PinSystemShortcut(activity, tv); 275 }; 276 277 class PinSystemShortcut extends SystemShortcut { 278 279 private static final String TAG = "PinSystemShortcut"; 280 281 private final TaskView mTaskView; 282 PinSystemShortcut(BaseDraggingActivity target, TaskView tv)283 public PinSystemShortcut(BaseDraggingActivity target, TaskView tv) { 284 super(R.drawable.ic_pin, R.string.recent_task_option_pin, target, tv.getItemInfo()); 285 mTaskView = tv; 286 } 287 288 @Override onClick(View view)289 public void onClick(View view) { 290 Consumer<Boolean> resultCallback = success -> { 291 if (success) { 292 SystemUiProxy.INSTANCE.get(mTarget).startScreenPinning( 293 mTaskView.getTask().key.id); 294 } else { 295 mTaskView.notifyTaskLaunchFailed(TAG); 296 } 297 }; 298 mTaskView.launchTask(true, resultCallback, Executors.MAIN_EXECUTOR.getHandler()); 299 dismissTaskMenuView(mTarget); 300 mTarget.getStatsLogManager().logger().withItemInfo(mTaskView.getItemInfo()) 301 .log(LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_PIN_TAP); 302 } 303 } 304 305 TaskShortcutFactory INSTALL = (activity, view) -> 306 InstantAppResolver.newInstance(activity).isInstantApp(activity, 307 view.getTask().getTopComponent().getPackageName()) 308 ? new SystemShortcut.Install(activity, view.getItemInfo()) : null; 309 310 TaskShortcutFactory WELLBEING = (activity, view) -> 311 WellbeingModel.SHORTCUT_FACTORY.getShortcut(activity, view.getItemInfo()); 312 313 TaskShortcutFactory SCREENSHOT = (activity, tv) -> { 314 if (ENABLE_OVERVIEW_ACTIONS.get()) { 315 return tv.getThumbnail().getTaskOverlay() 316 .getScreenshotShortcut(activity, tv.getItemInfo()); 317 } 318 return null; 319 }; 320 321 TaskShortcutFactory MODAL = (activity, tv) -> { 322 if (ENABLE_OVERVIEW_ACTIONS.get() && ENABLE_OVERVIEW_SELECTIONS.get()) { 323 return tv.getThumbnail().getTaskOverlay().getModalStateSystemShortcut(tv.getItemInfo()); 324 } 325 return null; 326 }; 327 } 328