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.internal.widget;
18 
19 import android.content.Context;
20 import android.graphics.Color;
21 import android.graphics.Rect;
22 import android.os.RemoteException;
23 import android.util.AttributeSet;
24 import android.util.Log;
25 import android.view.GestureDetector;
26 import android.view.MotionEvent;
27 import android.view.View;
28 import android.view.ViewConfiguration;
29 import android.view.ViewGroup;
30 import android.view.ViewOutlineProvider;
31 import android.view.Window;
32 
33 import com.android.internal.R;
34 import com.android.internal.policy.PhoneWindow;
35 
36 import 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 subtle/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  */
76 public 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     private boolean mOverlayWithAppContent = false;
86 
87     private View mCaption;
88     private View mContent;
89     private View mMaximize;
90     private View mClose;
91 
92     // Fields for detecting drag events.
93     private int mTouchDownX;
94     private int mTouchDownY;
95     private boolean mCheckForDragging;
96     private int mDragSlop;
97 
98     // Fields for detecting and intercepting click events on close/maximize.
99     private ArrayList<View> mTouchDispatchList = new ArrayList<>(2);
100     // We use the gesture detector to detect clicks on close/maximize buttons and to be consistent
101     // with existing click detection.
102     private GestureDetector mGestureDetector;
103     private final Rect mCloseRect = new Rect();
104     private final Rect mMaximizeRect = new Rect();
105     private View mClickTarget;
106     private int mRootScrollY;
107 
DecorCaptionView(Context context)108     public DecorCaptionView(Context context) {
109         super(context);
110         init(context);
111     }
112 
DecorCaptionView(Context context, AttributeSet attrs)113     public DecorCaptionView(Context context, AttributeSet attrs) {
114         super(context, attrs);
115         init(context);
116     }
117 
DecorCaptionView(Context context, AttributeSet attrs, int defStyle)118     public DecorCaptionView(Context context, AttributeSet attrs, int defStyle) {
119         super(context, attrs, defStyle);
120         init(context);
121     }
122 
init(Context context)123     private void init(Context context) {
124         mDragSlop = ViewConfiguration.get(context).getScaledTouchSlop();
125         mGestureDetector = new GestureDetector(context, this);
126     }
127 
128     @Override
onFinishInflate()129     protected void onFinishInflate() {
130         super.onFinishInflate();
131         mCaption = getChildAt(0);
132     }
133 
setPhoneWindow(PhoneWindow owner, boolean show)134     public void setPhoneWindow(PhoneWindow owner, boolean show) {
135         mOwner = owner;
136         mShow = show;
137         mOverlayWithAppContent = owner.isOverlayWithDecorCaptionEnabled();
138         if (mOverlayWithAppContent) {
139             // The caption is covering the content, so we make its background transparent to make
140             // the content visible.
141             mCaption.setBackgroundColor(Color.TRANSPARENT);
142         }
143         updateCaptionVisibility();
144         // By changing the outline provider to BOUNDS, the window can remove its
145         // background without removing the shadow.
146         mOwner.getDecorView().setOutlineProvider(ViewOutlineProvider.BOUNDS);
147         mMaximize = findViewById(R.id.maximize_window);
148         mClose = findViewById(R.id.close_window);
149     }
150 
151     @Override
onInterceptTouchEvent(MotionEvent ev)152     public boolean onInterceptTouchEvent(MotionEvent ev) {
153         // If the user starts touch on the maximize/close buttons, we immediately intercept, so
154         // that these buttons are always clickable.
155         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
156             final int x = (int) ev.getX();
157             final int y = (int) ev.getY();
158             // Only offset y for containment tests because the actual views are already translated.
159             if (mMaximizeRect.contains(x, y - mRootScrollY)) {
160                 mClickTarget = mMaximize;
161             }
162             if (mCloseRect.contains(x, y - mRootScrollY)) {
163                 mClickTarget = mClose;
164             }
165         }
166         return mClickTarget != null;
167     }
168 
169     @Override
onTouchEvent(MotionEvent event)170     public boolean onTouchEvent(MotionEvent event) {
171         if (mClickTarget != null) {
172             mGestureDetector.onTouchEvent(event);
173             final int action = event.getAction();
174             if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
175                 mClickTarget = null;
176             }
177             return true;
178         }
179         return false;
180     }
181 
182     @Override
onTouch(View v, MotionEvent e)183     public boolean onTouch(View v, MotionEvent e) {
184         // Note: There are no mixed events. When a new device gets used (e.g. 1. Mouse, 2. touch)
185         // the old input device events get cancelled first. So no need to remember the kind of
186         // input device we are listening to.
187         final int x = (int) e.getX();
188         final int y = (int) e.getY();
189         final boolean fromMouse = e.getToolType(e.getActionIndex()) == MotionEvent.TOOL_TYPE_MOUSE;
190         final boolean primaryButton = (e.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0;
191         final int actionMasked = e.getActionMasked();
192         switch (actionMasked) {
193             case MotionEvent.ACTION_DOWN:
194                 if (!mShow) {
195                     // When there is no caption we should not react to anything.
196                     return false;
197                 }
198                 // Checking for a drag action is started if we aren't dragging already and the
199                 // starting event is either a left mouse button or any other input device.
200                 if (!fromMouse || primaryButton) {
201                     mCheckForDragging = true;
202                     mTouchDownX = x;
203                     mTouchDownY = y;
204                 }
205                 break;
206 
207             case MotionEvent.ACTION_MOVE:
208                 if (!mDragging && mCheckForDragging && (fromMouse || passedSlop(x, y))) {
209                     mCheckForDragging = false;
210                     mDragging = true;
211                     startMovingTask(e.getRawX(), e.getRawY());
212                     // After the above call the framework will take over the input.
213                     // This handler will receive ACTION_CANCEL soon (possible after a few spurious
214                     // ACTION_MOVE events which are safe to ignore).
215                 }
216                 break;
217 
218             case MotionEvent.ACTION_UP:
219             case MotionEvent.ACTION_CANCEL:
220                 if (!mDragging) {
221                     break;
222                 }
223                 // Abort the ongoing dragging.
224                 if (actionMasked == MotionEvent.ACTION_UP) {
225                     // If it receives ACTION_UP event, the dragging is already finished and also
226                     // the system can not end drag on ACTION_UP event. So request to finish
227                     // dragging.
228                     finishMovingTask();
229                 }
230                 mDragging = false;
231                 return !mCheckForDragging;
232         }
233         return mDragging || mCheckForDragging;
234     }
235 
236     @Override
buildTouchDispatchChildList()237     public ArrayList<View> buildTouchDispatchChildList() {
238         mTouchDispatchList.ensureCapacity(3);
239         if (mCaption != null) {
240             mTouchDispatchList.add(mCaption);
241         }
242         if (mContent != null) {
243             mTouchDispatchList.add(mContent);
244         }
245         return mTouchDispatchList;
246     }
247 
248     @Override
shouldDelayChildPressedState()249     public boolean shouldDelayChildPressedState() {
250         return false;
251     }
252 
passedSlop(int x, int y)253     private boolean passedSlop(int x, int y) {
254         return Math.abs(x - mTouchDownX) > mDragSlop || Math.abs(y - mTouchDownY) > mDragSlop;
255     }
256 
257     /**
258      * The phone window configuration has changed and the caption needs to be updated.
259      * @param show True if the caption should be shown.
260      */
onConfigurationChanged(boolean show)261     public void onConfigurationChanged(boolean show) {
262         mShow = show;
263         updateCaptionVisibility();
264     }
265 
266     @Override
addView(View child, int index, ViewGroup.LayoutParams params)267     public void addView(View child, int index, ViewGroup.LayoutParams params) {
268         if (!(params instanceof MarginLayoutParams)) {
269             throw new IllegalArgumentException(
270                     "params " + params + " must subclass MarginLayoutParams");
271         }
272         // Make sure that we never get more then one client area in our view.
273         if (index >= 2 || getChildCount() >= 2) {
274             throw new IllegalStateException("DecorCaptionView can only handle 1 client view");
275         }
276         // To support the overlaying content in the caption, we need to put the content view as the
277         // first child to get the right Z-Ordering.
278         super.addView(child, 0, params);
279         mContent = child;
280     }
281 
282     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)283     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
284         final int captionHeight;
285         if (mCaption.getVisibility() != View.GONE) {
286             measureChildWithMargins(mCaption, widthMeasureSpec, 0, heightMeasureSpec, 0);
287             captionHeight = mCaption.getMeasuredHeight();
288         } else {
289             captionHeight = 0;
290         }
291         if (mContent != null) {
292             if (mOverlayWithAppContent) {
293                 measureChildWithMargins(mContent, widthMeasureSpec, 0, heightMeasureSpec, 0);
294             } else {
295                 measureChildWithMargins(mContent, widthMeasureSpec, 0, heightMeasureSpec,
296                         captionHeight);
297             }
298         }
299 
300         setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
301                 MeasureSpec.getSize(heightMeasureSpec));
302     }
303 
304     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)305     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
306         final int captionHeight;
307         if (mCaption.getVisibility() != View.GONE) {
308             mCaption.layout(0, 0, mCaption.getMeasuredWidth(), mCaption.getMeasuredHeight());
309             captionHeight = mCaption.getBottom() - mCaption.getTop();
310             mMaximize.getHitRect(mMaximizeRect);
311             mClose.getHitRect(mCloseRect);
312         } else {
313             captionHeight = 0;
314             mMaximizeRect.setEmpty();
315             mCloseRect.setEmpty();
316         }
317 
318         if (mContent != null) {
319             if (mOverlayWithAppContent) {
320                 mContent.layout(0, 0, mContent.getMeasuredWidth(), mContent.getMeasuredHeight());
321             } else {
322                 mContent.layout(0, captionHeight, mContent.getMeasuredWidth(),
323                         captionHeight + mContent.getMeasuredHeight());
324             }
325         }
326 
327         // This assumes that the caption bar is at the top.
328         mOwner.notifyRestrictedCaptionAreaCallback(mMaximize.getLeft(), mMaximize.getTop(),
329                 mClose.getRight(), mClose.getBottom());
330     }
331 
332     /**
333      * Updates the visibility of the caption.
334      **/
updateCaptionVisibility()335     private void updateCaptionVisibility() {
336         mCaption.setVisibility(mShow ? VISIBLE : GONE);
337         mCaption.setOnTouchListener(this);
338     }
339 
340     /**
341      * Maximize or restore the window by moving it to the maximized or freeform workspace stack.
342      **/
toggleFreeformWindowingMode()343     private void toggleFreeformWindowingMode() {
344         Window.WindowControllerCallback callback = mOwner.getWindowControllerCallback();
345         if (callback != null) {
346             try {
347                 callback.toggleFreeformWindowingMode();
348             } catch (RemoteException ex) {
349                 Log.e(TAG, "Cannot change task workspace.");
350             }
351         }
352     }
353 
isCaptionShowing()354     public boolean isCaptionShowing() {
355         return mShow;
356     }
357 
getCaptionHeight()358     public int getCaptionHeight() {
359         return (mCaption != null) ? mCaption.getHeight() : 0;
360     }
361 
removeContentView()362     public void removeContentView() {
363         if (mContent != null) {
364             removeView(mContent);
365             mContent = null;
366         }
367     }
368 
getCaption()369     public View getCaption() {
370         return mCaption;
371     }
372 
373     @Override
generateLayoutParams(AttributeSet attrs)374     public LayoutParams generateLayoutParams(AttributeSet attrs) {
375         return new MarginLayoutParams(getContext(), attrs);
376     }
377 
378     @Override
generateDefaultLayoutParams()379     protected LayoutParams generateDefaultLayoutParams() {
380         return new MarginLayoutParams(MarginLayoutParams.MATCH_PARENT,
381                 MarginLayoutParams.MATCH_PARENT);
382     }
383 
384     @Override
generateLayoutParams(LayoutParams p)385     protected LayoutParams generateLayoutParams(LayoutParams p) {
386         return new MarginLayoutParams(p);
387     }
388 
389     @Override
checkLayoutParams(ViewGroup.LayoutParams p)390     protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
391         return p instanceof MarginLayoutParams;
392     }
393 
394     @Override
onDown(MotionEvent e)395     public boolean onDown(MotionEvent e) {
396         return false;
397     }
398 
399     @Override
onShowPress(MotionEvent e)400     public void onShowPress(MotionEvent e) {
401 
402     }
403 
404     @Override
onSingleTapUp(MotionEvent e)405     public boolean onSingleTapUp(MotionEvent e) {
406         if (mClickTarget == mMaximize) {
407             toggleFreeformWindowingMode();
408         } else if (mClickTarget == mClose) {
409             mOwner.dispatchOnWindowDismissed(
410                     true /*finishTask*/, false /*suppressWindowTransition*/);
411         }
412         return true;
413     }
414 
415     @Override
onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)416     public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
417         return false;
418     }
419 
420     @Override
onLongPress(MotionEvent e)421     public void onLongPress(MotionEvent e) {
422 
423     }
424 
425     @Override
onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)426     public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
427         return false;
428     }
429 
430     /**
431      * Called when {@link android.view.ViewRootImpl} scrolls for adjustPan.
432      */
onRootViewScrollYChanged(int scrollY)433     public void onRootViewScrollYChanged(int scrollY) {
434         // Offset the caption opposite the root scroll. This keeps the caption at the
435         // top of the window during adjustPan.
436         if (mCaption != null) {
437             mRootScrollY = scrollY;
438             mCaption.setTranslationY(scrollY);
439         }
440     }
441 }
442