blob: 409a17f9c4db6499c5b2c96205d1bcff4540e69c [file] [log] [blame]
Stefan Kuhne61b47bb2015-07-28 14:04:25 -07001/*
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
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070019import android.content.Context;
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -080020import android.graphics.Color;
21import android.graphics.Rect;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070022import android.os.RemoteException;
23import android.util.AttributeSet;
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -080024import android.util.Log;
25import android.view.GestureDetector;
Skuhne81c524a2015-08-12 13:34:14 -070026import android.view.MotionEvent;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070027import android.view.View;
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -080028import android.view.ViewConfiguration;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070029import android.view.ViewGroup;
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -070030import android.view.ViewOutlineProvider;
31import android.view.Window;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070032
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070033import com.android.internal.R;
34import com.android.internal.policy.PhoneWindow;
35
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -080036import java.util.ArrayList;
37
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070038/**
Wale Ogunwale62a91d62015-11-18 11:44:10 -080039 * This class represents the special screen elements to control a window on freeform
40 * environment.
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070041 * 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>
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -080045 * </ul>
46 * After creating the view, the function {@link #setPhoneWindow} needs to be called to make
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070047 * 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>
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070051 * <li>setOutlineProvider</li>
52 * <li>setSurfaceFormat</li>
53 * <li>..</li>
54 * </ul>
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -080055 *
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>
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070075 */
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -080076public class DecorCaptionView extends ViewGroup implements View.OnTouchListener,
77 GestureDetector.OnGestureListener {
Wale Ogunwale62a91d62015-11-18 11:44:10 -080078 private final static String TAG = "DecorCaptionView";
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070079 private PhoneWindow mOwner = null;
Wale Ogunwale62a91d62015-11-18 11:44:10 -080080 private boolean mShow = false;
Skuhne81c524a2015-08-12 13:34:14 -070081
82 // True if the window is being dragged.
83 private boolean mDragging = false;
84
Skuhne81c524a2015-08-12 13:34:14 -070085 // True when the left mouse button got released while dragging.
86 private boolean mLeftMouseButtonReleased;
87
Filip Gruszczynski63250652015-11-18 14:43:01 -080088 private boolean mOverlayWithAppContent = false;
89
90 private View mCaption;
91 private View mContent;
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -080092 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;
Filip Gruszczynski63250652015-11-18 14:43:01 -0800109
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800110 public DecorCaptionView(Context context) {
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700111 super(context);
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800112 init(context);
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700113 }
114
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800115 public DecorCaptionView(Context context, AttributeSet attrs) {
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700116 super(context, attrs);
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800117 init(context);
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700118 }
119
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800120 public DecorCaptionView(Context context, AttributeSet attrs, int defStyle) {
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700121 super(context, attrs, defStyle);
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800122 init(context);
123 }
124
125 private void init(Context context) {
126 mDragSlop = ViewConfiguration.get(context).getScaledTouchSlop();
127 mGestureDetector = new GestureDetector(context, this);
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700128 }
129
Filip Gruszczynski63250652015-11-18 14:43:01 -0800130 @Override
131 protected void onFinishInflate() {
132 super.onFinishInflate();
133 mCaption = getChildAt(0);
134 }
135
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800136 public void setPhoneWindow(PhoneWindow owner, boolean show) {
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700137 mOwner = owner;
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800138 mShow = show;
Filip Gruszczynski63250652015-11-18 14:43:01 -0800139 mOverlayWithAppContent = owner.getOverlayDecorCaption();
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800140 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 }
Skuhnef7b882c2015-08-11 17:18:58 -0700145 updateCaptionVisibility();
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700146 // By changing the outline provider to BOUNDS, the window can remove its
147 // background without removing the shadow.
148 mOwner.getDecorView().setOutlineProvider(ViewOutlineProvider.BOUNDS);
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800149 mMaximize = findViewById(R.id.maximize_window);
150 mClose = findViewById(R.id.close_window);
151 }
Skuhneb8160872015-09-22 09:51:39 -0700152
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800153 @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;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700181 }
182
Skuhne81c524a2015-08-12 13:34:14 -0700183 @Override
Chong Zhang509ea6b2015-09-30 14:09:52 -0700184 public boolean onTouch(View v, MotionEvent e) {
Skuhne81c524a2015-08-12 13:34:14 -0700185 // 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.
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800188 final int x = (int) e.getX();
189 final int y = (int) e.getY();
Skuhne81c524a2015-08-12 13:34:14 -0700190 switch (e.getActionMasked()) {
191 case MotionEvent.ACTION_DOWN:
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800192 if (!mShow) {
193 // When there is no caption we should not react to anything.
Skuhnea635a262015-08-26 15:45:58 -0700194 return false;
195 }
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800196 // 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;
Skuhne81c524a2015-08-12 13:34:14 -0700203 }
204 break;
205
206 case MotionEvent.ACTION_MOVE:
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800207 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) {
Skuhne81c524a2015-08-12 13:34:14 -0700213 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 }
Skuhne81c524a2015-08-12 13:34:14 -0700220 }
221 break;
222
223 case MotionEvent.ACTION_UP:
Skuhne81c524a2015-08-12 13:34:14 -0700224 case MotionEvent.ACTION_CANCEL:
Skuhnea5a93ee2015-08-20 15:43:57 -0700225 if (!mDragging) {
226 break;
Skuhne81c524a2015-08-12 13:34:14 -0700227 }
Skuhnea5a93ee2015-08-20 15:43:57 -0700228 // Abort the ongoing dragging.
229 mDragging = false;
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800230 return !mCheckForDragging;
Skuhne81c524a2015-08-12 13:34:14 -0700231 }
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800232 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;
Skuhne81c524a2015-08-12 13:34:14 -0700249 }
250
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700251 /**
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800252 * The phone window configuration has changed and the caption needs to be updated.
253 * @param show True if the caption should be shown.
Wale Ogunwale2b547c32015-11-18 10:33:22 -0800254 */
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800255 public void onConfigurationChanged(boolean show) {
256 mShow = show;
Skuhnef7b882c2015-08-11 17:18:58 -0700257 updateCaptionVisibility();
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700258 }
259
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700260 @Override
Skuhnef7b882c2015-08-11 17:18:58 -0700261 public void addView(View child, int index, ViewGroup.LayoutParams params) {
Filip Gruszczynski63250652015-11-18 14:43:01 -0800262 if (!(params instanceof MarginLayoutParams)) {
263 throw new IllegalArgumentException(
264 "params " + params + " must subclass MarginLayoutParams");
265 }
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700266 // Make sure that we never get more then one client area in our view.
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700267 if (index >= 2 || getChildCount() >= 2) {
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800268 throw new IllegalStateException("DecorCaptionView can only handle 1 client view");
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700269 }
Filip Gruszczynski63250652015-11-18 14:43:01 -0800270 // 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;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700274 }
275
Filip Gruszczynski63250652015-11-18 14:43:01 -0800276 @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();
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800304 mMaximize.getHitRect(mMaximizeRect);
305 mClose.getHitRect(mCloseRect);
Filip Gruszczynski63250652015-11-18 14:43:01 -0800306 } else {
307 captionHeight = 0;
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800308 mMaximizeRect.setEmpty();
309 mCloseRect.setEmpty();
Filip Gruszczynski63250652015-11-18 14:43:01 -0800310 }
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 }
Filip Gruszczynski3dec0812015-12-09 08:42:41 -0800320
321 // This assumes that the caption bar is at the top.
322 mOwner.notifyRestrictedCaptionAreaCallback(mMaximize.getLeft(), mMaximize.getTop(),
323 mClose.getRight(), mClose.getBottom());
Filip Gruszczynski63250652015-11-18 14:43:01 -0800324 }
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700325 /**
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 **/
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700329 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
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700335 /**
Skuhnef7b882c2015-08-11 17:18:58 -0700336 * Updates the visibility of the caption.
337 **/
338 private void updateCaptionVisibility() {
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800339 // Don't show the caption if the window has e.g. entered full screen.
340 boolean invisible = isFillingScreen() || !mShow;
Filip Gruszczynski63250652015-11-18 14:43:01 -0800341 mCaption.setVisibility(invisible ? GONE : VISIBLE);
342 mCaption.setOnTouchListener(this);
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700343 }
Stefan Kuhne1b420572015-08-07 10:50:19 -0700344
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700345 /**
346 * Maximize the window by moving it to the maximized workspace stack.
347 **/
Stefan Kuhne1b420572015-08-07 10:50:19 -0700348 private void maximizeWindow() {
Skuhnece2faa52015-08-11 10:36:38 -0700349 Window.WindowControllerCallback callback = mOwner.getWindowControllerCallback();
Stefan Kuhne1b420572015-08-07 10:50:19 -0700350 if (callback != null) {
351 try {
Filip Gruszczynski411c06f2016-01-07 09:44:44 -0800352 callback.exitFreeformMode();
Stefan Kuhne1b420572015-08-07 10:50:19 -0700353 } catch (RemoteException ex) {
354 Log.e(TAG, "Cannot change task workspace.");
355 }
356 }
357 }
Wale Ogunwale8cc5a742015-11-17 15:41:05 -0800358
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800359 public boolean isCaptionShowing() {
360 return mShow;
Wale Ogunwale8cc5a742015-11-17 15:41:05 -0800361 }
362
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800363 public int getCaptionHeight() {
Filip Gruszczynski63250652015-11-18 14:43:01 -0800364 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;
Wale Ogunwale8cc5a742015-11-17 15:41:05 -0800397 }
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800398
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 }
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700433}