1/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.support.wear.widget;
18
19import static android.support.test.espresso.Espresso.onView;
20import static android.support.test.espresso.action.ViewActions.swipeRight;
21import static android.support.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;
22import static android.support.test.espresso.matcher.ViewMatchers.withId;
23import static android.support.wear.widget.util.AsyncViewActions.waitForMatchingView;
24import static android.support.wear.widget.util.MoreViewAssertions.withPositiveVerticalScrollOffset;
25
26import static org.hamcrest.Matchers.allOf;
27import static org.junit.Assert.assertFalse;
28import static org.junit.Assert.assertTrue;
29
30import android.app.Activity;
31import android.content.Intent;
32import android.graphics.RectF;
33import android.support.annotation.IdRes;
34import android.support.test.InstrumentationRegistry;
35import android.support.test.espresso.ViewAction;
36import android.support.test.espresso.action.GeneralLocation;
37import android.support.test.espresso.action.GeneralSwipeAction;
38import android.support.test.espresso.action.Press;
39import android.support.test.espresso.action.Swipe;
40import android.support.test.espresso.matcher.ViewMatchers;
41import android.support.test.filters.SmallTest;
42import android.support.test.rule.ActivityTestRule;
43import android.support.test.runner.AndroidJUnit4;
44import android.support.v7.widget.RecyclerView;
45import android.support.wear.test.R;
46import android.support.wear.widget.util.ArcSwipe;
47import android.support.wear.widget.util.WakeLockRule;
48import android.view.View;
49
50import org.junit.Rule;
51import org.junit.Test;
52import org.junit.runner.RunWith;
53
54@RunWith(AndroidJUnit4.class)
55public class SwipeDismissFrameLayoutTest {
56
57    private static final long MAX_WAIT_TIME = 4000; //ms
58    private final SwipeDismissFrameLayout.Callback mDismissCallback = new DismissCallback();
59
60    @Rule
61    public final WakeLockRule wakeLock = new WakeLockRule();
62
63    @Rule
64    public final ActivityTestRule<SwipeDismissFrameLayoutTestActivity> activityRule =
65            new ActivityTestRule<>(
66                    SwipeDismissFrameLayoutTestActivity.class,
67                    true, /** initial touch mode */
68                    false /** launchActivity */
69            );
70
71    private int mLayoutWidth;
72    private int mLayoutHeight;
73
74    @Test
75    @SmallTest
76    public void testCanScrollHorizontally() {
77        // GIVEN a freshly setup SwipeDismissFrameLayout
78        setUpSimpleLayout();
79        Activity activity = activityRule.getActivity();
80        SwipeDismissFrameLayout testLayout =
81                (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
82        // WHEN we check that the layout is horizontally scrollable from left to right.
83        // THEN the layout is found to be horizontally swipeable from left to right.
84        assertTrue(testLayout.canScrollHorizontally(-20));
85        // AND the layout is found to NOT be horizontally swipeable from right to left.
86        assertFalse(testLayout.canScrollHorizontally(20));
87
88        // WHEN we switch off the swipe-to-dismiss functionality for the layout
89        testLayout.setSwipeable(false);
90        // THEN the layout is found NOT to be horizontally swipeable from left to right.
91        assertFalse(testLayout.canScrollHorizontally(-20));
92        // AND the layout is found to NOT be horizontally swipeable from right to left.
93        assertFalse(testLayout.canScrollHorizontally(20));
94    }
95
96    @Test
97    @SmallTest
98    public void canScrollHorizontallyShouldBeFalseWhenInvisible() {
99        // GIVEN a freshly setup SwipeDismissFrameLayout
100        setUpSimpleLayout();
101        Activity activity = activityRule.getActivity();
102        final SwipeDismissFrameLayout testLayout = activity.findViewById(R.id.swipe_dismiss_root);
103        // GIVEN the layout is invisible
104        // Note: We have to run this on the main thread, because of thread checks in View.java.
105        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
106            @Override
107            public void run() {
108                testLayout.setVisibility(View.INVISIBLE);
109            }
110        });
111        // WHEN we check that the layout is horizontally scrollable
112        // THEN the layout is found to be NOT horizontally swipeable from left to right.
113        assertFalse(testLayout.canScrollHorizontally(-20));
114        // AND the layout is found to NOT be horizontally swipeable from right to left.
115        assertFalse(testLayout.canScrollHorizontally(20));
116    }
117
118    @Test
119    @SmallTest
120    public void canScrollHorizontallyShouldBeFalseWhenGone() {
121        // GIVEN a freshly setup SwipeDismissFrameLayout
122        setUpSimpleLayout();
123        Activity activity = activityRule.getActivity();
124        final SwipeDismissFrameLayout testLayout =
125                (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
126        // GIVEN the layout is gone
127        // Note: We have to run this on the main thread, because of thread checks in View.java.
128        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
129            @Override
130            public void run() {
131                testLayout.setVisibility(View.GONE);
132            }
133        });
134        // WHEN we check that the layout is horizontally scrollable
135        // THEN the layout is found to be NOT horizontally swipeable from left to right.
136        assertFalse(testLayout.canScrollHorizontally(-20));
137        // AND the layout is found to NOT be horizontally swipeable from right to left.
138        assertFalse(testLayout.canScrollHorizontally(20));
139    }
140
141    @Test
142    @SmallTest
143    public void testSwipeDismissEnabledByDefault() {
144        // GIVEN a freshly setup SwipeDismissFrameLayout
145        setUpSimpleLayout();
146        Activity activity = activityRule.getActivity();
147        SwipeDismissFrameLayout testLayout =
148                (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
149        // WHEN we check that the layout is dismissible
150        // THEN the layout is find to be dismissible
151        assertTrue(testLayout.isSwipeable());
152    }
153
154    @Test
155    @SmallTest
156    public void testSwipeDismissesViewIfEnabled() {
157        // GIVEN a freshly setup SwipeDismissFrameLayout
158        setUpSimpleLayout();
159        // WHEN we perform a swipe to dismiss
160        onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight());
161        // AND hidden
162        assertHidden(R.id.swipe_dismiss_root);
163    }
164
165    @Test
166    @SmallTest
167    public void testSwipeDoesNotDismissViewIfDisabled() {
168        // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned off.
169        setUpSimpleLayout();
170        Activity activity = activityRule.getActivity();
171        SwipeDismissFrameLayout testLayout =
172                (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
173        testLayout.setSwipeable(false);
174        // WHEN we perform a swipe to dismiss
175        onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight());
176        // THEN the layout is not hidden
177        assertNotHidden(R.id.swipe_dismiss_root);
178    }
179
180    @Test
181    @SmallTest
182    public void testAddRemoveCallback() {
183        // GIVEN a freshly setup SwipeDismissFrameLayout
184        setUpSimpleLayout();
185        Activity activity = activityRule.getActivity();
186        SwipeDismissFrameLayout testLayout = activity.findViewById(R.id.swipe_dismiss_root);
187        // WHEN we remove the swipe callback
188        testLayout.removeCallback(mDismissCallback);
189        onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight());
190        // THEN the layout is not hidden
191        assertNotHidden(R.id.swipe_dismiss_root);
192    }
193
194    @Test
195    @SmallTest
196    public void testSwipeDoesNotDismissViewIfScrollable() throws Throwable {
197        // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned off.
198        setUpSwipeDismissWithHorizontalRecyclerView();
199        activityRule.runOnUiThread(new Runnable() {
200            @Override
201            public void run() {
202                Activity activity = activityRule.getActivity();
203                RecyclerView testLayout = activity.findViewById(R.id.recycler_container);
204                // Scroll to a position from which the child is scrollable.
205                testLayout.scrollToPosition(50);
206            }
207        });
208
209        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
210        // WHEN we perform a swipe to dismiss from the center of the screen.
211        onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromCenter());
212        // THEN the layout is not hidden
213        assertNotHidden(R.id.swipe_dismiss_root);
214    }
215
216
217    @Test
218    @SmallTest
219    public void testEdgeSwipeDoesDismissViewIfScrollable() {
220        // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned off.
221        setUpSwipeDismissWithHorizontalRecyclerView();
222        // WHEN we perform a swipe to dismiss from the left edge of the screen.
223        onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromLeftEdge());
224        // THEN the layout is hidden
225        assertHidden(R.id.swipe_dismiss_root);
226    }
227
228    @Test
229    @SmallTest
230    public void testSwipeDoesNotDismissViewIfStartsInWrongPosition() {
231        // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned on, but only for an
232        // inner circle.
233        setUpSwipeableRegion();
234        // WHEN we perform a swipe to dismiss from the left edge of the screen.
235        onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromLeftEdge());
236        // THEN the layout is not not hidden
237        assertNotHidden(R.id.swipe_dismiss_root);
238    }
239
240    @Test
241    @SmallTest
242    public void testSwipeDoesDismissViewIfStartsInRightPosition() {
243        // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned on, but only for an
244        // inner circle.
245        setUpSwipeableRegion();
246        // WHEN we perform a swipe to dismiss from the center of the screen.
247        onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromCenter());
248        // THEN the layout is hidden
249        assertHidden(R.id.swipe_dismiss_root);
250    }
251
252    /**
253     @Test public void testSwipeInPreferenceFragmentAndNavDrawer() {
254     // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned on, but only for an inner
255     // circle.
256     setUpPreferenceFragmentAndNavDrawer();
257     // WHEN we perform a swipe to dismiss from the center of the screen to the bottom.
258     onView(withId(R.id.drawer_layout)).perform(swipeBottomFromCenter());
259     // THEN the navigation drawer is shown.
260     assertPeeking(R.id.top_drawer);
261     }*/
262
263    @Test
264    @SmallTest
265    public void testArcSwipeDoesNotTriggerDismiss() throws Throwable {
266        // GIVEN a freshly setup SwipeDismissFrameLayout with vertically scrollable content
267        setUpSwipeDismissWithVerticalRecyclerView();
268        int center = mLayoutHeight / 2;
269        int halfBound = mLayoutWidth / 2;
270        RectF bounds = new RectF(0, center - halfBound, mLayoutWidth, center + halfBound);
271        // WHEN the view is scrolled on an arc from top to bottom.
272        onView(withId(R.id.swipe_dismiss_root)).perform(swipeTopFromBottomOnArc(bounds));
273        // THEN the layout is not dismissed and not hidden.
274        assertNotHidden(R.id.swipe_dismiss_root);
275        // AND the content view is scrolled.
276        assertScrolledY(R.id.recycler_container);
277    }
278
279    /**
280     * Set ups the simplest possible layout for test cases - a {@link SwipeDismissFrameLayout} with
281     * a single static child.
282     */
283    private void setUpSimpleLayout() {
284        activityRule.launchActivity(
285                new Intent()
286                        .putExtra(
287                                LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
288                                R.layout.swipe_dismiss_layout_testcase_1));
289        setDismissCallback();
290    }
291
292
293    /**
294     * Sets up a slightly more involved layout for testing swipe-to-dismiss with scrollable
295     * containers. This layout contains a {@link SwipeDismissFrameLayout} with a horizontal {@link
296     * android.support.v7.widget.RecyclerView} as a child, ready to accept an adapter.
297     */
298    private void setUpSwipeDismissWithHorizontalRecyclerView() {
299        Intent launchIntent = new Intent();
300        launchIntent.putExtra(LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
301                R.layout.swipe_dismiss_layout_testcase_2);
302        launchIntent.putExtra(SwipeDismissFrameLayoutTestActivity.EXTRA_LAYOUT_HORIZONTAL, true);
303        activityRule.launchActivity(launchIntent);
304        setDismissCallback();
305    }
306
307    /**
308     * Sets up a slightly more involved layout for testing swipe-to-dismiss with scrollable
309     * containers. This layout contains a {@link SwipeDismissFrameLayout} with a vertical {@link
310     * WearableRecyclerView} as a child, ready to accept an adapter.
311     */
312    private void setUpSwipeDismissWithVerticalRecyclerView() {
313        Intent launchIntent = new Intent();
314        launchIntent.putExtra(LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
315                R.layout.swipe_dismiss_layout_testcase_2);
316        launchIntent.putExtra(SwipeDismissFrameLayoutTestActivity.EXTRA_LAYOUT_HORIZONTAL, false);
317        activityRule.launchActivity(launchIntent);
318        setDismissCallback();
319    }
320
321    /**
322     * Sets up a {@link SwipeDismissFrameLayout} in which only a certain region is allowed to react
323     * to swipe-dismiss gestures.
324     */
325    private void setUpSwipeableRegion() {
326        activityRule.launchActivity(
327                new Intent()
328                        .putExtra(
329                                LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
330                                R.layout.swipe_dismiss_layout_testcase_1));
331        setCallback(
332                new DismissCallback() {
333                    @Override
334                    public boolean onPreSwipeStart(SwipeDismissFrameLayout layout, float x,
335                            float y) {
336                        float normalizedX = x - mLayoutWidth / 2;
337                        float normalizedY = y - mLayoutWidth / 2;
338                        float squareX = normalizedX * normalizedX;
339                        float squareY = normalizedY * normalizedY;
340                        // 30 is an arbitrary number limiting the circle.
341                        return Math.sqrt(squareX + squareY) < (mLayoutWidth / 2 - 30);
342                    }
343                });
344    }
345
346    /**
347     * Sets up a more involved test case where the layout consists of a
348     * {@code WearableNavigationDrawer} and a
349     * {@code android.support.wear.internal.view.SwipeDismissPreferenceFragment}
350     */
351  /*
352  private void setUpPreferenceFragmentAndNavDrawer() {
353    activityRule.launchActivity(
354      new Intent()
355          .putExtra(
356              LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
357              R.layout.swipe_dismiss_layout_testcase_3));
358    Activity activity = activityRule.getActivity();
359    InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
360      WearableNavigationDrawer wearableNavigationDrawer =
361              (WearableNavigationDrawer) activity.findViewById(R.id.top_drawer);
362      wearableNavigationDrawer.setAdapter(
363              new WearableNavigationDrawer.WearableNavigationDrawerAdapter() {
364                @Override
365                public String getItemText(int pos) {
366                  return "test";
367                }
368
369                @Override
370                public Drawable getItemDrawable(int pos) {
371                  return null;
372                }
373
374                @Override
375                public void onItemSelected(int pos) {
376                  return;
377                }
378
379                @Override
380                public int getCount() {
381                  return 3;
382                }
383              });
384    });
385  }*/
386    private void setDismissCallback() {
387        setCallback(mDismissCallback);
388    }
389
390    private void setCallback(SwipeDismissFrameLayout.Callback callback) {
391        Activity activity = activityRule.getActivity();
392        SwipeDismissFrameLayout testLayout = activity.findViewById(R.id.swipe_dismiss_root);
393        mLayoutWidth = testLayout.getWidth();
394        mLayoutHeight = testLayout.getHeight();
395        testLayout.addCallback(callback);
396    }
397
398    /**
399     * private static void assertPeeking(@IdRes int layoutId) {
400     * onView(withId(layoutId))
401     * .perform(
402     * waitForMatchingView(
403     * allOf(withId(layoutId), isOpened(true)), MAX_WAIT_TIME));
404     * }
405     */
406
407    private static void assertHidden(@IdRes int layoutId) {
408        onView(withId(layoutId))
409                .perform(
410                        waitForMatchingView(
411                                allOf(withId(layoutId),
412                                        withEffectiveVisibility(ViewMatchers.Visibility.GONE)),
413                                MAX_WAIT_TIME));
414    }
415
416    private static void assertNotHidden(@IdRes int layoutId) {
417        onView(withId(layoutId))
418                .perform(
419                        waitForMatchingView(
420                                allOf(withId(layoutId),
421                                        withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)),
422                                MAX_WAIT_TIME));
423    }
424
425    private static void assertScrolledY(@IdRes int layoutId) {
426        onView(withId(layoutId))
427                .perform(
428                        waitForMatchingView(
429                                allOf(withId(layoutId), withPositiveVerticalScrollOffset()),
430                                MAX_WAIT_TIME));
431    }
432
433    private static ViewAction swipeRightFromCenter() {
434        return new GeneralSwipeAction(
435                Swipe.SLOW, GeneralLocation.CENTER, GeneralLocation.CENTER_RIGHT, Press.FINGER);
436    }
437
438    private static ViewAction swipeRightFromLeftEdge() {
439        return new GeneralSwipeAction(
440                Swipe.SLOW, GeneralLocation.CENTER_LEFT, GeneralLocation.CENTER_RIGHT,
441                Press.FINGER);
442    }
443
444    private static ViewAction swipeTopFromBottomOnArc(RectF bounds) {
445        return new GeneralSwipeAction(
446                new ArcSwipe(ArcSwipe.Gesture.SLOW_ANTICLOCKWISE, bounds),
447                GeneralLocation.BOTTOM_CENTER,
448                GeneralLocation.TOP_CENTER,
449                Press.FINGER);
450    }
451
452    /** Helper class hiding the view after a successful swipe-to-dismiss. */
453    private static class DismissCallback extends SwipeDismissFrameLayout.Callback {
454
455        @Override
456        public void onDismissed(SwipeDismissFrameLayout layout) {
457            layout.setVisibility(View.GONE);
458        }
459    }
460}
461