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 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()); 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