blob: 2f9b51df69e2fc5e7337ee529a3153635412fe2f [file] [log] [blame]
/*
* 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 android.support.test.espresso.contrib;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.contrib.DrawerMatchers.isClosed;
import static android.support.test.espresso.contrib.DrawerMatchers.isOpen;
import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import android.support.test.espresso.Espresso;
import android.support.test.espresso.IdlingResource;
import android.support.test.espresso.PerformException;
import android.support.test.espresso.UiController;
import android.support.test.espresso.ViewAction;
import android.support.annotation.Nullable;
import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout;
import android.support.v4.widget.DrawerLayout.DrawerListener;
import android.view.View;
import org.hamcrest.Matcher;
import java.lang.reflect.Field;
/**
* 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 DrawerActions() {
// forbid instantiation
}
private static Field listenerField;
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;
}
DrawerListener listener = getDrawerListener(drawer);
IdlingDrawerListener idlingListener;
if (listener instanceof IdlingDrawerListener) {
idlingListener = (IdlingDrawerListener) listener;
} else {
idlingListener = IdlingDrawerListener.getInstance(listener);
drawer.setDrawerListener(idlingListener);
Espresso.registerIdlingResources(idlingListener);
}
performAction(drawer);
uiController.loopMainThreadUntilIdle();
Espresso.unregisterIdlingResources(idlingListener);
drawer.setDrawerListener(idlingListener.parentListener);
idlingListener.parentListener = null;
}
protected abstract Matcher<View> checkAction();
protected abstract void performAction(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(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(DrawerLayout view) {
view.closeDrawer(gravity);
}
};
}
/**
* Pries the current {@link DrawerListener} loose from the cold dead hands of the given
* {@link DrawerLayout}. Uses reflection.
*/
@Nullable
private static DrawerListener getDrawerListener(DrawerLayout drawer) {
try {
if (listenerField == null) {
// lazy initialization of reflected field.
listenerField = DrawerLayout.class.getDeclaredField("mListener");
listenerField.setAccessible(true);
}
return (DrawerListener) listenerField.get(drawer);
} catch (IllegalArgumentException ex) {
// Pity we can't use Java 7 multi-catch for all of these.
throw new PerformException.Builder().withCause(ex).build();
} catch (IllegalAccessException ex) {
throw new PerformException.Builder().withCause(ex).build();
} catch (NoSuchFieldException ex) {
throw new PerformException.Builder().withCause(ex).build();
} catch (SecurityException ex) {
throw new PerformException.Builder().withCause(ex).build();
}
}
/**
* Drawer listener that wraps an existing {@link DrawerListener}, and functions as an
* {@link IdlingResource} for Espresso.
*/
private static class IdlingDrawerListener implements DrawerListener, IdlingResource {
private static IdlingDrawerListener instance;
private static IdlingDrawerListener getInstance(DrawerListener parentListener) {
if (instance == null) {
instance = new IdlingDrawerListener();
}
instance.setParentListener(parentListener);
return instance;
}
@Nullable private DrawerListener parentListener;
private ResourceCallback callback;
// Idle state is only accessible from main thread.
private boolean idle = true;
public void setParentListener(@Nullable DrawerListener parentListener) {
this.parentListener = parentListener;
}
@Override
public void onDrawerClosed(View drawer) {
if (parentListener != null) {
parentListener.onDrawerClosed(drawer);
}
}
@Override
public void onDrawerOpened(View drawer) {
if (parentListener != null) {
parentListener.onDrawerOpened(drawer);
}
}
@Override
public void onDrawerSlide(View drawer, float slideOffset) {
if (parentListener != null) {
parentListener.onDrawerSlide(drawer, slideOffset);
}
}
@Override
public void onDrawerStateChanged(int newState) {
if (newState == DrawerLayout.STATE_IDLE) {
idle = true;
if (callback != null) {
callback.onTransitionToIdle();
}
} else {
idle = false;
}
if (parentListener != null) {
parentListener.onDrawerStateChanged(newState);
}
}
@Override
public String getName() {
return "IdlingDrawerListener";
}
@Override
public boolean isIdleNow() {
return idle;
}
@Override
public void registerIdleTransitionCallback(ResourceCallback callback) {
this.callback = callback;
}
}
}