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.view;
  
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.content.Context;
  import android.graphics.Point;
  import android.graphics.Rect;
  import android.view.ActionMode;
  import android.view.Menu;
  import android.view.MenuInflater;
  import android.view.MenuItem;
  import android.view.View;
  import android.view.ViewConfiguration;
  import android.view.ViewGroup;
  import android.view.ViewParent;
  import android.view.WindowManager;
  
  import android.widget.PopupWindow;
  import com.android.internal.R;
  import com.android.internal.util.Preconditions;
  import com.android.internal.view.menu.MenuBuilder;
  import com.android.internal.widget.FloatingToolbar;
  
  import java.util.Arrays;
  
  public final class FloatingActionMode extends ActionMode {
  
      private static final int MAX_HIDE_DURATION = 3000;
      private static final int MOVING_HIDE_DELAY = 50;
  
      @NonNull private final Context mContext;
      @NonNull private final ActionMode.Callback2 mCallback;
      @NonNull private final MenuBuilder mMenu;
      @NonNull private final Rect mContentRect;
      @NonNull private final Rect mContentRectOnScreen;
      @NonNull private final Rect mPreviousContentRectOnScreen;
      @NonNull private final int[] mViewPositionOnScreen;
      @NonNull private final int[] mPreviousViewPositionOnScreen;
      @NonNull private final int[] mRootViewPositionOnScreen;
      @NonNull private final Rect mViewRectOnScreen;
      @NonNull private final Rect mPreviousViewRectOnScreen;
      @NonNull private final Rect mScreenRect;
      @NonNull private final View mOriginatingView;
      @NonNull private final Point mDisplaySize;
      private final int mBottomAllowance;
  
      private final Runnable mMovingOff = new Runnable() {
          public void run() {
              if (isViewStillActive()) {
                  mFloatingToolbarVisibilityHelper.setMoving(false);
                  mFloatingToolbarVisibilityHelper.updateToolbarVisibility();
              }
          }
      };
  
      private final Runnable mHideOff = new Runnable() {
          public void run() {
              if (isViewStillActive()) {
                  mFloatingToolbarVisibilityHelper.setHideRequested(false);
                  mFloatingToolbarVisibilityHelper.updateToolbarVisibility();
              }
          }
      };
  
      @NonNull private FloatingToolbar mFloatingToolbar;
      @NonNull private FloatingToolbarVisibilityHelper mFloatingToolbarVisibilityHelper;
  
      public FloatingActionMode(
              Context context, ActionMode.Callback2 callback,
              View originatingView, FloatingToolbar floatingToolbar) {
          mContext = Preconditions.checkNotNull(context);
          mCallback = Preconditions.checkNotNull(callback);
          mMenu = new MenuBuilder(context).setDefaultShowAsAction(
                  MenuItem.SHOW_AS_ACTION_IF_ROOM);
          setType(ActionMode.TYPE_FLOATING);
          mMenu.setCallback(new MenuBuilder.Callback() {
              @Override
              public void onMenuModeChange(MenuBuilder menu) {}
  
              @Override
              public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
                  return mCallback.onActionItemClicked(FloatingActionMode.this, item);
              }
          });
          mContentRect = new Rect();
          mContentRectOnScreen = new Rect();
          mPreviousContentRectOnScreen = new Rect();
          mViewPositionOnScreen = new int[2];
          mPreviousViewPositionOnScreen = new int[2];
          mRootViewPositionOnScreen = new int[2];
          mViewRectOnScreen = new Rect();
          mPreviousViewRectOnScreen = new Rect();
          mScreenRect = new Rect();
          mOriginatingView = Preconditions.checkNotNull(originatingView);
          mOriginatingView.getLocationOnScreen(mViewPositionOnScreen);
          // Allow the content rect to overshoot a little bit beyond the
          // bottom view bound if necessary.
          mBottomAllowance = context.getResources()
                  .getDimensionPixelSize(R.dimen.content_rect_bottom_clip_allowance);
          mDisplaySize = new Point();
          setFloatingToolbar(Preconditions.checkNotNull(floatingToolbar));
      }
  
      private void setFloatingToolbar(FloatingToolbar floatingToolbar) {
          mFloatingToolbar = floatingToolbar
                  .setMenu(mMenu)
                  .setOnMenuItemClickListener(item -> mMenu.performItemAction(item, 0));
          mFloatingToolbarVisibilityHelper = new FloatingToolbarVisibilityHelper(mFloatingToolbar);
          mFloatingToolbarVisibilityHelper.activate();
      }
  
      @Override
      public void setTitle(CharSequence title) {}
  
      @Override
      public void setTitle(int resId) {}
  
      @Override
      public void setSubtitle(CharSequence subtitle) {}
  
      @Override
      public void setSubtitle(int resId) {}
  
      @Override
      public void setCustomView(View view) {}
  
      @Override
      public void invalidate() {
          mCallback.onPrepareActionMode(this, mMenu);
          invalidateContentRect();  // Will re-layout and show the toolbar if necessary.
      }
  
      @Override
      public void invalidateContentRect() {
          mCallback.onGetContentRect(this, mOriginatingView, mContentRect);
          repositionToolbar();
      }
  
      public void updateViewLocationInWindow() {
          mOriginatingView.getLocationOnScreen(mViewPositionOnScreen);
          mOriginatingView.getRootView().getLocationOnScreen(mRootViewPositionOnScreen);
          mOriginatingView.getGlobalVisibleRect(mViewRectOnScreen);
          mViewRectOnScreen.offset(mRootViewPositionOnScreen[0], mRootViewPositionOnScreen[1]);
  
          if (!Arrays.equals(mViewPositionOnScreen, mPreviousViewPositionOnScreen)
                  || !mViewRectOnScreen.equals(mPreviousViewRectOnScreen)) {
              repositionToolbar();
              mPreviousViewPositionOnScreen[0] = mViewPositionOnScreen[0];
              mPreviousViewPositionOnScreen[1] = mViewPositionOnScreen[1];
              mPreviousViewRectOnScreen.set(mViewRectOnScreen);
          }
      }
  
      private void repositionToolbar() {
          mContentRectOnScreen.set(mContentRect);
  
          // Offset the content rect into screen coordinates, taking into account any transformations
          // that may be applied to the originating view or its ancestors.
          final ViewParent parent = mOriginatingView.getParent();
          if (parent instanceof ViewGroup) {
              ((ViewGroup) parent).getChildVisibleRect(
                      mOriginatingView, mContentRectOnScreen,
                      null /* offset */, true /* forceParentCheck */);
              mContentRectOnScreen.offset(mRootViewPositionOnScreen[0], mRootViewPositionOnScreen[1]);
          } else {
              mContentRectOnScreen.offset(mViewPositionOnScreen[0], mViewPositionOnScreen[1]);
          }
  
          if (isContentRectWithinBounds()) {
              mFloatingToolbarVisibilityHelper.setOutOfBounds(false);
              // Make sure that content rect is not out of the view's visible bounds.
              mContentRectOnScreen.set(
                      Math.max(mContentRectOnScreen.left, mViewRectOnScreen.left),
                      Math.max(mContentRectOnScreen.top, mViewRectOnScreen.top),
                      Math.min(mContentRectOnScreen.right, mViewRectOnScreen.right),
                      Math.min(mContentRectOnScreen.bottom,
                              mViewRectOnScreen.bottom + mBottomAllowance));
  
              if (!mContentRectOnScreen.equals(mPreviousContentRectOnScreen)) {
                  // Content rect is moving.
                  mOriginatingView.removeCallbacks(mMovingOff);
                  mFloatingToolbarVisibilityHelper.setMoving(true);
                  mOriginatingView.postDelayed(mMovingOff, MOVING_HIDE_DELAY);
  
                  mFloatingToolbar.setContentRect(mContentRectOnScreen);
                  mFloatingToolbar.updateLayout();
              }
          } else {
              mFloatingToolbarVisibilityHelper.setOutOfBounds(true);
              mContentRectOnScreen.setEmpty();
          }
          mFloatingToolbarVisibilityHelper.updateToolbarVisibility();
  
          mPreviousContentRectOnScreen.set(mContentRectOnScreen);
      }
  
      private boolean isContentRectWithinBounds() {
          mContext.getSystemService(WindowManager.class)
              .getDefaultDisplay().getRealSize(mDisplaySize);
          mScreenRect.set(0, 0, mDisplaySize.x, mDisplaySize.y);
  
          return intersectsClosed(mContentRectOnScreen, mScreenRect)
              && intersectsClosed(mContentRectOnScreen, mViewRectOnScreen);
      }
  
      /*
       * Same as Rect.intersects, but includes cases where the rectangles touch.
      */
      private static boolean intersectsClosed(Rect a, Rect b) {
           return a.left <= b.right && b.left <= a.right
                   && a.top <= b.bottom && b.top <= a.bottom;
      }
  
      @Override
      public void hide(long duration) {
          if (duration == ActionMode.DEFAULT_HIDE_DURATION) {
              duration = ViewConfiguration.getDefaultActionModeHideDuration();
          }
          duration = Math.min(MAX_HIDE_DURATION, duration);
          mOriginatingView.removeCallbacks(mHideOff);
          if (duration <= 0) {
              mHideOff.run();
          } else {
              mFloatingToolbarVisibilityHelper.setHideRequested(true);
              mFloatingToolbarVisibilityHelper.updateToolbarVisibility();
              mOriginatingView.postDelayed(mHideOff, duration);
          }
      }
  
      /**
       * If this is set to true, the action mode view will dismiss itself on touch events outside of
       * its window. This only makes sense if the action mode view is a PopupWindow that is touchable
       * but not focusable, which means touches outside of the window will be delivered to the window
       * behind. The default is false.
       *
       * This is for internal use only and the approach to this may change.
       * @hide
       *
       * @param outsideTouchable whether or not this action mode is "outside touchable"
       * @param onDismiss optional. Sets a callback for when this action mode popup dismisses itself
       */
      public void setOutsideTouchable(
              boolean outsideTouchable, @Nullable PopupWindow.OnDismissListener onDismiss) {
          mFloatingToolbar.setOutsideTouchable(outsideTouchable, onDismiss);
      }
  
      @Override
      public void onWindowFocusChanged(boolean hasWindowFocus) {
          mFloatingToolbarVisibilityHelper.setWindowFocused(hasWindowFocus);
          mFloatingToolbarVisibilityHelper.updateToolbarVisibility();
      }
  
      @Override
      public void finish() {
          reset();
          mCallback.onDestroyActionMode(this);
      }
  
      @Override
      public Menu getMenu() {
          return mMenu;
      }
  
      @Override
      public CharSequence getTitle() {
          return null;
      }
  
      @Override
      public CharSequence getSubtitle() {
          return null;
      }
  
      @Override
      public View getCustomView() {
          return null;
      }
  
      @Override
      public MenuInflater getMenuInflater() {
          return new MenuInflater(mContext);
      }
  
      private void reset() {
          mFloatingToolbar.dismiss();
          mFloatingToolbarVisibilityHelper.deactivate();
          mOriginatingView.removeCallbacks(mMovingOff);
          mOriginatingView.removeCallbacks(mHideOff);
      }
  
      private boolean isViewStillActive() {
          return mOriginatingView.getWindowVisibility() == View.VISIBLE
                  && mOriginatingView.isShown();
      }
  
      /**
       * A helper for showing/hiding the floating toolbar depending on certain states.
       */
      private static final class FloatingToolbarVisibilityHelper {
  
          private static final long MIN_SHOW_DURATION_FOR_MOVE_HIDE = 500;
  
          private final FloatingToolbar mToolbar;
  
          private boolean mHideRequested;
          private boolean mMoving;
          private boolean mOutOfBounds;
          private boolean mWindowFocused = true;
  
          private boolean mActive;
  
          private long mLastShowTime;
  
          public FloatingToolbarVisibilityHelper(FloatingToolbar toolbar) {
              mToolbar = Preconditions.checkNotNull(toolbar);
          }
  
          public void activate() {
              mHideRequested = false;
              mMoving = false;
              mOutOfBounds = false;
              mWindowFocused = true;
  
              mActive = true;
          }
  
          public void deactivate() {
              mActive = false;
              mToolbar.dismiss();
          }
  
          public void setHideRequested(boolean hide) {
              mHideRequested = hide;
          }
  
          public void setMoving(boolean moving) {
              // Avoid unintended flickering by allowing the toolbar to show long enough before
              // triggering the 'moving' flag - which signals a hide.
              final boolean showingLongEnough =
                  System.currentTimeMillis() - mLastShowTime > MIN_SHOW_DURATION_FOR_MOVE_HIDE;
              if (!moving || showingLongEnough) {
                  mMoving = moving;
              }
          }
  
          public void setOutOfBounds(boolean outOfBounds) {
              mOutOfBounds = outOfBounds;
          }
  
          public void setWindowFocused(boolean windowFocused) {
              mWindowFocused = windowFocused;
          }
  
          public void updateToolbarVisibility() {
              if (!mActive) {
                  return;
              }
  
              if (mHideRequested || mMoving || mOutOfBounds || !mWindowFocused) {
                  mToolbar.hide();
              } else {
                  mToolbar.show();
                  mLastShowTime = System.currentTimeMillis();
              }
          }
      }
  }
  