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