content: /*
   * Copyright (C) 2014 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 androidx.test.espresso.action;
  
  import static androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast;
  import static androidx.test.internal.util.Checks.checkArgument;
  
  import android.database.Cursor;
  import android.util.Log;
  import android.view.View;
  import android.widget.AbsListView;
  import android.widget.Adapter;
  import android.widget.AdapterView;
  import android.widget.AdapterViewAnimator;
  import android.widget.AdapterViewFlipper;
  import androidx.annotation.Nullable;
  import androidx.test.espresso.util.EspressoOptional;
  import java.util.ArrayList;
  import java.util.List;
  
  /** Implementations of {@link AdapterViewProtocol} for standard SDK Widgets. */
  public final class AdapterViewProtocols {
  
    /**
     * Consider views which have over this percentage of their area visible to the user to be fully
     * rendered.
     */
    private static final int FULLY_RENDERED_PERCENTAGE_CUTOFF = 90;
  
    private AdapterViewProtocols() {}
  
    private static final AdapterViewProtocol STANDARD_PROTOCOL = new StandardAdapterViewProtocol();
  
    /**
     * Creates an implementation of AdapterViewProtocol that can work with AdapterViews that do not
     * break method contracts on AdapterView.
     */
    public static AdapterViewProtocol standardProtocol() {
      return STANDARD_PROTOCOL;
    }
  
    private static final class StandardAdapterViewProtocol implements AdapterViewProtocol {
  
      private static final String TAG = "StdAdapterViewProtocol";
  
      /** Required for Espresso remote serialization, called reflectively. */
      public StandardAdapterViewProtocol() {}
  
      private static final class StandardDataFunction implements DataFunction {
        private final Object dataAtPosition;
        private final int position;
  
        private StandardDataFunction(Object dataAtPosition, int position) {
          checkArgument(position >= 0, "position must be >= 0");
          this.dataAtPosition = dataAtPosition;
          this.position = position;
        }
  
        @Override
        public Object getData() {
          if (dataAtPosition instanceof Cursor) {
            if (!((Cursor) dataAtPosition).moveToPosition(position)) {
              Log.e(TAG, "Cannot move cursor to position: " + position);
            }
          }
          return dataAtPosition;
        }
      }
  
      @Override
      public Iterable<AdaptedData> getDataInAdapterView(AdapterView<? extends Adapter> adapterView) {
        List<AdaptedData> datas = new ArrayList<>();
        for (int i = 0; i < adapterView.getCount(); i++) {
          int position = i;
          Object dataAtPosition = adapterView.getItemAtPosition(position);
          datas.add(
              new AdaptedData.Builder()
                  .withDataFunction(new StandardDataFunction(dataAtPosition, position))
                  .withOpaqueToken(position)
                  .build());
        }
        return datas;
      }
  
      /**
       * @deprecated use {@link #getDataRenderedByView2(AdapterView, View)}
       */
      @Override
      @Deprecated
      public EspressoOptional<AdaptedData> getDataRenderedByView(
          AdapterView<? extends Adapter> adapterView, View descendantView) {
        return EspressoOptional.of(getDataRenderedByView2(adapterView, descendantView));
      }
  
      @Override
      @Nullable
      public AdaptedData getDataRenderedByView2(
          AdapterView<? extends Adapter> adapterView, View descendantView) {
        if (adapterView == descendantView.getParent()) {
          int position = adapterView.getPositionForView(descendantView);
          if (position != AdapterView.INVALID_POSITION) {
            return new AdaptedData.Builder()
                .withDataFunction(
                    new StandardDataFunction(adapterView.getItemAtPosition(position), position))
                .withOpaqueToken(Integer.valueOf(position))
                .build();
          }
        }
        return null;
      }
  
      @Override
      public void makeDataRenderedWithinAdapterView(
          AdapterView<? extends Adapter> adapterView, AdaptedData data) {
        checkArgument(data.opaqueToken instanceof Integer, "Not my data: %s", data);
        int position = ((Integer) data.opaqueToken).intValue();
  
        boolean moved = false;
        // set selection should always work, we can give a little better experience if per subtype
        // though.
        if (adapterView instanceof AbsListView) {
          ((AbsListView) adapterView)
              .smoothScrollToPositionFromTop(position, adapterView.getPaddingTop(), 0);
  
          moved = true;
        }
  
        if (adapterView instanceof AdapterViewAnimator) {
          if (adapterView instanceof AdapterViewFlipper) {
            ((AdapterViewFlipper) adapterView).stopFlipping();
          }
          ((AdapterViewAnimator) adapterView).setDisplayedChild(position);
          moved = true;
        }
        if (!moved) {
          adapterView.setSelection(position);
        }
      }
  
      @Override
      public boolean isDataRenderedWithinAdapterView(
          AdapterView<? extends Adapter> adapterView, AdaptedData adaptedData) {
        checkArgument(adaptedData.opaqueToken instanceof Integer, "Not my data: %s", adaptedData);
        int dataPosition = ((Integer) adaptedData.opaqueToken).intValue();
        boolean inView = false;
  
        if (dataPosition >= adapterView.getFirstVisiblePosition()
            && dataPosition <= adapterView.getLastVisiblePosition()) {
          if (adapterView.getFirstVisiblePosition() == adapterView.getLastVisiblePosition()) {
            // thats a huge element.
            inView = true;
          } else {
            inView =
                isElementFullyRendered(
                    adapterView, dataPosition - adapterView.getFirstVisiblePosition());
          }
        }
        if (inView) {
          // stops animations - locks in our x/y location.
          adapterView.setSelection(dataPosition);
        }
  
        return inView;
      }
  
      private boolean isElementFullyRendered(
          AdapterView<? extends Adapter> adapterView, int childAt) {
        View element = adapterView.getChildAt(childAt);
        // Occassionally we'll have to fight with smooth scrolling logic on our definition of when
        // there is extra scrolling to be done. In particular if the element is the first or last
        // element of the list, the smooth scroller may decide that no work needs to be done to scroll
        // to the element if a certain percentage of it is on screen. Ugh. Sigh. Yuck.
  
        return isDisplayingAtLeast(FULLY_RENDERED_PERCENTAGE_CUTOFF).matches(element);
      }
    }
  }
  