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 settle/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 // True when the left mouse button got released while dragging. 86 private boolean mLeftMouseButtonReleased; 87 88 private boolean mOverlayWithAppContent = false; 89 90 private View mCaption; 91 private View mContent; 92 private View mMaximize; 93 private View mClose; 94 95 // Fields for detecting drag events. 96 private int mTouchDownX; 97 private int mTouchDownY; 98 private boolean mCheckForDragging; 99 private int mDragSlop; 100 101 // Fields for detecting and intercepting click events on close/maximize. 102 private ArrayList<View> mTouchDispatchList = new ArrayList<>(2); 103 // We use the gesture detector to detect clicks on close/maximize buttons and to be consistent 104 // with existing click detection. 105 private GestureDetector mGestureDetector; 106 private final Rect mCloseRect = new Rect(); 107 private final Rect mMaximizeRect = new Rect(); 108 private View mClickTarget; 109 110 public DecorCaptionView(Context context) { 111 super(context); 112 init(context); 113 } 114 115 public DecorCaptionView(Context context, AttributeSet attrs) { 116 super(context, attrs); 117 init(context); 118 } 119 120 public DecorCaptionView(Context context, AttributeSet attrs, int defStyle) { 121 super(context, attrs, defStyle); 122 init(context); 123 } 124 125 private void init(Context context) { 126 mDragSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 127 mGestureDetector = new GestureDetector(context, this); 128 } 129 130 @Override 131 protected void onFinishInflate() { 132 super.onFinishInflate(); 133 mCaption = getChildAt(0); 134 } 135 136 public void setPhoneWindow(PhoneWindow owner, boolean show) { 137 mOwner = owner; 138 mShow = show; 139 mOverlayWithAppContent = owner.isOverlayWithDecorCaptionEnabled(); 140 if (mOverlayWithAppContent) { 141 // The caption is covering the content, so we make its background transparent to make 142 // the content visible. 143 mCaption.setBackgroundColor(Color.TRANSPARENT); 144 } 145 updateCaptionVisibility(); 146 // By changing the outline provider to BOUNDS, the window can remove its 147 // background without removing the shadow. 148 mOwner.getDecorView().setOutlineProvider(ViewOutlineProvider.BOUNDS); 149 mMaximize = findViewById(R.id.maximize_window); 150 mClose = findViewById(R.id.close_window); 151 } 152 153 @Override 154 public boolean onInterceptTouchEvent(MotionEvent ev) { 155 // If the user starts touch on the maximize/close buttons, we immediately intercept, so 156 // that these buttons are always clickable. 157 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 158 final int x = (int) ev.getX(); 159 final int y = (int) ev.getY(); 160 if (mMaximizeRect.contains(x, y)) { 161 mClickTarget = mMaximize; 162 } 163 if (mCloseRect.contains(x, y)) { 164 mClickTarget = mClose; 165 } 166 } 167 return mClickTarget != null; 168 } 169 170 @Override 171 public boolean onTouchEvent(MotionEvent event) { 172 if (mClickTarget != null) { 173 mGestureDetector.onTouchEvent(event); 174 final int action = event.getAction(); 175 if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { 176 mClickTarget = null; 177 } 178 return true; 179 } 180 return false; 181 } 182 183 @Override 184 public boolean onTouch(View v, MotionEvent e) { 185 // Note: There are no mixed events. When a new device gets used (e.g. 1. Mouse, 2. touch) 186 // the old input device events get cancelled first. So no need to remember the kind of 187 // input device we are listening to. 188 final int x = (int) e.getX(); 189 final int y = (int) e.getY(); 190 switch (e.getActionMasked()) { 191 case MotionEvent.ACTION_DOWN: 192 if (!mShow) { 193 // When there is no caption we should not react to anything. 194 return false; 195 } 196 // Checking for a drag action is started if we aren't dragging already and the 197 // starting event is either a left mouse button or any other input device. 198 if (((e.getToolType(e.getActionIndex()) != MotionEvent.TOOL_TYPE_MOUSE || 199 (e.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0))) { 200 mCheckForDragging = true; 201 mTouchDownX = x; 202 mTouchDownY = y; 203 } 204 break; 205 206 case MotionEvent.ACTION_MOVE: 207 if (!mDragging && mCheckForDragging && passedSlop(x, y)) { 208 mCheckForDragging = false; 209 mDragging = true; 210 mLeftMouseButtonReleased = false; 211 startMovingTask(e.getRawX(), e.getRawY()); 212 } else if (mDragging && !mLeftMouseButtonReleased) { 213 if (e.getToolType(e.getActionIndex()) == MotionEvent.TOOL_TYPE_MOUSE && 214 (e.getButtonState() & MotionEvent.BUTTON_PRIMARY) == 0) { 215 // There is no separate mouse button up call and if the user mixes mouse 216 // button drag actions, we stop dragging once he releases the button. 217 mLeftMouseButtonReleased = true; 218 break; 219 } 220 } 221 break; 222 223 case MotionEvent.ACTION_UP: 224 case MotionEvent.ACTION_CANCEL: 225 if (!mDragging) { 226 break; 227 } 228 // Abort the ongoing dragging. 229 mDragging = false; 230 return !mCheckForDragging; 231 } 232 return mDragging || mCheckForDragging; 233 } 234 235 @Override 236 public ArrayList<View> buildTouchDispatchChildList() { 237 mTouchDispatchList.ensureCapacity(3); 238 if (mCaption != null) { 239 mTouchDispatchList.add(mCaption); 240 } 241 if (mContent != null) { 242 mTouchDispatchList.add(mContent); 243 } 244 return mTouchDispatchList; 245 } 246 247 @Override 248 public boolean shouldDelayChildPressedState() { 249 return false; 250 } 251 252 private boolean passedSlop(int x, int y) { 253 return Math.abs(x - mTouchDownX) > mDragSlop || Math.abs(y - mTouchDownY) > mDragSlop; 254 } 255 256 /** 257 * The phone window configuration has changed and the caption needs to be updated. 258 * @param show True if the caption should be shown. 259 */ 260 public void onConfigurationChanged(boolean show) { 261 mShow = show; 262 updateCaptionVisibility(); 263 } 264 265 @Override 266 public void addView(View child, int index, ViewGroup.LayoutParams params) { 267 if (!(params instanceof MarginLayoutParams)) { 268 throw new IllegalArgumentException( 269 "params " + params + " must subclass MarginLayoutParams"); 270 } 271 // Make sure that we never get more then one client area in our view. 272 if (index >= 2 || getChildCount() >= 2) { 273 throw new IllegalStateException("DecorCaptionView can only handle 1 client view"); 274 } 275 // To support the overlaying content in the caption, we need to put the content view as the 276 // first child to get the right Z-Ordering. 277 super.addView(child, 0, params); 278 mContent = child; 279 } 280 281 @Override 282 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 283 final int captionHeight; 284 if (mCaption.getVisibility() != View.GONE) { 285 measureChildWithMargins(mCaption, widthMeasureSpec, 0, heightMeasureSpec, 0); 286 captionHeight = mCaption.getMeasuredHeight(); 287 } else { 288 captionHeight = 0; 289 } 290 if (mContent != null) { 291 if (mOverlayWithAppContent) { 292 measureChildWithMargins(mContent, widthMeasureSpec, 0, heightMeasureSpec, 0); 293 } else { 294 measureChildWithMargins(mContent, widthMeasureSpec, 0, heightMeasureSpec, 295 captionHeight); 296 } 297 } 298 299 setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), 300 MeasureSpec.getSize(heightMeasureSpec)); 301 } 302 303 @Override 304 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 305 final int captionHeight; 306 if (mCaption.getVisibility() != View.GONE) { 307 mCaption.layout(0, 0, mCaption.getMeasuredWidth(), mCaption.getMeasuredHeight()); 308 captionHeight = mCaption.getBottom() - mCaption.getTop(); 309 mMaximize.getHitRect(mMaximizeRect); 310 mClose.getHitRect(mCloseRect); 311 } else { 312 captionHeight = 0; 313 mMaximizeRect.setEmpty(); 314 mCloseRect.setEmpty(); 315 } 316 317 if (mContent != null) { 318 if (mOverlayWithAppContent) { 319 mContent.layout(0, 0, mContent.getMeasuredWidth(), mContent.getMeasuredHeight()); 320 } else { 321 mContent.layout(0, captionHeight, mContent.getMeasuredWidth(), 322 captionHeight + mContent.getMeasuredHeight()); 323 } 324 } 325 326 // This assumes that the caption bar is at the top. 327 mOwner.notifyRestrictedCaptionAreaCallback(mMaximize.getLeft(), mMaximize.getTop(), 328 mClose.getRight(), mClose.getBottom()); 329 } 330 /** 331 * Determine if the workspace is entirely covered by the window. 332 * @return Returns true when the window is filling the entire screen/workspace. 333 **/ 334 private boolean isFillingScreen() { 335 return (0 != ((getWindowSystemUiVisibility() | getSystemUiVisibility()) & 336 (View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | 337 View.SYSTEM_UI_FLAG_IMMERSIVE | View.SYSTEM_UI_FLAG_LOW_PROFILE))); 338 } 339 340 /** 341 * Updates the visibility of the caption. 342 **/ 343 private void updateCaptionVisibility() { 344 // Don't show the caption if the window has e.g. entered full screen. 345 boolean invisible = isFillingScreen() || !mShow; 346 mCaption.setVisibility(invisible ? GONE : VISIBLE); 347 mCaption.setOnTouchListener(this); 348 } 349 350 /** 351 * Maximize the window by moving it to the maximized workspace stack. 352 **/ 353 private void maximizeWindow() { 354 Window.WindowControllerCallback callback = mOwner.getWindowControllerCallback(); 355 if (callback != null) { 356 try { 357 callback.exitFreeformMode(); 358 } catch (RemoteException ex) { 359 Log.e(TAG, "Cannot change task workspace."); 360 } 361 } 362 } 363 364 public boolean isCaptionShowing() { 365 return mShow; 366 } 367 368 public int getCaptionHeight() { 369 return (mCaption != null) ? mCaption.getHeight() : 0; 370 } 371 372 public void removeContentView() { 373 if (mContent != null) { 374 removeView(mContent); 375 mContent = null; 376 } 377 } 378 379 public View getCaption() { 380 return mCaption; 381 } 382 383 @Override 384 public LayoutParams generateLayoutParams(AttributeSet attrs) { 385 return new MarginLayoutParams(getContext(), attrs); 386 } 387 388 @Override 389 protected LayoutParams generateDefaultLayoutParams() { 390 return new MarginLayoutParams(MarginLayoutParams.MATCH_PARENT, 391 MarginLayoutParams.MATCH_PARENT); 392 } 393 394 @Override 395 protected LayoutParams generateLayoutParams(LayoutParams p) { 396 return new MarginLayoutParams(p); 397 } 398 399 @Override 400 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 401 return p instanceof MarginLayoutParams; 402 } 403 404 @Override 405 public boolean onDown(MotionEvent e) { 406 return false; 407 } 408 409 @Override 410 public void onShowPress(MotionEvent e) { 411 412 } 413 414 @Override 415 public boolean onSingleTapUp(MotionEvent e) { 416 if (mClickTarget == mMaximize) { 417 maximizeWindow(); 418 } else if (mClickTarget == mClose) { 419 mOwner.dispatchOnWindowDismissed(true /*finishTask*/); 420 } 421 return true; 422 } 423 424 @Override 425 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { 426 return false; 427 } 428 429 @Override 430 public void onLongPress(MotionEvent e) { 431 432 } 433 434 @Override 435 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { 436 return false; 437 } 438 } 439