1/*
2 * Copyright (C) 2015 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 */
16package androidx.leanback.app;
17
18import static org.junit.Assert.assertEquals;
19import static org.junit.Assert.assertNotNull;
20import static org.junit.Assert.assertNull;
21import static org.junit.Assert.assertTrue;
22import static org.mockito.Matchers.any;
23import static org.mockito.Mockito.timeout;
24import static org.mockito.Mockito.verify;
25
26import android.app.Instrumentation;
27import android.content.Intent;
28import android.os.Build;
29import android.os.Bundle;
30import android.os.SystemClock;
31import android.support.test.InstrumentationRegistry;
32import android.support.test.filters.LargeTest;
33import android.support.test.rule.ActivityTestRule;
34import android.support.test.runner.AndroidJUnit4;
35import android.view.KeyEvent;
36import android.view.LayoutInflater;
37import android.view.MotionEvent;
38import android.view.View;
39import android.view.ViewGroup;
40import android.widget.FrameLayout;
41
42import androidx.annotation.NonNull;
43import androidx.annotation.Nullable;
44import androidx.fragment.app.Fragment;
45import androidx.leanback.testutils.PollingCheck;
46import androidx.leanback.widget.ArrayObjectAdapter;
47import androidx.leanback.widget.ItemBridgeAdapter;
48import androidx.leanback.widget.ListRowPresenter;
49import androidx.leanback.widget.Presenter;
50import androidx.leanback.widget.Row;
51import androidx.leanback.widget.RowPresenter;
52import androidx.recyclerview.widget.RecyclerView;
53
54import org.junit.After;
55import org.junit.Rule;
56import org.junit.Test;
57import org.junit.runner.RunWith;
58import org.mockito.Mockito;
59
60@LargeTest
61@RunWith(AndroidJUnit4.class)
62public class BrowseSupportFragmentTest {
63
64    static final String TAG = "BrowseSupportFragmentTest";
65    static final long WAIT_TRANSIITON_TIMEOUT = 10000;
66
67    @Rule
68    public ActivityTestRule<BrowseSupportFragmentTestActivity> activityTestRule =
69            new ActivityTestRule<>(BrowseSupportFragmentTestActivity.class, false, false);
70    private BrowseSupportFragmentTestActivity mActivity;
71
72    @After
73    public void afterTest() throws Throwable {
74        activityTestRule.runOnUiThread(new Runnable() {
75            @Override
76            public void run() {
77                if (mActivity != null) {
78                    mActivity.finish();
79                    mActivity = null;
80                }
81            }
82        });
83    }
84
85    void waitForEntranceTransitionFinished() {
86        PollingCheck.waitFor(WAIT_TRANSIITON_TIMEOUT, new PollingCheck.PollingCheckCondition() {
87            @Override
88            public boolean canProceed() {
89                if (Build.VERSION.SDK_INT >= 21) {
90                    return mActivity.getBrowseTestSupportFragment() != null
91                            && mActivity.getBrowseTestSupportFragment().mEntranceTransitionEnded;
92                } else {
93                    // when entrance transition not supported, wait main fragment loaded.
94                    return mActivity.getBrowseTestSupportFragment() != null
95                            && mActivity.getBrowseTestSupportFragment().getMainFragment() != null;
96                }
97            }
98        });
99    }
100
101    void waitForHeaderTransitionFinished() {
102        View row = mActivity.getBrowseTestSupportFragment().getRowsSupportFragment().getRowViewHolder(
103                mActivity.getBrowseTestSupportFragment().getSelectedPosition()).view;
104        PollingCheck.waitFor(WAIT_TRANSIITON_TIMEOUT, new PollingCheck.ViewStableOnScreen(row));
105        PollingCheck.waitFor(WAIT_TRANSIITON_TIMEOUT, new PollingCheck.PollingCheckCondition() {
106            public boolean canProceed() {
107                return !mActivity.getBrowseTestSupportFragment().isInHeadersTransition();
108            }
109        });
110    }
111
112    @Test
113    public void testTouchMode() throws Throwable {
114        Intent intent = new Intent();
115        intent.putExtra(BrowseFragmentTestActivity.EXTRA_ADD_TO_BACKSTACK , true);
116        intent.putExtra(BrowseFragmentTestActivity.EXTRA_LOAD_DATA_DELAY , 0L);
117        mActivity = activityTestRule.launchActivity(intent);
118
119        waitForEntranceTransitionFinished();
120
121        ListRowPresenter.ViewHolder rowVh = (ListRowPresenter.ViewHolder) mActivity
122                .getBrowseTestSupportFragment().getRowsSupportFragment().getRowViewHolder(0);
123        View card = rowVh.getGridView().getChildAt(0);
124        tapView(card);
125        waitForHeaderTransitionFinished();
126        assertTrue(card.hasFocus());
127        assertTrue(card.isInTouchMode());
128        sendKeys(KeyEvent.KEYCODE_BACK);
129        waitForHeaderTransitionFinished();
130        assertTrue((mActivity.getBrowseTestSupportFragment().getHeadersSupportFragment()
131                .getVerticalGridView().getChildAt(0)).hasFocus());
132    }
133
134    @Test
135    public void testTwoBackKeysWithBackStack() throws Throwable {
136        final long dataLoadingDelay = 0L;
137        Intent intent = new Intent();
138        intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, dataLoadingDelay);
139        intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_ADD_TO_BACKSTACK , true);
140        mActivity = activityTestRule.launchActivity(intent);
141
142        waitForEntranceTransitionFinished();
143
144        assertNotNull(mActivity.getBrowseTestSupportFragment().getMainFragment());
145        sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
146        waitForHeaderTransitionFinished();
147        sendKeys(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_BACK);
148    }
149
150    @Test
151    public void testTwoBackKeysWithoutBackStack() throws Throwable {
152        final long dataLoadingDelay = 0L;
153        Intent intent = new Intent();
154        intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, dataLoadingDelay);
155        intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_ADD_TO_BACKSTACK , false);
156        mActivity = activityTestRule.launchActivity(intent);
157
158        waitForEntranceTransitionFinished();
159
160        assertNotNull(mActivity.getBrowseTestSupportFragment().getMainFragment());
161        sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
162        waitForHeaderTransitionFinished();
163        sendKeys(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_BACK);
164    }
165
166    @Test
167    public void testPressRightBeforeMainFragmentCreated() throws Throwable {
168        final long dataLoadingDelay = 1000;
169        Intent intent = new Intent();
170        intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, dataLoadingDelay);
171        intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_ADD_TO_BACKSTACK , false);
172        mActivity = activityTestRule.launchActivity(intent);
173
174        assertNull(mActivity.getBrowseTestSupportFragment().getMainFragment());
175        sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
176    }
177
178    public static class MyRow extends Row {
179    }
180
181    public static class MyFragment extends Fragment implements
182            BrowseSupportFragment.MainFragmentAdapterProvider {
183        BrowseSupportFragment.MainFragmentAdapter mMainFragmentAdapter = new BrowseSupportFragment
184                .MainFragmentAdapter(this);
185
186        @Nullable
187        @Override
188        public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
189                @Nullable Bundle savedInstanceState) {
190            return new FrameLayout(container.getContext());
191        }
192
193        @Override
194        public BrowseSupportFragment.MainFragmentAdapter getMainFragmentAdapter() {
195            return mMainFragmentAdapter;
196        }
197    }
198
199    public static class MyFragmentFactory extends
200            BrowseSupportFragment.FragmentFactory<MyFragment> {
201        public MyFragment createFragment(Object row) {
202            return new MyFragment();
203        }
204    }
205
206    @Test
207    public void testPressCenterBeforeMainFragmentCreated() throws Throwable {
208        Intent intent = new Intent();
209        intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, 0L);
210        mActivity = activityTestRule.launchActivity(intent);
211
212        final BrowseSupportFragment fragment = mActivity.getBrowseTestSupportFragment();
213        fragment.getMainFragmentRegistry().registerFragment(MyRow.class, new MyFragmentFactory());
214
215        final ArrayObjectAdapter adapter = new ArrayObjectAdapter(new RowPresenter() {
216            protected ViewHolder createRowViewHolder(ViewGroup parent) {
217                View view = new FrameLayout(parent.getContext());
218                return new RowPresenter.ViewHolder(view);
219            }
220        });
221        adapter.add(new MyRow());
222        activityTestRule.runOnUiThread(new Runnable() {
223            @Override
224            public void run() {
225                fragment.setAdapter(adapter);
226            }
227        });
228        activityTestRule.runOnUiThread(new Runnable() {
229            @Override
230            public void run() {
231                KeyEvent kv;
232                kv = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_CENTER);
233                fragment.getView().dispatchKeyEvent(kv);
234                kv = new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DPAD_CENTER);
235                fragment.getView().dispatchKeyEvent(kv);
236            }
237        });
238    }
239
240    @Test
241    public void testSelectCardOnARow() throws Throwable {
242        final int selectRow = 10;
243        final int selectItem = 20;
244        Intent intent = new Intent();
245        final long dataLoadingDelay = 0L;
246        intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, dataLoadingDelay);
247        intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_ADD_TO_BACKSTACK , true);
248        mActivity = activityTestRule.launchActivity(intent);
249
250        waitForEntranceTransitionFinished();
251
252        Presenter.ViewHolderTask itemTask = Mockito.spy(
253                new ItemSelectionTask(mActivity, selectRow));
254
255        final ListRowPresenter.SelectItemViewHolderTask task =
256                new ListRowPresenter.SelectItemViewHolderTask(selectItem);
257        task.setItemTask(itemTask);
258
259        mActivity.runOnUiThread(new Runnable() {
260            @Override
261            public void run() {
262                mActivity.getBrowseTestSupportFragment().setSelectedPosition(selectRow, true, task);
263            }
264        });
265
266        verify(itemTask, timeout(5000).times(1)).run(any(Presenter.ViewHolder.class));
267
268        activityTestRule.runOnUiThread(new Runnable() {
269            @Override
270            public void run() {
271                ListRowPresenter.ViewHolder row = (ListRowPresenter.ViewHolder) mActivity
272                        .getBrowseTestSupportFragment().getRowsSupportFragment().getRowViewHolder(selectRow);
273                assertNotNull(dumpRecyclerView(mActivity.getBrowseTestSupportFragment().getGridView()), row);
274                assertNotNull(row.getGridView());
275                assertEquals(selectItem, row.getGridView().getSelectedPosition());
276            }
277        });
278    }
279
280    @Test
281    public void activityRecreate_notCrash() throws Throwable {
282        final long dataLoadingDelay = 0L;
283        Intent intent = new Intent();
284        intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, dataLoadingDelay);
285        intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_ADD_TO_BACKSTACK , false);
286        intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_SET_ADAPTER_AFTER_DATA_LOAD, true);
287        mActivity = activityTestRule.launchActivity(intent);
288
289        waitForEntranceTransitionFinished();
290
291        InstrumentationRegistry.getInstrumentation().callActivityOnRestart(mActivity);
292        activityTestRule.runOnUiThread(new Runnable() {
293            @Override
294            public void run() {
295                mActivity.recreate();
296            }
297        });
298    }
299
300
301    @Test
302    public void lateLoadingHeaderDisabled() throws Throwable {
303        final long dataLoadingDelay = 0L;
304        Intent intent = new Intent();
305        intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, dataLoadingDelay);
306        intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_HEADERS_STATE,
307                BrowseSupportFragment.HEADERS_DISABLED);
308        mActivity = activityTestRule.launchActivity(intent);
309        waitForEntranceTransitionFinished();
310        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
311            @Override
312            public boolean canProceed() {
313                return mActivity.getBrowseTestSupportFragment().getGridView() != null
314                        && mActivity.getBrowseTestSupportFragment().getGridView().getChildCount() > 0;
315            }
316        });
317    }
318
319    static void tapView(View v) {
320        Instrumentation inst = InstrumentationRegistry.getInstrumentation();
321        int[] xy = new int[2];
322        v.getLocationOnScreen(xy);
323
324        final int viewWidth = v.getWidth();
325        final int viewHeight = v.getHeight();
326
327        final float x = xy[0] + (viewWidth / 2.0f);
328        float y = xy[1] + (viewHeight / 2.0f);
329
330        long downTime = SystemClock.uptimeMillis();
331        long eventTime = SystemClock.uptimeMillis();
332
333        MotionEvent event = MotionEvent.obtain(downTime, eventTime,
334                MotionEvent.ACTION_DOWN, x, y, 0);
335        inst.sendPointerSync(event);
336        inst.waitForIdleSync();
337
338        eventTime = SystemClock.uptimeMillis();
339        event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_UP, x, y, 0);
340        inst.sendPointerSync(event);
341        inst.waitForIdleSync();
342    }
343
344    private void sendKeys(int ...keys) {
345        for (int i = 0; i < keys.length; i++) {
346            InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keys[i]);
347        }
348    }
349
350    public static class ItemSelectionTask extends Presenter.ViewHolderTask {
351
352        private final BrowseSupportFragmentTestActivity activity;
353        private final int expectedRow;
354
355        public ItemSelectionTask(BrowseSupportFragmentTestActivity activity, int expectedRow) {
356            this.activity = activity;
357            this.expectedRow = expectedRow;
358        }
359
360        @Override
361        public void run(Presenter.ViewHolder holder) {
362            android.util.Log.d(TAG, dumpRecyclerView(activity.getBrowseTestSupportFragment()
363                    .getGridView()));
364            android.util.Log.d(TAG, "Row " + expectedRow + " " + activity.getBrowseTestSupportFragment()
365                    .getRowsSupportFragment().getRowViewHolder(expectedRow), new Exception());
366        }
367    }
368
369    static String dumpRecyclerView(RecyclerView recyclerView) {
370        StringBuffer b = new StringBuffer();
371        for (int i = 0; i < recyclerView.getChildCount(); i++) {
372            View child = recyclerView.getChildAt(i);
373            ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder)
374                    recyclerView.getChildViewHolder(child);
375            b.append("child").append(i).append(":").append(vh);
376            if (vh != null) {
377                b.append(",").append(vh.getViewHolder());
378            }
379            b.append(";");
380        }
381        return b.toString();
382    }
383}
384