blob: 6ab306cec730260fffee1ec4627deeab55884336 [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;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070029import android.util.Log;
30import android.util.TypedValue;
31
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070032import com.android.internal.R;
33import com.android.internal.policy.PhoneWindow;
34
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070035/**
36 * This class represents the special screen elements to control a window on free form
37 * environment. All thse screen elements are added in the "non client area" which is the area of
38 * the window which is handled by the OS and not the application.
39 * As such this class handles the following things:
40 * <ul>
41 * <li>The caption, containing the system buttons like maximize, close and such as well as
42 * allowing the user to drag the window around.</li>
43 * <li>The shadow - which is changing dependent on the window focus.</li>
44 * <li>The border around the client area (if there is one).</li>
45 * <li>The resize handles which allow to resize the window.</li>
46 * </ul>
47 * After creating the view, the function
48 * {@link #setPhoneWindow(PhoneWindow owner, boolean windowHasShadow)} needs to be called to make
49 * 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>
53 * <li>setElevation</li>
54 * <li>setOutlineProvider</li>
55 * <li>setSurfaceFormat</li>
56 * <li>..</li>
57 * </ul>
58 * This will be mitigated once b/22527834 will be addressed.
59 */
Skuhnef7b882c2015-08-11 17:18:58 -070060public class NonClientDecorView extends LinearLayout implements View.OnClickListener {
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070061 private final static String TAG = "NonClientDecorView";
62 // The height of a window which has focus in DIP.
63 private final int DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP = 20;
64 // The height of a window which has not in DIP.
65 private final int DECOR_SHADOW_UNFOCUSED_HEIGHT_IN_DIP = 5;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070066 private PhoneWindow mOwner = null;
Skuhne81c524a2015-08-12 13:34:14 -070067 private boolean mWindowHasShadow = false;
68 private boolean mShowDecor = false;
69
70 // True if the window is being dragged.
71 private boolean mDragging = false;
72
Skuhne81c524a2015-08-12 13:34:14 -070073 // True when the left mouse button got released while dragging.
74 private boolean mLeftMouseButtonReleased;
75
Skuhnea5a93ee2015-08-20 15:43:57 -070076 // True if this window is resizable (which is currently only true when the decor is shown).
Chong Zhang8e89b312015-09-09 15:09:30 -070077 public boolean mVisible = false;
Skuhnea5a93ee2015-08-20 15:43:57 -070078
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070079 // The current focus state of the window for updating the window elevation.
Skuhne81c524a2015-08-12 13:34:14 -070080 private boolean mWindowHasFocus = true;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070081
82 // Cludge to address b/22668382: Set the shadow size to the maximum so that the layer
83 // size calculation takes the shadow size into account. We set the elevation currently
84 // to max until the first layout command has been executed.
Skuhne81c524a2015-08-12 13:34:14 -070085 private boolean mAllowUpdateElevation = false;
Stefan Kuhne61b47bb2015-07-28 14:04:25 -070086
87 public NonClientDecorView(Context context) {
88 super(context);
89 }
90
91 public NonClientDecorView(Context context, AttributeSet attrs) {
92 super(context, attrs);
93 }
94
95 public NonClientDecorView(Context context, AttributeSet attrs, int defStyle) {
96 super(context, attrs, defStyle);
97 }
98
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -070099 public void setPhoneWindow(PhoneWindow owner, boolean showDecor, boolean windowHasShadow) {
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700100 mOwner = owner;
101 mWindowHasShadow = windowHasShadow;
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700102 mShowDecor = showDecor;
Skuhnef7b882c2015-08-11 17:18:58 -0700103 updateCaptionVisibility();
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700104 if (mWindowHasShadow) {
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700105 initializeElevation();
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700106 }
107 // By changing the outline provider to BOUNDS, the window can remove its
108 // background without removing the shadow.
109 mOwner.getDecorView().setOutlineProvider(ViewOutlineProvider.BOUNDS);
110 findViewById(R.id.maximize_window).setOnClickListener(this);
111 findViewById(R.id.close_window).setOnClickListener(this);
112 }
113
Skuhne81c524a2015-08-12 13:34:14 -0700114 @Override
115 public boolean onTouchEvent(MotionEvent e) {
116 // Note: There are no mixed events. When a new device gets used (e.g. 1. Mouse, 2. touch)
117 // the old input device events get cancelled first. So no need to remember the kind of
118 // input device we are listening to.
119 switch (e.getActionMasked()) {
120 case MotionEvent.ACTION_DOWN:
Skuhnea635a262015-08-26 15:45:58 -0700121 if (!mShowDecor) {
122 // When there is no decor we should not react to anything.
123 return false;
124 }
Skuhne81c524a2015-08-12 13:34:14 -0700125 // A drag action is started if we aren't dragging already and the starting event is
126 // either a left mouse button or any other input device.
127 if (!mDragging &&
128 (e.getToolType(e.getActionIndex()) != MotionEvent.TOOL_TYPE_MOUSE ||
129 (e.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0)) {
130 mDragging = true;
Skuhne81c524a2015-08-12 13:34:14 -0700131 mLeftMouseButtonReleased = false;
Chong Zhang8e89b312015-09-09 15:09:30 -0700132 startMovingTask(e.getRawX(), e.getRawY());
Skuhne81c524a2015-08-12 13:34:14 -0700133 }
134 break;
135
136 case MotionEvent.ACTION_MOVE:
137 if (mDragging && !mLeftMouseButtonReleased) {
138 if (e.getToolType(e.getActionIndex()) == MotionEvent.TOOL_TYPE_MOUSE &&
139 (e.getButtonState() & MotionEvent.BUTTON_PRIMARY) == 0) {
140 // There is no separate mouse button up call and if the user mixes mouse
141 // button drag actions, we stop dragging once he releases the button.
142 mLeftMouseButtonReleased = true;
143 break;
144 }
Skuhne81c524a2015-08-12 13:34:14 -0700145 }
146 break;
147
148 case MotionEvent.ACTION_UP:
Skuhne81c524a2015-08-12 13:34:14 -0700149 case MotionEvent.ACTION_CANCEL:
Skuhnea5a93ee2015-08-20 15:43:57 -0700150 if (!mDragging) {
151 break;
Skuhne81c524a2015-08-12 13:34:14 -0700152 }
Skuhnea5a93ee2015-08-20 15:43:57 -0700153 // Abort the ongoing dragging.
154 mDragging = false;
Skuhnea5a93ee2015-08-20 15:43:57 -0700155 return true;
Skuhne81c524a2015-08-12 13:34:14 -0700156 }
157 return mDragging;
158 }
159
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700160 /**
161 * The phone window configuration has changed and the decor needs to be updated.
162 * @param showDecor True if the decor should be shown.
163 * @param windowHasShadow True when the window should show a shadow.
164 **/
165 public void phoneWindowUpdated(boolean showDecor, boolean windowHasShadow) {
166 mShowDecor = showDecor;
Skuhnef7b882c2015-08-11 17:18:58 -0700167 updateCaptionVisibility();
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700168 if (windowHasShadow != mWindowHasShadow) {
169 mWindowHasShadow = windowHasShadow;
170 initializeElevation();
171 }
172 }
173
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700174 @Override
175 public void onClick(View view) {
176 if (view.getId() == R.id.maximize_window) {
Stefan Kuhne1b420572015-08-07 10:50:19 -0700177 maximizeWindow();
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700178 } else if (view.getId() == R.id.close_window) {
Stefan Kuhne1b420572015-08-07 10:50:19 -0700179 mOwner.dispatchOnWindowDismissed(true /*finishTask*/);
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700180 }
181 }
182
183 @Override
184 public void onWindowFocusChanged(boolean hasWindowFocus) {
185 mWindowHasFocus = hasWindowFocus;
186 updateElevation();
187 super.onWindowFocusChanged(hasWindowFocus);
188 }
189
190 @Override
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700191 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700192 // If the application changed its SystemUI metrics, we might also have to adapt
193 // our shadow elevation.
194 updateElevation();
195 mAllowUpdateElevation = true;
196
Skuhnef7b882c2015-08-11 17:18:58 -0700197 super.onLayout(changed, left, top, right, bottom);
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700198 }
199
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700200 @Override
Skuhnef7b882c2015-08-11 17:18:58 -0700201 public void addView(View child, int index, ViewGroup.LayoutParams params) {
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700202 // Make sure that we never get more then one client area in our view.
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700203 if (index >= 2 || getChildCount() >= 2) {
204 throw new IllegalStateException("NonClientDecorView can only handle 1 client view");
205 }
206 super.addView(child, index, params);
207 }
208
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700209 /**
210 * Determine if the workspace is entirely covered by the window.
211 * @return Returns true when the window is filling the entire screen/workspace.
212 **/
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700213 private boolean isFillingScreen() {
214 return (0 != ((getWindowSystemUiVisibility() | getSystemUiVisibility()) &
215 (View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
216 View.SYSTEM_UI_FLAG_IMMERSIVE | View.SYSTEM_UI_FLAG_LOW_PROFILE)));
217 }
218
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700219 /**
Skuhnef7b882c2015-08-11 17:18:58 -0700220 * Updates the visibility of the caption.
221 **/
222 private void updateCaptionVisibility() {
223 // Don't show the decor if the window has e.g. entered full screen.
224 boolean invisible = isFillingScreen() || !mShowDecor;
225 View caption = getChildAt(0);
226 caption.setVisibility(invisible ? GONE : VISIBLE);
Chong Zhang8e89b312015-09-09 15:09:30 -0700227 mVisible = !invisible;
Skuhnef7b882c2015-08-11 17:18:58 -0700228 }
229
230 /**
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700231 * The elevation gets set for the first time and the framework needs to be informed that
232 * the surface layer gets created with the shadow size in mind.
233 **/
234 private void initializeElevation() {
235 // TODO(skuhne): Call setMaxElevation here accordingly after b/22668382 got fixed.
236 mAllowUpdateElevation = false;
237 if (mWindowHasShadow) {
238 updateElevation();
239 } else {
240 mOwner.setElevation(0);
241 }
242 }
243
244 /**
245 * The shadow height gets controlled by the focus to visualize highlighted windows.
246 * Note: This will overwrite application elevation properties.
247 * Note: Windows which have (temporarily) changed their attributes to cover the SystemUI
248 * will get no shadow as they are expected to be "full screen".
249 **/
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700250 private void updateElevation() {
251 float elevation = 0;
252 if (mWindowHasShadow) {
253 boolean fill = isFillingScreen();
254 elevation = fill ? 0 :
255 (mWindowHasFocus ? DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP :
256 DECOR_SHADOW_UNFOCUSED_HEIGHT_IN_DIP);
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700257 // TODO(skuhne): Remove this if clause once b/22668382 got fixed.
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700258 if (!mAllowUpdateElevation && !fill) {
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700259 elevation = DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP;
260 }
261 // Convert the DP elevation into physical pixels.
262 elevation = dipToPx(elevation);
263 }
264 // Don't change the elevation if it didn't change since it can require some time.
265 if (mOwner.getDecorView().getElevation() != elevation) {
266 mOwner.setElevation(elevation);
267 }
268 }
269
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700270 /**
271 * Converts a DIP measure into physical pixels.
272 * @param dip The dip value.
273 * @return Returns the number of pixels.
274 */
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700275 private float dipToPx(float dip) {
276 return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip,
277 getResources().getDisplayMetrics());
278 }
Stefan Kuhne1b420572015-08-07 10:50:19 -0700279
Stefan Kuhnef4dd71a2015-08-07 09:28:52 -0700280 /**
281 * Maximize the window by moving it to the maximized workspace stack.
282 **/
Stefan Kuhne1b420572015-08-07 10:50:19 -0700283 private void maximizeWindow() {
Skuhnece2faa52015-08-11 10:36:38 -0700284 Window.WindowControllerCallback callback = mOwner.getWindowControllerCallback();
Stefan Kuhne1b420572015-08-07 10:50:19 -0700285 if (callback != null) {
286 try {
287 callback.changeWindowStack(
288 android.app.ActivityManager.FULLSCREEN_WORKSPACE_STACK_ID);
289 } catch (RemoteException ex) {
290 Log.e(TAG, "Cannot change task workspace.");
291 }
292 }
293 }
Stefan Kuhne61b47bb2015-07-28 14:04:25 -0700294}