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