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.internal.util.Checks.checkNotNull;
  
  import android.database.Cursor;
  import android.view.View;
  import android.widget.Adapter;
  import android.widget.AdapterView;
  import androidx.annotation.Nullable;
  import androidx.test.espresso.util.EspressoOptional;
  import androidx.test.espresso.util.HumanReadables;
  import java.util.Locale;
  
  /**
   * A sadly necessary layer of indirection to interact with AdapterViews.
   *
   * <p>Generally any subclass should respect the contracts and behaviors of its superclass. Otherwise
   * it becomes impossible to work generically with objects that all claim to share a supertype - you
   * need special cases to perform the same operation 'owned' by the supertype for each sub-type. The
   * 'is - a' relationship is broken.
   *
   * <p>Android breaks the Liskov substitution principal with ExpandableListView - you can't use
   * getAdapter(), getItemAtPosition(), and other methods common to AdapterViews on an
   * ExpandableListView because an ExpandableListView isn't an adapterView - they just share a lot of
   * code.
   *
   * <p>This interface exists to work around this wart (which sadly is copied in other projects too)
   * and lets the implementor translate Espresso's needs and manipulations of the AdapterView into
   * calls that make sense for the given subtype and context.
   *
   * <p><i> If you have to implement this to talk to widgets your own project defines - I'm sorry.
   * </i>
   */
  public interface AdapterViewProtocol {
  
    /**
     * Returns all data this AdapterViewProtocol can find within the given AdapterView.
     *
     * <p>Any AdaptedData returned by this method can be passed to makeDataRenderedWithinView and the
     * implementation should make the AdapterView bring that data item onto the screen.
     *
     * @param adapterView the AdapterView we want to interrogate the contents of.
     * @return an {@link Iterable} of AdaptedDatas representing all data the implementation sees in
     *     this view
     * @throws IllegalArgumentException if the implementation doesn't know how to manipulate the given
     *     adapter view.
     */
    Iterable<AdaptedData> getDataInAdapterView(AdapterView<? extends Adapter> adapterView);
  
    /**
     * @deprecated use {@link #getDataRenderedByView2(AdapterView, View)}
     */
    @Deprecated
    EspressoOptional<AdaptedData> getDataRenderedByView(
        AdapterView<? extends Adapter> adapterView, View descendantView);
  
    /**
     * Returns the data object this particular view is rendering if possible.
     *
     * <p>Implementations are expected to create a relationship between the data in the AdapterView
     * and the descendant views of the AdapterView that obeys the following conditions:
     *
     * <ul>
     *   <li>For each descendant view there exists either 0 or 1 data objects it is rendering.
     *   <li>For each data object the AdapterView there exists either 0 or 1 descendant views which
     *       claim to be rendering it.
     * </ul>
     *
     * <p>For example - if a PersonObject is rendered into: <code>
     * LinearLayout
     *   ImageView picture
     *   TextView firstName
     *   TextView lastName
     * </code>
     *
     * <p>It would be expected that getDataRenderedByView(adapter, LinearLayout) would return the
     * PersonObject. If it were called instead with the TextView or ImageView it would return
     * Object.absent().
     *
     * @param adapterView the adapterview hosting the data.
     * @param descendantView a view which is a child, grand-child, or deeper descendant of adapterView
     * @return an optional data object the descendant view is rendering.
     * @throws IllegalArgumentException if this protocol cannot interrogate this class of adapterView
     */
    @Nullable
    default AdaptedData getDataRenderedByView2(
        AdapterView<? extends Adapter> adapterView, View descendantView) {
      return getDataRenderedByView(adapterView, descendantView).get();
    }
  
    /**
     * Requests that a particular piece of data held in this AdapterView is actually rendered by it.
     *
     * <p>After calling this method it expected that there will exist some descendant view of
     * adapterView for which calling getDataRenderedByView(adapterView, descView).get() == data.data
     * is true.
     *
     * <p>Note: this need not happen immediately. EG: an implementor handling ListView may call
     * listView.smoothScrollToPosition(data.opaqueToken) - which kicks off an animated scroll over the
     * list to the given position. The animation may be in progress after this call returns. The only
     * guarantee is that eventually - with no further interaction necessary - this data item will be
     * rendered as a child or deeper descendant of this AdapterView.
     *
     * @param adapterView the adapterView hosting the data.
     * @param data an AdaptedData instance retrieved by a prior call to getDataInAdapterView
     * @throws IllegalArgumentException if this protocol cannot manipulate adapterView or if data is
     *     not owned by this AdapterViewProtocol.
     */
    void makeDataRenderedWithinAdapterView(
        AdapterView<? extends Adapter> adapterView, AdaptedData data);
  
    /**
     * Indicates whether or not there now exists a descendant view within adapterView that is
     * rendering this data.
     *
     * @param adapterView the AdapterView hosting this data.
     * @param adaptedData the data we are checking the display state for.
     * @return true if the data is rendered by a view in the adapterView, false otherwise.
     */
    boolean isDataRenderedWithinAdapterView(
        AdapterView<? extends Adapter> adapterView, AdaptedData adaptedData);
  
    /**
     * A custom function that is applied when {@link AdaptedData#getData()} is executed.
     *
     * @see AdapterViewProtocol.AdaptedData.Builder#withDataFunction(DataFunction)
     */
    public interface DataFunction {
      public Object getData();
    }
  
    /**
     * A holder that associates a data object from an AdapterView with a token the AdapterViewProtocol
     * can use to force that data object to be rendered as a child or deeper descendant of the adapter
     * view.
     */
    public static class AdaptedData {
  
      /**
       * One of the objects the AdapterView is exposing to the user.
       *
       * @deprecated use {@link AdaptedData#getData()} instead.
       */
      @Nullable @Deprecated public final Object data;
  
      /**
       * A token the implementor of AdapterViewProtocol can use to force the adapterView to display
       * this data object as a child or deeper descendant in it. Equal opaqueToken point to the same
       * data object on the AdapterView.
       */
      public final Object opaqueToken;
  
      private final DataFunction dataFunction;
  
      public Object getData() {
        return dataFunction.getData();
      }
  
      @Override
      public String toString() {
        Object myData = getData();
        String itsClass = null == myData ? "null" : myData.getClass().getName();
        if (myData instanceof Cursor) {
          myData = HumanReadables.describe((Cursor) myData);
        }
        return String.format(
            Locale.ROOT, "Data: %s (class: %s) token: %s", myData, itsClass, opaqueToken);
      }
  
      private AdaptedData(Object data, Object opaqueToken, DataFunction dataFunction) {
        this.data = data;
        this.opaqueToken = checkNotNull(opaqueToken);
        this.dataFunction = checkNotNull(dataFunction);
      }
  
      public static class Builder {
        private Object data;
        private Object opaqueToken;
        private DataFunction dataFunction;
  
        public Builder withDataFunction(@Nullable DataFunction dataFunction) {
          this.dataFunction = dataFunction;
          return this;
        }
  
        public Builder withData(@Nullable Object data) {
          this.data = data;
          return this;
        }
  
        public Builder withOpaqueToken(@Nullable Object opaqueToken) {
          this.opaqueToken = opaqueToken;
          return this;
        }
  
        public AdaptedData build() {
          if (null != dataFunction) {
            data = dataFunction.getData();
          } else {
            dataFunction =
                new DataFunction() {
                  @Override
                  public Object getData() {
                    return data;
                  }
                };
          }
  
          return new AdaptedData(data, opaqueToken, dataFunction);
        }
      }
    }
  }
  