1 /* 2 * Copyright (C) 2020 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.wm.shell.draganddrop; 18 19 import static android.app.ActivityTaskManager.INVALID_TASK_ID; 20 import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED; 21 import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION; 22 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; 23 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; 24 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; 25 import static android.content.ClipDescription.EXTRA_ACTIVITY_OPTIONS; 26 import static android.content.ClipDescription.EXTRA_PENDING_INTENT; 27 import static android.content.ClipDescription.MIMETYPE_APPLICATION_SHORTCUT; 28 import static android.content.ClipDescription.MIMETYPE_APPLICATION_TASK; 29 import static android.content.Intent.EXTRA_PACKAGE_NAME; 30 import static android.content.Intent.EXTRA_SHORTCUT_ID; 31 import static android.content.Intent.EXTRA_TASK_ID; 32 import static android.content.Intent.EXTRA_USER; 33 34 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; 35 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; 36 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; 37 import static com.android.wm.shell.draganddrop.DragAndDropConstants.EXTRA_DISALLOW_HIT_REGION; 38 import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_FULLSCREEN; 39 import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_BOTTOM; 40 import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_LEFT; 41 import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_RIGHT; 42 import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_TOP; 43 44 import android.app.ActivityManager; 45 import android.app.ActivityTaskManager; 46 import android.app.PendingIntent; 47 import android.app.WindowConfiguration; 48 import android.content.ActivityNotFoundException; 49 import android.content.ClipData; 50 import android.content.ClipDescription; 51 import android.content.Context; 52 import android.content.Intent; 53 import android.content.pm.ActivityInfo; 54 import android.content.pm.LauncherApps; 55 import android.graphics.Insets; 56 import android.graphics.Rect; 57 import android.graphics.RectF; 58 import android.os.Bundle; 59 import android.os.RemoteException; 60 import android.os.UserHandle; 61 import android.util.Slog; 62 63 import androidx.annotation.IntDef; 64 import androidx.annotation.NonNull; 65 import androidx.annotation.Nullable; 66 import androidx.annotation.VisibleForTesting; 67 68 import com.android.internal.logging.InstanceId; 69 import com.android.wm.shell.R; 70 import com.android.wm.shell.common.DisplayLayout; 71 import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; 72 import com.android.wm.shell.splitscreen.SplitScreenController; 73 74 import java.lang.annotation.Retention; 75 import java.lang.annotation.RetentionPolicy; 76 import java.util.ArrayList; 77 import java.util.List; 78 79 /** 80 * The policy for handling drag and drop operations to shell. 81 */ 82 public class DragAndDropPolicy { 83 84 private static final String TAG = DragAndDropPolicy.class.getSimpleName(); 85 86 private final Context mContext; 87 private final ActivityTaskManager mActivityTaskManager; 88 private final Starter mStarter; 89 private final SplitScreenController mSplitScreen; 90 private final ArrayList<DragAndDropPolicy.Target> mTargets = new ArrayList<>(); 91 private final RectF mDisallowHitRegion = new RectF(); 92 93 private InstanceId mLoggerSessionId; 94 private DragSession mSession; 95 DragAndDropPolicy(Context context, SplitScreenController splitScreen)96 public DragAndDropPolicy(Context context, SplitScreenController splitScreen) { 97 this(context, ActivityTaskManager.getInstance(), splitScreen, new DefaultStarter(context)); 98 } 99 100 @VisibleForTesting DragAndDropPolicy(Context context, ActivityTaskManager activityTaskManager, SplitScreenController splitScreen, Starter starter)101 DragAndDropPolicy(Context context, ActivityTaskManager activityTaskManager, 102 SplitScreenController splitScreen, Starter starter) { 103 mContext = context; 104 mActivityTaskManager = activityTaskManager; 105 mSplitScreen = splitScreen; 106 mStarter = mSplitScreen != null ? mSplitScreen : starter; 107 } 108 109 /** 110 * Starts a new drag session with the given initial drag data. 111 */ start(DisplayLayout displayLayout, ClipData data, InstanceId loggerSessionId)112 void start(DisplayLayout displayLayout, ClipData data, InstanceId loggerSessionId) { 113 mLoggerSessionId = loggerSessionId; 114 mSession = new DragSession(mActivityTaskManager, displayLayout, data); 115 // TODO(b/169894807): Also update the session data with task stack changes 116 mSession.update(); 117 RectF disallowHitRegion = (RectF) mSession.dragData.getExtra(EXTRA_DISALLOW_HIT_REGION); 118 if (disallowHitRegion == null) { 119 mDisallowHitRegion.setEmpty(); 120 } else { 121 mDisallowHitRegion.set(disallowHitRegion); 122 } 123 } 124 125 /** 126 * Returns the last running task. 127 */ getLatestRunningTask()128 ActivityManager.RunningTaskInfo getLatestRunningTask() { 129 return mSession.runningTaskInfo; 130 } 131 132 /** 133 * Returns the number of targets. 134 */ getNumTargets()135 int getNumTargets() { 136 return mTargets.size(); 137 } 138 139 /** 140 * Returns the target's regions based on the current state of the device and display. 141 */ 142 @NonNull getTargets(Insets insets)143 ArrayList<Target> getTargets(Insets insets) { 144 mTargets.clear(); 145 if (mSession == null) { 146 // Return early if this isn't an app drag 147 return mTargets; 148 } 149 150 final int w = mSession.displayLayout.width(); 151 final int h = mSession.displayLayout.height(); 152 final int iw = w - insets.left - insets.right; 153 final int ih = h - insets.top - insets.bottom; 154 final int l = insets.left; 155 final int t = insets.top; 156 final Rect displayRegion = new Rect(l, t, l + iw, t + ih); 157 final Rect fullscreenDrawRegion = new Rect(displayRegion); 158 final Rect fullscreenHitRegion = new Rect(displayRegion); 159 final boolean inLandscape = mSession.displayLayout.isLandscape(); 160 final boolean inSplitScreen = mSplitScreen != null && mSplitScreen.isSplitScreenVisible(); 161 final float dividerWidth = mContext.getResources().getDimensionPixelSize( 162 R.dimen.split_divider_bar_width); 163 // We allow splitting if we are already in split-screen or the running task is a standard 164 // task in fullscreen mode. 165 final boolean allowSplit = inSplitScreen 166 || (mSession.runningTaskActType == ACTIVITY_TYPE_STANDARD 167 && mSession.runningTaskWinMode == WINDOWING_MODE_FULLSCREEN); 168 if (allowSplit) { 169 // Already split, allow replacing existing split task 170 final Rect topOrLeftBounds = new Rect(); 171 final Rect bottomOrRightBounds = new Rect(); 172 mSplitScreen.getStageBounds(topOrLeftBounds, bottomOrRightBounds); 173 topOrLeftBounds.intersect(displayRegion); 174 bottomOrRightBounds.intersect(displayRegion); 175 176 if (inLandscape) { 177 final Rect leftHitRegion = new Rect(); 178 final Rect rightHitRegion = new Rect(); 179 180 // If we have existing split regions use those bounds, otherwise split it 50/50 181 if (inSplitScreen) { 182 // The bounds of the existing split will have a divider bar, the hit region 183 // should include that space. Find the center of the divider bar: 184 float centerX = topOrLeftBounds.right + (dividerWidth / 2); 185 // Now set the hit regions using that center. 186 leftHitRegion.set(displayRegion); 187 leftHitRegion.right = (int) centerX; 188 rightHitRegion.set(displayRegion); 189 rightHitRegion.left = (int) centerX; 190 } else { 191 displayRegion.splitVertically(leftHitRegion, rightHitRegion); 192 } 193 194 mTargets.add(new Target(TYPE_SPLIT_LEFT, leftHitRegion, topOrLeftBounds)); 195 mTargets.add(new Target(TYPE_SPLIT_RIGHT, rightHitRegion, bottomOrRightBounds)); 196 197 } else { 198 final Rect topHitRegion = new Rect(); 199 final Rect bottomHitRegion = new Rect(); 200 201 // If we have existing split regions use those bounds, otherwise split it 50/50 202 if (inSplitScreen) { 203 // The bounds of the existing split will have a divider bar, the hit region 204 // should include that space. Find the center of the divider bar: 205 float centerX = topOrLeftBounds.bottom + (dividerWidth / 2); 206 // Now set the hit regions using that center. 207 topHitRegion.set(displayRegion); 208 topHitRegion.bottom = (int) centerX; 209 bottomHitRegion.set(displayRegion); 210 bottomHitRegion.top = (int) centerX; 211 } else { 212 displayRegion.splitHorizontally(topHitRegion, bottomHitRegion); 213 } 214 215 mTargets.add(new Target(TYPE_SPLIT_TOP, topHitRegion, topOrLeftBounds)); 216 mTargets.add(new Target(TYPE_SPLIT_BOTTOM, bottomHitRegion, bottomOrRightBounds)); 217 } 218 } else { 219 // Split-screen not allowed, so only show the fullscreen target 220 mTargets.add(new Target(TYPE_FULLSCREEN, fullscreenHitRegion, fullscreenDrawRegion)); 221 } 222 return mTargets; 223 } 224 225 /** 226 * Returns the target at the given position based on the targets previously calculated. 227 */ 228 @Nullable getTargetAtLocation(int x, int y)229 Target getTargetAtLocation(int x, int y) { 230 if (mDisallowHitRegion.contains(x, y)) { 231 return null; 232 } 233 for (int i = mTargets.size() - 1; i >= 0; i--) { 234 DragAndDropPolicy.Target t = mTargets.get(i); 235 if (t.hitRegion.contains(x, y)) { 236 return t; 237 } 238 } 239 return null; 240 } 241 242 @VisibleForTesting handleDrop(Target target, ClipData data)243 void handleDrop(Target target, ClipData data) { 244 if (target == null || !mTargets.contains(target)) { 245 return; 246 } 247 248 final boolean leftOrTop = target.type == TYPE_SPLIT_TOP || target.type == TYPE_SPLIT_LEFT; 249 250 @SplitPosition int position = SPLIT_POSITION_UNDEFINED; 251 if (target.type != TYPE_FULLSCREEN && mSplitScreen != null) { 252 // Update launch options for the split side we are targeting. 253 position = leftOrTop ? SPLIT_POSITION_TOP_OR_LEFT : SPLIT_POSITION_BOTTOM_OR_RIGHT; 254 // Add some data for logging splitscreen once it is invoked 255 mSplitScreen.onDroppedToSplit(position, mLoggerSessionId); 256 } 257 258 final ClipDescription description = data.getDescription(); 259 final Intent dragData = mSession.dragData; 260 startClipDescription(description, dragData, position); 261 } 262 startClipDescription(ClipDescription description, Intent intent, @SplitPosition int position)263 private void startClipDescription(ClipDescription description, Intent intent, 264 @SplitPosition int position) { 265 final boolean isTask = description.hasMimeType(MIMETYPE_APPLICATION_TASK); 266 final boolean isShortcut = description.hasMimeType(MIMETYPE_APPLICATION_SHORTCUT); 267 final Bundle opts = intent.hasExtra(EXTRA_ACTIVITY_OPTIONS) 268 ? intent.getBundleExtra(EXTRA_ACTIVITY_OPTIONS) : new Bundle(); 269 270 if (isTask) { 271 final int taskId = intent.getIntExtra(EXTRA_TASK_ID, INVALID_TASK_ID); 272 mStarter.startTask(taskId, position, opts); 273 } else if (isShortcut) { 274 final String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME); 275 final String id = intent.getStringExtra(EXTRA_SHORTCUT_ID); 276 final UserHandle user = intent.getParcelableExtra(EXTRA_USER); 277 mStarter.startShortcut(packageName, id, position, opts, user); 278 } else { 279 final PendingIntent launchIntent = intent.getParcelableExtra(EXTRA_PENDING_INTENT); 280 // Put BAL flags to avoid activity start aborted. 281 opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, true); 282 opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION, true); 283 mStarter.startIntent(launchIntent, null /* fillIntent */, position, opts); 284 } 285 } 286 287 /** 288 * Per-drag session data. 289 */ 290 private static class DragSession { 291 private final ActivityTaskManager mActivityTaskManager; 292 private final ClipData mInitialDragData; 293 294 final DisplayLayout displayLayout; 295 Intent dragData; 296 ActivityManager.RunningTaskInfo runningTaskInfo; 297 @WindowConfiguration.WindowingMode 298 int runningTaskWinMode = WINDOWING_MODE_UNDEFINED; 299 @WindowConfiguration.ActivityType 300 int runningTaskActType = ACTIVITY_TYPE_STANDARD; 301 boolean dragItemSupportsSplitscreen; 302 DragSession(ActivityTaskManager activityTaskManager, DisplayLayout dispLayout, ClipData data)303 DragSession(ActivityTaskManager activityTaskManager, 304 DisplayLayout dispLayout, ClipData data) { 305 mActivityTaskManager = activityTaskManager; 306 mInitialDragData = data; 307 displayLayout = dispLayout; 308 } 309 310 /** 311 * Updates the session data based on the current state of the system. 312 */ update()313 void update() { 314 List<ActivityManager.RunningTaskInfo> tasks = 315 mActivityTaskManager.getTasks(1, false /* filterOnlyVisibleRecents */); 316 if (!tasks.isEmpty()) { 317 final ActivityManager.RunningTaskInfo task = tasks.get(0); 318 runningTaskInfo = task; 319 runningTaskWinMode = task.getWindowingMode(); 320 runningTaskActType = task.getActivityType(); 321 } 322 323 final ActivityInfo info = mInitialDragData.getItemAt(0).getActivityInfo(); 324 dragItemSupportsSplitscreen = info == null 325 || ActivityInfo.isResizeableMode(info.resizeMode); 326 dragData = mInitialDragData.getItemAt(0).getIntent(); 327 } 328 } 329 330 /** 331 * Interface for actually committing the task launches. 332 */ 333 public interface Starter { startTask(int taskId, @SplitPosition int position, @Nullable Bundle options)334 void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options); startShortcut(String packageName, String shortcutId, @SplitPosition int position, @Nullable Bundle options, UserHandle user)335 void startShortcut(String packageName, String shortcutId, @SplitPosition int position, 336 @Nullable Bundle options, UserHandle user); startIntent(PendingIntent intent, Intent fillInIntent, @SplitPosition int position, @Nullable Bundle options)337 void startIntent(PendingIntent intent, Intent fillInIntent, @SplitPosition int position, 338 @Nullable Bundle options); enterSplitScreen(int taskId, boolean leftOrTop)339 void enterSplitScreen(int taskId, boolean leftOrTop); 340 341 /** 342 * Exits splitscreen, with an associated exit trigger from the SplitscreenUIChanged proto 343 * for logging. 344 */ exitSplitScreen(int toTopTaskId, int exitTrigger)345 void exitSplitScreen(int toTopTaskId, int exitTrigger); 346 } 347 348 /** 349 * Default implementation of the starter which calls through the system services to launch the 350 * tasks. 351 */ 352 private static class DefaultStarter implements Starter { 353 private final Context mContext; 354 DefaultStarter(Context context)355 public DefaultStarter(Context context) { 356 mContext = context; 357 } 358 359 @Override startTask(int taskId, int position, @Nullable Bundle options)360 public void startTask(int taskId, int position, @Nullable Bundle options) { 361 try { 362 ActivityTaskManager.getService().startActivityFromRecents(taskId, options); 363 } catch (RemoteException e) { 364 Slog.e(TAG, "Failed to launch task", e); 365 } 366 } 367 368 @Override startShortcut(String packageName, String shortcutId, int position, @Nullable Bundle options, UserHandle user)369 public void startShortcut(String packageName, String shortcutId, int position, 370 @Nullable Bundle options, UserHandle user) { 371 try { 372 LauncherApps launcherApps = 373 mContext.getSystemService(LauncherApps.class); 374 launcherApps.startShortcut(packageName, shortcutId, null /* sourceBounds */, 375 options, user); 376 } catch (ActivityNotFoundException e) { 377 Slog.e(TAG, "Failed to launch shortcut", e); 378 } 379 } 380 381 @Override startIntent(PendingIntent intent, @Nullable Intent fillInIntent, int position, @Nullable Bundle options)382 public void startIntent(PendingIntent intent, @Nullable Intent fillInIntent, int position, 383 @Nullable Bundle options) { 384 try { 385 intent.send(mContext, 0, fillInIntent, null, null, null, options); 386 } catch (PendingIntent.CanceledException e) { 387 Slog.e(TAG, "Failed to launch activity", e); 388 } 389 } 390 391 @Override enterSplitScreen(int taskId, boolean leftOrTop)392 public void enterSplitScreen(int taskId, boolean leftOrTop) { 393 throw new UnsupportedOperationException("enterSplitScreen not implemented by starter"); 394 } 395 396 @Override exitSplitScreen(int toTopTaskId, int exitTrigger)397 public void exitSplitScreen(int toTopTaskId, int exitTrigger) { 398 throw new UnsupportedOperationException("exitSplitScreen not implemented by starter"); 399 } 400 } 401 402 /** 403 * Represents a drop target. 404 */ 405 static class Target { 406 static final int TYPE_FULLSCREEN = 0; 407 static final int TYPE_SPLIT_LEFT = 1; 408 static final int TYPE_SPLIT_TOP = 2; 409 static final int TYPE_SPLIT_RIGHT = 3; 410 static final int TYPE_SPLIT_BOTTOM = 4; 411 @IntDef(value = { 412 TYPE_FULLSCREEN, 413 TYPE_SPLIT_LEFT, 414 TYPE_SPLIT_TOP, 415 TYPE_SPLIT_RIGHT, 416 TYPE_SPLIT_BOTTOM 417 }) 418 @Retention(RetentionPolicy.SOURCE) 419 @interface Type{} 420 421 final @Type int type; 422 423 // The actual hit region for this region 424 final Rect hitRegion; 425 // The approximate visual region for where the task will start 426 final Rect drawRegion; 427 Target(@ype int t, Rect hit, Rect draw)428 public Target(@Type int t, Rect hit, Rect draw) { 429 type = t; 430 hitRegion = hit; 431 drawRegion = draw; 432 } 433 434 @Override toString()435 public String toString() { 436 return "Target {hit=" + hitRegion + " draw=" + drawRegion + "}"; 437 } 438 } 439 } 440