• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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