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 17package com.android.internal.widget; 18 19import android.content.Context; 20import android.graphics.Color; 21import android.graphics.Rect; 22import android.os.RemoteException; 23import android.util.AttributeSet; 24import android.util.Log; 25import android.view.GestureDetector; 26import android.view.MotionEvent; 27import android.view.View; 28import android.view.ViewConfiguration; 29import android.view.ViewGroup; 30import android.view.ViewOutlineProvider; 31import android.view.Window; 32 33import com.android.internal.R; 34import com.android.internal.policy.PhoneWindow; 35 36import 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 */ 76public 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 private boolean passedSlop(int x, int y) { 248 return Math.abs(x - mTouchDownX) > mDragSlop || Math.abs(y - mTouchDownY) > mDragSlop; 249 } 250 251 /** 252 * The phone window configuration has changed and the caption needs to be updated. 253 * @param show True if the caption should be shown. 254 */ 255 public void onConfigurationChanged(boolean show) { 256 mShow = show; 257 updateCaptionVisibility(); 258 } 259 260 @Override 261 public void addView(View child, int index, ViewGroup.LayoutParams params) { 262 if (!(params instanceof MarginLayoutParams)) { 263 throw new IllegalArgumentException( 264 "params " + params + " must subclass MarginLayoutParams"); 265 } 266 // Make sure that we never get more then one client area in our view. 267 if (index >= 2 || getChildCount() >= 2) { 268 throw new IllegalStateException("DecorCaptionView can only handle 1 client view"); 269 } 270 // To support the overlaying content in the caption, we need to put the content view as the 271 // first child to get the right Z-Ordering. 272 super.addView(child, 0, params); 273 mContent = child; 274 } 275 276 @Override 277 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 278 final int captionHeight; 279 if (mCaption.getVisibility() != View.GONE) { 280 measureChildWithMargins(mCaption, widthMeasureSpec, 0, heightMeasureSpec, 0); 281 captionHeight = mCaption.getMeasuredHeight(); 282 } else { 283 captionHeight = 0; 284 } 285 if (mContent != null) { 286 if (mOverlayWithAppContent) { 287 measureChildWithMargins(mContent, widthMeasureSpec, 0, heightMeasureSpec, 0); 288 } else { 289 measureChildWithMargins(mContent, widthMeasureSpec, 0, heightMeasureSpec, 290 captionHeight); 291 } 292 } 293 294 setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), 295 MeasureSpec.getSize(heightMeasureSpec)); 296 } 297 298 @Override 299 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 300 final int captionHeight; 301 if (mCaption.getVisibility() != View.GONE) { 302 mCaption.layout(0, 0, mCaption.getMeasuredWidth(), mCaption.getMeasuredHeight()); 303 captionHeight = mCaption.getBottom() - mCaption.getTop(); 304 mMaximize.getHitRect(mMaximizeRect); 305 mClose.getHitRect(mCloseRect); 306 } else { 307 captionHeight = 0; 308 mMaximizeRect.setEmpty(); 309 mCloseRect.setEmpty(); 310 } 311 312 if (mContent != null) { 313 if (mOverlayWithAppContent) { 314 mContent.layout(0, 0, mContent.getMeasuredWidth(), mContent.getMeasuredHeight()); 315 } else { 316 mContent.layout(0, captionHeight, mContent.getMeasuredWidth(), 317 captionHeight + mContent.getMeasuredHeight()); 318 } 319 } 320 321 // This assumes that the caption bar is at the top. 322 mOwner.notifyRestrictedCaptionAreaCallback(mMaximize.getLeft(), mMaximize.getTop(), 323 mClose.getRight(), mClose.getBottom()); 324 } 325 /** 326 * Determine if the workspace is entirely covered by the window. 327 * @return Returns true when the window is filling the entire screen/workspace. 328 **/ 329 private boolean isFillingScreen() { 330 return (0 != ((getWindowSystemUiVisibility() | getSystemUiVisibility()) & 331 (View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | 332 View.SYSTEM_UI_FLAG_IMMERSIVE | View.SYSTEM_UI_FLAG_LOW_PROFILE))); 333 } 334 335 /** 336 * Updates the visibility of the caption. 337 **/ 338 private void updateCaptionVisibility() { 339 // Don't show the caption if the window has e.g. entered full screen. 340 boolean invisible = isFillingScreen() || !mShow; 341 mCaption.setVisibility(invisible ? GONE : VISIBLE); 342 mCaption.setOnTouchListener(this); 343 } 344 345 /** 346 * Maximize the window by moving it to the maximized workspace stack. 347 **/ 348 private void maximizeWindow() { 349 Window.WindowControllerCallback callback = mOwner.getWindowControllerCallback(); 350 if (callback != null) { 351 try { 352 callback.exitFreeformMode(); 353 } catch (RemoteException ex) { 354 Log.e(TAG, "Cannot change task workspace."); 355 } 356 } 357 } 358 359 public boolean isCaptionShowing() { 360 return mShow; 361 } 362 363 public int getCaptionHeight() { 364 return (mCaption != null) ? mCaption.getHeight() : 0; 365 } 366 367 public void removeContentView() { 368 if (mContent != null) { 369 removeView(mContent); 370 mContent = null; 371 } 372 } 373 374 public View getCaption() { 375 return mCaption; 376 } 377 378 @Override 379 public LayoutParams generateLayoutParams(AttributeSet attrs) { 380 return new MarginLayoutParams(getContext(), attrs); 381 } 382 383 @Override 384 protected LayoutParams generateDefaultLayoutParams() { 385 return new MarginLayoutParams(MarginLayoutParams.MATCH_PARENT, 386 MarginLayoutParams.MATCH_PARENT); 387 } 388 389 @Override 390 protected LayoutParams generateLayoutParams(LayoutParams p) { 391 return new MarginLayoutParams(p); 392 } 393 394 @Override 395 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 396 return p instanceof MarginLayoutParams; 397 } 398 399 @Override 400 public boolean onDown(MotionEvent e) { 401 return false; 402 } 403 404 @Override 405 public void onShowPress(MotionEvent e) { 406 407 } 408 409 @Override 410 public boolean onSingleTapUp(MotionEvent e) { 411 if (mClickTarget == mMaximize) { 412 maximizeWindow(); 413 } else if (mClickTarget == mClose) { 414 mOwner.dispatchOnWindowDismissed(true /*finishTask*/); 415 } 416 return true; 417 } 418 419 @Override 420 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { 421 return false; 422 } 423 424 @Override 425 public void onLongPress(MotionEvent e) { 426 427 } 428 429 @Override 430 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { 431 return false; 432 } 433} 434