From 816a4be1a0f34f6a48877c8afd3dbbca19eac435 Mon Sep 17 00:00:00 2001 From: Nick Chalko Date: Mon, 3 Aug 2015 15:39:56 -0700 Subject: Migrate Live Channels App Src to AOSP branch Bug: 21625152 Change-Id: I07e2830b27440556dc757e6340b4f77d1c0cbc66 --- .../android/tv/data/ProgramDataManagerTest.java | 533 +++++++++++++++++++++ 1 file changed, 533 insertions(+) create mode 100644 tests/unit/src/com/android/tv/data/ProgramDataManagerTest.java (limited to 'tests/unit/src/com/android/tv/data/ProgramDataManagerTest.java') 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. + * + *

+ * {@link ProgramDataManager#getCurrentProgram(long)}, + * {@link ProgramDataManager#getPrograms(long, long)}, + * {@link ProgramDataManager#setPrefetchTimeRange(long)}. + *

+ */ + 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 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. + * + *

+ * {@link ProgramDataManager#addOnCurrentProgramUpdatedListener}, + * {@link ProgramDataManager#removeOnCurrentProgramUpdatedListener}. + *

+ */ + 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 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> mProgramInfoList = new SparseArray<>(); + + /** + * Constructor for FakeContentProvider + *

+ * This initializes program info assuming that + * channel IDs are 1, 2, 3, ... {@link Constants#UNIT_TEST_CHANNEL_COUNT}. + *

+ */ + 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 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 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 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(); + } + } +} -- cgit v1.2.3