Java tutorial
/* * 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.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()); onView(withId(drawerLayoutId)).perform(unregisterListener()); } /** * 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()); onView(withId(drawerLayoutId)).perform(unregisterListener()); } /** * 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; } IdlingDrawerListener instance = IdlingDrawerListener.getInstance(existingListener); drawer.setDrawerListener(instance); Espresso.registerIdlingResources(instance); } }; } /** * Returns a {@link ViewAction} that removes the {@link IdlingDrawerListener} from the * {@link DrawerLayout}. All listeners that exists prior the registration of * {@link IdlingDrawerListener}, are retained. */ private static ViewAction unregisterListener() { return new ViewAction() { @Override public Matcher<View> getConstraints() { return isAssignableFrom(DrawerLayout.class); } @Override public String getDescription() { return "unregister idling drawer listener"; } @Override public void perform(UiController uiController, View view) { DrawerLayout drawer = (DrawerLayout) view; DrawerListener existingListener = getDrawerListener(drawer); if (existingListener instanceof IdlingDrawerListener) { Espresso.unregisterIdlingResources((IdlingResource) existingListener); drawer.setDrawerListener(((IdlingDrawerListener) existingListener).parentListener); } } }; } /** * 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; } } }