blob: dd5c5d7d0b6cce8421a716c823a804d4d8906cff [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;
23import android.view.View;
24import android.view.ViewGroup;
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -070025import android.view.ViewOutlineProvider;
26import android.view.Window;
27import android.view.WindowInsets;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070028import android.util.Log;
29import android.util.TypedValue;
30
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070031import com.android.internal.R;
32import com.android.internal.policy.PhoneWindow;
33
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070034/**
35 * This class represents the special screen elements to control a window on free form
36 * environment. All thse screen elements are added in the "non client area" which is the area of
37 * the window which is handled by the OS and not the application.
38 * As such this class handles the following things:
39 * <ul>
40 * <li>The caption, containing the system buttons like maximize, close and such as well as
41 * allowing the user to drag the window around.</li>
42 * <li>The shadow - which is changing dependent on the window focus.</li>
43 * <li>The border around the client area (if there is one).</li>
44 * <li>The resize handles which allow to resize the window.</li>
45 * </ul>
46 * After creating the view, the function
47 * {@link #setPhoneWindow(PhoneWindow owner, boolean windowHasShadow)} needs to be called to make
48 * the connection to it's owning PhoneWindow.
49 * Note: At this time the application can change various attributes of the DecorView which
50 * will break things (in settle/unexpected ways):
51 * <ul>
52 * <li>setElevation</li>
53 * <li>setOutlineProvider</li>
54 * <li>setSurfaceFormat</li>
55 * <li>..</li>
56 * </ul>
57 * This will be mitigated once b/22527834 will be addressed.
58 */
59public class NonClientDecorView extends ViewGroup implements View.OnClickListener {
60 private final static String TAG = "NonClientDecorView";
61 // The height of a window which has focus in DIP.
62 private final int DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP = 20;
63 // The height of a window which has not in DIP.
64 private final int DECOR_SHADOW_UNFOCUSED_HEIGHT_IN_DIP = 5;
65
66 private PhoneWindow mOwner = null;
67 boolean mWindowHasShadow = false;
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -070068 boolean mShowDecor = false;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070069
70 // The current focus state of the window for updating the window elevation.
71 boolean mWindowHasFocus = true;
72
73 // Cludge to address b/22668382: Set the shadow size to the maximum so that the layer
74 // size calculation takes the shadow size into account. We set the elevation currently
75 // to max until the first layout command has been executed.
76 boolean mAllowUpdateElevation = false;
77
78 public NonClientDecorView(Context context) {
79 super(context);
80 }
81
82 public NonClientDecorView(Context context, AttributeSet attrs) {
83 super(context, attrs);
84 }
85
86 public NonClientDecorView(Context context, AttributeSet attrs, int defStyle) {
87 super(context, attrs, defStyle);
88 }
89
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -070090 public void setPhoneWindow(PhoneWindow owner, boolean showDecor, boolean windowHasShadow) {
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070091 mOwner = owner;
92 mWindowHasShadow = windowHasShadow;
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -070093 mShowDecor = showDecor;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070094 if (mWindowHasShadow) {
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -070095 initializeElevation();
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070096 }
97 // By changing the outline provider to BOUNDS, the window can remove its
98 // background without removing the shadow.
99 mOwner.getDecorView().setOutlineProvider(ViewOutlineProvider.BOUNDS);
100 findViewById(R.id.maximize_window).setOnClickListener(this);
101 findViewById(R.id.close_window).setOnClickListener(this);
102 }
103
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700104 /**
105 * The phone window configuration has changed and the decor needs to be updated.
106 * @param showDecor True if the decor should be shown.
107 * @param windowHasShadow True when the window should show a shadow.
108 **/
109 public void phoneWindowUpdated(boolean showDecor, boolean windowHasShadow) {
110 mShowDecor = showDecor;
111 if (windowHasShadow != mWindowHasShadow) {
112 mWindowHasShadow = windowHasShadow;
113 initializeElevation();
114 }
115 }
116
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700117 @Override
118 public void onClick(View view) {
119 if (view.getId() == R.id.maximize_window) {
Stefan Kuhne1b420572015-08-07 10:50:19 -0700120 maximizeWindow();
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700121 } else if (view.getId() == R.id.close_window) {
Stefan Kuhne1b420572015-08-07 10:50:19 -0700122 mOwner.dispatchOnWindowDismissed(true /*finishTask*/);
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700123 }
124 }
125
126 @Override
127 public void onWindowFocusChanged(boolean hasWindowFocus) {
128 mWindowHasFocus = hasWindowFocus;
129 updateElevation();
130 super.onWindowFocusChanged(hasWindowFocus);
131 }
132
133 @Override
134 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
135 measureChildren(widthMeasureSpec, heightMeasureSpec);
136 final int width = MeasureSpec.getSize(widthMeasureSpec);
137 final int height = MeasureSpec.getSize(heightMeasureSpec);
138 setMeasuredDimension(width, height);
139 }
140
141 @Override
142 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
143 // The system inset needs only to be applied to the caption. The client area of
144 // the window will automatically be adjusted by the the DecorView.
145 WindowInsets insets = getRootWindowInsets();
146 int systemMargin = insets.getSystemWindowInsetTop();
147
148 final int leftPos = getPaddingLeft();
149 final int rightPos = right - left - getPaddingRight();
150 final int topPos = getPaddingTop();
151 final int bottomPos = bottom - top - getPaddingBottom();
152
153 // On top we have the caption which has to fill left to right with a fixed height.
154 final int width = rightPos - leftPos;
155 final View caption = getChildAt(0);
156
157 // If the application changed its SystemUI metrics, we might also have to adapt
158 // our shadow elevation.
159 updateElevation();
160 mAllowUpdateElevation = true;
161
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700162 // Don't show the decor if the window has e.g. entered full screen.
163 final int captionHeight =
164 (isFillingScreen() || !mShowDecor) ? 0 : caption.getMeasuredHeight();
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700165 caption.layout(leftPos, topPos + systemMargin, leftPos + width,
166 topPos + systemMargin + captionHeight);
167
168 // Note: We should never have more then 1 additional item in here.
169 if (getChildCount() > 1) {
170 getChildAt(1).layout(leftPos, topPos + captionHeight, leftPos + width, bottomPos);
171 }
172 }
173
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700174 @Override
175 public void addView(View child, int index, LayoutParams params) {
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700176 // Make sure that we never get more then one client area in our view.
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700177 if (index >= 2 || getChildCount() >= 2) {
178 throw new IllegalStateException("NonClientDecorView can only handle 1 client view");
179 }
180 super.addView(child, index, params);
181 }
182
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700183 /**
184 * Determine if the workspace is entirely covered by the window.
185 * @return Returns true when the window is filling the entire screen/workspace.
186 **/
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700187 private boolean isFillingScreen() {
188 return (0 != ((getWindowSystemUiVisibility() | getSystemUiVisibility()) &
189 (View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
190 View.SYSTEM_UI_FLAG_IMMERSIVE | View.SYSTEM_UI_FLAG_LOW_PROFILE)));
191 }
192
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700193 /**
194 * The elevation gets set for the first time and the framework needs to be informed that
195 * the surface layer gets created with the shadow size in mind.
196 **/
197 private void initializeElevation() {
198 // TODO(skuhne): Call setMaxElevation here accordingly after b/22668382 got fixed.
199 mAllowUpdateElevation = false;
200 if (mWindowHasShadow) {
201 updateElevation();
202 } else {
203 mOwner.setElevation(0);
204 }
205 }
206
207 /**
208 * The shadow height gets controlled by the focus to visualize highlighted windows.
209 * Note: This will overwrite application elevation properties.
210 * Note: Windows which have (temporarily) changed their attributes to cover the SystemUI
211 * will get no shadow as they are expected to be "full screen".
212 **/
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700213 private void updateElevation() {
214 float elevation = 0;
215 if (mWindowHasShadow) {
216 boolean fill = isFillingScreen();
217 elevation = fill ? 0 :
218 (mWindowHasFocus ? DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP :
219 DECOR_SHADOW_UNFOCUSED_HEIGHT_IN_DIP);
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700220 // TODO(skuhne): Remove this if clause once b/22668382 got fixed.
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700221 if (!mAllowUpdateElevation && !fill) {
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700222 elevation = DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP;
223 }
224 // Convert the DP elevation into physical pixels.
225 elevation = dipToPx(elevation);
226 }
227 // Don't change the elevation if it didn't change since it can require some time.
228 if (mOwner.getDecorView().getElevation() != elevation) {
229 mOwner.setElevation(elevation);
230 }
231 }
232
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700233 /**
234 * Converts a DIP measure into physical pixels.
235 * @param dip The dip value.
236 * @return Returns the number of pixels.
237 */
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700238 private float dipToPx(float dip) {
239 return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip,
240 getResources().getDisplayMetrics());
241 }
Stefan Kuhne1b420572015-08-07 10:50:19 -0700242
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700243 /**
244 * Maximize the window by moving it to the maximized workspace stack.
245 **/
Stefan Kuhne1b420572015-08-07 10:50:19 -0700246 private void maximizeWindow() {
Skuhnece2faa52015-08-11 10:36:38 -0700247 Window.WindowControllerCallback callback = mOwner.getWindowControllerCallback();
Stefan Kuhne1b420572015-08-07 10:50:19 -0700248 if (callback != null) {
249 try {
250 callback.changeWindowStack(
251 android.app.ActivityManager.FULLSCREEN_WORKSPACE_STACK_ID);
252 } catch (RemoteException ex) {
253 Log.e(TAG, "Cannot change task workspace.");
254 }
255 }
256 }
Skuhnece2faa52015-08-11 10:36:38 -0700257
258 /**
259 * Returns the bounds of this activity.
260 * @return Returns bounds of the activity. It will return null if either the window is
261 * fullscreen or the bounds could not be retrieved.
262 */
263 private Rect getActivityBounds() {
264 Window.WindowControllerCallback callback = mOwner.getWindowControllerCallback();
265 if (callback != null) {
266 try {
267 return callback.getActivityBounds();
268 } catch (RemoteException ex) {
269 Log.e(TAG, "Failed to get the activity bounds.");
270 }
271 }
272 return null;
273 }
274
275 /**
276 * Sets the bounds of this Activity on the stack.
277 * @param newBounds The bounds of the activity. Passing null is not allowed.
278 */
279 private void setActivityBounds(Rect newBounds) {
280 if (newBounds == null) {
281 Log.e(TAG, "Failed to set null bounds to the activity.");
282 return;
283 }
284 Window.WindowControllerCallback callback = mOwner.getWindowControllerCallback();
285 if (callback != null) {
286 try {
287 callback.setActivityBounds(newBounds);
288 } catch (RemoteException ex) {
289 Log.e(TAG, "Failed to set the activity bounds.");
290 }
291 }
292 }
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700293}