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