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