DrawerActions.java

/*
 * 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.contrib;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.contrib.DrawerMatchers.isClosed;
import static androidx.test.espresso.contrib.DrawerMatchers.isOpen;
import static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom;
import static androidx.test.espresso.matcher.ViewMatchers.withId;

import android.support.v4.view.GravityCompat;
import android.view.View;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.drawerlayout.widget.DrawerLayout.SimpleDrawerListener;
import androidx.test.espresso.IdlingRegistry;
import androidx.test.espresso.IdlingResource;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import java.util.concurrent.atomic.AtomicInteger;
import org.hamcrest.Matcher;

/**
 * Espresso actions for using a {@link DrawerLayout}.
 *
 * @see <a href="http://developer.android.com/design/patterns/navigation-drawer.html">Navigation
 *     drawer design guide</a>
 */
public final class DrawerActions {
  private static final AtomicInteger nextId = new AtomicInteger();

  private static final int TAG = getTag();

  private static int getTag() {
    try {
      return R.id.androidx_test_espresso_contrib_drawer_layout_tag;
    } catch (NoClassDefFoundError e) {
      // If the caller of this class is compiled as the src of an android_test rule, it may not
      // generate the R file, which leads to a NoClassDefFoundError at runtime. In this case, fall
      // back to a randomly chosen value that hopefully won't conflict with any other tags.
      return 0xFF3DE250;
    }
  }

  private DrawerActions() {
    // forbid instantiation
  }

  private abstract static class DrawerAction implements ViewAction {

    @Override
    public final Matcher<View> getConstraints() {
      return isAssignableFrom(DrawerLayout.class);
    }

    @Override
    public final void perform(UiController uiController, View view) {
      DrawerLayout drawer = (DrawerLayout) view;

      if (!checkAction().matches(drawer)) {
        return;
      }

      Object tag = drawer.getTag(TAG);
      IdlingDrawerListener idlingListener;
      if (tag instanceof IdlingDrawerListener) {
        idlingListener = (IdlingDrawerListener) tag;
      } else {
        idlingListener = new IdlingDrawerListener();
        drawer.setTag(TAG, idlingListener);
        drawer.addDrawerListener(idlingListener);
        IdlingRegistry.getInstance().register(idlingListener);
      }

      performAction(uiController, drawer);
      uiController.loopMainThreadUntilIdle();

      IdlingRegistry.getInstance().unregister(idlingListener);
      drawer.removeDrawerListener(idlingListener);
      drawer.setTag(TAG, null);
    }

    protected abstract Matcher<View> checkAction();

    protected abstract void performAction(UiController uiController, DrawerLayout view);
  }

  /**
   * @deprecated Use {@link #open()} with {@code perform} after matching a view. This method will be
   *     removed in the next release.
   */
  @Deprecated
  public static void openDrawer(int drawerLayoutId) {
    openDrawer(drawerLayoutId, GravityCompat.START);
  }

  /**
   * @deprecated Use {@link #open(int)} with {@code perform} after matching a view. This method will
   *     be removed in the next release.
   */
  @Deprecated
  public static void openDrawer(int drawerLayoutId, int gravity) {
    onView(withId(drawerLayoutId)).perform(open(gravity));
  }

  /**
   * Creates an action which opens the {@link DrawerLayout} drawer with gravity START. This method
   * blocks until the drawer is fully open. No operation if the drawer is already open.
   */
  // TODO alias to openDrawer before 3.0 and deprecate this method.
  public static ViewAction open() {
    return open(GravityCompat.START);
  }

  /**
   * Creates an action which opens the {@link DrawerLayout} drawer with the gravity. This method
   * blocks until the drawer is fully open. No operation if the drawer is already open.
   */
  // TODO alias to openDrawer before 3.0 and deprecate this method.
  public static ViewAction open(final int gravity) {
    return new DrawerAction() {
      @Override
      public String getDescription() {
        return "open drawer with gravity " + gravity;
      }

      @Override
      protected Matcher<View> checkAction() {
        return isClosed(gravity);
      }

      @Override
      protected void performAction(UiController uiController, DrawerLayout view) {
        view.openDrawer(gravity);
      }
    };
  }

  /**
   * @deprecated Use {@link #close()} with {@code perform} after matching a view. This method will
   *     be removed in the next release.
   */
  @Deprecated
  public static void closeDrawer(int drawerLayoutId) {
    closeDrawer(drawerLayoutId, GravityCompat.START);
  }

  /**
   * @deprecated Use {@link #open(int)} with {@code perform} after matching a view. This method will
   *     be removed in the next release.
   */
  @Deprecated
  public static void closeDrawer(int drawerLayoutId, int gravity) {
    onView(withId(drawerLayoutId)).perform(close(gravity));
  }

  /**
   * Creates an action which closes the {@link DrawerLayout} with gravity START. This method blocks
   * until the drawer is fully closed. No operation if the drawer is already closed.
   */
  // TODO alias to closeDrawer before 3.0 and deprecate this method.
  public static ViewAction close() {
    return close(GravityCompat.START);
  }

  /**
   * Creates an action which closes the {@link DrawerLayout} with the gravity. This method blocks
   * until the drawer is fully closed. No operation if the drawer is already closed.
   */
  // TODO alias to closeDrawer before 3.0 and deprecate this method.
  public static ViewAction close(final int gravity) {
    return new DrawerAction() {
      @Override
      public String getDescription() {
        return "close drawer with gravity " + gravity;
      }

      @Override
      protected Matcher<View> checkAction() {
        return isOpen(gravity);
      }

      @Override
      protected void performAction(UiController uiController, DrawerLayout view) {
        view.closeDrawer(gravity);
        uiController.loopMainThreadUntilIdle();
        // If still open wait some more...
        if (view.isDrawerVisible(gravity)) {
          uiController.loopMainThreadForAtLeast(300);
        }
      }
    };
  }

  /** Drawer listener that functions as an {@link IdlingResource} for Espresso. */
  private static final class IdlingDrawerListener extends SimpleDrawerListener
      implements IdlingResource {

    private final int id = nextId.getAndIncrement();

    private ResourceCallback callback;
    // Idle state is only accessible from main thread.
    private boolean idle = true;

    @Override
    public void onDrawerStateChanged(int newState) {
      if (newState == DrawerLayout.STATE_IDLE) {
        idle = true;
        if (callback != null) {
          callback.onTransitionToIdle();
        }
      } else {
        idle = false;
      }
    }

    @Override
    public String getName() {
      return "IdlingDrawerListener::" + id;
    }

    @Override
    public boolean isIdleNow() {
      return idle;
    }

    @Override
    public void registerIdleTransitionCallback(ResourceCallback callback) {
      this.callback = callback;
    }
  }
}