| /* |
| * 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; |
| } |
| } |
| } |