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 androidx.leanback.widget;
18
19import static org.junit.Assert.assertEquals;
20import static org.junit.Assert.assertNull;
21import static org.junit.Assert.assertSame;
22import static org.junit.Assert.assertTrue;
23import static org.mockito.Matchers.any;
24import static org.mockito.Matchers.anyInt;
25import static org.mockito.Matchers.eq;
26import static org.mockito.Mockito.times;
27import static org.mockito.Mockito.verify;
28import static org.mockito.Mockito.when;
29
30import android.content.Context;
31import android.graphics.Bitmap;
32import android.graphics.drawable.ColorDrawable;
33import android.support.test.InstrumentationRegistry;
34import android.support.test.filters.SmallTest;
35import android.view.ContextThemeWrapper;
36import android.view.KeyEvent;
37import android.view.View;
38import android.view.ViewParent;
39
40import androidx.leanback.media.PlaybackTransportControlGlue;
41import androidx.leanback.media.PlayerAdapter;
42import androidx.leanback.widget.PlaybackSeekDataProvider.ResultCallback;
43
44import org.junit.Before;
45import org.junit.Test;
46import org.mockito.Mockito;
47
48import java.util.Arrays;
49
50@SmallTest
51public class PlaybackTransportRowPresenterTest {
52
53    Context mContext;
54    PlaybackTransportControlGlue mGlue;
55    PlaybackGlueHostImplWithViewHolder mHost;
56    PlayerAdapter mImpl;
57    PlaybackTransportRowPresenter.ViewHolder mViewHolder;
58    AbstractDetailsDescriptionPresenter.ViewHolder mDescriptionViewHolder;
59    int mNumbThumbs;
60
61    @Before
62    public void setUp() {
63        mContext = new ContextThemeWrapper(
64                InstrumentationRegistry.getInstrumentation().getTargetContext(),
65                androidx.leanback.test.R.style.Theme_Leanback);
66        mHost = new PlaybackGlueHostImplWithViewHolder(mContext);
67        mImpl = Mockito.mock(PlayerAdapter.class);
68        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
69            @Override
70            public void run() {
71                mGlue = new PlaybackTransportControlGlue(mContext, mImpl) {
72                    @Override
73                    protected void onCreatePrimaryActions(ArrayObjectAdapter
74                            primaryActionsAdapter) {
75                        super.onCreatePrimaryActions(primaryActionsAdapter);
76                        primaryActionsAdapter.add(
77                                new PlaybackControlsRow.ClosedCaptioningAction(mContext));
78                    }
79
80                    @Override
81                    protected void onCreateSecondaryActions(ArrayObjectAdapter
82                            secondaryActionsAdapter) {
83                        secondaryActionsAdapter.add(
84                                new PlaybackControlsRow.HighQualityAction(mContext));
85                        secondaryActionsAdapter.add(
86                                new PlaybackControlsRow.PictureInPictureAction(mContext));
87                    }
88                };
89                mGlue.setHost(mHost);
90
91            }
92        });
93        mViewHolder = (PlaybackTransportRowPresenter.ViewHolder) mHost.mViewHolder;
94        mDescriptionViewHolder = (AbstractDetailsDescriptionPresenter.ViewHolder)
95                mViewHolder.mDescriptionViewHolder;
96        mNumbThumbs = mViewHolder.mThumbsBar.getChildCount();
97        assertTrue((mNumbThumbs & 1) != 0);
98    }
99
100    void sendKeyUIThread(int keyCode) {
101        sendKeyUIThread(keyCode, 1);
102    }
103
104    void sendKeyUIThread(final int keyCode, final int repeat) {
105        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
106            @Override
107            public void run() {
108                mHost.sendKeyDownUp(keyCode, repeat);
109            }
110        });
111    }
112
113    void verifyGetThumbCalls(int firstHeroIndex, int lastHeroIndex,
114            PlaybackSeekDataProvider provider, long[] positions) {
115        int firstThumbIndex = Math.max(firstHeroIndex - (mNumbThumbs / 2), 0);
116        int lastThumbIndex = Math.min(lastHeroIndex + (mNumbThumbs / 2), positions.length - 1);
117        for (int i = firstThumbIndex; i <= lastThumbIndex; i++) {
118            Mockito.verify(provider, times(1)).getThumbnail(eq(i), any(ResultCallback.class));
119        }
120        Mockito.verify(provider, times(0)).getThumbnail(
121                eq(firstThumbIndex - 1), any(ResultCallback.class));
122        Mockito.verify(provider, times(0)).getThumbnail(
123                eq(firstThumbIndex - 2), any(ResultCallback.class));
124        Mockito.verify(provider, times(0)).getThumbnail(
125                eq(lastThumbIndex + 1), any(ResultCallback.class));
126        Mockito.verify(provider, times(0)).getThumbnail(
127                eq(lastThumbIndex + 2), any(ResultCallback.class));
128    }
129
130    void verifyAtHeroIndexWithDifferentPosition(long position, int heroIndex) {
131        assertEquals(position, mGlue.getControlsRow().getCurrentPosition());
132        assertEquals(mViewHolder.mThumbHeroIndex, heroIndex);
133    }
134
135    void verifyAtHeroIndex(long[] positions, int heroIndex) {
136        verifyAtHeroIndex(positions, heroIndex, null);
137    }
138
139    void verifyAtHeroIndex(long[] positions, int heroIndex, Bitmap[] thumbs) {
140        assertEquals(positions[heroIndex], mGlue.getControlsRow().getCurrentPosition());
141        assertEquals(mViewHolder.mThumbHeroIndex, heroIndex);
142        if (thumbs != null) {
143            int start = Math.max(0, mViewHolder.mThumbHeroIndex - mNumbThumbs / 2);
144            int end = Math.min(positions.length - 1, mViewHolder.mThumbHeroIndex + mNumbThumbs / 2);
145            verifyThumbBitmaps(thumbs, start, end,
146                    mViewHolder.mThumbsBar, start + mNumbThumbs / 2 - mViewHolder.mThumbHeroIndex,
147                    end + mNumbThumbs / 2 - mViewHolder.mThumbHeroIndex);
148        }
149    }
150
151    void verifyThumbBitmaps(Bitmap[] thumbs, int start, int end,
152            ThumbsBar thumbsBar, int childStart, int childEnd) {
153        assertEquals(end - start, childEnd - childStart);
154        for (int i = start; i <= end; i++) {
155            assertSame(thumbs[i], thumbsBar.getThumbBitmap(childStart + (i - start)));
156        }
157        for (int i = 0; i < childStart; i++) {
158            assertNull(thumbsBar.getThumbBitmap(i));
159        }
160        for (int i = childEnd + 1; i < mNumbThumbs; i++) {
161            assertNull(thumbsBar.getThumbBitmap(i));
162        }
163    }
164
165    @Test
166    public void progressUpdating() {
167        when(mImpl.isPrepared()).thenReturn(true);
168        when(mImpl.getCurrentPosition()).thenReturn(123L);
169        when(mImpl.getDuration()).thenReturn(20000L);
170        when(mImpl.getBufferedPosition()).thenReturn(321L);
171
172        mGlue.play();
173        Mockito.verify(mImpl, times(1)).play();
174        mGlue.pause();
175        Mockito.verify(mImpl, times(1)).pause();
176        mGlue.seekTo(1231);
177        Mockito.verify(mImpl, times(1)).seekTo(1231);
178        mImpl.getCallback().onCurrentPositionChanged(mImpl);
179        mImpl.getCallback().onDurationChanged(mImpl);
180        mImpl.getCallback().onBufferedPositionChanged(mImpl);
181        assertEquals(123L, mGlue.getCurrentPosition());
182        assertEquals(20000L, mGlue.getDuration());
183        assertEquals(321L, mGlue.getBufferedPosition());
184        assertEquals(123L, mViewHolder.mCurrentTimeInMs);
185        assertEquals(20000L, mViewHolder.mTotalTimeInMs);
186        assertEquals(321L, mViewHolder.mSecondaryProgressInMs);
187
188        when(mImpl.getCurrentPosition()).thenReturn(124L);
189        mImpl.getCallback().onCurrentPositionChanged(mImpl);
190        assertEquals(124L, mGlue.getControlsRow().getCurrentPosition());
191        assertEquals(124L, mViewHolder.mCurrentTimeInMs);
192        when(mImpl.getBufferedPosition()).thenReturn(333L);
193        mImpl.getCallback().onBufferedPositionChanged(mImpl);
194        assertEquals(333L, mGlue.getControlsRow().getBufferedPosition());
195        assertEquals(333L, mViewHolder.mSecondaryProgressInMs);
196        when(mImpl.getDuration()).thenReturn((long) (Integer.MAX_VALUE) * 2);
197        mImpl.getCallback().onDurationChanged(mImpl);
198        assertEquals((long) (Integer.MAX_VALUE) * 2, mGlue.getControlsRow().getDuration());
199        assertEquals((long) (Integer.MAX_VALUE) * 2, mViewHolder.mTotalTimeInMs);
200    }
201
202    @Test
203    public void mediaInfo() {
204        final ColorDrawable art = new ColorDrawable();
205        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
206            @Override
207            public void run() {
208                mGlue.setTitle("xyz");
209                mGlue.setSubtitle("zyx");
210                mGlue.setArt(art);
211            }
212        });
213        assertEquals("xyz", mDescriptionViewHolder.mTitle.getText());
214        assertEquals("zyx", mDescriptionViewHolder.mSubtitle.getText());
215        assertSame(art, mViewHolder.mImageView.getDrawable());
216    }
217
218    static boolean isDescendant(View view, View descendant) {
219        while (descendant != view) {
220            ViewParent p = descendant.getParent();
221            if (!(p instanceof View)) {
222                return false;
223            }
224            descendant = (View) p;
225        }
226        return true;
227    }
228
229    @Test
230    public void navigateRightInPrimary() {
231        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
232            @Override
233            public void run() {
234                mViewHolder.mControlsVh.mControlBar.getChildAt(0).requestFocus();
235            }
236        });
237        View view = mViewHolder.view.findFocus();
238        assertTrue(isDescendant(mViewHolder.mControlsVh.mControlBar.getChildAt(0), view));
239        assertTrue(isDescendant(mViewHolder.mControlsVh.mControlBar.getChildAt(1),
240                view.focusSearch(View.FOCUS_RIGHT)));
241    }
242
243    @Test
244    public void navigateRightInSecondary() {
245        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
246            @Override
247            public void run() {
248                mViewHolder.mSecondaryControlsVh.mControlBar.getChildAt(0).requestFocus();
249            }
250        });
251        View view = mViewHolder.view.findFocus();
252        assertTrue(isDescendant(mViewHolder.mSecondaryControlsVh.mControlBar.getChildAt(0), view));
253        assertTrue(isDescendant(mViewHolder.mSecondaryControlsVh.mControlBar.getChildAt(1),
254                view.focusSearch(View.FOCUS_RIGHT)));
255    }
256
257    @Test
258    public void navigatePrimaryDownToProgress() {
259        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
260            @Override
261            public void run() {
262                mViewHolder.mControlsVh.mControlBar.getChildAt(0).requestFocus();
263            }
264        });
265        View view = mViewHolder.view.findFocus();
266        assertTrue(isDescendant(mViewHolder.mControlsVh.mControlBar.getChildAt(0), view));
267        assertSame(mViewHolder.mProgressBar, view.focusSearch(View.FOCUS_DOWN));
268    }
269
270    @Test
271    public void navigateProgressUpToPrimary() {
272        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
273            @Override
274            public void run() {
275                mViewHolder.mProgressBar.requestFocus();
276            }
277        });
278        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
279            @Override
280            public void run() {
281                mViewHolder.mProgressBar.focusSearch(View.FOCUS_UP).requestFocus();
282            }
283        });
284        View view = mViewHolder.view.findFocus();
285        assertTrue(isDescendant(mViewHolder.mControlsVh.mControlBar.getChildAt(0), view));
286    }
287
288    @Test
289    public void navigateProgressDownToSecondary() {
290        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
291            @Override
292            public void run() {
293                mViewHolder.mProgressBar.requestFocus();
294            }
295        });
296        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
297            @Override
298            public void run() {
299                mViewHolder.mProgressBar.focusSearch(View.FOCUS_DOWN).requestFocus();
300            }
301        });
302        View view = mViewHolder.view.findFocus();
303        assertTrue(isDescendant(mViewHolder.mSecondaryControlsVh.mControlBar.getChildAt(0), view));
304    }
305
306    @Test
307    public void navigateSecondaryUpToProgress() {
308        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
309            @Override
310            public void run() {
311                mViewHolder.mSecondaryControlsVh.mControlBar.getChildAt(0).requestFocus();
312            }
313        });
314        View view = mViewHolder.view.findFocus();
315        assertTrue(isDescendant(mViewHolder.mSecondaryControlsVh.mControlBar.getChildAt(0), view));
316        assertSame(mViewHolder.mProgressBar, view.focusSearch(View.FOCUS_UP));
317    }
318
319    @Test
320    public void seekAndConfirm() {
321        when(mImpl.isPrepared()).thenReturn(true);
322        when(mImpl.getCurrentPosition()).thenReturn(0L);
323        when(mImpl.getDuration()).thenReturn(20000L);
324        when(mImpl.getBufferedPosition()).thenReturn(321L);
325        mImpl.getCallback().onCurrentPositionChanged(mImpl);
326        mImpl.getCallback().onDurationChanged(mImpl);
327        mImpl.getCallback().onBufferedPositionChanged(mImpl);
328
329        PlaybackSeekProviderSample provider = Mockito.spy(
330                new PlaybackSeekProviderSample(10000L, 101));
331        final long[] positions = provider.getSeekPositions();
332        mGlue.setSeekProvider(provider);
333        mViewHolder.mProgressBar.requestFocus();
334        assertTrue(mViewHolder.mProgressBar.hasFocus());
335
336        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_RIGHT);
337        verifyAtHeroIndex(positions, 1);
338        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_RIGHT);
339        verifyAtHeroIndex(positions, 2);
340
341        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_CENTER);
342        Mockito.verify(mImpl).seekTo(positions[2]);
343
344        verifyGetThumbCalls(1, 2, provider, positions);
345    }
346
347    @Test
348    public void playSeekToZero() {
349        when(mImpl.isPrepared()).thenReturn(true);
350        when(mImpl.getCurrentPosition()).thenReturn(0L);
351        when(mImpl.getDuration()).thenReturn(20000L);
352        when(mImpl.getBufferedPosition()).thenReturn(321L);
353        mImpl.getCallback().onCurrentPositionChanged(mImpl);
354        mImpl.getCallback().onDurationChanged(mImpl);
355        mImpl.getCallback().onBufferedPositionChanged(mImpl);
356
357        PlaybackSeekProviderSample provider = Mockito.spy(
358                new PlaybackSeekProviderSample(10000L, 101));
359        final long[] positions = provider.getSeekPositions();
360        mGlue.setSeekProvider(provider);
361
362        // start play
363        mGlue.play();
364        verify(mImpl).play();
365
366        // focus to seek bar
367        mViewHolder.mProgressBar.requestFocus();
368        assertTrue(mViewHolder.mProgressBar.hasFocus());
369
370        // using DPAD_RIGHT to initiate seeking
371        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_RIGHT);
372        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_RIGHT);
373        verifyAtHeroIndex(positions, 2);
374        // press DPAD_CENTER to seek to new position and continue play
375        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_CENTER);
376        verify(mImpl).seekTo(positions[2]);
377        verify(mImpl).play();
378
379        // press DPAD_LEFT seek to 0
380        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_LEFT);
381        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_LEFT);
382        verifyAtHeroIndex(positions, 0);
383        // press DPAD_CENTER to continue play from 0
384        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_CENTER);
385        verify(mImpl).seekTo(0);
386        verify(mImpl).play();
387    }
388
389    @Test
390    public void playSeekAndCancel() {
391        when(mImpl.isPrepared()).thenReturn(true);
392        when(mImpl.getCurrentPosition()).thenReturn(0L);
393        when(mImpl.getDuration()).thenReturn(20000L);
394        when(mImpl.getBufferedPosition()).thenReturn(321L);
395        mImpl.getCallback().onCurrentPositionChanged(mImpl);
396        mImpl.getCallback().onDurationChanged(mImpl);
397        mImpl.getCallback().onBufferedPositionChanged(mImpl);
398
399        PlaybackSeekProviderSample provider = Mockito.spy(
400                new PlaybackSeekProviderSample(10000L, 101));
401        final long[] positions = provider.getSeekPositions();
402        mGlue.setSeekProvider(provider);
403
404        // start play
405        mGlue.play();
406        verify(mImpl).play();
407
408        // focus to seek bar
409        mViewHolder.mProgressBar.requestFocus();
410        assertTrue(mViewHolder.mProgressBar.hasFocus());
411
412        // using DPAD_RIGHT to initiate seeking
413        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_RIGHT);
414        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_RIGHT);
415        verifyAtHeroIndex(positions, 2);
416        // press DPAD_CENTER to seek to new position and continue play
417        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_CENTER);
418        verify(mImpl).seekTo(positions[2]);
419        verify(mImpl).play();
420
421        // press DPAD_LEFT seek to 0
422        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_LEFT);
423        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_LEFT);
424        verifyAtHeroIndex(positions, 0);
425        // press BACK to cancel and continue play from position before seek
426        sendKeyUIThread(KeyEvent.KEYCODE_BACK);
427        verify(mImpl).seekTo(positions[2]);
428        verify(mImpl).play();
429    }
430
431    @Test
432    public void seekHoldKeyDown() {
433        when(mImpl.isPrepared()).thenReturn(true);
434        when(mImpl.getCurrentPosition()).thenReturn(4489L);
435        when(mImpl.getDuration()).thenReturn(20000L);
436        when(mImpl.getBufferedPosition()).thenReturn(4489L);
437        mImpl.getCallback().onCurrentPositionChanged(mImpl);
438        mImpl.getCallback().onDurationChanged(mImpl);
439        mImpl.getCallback().onBufferedPositionChanged(mImpl);
440
441        PlaybackSeekProviderSample provider = Mockito.spy(
442                new PlaybackSeekProviderSample(10000L, 101));
443        final long[] positions = provider.getSeekPositions();
444        mGlue.setSeekProvider(provider);
445        mViewHolder.mProgressBar.requestFocus();
446        assertTrue(mViewHolder.mProgressBar.hasFocus());
447
448        int insertPosition = -1 - Arrays.binarySearch(positions, 4489L);
449        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_RIGHT, 5);
450        verifyAtHeroIndex(positions, insertPosition + 4);
451        verifyGetThumbCalls(insertPosition, insertPosition + 4, provider, positions);
452
453        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_LEFT, 5);
454        verifyAtHeroIndex(positions, insertPosition - 1);
455    }
456
457    @Test
458    public void seekAndCancel() {
459        when(mImpl.isPrepared()).thenReturn(true);
460        when(mImpl.getCurrentPosition()).thenReturn(0L);
461        when(mImpl.getDuration()).thenReturn(20000L);
462        when(mImpl.getBufferedPosition()).thenReturn(321L);
463        mImpl.getCallback().onCurrentPositionChanged(mImpl);
464        mImpl.getCallback().onDurationChanged(mImpl);
465        mImpl.getCallback().onBufferedPositionChanged(mImpl);
466
467        PlaybackSeekProviderSample provider = Mockito.spy(
468                new PlaybackSeekProviderSample(10000L, 101));
469        final long[] positions = provider.getSeekPositions();
470        mGlue.setSeekProvider(provider);
471        mViewHolder.mProgressBar.requestFocus();
472        assertTrue(mViewHolder.mProgressBar.hasFocus());
473
474        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_RIGHT);
475        verifyAtHeroIndex(positions, 1);
476
477        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_RIGHT);
478        verifyAtHeroIndex(positions, 2);
479
480        sendKeyUIThread(KeyEvent.KEYCODE_BACK);
481        Mockito.verify(mImpl, times(0)).seekTo(anyInt());
482        verifyGetThumbCalls(1, 2, provider, positions);
483    }
484
485    @Test
486    public void seekUpBetweenTwoKeyPosition() {
487        PlaybackSeekProviderSample provider = Mockito.spy(
488                new PlaybackSeekProviderSample(10000L, 101));
489        final long[] positions = provider.getSeekPositions();
490
491        // initially select between 0 and 1
492        when(mImpl.isPrepared()).thenReturn(true);
493        when(mImpl.getCurrentPosition()).thenReturn((positions[0] + positions[1]) / 2);
494        mImpl.getCallback().onCurrentPositionChanged(mImpl);
495        when(mImpl.getDuration()).thenReturn(20000L);
496        when(mImpl.getBufferedPosition()).thenReturn(321L);
497        mImpl.getCallback().onDurationChanged(mImpl);
498        mImpl.getCallback().onBufferedPositionChanged(mImpl);
499
500        mGlue.setSeekProvider(provider);
501        mViewHolder.mProgressBar.requestFocus();
502        assertTrue(mViewHolder.mProgressBar.hasFocus());
503
504        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_RIGHT);
505        verifyAtHeroIndex(positions, 1);
506        verifyGetThumbCalls(1, 1, provider, positions);
507    }
508
509    @Test
510    public void seekDownBetweenTwoKeyPosition() {
511        PlaybackSeekProviderSample provider = Mockito.spy(
512                new PlaybackSeekProviderSample(10000L, 101));
513        final long[] positions = provider.getSeekPositions();
514        assertTrue(positions[0] == 0);
515
516        // initially select between 0 and 1
517        when(mImpl.isPrepared()).thenReturn(true);
518        when(mImpl.getCurrentPosition()).thenReturn((positions[0] + positions[1]) / 2);
519        mImpl.getCallback().onCurrentPositionChanged(mImpl);
520        when(mImpl.getDuration()).thenReturn(20000L);
521        when(mImpl.getBufferedPosition()).thenReturn(321L);
522        mImpl.getCallback().onDurationChanged(mImpl);
523        mImpl.getCallback().onBufferedPositionChanged(mImpl);
524
525        mGlue.setSeekProvider(provider);
526        mViewHolder.mProgressBar.requestFocus();
527        assertTrue(mViewHolder.mProgressBar.hasFocus());
528
529        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_LEFT);
530        verifyAtHeroIndex(positions, 0);
531        verifyGetThumbCalls(0, 0, provider, positions);
532    }
533
534    @Test
535    public void seekDownOutOfKeyPositions() {
536        PlaybackSeekProviderSample provider = Mockito.spy(
537                new PlaybackSeekProviderSample(1000L, 10000L, 101));
538        final long[] positions = provider.getSeekPositions();
539        assertTrue(positions[0] > 0);
540
541        // initially select between 0 and 1
542        when(mImpl.isPrepared()).thenReturn(true);
543        when(mImpl.getCurrentPosition()).thenReturn((positions[0] + positions[1]) / 2);
544        mImpl.getCallback().onCurrentPositionChanged(mImpl);
545        when(mImpl.getDuration()).thenReturn(20000L);
546        when(mImpl.getBufferedPosition()).thenReturn(321L);
547        mImpl.getCallback().onDurationChanged(mImpl);
548        mImpl.getCallback().onBufferedPositionChanged(mImpl);
549
550        mGlue.setSeekProvider(provider);
551        mViewHolder.mProgressBar.requestFocus();
552        assertTrue(mViewHolder.mProgressBar.hasFocus());
553
554        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_LEFT);
555        verifyAtHeroIndex(positions, 0);
556        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_LEFT);
557        verifyAtHeroIndexWithDifferentPosition(0, 0);
558        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_LEFT);
559        verifyAtHeroIndexWithDifferentPosition(0, 0);
560        verifyGetThumbCalls(0, 0, provider, positions);
561    }
562
563    @Test
564    public void seekDownAheadOfKeyPositions() {
565        PlaybackSeekProviderSample provider = Mockito.spy(
566                new PlaybackSeekProviderSample(1000L, 10000L, 101));
567        final long[] positions = provider.getSeekPositions();
568        assertTrue(positions[0] > 0);
569
570        // initially select between 0 and 1
571        when(mImpl.isPrepared()).thenReturn(true);
572        when(mImpl.getCurrentPosition()).thenReturn(positions[0] / 2);
573        mImpl.getCallback().onCurrentPositionChanged(mImpl);
574        when(mImpl.getDuration()).thenReturn(20000L);
575        when(mImpl.getBufferedPosition()).thenReturn(321L);
576        mImpl.getCallback().onDurationChanged(mImpl);
577        mImpl.getCallback().onBufferedPositionChanged(mImpl);
578
579        mGlue.setSeekProvider(provider);
580        mViewHolder.mProgressBar.requestFocus();
581        assertTrue(mViewHolder.mProgressBar.hasFocus());
582
583        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_LEFT);
584        verifyAtHeroIndexWithDifferentPosition(0, 0);
585        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_RIGHT);
586        verifyAtHeroIndex(positions, 0);
587        verifyGetThumbCalls(0, 0, provider, positions);
588    }
589
590    @Test
591    public void seekUpAheadOfKeyPositions() {
592        PlaybackSeekProviderSample provider = Mockito.spy(
593                new PlaybackSeekProviderSample(1000L, 10000L, 101));
594        final long[] positions = provider.getSeekPositions();
595        assertTrue(positions[0] > 0);
596
597        // initially select between 0 and 1
598        when(mImpl.isPrepared()).thenReturn(true);
599        when(mImpl.getCurrentPosition()).thenReturn(positions[0] / 2);
600        mImpl.getCallback().onCurrentPositionChanged(mImpl);
601        when(mImpl.getDuration()).thenReturn(20000L);
602        when(mImpl.getBufferedPosition()).thenReturn(321L);
603        mImpl.getCallback().onDurationChanged(mImpl);
604        mImpl.getCallback().onBufferedPositionChanged(mImpl);
605
606        mGlue.setSeekProvider(provider);
607        mViewHolder.mProgressBar.requestFocus();
608        assertTrue(mViewHolder.mProgressBar.hasFocus());
609
610        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_RIGHT);
611        verifyAtHeroIndex(positions, 0);
612        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_LEFT);
613        verifyAtHeroIndexWithDifferentPosition(0, 0);
614        verifyGetThumbCalls(0, 0, provider, positions);
615    }
616
617    @Test
618    public void seekUpOutOfKeyPositions() {
619        PlaybackSeekProviderSample provider = Mockito.spy(
620                new PlaybackSeekProviderSample(10000L, 101));
621        final long[] positions = provider.getSeekPositions();
622
623        // initially select between nth-1 and nth
624        when(mImpl.isPrepared()).thenReturn(true);
625        when(mImpl.getCurrentPosition()).thenReturn((positions[positions.length - 2]
626                + positions[positions.length - 1]) / 2);
627        mImpl.getCallback().onCurrentPositionChanged(mImpl);
628        when(mImpl.getDuration()).thenReturn(20000L);
629        when(mImpl.getBufferedPosition()).thenReturn(321L);
630        mImpl.getCallback().onDurationChanged(mImpl);
631        mImpl.getCallback().onBufferedPositionChanged(mImpl);
632
633        mGlue.setSeekProvider(provider);
634        mViewHolder.mProgressBar.requestFocus();
635        assertTrue(mViewHolder.mProgressBar.hasFocus());
636
637        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_RIGHT);
638        verifyAtHeroIndex(positions, positions.length - 1);
639        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_LEFT);
640        verifyAtHeroIndex(positions, positions.length - 2);
641        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_RIGHT);
642        verifyAtHeroIndex(positions, positions.length - 1);
643        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_RIGHT);
644        verifyAtHeroIndexWithDifferentPosition(20000L, positions.length - 1);
645        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_RIGHT);
646        verifyAtHeroIndexWithDifferentPosition(20000L, positions.length - 1);
647        verifyGetThumbCalls(positions.length - 2, positions.length - 1, provider, positions);
648    }
649
650    @Test
651    public void seekUpAfterKeyPositions() {
652        PlaybackSeekProviderSample provider = Mockito.spy(
653                new PlaybackSeekProviderSample(10000L, 101));
654        final long[] positions = provider.getSeekPositions();
655
656        // initially select after last item
657        when(mImpl.isPrepared()).thenReturn(true);
658        when(mImpl.getCurrentPosition()).thenReturn(positions[positions.length - 1] + 100);
659        mImpl.getCallback().onCurrentPositionChanged(mImpl);
660        when(mImpl.getDuration()).thenReturn(20000L);
661        when(mImpl.getBufferedPosition()).thenReturn(321L);
662        mImpl.getCallback().onDurationChanged(mImpl);
663        mImpl.getCallback().onBufferedPositionChanged(mImpl);
664
665        mGlue.setSeekProvider(provider);
666        mViewHolder.mProgressBar.requestFocus();
667        assertTrue(mViewHolder.mProgressBar.hasFocus());
668
669        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_RIGHT);
670        verifyAtHeroIndexWithDifferentPosition(20000L, positions.length - 1);
671        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_LEFT);
672        verifyAtHeroIndex(positions, positions.length - 1);
673        verifyGetThumbCalls(positions.length - 1, positions.length - 1, provider, positions);
674    }
675
676    @Test
677    public void seekDownAfterKeyPositions() {
678        PlaybackSeekProviderSample provider = Mockito.spy(
679                new PlaybackSeekProviderSample(10000L, 101));
680        final long[] positions = provider.getSeekPositions();
681
682        // initially select after last item
683        when(mImpl.isPrepared()).thenReturn(true);
684        when(mImpl.getCurrentPosition()).thenReturn(positions[positions.length - 1] + 100);
685        mImpl.getCallback().onCurrentPositionChanged(mImpl);
686        when(mImpl.getDuration()).thenReturn(20000L);
687        when(mImpl.getBufferedPosition()).thenReturn(321L);
688        mImpl.getCallback().onDurationChanged(mImpl);
689        mImpl.getCallback().onBufferedPositionChanged(mImpl);
690
691        mGlue.setSeekProvider(provider);
692        mViewHolder.mProgressBar.requestFocus();
693        assertTrue(mViewHolder.mProgressBar.hasFocus());
694
695        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_LEFT);
696        verifyAtHeroIndex(positions, positions.length - 1);
697        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_RIGHT);
698        verifyAtHeroIndexWithDifferentPosition(20000L, positions.length - 1);
699        verifyGetThumbCalls(positions.length - 1, positions.length - 1, provider, positions);
700    }
701
702    @Test
703    public void thumbLoadedInCallback() {
704        when(mImpl.isPrepared()).thenReturn(true);
705        when(mImpl.getCurrentPosition()).thenReturn(0L);
706        when(mImpl.getDuration()).thenReturn(20000L);
707        when(mImpl.getBufferedPosition()).thenReturn(321L);
708        mImpl.getCallback().onCurrentPositionChanged(mImpl);
709        mImpl.getCallback().onDurationChanged(mImpl);
710        mImpl.getCallback().onBufferedPositionChanged(mImpl);
711
712        final Bitmap[] thumbs = new Bitmap[101];
713        for (int i = 0; i < 101; i++) {
714            thumbs[i] = Bitmap.createBitmap(16, 16, Bitmap.Config.ARGB_8888);
715        }
716        PlaybackSeekProviderSample provider = new PlaybackSeekProviderSample(10000L, 101) {
717            @Override
718            public void getThumbnail(int index, ResultCallback callback) {
719                callback.onThumbnailLoaded(thumbs[index], index);
720            }
721        };
722        final long[] positions = provider.getSeekPositions();
723        mGlue.setSeekProvider(provider);
724        mViewHolder.mProgressBar.requestFocus();
725        assertTrue(mViewHolder.mProgressBar.hasFocus());
726
727        sendKeyUIThread(KeyEvent.KEYCODE_DPAD_RIGHT);
728        verifyAtHeroIndex(positions, 1, thumbs);
729    }
730
731}
732