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
17package com.android.internal.widget;
18
19import android.content.Context;
20import android.graphics.Color;
21import android.graphics.Rect;
22import android.os.RemoteException;
23import android.util.AttributeSet;
24import android.util.Log;
25import android.view.GestureDetector;
26import android.view.MotionEvent;
27import android.view.View;
28import android.view.ViewConfiguration;
29import android.view.ViewGroup;
30import android.view.ViewOutlineProvider;
31import android.view.Window;
32
33import com.android.internal.R;
34import com.android.internal.policy.PhoneWindow;
35
36import java.util.ArrayList;
37
38/**
39 * This class represents the special screen elements to control a window on freeform
40 * environment.
41 * As such this class handles the following things:
42 * <ul>
43 * <li>The caption, containing the system buttons like maximize, close and such as well as
44 * allowing the user to drag the window around.</li>
45 * </ul>
46 * After creating the view, the function {@link #setPhoneWindow} needs to be called to make
47 * the connection to it's owning PhoneWindow.
48 * Note: At this time the application can change various attributes of the DecorView which
49 * will break things (in settle/unexpected ways):
50 * <ul>
51 * <li>setOutlineProvider</li>
52 * <li>setSurfaceFormat</li>
53 * <li>..</li>
54 * </ul>
55 *
56 * Although this ViewGroup has only two direct sub-Views, its behavior is more complex due to
57 * overlaying caption on the content and drawing.
58 *
59 * First, no matter where the content View gets added, it will always be the first child and the
60 * caption will be the second. This way the caption will always be drawn on top of the content when
61 * overlaying is enabled.
62 *
63 * Second, the touch dispatch is customized to handle overlaying. This is what happens when touch
64 * is dispatched on the caption area while overlaying it on content:
65 * <ul>
66 * <li>DecorCaptionView.onInterceptTouchEvent() will try intercepting the touch events if the
67 * down action is performed on top close or maximize buttons; the reason for that is we want these
68 * buttons to always work.</li>
69 * <li>The content View will receive the touch event. Mind that content is actually underneath the
70 * caption, so we need to introduce our own dispatch ordering. We achieve this by overriding
71 * {@link #buildTouchDispatchChildList()}.</li>
72 * <li>If the touch event is not consumed by the content View, it will go to the caption View
73 * and the dragging logic will be executed.</li>
74 * </ul>
75 */
76public class DecorCaptionView extends ViewGroup implements View.OnTouchListener,
77        GestureDetector.OnGestureListener {
78    private final static String TAG = "DecorCaptionView";
79    private PhoneWindow mOwner = null;
80    private boolean mShow = false;
81
82    // True if the window is being dragged.
83    private boolean mDragging = false;
84
85    // True when the left mouse button got released while dragging.
86    private boolean mLeftMouseButtonReleased;
87
88    private boolean mOverlayWithAppContent = false;
89
90    private View mCaption;
91    private View mContent;
92    private View mMaximize;
93    private View mClose;
94
95    // Fields for detecting drag events.
96    private int mTouchDownX;
97    private int mTouchDownY;
98    private boolean mCheckForDragging;
99    private int mDragSlop;
100
101    // Fields for detecting and intercepting click events on close/maximize.
102    private ArrayList<View> mTouchDispatchList = new ArrayList<>(2);
103    // We use the gesture detector to detect clicks on close/maximize buttons and to be consistent
104    // with existing click detection.
105    private GestureDetector mGestureDetector;
106    private final Rect mCloseRect = new Rect();
107    private final Rect mMaximizeRect = new Rect();
108    private View mClickTarget;
109
110    public DecorCaptionView(Context context) {
111        super(context);
112        init(context);
113    }
114
115    public DecorCaptionView(Context context, AttributeSet attrs) {
116        super(context, attrs);
117        init(context);
118    }
119
120    public DecorCaptionView(Context context, AttributeSet attrs, int defStyle) {
121        super(context, attrs, defStyle);
122        init(context);
123    }
124
125    private void init(Context context) {
126        mDragSlop = ViewConfiguration.get(context).getScaledTouchSlop();
127        mGestureDetector = new GestureDetector(context, this);
128    }
129
130    @Override
131    protected void onFinishInflate() {
132        super.onFinishInflate();
133        mCaption = getChildAt(0);
134    }
135
136    public void setPhoneWindow(PhoneWindow owner, boolean show) {
137        mOwner = owner;
138        mShow = show;
139        mOverlayWithAppContent = owner.isOverlayWithDecorCaptionEnabled();
140        if (mOverlayWithAppContent) {
141            // The caption is covering the content, so we make its background transparent to make
142            // the content visible.
143            mCaption.setBackgroundColor(Color.TRANSPARENT);
144        }
145        updateCaptionVisibility();
146        // By changing the outline provider to BOUNDS, the window can remove its
147        // background without removing the shadow.
148        mOwner.getDecorView().setOutlineProvider(ViewOutlineProvider.BOUNDS);
149        mMaximize = findViewById(R.id.maximize_window);
150        mClose = findViewById(R.id.close_window);
151    }
152
153    @Override
154    public boolean onInterceptTouchEvent(MotionEvent ev) {
155        // If the user starts touch on the maximize/close buttons, we immediately intercept, so
156        // that these buttons are always clickable.
157        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
158            final int x = (int) ev.getX();
159            final int y = (int) ev.getY();
160            if (mMaximizeRect.contains(x, y)) {
161                mClickTarget = mMaximize;
162            }
163            if (mCloseRect.contains(x, y)) {
164                mClickTarget = mClose;
165            }
166        }
167        return mClickTarget != null;
168    }
169
170    @Override
171    public boolean onTouchEvent(MotionEvent event) {
172        if (mClickTarget != null) {
173            mGestureDetector.onTouchEvent(event);
174            final int action = event.getAction();
175            if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
176                mClickTarget = null;
177            }
178            return true;
179        }
180        return false;
181    }
182
183    @Override
184    public boolean onTouch(View v, MotionEvent e) {
185        // Note: There are no mixed events. When a new device gets used (e.g. 1. Mouse, 2. touch)
186        // the old input device events get cancelled first. So no need to remember the kind of
187        // input device we are listening to.
188        final int x = (int) e.getX();
189        final int y = (int) e.getY();
190        switch (e.getActionMasked()) {
191            case MotionEvent.ACTION_DOWN:
192                if (!mShow) {
193                    // When there is no caption we should not react to anything.
194                    return false;
195                }
196                // Checking for a drag action is started if we aren't dragging already and the
197                // starting event is either a left mouse button or any other input device.
198                if (((e.getToolType(e.getActionIndex()) != MotionEvent.TOOL_TYPE_MOUSE ||
199                        (e.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0))) {
200                    mCheckForDragging = true;
201                    mTouchDownX = x;
202                    mTouchDownY = y;
203                }
204                break;
205
206            case MotionEvent.ACTION_MOVE:
207                if (!mDragging && mCheckForDragging && passedSlop(x, y)) {
208                    mCheckForDragging = false;
209                    mDragging = true;
210                    mLeftMouseButtonReleased = false;
211                    startMovingTask(e.getRawX(), e.getRawY());
212                } else if (mDragging && !mLeftMouseButtonReleased) {
213                    if (e.getToolType(e.getActionIndex()) == MotionEvent.TOOL_TYPE_MOUSE &&
214                            (e.getButtonState() & MotionEvent.BUTTON_PRIMARY) == 0) {
215                        // There is no separate mouse button up call and if the user mixes mouse
216                        // button drag actions, we stop dragging once he releases the button.
217                        mLeftMouseButtonReleased = true;
218                        break;
219                    }
220                }
221                break;
222
223            case MotionEvent.ACTION_UP:
224            case MotionEvent.ACTION_CANCEL:
225                if (!mDragging) {
226                    break;
227                }
228                // Abort the ongoing dragging.
229                mDragging = false;
230                return !mCheckForDragging;
231        }
232        return mDragging || mCheckForDragging;
233    }
234
235    @Override
236    public ArrayList<View> buildTouchDispatchChildList() {
237        mTouchDispatchList.ensureCapacity(3);
238        if (mCaption != null) {
239            mTouchDispatchList.add(mCaption);
240        }
241        if (mContent != null) {
242            mTouchDispatchList.add(mContent);
243        }
244        return mTouchDispatchList;
245    }
246
247    private boolean passedSlop(int x, int y) {
248        return Math.abs(x - mTouchDownX) > mDragSlop || Math.abs(y - mTouchDownY) > mDragSlop;
249    }
250
251    /**
252     * The phone window configuration has changed and the caption needs to be updated.
253     * @param show True if the caption should be shown.
254     */
255    public void onConfigurationChanged(boolean show) {
256        mShow = show;
257        updateCaptionVisibility();
258    }
259
260    @Override
261    public void addView(View child, int index, ViewGroup.LayoutParams params) {
262        if (!(params instanceof MarginLayoutParams)) {
263            throw new IllegalArgumentException(
264                    "params " + params + " must subclass MarginLayoutParams");
265        }
266        // Make sure that we never get more then one client area in our view.
267        if (index >= 2 || getChildCount() >= 2) {
268            throw new IllegalStateException("DecorCaptionView can only handle 1 client view");
269        }
270        // To support the overlaying content in the caption, we need to put the content view as the
271        // first child to get the right Z-Ordering.
272        super.addView(child, 0, params);
273        mContent = child;
274    }
275
276    @Override
277    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
278        final int captionHeight;
279        if (mCaption.getVisibility() != View.GONE) {
280            measureChildWithMargins(mCaption, widthMeasureSpec, 0, heightMeasureSpec, 0);
281            captionHeight = mCaption.getMeasuredHeight();
282        } else {
283            captionHeight = 0;
284        }
285        if (mContent != null) {
286            if (mOverlayWithAppContent) {
287                measureChildWithMargins(mContent, widthMeasureSpec, 0, heightMeasureSpec, 0);
288            } else {
289                measureChildWithMargins(mContent, widthMeasureSpec, 0, heightMeasureSpec,
290                        captionHeight);
291            }
292        }
293
294        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
295                MeasureSpec.getSize(heightMeasureSpec));
296    }
297
298    @Override
299    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
300        final int captionHeight;
301        if (mCaption.getVisibility() != View.GONE) {
302            mCaption.layout(0, 0, mCaption.getMeasuredWidth(), mCaption.getMeasuredHeight());
303            captionHeight = mCaption.getBottom() - mCaption.getTop();
304            mMaximize.getHitRect(mMaximizeRect);
305            mClose.getHitRect(mCloseRect);
306        } else {
307            captionHeight = 0;
308            mMaximizeRect.setEmpty();
309            mCloseRect.setEmpty();
310        }
311
312        if (mContent != null) {
313            if (mOverlayWithAppContent) {
314                mContent.layout(0, 0, mContent.getMeasuredWidth(), mContent.getMeasuredHeight());
315            } else {
316                mContent.layout(0, captionHeight, mContent.getMeasuredWidth(),
317                        captionHeight + mContent.getMeasuredHeight());
318            }
319        }
320
321        // This assumes that the caption bar is at the top.
322        mOwner.notifyRestrictedCaptionAreaCallback(mMaximize.getLeft(), mMaximize.getTop(),
323                mClose.getRight(), mClose.getBottom());
324    }
325    /**
326     * Determine if the workspace is entirely covered by the window.
327     * @return Returns true when the window is filling the entire screen/workspace.
328     **/
329    private boolean isFillingScreen() {
330        return (0 != ((getWindowSystemUiVisibility() | getSystemUiVisibility()) &
331                (View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
332                        View.SYSTEM_UI_FLAG_IMMERSIVE | View.SYSTEM_UI_FLAG_LOW_PROFILE)));
333    }
334
335    /**
336     * Updates the visibility of the caption.
337     **/
338    private void updateCaptionVisibility() {
339        // Don't show the caption if the window has e.g. entered full screen.
340        boolean invisible = isFillingScreen() || !mShow;
341        mCaption.setVisibility(invisible ? GONE : VISIBLE);
342        mCaption.setOnTouchListener(this);
343    }
344
345    /**
346     * Maximize the window by moving it to the maximized workspace stack.
347     **/
348    private void maximizeWindow() {
349        Window.WindowControllerCallback callback = mOwner.getWindowControllerCallback();
350        if (callback != null) {
351            try {
352                callback.exitFreeformMode();
353            } catch (RemoteException ex) {
354                Log.e(TAG, "Cannot change task workspace.");
355            }
356        }
357    }
358
359    public boolean isCaptionShowing() {
360        return mShow;
361    }
362
363    public int getCaptionHeight() {
364        return (mCaption != null) ? mCaption.getHeight() : 0;
365    }
366
367    public void removeContentView() {
368        if (mContent != null) {
369            removeView(mContent);
370            mContent = null;
371        }
372    }
373
374    public View getCaption() {
375        return mCaption;
376    }
377
378    @Override
379    public LayoutParams generateLayoutParams(AttributeSet attrs) {
380        return new MarginLayoutParams(getContext(), attrs);
381    }
382
383    @Override
384    protected LayoutParams generateDefaultLayoutParams() {
385        return new MarginLayoutParams(MarginLayoutParams.MATCH_PARENT,
386                MarginLayoutParams.MATCH_PARENT);
387    }
388
389    @Override
390    protected LayoutParams generateLayoutParams(LayoutParams p) {
391        return new MarginLayoutParams(p);
392    }
393
394    @Override
395    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
396        return p instanceof MarginLayoutParams;
397    }
398
399    @Override
400    public boolean onDown(MotionEvent e) {
401        return false;
402    }
403
404    @Override
405    public void onShowPress(MotionEvent e) {
406
407    }
408
409    @Override
410    public boolean onSingleTapUp(MotionEvent e) {
411        if (mClickTarget == mMaximize) {
412            maximizeWindow();
413        } else if (mClickTarget == mClose) {
414            mOwner.dispatchOnWindowDismissed(true /*finishTask*/);
415        }
416        return true;
417    }
418
419    @Override
420    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
421        return false;
422    }
423
424    @Override
425    public void onLongPress(MotionEvent e) {
426
427    }
428
429    @Override
430    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
431        return false;
432    }
433}
434