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  */
16 
17 package com.android.tv.data;
18 
19 import android.content.Context;
20 import android.database.ContentObserver;
21 import android.database.Cursor;
22 import android.media.tv.TvContract;
23 import android.net.Uri;
24 import android.os.HandlerThread;
25 import android.support.test.filters.SmallTest;
26 import android.test.AndroidTestCase;
27 import android.test.mock.MockContentProvider;
28 import android.test.mock.MockContentResolver;
29 import android.test.mock.MockCursor;
30 import android.util.Log;
31 import android.util.SparseArray;
32 
33 import com.android.tv.testing.Constants;
34 import com.android.tv.testing.FakeClock;
35 import com.android.tv.testing.ProgramInfo;
36 import com.android.tv.util.Utils;
37 
38 import java.util.ArrayList;
39 import java.util.Arrays;
40 import java.util.List;
41 import java.util.concurrent.CountDownLatch;
42 import java.util.concurrent.TimeUnit;
43 
44 /**
45  * Test for {@link com.android.tv.data.ProgramDataManager}
46  */
47 @SmallTest
48 public class ProgramDataManagerTest extends AndroidTestCase {
49     private static final boolean DEBUG = false;
50     private static final String TAG = "ProgramDataManagerTest";
51 
52     // Wait time for expected success.
53     private static final long WAIT_TIME_OUT_MS = 1000L;
54     // Wait time for expected failure.
55     private static final long FAILURE_TIME_OUT_MS = 300L;
56 
57     // TODO: Use TvContract constants, once they become public.
58     private static final String PARAM_CHANNEL = "channel";
59     private static final String PARAM_START_TIME = "start_time";
60     private static final String PARAM_END_TIME = "end_time";
61 
62     private ProgramDataManager mProgramDataManager;
63     private FakeClock mClock;
64     private HandlerThread mHandlerThread;
65     private TestProgramDataManagerListener mListener;
66     private FakeContentResolver mContentResolver;
67     private FakeContentProvider mContentProvider;
68 
69     @Override
setUp()70     protected void setUp() throws Exception {
71         super.setUp();
72 
73         mClock = FakeClock.createWithCurrentTime();
74         mListener = new TestProgramDataManagerListener();
75         mContentProvider = new FakeContentProvider(getContext());
76         mContentResolver = new FakeContentResolver();
77         mContentResolver.addProvider(TvContract.AUTHORITY, mContentProvider);
78         mHandlerThread = new HandlerThread(TAG);
79         mHandlerThread.start();
80         mProgramDataManager = new ProgramDataManager(
81                 mContentResolver, mClock, mHandlerThread.getLooper(), null);
82         mProgramDataManager.setPrefetchEnabled(true);
83         mProgramDataManager.addListener(mListener);
84     }
85 
86     @Override
tearDown()87     protected void tearDown() throws Exception {
88         super.tearDown();
89         mHandlerThread.quitSafely();
90         mProgramDataManager.stop();
91     }
92 
startAndWaitForComplete()93     private void startAndWaitForComplete() throws Exception {
94         mProgramDataManager.start();
95         assertTrue(mListener.programUpdatedLatch.await(WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS));
96     }
97 
98     /**
99      * Test for {@link ProgramInfo#getIndex} and {@link ProgramInfo#getStartTimeMs}.
100      */
testProgramUtils()101     public void testProgramUtils() {
102         ProgramInfo stub = ProgramInfo.create();
103         for (long channelId = 1; channelId < Constants.UNIT_TEST_CHANNEL_COUNT; channelId++) {
104             int index = stub.getIndex(mClock.currentTimeMillis(), channelId);
105             long startTimeMs = stub.getStartTimeMs(index, channelId);
106             ProgramInfo programAt = stub.build(getContext(), index);
107             assertTrue(startTimeMs <= mClock.currentTimeMillis());
108             assertTrue(mClock.currentTimeMillis() < startTimeMs + programAt.durationMs);
109         }
110     }
111 
112     /**
113      * Test for following methods.
114      *
115      * <p>
116      * {@link ProgramDataManager#getCurrentProgram(long)},
117      * {@link ProgramDataManager#getPrograms(long, long)},
118      * {@link ProgramDataManager#setPrefetchTimeRange(long)}.
119      * </p>
120      */
121     public void testGetPrograms() throws Exception {
122         // Initial setup to test {@link ProgramDataManager#setPrefetchTimeRange(long)}.
123         long preventSnapDelayMs = ProgramDataManager.PROGRAM_GUIDE_SNAP_TIME_MS * 2;
124         long prefetchTimeRangeStartMs = System.currentTimeMillis() + preventSnapDelayMs;
125         mClock.setCurrentTimeMillis(prefetchTimeRangeStartMs + preventSnapDelayMs);
126         mProgramDataManager.setPrefetchTimeRange(prefetchTimeRangeStartMs);
127 
128         startAndWaitForComplete();
129 
130         for (long channelId = 1; channelId <= Constants.UNIT_TEST_CHANNEL_COUNT; channelId++) {
131             Program currentProgram = mProgramDataManager.getCurrentProgram(channelId);
132             // Test {@link ProgramDataManager#getCurrentProgram(long)}.
133             assertTrue(currentProgram.getStartTimeUtcMillis() <= mClock.currentTimeMillis()
134                     && mClock.currentTimeMillis() <= currentProgram.getEndTimeUtcMillis());
135 
136             // Test {@link ProgramDataManager#getPrograms(long)}.
137             // Case #1: Normal case
138             List<Program> programs =
139                     mProgramDataManager.getPrograms(channelId, mClock.currentTimeMillis());
140             ProgramInfo stub = ProgramInfo.create();
141             int index = stub.getIndex(mClock.currentTimeMillis(), channelId);
142             for (Program program : programs) {
143                 ProgramInfo programInfoAt = stub.build(getContext(), index);
144                 long startTimeMs = stub.getStartTimeMs(index, channelId);
145                 assertProgramEquals(startTimeMs, programInfoAt, program);
146                 index++;
147             }
148             // Case #2: Corner cases where there's a program that starts at the start of the range.
149             long startTimeMs = programs.get(0).getStartTimeUtcMillis();
150             programs = mProgramDataManager.getPrograms(channelId, startTimeMs);
151             assertEquals(startTimeMs, programs.get(0).getStartTimeUtcMillis());
152 
153             // Test {@link ProgramDataManager#setPrefetchTimeRange(long)}.
154             programs = mProgramDataManager.getPrograms(channelId,
155                     prefetchTimeRangeStartMs - TimeUnit.HOURS.toMillis(1));
156             for (Program program : programs) {
157                 assertTrue(program.getEndTimeUtcMillis() >= prefetchTimeRangeStartMs);
158             }
159         }
160     }
161 
162     /**
163      * Test for following methods.
164      *
165      * <p>
166      * {@link ProgramDataManager#addOnCurrentProgramUpdatedListener},
167      * {@link ProgramDataManager#removeOnCurrentProgramUpdatedListener}.
168      * </p>
169      */
170     public void testCurrentProgramListener() throws Exception {
171         final long testChannelId = 1;
172         ProgramInfo stub = ProgramInfo.create();
173         int index = stub.getIndex(mClock.currentTimeMillis(), testChannelId);
174         // Set current time to few seconds before the current program ends,
175         // so we can see if callback is called as expected.
176         long nextProgramStartTimeMs = stub.getStartTimeMs(index + 1, testChannelId);
177         ProgramInfo nextProgramInfo = stub.build(getContext(), index + 1);
178         mClock.setCurrentTimeMillis(nextProgramStartTimeMs - (WAIT_TIME_OUT_MS / 2));
179 
180         startAndWaitForComplete();
181         // Note that changing current time doesn't affect the current program
182         // because current program is updated after waiting for the program's duration.
183         // See {@link ProgramDataManager#updateCurrentProgram}.
184         mClock.setCurrentTimeMillis(mClock.currentTimeMillis() + WAIT_TIME_OUT_MS);
185         TestProgramDataManagerOnCurrentProgramUpdatedListener listener =
186                 new TestProgramDataManagerOnCurrentProgramUpdatedListener();
187         mProgramDataManager.addOnCurrentProgramUpdatedListener(testChannelId, listener);
188         assertTrue(
189                 listener.currentProgramUpdatedLatch.await(WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS));
190         assertEquals(testChannelId, listener.updatedChannelId);
191         Program currentProgram = mProgramDataManager.getCurrentProgram(testChannelId);
192         assertProgramEquals(nextProgramStartTimeMs, nextProgramInfo, currentProgram);
193         assertEquals(listener.updatedProgram, currentProgram);
194     }
195 
196     /**
197      * Test if program data is refreshed after the program insertion.
198      */
199     public void testContentProviderUpdate() throws Exception {
200         final long testChannelId = 1;
201         startAndWaitForComplete();
202         // Force program data manager to update program data whenever it's changes.
203         mProgramDataManager.setProgramPrefetchUpdateWait(0);
204         mListener.reset();
205         List<Program> programList =
206                 mProgramDataManager.getPrograms(testChannelId, mClock.currentTimeMillis());
207         assertNotNull(programList);
208         long lastProgramEndTime = programList.get(programList.size() - 1).getEndTimeUtcMillis();
209         // Make change in content provider
210         mContentProvider.simulateAppend(testChannelId);
211         assertTrue(mListener.programUpdatedLatch.await(WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS));
212         programList = mProgramDataManager.getPrograms(testChannelId, mClock.currentTimeMillis());
213         assertTrue(
214                 lastProgramEndTime < programList.get(programList.size() - 1).getEndTimeUtcMillis());
215     }
216 
217     /**
218      * Test for {@link ProgramDataManager#setPauseProgramUpdate(boolean)}.
219      */
220     public void testSetPauseProgramUpdate() throws Exception {
221         final long testChannelId = 1;
222         startAndWaitForComplete();
223         // Force program data manager to update program data whenever it's changes.
224         mProgramDataManager.setProgramPrefetchUpdateWait(0);
225         mListener.reset();
226         mProgramDataManager.setPauseProgramUpdate(true);
227         mContentProvider.simulateAppend(testChannelId);
228         assertFalse(mListener.programUpdatedLatch.await(FAILURE_TIME_OUT_MS,
229                 TimeUnit.MILLISECONDS));
230     }
231 
232     public static void assertProgramEquals(long expectedStartTime, ProgramInfo expectedInfo,
233             Program actualProgram) {
234         assertEquals("title", expectedInfo.title, actualProgram.getTitle());
235         assertEquals("episode", expectedInfo.episode, actualProgram.getEpisodeTitle());
236         assertEquals("description", expectedInfo.description, actualProgram.getDescription());
237         assertEquals("startTime", expectedStartTime, actualProgram.getStartTimeUtcMillis());
238         assertEquals("endTime", expectedStartTime + expectedInfo.durationMs,
239                 actualProgram.getEndTimeUtcMillis());
240     }
241 
242     private class FakeContentResolver extends MockContentResolver {
243         @Override
244         public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) {
245             super.notifyChange(uri, observer, syncToNetwork);
246             if (DEBUG) {
247                 Log.d(TAG, "onChanged(uri=" + uri + ")");
248             }
249             if (observer != null) {
250                 observer.dispatchChange(false, uri);
251             } else {
252                 mProgramDataManager.getContentObserver().dispatchChange(false, uri);
253             }
254         }
255     }
256 
257     private static class ProgramInfoWrapper {
258         private final int index;
259         private final long startTimeMs;
260         private final ProgramInfo programInfo;
261 
262         public ProgramInfoWrapper(int index, long startTimeMs, ProgramInfo programInfo) {
263             this.index = index;
264             this.startTimeMs = startTimeMs;
265             this.programInfo = programInfo;
266         }
267     }
268 
269     // This implements the minimal methods in content resolver
270     // and detailed assumptions are written in each method.
271     private class FakeContentProvider extends MockContentProvider {
272         private final SparseArray<List<ProgramInfoWrapper>> mProgramInfoList = new SparseArray<>();
273 
274         /**
275          * Constructor for FakeContentProvider
276          * <p>
277          * This initializes program info assuming that
278          * channel IDs are 1, 2, 3, ... {@link Constants#UNIT_TEST_CHANNEL_COUNT}.
279          * </p>
280          */
281         public FakeContentProvider(Context context) {
282             super(context);
283             long startTimeMs = Utils.floorTime(
284                     mClock.currentTimeMillis() - ProgramDataManager.PROGRAM_GUIDE_SNAP_TIME_MS,
285                     ProgramDataManager.PROGRAM_GUIDE_SNAP_TIME_MS);
286             long endTimeMs = startTimeMs + (ProgramDataManager.PROGRAM_GUIDE_MAX_TIME_RANGE / 2);
287             for (int i = 1; i <= Constants.UNIT_TEST_CHANNEL_COUNT; i++) {
288                 List<ProgramInfoWrapper> programInfoList = new ArrayList<>();
289                 ProgramInfo stub = ProgramInfo.create();
290                 int index = stub.getIndex(startTimeMs, i);
291                 long programStartTimeMs = stub.getStartTimeMs(index, i);
292                 while (programStartTimeMs < endTimeMs) {
293                     ProgramInfo programAt = stub.build(getContext(), index);
294                     programInfoList.add(
295                             new ProgramInfoWrapper(index, programStartTimeMs, programAt));
296                     index++;
297                     programStartTimeMs += programAt.durationMs;
298                 }
299                 mProgramInfoList.put(i, programInfoList);
300             }
301         }
302 
303         @Override
304         public Cursor query(Uri uri, String[] projection, String selection,
305                 String[] selectionArgs, String sortOrder) {
306             if (DEBUG) {
307                 Log.d(TAG, "dump query");
308                 Log.d(TAG, "  uri=" + uri);
309                 Log.d(TAG, "  projection=" + Arrays.toString(projection));
310                 Log.d(TAG, "  selection=" + selection);
311             }
312             long startTimeMs = Long.parseLong(uri.getQueryParameter(PARAM_START_TIME));
313             long endTimeMs = Long.parseLong(uri.getQueryParameter(PARAM_END_TIME));
314             if (startTimeMs == 0 || endTimeMs == 0) {
315                 throw new UnsupportedOperationException();
316             }
317             assertProgramUri(uri);
318             long channelId;
319             try {
320                 channelId = Long.parseLong(uri.getQueryParameter(PARAM_CHANNEL));
321             } catch (NumberFormatException e) {
322                 channelId = -1;
323             }
324             return new FakeCursor(projection, channelId, startTimeMs, endTimeMs);
325         }
326 
327         /**
328          * Simulate program data appends at the end of the existing programs.
329          * This appends programs until the maximum program query range
330          * ({@link ProgramDataManager#PROGRAM_GUIDE_MAX_TIME_RANGE})
331          * where we started with the inserting half of it.
332          */
333         public void simulateAppend(long channelId) {
334             long endTimeMs =
335                     mClock.currentTimeMillis() + ProgramDataManager.PROGRAM_GUIDE_MAX_TIME_RANGE;
336             List<ProgramInfoWrapper> programList = mProgramInfoList.get((int) channelId);
337             if (mProgramInfoList == null) {
338                 return;
339             }
340             ProgramInfo stub = ProgramInfo.create();
341             ProgramInfoWrapper last = programList.get(programList.size() - 1);
342             while (last.startTimeMs < endTimeMs) {
343                 ProgramInfo nextProgramInfo = stub.build(getContext(), last.index + 1);
344                 ProgramInfoWrapper next = new ProgramInfoWrapper(last.index + 1,
345                         last.startTimeMs + last.programInfo.durationMs, nextProgramInfo);
346                 programList.add(next);
347                 last = next;
348             }
349             mContentResolver.notifyChange(TvContract.Programs.CONTENT_URI, null);
350         }
351 
352         private void assertProgramUri(Uri uri) {
353             assertTrue("Uri(" + uri + ") isn't channel uri",
354                     uri.toString().startsWith(TvContract.Programs.CONTENT_URI.toString()));
355         }
356 
357         public ProgramInfoWrapper get(long channelId, int position) {
358             List<ProgramInfoWrapper> programList = mProgramInfoList.get((int) channelId);
359             if (programList == null || position >= programList.size()) {
360                 return null;
361             }
362             return programList.get(position);
363         }
364     }
365 
366     private class FakeCursor extends MockCursor {
367         private final String[] ALL_COLUMNS =  {
368                 TvContract.Programs.COLUMN_CHANNEL_ID,
369                 TvContract.Programs.COLUMN_TITLE,
370                 TvContract.Programs.COLUMN_SHORT_DESCRIPTION,
371                 TvContract.Programs.COLUMN_EPISODE_TITLE,
372                 TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS,
373                 TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS};
374         private final String[] mColumns;
375         private final boolean mIsQueryForSingleChannel;
376         private final long mStartTimeMs;
377         private final long mEndTimeMs;
378         private final int mCount;
379         private long mChannelId;
380         private int mProgramPosition;
381         private ProgramInfoWrapper mCurrentProgram;
382 
383         /**
384          * Constructor
385          * @param columns the same as projection passed from {@link FakeContentProvider#query}.
386          *                Can be null for query all.
387          * @param channelId channel ID to query programs belongs to the specified channel.
388          *                  Can be negative to indicate all channels.
389          * @param startTimeMs start of the time range to query programs.
390          * @param endTimeMs end of the time range to query programs.
391          */
392         public FakeCursor(String[] columns, long channelId, long startTimeMs, long endTimeMs) {
393             mColumns = (columns == null) ? ALL_COLUMNS : columns;
394             mIsQueryForSingleChannel = (channelId > 0);
395             mChannelId = channelId;
396             mProgramPosition = -1;
397             mStartTimeMs = startTimeMs;
398             mEndTimeMs = endTimeMs;
399             int count = 0;
400             while (moveToNext()) {
401                 count++;
402             }
403             mCount = count;
404             // Rewind channel Id and program index.
405             mChannelId = channelId;
406             mProgramPosition = -1;
407             if (DEBUG) {
408                 Log.d(TAG, "FakeCursor(columns=" + Arrays.toString(columns)
409                         + ", channelId=" + channelId + ", startTimeMs=" + startTimeMs
410                         + ", endTimeMs=" + endTimeMs + ") has mCount=" + mCount);
411             }
412         }
413 
414         @Override
415         public String getColumnName(int columnIndex) {
416             return mColumns[columnIndex];
417         }
418 
419         @Override
420         public int getColumnIndex(String columnName) {
421             for (int i = 0; i < mColumns.length; i++) {
422                 if (mColumns[i].equalsIgnoreCase(columnName)) {
423                     return i;
424                 }
425             }
426             return -1;
427         }
428 
429         @Override
430         public int getInt(int columnIndex) {
431             if (DEBUG) {
432                 Log.d(TAG, "Column (" + getColumnName(columnIndex) + ") is ignored in getInt()");
433             }
434             return 0;
435         }
436 
437         @Override
438         public long getLong(int columnIndex) {
439             String columnName = getColumnName(columnIndex);
440             switch (columnName) {
441                 case TvContract.Programs.COLUMN_CHANNEL_ID:
442                     return mChannelId;
443                 case TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS:
444                     return mCurrentProgram.startTimeMs;
445                 case TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS:
446                     return mCurrentProgram.startTimeMs + mCurrentProgram.programInfo.durationMs;
447             }
448             if (DEBUG) {
449                 Log.d(TAG, "Column (" + columnName + ") is ignored in getLong()");
450             }
451             return 0;
452         }
453 
454         @Override
455         public String getString(int columnIndex) {
456             String columnName = getColumnName(columnIndex);
457             switch (columnName) {
458                 case TvContract.Programs.COLUMN_TITLE:
459                     return mCurrentProgram.programInfo.title;
460                 case TvContract.Programs.COLUMN_SHORT_DESCRIPTION:
461                     return mCurrentProgram.programInfo.description;
462                 case TvContract.Programs.COLUMN_EPISODE_TITLE:
463                     return mCurrentProgram.programInfo.episode;
464             }
465             if (DEBUG) {
466                 Log.d(TAG, "Column (" + columnName + ") is ignored in getString()");
467             }
468             return null;
469         }
470 
471         @Override
472         public int getCount() {
473             return mCount;
474         }
475 
476         @Override
477         public boolean moveToNext() {
478             while (true) {
479                 ProgramInfoWrapper program = mContentProvider.get(mChannelId, ++mProgramPosition);
480                 if (program == null || program.startTimeMs >= mEndTimeMs) {
481                     if (mIsQueryForSingleChannel) {
482                         return false;
483                     } else {
484                         if (++mChannelId > Constants.UNIT_TEST_CHANNEL_COUNT) {
485                             return false;
486                         }
487                         mProgramPosition = -1;
488                     }
489                 } else if (program.startTimeMs + program.programInfo.durationMs >= mStartTimeMs) {
490                     mCurrentProgram = program;
491                     break;
492                 }
493             }
494             return true;
495         }
496 
497         @Override
498         public void close() {
499             // No-op.
500         }
501     }
502 
503     private class TestProgramDataManagerListener implements ProgramDataManager.Listener {
504         public CountDownLatch programUpdatedLatch = new CountDownLatch(1);
505 
506         @Override
507         public void onProgramUpdated() {
508             programUpdatedLatch.countDown();
509         }
510 
511         public void reset() {
512             programUpdatedLatch = new CountDownLatch(1);
513         }
514     }
515 
516     private class TestProgramDataManagerOnCurrentProgramUpdatedListener implements
517             OnCurrentProgramUpdatedListener {
518         public final CountDownLatch currentProgramUpdatedLatch = new CountDownLatch(1);
519         public long updatedChannelId = -1;
520         public Program updatedProgram = null;
521 
522         @Override
523         public void onCurrentProgramUpdated(long channelId, Program program) {
524             updatedChannelId = channelId;
525             updatedProgram = program;
526             currentProgramUpdatedLatch.countDown();
527         }
528     }
529 }
530