aboutsummaryrefslogtreecommitdiff
path: root/tests/unit/src/com/android/tv/data/ProgramDataManagerTest.java
diff options
context:
space:
mode:
authorNick Chalko <nchalko@google.com>2015-08-03 15:39:56 -0700
committerNick Chalko <nchalko@google.com>2015-08-03 15:53:37 -0700
commit816a4be1a0f34f6a48877c8afd3dbbca19eac435 (patch)
tree4f18dda269764494942f5313acc93db4a35d47db /tests/unit/src/com/android/tv/data/ProgramDataManagerTest.java
parent6edd2b09e5d16a29c703a5fcbd2e88c5cf5e55b7 (diff)
downloadTV-816a4be1a0f34f6a48877c8afd3dbbca19eac435.tar.gz
Migrate Live Channels App Src to AOSP branch
Bug: 21625152 Change-Id: I07e2830b27440556dc757e6340b4f77d1c0cbc66
Diffstat (limited to 'tests/unit/src/com/android/tv/data/ProgramDataManagerTest.java')
-rw-r--r--tests/unit/src/com/android/tv/data/ProgramDataManagerTest.java533
1 files changed, 533 insertions, 0 deletions
diff --git a/tests/unit/src/com/android/tv/data/ProgramDataManagerTest.java b/tests/unit/src/com/android/tv/data/ProgramDataManagerTest.java
new file mode 100644
index 00000000..31ad54f0
--- /dev/null
+++ b/tests/unit/src/com/android/tv/data/ProgramDataManagerTest.java
@@ -0,0 +1,533 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.data;
+
+import android.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.media.tv.TvContract;
+import android.net.Uri;
+import android.os.HandlerThread;
+import android.test.AndroidTestCase;
+import android.test.mock.MockContentProvider;
+import android.test.mock.MockContentResolver;
+import android.test.mock.MockCursor;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.tv.testing.Constants;
+import com.android.tv.testing.ProgramInfo;
+import com.android.tv.util.FakeClock;
+import com.android.tv.util.Utils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Test for {@link com.android.tv.data.ProgramDataManager}
+ */
+@SmallTest
+public class ProgramDataManagerTest extends AndroidTestCase {
+ private static final boolean DEBUG = false;
+ private static final String TAG = "ProgramDataManagerTest";
+
+ // Wait time for expected success.
+ private static final long WAIT_TIME_OUT_MS = 1000L;
+ // Wait time for expected failure.
+ private static final long FAILURE_TIME_OUT_MS = 300L;
+
+ // TODO: Use TvContract constants, once they become public.
+ private static final String PARAM_CHANNEL = "channel";
+ private static final String PARAM_START_TIME = "start_time";
+ private static final String PARAM_END_TIME = "end_time";
+
+ private ProgramDataManager mProgramDataManager;
+ private FakeClock mClock;
+ private HandlerThread mHandlerThread;
+ private TestProgramDataManagerListener mListener;
+ private FakeContentResolver mContentResolver;
+ private FakeContentProvider mContentProvider;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ mClock = new FakeClock();
+ mListener = new TestProgramDataManagerListener();
+ mContentProvider = new FakeContentProvider(getContext());
+ mContentResolver = new FakeContentResolver();
+ mContentResolver.addProvider(TvContract.AUTHORITY, mContentProvider);
+ mHandlerThread = new HandlerThread(TAG);
+ mHandlerThread.start();
+ mProgramDataManager = new ProgramDataManager(
+ mContentResolver, mClock, mHandlerThread.getLooper());
+ mProgramDataManager.addListener(mListener);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ mHandlerThread.quitSafely();
+ mProgramDataManager.stop();
+ }
+
+ private void startAndWaitForComplete() throws Exception {
+ mProgramDataManager.start();
+ assertTrue(mListener.programUpdatedLatch.await(WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS));
+ }
+
+ private static boolean equals(ProgramInfo lhs, long lhsStarTimeMs, Program rhs) {
+ return TextUtils.equals(lhs.title, rhs.getTitle())
+ && TextUtils.equals(lhs.episode, rhs.getEpisodeTitle())
+ && TextUtils.equals(lhs.description, rhs.getDescription())
+ && lhsStarTimeMs == rhs.getStartTimeUtcMillis()
+ && lhsStarTimeMs + lhs.durationMs == rhs.getEndTimeUtcMillis();
+ }
+
+ /**
+ * Test for {@link ProgramInfo#getIndex} and {@link ProgramInfo#getStartTimeMs}.
+ */
+ public void testProgramUtils() {
+ ProgramInfo stub = ProgramInfo.create();
+ for (long channelId = 1; channelId < Constants.UNIT_TEST_CHANNEL_COUNT; channelId++) {
+ int index = stub.getIndex(mClock.currentTimeMillis(), channelId);
+ long startTimeMs = stub.getStartTimeMs(index, channelId);
+ ProgramInfo programAt = stub.build(getContext(), index);
+ assertTrue(startTimeMs <= mClock.currentTimeMillis());
+ assertTrue(mClock.currentTimeMillis() < startTimeMs + programAt.durationMs);
+ }
+ }
+
+ /**
+ * Test for following methods.
+ *
+ * <p>
+ * {@link ProgramDataManager#getCurrentProgram(long)},
+ * {@link ProgramDataManager#getPrograms(long, long)},
+ * {@link ProgramDataManager#setPrefetchTimeRange(long)}.
+ * </p>
+ */
+ public void testGetPrograms() throws Exception {
+ // Initial setup to test {@link ProgramDataManager#setPrefetchTimeRange(long)}.
+ long preventSnapDelayMs = ProgramDataManager.PROGRAM_GUIDE_SNAP_TIME_MS * 2;
+ long prefetchTimeRangeStartMs = System.currentTimeMillis() + preventSnapDelayMs;
+ mClock.setCurrentTimeMillis(prefetchTimeRangeStartMs + preventSnapDelayMs);
+ mProgramDataManager.setPrefetchTimeRange(prefetchTimeRangeStartMs);
+
+ startAndWaitForComplete();
+
+ for (long channelId = 1; channelId <= Constants.UNIT_TEST_CHANNEL_COUNT; channelId++) {
+ Program currentProgram = mProgramDataManager.getCurrentProgram(channelId);
+ // Test {@link ProgramDataManager#getCurrentProgram(long)}.
+ assertTrue(currentProgram.getStartTimeUtcMillis() <= mClock.currentTimeMillis()
+ && mClock.currentTimeMillis() <= currentProgram.getEndTimeUtcMillis());
+
+ // Test {@link ProgramDataManager#getPrograms(long)}.
+ // Case #1: Normal case
+ List<Program> programs =
+ mProgramDataManager.getPrograms(channelId, mClock.currentTimeMillis());
+ ProgramInfo stub = ProgramInfo.create();
+ int index = stub.getIndex(mClock.currentTimeMillis(), channelId);
+ for (Program program : programs) {
+ ProgramInfo programInfoAt = stub.build(getContext(), index);
+ long startTimeMs = stub.getStartTimeMs(index, channelId);
+ assertTrue(program.toString() + " differ from " + programInfoAt,
+ equals(programInfoAt, startTimeMs, program));
+ index++;
+ }
+ // Case #2: Corner cases where there's a program that starts at the start of the range.
+ long startTimeMs = programs.get(0).getStartTimeUtcMillis();
+ programs = mProgramDataManager.getPrograms(channelId, startTimeMs);
+ assertEquals(startTimeMs, programs.get(0).getStartTimeUtcMillis());
+
+ // Test {@link ProgramDataManager#setPrefetchTimeRange(long)}.
+ programs = mProgramDataManager.getPrograms(channelId,
+ prefetchTimeRangeStartMs - TimeUnit.HOURS.toMillis(1));
+ for (Program program : programs) {
+ assertTrue(program.getEndTimeUtcMillis() >= prefetchTimeRangeStartMs);
+ }
+ }
+ }
+
+ /**
+ * Test for following methods.
+ *
+ * <p>
+ * {@link ProgramDataManager#addOnCurrentProgramUpdatedListener},
+ * {@link ProgramDataManager#removeOnCurrentProgramUpdatedListener}.
+ * </p>
+ */
+ public void testCurrentProgramListener() throws Exception {
+ final long testChannelId = 1;
+ ProgramInfo stub = ProgramInfo.create();
+ int index = stub.getIndex(mClock.currentTimeMillis(), testChannelId);
+ // Set current time to few seconds before the current program ends,
+ // so we can see if callback is called as expected.
+ long nextProgramStartTimeMs = stub.getStartTimeMs(index + 1, testChannelId);
+ ProgramInfo nextProgramInfo = stub.build(getContext(), index + 1);
+ mClock.setCurrentTimeMillis(nextProgramStartTimeMs - (WAIT_TIME_OUT_MS / 2));
+
+ startAndWaitForComplete();
+ // Note that changing current time doesn't affect the current program
+ // because current program is updated after waiting for the program's duration.
+ // See {@link ProgramDataManager#updateCurrentProgram}.
+ mClock.setCurrentTimeMillis(mClock.currentTimeMillis() + WAIT_TIME_OUT_MS);
+ TestProgramDataManagerOnCurrentProgramUpdatedListener listener =
+ new TestProgramDataManagerOnCurrentProgramUpdatedListener();
+ mProgramDataManager.addOnCurrentProgramUpdatedListener(testChannelId, listener);
+ assertTrue(listener.currentProgramUpdatedLatch.await(WAIT_TIME_OUT_MS,
+ TimeUnit.MILLISECONDS));
+ assertEquals(testChannelId, listener.updatedChannelId);
+ assertTrue(ProgramDataManagerTest.equals(
+ nextProgramInfo, nextProgramStartTimeMs,
+ mProgramDataManager.getCurrentProgram(testChannelId)));
+ assertEquals(listener.updatedProgram, mProgramDataManager.getCurrentProgram(testChannelId));
+ }
+
+ /**
+ * Test if program data is refreshed after the program insertion.
+ */
+ public void testContentProviderUpdate() throws Exception {
+ final long testChannelId = 1;
+ startAndWaitForComplete();
+ // Force program data manager to update program data whenever it's changes.
+ mProgramDataManager.setProgramPrefetchUpdateWait(0);
+ mListener.reset();
+ List<Program> programList =
+ mProgramDataManager.getPrograms(testChannelId, mClock.currentTimeMillis());
+ assertNotNull(programList);
+ long lastProgramEndTime = programList.get(programList.size() - 1).getEndTimeUtcMillis();
+ // Make change in content provider
+ mContentProvider.simulateAppend(testChannelId);
+ assertTrue(mListener.programUpdatedLatch.await(WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS));
+ programList = mProgramDataManager.getPrograms(testChannelId, mClock.currentTimeMillis());
+ assertTrue(lastProgramEndTime
+ < programList.get(programList.size() - 1).getEndTimeUtcMillis());
+ }
+
+ /**
+ * Test for {@link ProgramDataManager#setPauseProgramUpdate(boolean)}.
+ */
+ public void testSetPauseProgramUpdate() throws Exception {
+ final long testChannelId = 1;
+ startAndWaitForComplete();
+ // Force program data manager to update program data whenever it's changes.
+ mProgramDataManager.setProgramPrefetchUpdateWait(0);
+ mListener.reset();
+ mProgramDataManager.setPauseProgramUpdate(true);
+ mContentProvider.simulateAppend(testChannelId);
+ assertFalse(mListener.programUpdatedLatch.await(FAILURE_TIME_OUT_MS,
+ TimeUnit.MILLISECONDS));
+ }
+
+ private class FakeContentResolver extends MockContentResolver {
+ @Override
+ public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) {
+ super.notifyChange(uri, observer, syncToNetwork);
+ if (DEBUG) {
+ Log.d(TAG, "onChanged(uri=" + uri + ")");
+ }
+ if (observer != null) {
+ observer.dispatchChange(false, uri);
+ } else {
+ mProgramDataManager.getContentObserver().dispatchChange(false, uri);
+ }
+ }
+ }
+
+ private static class ProgramInfoWrapper {
+ private final int index;
+ private final long startTimeMs;
+ private final ProgramInfo programInfo;
+ public ProgramInfoWrapper(int index, long startTimeMs, ProgramInfo programInfo) {
+ this.index = index;
+ this.startTimeMs = startTimeMs;
+ this.programInfo = programInfo;
+ }
+ }
+
+ // This implements the minimal methods in content resolver
+ // and detailed assumptions are written in each method.
+ private class FakeContentProvider extends MockContentProvider {
+ private SparseArray<List<ProgramInfoWrapper>> mProgramInfoList = new SparseArray<>();
+
+ /**
+ * Constructor for FakeContentProvider
+ * <p>
+ * This initializes program info assuming that
+ * channel IDs are 1, 2, 3, ... {@link Constants#UNIT_TEST_CHANNEL_COUNT}.
+ * </p>
+ */
+ public FakeContentProvider(Context context) {
+ super(context);
+ long startTimeMs = Utils.floorTime(
+ mClock.currentTimeMillis() - ProgramDataManager.PROGRAM_GUIDE_SNAP_TIME_MS,
+ ProgramDataManager.PROGRAM_GUIDE_SNAP_TIME_MS);
+ long endTimeMs = startTimeMs + (ProgramDataManager.PROGRAM_GUIDE_MAX_TIME_RANGE / 2);
+ for (int i = 1; i <= Constants.UNIT_TEST_CHANNEL_COUNT; i++) {
+ List<ProgramInfoWrapper> programInfoList = new ArrayList<>();
+ ProgramInfo stub = ProgramInfo.create();
+ int index = stub.getIndex(startTimeMs, i);
+ long programStartTimeMs = stub.getStartTimeMs(index, i);
+ while (programStartTimeMs < endTimeMs) {
+ ProgramInfo programAt = stub.build(getContext(), index);
+ programInfoList.add(
+ new ProgramInfoWrapper(index, programStartTimeMs, programAt));
+ index++;
+ programStartTimeMs += programAt.durationMs;
+ }
+ mProgramInfoList.put(i, programInfoList);
+ }
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder) {
+ if (DEBUG) {
+ Log.d(TAG, "dump query");
+ Log.d(TAG, " uri=" + uri);
+ if (projection == null || projection.length == 0) {
+ Log.d(TAG, " projection=" + projection);
+ } else {
+ for (int i = 0; i < projection.length; i++) {
+ Log.d(TAG, " projection=" + projection[i]);
+ }
+ }
+ Log.d(TAG," selection=" + selection);
+ }
+ long startTimeMs = Long.parseLong(uri.getQueryParameter(PARAM_START_TIME));
+ long endTimeMs = Long.parseLong(uri.getQueryParameter(PARAM_END_TIME));
+ if (startTimeMs == 0 || endTimeMs == 0) {
+ throw new UnsupportedOperationException();
+ }
+ assertProgramUri(uri);
+ long channelId;
+ try {
+ channelId = Long.parseLong(uri.getQueryParameter(PARAM_CHANNEL));
+ } catch (NumberFormatException e) {
+ channelId = -1;
+ }
+ return new FakeCursor(projection, channelId, startTimeMs, endTimeMs);
+ }
+
+ /**
+ * Simulate program data appends at the end of the existing programs.
+ * This appends programs until the maximum program query range
+ * ({@link ProgramDataManager#PROGRAM_GUIDE_MAX_TIME_RANGE})
+ * where we started with the inserting half of it.
+ */
+ public void simulateAppend(long channelId) {
+ long endTimeMs =
+ mClock.currentTimeMillis() + ProgramDataManager.PROGRAM_GUIDE_MAX_TIME_RANGE;
+ List<ProgramInfoWrapper> programList = mProgramInfoList.get((int) channelId);
+ if (mProgramInfoList == null) {
+ return;
+ }
+ ProgramInfo stub = ProgramInfo.create();
+ ProgramInfoWrapper last = programList.get(programList.size() - 1);
+ while (last.startTimeMs < endTimeMs) {
+ ProgramInfo nextProgramInfo = stub.build(getContext(), last.index + 1);
+ ProgramInfoWrapper next = new ProgramInfoWrapper(last.index + 1,
+ last.startTimeMs + last.programInfo.durationMs, nextProgramInfo);
+ programList.add(next);
+ last = next;
+ }
+ mContentResolver.notifyChange(TvContract.Programs.CONTENT_URI, null);
+ }
+
+ private void assertProgramUri(Uri uri) {
+ assertTrue("Uri(" + uri + ") isn't channel uri",
+ uri.toString().startsWith(TvContract.Programs.CONTENT_URI.toString()));
+ }
+
+ public ProgramInfoWrapper get(long channelId, int position) {
+ List<ProgramInfoWrapper> programList = mProgramInfoList.get((int) channelId);
+ if (programList == null || position >= programList.size()) {
+ return null;
+ }
+ return programList.get(position);
+ }
+ }
+
+ private class FakeCursor extends MockCursor {
+ private String[] ALL_COLUMNS = {
+ TvContract.Programs.COLUMN_CHANNEL_ID,
+ TvContract.Programs.COLUMN_TITLE,
+ TvContract.Programs.COLUMN_SHORT_DESCRIPTION,
+ TvContract.Programs.COLUMN_EPISODE_TITLE,
+ TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS,
+ TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS};
+ private final String[] mColumns;
+ private final boolean mIsQueryForSingleChannel;
+ private final long mStartTimeMs;
+ private final long mEndTimeMs;
+ private final int mCount;
+ private long mChannelId;
+ private int mProgramPosition;
+ private ProgramInfoWrapper mCurrentProgram;
+
+ /**
+ * Constructor
+ * @param columns the same as projection passed from {@link FakeContentProvider#query}.
+ * Can be null for query all.
+ * @param channelId channel ID to query programs belongs to the specified channel.
+ * Can be negative to indicate all channels.
+ * @param startTimeMs start of the time range to query programs.
+ * @param endTimeMs end of the time range to query programs.
+ */
+ public FakeCursor(String[] columns, long channelId, long startTimeMs, long endTimeMs) {
+ mColumns = (columns == null) ? ALL_COLUMNS : columns;
+ mIsQueryForSingleChannel = (channelId > 0);
+ mChannelId = channelId;
+ mProgramPosition = -1;
+ mStartTimeMs = startTimeMs;
+ mEndTimeMs = endTimeMs;
+ int count = 0;
+ while (moveToNext()) {
+ count++;
+ }
+ mCount = count;
+ // Rewind channel Id and program index.
+ mChannelId = channelId;
+ mProgramPosition = -1;
+ if (DEBUG) {
+ Log.d(TAG, "FakeCursor(columns=" + columns + ", channelId=" + channelId
+ + ", startTimeMs=" + startTimeMs + ", endTimeMs=" + endTimeMs
+ + ") has mCount=" + mCount);
+ }
+ }
+
+ @Override
+ public String getColumnName(int columnIndex) {
+ return mColumns[columnIndex];
+ }
+
+ @Override
+ public int getColumnIndex(String columnName) {
+ for (int i = 0; i < mColumns.length; i++) {
+ if (mColumns[i].equalsIgnoreCase(columnName)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public int getInt(int columnIndex) {
+ if (DEBUG) {
+ Log.d(TAG, "Column (" + getColumnName(columnIndex) + ") is ignored in getInt()");
+ }
+ return 0;
+ }
+
+ @Override
+ public long getLong(int columnIndex) {
+ String columnName = getColumnName(columnIndex);
+ switch (columnName) {
+ case TvContract.Programs.COLUMN_CHANNEL_ID:
+ return mChannelId;
+ case TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS:
+ return mCurrentProgram.startTimeMs;
+ case TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS:
+ return mCurrentProgram.startTimeMs + mCurrentProgram.programInfo.durationMs;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Column (" + columnName + ") is ignored in getLong()");
+ }
+ return 0;
+ }
+
+ @Override
+ public String getString(int columnIndex) {
+ String columnName = getColumnName(columnIndex);
+ switch (columnName) {
+ case TvContract.Programs.COLUMN_TITLE:
+ return mCurrentProgram.programInfo.title;
+ case TvContract.Programs.COLUMN_SHORT_DESCRIPTION:
+ return mCurrentProgram.programInfo.description;
+ case TvContract.Programs.COLUMN_EPISODE_TITLE:
+ return mCurrentProgram.programInfo.episode;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Column (" + columnName + ") is ignored in getString()");
+ }
+ return null;
+ }
+
+ @Override
+ public int getCount() {
+ return mCount;
+ }
+
+ @Override
+ public boolean moveToNext() {
+ while (true) {
+ ProgramInfoWrapper program = mContentProvider.get(mChannelId, ++mProgramPosition);
+ if (program == null || program.startTimeMs >= mEndTimeMs) {
+ if (mIsQueryForSingleChannel) {
+ return false;
+ } else {
+ if (++mChannelId > Constants.UNIT_TEST_CHANNEL_COUNT) {
+ return false;
+ }
+ mProgramPosition = -1;
+ }
+ } else if (program.startTimeMs + program.programInfo.durationMs >= mStartTimeMs) {
+ mCurrentProgram = program;
+ break;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public void close() {
+ // No-op.
+ }
+ }
+
+ private class TestProgramDataManagerListener implements ProgramDataManager.Listener {
+ public CountDownLatch programUpdatedLatch = new CountDownLatch(1);
+
+ @Override
+ public void onProgramUpdated() {
+ programUpdatedLatch.countDown();
+ }
+
+ public void reset() {
+ programUpdatedLatch = new CountDownLatch(1);
+ }
+ }
+
+ private class TestProgramDataManagerOnCurrentProgramUpdatedListener implements
+ OnCurrentProgramUpdatedListener {
+ public CountDownLatch currentProgramUpdatedLatch = new CountDownLatch(1);
+ public long updatedChannelId = -1;
+ public Program updatedProgram = null;
+
+ @Override
+ public void onCurrentProgramUpdated(long channelId, Program program) {
+ updatedChannelId = channelId;
+ updatedProgram = program;
+ currentProgramUpdatedLatch.countDown();
+ }
+ }
+}