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