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