• 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.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