blob: 0e22553aa065dc6a50653c72a25ccf62a27ee57f [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
Skuhnea5a93ee2015-08-20 15:43:57 -070019import android.app.ActivityThread;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070020import android.content.Context;
Skuhnece2faa52015-08-11 10:36:38 -070021import android.graphics.Rect;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070022import android.os.RemoteException;
23import android.util.AttributeSet;
Skuhne81c524a2015-08-12 13:34:14 -070024import android.view.MotionEvent;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070025import android.view.View;
Skuhnef7b882c2015-08-11 17:18:58 -070026import android.widget.LinearLayout;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070027import android.view.ViewGroup;
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -070028import android.view.ViewOutlineProvider;
29import android.view.Window;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070030import android.util.Log;
31import android.util.TypedValue;
32
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070033import com.android.internal.R;
34import com.android.internal.policy.PhoneWindow;
35
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070036/**
37 * This class represents the special screen elements to control a window on free form
38 * environment. All thse screen elements are added in the "non client area" which is the area of
39 * the window which is handled by the OS and not the application.
40 * As such this class handles the following things:
41 * <ul>
42 * <li>The caption, containing the system buttons like maximize, close and such as well as
43 * allowing the user to drag the window around.</li>
44 * <li>The shadow - which is changing dependent on the window focus.</li>
45 * <li>The border around the client area (if there is one).</li>
46 * <li>The resize handles which allow to resize the window.</li>
47 * </ul>
48 * After creating the view, the function
49 * {@link #setPhoneWindow(PhoneWindow owner, boolean windowHasShadow)} needs to be called to make
50 * the connection to it's owning PhoneWindow.
51 * Note: At this time the application can change various attributes of the DecorView which
52 * will break things (in settle/unexpected ways):
53 * <ul>
54 * <li>setElevation</li>
55 * <li>setOutlineProvider</li>
56 * <li>setSurfaceFormat</li>
57 * <li>..</li>
58 * </ul>
59 * This will be mitigated once b/22527834 will be addressed.
60 */
Skuhnef7b882c2015-08-11 17:18:58 -070061public class NonClientDecorView extends LinearLayout implements View.OnClickListener {
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070062 private final static String TAG = "NonClientDecorView";
63 // The height of a window which has focus in DIP.
64 private final int DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP = 20;
65 // The height of a window which has not in DIP.
66 private final int DECOR_SHADOW_UNFOCUSED_HEIGHT_IN_DIP = 5;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070067 private PhoneWindow mOwner = null;
Skuhne81c524a2015-08-12 13:34:14 -070068 private boolean mWindowHasShadow = false;
69 private boolean mShowDecor = false;
70
71 // True if the window is being dragged.
72 private boolean mDragging = false;
73
74 // The bounds of the window and the absolute mouse pointer coordinates from before we started to
75 // drag the window. They will be used to determine the next window position.
76 private final Rect mWindowOriginalBounds = new Rect();
77 private float mStartDragX;
78 private float mStartDragY;
79 // True when the left mouse button got released while dragging.
80 private boolean mLeftMouseButtonReleased;
81
Skuhnea5a93ee2015-08-20 15:43:57 -070082 private static final int NONE = 0;
83 private static final int LEFT = 1;
84 private static final int RIGHT = 2;
85 private static final int TOP = 4;
86 private static final int BOTTOM = 8;
87 private static final int TOP_LEFT = TOP | LEFT;
88 private static final int TOP_RIGHT = TOP | RIGHT;
89 private static final int BOTTOM_LEFT = BOTTOM | LEFT;
90 private static final int BOTTOM_RIGHT = BOTTOM | RIGHT;
91 private int mSizeCorner = NONE;
92
Skuhne81c524a2015-08-12 13:34:14 -070093 // Avoiding re-creation of Rect's by keeping a temporary window drag bound.
94 private final Rect mWindowDragBounds = new Rect();
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070095
Skuhnea5a93ee2015-08-20 15:43:57 -070096 // True while the task is resizing itself to avoid overlapping resize operations.
97 private boolean mTaskResizingInProgress = false;
98
99 // True if this window is resizable (which is currently only true when the decor is shown).
100 public boolean mResizable = false;
101
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700102 // The current focus state of the window for updating the window elevation.
Skuhne81c524a2015-08-12 13:34:14 -0700103 private boolean mWindowHasFocus = true;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700104
105 // Cludge to address b/22668382: Set the shadow size to the maximum so that the layer
106 // size calculation takes the shadow size into account. We set the elevation currently
107 // to max until the first layout command has been executed.
Skuhne81c524a2015-08-12 13:34:14 -0700108 private boolean mAllowUpdateElevation = false;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700109
110 public NonClientDecorView(Context context) {
111 super(context);
112 }
113
114 public NonClientDecorView(Context context, AttributeSet attrs) {
115 super(context, attrs);
116 }
117
118 public NonClientDecorView(Context context, AttributeSet attrs, int defStyle) {
119 super(context, attrs, defStyle);
120 }
121
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700122 public void setPhoneWindow(PhoneWindow owner, boolean showDecor, boolean windowHasShadow) {
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700123 mOwner = owner;
124 mWindowHasShadow = windowHasShadow;
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700125 mShowDecor = showDecor;
Skuhnef7b882c2015-08-11 17:18:58 -0700126 updateCaptionVisibility();
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700127 if (mWindowHasShadow) {
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700128 initializeElevation();
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700129 }
130 // By changing the outline provider to BOUNDS, the window can remove its
131 // background without removing the shadow.
132 mOwner.getDecorView().setOutlineProvider(ViewOutlineProvider.BOUNDS);
133 findViewById(R.id.maximize_window).setOnClickListener(this);
134 findViewById(R.id.close_window).setOnClickListener(this);
135 }
136
Skuhne81c524a2015-08-12 13:34:14 -0700137 @Override
138 public boolean onTouchEvent(MotionEvent e) {
139 // Note: There are no mixed events. When a new device gets used (e.g. 1. Mouse, 2. touch)
140 // the old input device events get cancelled first. So no need to remember the kind of
141 // input device we are listening to.
142 switch (e.getActionMasked()) {
143 case MotionEvent.ACTION_DOWN:
Skuhnea635a262015-08-26 15:45:58 -0700144 if (!mShowDecor) {
145 // When there is no decor we should not react to anything.
146 return false;
147 }
Skuhne81c524a2015-08-12 13:34:14 -0700148 // A drag action is started if we aren't dragging already and the starting event is
149 // either a left mouse button or any other input device.
150 if (!mDragging &&
151 (e.getToolType(e.getActionIndex()) != MotionEvent.TOOL_TYPE_MOUSE ||
152 (e.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0)) {
153 mDragging = true;
154 mWindowOriginalBounds.set(getActivityBounds());
155 mLeftMouseButtonReleased = false;
156 mStartDragX = e.getRawX();
157 mStartDragY = e.getRawY();
Skuhnea5a93ee2015-08-20 15:43:57 -0700158 // Determine if this is a resizing user action.
159 final int x = (int) (e.getX());
160 final int y = (int) (e.getY());
161 mSizeCorner = (x < 0 ? LEFT : (x >= getWidth() ? RIGHT : NONE)) |
162 (y < 0 ? TOP : (y >= getHeight() ? BOTTOM : NONE));
163 if (mSizeCorner != 0) {
164 // Suppress any configuration changes for now.
165 ActivityThread.currentActivityThread().suppressConfigurationChanges(true);
166 }
Skuhne81c524a2015-08-12 13:34:14 -0700167 }
168 break;
169
170 case MotionEvent.ACTION_MOVE:
171 if (mDragging && !mLeftMouseButtonReleased) {
172 if (e.getToolType(e.getActionIndex()) == MotionEvent.TOOL_TYPE_MOUSE &&
173 (e.getButtonState() & MotionEvent.BUTTON_PRIMARY) == 0) {
174 // There is no separate mouse button up call and if the user mixes mouse
175 // button drag actions, we stop dragging once he releases the button.
176 mLeftMouseButtonReleased = true;
177 break;
178 }
Skuhnea5a93ee2015-08-20 15:43:57 -0700179 if (mSizeCorner != NONE) {
180 // Avoid overlapping resizing operations.
181 if (mTaskResizingInProgress) {
182 break;
183 }
184 mTaskResizingInProgress = true;
185 // This is a resizing operation.
186 final int deltaX = Math.round(e.getRawX() - mStartDragX);
187 final int deltaY = Math.round(e.getRawY() - mStartDragY);
188 final int minSizeX = (int)(dipToPx(96));
189 final int minSizeY = (int)(dipToPx(64));
190 int left = mWindowOriginalBounds.left;
191 int top = mWindowOriginalBounds.top;
192 int right = mWindowOriginalBounds.right;
193 int bottom = mWindowOriginalBounds.bottom;
194 if ((mSizeCorner & LEFT) != 0) {
195 left = Math.min(left + deltaX, right - minSizeX);
196 }
197 if ((mSizeCorner & TOP) != 0) {
198 top = Math.min(top + deltaY, bottom - minSizeY);
199 }
200 if ((mSizeCorner & RIGHT) != 0) {
201 right = Math.max(left + minSizeX, right + deltaX);
202 }
203 if ((mSizeCorner & BOTTOM) != 0) {
204 bottom = Math.max(top + minSizeY, bottom + deltaY);
205 }
206 mWindowDragBounds.set(left, top, right, bottom);
207 setActivityBounds(mWindowDragBounds);
208 mTaskResizingInProgress = false;
209 } else {
210 // This is a moving operation.
211 mWindowDragBounds.set(mWindowOriginalBounds);
212 mWindowDragBounds.offset(Math.round(e.getRawX() - mStartDragX),
213 Math.round(e.getRawY() - mStartDragY));
214 setActivityBounds(mWindowDragBounds);
215 }
Skuhne81c524a2015-08-12 13:34:14 -0700216 }
217 break;
218
219 case MotionEvent.ACTION_UP:
Skuhnea5a93ee2015-08-20 15:43:57 -0700220 if (!mDragging) {
221 break;
222 }
223 // Finsih the dragging now.
224 mDragging = false;
225 if (mSizeCorner == NONE) {
Skuhne81c524a2015-08-12 13:34:14 -0700226 return true;
227 }
Skuhnea5a93ee2015-08-20 15:43:57 -0700228
229 // Allow configuration changes again.
230 ActivityThread.currentActivityThread().suppressConfigurationChanges(false);
231 // Set the same bounds once more - which might trigger a configuration change now.
232 setActivityBounds(mWindowDragBounds);
233 // Tell the DecorView that we are done with out event interception by
234 // returning false.
235 return false;
Skuhne81c524a2015-08-12 13:34:14 -0700236
237 case MotionEvent.ACTION_CANCEL:
Skuhnea5a93ee2015-08-20 15:43:57 -0700238 if (!mDragging) {
239 break;
Skuhne81c524a2015-08-12 13:34:14 -0700240 }
Skuhnea5a93ee2015-08-20 15:43:57 -0700241 // Abort the ongoing dragging.
242 mDragging = false;
243 // Restore the previous bounds.
244 setActivityBounds(mWindowOriginalBounds);
245 if (mSizeCorner != NONE) {
246 // ALlow configuration changes again.
247 ActivityThread.currentActivityThread().suppressConfigurationChanges(false);
248 // Tell the DecorView that we are done with out event interception by
249 // returning false.
250 return false;
251 }
252 return true;
Skuhne81c524a2015-08-12 13:34:14 -0700253 }
254 return mDragging;
255 }
256
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700257 /**
258 * The phone window configuration has changed and the decor needs to be updated.
259 * @param showDecor True if the decor should be shown.
260 * @param windowHasShadow True when the window should show a shadow.
261 **/
262 public void phoneWindowUpdated(boolean showDecor, boolean windowHasShadow) {
263 mShowDecor = showDecor;
Skuhnef7b882c2015-08-11 17:18:58 -0700264 updateCaptionVisibility();
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700265 if (windowHasShadow != mWindowHasShadow) {
266 mWindowHasShadow = windowHasShadow;
267 initializeElevation();
268 }
269 }
270
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700271 @Override
272 public void onClick(View view) {
273 if (view.getId() == R.id.maximize_window) {
Stefan Kuhne1b420572015-08-07 10:50:19 -0700274 maximizeWindow();
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700275 } else if (view.getId() == R.id.close_window) {
Stefan Kuhne1b420572015-08-07 10:50:19 -0700276 mOwner.dispatchOnWindowDismissed(true /*finishTask*/);
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700277 }
278 }
279
280 @Override
281 public void onWindowFocusChanged(boolean hasWindowFocus) {
282 mWindowHasFocus = hasWindowFocus;
283 updateElevation();
284 super.onWindowFocusChanged(hasWindowFocus);
285 }
286
287 @Override
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700288 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700289 // If the application changed its SystemUI metrics, we might also have to adapt
290 // our shadow elevation.
291 updateElevation();
292 mAllowUpdateElevation = true;
293
Skuhnef7b882c2015-08-11 17:18:58 -0700294 super.onLayout(changed, left, top, right, bottom);
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700295 }
296
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700297 @Override
Skuhnef7b882c2015-08-11 17:18:58 -0700298 public void addView(View child, int index, ViewGroup.LayoutParams params) {
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700299 // Make sure that we never get more then one client area in our view.
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700300 if (index >= 2 || getChildCount() >= 2) {
301 throw new IllegalStateException("NonClientDecorView can only handle 1 client view");
302 }
303 super.addView(child, index, params);
304 }
305
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700306 /**
307 * Determine if the workspace is entirely covered by the window.
308 * @return Returns true when the window is filling the entire screen/workspace.
309 **/
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700310 private boolean isFillingScreen() {
311 return (0 != ((getWindowSystemUiVisibility() | getSystemUiVisibility()) &
312 (View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
313 View.SYSTEM_UI_FLAG_IMMERSIVE | View.SYSTEM_UI_FLAG_LOW_PROFILE)));
314 }
315
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700316 /**
Skuhnef7b882c2015-08-11 17:18:58 -0700317 * Updates the visibility of the caption.
318 **/
319 private void updateCaptionVisibility() {
320 // Don't show the decor if the window has e.g. entered full screen.
321 boolean invisible = isFillingScreen() || !mShowDecor;
322 View caption = getChildAt(0);
323 caption.setVisibility(invisible ? GONE : VISIBLE);
Skuhnea5a93ee2015-08-20 15:43:57 -0700324 mResizable = !invisible;
Skuhnef7b882c2015-08-11 17:18:58 -0700325 }
326
327 /**
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700328 * The elevation gets set for the first time and the framework needs to be informed that
329 * the surface layer gets created with the shadow size in mind.
330 **/
331 private void initializeElevation() {
332 // TODO(skuhne): Call setMaxElevation here accordingly after b/22668382 got fixed.
333 mAllowUpdateElevation = false;
334 if (mWindowHasShadow) {
335 updateElevation();
336 } else {
337 mOwner.setElevation(0);
338 }
339 }
340
341 /**
342 * The shadow height gets controlled by the focus to visualize highlighted windows.
343 * Note: This will overwrite application elevation properties.
344 * Note: Windows which have (temporarily) changed their attributes to cover the SystemUI
345 * will get no shadow as they are expected to be "full screen".
346 **/
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700347 private void updateElevation() {
348 float elevation = 0;
349 if (mWindowHasShadow) {
350 boolean fill = isFillingScreen();
351 elevation = fill ? 0 :
352 (mWindowHasFocus ? DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP :
353 DECOR_SHADOW_UNFOCUSED_HEIGHT_IN_DIP);
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700354 // TODO(skuhne): Remove this if clause once b/22668382 got fixed.
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700355 if (!mAllowUpdateElevation && !fill) {
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700356 elevation = DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP;
357 }
358 // Convert the DP elevation into physical pixels.
359 elevation = dipToPx(elevation);
360 }
361 // Don't change the elevation if it didn't change since it can require some time.
362 if (mOwner.getDecorView().getElevation() != elevation) {
363 mOwner.setElevation(elevation);
364 }
365 }
366
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700367 /**
368 * Converts a DIP measure into physical pixels.
369 * @param dip The dip value.
370 * @return Returns the number of pixels.
371 */
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700372 private float dipToPx(float dip) {
373 return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip,
374 getResources().getDisplayMetrics());
375 }
Stefan Kuhne1b420572015-08-07 10:50:19 -0700376
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700377 /**
378 * Maximize the window by moving it to the maximized workspace stack.
379 **/
Stefan Kuhne1b420572015-08-07 10:50:19 -0700380 private void maximizeWindow() {
Skuhnece2faa52015-08-11 10:36:38 -0700381 Window.WindowControllerCallback callback = mOwner.getWindowControllerCallback();
Stefan Kuhne1b420572015-08-07 10:50:19 -0700382 if (callback != null) {
383 try {
384 callback.changeWindowStack(
385 android.app.ActivityManager.FULLSCREEN_WORKSPACE_STACK_ID);
386 } catch (RemoteException ex) {
387 Log.e(TAG, "Cannot change task workspace.");
388 }
389 }
390 }
Skuhnece2faa52015-08-11 10:36:38 -0700391
392 /**
393 * Returns the bounds of this activity.
394 * @return Returns bounds of the activity. It will return null if either the window is
395 * fullscreen or the bounds could not be retrieved.
396 */
397 private Rect getActivityBounds() {
398 Window.WindowControllerCallback callback = mOwner.getWindowControllerCallback();
399 if (callback != null) {
400 try {
401 return callback.getActivityBounds();
402 } catch (RemoteException ex) {
403 Log.e(TAG, "Failed to get the activity bounds.");
404 }
405 }
406 return null;
407 }
408
409 /**
410 * Sets the bounds of this Activity on the stack.
411 * @param newBounds The bounds of the activity. Passing null is not allowed.
412 */
413 private void setActivityBounds(Rect newBounds) {
414 if (newBounds == null) {
415 Log.e(TAG, "Failed to set null bounds to the activity.");
416 return;
417 }
418 Window.WindowControllerCallback callback = mOwner.getWindowControllerCallback();
419 if (callback != null) {
420 try {
421 callback.setActivityBounds(newBounds);
422 } catch (RemoteException ex) {
423 Log.e(TAG, "Failed to set the activity bounds.");
424 }
425 }
426 }
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700427}