content: /*
   * Copyright (C) 2015 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.internal.widget;
  
  import android.content.Context;
  import android.graphics.Color;
  import android.graphics.Rect;
  import android.os.RemoteException;
  import android.util.AttributeSet;
  import android.util.Log;
  import android.view.GestureDetector;
  import android.view.MotionEvent;
  import android.view.View;
  import android.view.ViewConfiguration;
  import android.view.ViewGroup;
  import android.view.ViewOutlineProvider;
  import android.view.Window;
  
  import com.android.internal.R;
  import com.android.internal.policy.PhoneWindow;
  
  import java.util.ArrayList;
  
  /**
   * This class represents the special screen elements to control a window on freeform
   * environment.
   * As such this class handles the following things:
   * <ul>
   * <li>The caption, containing the system buttons like maximize, close and such as well as
   * allowing the user to drag the window around.</li>
   * </ul>
   * After creating the view, the function {@link #setPhoneWindow} needs to be called to make
   * the connection to it's owning PhoneWindow.
   * Note: At this time the application can change various attributes of the DecorView which
   * will break things (in subtle/unexpected ways):
   * <ul>
   * <li>setOutlineProvider</li>
   * <li>setSurfaceFormat</li>
   * <li>..</li>
   * </ul>
   *
   * Although this ViewGroup has only two direct sub-Views, its behavior is more complex due to
   * overlaying caption on the content and drawing.
   *
   * First, no matter where the content View gets added, it will always be the first child and the
   * caption will be the second. This way the caption will always be drawn on top of the content when
   * overlaying is enabled.
   *
   * Second, the touch dispatch is customized to handle overlaying. This is what happens when touch
   * is dispatched on the caption area while overlaying it on content:
   * <ul>
   * <li>DecorCaptionView.onInterceptTouchEvent() will try intercepting the touch events if the
   * down action is performed on top close or maximize buttons; the reason for that is we want these
   * buttons to always work.</li>
   * <li>The content View will receive the touch event. Mind that content is actually underneath the
   * caption, so we need to introduce our own dispatch ordering. We achieve this by overriding
   * {@link #buildTouchDispatchChildList()}.</li>
   * <li>If the touch event is not consumed by the content View, it will go to the caption View
   * and the dragging logic will be executed.</li>
   * </ul>
   */
  public class DecorCaptionView extends ViewGroup implements View.OnTouchListener,
          GestureDetector.OnGestureListener {
      private final static String TAG = "DecorCaptionView";
      private PhoneWindow mOwner = null;
      private boolean mShow = false;
  
      // True if the window is being dragged.
      private boolean mDragging = false;
  
      private boolean mOverlayWithAppContent = false;
  
      private View mCaption;
      private View mContent;
      private View mMaximize;
      private View mClose;
  
      // Fields for detecting drag events.
      private int mTouchDownX;
      private int mTouchDownY;
      private boolean mCheckForDragging;
      private int mDragSlop;
  
      // Fields for detecting and intercepting click events on close/maximize.
      private ArrayList<View> mTouchDispatchList = new ArrayList<>(2);
      // We use the gesture detector to detect clicks on close/maximize buttons and to be consistent
      // with existing click detection.
      private GestureDetector mGestureDetector;
      private final Rect mCloseRect = new Rect();
      private final Rect mMaximizeRect = new Rect();
      private View mClickTarget;
      private int mRootScrollY;
  
      public DecorCaptionView(Context context) {
          super(context);
          init(context);
      }
  
      public DecorCaptionView(Context context, AttributeSet attrs) {
          super(context, attrs);
          init(context);
      }
  
      public DecorCaptionView(Context context, AttributeSet attrs, int defStyle) {
          super(context, attrs, defStyle);
          init(context);
      }
  
      private void init(Context context) {
          mDragSlop = ViewConfiguration.get(context).getScaledTouchSlop();
          mGestureDetector = new GestureDetector(context, this);
      }
  
      @Override
      protected void onFinishInflate() {
          super.onFinishInflate();
          mCaption = getChildAt(0);
      }
  
      public void setPhoneWindow(PhoneWindow owner, boolean show) {
          mOwner = owner;
          mShow = show;
          mOverlayWithAppContent = owner.isOverlayWithDecorCaptionEnabled();
          if (mOverlayWithAppContent) {
              // The caption is covering the content, so we make its background transparent to make
              // the content visible.
              mCaption.setBackgroundColor(Color.TRANSPARENT);
          }
          updateCaptionVisibility();
          // By changing the outline provider to BOUNDS, the window can remove its
          // background without removing the shadow.
          mOwner.getDecorView().setOutlineProvider(ViewOutlineProvider.BOUNDS);
          mMaximize = findViewById(R.id.maximize_window);
          mClose = findViewById(R.id.close_window);
      }
  
      @Override
      public boolean onInterceptTouchEvent(MotionEvent ev) {
          // If the user starts touch on the maximize/close buttons, we immediately intercept, so
          // that these buttons are always clickable.
          if (ev.getAction() == MotionEvent.ACTION_DOWN) {
              final int x = (int) ev.getX();
              final int y = (int) ev.getY();
              // Only offset y for containment tests because the actual views are already translated.
              if (mMaximizeRect.contains(x, y - mRootScrollY)) {
                  mClickTarget = mMaximize;
              }
              if (mCloseRect.contains(x, y - mRootScrollY)) {
                  mClickTarget = mClose;
              }
          }
          return mClickTarget != null;
      }
  
      @Override
      public boolean onTouchEvent(MotionEvent event) {
          if (mClickTarget != null) {
              mGestureDetector.onTouchEvent(event);
              final int action = event.getAction();
              if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
                  mClickTarget = null;
              }
              return true;
          }
          return false;
      }
  
      @Override
      public boolean onTouch(View v, MotionEvent e) {
          // Note: There are no mixed events. When a new device gets used (e.g. 1. Mouse, 2. touch)
          // the old input device events get cancelled first. So no need to remember the kind of
          // input device we are listening to.
          final int x = (int) e.getX();
          final int y = (int) e.getY();
          final boolean fromMouse = e.getToolType(e.getActionIndex()) == MotionEvent.TOOL_TYPE_MOUSE;
          final boolean primaryButton = (e.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0;
          final int actionMasked = e.getActionMasked();
          switch (actionMasked) {
              case MotionEvent.ACTION_DOWN:
                  if (!mShow) {
                      // When there is no caption we should not react to anything.
                      return false;
                  }
                  // Checking for a drag action is started if we aren't dragging already and the
                  // starting event is either a left mouse button or any other input device.
                  if (!fromMouse || primaryButton) {
                      mCheckForDragging = true;
                      mTouchDownX = x;
                      mTouchDownY = y;
                  }
                  break;
  
              case MotionEvent.ACTION_MOVE:
                  if (!mDragging && mCheckForDragging && (fromMouse || passedSlop(x, y))) {
                      mCheckForDragging = false;
                      mDragging = true;
                      startMovingTask(e.getRawX(), e.getRawY());
                      // After the above call the framework will take over the input.
                      // This handler will receive ACTION_CANCEL soon (possible after a few spurious
                      // ACTION_MOVE events which are safe to ignore).
                  }
                  break;
  
              case MotionEvent.ACTION_UP:
              case MotionEvent.ACTION_CANCEL:
                  if (!mDragging) {
                      break;
                  }
                  // Abort the ongoing dragging.
                  if (actionMasked == MotionEvent.ACTION_UP) {
                      // If it receives ACTION_UP event, the dragging is already finished and also
                      // the system can not end drag on ACTION_UP event. So request to finish
                      // dragging.
                      finishMovingTask();
                  }
                  mDragging = false;
                  return !mCheckForDragging;
          }
          return mDragging || mCheckForDragging;
      }
  
      @Override
      public ArrayList<View> buildTouchDispatchChildList() {
          mTouchDispatchList.ensureCapacity(3);
          if (mCaption != null) {
              mTouchDispatchList.add(mCaption);
          }
          if (mContent != null) {
              mTouchDispatchList.add(mContent);
          }
          return mTouchDispatchList;
      }
  
      @Override
      public boolean shouldDelayChildPressedState() {
          return false;
      }
  
      private boolean passedSlop(int x, int y) {
          return Math.abs(x - mTouchDownX) > mDragSlop || Math.abs(y - mTouchDownY) > mDragSlop;
      }
  
      /**
       * The phone window configuration has changed and the caption needs to be updated.
       * @param show True if the caption should be shown.
       */
      public void onConfigurationChanged(boolean show) {
          mShow = show;
          updateCaptionVisibility();
      }
  
      @Override
      public void addView(View child, int index, ViewGroup.LayoutParams params) {
          if (!(params instanceof MarginLayoutParams)) {
              throw new IllegalArgumentException(
                      "params " + params + " must subclass MarginLayoutParams");
          }
          // Make sure that we never get more then one client area in our view.
          if (index >= 2 || getChildCount() >= 2) {
              throw new IllegalStateException("DecorCaptionView can only handle 1 client view");
          }
          // To support the overlaying content in the caption, we need to put the content view as the
          // first child to get the right Z-Ordering.
          super.addView(child, 0, params);
          mContent = child;
      }
  
      @Override
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
          final int captionHeight;
          if (mCaption.getVisibility() != View.GONE) {
              measureChildWithMargins(mCaption, widthMeasureSpec, 0, heightMeasureSpec, 0);
              captionHeight = mCaption.getMeasuredHeight();
          } else {
              captionHeight = 0;
          }
          if (mContent != null) {
              if (mOverlayWithAppContent) {
                  measureChildWithMargins(mContent, widthMeasureSpec, 0, heightMeasureSpec, 0);
              } else {
                  measureChildWithMargins(mContent, widthMeasureSpec, 0, heightMeasureSpec,
                          captionHeight);
              }
          }
  
          setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
                  MeasureSpec.getSize(heightMeasureSpec));
      }
  
      @Override
      protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
          final int captionHeight;
          if (mCaption.getVisibility() != View.GONE) {
              mCaption.layout(0, 0, mCaption.getMeasuredWidth(), mCaption.getMeasuredHeight());
              captionHeight = mCaption.getBottom() - mCaption.getTop();
              mMaximize.getHitRect(mMaximizeRect);
              mClose.getHitRect(mCloseRect);
          } else {
              captionHeight = 0;
              mMaximizeRect.setEmpty();
              mCloseRect.setEmpty();
          }
  
          if (mContent != null) {
              if (mOverlayWithAppContent) {
                  mContent.layout(0, 0, mContent.getMeasuredWidth(), mContent.getMeasuredHeight());
              } else {
                  mContent.layout(0, captionHeight, mContent.getMeasuredWidth(),
                          captionHeight + mContent.getMeasuredHeight());
              }
          }
  
          // This assumes that the caption bar is at the top.
          mOwner.notifyRestrictedCaptionAreaCallback(mMaximize.getLeft(), mMaximize.getTop(),
                  mClose.getRight(), mClose.getBottom());
      }
  
      /**
       * Updates the visibility of the caption.
       **/
      private void updateCaptionVisibility() {
          mCaption.setVisibility(mShow ? VISIBLE : GONE);
          mCaption.setOnTouchListener(this);
      }
  
      /**
       * Maximize or restore the window by moving it to the maximized or freeform workspace stack.
       **/
      private void toggleFreeformWindowingMode() {
          Window.WindowControllerCallback callback = mOwner.getWindowControllerCallback();
          if (callback != null) {
              try {
                  callback.toggleFreeformWindowingMode();
              } catch (RemoteException ex) {
                  Log.e(TAG, "Cannot change task workspace.");
              }
          }
      }
  
      public boolean isCaptionShowing() {
          return mShow;
      }
  
      public int getCaptionHeight() {
          return (mCaption != null) ? mCaption.getHeight() : 0;
      }
  
      public void removeContentView() {
          if (mContent != null) {
              removeView(mContent);
              mContent = null;
          }
      }
  
      public View getCaption() {
          return mCaption;
      }
  
      @Override
      public LayoutParams generateLayoutParams(AttributeSet attrs) {
          return new MarginLayoutParams(getContext(), attrs);
      }
  
      @Override
      protected LayoutParams generateDefaultLayoutParams() {
          return new MarginLayoutParams(MarginLayoutParams.MATCH_PARENT,
                  MarginLayoutParams.MATCH_PARENT);
      }
  
      @Override
      protected LayoutParams generateLayoutParams(LayoutParams p) {
          return new MarginLayoutParams(p);
      }
  
      @Override
      protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
          return p instanceof MarginLayoutParams;
      }
  
      @Override
      public boolean onDown(MotionEvent e) {
          return false;
      }
  
      @Override
      public void onShowPress(MotionEvent e) {
  
      }
  
      @Override
      public boolean onSingleTapUp(MotionEvent e) {
          if (mClickTarget == mMaximize) {
              toggleFreeformWindowingMode();
          } else if (mClickTarget == mClose) {
              mOwner.dispatchOnWindowDismissed(
                      true /*finishTask*/, false /*suppressWindowTransition*/);
          }
          return true;
      }
  
      @Override
      public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
          return false;
      }
  
      @Override
      public void onLongPress(MotionEvent e) {
  
      }
  
      @Override
      public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
          return false;
      }
  
      /**
       * Called when {@link android.view.ViewRootImpl} scrolls for adjustPan.
       */
      public void onRootViewScrollYChanged(int scrollY) {
          // Offset the caption opposite the root scroll. This keeps the caption at the
          // top of the window during adjustPan.
          if (mCaption != null) {
              mRootScrollY = scrollY;
              mCaption.setTranslationY(scrollY);
          }
      }
  }
  