1 /* 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 17 package com.android.internal.widget; 18 19 import android.content.Context; 20 import android.graphics.Color; 21 import android.graphics.Rect; 22 import android.os.RemoteException; 23 import android.util.AttributeSet; 24 import android.util.Log; 25 import android.view.GestureDetector; 26 import android.view.MotionEvent; 27 import android.view.View; 28 import android.view.ViewConfiguration; 29 import android.view.ViewGroup; 30 import android.view.ViewOutlineProvider; 31 import android.view.Window; 32 33 import com.android.internal.R; 34 import com.android.internal.policy.PhoneWindow; 35 36 import java.util.ArrayList; 37 38 /** 39 * This class represents the special screen elements to control a window on freeform 40 * environment. 41 * As such this class handles the following things: 42 * <ul> 43 * <li>The caption, containing the system buttons like maximize, close and such as well as 44 * allowing the user to drag the window around.</li> 45 * </ul> 46 * After creating the view, the function {@link #setPhoneWindow} needs to be called to make 47 * the connection to it's owning PhoneWindow. 48 * Note: At this time the application can change various attributes of the DecorView which 49 * will break things (in subtle/unexpected ways): 50 * <ul> 51 * <li>setOutlineProvider</li> 52 * <li>setSurfaceFormat</li> 53 * <li>..</li> 54 * </ul> 55 * 56 * Although this ViewGroup has only two direct sub-Views, its behavior is more complex due to 57 * overlaying caption on the content and drawing. 58 * 59 * First, no matter where the content View gets added, it will always be the first child and the 60 * caption will be the second. This way the caption will always be drawn on top of the content when 61 * overlaying is enabled. 62 * 63 * Second, the touch dispatch is customized to handle overlaying. This is what happens when touch 64 * is dispatched on the caption area while overlaying it on content: 65 * <ul> 66 * <li>DecorCaptionView.onInterceptTouchEvent() will try intercepting the touch events if the 67 * down action is performed on top close or maximize buttons; the reason for that is we want these 68 * buttons to always work.</li> 69 * <li>The content View will receive the touch event. Mind that content is actually underneath the 70 * caption, so we need to introduce our own dispatch ordering. We achieve this by overriding 71 * {@link #buildTouchDispatchChildList()}.</li> 72 * <li>If the touch event is not consumed by the content View, it will go to the caption View 73 * and the dragging logic will be executed.</li> 74 * </ul> 75 */ 76 public class DecorCaptionView extends ViewGroup implements View.OnTouchListener, 77 GestureDetector.OnGestureListener { 78 private final static String TAG = "DecorCaptionView"; 79 private PhoneWindow mOwner = null; 80 private boolean mShow = false; 81 82 // True if the window is being dragged. 83 private boolean mDragging = false; 84 85 private boolean mOverlayWithAppContent = false; 86 87 private View mCaption; 88 private View mContent; 89 private View mMaximize; 90 private View mClose; 91 92 // Fields for detecting drag events. 93 private int mTouchDownX; 94 private int mTouchDownY; 95 private boolean mCheckForDragging; 96 private int mDragSlop; 97 98 // Fields for detecting and intercepting click events on close/maximize. 99 private ArrayList<View> mTouchDispatchList = new ArrayList<>(2); 100 // We use the gesture detector to detect clicks on close/maximize buttons and to be consistent 101 // with existing click detection. 102 private GestureDetector mGestureDetector; 103 private final Rect mCloseRect = new Rect(); 104 private final Rect mMaximizeRect = new Rect(); 105 private View mClickTarget; 106 107 public DecorCaptionView(Context context) { 108 super(context); 109 init(context); 110 } 111 112 public DecorCaptionView(Context context, AttributeSet attrs) { 113 super(context, attrs); 114 init(context); 115 } 116 117 public DecorCaptionView(Context context, AttributeSet attrs, int defStyle) { 118 super(context, attrs, defStyle); 119 init(context); 120 } 121 122 private void init(Context context) { 123 mDragSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 124 mGestureDetector = new GestureDetector(context, this); 125 } 126 127 @Override 128 protected void onFinishInflate() { 129 super.onFinishInflate(); 130 mCaption = getChildAt(0); 131 } 132 133 public void setPhoneWindow(PhoneWindow owner, boolean show) { 134 mOwner = owner; 135 mShow = show; 136 mOverlayWithAppContent = owner.isOverlayWithDecorCaptionEnabled(); 137 if (mOverlayWithAppContent) { 138 // The caption is covering the content, so we make its background transparent to make 139 // the content visible. 140 mCaption.setBackgroundColor(Color.TRANSPARENT); 141 } 142 updateCaptionVisibility(); 143 // By changing the outline provider to BOUNDS, the window can remove its 144 // background without removing the shadow. 145 mOwner.getDecorView().setOutlineProvider(ViewOutlineProvider.BOUNDS); 146 mMaximize = findViewById(R.id.maximize_window); 147 mClose = findViewById(R.id.close_window); 148 } 149 150 @Override 151 public boolean onInterceptTouchEvent(MotionEvent ev) { 152 // If the user starts touch on the maximize/close buttons, we immediately intercept, so 153 // that these buttons are always clickable. 154 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 155 final int x = (int) ev.getX(); 156 final int y = (int) ev.getY(); 157 if (mMaximizeRect.contains(x, y)) { 158 mClickTarget = mMaximize; 159 } 160 if (mCloseRect.contains(x, y)) { 161 mClickTarget = mClose; 162 } 163 } 164 return mClickTarget != null; 165 } 166 167 @Override 168 public boolean onTouchEvent(MotionEvent event) { 169 if (mClickTarget != null) { 170 mGestureDetector.onTouchEvent(event); 171 final int action = event.getAction(); 172 if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { 173 mClickTarget = null; 174 } 175 return true; 176 } 177 return false; 178 } 179 180 @Override 181 public boolean onTouch(View v, MotionEvent e) { 182 // Note: There are no mixed events. When a new device gets used (e.g. 1. Mouse, 2. touch) 183 // the old input device events get cancelled first. So no need to remember the kind of 184 // input device we are listening to. 185 final int x = (int) e.getX(); 186 final int y = (int) e.getY(); 187 final boolean fromMouse = e.getToolType(e.getActionIndex()) == MotionEvent.TOOL_TYPE_MOUSE; 188 final boolean primaryButton = (e.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0; 189 switch (e.getActionMasked()) { 190 case MotionEvent.ACTION_DOWN: 191 if (!mShow) { 192 // When there is no caption we should not react to anything. 193 return false; 194 } 195 // Checking for a drag action is started if we aren't dragging already and the 196 // starting event is either a left mouse button or any other input device. 197 if (!fromMouse || primaryButton) { 198 mCheckForDragging = true; 199 mTouchDownX = x; 200 mTouchDownY = y; 201 } 202 break; 203 204 case MotionEvent.ACTION_MOVE: 205 if (!mDragging && mCheckForDragging && (fromMouse || passedSlop(x, y))) { 206 mCheckForDragging = false; 207 mDragging = true; 208 startMovingTask(e.getRawX(), e.getRawY()); 209 // After the above call the framework will take over the input. 210 // This handler will receive ACTION_CANCEL soon (possible after a few spurious 211 // ACTION_MOVE events which are safe to ignore). 212 } 213 break; 214 215 case MotionEvent.ACTION_UP: 216 case MotionEvent.ACTION_CANCEL: 217 if (!mDragging) { 218 break; 219 } 220 // Abort the ongoing dragging. 221 mDragging = false; 222 return !mCheckForDragging; 223 } 224 return mDragging || mCheckForDragging; 225 } 226 227 @Override 228 public ArrayList<View> buildTouchDispatchChildList() { 229 mTouchDispatchList.ensureCapacity(3); 230 if (mCaption != null) { 231 mTouchDispatchList.add(mCaption); 232 } 233 if (mContent != null) { 234 mTouchDispatchList.add(mContent); 235 } 236 return mTouchDispatchList; 237 } 238 239 @Override 240 public boolean shouldDelayChildPressedState() { 241 return false; 242 } 243 244 private boolean passedSlop(int x, int y) { 245 return Math.abs(x - mTouchDownX) > mDragSlop || Math.abs(y - mTouchDownY) > mDragSlop; 246 } 247 248 /** 249 * The phone window configuration has changed and the caption needs to be updated. 250 * @param show True if the caption should be shown. 251 */ 252 public void onConfigurationChanged(boolean show) { 253 mShow = show; 254 updateCaptionVisibility(); 255 } 256 257 @Override 258 public void addView(View child, int index, ViewGroup.LayoutParams params) { 259 if (!(params instanceof MarginLayoutParams)) { 260 throw new IllegalArgumentException( 261 "params " + params + " must subclass MarginLayoutParams"); 262 } 263 // Make sure that we never get more then one client area in our view. 264 if (index >= 2 || getChildCount() >= 2) { 265 throw new IllegalStateException("DecorCaptionView can only handle 1 client view"); 266 } 267 // To support the overlaying content in the caption, we need to put the content view as the 268 // first child to get the right Z-Ordering. 269 super.addView(child, 0, params); 270 mContent = child; 271 } 272 273 @Override 274 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 275 final int captionHeight; 276 if (mCaption.getVisibility() != View.GONE) { 277 measureChildWithMargins(mCaption, widthMeasureSpec, 0, heightMeasureSpec, 0); 278 captionHeight = mCaption.getMeasuredHeight(); 279 } else { 280 captionHeight = 0; 281 } 282 if (mContent != null) { 283 if (mOverlayWithAppContent) { 284 measureChildWithMargins(mContent, widthMeasureSpec, 0, heightMeasureSpec, 0); 285 } else { 286 measureChildWithMargins(mContent, widthMeasureSpec, 0, heightMeasureSpec, 287 captionHeight); 288 } 289 } 290 291 setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), 292 MeasureSpec.getSize(heightMeasureSpec)); 293 } 294 295 @Override 296 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 297 final int captionHeight; 298 if (mCaption.getVisibility() != View.GONE) { 299 mCaption.layout(0, 0, mCaption.getMeasuredWidth(), mCaption.getMeasuredHeight()); 300 captionHeight = mCaption.getBottom() - mCaption.getTop(); 301 mMaximize.getHitRect(mMaximizeRect); 302 mClose.getHitRect(mCloseRect); 303 } else { 304 captionHeight = 0; 305 mMaximizeRect.setEmpty(); 306 mCloseRect.setEmpty(); 307 } 308 309 if (mContent != null) { 310 if (mOverlayWithAppContent) { 311 mContent.layout(0, 0, mContent.getMeasuredWidth(), mContent.getMeasuredHeight()); 312 } else { 313 mContent.layout(0, captionHeight, mContent.getMeasuredWidth(), 314 captionHeight + mContent.getMeasuredHeight()); 315 } 316 } 317 318 // This assumes that the caption bar is at the top. 319 mOwner.notifyRestrictedCaptionAreaCallback(mMaximize.getLeft(), mMaximize.getTop(), 320 mClose.getRight(), mClose.getBottom()); 321 } 322 /** 323 * Determine if the workspace is entirely covered by the window. 324 * @return Returns true when the window is filling the entire screen/workspace. 325 **/ 326 private boolean isFillingScreen() { 327 return (0 != ((getWindowSystemUiVisibility() | getSystemUiVisibility()) & 328 (View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | 329 View.SYSTEM_UI_FLAG_IMMERSIVE | View.SYSTEM_UI_FLAG_LOW_PROFILE))); 330 } 331 332 /** 333 * Updates the visibility of the caption. 334 **/ 335 private void updateCaptionVisibility() { 336 // Don't show the caption if the window has e.g. entered full screen. 337 boolean invisible = isFillingScreen() || !mShow; 338 mCaption.setVisibility(invisible ? GONE : VISIBLE); 339 mCaption.setOnTouchListener(this); 340 } 341 342 /** 343 * Maximize the window by moving it to the maximized workspace stack. 344 **/ 345 private void maximizeWindow() { 346 Window.WindowControllerCallback callback = mOwner.getWindowControllerCallback(); 347 if (callback != null) { 348 try { 349 callback.exitFreeformMode(); 350 } catch (RemoteException ex) { 351 Log.e(TAG, "Cannot change task workspace."); 352 } 353 } 354 } 355 356 public boolean isCaptionShowing() { 357 return mShow; 358 } 359 360 public int getCaptionHeight() { 361 return (mCaption != null) ? mCaption.getHeight() : 0; 362 } 363 364 public void removeContentView() { 365 if (mContent != null) { 366 removeView(mContent); 367 mContent = null; 368 } 369 } 370 371 public View getCaption() { 372 return mCaption; 373 } 374 375 @Override 376 public LayoutParams generateLayoutParams(AttributeSet attrs) { 377 return new MarginLayoutParams(getContext(), attrs); 378 } 379 380 @Override 381 protected LayoutParams generateDefaultLayoutParams() { 382 return new MarginLayoutParams(MarginLayoutParams.MATCH_PARENT, 383 MarginLayoutParams.MATCH_PARENT); 384 } 385 386 @Override 387 protected LayoutParams generateLayoutParams(LayoutParams p) { 388 return new MarginLayoutParams(p); 389 } 390 391 @Override 392 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 393 return p instanceof MarginLayoutParams; 394 } 395 396 @Override 397 public boolean onDown(MotionEvent e) { 398 return false; 399 } 400 401 @Override 402 public void onShowPress(MotionEvent e) { 403 404 } 405 406 @Override 407 public boolean onSingleTapUp(MotionEvent e) { 408 if (mClickTarget == mMaximize) { 409 maximizeWindow(); 410 } else if (mClickTarget == mClose) { 411 mOwner.dispatchOnWindowDismissed( 412 true /*finishTask*/, false /*suppressWindowTransition*/); 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 } 431 } 432