blob: d747686ca76200b9a045042a7c966adf90bf9bc9 [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
Wale Ogunwale3797c222015-10-27 14:21:58 -070019import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID;
20
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070021import android.content.Context;
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -080022import android.graphics.Color;
23import android.graphics.Rect;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070024import android.os.RemoteException;
25import android.util.AttributeSet;
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -080026import android.util.Log;
27import android.view.GestureDetector;
Skuhne81c524a2015-08-12 13:34:14 -070028import android.view.MotionEvent;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070029import android.view.View;
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -080030import android.view.ViewConfiguration;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070031import android.view.ViewGroup;
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -070032import android.view.ViewOutlineProvider;
33import android.view.Window;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070034
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070035import com.android.internal.R;
36import com.android.internal.policy.PhoneWindow;
37
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -080038import java.util.ArrayList;
39
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070040/**
Wale Ogunwale62a91d62015-11-18 11:44:10 -080041 * This class represents the special screen elements to control a window on freeform
42 * environment.
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070043 * As such this class handles the following things:
44 * <ul>
45 * <li>The caption, containing the system buttons like maximize, close and such as well as
46 * allowing the user to drag the window around.</li>
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -080047 * </ul>
48 * After creating the view, the function {@link #setPhoneWindow} needs to be called to make
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070049 * the connection to it's owning PhoneWindow.
50 * Note: At this time the application can change various attributes of the DecorView which
51 * will break things (in settle/unexpected ways):
52 * <ul>
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070053 * <li>setOutlineProvider</li>
54 * <li>setSurfaceFormat</li>
55 * <li>..</li>
56 * </ul>
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -080057 *
58 * Although this ViewGroup has only two direct sub-Views, its behavior is more complex due to
59 * overlaying caption on the content and drawing.
60 *
61 * First, no matter where the content View gets added, it will always be the first child and the
62 * caption will be the second. This way the caption will always be drawn on top of the content when
63 * overlaying is enabled.
64 *
65 * Second, the touch dispatch is customized to handle overlaying. This is what happens when touch
66 * is dispatched on the caption area while overlaying it on content:
67 * <ul>
68 * <li>DecorCaptionView.onInterceptTouchEvent() will try intercepting the touch events if the
69 * down action is performed on top close or maximize buttons; the reason for that is we want these
70 * buttons to always work.</li>
71 * <li>The content View will receive the touch event. Mind that content is actually underneath the
72 * caption, so we need to introduce our own dispatch ordering. We achieve this by overriding
73 * {@link #buildTouchDispatchChildList()}.</li>
74 * <li>If the touch event is not consumed by the content View, it will go to the caption View
75 * and the dragging logic will be executed.</li>
76 * </ul>
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070077 */
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -080078public class DecorCaptionView extends ViewGroup implements View.OnTouchListener,
79 GestureDetector.OnGestureListener {
Wale Ogunwale62a91d62015-11-18 11:44:10 -080080 private final static String TAG = "DecorCaptionView";
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070081 private PhoneWindow mOwner = null;
Wale Ogunwale62a91d62015-11-18 11:44:10 -080082 private boolean mShow = false;
Skuhne81c524a2015-08-12 13:34:14 -070083
84 // True if the window is being dragged.
85 private boolean mDragging = false;
86
Skuhne81c524a2015-08-12 13:34:14 -070087 // True when the left mouse button got released while dragging.
88 private boolean mLeftMouseButtonReleased;
89
Filip Gruszczynski63250652015-11-18 14:43:01 -080090 private boolean mOverlayWithAppContent = false;
91
92 private View mCaption;
93 private View mContent;
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -080094 private View mMaximize;
95 private View mClose;
96
97 // Fields for detecting drag events.
98 private int mTouchDownX;
99 private int mTouchDownY;
100 private boolean mCheckForDragging;
101 private int mDragSlop;
102
103 // Fields for detecting and intercepting click events on close/maximize.
104 private ArrayList<View> mTouchDispatchList = new ArrayList<>(2);
105 // We use the gesture detector to detect clicks on close/maximize buttons and to be consistent
106 // with existing click detection.
107 private GestureDetector mGestureDetector;
108 private final Rect mCloseRect = new Rect();
109 private final Rect mMaximizeRect = new Rect();
110 private View mClickTarget;
Filip Gruszczynski63250652015-11-18 14:43:01 -0800111
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800112 public DecorCaptionView(Context context) {
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700113 super(context);
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800114 init(context);
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700115 }
116
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800117 public DecorCaptionView(Context context, AttributeSet attrs) {
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700118 super(context, attrs);
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800119 init(context);
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700120 }
121
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800122 public DecorCaptionView(Context context, AttributeSet attrs, int defStyle) {
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700123 super(context, attrs, defStyle);
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800124 init(context);
125 }
126
127 private void init(Context context) {
128 mDragSlop = ViewConfiguration.get(context).getScaledTouchSlop();
129 mGestureDetector = new GestureDetector(context, this);
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700130 }
131
Filip Gruszczynski63250652015-11-18 14:43:01 -0800132 @Override
133 protected void onFinishInflate() {
134 super.onFinishInflate();
135 mCaption = getChildAt(0);
136 }
137
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800138 public void setPhoneWindow(PhoneWindow owner, boolean show) {
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700139 mOwner = owner;
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800140 mShow = show;
Filip Gruszczynski63250652015-11-18 14:43:01 -0800141 mOverlayWithAppContent = owner.getOverlayDecorCaption();
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800142 if (mOverlayWithAppContent) {
143 // The caption is covering the content, so we make its background transparent to make
144 // the content visible.
145 mCaption.setBackgroundColor(Color.TRANSPARENT);
146 }
Skuhnef7b882c2015-08-11 17:18:58 -0700147 updateCaptionVisibility();
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700148 // By changing the outline provider to BOUNDS, the window can remove its
149 // background without removing the shadow.
150 mOwner.getDecorView().setOutlineProvider(ViewOutlineProvider.BOUNDS);
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800151 mMaximize = findViewById(R.id.maximize_window);
152 mClose = findViewById(R.id.close_window);
153 }
Skuhneb8160872015-09-22 09:51:39 -0700154
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800155 @Override
156 public boolean onInterceptTouchEvent(MotionEvent ev) {
157 // If the user starts touch on the maximize/close buttons, we immediately intercept, so
158 // that these buttons are always clickable.
159 if (ev.getAction() == MotionEvent.ACTION_DOWN) {
160 final int x = (int) ev.getX();
161 final int y = (int) ev.getY();
162 if (mMaximizeRect.contains(x, y)) {
163 mClickTarget = mMaximize;
164 }
165 if (mCloseRect.contains(x, y)) {
166 mClickTarget = mClose;
167 }
168 }
169 return mClickTarget != null;
170 }
171
172 @Override
173 public boolean onTouchEvent(MotionEvent event) {
174 if (mClickTarget != null) {
175 mGestureDetector.onTouchEvent(event);
176 final int action = event.getAction();
177 if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
178 mClickTarget = null;
179 }
180 return true;
181 }
182 return false;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700183 }
184
Skuhne81c524a2015-08-12 13:34:14 -0700185 @Override
Chong Zhang509ea6b2015-09-30 14:09:52 -0700186 public boolean onTouch(View v, MotionEvent e) {
Skuhne81c524a2015-08-12 13:34:14 -0700187 // Note: There are no mixed events. When a new device gets used (e.g. 1. Mouse, 2. touch)
188 // the old input device events get cancelled first. So no need to remember the kind of
189 // input device we are listening to.
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800190 final int x = (int) e.getX();
191 final int y = (int) e.getY();
Skuhne81c524a2015-08-12 13:34:14 -0700192 switch (e.getActionMasked()) {
193 case MotionEvent.ACTION_DOWN:
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800194 if (!mShow) {
195 // When there is no caption we should not react to anything.
Skuhnea635a262015-08-26 15:45:58 -0700196 return false;
197 }
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800198 // 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 (((e.getToolType(e.getActionIndex()) != MotionEvent.TOOL_TYPE_MOUSE ||
201 (e.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0))) {
202 mCheckForDragging = true;
203 mTouchDownX = x;
204 mTouchDownY = y;
Skuhne81c524a2015-08-12 13:34:14 -0700205 }
206 break;
207
208 case MotionEvent.ACTION_MOVE:
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800209 if (!mDragging && mCheckForDragging && passedSlop(x, y)) {
210 mCheckForDragging = false;
211 mDragging = true;
212 mLeftMouseButtonReleased = false;
213 startMovingTask(e.getRawX(), e.getRawY());
214 } else if (mDragging && !mLeftMouseButtonReleased) {
Skuhne81c524a2015-08-12 13:34:14 -0700215 if (e.getToolType(e.getActionIndex()) == MotionEvent.TOOL_TYPE_MOUSE &&
216 (e.getButtonState() & MotionEvent.BUTTON_PRIMARY) == 0) {
217 // There is no separate mouse button up call and if the user mixes mouse
218 // button drag actions, we stop dragging once he releases the button.
219 mLeftMouseButtonReleased = true;
220 break;
221 }
Skuhne81c524a2015-08-12 13:34:14 -0700222 }
223 break;
224
225 case MotionEvent.ACTION_UP:
Skuhne81c524a2015-08-12 13:34:14 -0700226 case MotionEvent.ACTION_CANCEL:
Skuhnea5a93ee2015-08-20 15:43:57 -0700227 if (!mDragging) {
228 break;
Skuhne81c524a2015-08-12 13:34:14 -0700229 }
Skuhnea5a93ee2015-08-20 15:43:57 -0700230 // Abort the ongoing dragging.
231 mDragging = false;
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800232 return !mCheckForDragging;
Skuhne81c524a2015-08-12 13:34:14 -0700233 }
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800234 return mDragging || mCheckForDragging;
235 }
236
237 @Override
238 public ArrayList<View> buildTouchDispatchChildList() {
239 mTouchDispatchList.ensureCapacity(3);
240 if (mCaption != null) {
241 mTouchDispatchList.add(mCaption);
242 }
243 if (mContent != null) {
244 mTouchDispatchList.add(mContent);
245 }
246 return mTouchDispatchList;
247 }
248
249 private boolean passedSlop(int x, int y) {
250 return Math.abs(x - mTouchDownX) > mDragSlop || Math.abs(y - mTouchDownY) > mDragSlop;
Skuhne81c524a2015-08-12 13:34:14 -0700251 }
252
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700253 /**
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800254 * The phone window configuration has changed and the caption needs to be updated.
255 * @param show True if the caption should be shown.
Wale Ogunwale2b547c32015-11-18 10:33:22 -0800256 */
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800257 public void onConfigurationChanged(boolean show) {
258 mShow = show;
Skuhnef7b882c2015-08-11 17:18:58 -0700259 updateCaptionVisibility();
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700260 }
261
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700262 @Override
Skuhnef7b882c2015-08-11 17:18:58 -0700263 public void addView(View child, int index, ViewGroup.LayoutParams params) {
Filip Gruszczynski63250652015-11-18 14:43:01 -0800264 if (!(params instanceof MarginLayoutParams)) {
265 throw new IllegalArgumentException(
266 "params " + params + " must subclass MarginLayoutParams");
267 }
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700268 // Make sure that we never get more then one client area in our view.
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700269 if (index >= 2 || getChildCount() >= 2) {
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800270 throw new IllegalStateException("DecorCaptionView can only handle 1 client view");
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700271 }
Filip Gruszczynski63250652015-11-18 14:43:01 -0800272 // To support the overlaying content in the caption, we need to put the content view as the
273 // first child to get the right Z-Ordering.
274 super.addView(child, 0, params);
275 mContent = child;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700276 }
277
Filip Gruszczynski63250652015-11-18 14:43:01 -0800278 @Override
279 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
280 final int captionHeight;
281 if (mCaption.getVisibility() != View.GONE) {
282 measureChildWithMargins(mCaption, widthMeasureSpec, 0, heightMeasureSpec, 0);
283 captionHeight = mCaption.getMeasuredHeight();
284 } else {
285 captionHeight = 0;
286 }
287 if (mContent != null) {
288 if (mOverlayWithAppContent) {
289 measureChildWithMargins(mContent, widthMeasureSpec, 0, heightMeasureSpec, 0);
290 } else {
291 measureChildWithMargins(mContent, widthMeasureSpec, 0, heightMeasureSpec,
292 captionHeight);
293 }
294 }
295
296 setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
297 MeasureSpec.getSize(heightMeasureSpec));
298 }
299
300 @Override
301 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
302 final int captionHeight;
303 if (mCaption.getVisibility() != View.GONE) {
304 mCaption.layout(0, 0, mCaption.getMeasuredWidth(), mCaption.getMeasuredHeight());
305 captionHeight = mCaption.getBottom() - mCaption.getTop();
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800306 mMaximize.getHitRect(mMaximizeRect);
307 mClose.getHitRect(mCloseRect);
Filip Gruszczynski63250652015-11-18 14:43:01 -0800308 } else {
309 captionHeight = 0;
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800310 mMaximizeRect.setEmpty();
311 mCloseRect.setEmpty();
Filip Gruszczynski63250652015-11-18 14:43:01 -0800312 }
313
314 if (mContent != null) {
315 if (mOverlayWithAppContent) {
316 mContent.layout(0, 0, mContent.getMeasuredWidth(), mContent.getMeasuredHeight());
317 } else {
318 mContent.layout(0, captionHeight, mContent.getMeasuredWidth(),
319 captionHeight + mContent.getMeasuredHeight());
320 }
321 }
322 }
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700323 /**
324 * Determine if the workspace is entirely covered by the window.
325 * @return Returns true when the window is filling the entire screen/workspace.
326 **/
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700327 private boolean isFillingScreen() {
328 return (0 != ((getWindowSystemUiVisibility() | getSystemUiVisibility()) &
329 (View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
330 View.SYSTEM_UI_FLAG_IMMERSIVE | View.SYSTEM_UI_FLAG_LOW_PROFILE)));
331 }
332
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700333 /**
Skuhnef7b882c2015-08-11 17:18:58 -0700334 * Updates the visibility of the caption.
335 **/
336 private void updateCaptionVisibility() {
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800337 // Don't show the caption if the window has e.g. entered full screen.
338 boolean invisible = isFillingScreen() || !mShow;
Filip Gruszczynski63250652015-11-18 14:43:01 -0800339 mCaption.setVisibility(invisible ? GONE : VISIBLE);
340 mCaption.setOnTouchListener(this);
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700341 }
Stefan Kuhne1b420572015-08-07 10:50:19 -0700342
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700343 /**
344 * Maximize the window by moving it to the maximized workspace stack.
345 **/
Stefan Kuhne1b420572015-08-07 10:50:19 -0700346 private void maximizeWindow() {
Skuhnece2faa52015-08-11 10:36:38 -0700347 Window.WindowControllerCallback callback = mOwner.getWindowControllerCallback();
Stefan Kuhne1b420572015-08-07 10:50:19 -0700348 if (callback != null) {
349 try {
Wale Ogunwale3797c222015-10-27 14:21:58 -0700350 callback.changeWindowStack(FULLSCREEN_WORKSPACE_STACK_ID);
Stefan Kuhne1b420572015-08-07 10:50:19 -0700351 } catch (RemoteException ex) {
352 Log.e(TAG, "Cannot change task workspace.");
353 }
354 }
355 }
Wale Ogunwale8cc5a742015-11-17 15:41:05 -0800356
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800357 public boolean isCaptionShowing() {
358 return mShow;
Wale Ogunwale8cc5a742015-11-17 15:41:05 -0800359 }
360
Wale Ogunwale62a91d62015-11-18 11:44:10 -0800361 public int getCaptionHeight() {
Filip Gruszczynski63250652015-11-18 14:43:01 -0800362 return (mCaption != null) ? mCaption.getHeight() : 0;
363 }
364
365 public void removeContentView() {
366 if (mContent != null) {
367 removeView(mContent);
368 mContent = null;
369 }
370 }
371
372 public View getCaption() {
373 return mCaption;
374 }
375
376 @Override
377 public LayoutParams generateLayoutParams(AttributeSet attrs) {
378 return new MarginLayoutParams(getContext(), attrs);
379 }
380
381 @Override
382 protected LayoutParams generateDefaultLayoutParams() {
383 return new MarginLayoutParams(MarginLayoutParams.MATCH_PARENT,
384 MarginLayoutParams.MATCH_PARENT);
385 }
386
387 @Override
388 protected LayoutParams generateLayoutParams(LayoutParams p) {
389 return new MarginLayoutParams(p);
390 }
391
392 @Override
393 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
394 return p instanceof MarginLayoutParams;
Wale Ogunwale8cc5a742015-11-17 15:41:05 -0800395 }
Filip Gruszczynskia33bdf32015-11-19 18:22:16 -0800396
397 @Override
398 public boolean onDown(MotionEvent e) {
399 return false;
400 }
401
402 @Override
403 public void onShowPress(MotionEvent e) {
404
405 }
406
407 @Override
408 public boolean onSingleTapUp(MotionEvent e) {
409 if (mClickTarget == mMaximize) {
410 maximizeWindow();
411 } else if (mClickTarget == mClose) {
412 mOwner.dispatchOnWindowDismissed(true /*finishTask*/);
413 }
414 return true;
415 }
416
417 @Override
418 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
419 return false;
420 }
421
422 @Override
423 public void onLongPress(MotionEvent e) {
424
425 }
426
427 @Override
428 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
429 return false;
430 }
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700431}