blob: 733c94f81a1d14ae63586ea29eae87d3c498034c [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 com.google.android.apps.common.testing.ui.espresso.contrib;
import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
import static com.google.android.apps.common.testing.ui.espresso.contrib.DrawerMatchers.isClosed;
import static com.google.android.apps.common.testing.ui.espresso.contrib.DrawerMatchers.isOpen;
import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isAssignableFrom;
import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
import com.google.android.apps.common.testing.ui.espresso.Espresso;
import com.google.android.apps.common.testing.ui.espresso.IdlingResource;
import com.google.android.apps.common.testing.ui.espresso.PerformException;
import com.google.android.apps.common.testing.ui.espresso.UiController;
import com.google.android.apps.common.testing.ui.espresso.ViewAction;
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;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Nullable;
/**
* 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;
/**
* Opens the {@link DrawerLayout} with the given id. This method blocks until the drawer is fully
* open. No operation if the drawer is already open.
*/
public static void openDrawer(int drawerLayoutId) {
//if the drawer is already open, return.
if (checkDrawer(drawerLayoutId, isOpen())) {
return;
}
onView(withId(drawerLayoutId)).perform(registerListener());
onView(withId(drawerLayoutId)).perform(actionOpenDrawer());
}
/**
* Closes the {@link DrawerLayout} with the given id. This method blocks until the drawer is fully
* closed. No operation if the drawer is already closed.
*/
public static void closeDrawer(int drawerLayoutId) {
//if the drawer is already closed, return.
if (checkDrawer(drawerLayoutId, isClosed())) {
return;
}
onView(withId(drawerLayoutId)).perform(registerListener());
onView(withId(drawerLayoutId)).perform(actionCloseDrawer());
}
/**
* Returns true if the given matcher matches the drawer.
*/
private static boolean checkDrawer(int drawerLayoutId, final Matcher<View> matcher) {
final AtomicBoolean matches = new AtomicBoolean(false);
onView(withId(drawerLayoutId)).perform(new ViewAction() {
@Override
public Matcher<View> getConstraints() {
return isAssignableFrom(DrawerLayout.class);
}
@Override
public String getDescription() {
return "check drawer";
}
@Override
public void perform(UiController uiController, View view) {
matches.set(matcher.matches(view));
}
});
return matches.get();
}
private static ViewAction actionOpenDrawer() {
return new ViewAction() {
@Override
public Matcher<View> getConstraints() {
return isAssignableFrom(DrawerLayout.class);
}
@Override
public String getDescription() {
return "open drawer";
}
@Override
public void perform(UiController uiController, View view) {
((DrawerLayout) view).openDrawer(GravityCompat.START);
}
};
}
private static ViewAction actionCloseDrawer() {
return new ViewAction() {
@Override
public Matcher<View> getConstraints() {
return isAssignableFrom(DrawerLayout.class);
}
@Override
public String getDescription() {
return "close drawer";
}
@Override
public void perform(UiController uiController, View view) {
((DrawerLayout) view).closeDrawer(GravityCompat.START);
}
};
}
/**
* Returns a {@link ViewAction} that adds an {@link IdlingDrawerListener} as a drawer listener to
* the {@link DrawerLayout}. The idling drawer listener wraps any listener that already exists.
*/
private static ViewAction registerListener() {
return new ViewAction() {
@Override
public Matcher<View> getConstraints() {
return isAssignableFrom(DrawerLayout.class);
}
@Override
public String getDescription() {
return "register idling drawer listener";
}
@Override
public void perform(UiController uiController, View view) {
DrawerLayout drawer = (DrawerLayout) view;
DrawerListener existingListener = getDrawerListener(drawer);
if (existingListener instanceof IdlingDrawerListener) {
// listener is already registered. No need to assign.
return;
}
drawer.setDrawerListener(IdlingDrawerListener.getInstance(existingListener));
}
};
}
/**
* 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();
Espresso.registerIdlingResources(instance);
}
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;
}
}
}