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