Home | History | Annotate | Download | only in data
      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.test.AndroidTestCase;
     26 import android.test.mock.MockContentProvider;
     27 import android.test.mock.MockContentResolver;
     28 import android.test.mock.MockCursor;
     29 import android.test.suitebuilder.annotation.SmallTest;
     30 import android.util.Log;
     31 import android.util.SparseArray;
     32 
     33 import com.android.tv.testing.Constants;
     34 import com.android.tv.testing.ProgramInfo;
     35 import com.android.tv.testing.FakeClock;
     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
     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());
     82         mProgramDataManager.setPrefetchEnabled(true);
     83         mProgramDataManager.addListener(mListener);
     84     }
     85 
     86     @Override
     87     protected void tearDown() throws Exception {
     88         super.tearDown();
     89         mHandlerThread.quitSafely();
     90         mProgramDataManager.stop();
     91     }
     92 
     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      */
    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