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.StatusBarManager.DISABLE_NONE;
20 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
21 import static android.content.pm.ActivityInfo.CONFIG_ASSETS_PATHS;
22 import static android.content.pm.ActivityInfo.CONFIG_UI_MODE;
23 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
24 
25 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
26 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
27 import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_BOTTOM;
28 import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_LEFT;
29 import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_RIGHT;
30 import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_TOP;
31 
32 import android.animation.Animator;
33 import android.animation.AnimatorListenerAdapter;
34 import android.animation.ValueAnimator;
35 import android.annotation.SuppressLint;
36 import android.app.ActivityManager;
37 import android.app.StatusBarManager;
38 import android.content.Context;
39 import android.content.res.Configuration;
40 import android.graphics.Color;
41 import android.graphics.Insets;
42 import android.graphics.Rect;
43 import android.graphics.drawable.Drawable;
44 import android.view.DragEvent;
45 import android.view.SurfaceControl;
46 import android.view.WindowInsets;
47 import android.view.WindowInsets.Type;
48 import android.widget.LinearLayout;
49 
50 import com.android.internal.logging.InstanceId;
51 import com.android.internal.protolog.common.ProtoLog;
52 import com.android.launcher3.icons.IconProvider;
53 import com.android.wm.shell.R;
54 import com.android.wm.shell.animation.Interpolators;
55 import com.android.wm.shell.protolog.ShellProtoLogGroup;
56 import com.android.wm.shell.splitscreen.SplitScreenController;
57 
58 import java.util.ArrayList;
59 
60 /**
61  * Coordinates the visible drop targets for the current drag within a single display.
62  */
63 public class DragLayout extends LinearLayout {
64 
65     // While dragging the status bar is hidden.
66     private static final int HIDE_STATUS_BAR_FLAGS = StatusBarManager.DISABLE_NOTIFICATION_ICONS
67             | StatusBarManager.DISABLE_NOTIFICATION_ALERTS
68             | StatusBarManager.DISABLE_CLOCK
69             | StatusBarManager.DISABLE_SYSTEM_INFO;
70 
71     private final DragAndDropPolicy mPolicy;
72     private final SplitScreenController mSplitScreenController;
73     private final IconProvider mIconProvider;
74     private final StatusBarManager mStatusBarManager;
75     private final Configuration mLastConfiguration = new Configuration();
76 
77     private DragAndDropPolicy.Target mCurrentTarget = null;
78     private DropZoneView mDropZoneView1;
79     private DropZoneView mDropZoneView2;
80 
81     private int mDisplayMargin;
82     private int mDividerSize;
83     private Insets mInsets = Insets.NONE;
84 
85     private boolean mIsShowing;
86     private boolean mHasDropped;
87     private DragSession mSession;
88 
89     @SuppressLint("WrongConstant")
DragLayout(Context context, SplitScreenController splitScreenController, IconProvider iconProvider)90     public DragLayout(Context context, SplitScreenController splitScreenController,
91             IconProvider iconProvider) {
92         super(context);
93         mSplitScreenController = splitScreenController;
94         mIconProvider = iconProvider;
95         mPolicy = new DragAndDropPolicy(context, splitScreenController);
96         mStatusBarManager = context.getSystemService(StatusBarManager.class);
97         mLastConfiguration.setTo(context.getResources().getConfiguration());
98 
99         mDisplayMargin = context.getResources().getDimensionPixelSize(
100                 R.dimen.drop_layout_display_margin);
101         mDividerSize = context.getResources().getDimensionPixelSize(
102                 R.dimen.split_divider_bar_width);
103 
104         // Always use LTR because we assume dropZoneView1 is on the left and 2 is on the right when
105         // showing the highlight.
106         setLayoutDirection(LAYOUT_DIRECTION_LTR);
107         mDropZoneView1 = new DropZoneView(context);
108         mDropZoneView2 = new DropZoneView(context);
109         addView(mDropZoneView1, new LinearLayout.LayoutParams(MATCH_PARENT,
110                 MATCH_PARENT));
111         addView(mDropZoneView2, new LinearLayout.LayoutParams(MATCH_PARENT,
112                 MATCH_PARENT));
113         ((LayoutParams) mDropZoneView1.getLayoutParams()).weight = 1;
114         ((LayoutParams) mDropZoneView2.getLayoutParams()).weight = 1;
115         int orientation = getResources().getConfiguration().orientation;
116         setOrientation(orientation == Configuration.ORIENTATION_LANDSCAPE
117                 ? LinearLayout.HORIZONTAL
118                 : LinearLayout.VERTICAL);
119         updateContainerMargins(getResources().getConfiguration().orientation);
120     }
121 
122     @Override
onApplyWindowInsets(WindowInsets insets)123     public WindowInsets onApplyWindowInsets(WindowInsets insets) {
124         mInsets = insets.getInsets(Type.tappableElement() | Type.displayCutout());
125         recomputeDropTargets();
126 
127         final int orientation = getResources().getConfiguration().orientation;
128         if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
129             mDropZoneView1.setBottomInset(mInsets.bottom);
130             mDropZoneView2.setBottomInset(mInsets.bottom);
131         } else if (orientation == Configuration.ORIENTATION_PORTRAIT) {
132             mDropZoneView1.setBottomInset(0);
133             mDropZoneView2.setBottomInset(mInsets.bottom);
134         }
135         return super.onApplyWindowInsets(insets);
136     }
137 
onConfigChanged(Configuration newConfig)138     public void onConfigChanged(Configuration newConfig) {
139         if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE
140                 && getOrientation() != HORIZONTAL) {
141             setOrientation(LinearLayout.HORIZONTAL);
142             updateContainerMargins(newConfig.orientation);
143         } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT
144                 && getOrientation() != VERTICAL) {
145             setOrientation(LinearLayout.VERTICAL);
146             updateContainerMargins(newConfig.orientation);
147         }
148 
149         final int diff = newConfig.diff(mLastConfiguration);
150         final boolean themeChanged = (diff & CONFIG_ASSETS_PATHS) != 0
151                 || (diff & CONFIG_UI_MODE) != 0;
152         if (themeChanged) {
153             mDropZoneView1.onThemeChange();
154             mDropZoneView2.onThemeChange();
155         }
156         mLastConfiguration.setTo(newConfig);
157     }
158 
updateContainerMarginsForSingleTask()159     private void updateContainerMarginsForSingleTask() {
160         mDropZoneView1.setContainerMargin(
161                 mDisplayMargin, mDisplayMargin, mDisplayMargin, mDisplayMargin);
162         mDropZoneView2.setContainerMargin(0, 0, 0, 0);
163     }
164 
updateContainerMargins(int orientation)165     private void updateContainerMargins(int orientation) {
166         final float halfMargin = mDisplayMargin / 2f;
167         if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
168             mDropZoneView1.setContainerMargin(
169                     mDisplayMargin, mDisplayMargin, halfMargin, mDisplayMargin);
170             mDropZoneView2.setContainerMargin(
171                     halfMargin, mDisplayMargin, mDisplayMargin, mDisplayMargin);
172         } else if (orientation == Configuration.ORIENTATION_PORTRAIT) {
173             mDropZoneView1.setContainerMargin(
174                     mDisplayMargin, mDisplayMargin, mDisplayMargin, halfMargin);
175             mDropZoneView2.setContainerMargin(
176                     mDisplayMargin, halfMargin, mDisplayMargin, mDisplayMargin);
177         }
178     }
179 
hasDropped()180     public boolean hasDropped() {
181         return mHasDropped;
182     }
183 
184     /**
185      * Called when a new drag is started.
186      */
prepare(DragSession session, InstanceId loggerSessionId)187     public void prepare(DragSession session, InstanceId loggerSessionId) {
188         mPolicy.start(session, loggerSessionId);
189         mSession = session;
190         mHasDropped = false;
191         mCurrentTarget = null;
192 
193         boolean alreadyInSplit = mSplitScreenController != null
194                 && mSplitScreenController.isSplitScreenVisible();
195         if (!alreadyInSplit) {
196             ActivityManager.RunningTaskInfo taskInfo1 = mSession.runningTaskInfo;
197             if (taskInfo1 != null) {
198                 final int activityType = taskInfo1.getActivityType();
199                 if (activityType == ACTIVITY_TYPE_STANDARD) {
200                     Drawable icon1 = mIconProvider.getIcon(taskInfo1.topActivityInfo);
201                     int bgColor1 = getResizingBackgroundColor(taskInfo1);
202                     mDropZoneView1.setAppInfo(bgColor1, icon1);
203                     mDropZoneView2.setAppInfo(bgColor1, icon1);
204                     updateDropZoneSizes(null, null); // passing null splits the views evenly
205                 } else {
206                     // We use the first drop zone to show the fullscreen highlight, and don't need
207                     // to set additional info
208                     mDropZoneView1.setForceIgnoreBottomMargin(true);
209                     updateDropZoneSizesForSingleTask();
210                     updateContainerMarginsForSingleTask();
211                 }
212             }
213         } else {
214             // We're already in split so get taskInfo from the controller to populate icon / color.
215             ActivityManager.RunningTaskInfo topOrLeftTask =
216                     mSplitScreenController.getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT);
217             ActivityManager.RunningTaskInfo bottomOrRightTask =
218                     mSplitScreenController.getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT);
219             if (topOrLeftTask != null && bottomOrRightTask != null) {
220                 Drawable topOrLeftIcon = mIconProvider.getIcon(topOrLeftTask.topActivityInfo);
221                 int topOrLeftColor = getResizingBackgroundColor(topOrLeftTask);
222                 Drawable bottomOrRightIcon = mIconProvider.getIcon(
223                         bottomOrRightTask.topActivityInfo);
224                 int bottomOrRightColor = getResizingBackgroundColor(bottomOrRightTask);
225                 mDropZoneView1.setAppInfo(topOrLeftColor, topOrLeftIcon);
226                 mDropZoneView2.setAppInfo(bottomOrRightColor, bottomOrRightIcon);
227             }
228 
229             // Update the dropzones to match existing split sizes
230             Rect topOrLeftBounds = new Rect();
231             Rect bottomOrRightBounds = new Rect();
232             mSplitScreenController.getStageBounds(topOrLeftBounds, bottomOrRightBounds);
233             updateDropZoneSizes(topOrLeftBounds, bottomOrRightBounds);
234         }
235     }
236 
updateDropZoneSizesForSingleTask()237     private void updateDropZoneSizesForSingleTask() {
238         final LinearLayout.LayoutParams dropZoneView1 =
239                 (LayoutParams) mDropZoneView1.getLayoutParams();
240         final LinearLayout.LayoutParams dropZoneView2 =
241                 (LayoutParams) mDropZoneView2.getLayoutParams();
242         dropZoneView1.width = MATCH_PARENT;
243         dropZoneView1.height = MATCH_PARENT;
244         dropZoneView2.width = 0;
245         dropZoneView2.height = 0;
246         dropZoneView1.weight = 1;
247         dropZoneView2.weight = 0;
248         mDropZoneView1.setLayoutParams(dropZoneView1);
249         mDropZoneView2.setLayoutParams(dropZoneView2);
250     }
251 
252     /**
253      * Sets the size of the two drop zones based on the provided bounds. The divider sits between
254      * the views and its size is included in the calculations.
255      *
256      * @param bounds1 bounds to apply to the first dropzone view, null if split in half.
257      * @param bounds2 bounds to apply to the second dropzone view, null if split in half.
258      */
updateDropZoneSizes(Rect bounds1, Rect bounds2)259     private void updateDropZoneSizes(Rect bounds1, Rect bounds2) {
260         final int orientation = getResources().getConfiguration().orientation;
261         final boolean isPortrait = orientation == Configuration.ORIENTATION_PORTRAIT;
262         final int halfDivider = mDividerSize / 2;
263         final LinearLayout.LayoutParams dropZoneView1 =
264                 (LayoutParams) mDropZoneView1.getLayoutParams();
265         final LinearLayout.LayoutParams dropZoneView2 =
266                 (LayoutParams) mDropZoneView2.getLayoutParams();
267         if (isPortrait) {
268             dropZoneView1.width = MATCH_PARENT;
269             dropZoneView2.width = MATCH_PARENT;
270             dropZoneView1.height = bounds1 != null ? bounds1.height() + halfDivider : MATCH_PARENT;
271             dropZoneView2.height = bounds2 != null ? bounds2.height() + halfDivider : MATCH_PARENT;
272         } else {
273             dropZoneView1.width = bounds1 != null ? bounds1.width() + halfDivider : MATCH_PARENT;
274             dropZoneView2.width = bounds2 != null ? bounds2.width() + halfDivider : MATCH_PARENT;
275             dropZoneView1.height = MATCH_PARENT;
276             dropZoneView2.height = MATCH_PARENT;
277         }
278         dropZoneView1.weight = bounds1 != null ? 0 : 1;
279         dropZoneView2.weight = bounds2 != null ? 0 : 1;
280         mDropZoneView1.setLayoutParams(dropZoneView1);
281         mDropZoneView2.setLayoutParams(dropZoneView2);
282     }
283 
show()284     public void show() {
285         mIsShowing = true;
286         recomputeDropTargets();
287     }
288 
289     /**
290      * Recalculates the drop targets based on the current policy.
291      */
recomputeDropTargets()292     private void recomputeDropTargets() {
293         if (!mIsShowing) {
294             return;
295         }
296         final ArrayList<DragAndDropPolicy.Target> targets = mPolicy.getTargets(mInsets);
297         for (int i = 0; i < targets.size(); i++) {
298             final DragAndDropPolicy.Target target = targets.get(i);
299             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Add target: %s", target);
300             // Inset the draw region by a little bit
301             target.drawRegion.inset(mDisplayMargin, mDisplayMargin);
302         }
303     }
304 
305     /**
306      * Updates the visible drop target as the user drags.
307      */
update(DragEvent event)308     public void update(DragEvent event) {
309         if (mHasDropped) {
310             return;
311         }
312         // Find containing region, if the same as mCurrentRegion, then skip, otherwise, animate the
313         // visibility of the current region
314         DragAndDropPolicy.Target target = mPolicy.getTargetAtLocation(
315                 (int) event.getX(), (int) event.getY());
316         if (mCurrentTarget != target) {
317             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Current target: %s", target);
318             if (target == null) {
319                 // Animating to no target
320                 animateSplitContainers(false, null /* animCompleteCallback */);
321             } else if (mCurrentTarget == null) {
322                 if (mPolicy.getNumTargets() == 1) {
323                     animateFullscreenContainer(true);
324                 } else {
325                     animateSplitContainers(true, null /* animCompleteCallback */);
326                     animateHighlight(target);
327                 }
328             } else if (mCurrentTarget.type != target.type) {
329                 // Switching between targets
330                 mDropZoneView1.animateSwitch();
331                 mDropZoneView2.animateSwitch();
332                 // Announce for accessibility.
333                 switch (target.type) {
334                     case TYPE_SPLIT_LEFT:
335                         mDropZoneView1.announceForAccessibility(
336                                 mContext.getString(R.string.accessibility_split_left));
337                         break;
338                     case TYPE_SPLIT_RIGHT:
339                         mDropZoneView2.announceForAccessibility(
340                                 mContext.getString(R.string.accessibility_split_right));
341                         break;
342                     case TYPE_SPLIT_TOP:
343                         mDropZoneView1.announceForAccessibility(
344                                 mContext.getString(R.string.accessibility_split_top));
345                         break;
346                     case TYPE_SPLIT_BOTTOM:
347                         mDropZoneView2.announceForAccessibility(
348                                 mContext.getString(R.string.accessibility_split_bottom));
349                         break;
350                 }
351             }
352             mCurrentTarget = target;
353         }
354     }
355 
356     /**
357      * Hides the drag layout and animates out the visible drop targets.
358      */
hide(DragEvent event, Runnable hideCompleteCallback)359     public void hide(DragEvent event, Runnable hideCompleteCallback) {
360         mIsShowing = false;
361         animateSplitContainers(false, () -> {
362             if (hideCompleteCallback != null) {
363                 hideCompleteCallback.run();
364             }
365             switch (event.getAction()) {
366                 case DragEvent.ACTION_DROP:
367                 case DragEvent.ACTION_DRAG_ENDED:
368                     mSession = null;
369             }
370         });
371         // Reset the state if we previously force-ignore the bottom margin
372         mDropZoneView1.setForceIgnoreBottomMargin(false);
373         mDropZoneView2.setForceIgnoreBottomMargin(false);
374         updateContainerMargins(getResources().getConfiguration().orientation);
375         mCurrentTarget = null;
376     }
377 
378     /**
379      * Handles the drop onto a target and animates out the visible drop targets.
380      */
drop(DragEvent event, SurfaceControl dragSurface, Runnable dropCompleteCallback)381     public boolean drop(DragEvent event, SurfaceControl dragSurface,
382             Runnable dropCompleteCallback) {
383         final boolean handledDrop = mCurrentTarget != null;
384         mHasDropped = true;
385 
386         // Process the drop
387         mPolicy.handleDrop(mCurrentTarget, event.getClipData());
388 
389         // Start animating the drop UI out with the drag surface
390         hide(event, dropCompleteCallback);
391         if (handledDrop) {
392             hideDragSurface(dragSurface);
393         }
394         return handledDrop;
395     }
396 
hideDragSurface(SurfaceControl dragSurface)397     private void hideDragSurface(SurfaceControl dragSurface) {
398         final SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
399         final ValueAnimator dragSurfaceAnimator = ValueAnimator.ofFloat(0f, 1f);
400         // Currently the splash icon animation runs with the default ValueAnimator duration of
401         // 300ms
402         dragSurfaceAnimator.setDuration(300);
403         dragSurfaceAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
404         dragSurfaceAnimator.addUpdateListener(animation -> {
405             float t = animation.getAnimatedFraction();
406             float alpha = 1f - t;
407             // TODO: Scale the drag surface as well once we make all the source surfaces
408             //       consistent
409             tx.setAlpha(dragSurface, alpha);
410             tx.apply();
411         });
412         dragSurfaceAnimator.addListener(new AnimatorListenerAdapter() {
413             private boolean mCanceled = false;
414 
415             @Override
416             public void onAnimationCancel(Animator animation) {
417                 cleanUpSurface();
418                 mCanceled = true;
419             }
420 
421             @Override
422             public void onAnimationEnd(Animator animation) {
423                 if (mCanceled) {
424                     // Already handled above
425                     return;
426                 }
427                 cleanUpSurface();
428             }
429 
430             private void cleanUpSurface() {
431                 // Clean up the drag surface
432                 tx.remove(dragSurface);
433                 tx.apply();
434             }
435         });
436         dragSurfaceAnimator.start();
437     }
438 
animateFullscreenContainer(boolean visible)439     private void animateFullscreenContainer(boolean visible) {
440         mStatusBarManager.disable(visible
441                 ? HIDE_STATUS_BAR_FLAGS
442                 : DISABLE_NONE);
443         // We're only using the first drop zone if there is one fullscreen target
444         mDropZoneView1.setShowingMargin(visible);
445         mDropZoneView1.setShowingHighlight(visible);
446     }
447 
animateSplitContainers(boolean visible, Runnable animCompleteCallback)448     private void animateSplitContainers(boolean visible, Runnable animCompleteCallback) {
449         mStatusBarManager.disable(visible
450                 ? HIDE_STATUS_BAR_FLAGS
451                 : DISABLE_NONE);
452         mDropZoneView1.setShowingMargin(visible);
453         mDropZoneView2.setShowingMargin(visible);
454         Animator animator = mDropZoneView1.getAnimator();
455         if (animCompleteCallback != null) {
456             if (animator != null) {
457                 animator.addListener(new AnimatorListenerAdapter() {
458                     @Override
459                     public void onAnimationEnd(Animator animation) {
460                         animCompleteCallback.run();
461                     }
462                 });
463             } else {
464                 // If there's no animator the animation is done so run immediately
465                 animCompleteCallback.run();
466             }
467         }
468     }
469 
animateHighlight(DragAndDropPolicy.Target target)470     private void animateHighlight(DragAndDropPolicy.Target target) {
471         if (target.type == TYPE_SPLIT_LEFT || target.type == TYPE_SPLIT_TOP) {
472             mDropZoneView1.setShowingHighlight(true);
473             mDropZoneView2.setShowingHighlight(false);
474         } else if (target.type == TYPE_SPLIT_RIGHT || target.type == TYPE_SPLIT_BOTTOM) {
475             mDropZoneView1.setShowingHighlight(false);
476             mDropZoneView2.setShowingHighlight(true);
477         }
478     }
479 
getResizingBackgroundColor(ActivityManager.RunningTaskInfo taskInfo)480     private static int getResizingBackgroundColor(ActivityManager.RunningTaskInfo taskInfo) {
481         final int taskBgColor = taskInfo.taskDescription.getBackgroundColor();
482         return Color.valueOf(taskBgColor == -1 ? Color.WHITE : taskBgColor).toArgb();
483     }
484 }
485