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 android.widget;
  
  
  import android.annotation.NonNull;
  import android.content.Context;
  import android.view.MotionEvent;
  import android.view.View;
  
  import com.android.internal.widget.AutoScrollHelper.AbsListViewAutoScroller;
  
  /**
   * Wrapper class for a ListView. This wrapper can hijack the focus to
   * make sure the list uses the appropriate drawables and states when
   * displayed on screen within a drop down. The focus is never actually
   * passed to the drop down in this mode; the list only looks focused.
   *
   * @hide
   */
  public class DropDownListView extends ListView {
      /*
       * WARNING: This is a workaround for a touch mode issue.
       *
       * Touch mode is propagated lazily to windows. This causes problems in
       * the following scenario:
       * - Type something in the AutoCompleteTextView and get some results
       * - Move down with the d-pad to select an item in the list
       * - Move up with the d-pad until the selection disappears
       * - Type more text in the AutoCompleteTextView *using the soft keyboard*
       *   and get new results; you are now in touch mode
       * - The selection comes back on the first item in the list, even though
       *   the list is supposed to be in touch mode
       *
       * Using the soft keyboard triggers the touch mode change but that change
       * is propagated to our window only after the first list layout, therefore
       * after the list attempts to resurrect the selection.
       *
       * The trick to work around this issue is to pretend the list is in touch
       * mode when we know that the selection should not appear, that is when
       * we know the user moved the selection away from the list.
       *
       * This boolean is set to true whenever we explicitly hide the list's
       * selection and reset to false whenever we know the user moved the
       * selection back to the list.
       *
       * When this boolean is true, isInTouchMode() returns true, otherwise it
       * returns super.isInTouchMode().
       */
      private boolean mListSelectionHidden;
  
      /**
       * True if this wrapper should fake focus.
       */
      private boolean mHijackFocus;
  
      /** Whether to force drawing of the pressed state selector. */
      private boolean mDrawsInPressedState;
  
      /** Helper for drag-to-open auto scrolling. */
      private AbsListViewAutoScroller mScrollHelper;
  
      /**
       * Runnable posted when we are awaiting hover event resolution. When set,
       * drawable state changes are postponed.
       */
      private ResolveHoverRunnable mResolveHoverRunnable;
  
      /**
       * Creates a new list view wrapper.
       *
       * @param context this view's context
       */
      public DropDownListView(@NonNull Context context, boolean hijackFocus) {
          this(context, hijackFocus, com.android.internal.R.attr.dropDownListViewStyle);
      }
  
      /**
       * Creates a new list view wrapper.
       *
       * @param context this view's context
       */
      public DropDownListView(@NonNull Context context, boolean hijackFocus, int defStyleAttr) {
          super(context, null, defStyleAttr);
          mHijackFocus = hijackFocus;
          // TODO: Add an API to control this
          setCacheColorHint(0); // Transparent, since the background drawable could be anything.
      }
  
      @Override
      boolean shouldShowSelector() {
          return isHovered() || super.shouldShowSelector();
      }
  
      @Override
      public boolean onTouchEvent(MotionEvent ev) {
          if (mResolveHoverRunnable != null) {
              // Resolved hover event as hover => touch transition.
              mResolveHoverRunnable.cancel();
          }
  
          return super.onTouchEvent(ev);
      }
  
      @Override
      public boolean onHoverEvent(@NonNull MotionEvent ev) {
          final int action = ev.getActionMasked();
          if (action == MotionEvent.ACTION_HOVER_EXIT && mResolveHoverRunnable == null) {
              // This may be transitioning to TOUCH_DOWN. Postpone drawable state
              // updates until either the next frame or the next touch event.
              mResolveHoverRunnable = new ResolveHoverRunnable();
              mResolveHoverRunnable.post();
          }
  
          // Allow the super class to handle hover state management first.
          final boolean handled = super.onHoverEvent(ev);
  
          if (action == MotionEvent.ACTION_HOVER_ENTER
                  || action == MotionEvent.ACTION_HOVER_MOVE) {
              final int position = pointToPosition((int) ev.getX(), (int) ev.getY());
              if (position != INVALID_POSITION && position != mSelectedPosition) {
                  final View hoveredItem = getChildAt(position - getFirstVisiblePosition());
                  if (hoveredItem.isEnabled()) {
                      // Force a focus so that the proper selector state gets
                      // used when we update.
                      requestFocus();
  
                      positionSelector(position, hoveredItem);
                      setSelectedPositionInt(position);
                      setNextSelectedPositionInt(position);
                  }
                  updateSelectorState();
              }
          } else {
              // Do not cancel the selected position if the selection is visible
              // by other means.
              if (!super.shouldShowSelector()) {
                  setSelectedPositionInt(INVALID_POSITION);
                  setNextSelectedPositionInt(INVALID_POSITION);
              }
          }
  
          return handled;
      }
  
      @Override
      protected void drawableStateChanged() {
          if (mResolveHoverRunnable == null) {
              super.drawableStateChanged();
          }
      }
  
      /**
       * Handles forwarded events.
       *
       * @param activePointerId id of the pointer that activated forwarding
       * @return whether the event was handled
       */
      public boolean onForwardedEvent(@NonNull MotionEvent event, int activePointerId) {
          boolean handledEvent = true;
          boolean clearPressedItem = false;
  
          final int actionMasked = event.getActionMasked();
          switch (actionMasked) {
              case MotionEvent.ACTION_CANCEL:
                  handledEvent = false;
                  break;
              case MotionEvent.ACTION_UP:
                  handledEvent = false;
                  // $FALL-THROUGH$
              case MotionEvent.ACTION_MOVE:
                  final int activeIndex = event.findPointerIndex(activePointerId);
                  if (activeIndex < 0) {
                      handledEvent = false;
                      break;
                  }
  
                  final int x = (int) event.getX(activeIndex);
                  final int y = (int) event.getY(activeIndex);
                  final int position = pointToPosition(x, y);
                  if (position == INVALID_POSITION) {
                      clearPressedItem = true;
                      break;
                  }
  
                  final View child = getChildAt(position - getFirstVisiblePosition());
                  setPressedItem(child, position, x, y);
                  handledEvent = true;
  
                  if (actionMasked == MotionEvent.ACTION_UP) {
                      final long id = getItemIdAtPosition(position);
                      performItemClick(child, position, id);
                  }
                  break;
          }
  
          // Failure to handle the event cancels forwarding.
          if (!handledEvent || clearPressedItem) {
              clearPressedItem();
          }
  
          // Manage automatic scrolling.
          if (handledEvent) {
              if (mScrollHelper == null) {
                  mScrollHelper = new AbsListViewAutoScroller(this);
              }
              mScrollHelper.setEnabled(true);
              mScrollHelper.onTouch(this, event);
          } else if (mScrollHelper != null) {
              mScrollHelper.setEnabled(false);
          }
  
          return handledEvent;
      }
  
      /**
       * Sets whether the list selection is hidden, as part of a workaround for a
       * touch mode issue (see the declaration for mListSelectionHidden).
       *
       * @param hideListSelection {@code true} to hide list selection,
       *                          {@code false} to show
       */
      public void setListSelectionHidden(boolean hideListSelection) {
          mListSelectionHidden = hideListSelection;
      }
  
      private void clearPressedItem() {
          mDrawsInPressedState = false;
          setPressed(false);
          updateSelectorState();
  
          final View motionView = getChildAt(mMotionPosition - mFirstPosition);
          if (motionView != null) {
              motionView.setPressed(false);
          }
      }
  
      private void setPressedItem(@NonNull View child, int position, float x, float y) {
          mDrawsInPressedState = true;
  
          // Ordering is essential. First, update the container's pressed state.
          drawableHotspotChanged(x, y);
          if (!isPressed()) {
              setPressed(true);
          }
  
          // Next, run layout if we need to stabilize child positions.
          if (mDataChanged) {
              layoutChildren();
          }
  
          // Manage the pressed view based on motion position. This allows us to
          // play nicely with actual touch and scroll events.
          final View motionView = getChildAt(mMotionPosition - mFirstPosition);
          if (motionView != null && motionView != child && motionView.isPressed()) {
              motionView.setPressed(false);
          }
          mMotionPosition = position;
  
          // Offset for child coordinates.
          final float childX = x - child.getLeft();
          final float childY = y - child.getTop();
          child.drawableHotspotChanged(childX, childY);
          if (!child.isPressed()) {
              child.setPressed(true);
          }
  
          // Ensure that keyboard focus starts from the last touched position.
          setSelectedPositionInt(position);
          positionSelectorLikeTouch(position, child, x, y);
  
          // Refresh the drawable state to reflect the new pressed state,
          // which will also update the selector state.
          refreshDrawableState();
      }
  
      @Override
      boolean touchModeDrawsInPressedState() {
          return mDrawsInPressedState || super.touchModeDrawsInPressedState();
      }
  
      /**
       * Avoids jarring scrolling effect by ensuring that list elements
       * made of a text view fit on a single line.
       *
       * @param position the item index in the list to get a view for
       * @return the view for the specified item
       */
      @Override
      View obtainView(int position, boolean[] isScrap) {
          View view = super.obtainView(position, isScrap);
  
          if (view instanceof TextView) {
              ((TextView) view).setHorizontallyScrolling(true);
          }
  
          return view;
      }
  
      @Override
      public boolean isInTouchMode() {
          // WARNING: Please read the comment where mListSelectionHidden is declared
          return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode();
      }
  
      /**
       * Returns the focus state in the drop down.
       *
       * @return true always if hijacking focus
       */
      @Override
      public boolean hasWindowFocus() {
          return mHijackFocus || super.hasWindowFocus();
      }
  
      /**
       * Returns the focus state in the drop down.
       *
       * @return true always if hijacking focus
       */
      @Override
      public boolean isFocused() {
          return mHijackFocus || super.isFocused();
      }
  
      /**
       * Returns the focus state in the drop down.
       *
       * @return true always if hijacking focus
       */
      @Override
      public boolean hasFocus() {
          return mHijackFocus || super.hasFocus();
      }
  
      /**
       * Runnable that forces hover event resolution and updates drawable state.
       */
      private class ResolveHoverRunnable implements Runnable {
          @Override
          public void run() {
              // Resolved hover event as standard hover exit.
              mResolveHoverRunnable = null;
              drawableStateChanged();
          }
  
          public void cancel() {
              mResolveHoverRunnable = null;
              removeCallbacks(this);
          }
  
          public void post() {
              DropDownListView.this.post(this);
          }
      }
  }