1 /*
2  * Copyright (C) 2015 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.server.wm;
18 
19 import static android.app.ActivityManager.DOCKED_STACK_CREATE_MODE_BOTTOM_OR_RIGHT;
20 import static android.app.ActivityManager.DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT;
21 import static android.app.ActivityManager.RESIZE_MODE_USER;
22 import static android.app.ActivityManager.RESIZE_MODE_USER_FORCED;
23 import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID;
24 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
25 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
26 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ORIENTATION;
27 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_TASK_POSITIONING;
28 import static com.android.server.wm.WindowManagerDebugConfig.SHOW_TRANSACTIONS;
29 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
30 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
31 import static com.android.server.wm.WindowManagerService.dipToPixel;
32 import static com.android.server.wm.DragResizeMode.DRAG_RESIZE_MODE_FREEFORM;
33 import static com.android.server.wm.WindowState.MINIMUM_VISIBLE_HEIGHT_IN_DP;
34 import static com.android.server.wm.WindowState.MINIMUM_VISIBLE_WIDTH_IN_DP;
35 
36 import android.annotation.IntDef;
37 import android.graphics.Point;
38 import android.graphics.Rect;
39 import android.os.Looper;
40 import android.os.Process;
41 import android.os.RemoteException;
42 import android.os.Trace;
43 import android.util.DisplayMetrics;
44 import android.util.Slog;
45 import android.view.BatchedInputEventReceiver;
46 import android.view.Choreographer;
47 import android.view.Display;
48 import android.view.DisplayInfo;
49 import android.view.InputChannel;
50 import android.view.InputDevice;
51 import android.view.InputEvent;
52 import android.view.MotionEvent;
53 import android.view.SurfaceControl;
54 import android.view.WindowManager;
55 
56 import com.android.server.input.InputApplicationHandle;
57 import com.android.server.input.InputWindowHandle;
58 import com.android.server.wm.WindowManagerService.H;
59 
60 import java.lang.annotation.Retention;
61 import java.lang.annotation.RetentionPolicy;
62 
63 class TaskPositioner implements DimLayer.DimLayerUser {
64     private static final String TAG_LOCAL = "TaskPositioner";
65     private static final String TAG = TAG_WITH_CLASS_NAME ? TAG_LOCAL : TAG_WM;
66 
67     // The margin the pointer position has to be within the side of the screen to be
68     // considered at the side of the screen.
69     static final int SIDE_MARGIN_DIP = 100;
70 
71     @IntDef(flag = true,
72             value = {
73                     CTRL_NONE,
74                     CTRL_LEFT,
75                     CTRL_RIGHT,
76                     CTRL_TOP,
77                     CTRL_BOTTOM
78             })
79     @Retention(RetentionPolicy.SOURCE)
80     @interface CtrlType {}
81 
82     private static final int CTRL_NONE   = 0x0;
83     private static final int CTRL_LEFT   = 0x1;
84     private static final int CTRL_RIGHT  = 0x2;
85     private static final int CTRL_TOP    = 0x4;
86     private static final int CTRL_BOTTOM = 0x8;
87 
88     public static final float RESIZING_HINT_ALPHA = 0.5f;
89 
90     public static final int RESIZING_HINT_DURATION_MS = 0;
91 
92     private final WindowManagerService mService;
93     private WindowPositionerEventReceiver mInputEventReceiver;
94     private Display mDisplay;
95     private final DisplayMetrics mDisplayMetrics = new DisplayMetrics();
96     private DimLayer mDimLayer;
97     @CtrlType
98     private int mCurrentDimSide;
99     private Rect mTmpRect = new Rect();
100     private int mSideMargin;
101     private int mMinVisibleWidth;
102     private int mMinVisibleHeight;
103 
104     private Task mTask;
105     private boolean mResizing;
106     private final Rect mWindowOriginalBounds = new Rect();
107     private final Rect mWindowDragBounds = new Rect();
108     private float mStartDragX;
109     private float mStartDragY;
110     @CtrlType
111     private int mCtrlType = CTRL_NONE;
112     private boolean mDragEnded = false;
113 
114     InputChannel mServerChannel;
115     InputChannel mClientChannel;
116     InputApplicationHandle mDragApplicationHandle;
117     InputWindowHandle mDragWindowHandle;
118 
119     private final class WindowPositionerEventReceiver extends BatchedInputEventReceiver {
WindowPositionerEventReceiver( InputChannel inputChannel, Looper looper, Choreographer choreographer)120         public WindowPositionerEventReceiver(
121                 InputChannel inputChannel, Looper looper, Choreographer choreographer) {
122             super(inputChannel, looper, choreographer);
123         }
124 
125         @Override
onInputEvent(InputEvent event)126         public void onInputEvent(InputEvent event) {
127             if (!(event instanceof MotionEvent)
128                     || (event.getSource() & InputDevice.SOURCE_CLASS_POINTER) == 0) {
129                 return;
130             }
131             final MotionEvent motionEvent = (MotionEvent) event;
132             boolean handled = false;
133 
134             try {
135                 if (mDragEnded) {
136                     // The drag has ended but the clean-up message has not been processed by
137                     // window manager. Drop events that occur after this until window manager
138                     // has a chance to clean-up the input handle.
139                     handled = true;
140                     return;
141                 }
142 
143                 final float newX = motionEvent.getRawX();
144                 final float newY = motionEvent.getRawY();
145 
146                 switch (motionEvent.getAction()) {
147                     case MotionEvent.ACTION_DOWN: {
148                         if (DEBUG_TASK_POSITIONING) {
149                             Slog.w(TAG, "ACTION_DOWN @ {" + newX + ", " + newY + "}");
150                         }
151                     } break;
152 
153                     case MotionEvent.ACTION_MOVE: {
154                         if (DEBUG_TASK_POSITIONING){
155                             Slog.w(TAG, "ACTION_MOVE @ {" + newX + ", " + newY + "}");
156                         }
157                         synchronized (mService.mWindowMap) {
158                             mDragEnded = notifyMoveLocked(newX, newY);
159                             mTask.getDimBounds(mTmpRect);
160                         }
161                         if (!mTmpRect.equals(mWindowDragBounds)) {
162                             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER,
163                                     "wm.TaskPositioner.resizeTask");
164                             try {
165                                 mService.mActivityManager.resizeTask(
166                                         mTask.mTaskId, mWindowDragBounds, RESIZE_MODE_USER);
167                             } catch (RemoteException e) {
168                             }
169                             Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
170                         }
171                     } break;
172 
173                     case MotionEvent.ACTION_UP: {
174                         if (DEBUG_TASK_POSITIONING) {
175                             Slog.w(TAG, "ACTION_UP @ {" + newX + ", " + newY + "}");
176                         }
177                         mDragEnded = true;
178                     } break;
179 
180                     case MotionEvent.ACTION_CANCEL: {
181                         if (DEBUG_TASK_POSITIONING) {
182                             Slog.w(TAG, "ACTION_CANCEL @ {" + newX + ", " + newY + "}");
183                         }
184                         mDragEnded = true;
185                     } break;
186                 }
187 
188                 if (mDragEnded) {
189                     final boolean wasResizing = mResizing;
190                     synchronized (mService.mWindowMap) {
191                         endDragLocked();
192                     }
193                     try {
194                         if (wasResizing) {
195                             // We were using fullscreen surface during resizing. Request
196                             // resizeTask() one last time to restore surface to window size.
197                             mService.mActivityManager.resizeTask(
198                                     mTask.mTaskId, mWindowDragBounds, RESIZE_MODE_USER_FORCED);
199                         }
200 
201                         if (mCurrentDimSide != CTRL_NONE) {
202                             final int createMode = mCurrentDimSide == CTRL_LEFT
203                                     ? DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT
204                                     : DOCKED_STACK_CREATE_MODE_BOTTOM_OR_RIGHT;
205                             mService.mActivityManager.moveTaskToDockedStack(
206                                     mTask.mTaskId, createMode, true /*toTop*/, true /* animate */,
207                                     null /* initialBounds */, false /* moveHomeStackFront */);
208                         }
209                     } catch(RemoteException e) {}
210 
211                     // Post back to WM to handle clean-ups. We still need the input
212                     // event handler for the last finishInputEvent()!
213                     mService.mH.sendEmptyMessage(H.FINISH_TASK_POSITIONING);
214                 }
215                 handled = true;
216             } catch (Exception e) {
217                 Slog.e(TAG, "Exception caught by drag handleMotion", e);
218             } finally {
219                 finishInputEvent(event, handled);
220             }
221         }
222     }
223 
TaskPositioner(WindowManagerService service)224     TaskPositioner(WindowManagerService service) {
225         mService = service;
226     }
227 
228     /**
229      * @param display The Display that the window being dragged is on.
230      */
register(Display display)231     void register(Display display) {
232         if (DEBUG_TASK_POSITIONING) {
233             Slog.d(TAG, "Registering task positioner");
234         }
235 
236         if (mClientChannel != null) {
237             Slog.e(TAG, "Task positioner already registered");
238             return;
239         }
240 
241         mDisplay = display;
242         mDisplay.getMetrics(mDisplayMetrics);
243         final InputChannel[] channels = InputChannel.openInputChannelPair(TAG);
244         mServerChannel = channels[0];
245         mClientChannel = channels[1];
246         mService.mInputManager.registerInputChannel(mServerChannel, null);
247 
248         mInputEventReceiver = new WindowPositionerEventReceiver(
249                 mClientChannel, mService.mH.getLooper(), mService.mChoreographer);
250 
251         mDragApplicationHandle = new InputApplicationHandle(null);
252         mDragApplicationHandle.name = TAG;
253         mDragApplicationHandle.dispatchingTimeoutNanos =
254                 WindowManagerService.DEFAULT_INPUT_DISPATCHING_TIMEOUT_NANOS;
255 
256         mDragWindowHandle = new InputWindowHandle(mDragApplicationHandle, null,
257                 mDisplay.getDisplayId());
258         mDragWindowHandle.name = TAG;
259         mDragWindowHandle.inputChannel = mServerChannel;
260         mDragWindowHandle.layer = mService.getDragLayerLocked();
261         mDragWindowHandle.layoutParamsFlags = 0;
262         mDragWindowHandle.layoutParamsType = WindowManager.LayoutParams.TYPE_DRAG;
263         mDragWindowHandle.dispatchingTimeoutNanos =
264                 WindowManagerService.DEFAULT_INPUT_DISPATCHING_TIMEOUT_NANOS;
265         mDragWindowHandle.visible = true;
266         mDragWindowHandle.canReceiveKeys = false;
267         mDragWindowHandle.hasFocus = true;
268         mDragWindowHandle.hasWallpaper = false;
269         mDragWindowHandle.paused = false;
270         mDragWindowHandle.ownerPid = Process.myPid();
271         mDragWindowHandle.ownerUid = Process.myUid();
272         mDragWindowHandle.inputFeatures = 0;
273         mDragWindowHandle.scaleFactor = 1.0f;
274 
275         // The drag window cannot receive new touches.
276         mDragWindowHandle.touchableRegion.setEmpty();
277 
278         // The drag window covers the entire display
279         mDragWindowHandle.frameLeft = 0;
280         mDragWindowHandle.frameTop = 0;
281         final Point p = new Point();
282         mDisplay.getRealSize(p);
283         mDragWindowHandle.frameRight = p.x;
284         mDragWindowHandle.frameBottom = p.y;
285 
286         // Pause rotations before a drag.
287         if (DEBUG_ORIENTATION) {
288             Slog.d(TAG, "Pausing rotation during re-position");
289         }
290         mService.pauseRotationLocked();
291 
292         mDimLayer = new DimLayer(mService, this, mDisplay.getDisplayId(), TAG_LOCAL);
293         mSideMargin = dipToPixel(SIDE_MARGIN_DIP, mDisplayMetrics);
294         mMinVisibleWidth = dipToPixel(MINIMUM_VISIBLE_WIDTH_IN_DP, mDisplayMetrics);
295         mMinVisibleHeight = dipToPixel(MINIMUM_VISIBLE_HEIGHT_IN_DP, mDisplayMetrics);
296 
297         mDragEnded = false;
298     }
299 
unregister()300     void unregister() {
301         if (DEBUG_TASK_POSITIONING) {
302             Slog.d(TAG, "Unregistering task positioner");
303         }
304 
305         if (mClientChannel == null) {
306             Slog.e(TAG, "Task positioner not registered");
307             return;
308         }
309 
310         mService.mInputManager.unregisterInputChannel(mServerChannel);
311 
312         mInputEventReceiver.dispose();
313         mInputEventReceiver = null;
314         mClientChannel.dispose();
315         mServerChannel.dispose();
316         mClientChannel = null;
317         mServerChannel = null;
318 
319         mDragWindowHandle = null;
320         mDragApplicationHandle = null;
321         mDisplay = null;
322 
323         if (mDimLayer != null) {
324             mDimLayer.destroySurface();
325             mDimLayer = null;
326         }
327         mCurrentDimSide = CTRL_NONE;
328         mDragEnded = true;
329 
330         // Resume rotations after a drag.
331         if (DEBUG_ORIENTATION) {
332             Slog.d(TAG, "Resuming rotation after re-position");
333         }
334         mService.resumeRotationLocked();
335     }
336 
startDragLocked(WindowState win, boolean resize, float startX, float startY)337     void startDragLocked(WindowState win, boolean resize, float startX, float startY) {
338         if (DEBUG_TASK_POSITIONING) {
339             Slog.d(TAG, "startDragLocked: win=" + win + ", resize=" + resize
340                 + ", {" + startX + ", " + startY + "}");
341         }
342         mCtrlType = CTRL_NONE;
343         mTask = win.getTask();
344         mStartDragX = startX;
345         mStartDragY = startY;
346 
347         if (mTask.isDockedInEffect()) {
348             // If this is a docked task or if task size is affected by docked stack changing size,
349             // we can only be here if the task is not resizeable and we're handling a two-finger
350             // scrolling. Use the original task bounds to position the task, the dim bounds
351             // is cropped and doesn't move.
352             mTask.getBounds(mTmpRect);
353         } else {
354             // Use the dim bounds, not the original task bounds. The cursor
355             // movement should be calculated relative to the visible bounds.
356             // Also, use the dim bounds of the task which accounts for
357             // multiple app windows. Don't use any bounds from win itself as it
358             // may not be the same size as the task.
359             mTask.getDimBounds(mTmpRect);
360         }
361 
362         if (resize) {
363             if (startX < mTmpRect.left) {
364                 mCtrlType |= CTRL_LEFT;
365             }
366             if (startX > mTmpRect.right) {
367                 mCtrlType |= CTRL_RIGHT;
368             }
369             if (startY < mTmpRect.top) {
370                 mCtrlType |= CTRL_TOP;
371             }
372             if (startY > mTmpRect.bottom) {
373                 mCtrlType |= CTRL_BOTTOM;
374             }
375             mResizing = true;
376         }
377 
378         mWindowOriginalBounds.set(mTmpRect);
379     }
380 
endDragLocked()381     private void endDragLocked() {
382         mResizing = false;
383         mTask.setDragResizing(false, DRAG_RESIZE_MODE_FREEFORM);
384     }
385 
386     /** Returns true if the move operation should be ended. */
notifyMoveLocked(float x, float y)387     private boolean notifyMoveLocked(float x, float y) {
388         if (DEBUG_TASK_POSITIONING) {
389             Slog.d(TAG, "notifyMoveLocked: {" + x + "," + y + "}");
390         }
391 
392         if (mCtrlType != CTRL_NONE) {
393             // This is a resizing operation.
394             final int deltaX = Math.round(x - mStartDragX);
395             final int deltaY = Math.round(y - mStartDragY);
396             int left = mWindowOriginalBounds.left;
397             int top = mWindowOriginalBounds.top;
398             int right = mWindowOriginalBounds.right;
399             int bottom = mWindowOriginalBounds.bottom;
400             if ((mCtrlType & CTRL_LEFT) != 0) {
401                 left = Math.min(left + deltaX, right - mMinVisibleWidth);
402             }
403             if ((mCtrlType & CTRL_TOP) != 0) {
404                 top = Math.min(top + deltaY, bottom - mMinVisibleHeight);
405             }
406             if ((mCtrlType & CTRL_RIGHT) != 0) {
407                 right = Math.max(left + mMinVisibleWidth, right + deltaX);
408             }
409             if ((mCtrlType & CTRL_BOTTOM) != 0) {
410                 bottom = Math.max(top + mMinVisibleHeight, bottom + deltaY);
411             }
412             mWindowDragBounds.set(left, top, right, bottom);
413             mTask.setDragResizing(true, DRAG_RESIZE_MODE_FREEFORM);
414             return false;
415         }
416 
417         // This is a moving operation.
418         mTask.mStack.getDimBounds(mTmpRect);
419 
420         // If this is a non-resizeable task put into side-by-side mode, we are
421         // handling a two-finger scrolling action. No need to shrink the bounds.
422         if (!mTask.isDockedInEffect()) {
423             mTmpRect.inset(mMinVisibleWidth, mMinVisibleHeight);
424         }
425 
426         boolean dragEnded = false;
427         final int nX = (int) x;
428         final int nY = (int) y;
429         if (!mTmpRect.contains(nX, nY)) {
430             // We end the moving operation if position is outside the stack bounds.
431             // In this case we need to clamp the position to stack bounds and calculate
432             // the final window drag bounds.
433             x = Math.min(Math.max(x, mTmpRect.left), mTmpRect.right);
434             y = Math.min(Math.max(y, mTmpRect.top), mTmpRect.bottom);
435             dragEnded = true;
436         }
437 
438         updateWindowDragBounds(nX, nY);
439         updateDimLayerVisibility(nX);
440         return dragEnded;
441     }
442 
updateWindowDragBounds(int x, int y)443     private void updateWindowDragBounds(int x, int y) {
444         mWindowDragBounds.set(mWindowOriginalBounds);
445         if (mTask.isDockedInEffect()) {
446             // Offset the bounds without clamp, the bounds will be shifted later
447             // by window manager before applying the scrolling.
448             if (mService.mCurConfiguration.orientation == ORIENTATION_LANDSCAPE) {
449                 mWindowDragBounds.offset(Math.round(x - mStartDragX), 0);
450             } else {
451                 mWindowDragBounds.offset(0, Math.round(y - mStartDragY));
452             }
453         } else {
454             mWindowDragBounds.offset(Math.round(x - mStartDragX), Math.round(y - mStartDragY));
455         }
456         if (DEBUG_TASK_POSITIONING) Slog.d(TAG,
457                 "updateWindowDragBounds: " + mWindowDragBounds);
458     }
459 
updateDimLayerVisibility(int x)460     private void updateDimLayerVisibility(int x) {
461         @CtrlType
462         int dimSide = getDimSide(x);
463         if (dimSide == mCurrentDimSide) {
464             return;
465         }
466 
467         mCurrentDimSide = dimSide;
468 
469         if (SHOW_TRANSACTIONS) Slog.i(TAG, ">>> OPEN TRANSACTION updateDimLayerVisibility");
470         SurfaceControl.openTransaction();
471         if (mCurrentDimSide == CTRL_NONE) {
472             mDimLayer.hide();
473         } else {
474             showDimLayer();
475         }
476         SurfaceControl.closeTransaction();
477     }
478 
479     /**
480      * Returns the side of the screen the dim layer should be shown.
481      * @param x horizontal coordinate used to determine if the dim layer should be shown
482      * @return Returns {@link #CTRL_LEFT} if the dim layer should be shown on the left half of the
483      * screen, {@link #CTRL_RIGHT} if on the right side, or {@link #CTRL_NONE} if the dim layer
484      * shouldn't be shown.
485      */
getDimSide(int x)486     private int getDimSide(int x) {
487         if (mTask.mStack.mStackId != FREEFORM_WORKSPACE_STACK_ID
488                 || !mTask.mStack.isFullscreen()
489                 || mService.mCurConfiguration.orientation != ORIENTATION_LANDSCAPE) {
490             return CTRL_NONE;
491         }
492 
493         mTask.mStack.getDimBounds(mTmpRect);
494         if (x - mSideMargin <= mTmpRect.left) {
495             return CTRL_LEFT;
496         }
497         if (x + mSideMargin >= mTmpRect.right) {
498             return CTRL_RIGHT;
499         }
500 
501         return CTRL_NONE;
502     }
503 
showDimLayer()504     private void showDimLayer() {
505         mTask.mStack.getDimBounds(mTmpRect);
506         if (mCurrentDimSide == CTRL_LEFT) {
507             mTmpRect.right = mTmpRect.centerX();
508         } else if (mCurrentDimSide == CTRL_RIGHT) {
509             mTmpRect.left = mTmpRect.centerX();
510         }
511 
512         mDimLayer.setBounds(mTmpRect);
513         mDimLayer.show(mService.getDragLayerLocked(), RESIZING_HINT_ALPHA,
514                 RESIZING_HINT_DURATION_MS);
515     }
516 
517     @Override /** {@link DimLayer.DimLayerUser} */
dimFullscreen()518     public boolean dimFullscreen() {
519         return isFullscreen();
520     }
521 
isFullscreen()522     boolean isFullscreen() {
523         return false;
524     }
525 
526     @Override /** {@link DimLayer.DimLayerUser} */
getDisplayInfo()527     public DisplayInfo getDisplayInfo() {
528         return mTask.mStack.getDisplayInfo();
529     }
530 
531     @Override
getDimBounds(Rect out)532     public void getDimBounds(Rect out) {
533         // This dim layer user doesn't need this.
534     }
535 
536     @Override
toShortString()537     public String toShortString() {
538         return TAG;
539     }
540 }
541