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.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.view.View;
  import androidx.core.view.GravityCompat;
  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}.
   *
   * <p>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
        public void performAction(UiController uiController, DrawerLayout drawer) {
          IdlingDrawerClosedListener closeListener = new IdlingDrawerClosedListener();
          drawer.addDrawerListener(closeListener);
          drawer.closeDrawer(gravity);
          uiController.loopMainThreadUntilIdle();
          // Need to wait extra time after it closes to reduce flakiness.
          uiController.loopMainThreadForAtLeast(300);
          drawer.removeDrawerListener(closeListener);
        }
      };
    }
  
    /**
     * Creates an action that will wait for the drawer to close. Use this in cases where the closing
     * of the drawer is implicit, such as selecting an item in the drawer. No operation if the drawer
     * is closed. This uses an idling resource to wait for close, so it will fail if it does not close
     * within the idling resource timeout.
     */
    public static ViewAction waitForClose() {
      return new DrawerAction() {
        @Override
        public String getDescription() {
          return "waiting for drawer to close";
        }
  
        @Override
        protected Matcher<View> checkAction() {
          return isOpen();
        }
  
        @Override
        public void performAction(UiController uiController, DrawerLayout drawer) {
          // Add a listener that waits for the drawer to be closed, wait for it to idle,
          // and then remove the listener immediately.
          IdlingDrawerClosedListener closeListener = new IdlingDrawerClosedListener();
          drawer.addDrawerListener(closeListener);
          uiController.loopMainThreadUntilIdle();
          drawer.removeDrawerListener(closeListener);
        }
      };
    }
  
    /** Drawer listener that functions as an {@link IdlingResource} for Espresso. */
    private static final class IdlingDrawerClosedListener extends SimpleDrawerListener
        implements IdlingResource {
  
      private final int id = nextId.getAndIncrement();
  
      private ResourceCallback callback;
      private boolean isClosed = false;
  
      @Override
      public void onDrawerClosed(View view) {
        isClosed = true;
        if (callback != null) {
          callback.onTransitionToIdle();
        }
      }
  
      @Override
      public String getName() {
        return "IdlingDrawerListener::" + id;
      }
  
      @Override
      public boolean isIdleNow() {
        return isClosed;
      }
  
      @Override
      public void registerIdleTransitionCallback(ResourceCallback callback) {
        this.callback = callback;
      }
    }
  
    /** 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;
      }
    }
  }
  