blob: 372d934dde057cdaf8be43cc8f74b0d1b0930a68 [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;
Skuhnece2faa52015-08-11 10:36:38 -070020import android.graphics.Rect;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070021import android.os.RemoteException;
22import android.util.AttributeSet;
Skuhne81c524a2015-08-12 13:34:14 -070023import android.view.MotionEvent;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070024import android.view.View;
Skuhnef7b882c2015-08-11 17:18:58 -070025import android.widget.LinearLayout;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070026import android.view.ViewGroup;
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -070027import android.view.ViewOutlineProvider;
28import android.view.Window;
29import android.view.WindowInsets;
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
82 // Avoiding re-creation of Rect's by keeping a temporary window drag bound.
83 private final Rect mWindowDragBounds = new Rect();
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070084
85 // The current focus state of the window for updating the window elevation.
Skuhne81c524a2015-08-12 13:34:14 -070086 private boolean mWindowHasFocus = true;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070087
88 // Cludge to address b/22668382: Set the shadow size to the maximum so that the layer
89 // size calculation takes the shadow size into account. We set the elevation currently
90 // to max until the first layout command has been executed.
Skuhne81c524a2015-08-12 13:34:14 -070091 private boolean mAllowUpdateElevation = false;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070092
93 public NonClientDecorView(Context context) {
94 super(context);
95 }
96
97 public NonClientDecorView(Context context, AttributeSet attrs) {
98 super(context, attrs);
99 }
100
101 public NonClientDecorView(Context context, AttributeSet attrs, int defStyle) {
102 super(context, attrs, defStyle);
103 }
104
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700105 public void setPhoneWindow(PhoneWindow owner, boolean showDecor, boolean windowHasShadow) {
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700106 mOwner = owner;
107 mWindowHasShadow = windowHasShadow;
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700108 mShowDecor = showDecor;
Skuhnef7b882c2015-08-11 17:18:58 -0700109 updateCaptionVisibility();
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700110 if (mWindowHasShadow) {
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700111 initializeElevation();
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700112 }
113 // By changing the outline provider to BOUNDS, the window can remove its
114 // background without removing the shadow.
115 mOwner.getDecorView().setOutlineProvider(ViewOutlineProvider.BOUNDS);
116 findViewById(R.id.maximize_window).setOnClickListener(this);
117 findViewById(R.id.close_window).setOnClickListener(this);
118 }
119
Skuhne81c524a2015-08-12 13:34:14 -0700120 @Override
121 public boolean onTouchEvent(MotionEvent e) {
122 // Note: There are no mixed events. When a new device gets used (e.g. 1. Mouse, 2. touch)
123 // the old input device events get cancelled first. So no need to remember the kind of
124 // input device we are listening to.
125 switch (e.getActionMasked()) {
126 case MotionEvent.ACTION_DOWN:
Skuhnea635a262015-08-26 15:45:58 -0700127 if (!mShowDecor) {
128 // When there is no decor we should not react to anything.
129 return false;
130 }
Skuhne81c524a2015-08-12 13:34:14 -0700131 // A drag action is started if we aren't dragging already and the starting event is
132 // either a left mouse button or any other input device.
133 if (!mDragging &&
134 (e.getToolType(e.getActionIndex()) != MotionEvent.TOOL_TYPE_MOUSE ||
135 (e.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0)) {
136 mDragging = true;
137 mWindowOriginalBounds.set(getActivityBounds());
138 mLeftMouseButtonReleased = false;
139 mStartDragX = e.getRawX();
140 mStartDragY = e.getRawY();
141 }
142 break;
143
144 case MotionEvent.ACTION_MOVE:
145 if (mDragging && !mLeftMouseButtonReleased) {
146 if (e.getToolType(e.getActionIndex()) == MotionEvent.TOOL_TYPE_MOUSE &&
147 (e.getButtonState() & MotionEvent.BUTTON_PRIMARY) == 0) {
148 // There is no separate mouse button up call and if the user mixes mouse
149 // button drag actions, we stop dragging once he releases the button.
150 mLeftMouseButtonReleased = true;
151 break;
152 }
153 mWindowDragBounds.set(mWindowOriginalBounds);
154 mWindowDragBounds.offset(Math.round(e.getRawX() - mStartDragX),
155 Math.round(e.getRawY() - mStartDragY));
156 setActivityBounds(mWindowDragBounds);
157 }
158 break;
159
160 case MotionEvent.ACTION_UP:
161 if (mDragging) {
162 // Since the window is already where it should be we don't have to do anything
163 // special at this time.
164 mDragging = false;
165 return true;
166 }
167 break;
168
169 case MotionEvent.ACTION_CANCEL:
170 if (mDragging) {
171 mDragging = false;
172 setActivityBounds(mWindowOriginalBounds);
173 return true;
174 }
175 break;
176 }
177 return mDragging;
178 }
179
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700180 /**
181 * The phone window configuration has changed and the decor needs to be updated.
182 * @param showDecor True if the decor should be shown.
183 * @param windowHasShadow True when the window should show a shadow.
184 **/
185 public void phoneWindowUpdated(boolean showDecor, boolean windowHasShadow) {
186 mShowDecor = showDecor;
Skuhnef7b882c2015-08-11 17:18:58 -0700187 updateCaptionVisibility();
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700188 if (windowHasShadow != mWindowHasShadow) {
189 mWindowHasShadow = windowHasShadow;
190 initializeElevation();
191 }
192 }
193
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700194 @Override
195 public void onClick(View view) {
196 if (view.getId() == R.id.maximize_window) {
Stefan Kuhne1b420572015-08-07 10:50:19 -0700197 maximizeWindow();
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700198 } else if (view.getId() == R.id.close_window) {
Stefan Kuhne1b420572015-08-07 10:50:19 -0700199 mOwner.dispatchOnWindowDismissed(true /*finishTask*/);
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700200 }
201 }
202
203 @Override
204 public void onWindowFocusChanged(boolean hasWindowFocus) {
205 mWindowHasFocus = hasWindowFocus;
206 updateElevation();
207 super.onWindowFocusChanged(hasWindowFocus);
208 }
209
210 @Override
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700211 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700212 // If the application changed its SystemUI metrics, we might also have to adapt
213 // our shadow elevation.
214 updateElevation();
215 mAllowUpdateElevation = true;
216
Skuhnef7b882c2015-08-11 17:18:58 -0700217 super.onLayout(changed, left, top, right, bottom);
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700218 }
219
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700220 @Override
Skuhnef7b882c2015-08-11 17:18:58 -0700221 public void addView(View child, int index, ViewGroup.LayoutParams params) {
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700222 // Make sure that we never get more then one client area in our view.
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700223 if (index >= 2 || getChildCount() >= 2) {
224 throw new IllegalStateException("NonClientDecorView can only handle 1 client view");
225 }
226 super.addView(child, index, params);
227 }
228
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700229 /**
230 * Determine if the workspace is entirely covered by the window.
231 * @return Returns true when the window is filling the entire screen/workspace.
232 **/
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700233 private boolean isFillingScreen() {
234 return (0 != ((getWindowSystemUiVisibility() | getSystemUiVisibility()) &
235 (View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
236 View.SYSTEM_UI_FLAG_IMMERSIVE | View.SYSTEM_UI_FLAG_LOW_PROFILE)));
237 }
238
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700239 /**
Skuhnef7b882c2015-08-11 17:18:58 -0700240 * Updates the visibility of the caption.
241 **/
242 private void updateCaptionVisibility() {
243 // Don't show the decor if the window has e.g. entered full screen.
244 boolean invisible = isFillingScreen() || !mShowDecor;
245 View caption = getChildAt(0);
246 caption.setVisibility(invisible ? GONE : VISIBLE);
247 }
248
249 /**
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700250 * The elevation gets set for the first time and the framework needs to be informed that
251 * the surface layer gets created with the shadow size in mind.
252 **/
253 private void initializeElevation() {
254 // TODO(skuhne): Call setMaxElevation here accordingly after b/22668382 got fixed.
255 mAllowUpdateElevation = false;
256 if (mWindowHasShadow) {
257 updateElevation();
258 } else {
259 mOwner.setElevation(0);
260 }
261 }
262
263 /**
264 * The shadow height gets controlled by the focus to visualize highlighted windows.
265 * Note: This will overwrite application elevation properties.
266 * Note: Windows which have (temporarily) changed their attributes to cover the SystemUI
267 * will get no shadow as they are expected to be "full screen".
268 **/
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700269 private void updateElevation() {
270 float elevation = 0;
271 if (mWindowHasShadow) {
272 boolean fill = isFillingScreen();
273 elevation = fill ? 0 :
274 (mWindowHasFocus ? DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP :
275 DECOR_SHADOW_UNFOCUSED_HEIGHT_IN_DIP);
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700276 // TODO(skuhne): Remove this if clause once b/22668382 got fixed.
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700277 if (!mAllowUpdateElevation && !fill) {
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700278 elevation = DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP;
279 }
280 // Convert the DP elevation into physical pixels.
281 elevation = dipToPx(elevation);
282 }
283 // Don't change the elevation if it didn't change since it can require some time.
284 if (mOwner.getDecorView().getElevation() != elevation) {
285 mOwner.setElevation(elevation);
286 }
287 }
288
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700289 /**
290 * Converts a DIP measure into physical pixels.
291 * @param dip The dip value.
292 * @return Returns the number of pixels.
293 */
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700294 private float dipToPx(float dip) {
295 return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip,
296 getResources().getDisplayMetrics());
297 }
Stefan Kuhne1b420572015-08-07 10:50:19 -0700298
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700299 /**
300 * Maximize the window by moving it to the maximized workspace stack.
301 **/
Stefan Kuhne1b420572015-08-07 10:50:19 -0700302 private void maximizeWindow() {
Skuhnece2faa52015-08-11 10:36:38 -0700303 Window.WindowControllerCallback callback = mOwner.getWindowControllerCallback();
Stefan Kuhne1b420572015-08-07 10:50:19 -0700304 if (callback != null) {
305 try {
306 callback.changeWindowStack(
307 android.app.ActivityManager.FULLSCREEN_WORKSPACE_STACK_ID);
308 } catch (RemoteException ex) {
309 Log.e(TAG, "Cannot change task workspace.");
310 }
311 }
312 }
Skuhnece2faa52015-08-11 10:36:38 -0700313
314 /**
315 * Returns the bounds of this activity.
316 * @return Returns bounds of the activity. It will return null if either the window is
317 * fullscreen or the bounds could not be retrieved.
318 */
319 private Rect getActivityBounds() {
320 Window.WindowControllerCallback callback = mOwner.getWindowControllerCallback();
321 if (callback != null) {
322 try {
323 return callback.getActivityBounds();
324 } catch (RemoteException ex) {
325 Log.e(TAG, "Failed to get the activity bounds.");
326 }
327 }
328 return null;
329 }
330
331 /**
332 * Sets the bounds of this Activity on the stack.
333 * @param newBounds The bounds of the activity. Passing null is not allowed.
334 */
335 private void setActivityBounds(Rect newBounds) {
336 if (newBounds == null) {
337 Log.e(TAG, "Failed to set null bounds to the activity.");
338 return;
339 }
340 Window.WindowControllerCallback callback = mOwner.getWindowControllerCallback();
341 if (callback != null) {
342 try {
343 callback.setActivityBounds(newBounds);
344 } catch (RemoteException ex) {
345 Log.e(TAG, "Failed to set the activity bounds.");
346 }
347 }
348 }
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700349}