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 --- .../com/android/tv/BaseMainActivityTestCase.java | 135 +++++ .../android/tv/CurrentPositionMediatorTest.java | 79 +++ .../unit/src/com/android/tv/MainActivityTest.java | 106 ++++ .../src/com/android/tv/TimeShiftManagerTest.java | 100 ++++ .../android/tv/data/ChannelDataManagerTest.java | 646 +++++++++++++++++++++ .../src/com/android/tv/data/ChannelNumberTest.java | 87 +++ .../unit/src/com/android/tv/data/ChannelTest.java | 222 +++++++ .../android/tv/data/ProgramDataManagerTest.java | 533 +++++++++++++++++ .../unit/src/com/android/tv/data/ProgramTest.java | 98 ++++ .../android/tv/menu/TvOptionsRowAdapterTest.java | 109 ++++ .../tv/recommendation/ChannelRecordTest.java | 118 ++++ .../tv/recommendation/EvaluatorTestCase.java | 128 ++++ .../FavoriteChannelEvaluatorTest.java | 144 +++++ .../recommendation/RecentChannelEvaluatorTest.java | 140 +++++ .../tv/recommendation/RecommendationUtils.java | 180 ++++++ .../android/tv/recommendation/RecommenderTest.java | 324 +++++++++++ .../recommendation/RoutineWatchEvaluatorTest.java | 205 +++++++ .../src/com/android/tv/tests/TvActivityTest.java | 34 ++ .../unit/src/com/android/tv/ui/SetupViewTest.java | 86 +++ tests/unit/src/com/android/tv/util/FakeClock.java | 34 ++ .../src/com/android/tv/util/ImageCacheTest.java | 75 +++ .../com/android/tv/util/ScaledBitmapInfoTest.java | 52 ++ tests/unit/src/com/android/tv/util/TestUtils.java | 65 +++ .../android/tv/util/TvInputManagerHelperTest.java | 72 +++ .../com/android/tv/util/TvTrackInfoUtilsTest.java | 98 ++++ .../tv/util/UtilsTest_GetDurationString.java | 250 ++++++++ .../android/tv/util/UtilsTest_IsInGivenDay.java | 61 ++ 27 files changed, 4181 insertions(+) create mode 100644 tests/unit/src/com/android/tv/BaseMainActivityTestCase.java create mode 100644 tests/unit/src/com/android/tv/CurrentPositionMediatorTest.java create mode 100644 tests/unit/src/com/android/tv/MainActivityTest.java create mode 100644 tests/unit/src/com/android/tv/TimeShiftManagerTest.java create mode 100644 tests/unit/src/com/android/tv/data/ChannelDataManagerTest.java create mode 100644 tests/unit/src/com/android/tv/data/ChannelNumberTest.java create mode 100644 tests/unit/src/com/android/tv/data/ChannelTest.java create mode 100644 tests/unit/src/com/android/tv/data/ProgramDataManagerTest.java create mode 100644 tests/unit/src/com/android/tv/data/ProgramTest.java create mode 100644 tests/unit/src/com/android/tv/menu/TvOptionsRowAdapterTest.java create mode 100644 tests/unit/src/com/android/tv/recommendation/ChannelRecordTest.java create mode 100644 tests/unit/src/com/android/tv/recommendation/EvaluatorTestCase.java create mode 100644 tests/unit/src/com/android/tv/recommendation/FavoriteChannelEvaluatorTest.java create mode 100644 tests/unit/src/com/android/tv/recommendation/RecentChannelEvaluatorTest.java create mode 100644 tests/unit/src/com/android/tv/recommendation/RecommendationUtils.java create mode 100644 tests/unit/src/com/android/tv/recommendation/RecommenderTest.java create mode 100644 tests/unit/src/com/android/tv/recommendation/RoutineWatchEvaluatorTest.java create mode 100644 tests/unit/src/com/android/tv/tests/TvActivityTest.java create mode 100644 tests/unit/src/com/android/tv/ui/SetupViewTest.java create mode 100644 tests/unit/src/com/android/tv/util/FakeClock.java create mode 100644 tests/unit/src/com/android/tv/util/ImageCacheTest.java create mode 100644 tests/unit/src/com/android/tv/util/ScaledBitmapInfoTest.java create mode 100644 tests/unit/src/com/android/tv/util/TestUtils.java create mode 100644 tests/unit/src/com/android/tv/util/TvInputManagerHelperTest.java create mode 100644 tests/unit/src/com/android/tv/util/TvTrackInfoUtilsTest.java create mode 100644 tests/unit/src/com/android/tv/util/UtilsTest_GetDurationString.java create mode 100644 tests/unit/src/com/android/tv/util/UtilsTest_IsInGivenDay.java (limited to 'tests/unit/src/com/android') diff --git a/tests/unit/src/com/android/tv/BaseMainActivityTestCase.java b/tests/unit/src/com/android/tv/BaseMainActivityTestCase.java new file mode 100644 index 00000000..99d5ea3e --- /dev/null +++ b/tests/unit/src/com/android/tv/BaseMainActivityTestCase.java @@ -0,0 +1,135 @@ +/* + * 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; + +import android.content.Context; +import android.os.SystemClock; +import android.test.ActivityInstrumentationTestCase2; + +import com.android.tv.data.Channel; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.testing.ChannelInfo; +import com.android.tv.testing.testinput.ChannelStateData; +import com.android.tv.testing.testinput.TestInputControlConnection; +import com.android.tv.testing.testinput.TestInputControlUtils; + +import java.util.List; + +/** + * Base TestCase for tests that need a {@link MainActivity}. + */ +public abstract class BaseMainActivityTestCase + extends ActivityInstrumentationTestCase2 { + private static final String TAG = "BaseMainActivityTest"; + private static final int CHANNEL_LOADING_CHECK_INTERVAL_MS = 10; + + protected final TestInputControlConnection mConnection = new TestInputControlConnection(); + + protected MainActivity mActivity; + + public BaseMainActivityTestCase(Class activityClass) { + super(activityClass); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + // TODO: ensure the SampleInputs are setup. + setActivityInitialTouchMode(false); + mActivity = getActivity(); + getInstrumentation().getContext() + .bindService(TestInputControlUtils.createIntent(), mConnection, + Context.BIND_AUTO_CREATE); + } + + @Override + protected void tearDown() throws Exception { + if (mConnection.isBound()) { + getInstrumentation().getContext().unbindService(mConnection); + } + super.tearDown(); + } + + /** + * Tune to {@code channel}. + * + * @param channel the channel to tune to. + */ + protected void tuneToChannel(final Channel channel) { + // Run on UI thread so views can be modified + try { + runTestOnUiThread(new Runnable() { + @Override + public void run() { + mActivity.tuneToChannel(channel); + } + }); + } catch (Throwable throwable) { + throw new RuntimeException(throwable); + } + } + + /** + * Sleep until @{@link ChannelDataManager#isDbLoadFinished()} is true. + */ + protected void waitUntilChannelLoadingFinish() { + ChannelDataManager channelDataManager = mActivity.getChannelDataManager(); + while (!channelDataManager.isDbLoadFinished()) { + getInstrumentation().waitForIdleSync(); + SystemClock.sleep(CHANNEL_LOADING_CHECK_INTERVAL_MS); + } + } + + /** + * Tune to the channel with {@code name}. + * + * @param name the name of the channel to find. + */ + protected void tuneToChannel(String name) { + Channel c = findChannelWithName(name); + tuneToChannel(c); + } + + /** + * Tune to channel. + */ + protected void tuneToChannel(ChannelInfo channel) { + tuneToChannel(channel.name); + } + + /** + * Update the channel state to {@code data} then tune to that channel. + * + * @param data the state to update the channel with. + * @param channel the channel to tune to + */ + protected void updateThenTune(ChannelStateData data, ChannelInfo channel) { + mConnection.updateChannelState(channel, data); + tuneToChannel(channel); + } + + private Channel findChannelWithName(String displayName) { + waitUntilChannelLoadingFinish(); + List channelList = mActivity.getChannelDataManager().getChannelList(); + for (Channel c : channelList) { + if (c.getDisplayName().equals(displayName)) { + return c; + } + } + throw new AssertionError("'" + displayName + "' channel not found"); + } + +} diff --git a/tests/unit/src/com/android/tv/CurrentPositionMediatorTest.java b/tests/unit/src/com/android/tv/CurrentPositionMediatorTest.java new file mode 100644 index 00000000..6e01f423 --- /dev/null +++ b/tests/unit/src/com/android/tv/CurrentPositionMediatorTest.java @@ -0,0 +1,79 @@ +/* + * 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; + +import static com.android.tv.TimeShiftManager.INVALID_TIME; +import static com.android.tv.TimeShiftManager.REQUEST_TIMEOUT_MS; + +import android.test.UiThreadTest; +import android.test.suitebuilder.annotation.MediumTest; + +@MediumTest +public class CurrentPositionMediatorTest extends BaseMainActivityTestCase { + private TimeShiftManager.CurrentPositionMediator mMediator; + + public CurrentPositionMediatorTest() { + super(MainActivity.class); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + mMediator = mActivity.getTimeShiftManager().mCurrentPositionMediator; + } + + @UiThreadTest + public void testInitialize() throws Throwable { + long currentTimeMs = System.currentTimeMillis(); + mMediator.initialize(currentTimeMs); + assertCurrentPositionMediator(INVALID_TIME, currentTimeMs); + } + + @UiThreadTest + public void testOnSeekRequested() throws Throwable { + long seekToTimeMs = System.currentTimeMillis() - REQUEST_TIMEOUT_MS * 3; + mMediator.onSeekRequested(seekToTimeMs); + assertNotSame("Seek request time", INVALID_TIME, mMediator.mSeekRequestTimeMs); + assertEquals("Current position", seekToTimeMs, mMediator.mCurrentPositionMs); + } + + @UiThreadTest + public void testOnCurrentPositionChangedInvalidInput() throws Throwable { + long seekToTimeMs = System.currentTimeMillis() - REQUEST_TIMEOUT_MS * 3; + long newCurrentTimeMs = seekToTimeMs + REQUEST_TIMEOUT_MS; + mMediator.onSeekRequested(seekToTimeMs); + mMediator.onCurrentPositionChanged(newCurrentTimeMs); + assertNotSame("Seek request time", INVALID_TIME, mMediator.mSeekRequestTimeMs); + assertNotSame("Current position", seekToTimeMs, mMediator.mCurrentPositionMs); + assertNotSame("Current position", newCurrentTimeMs, mMediator.mCurrentPositionMs); + } + + @UiThreadTest + public void testOnCurrentPositionChangedValidInput() throws Throwable { + long seekToTimeMs = System.currentTimeMillis() - REQUEST_TIMEOUT_MS * 3; + long newCurrentTimeMs = seekToTimeMs + REQUEST_TIMEOUT_MS - 1; + mMediator.onSeekRequested(seekToTimeMs); + mMediator.onCurrentPositionChanged(newCurrentTimeMs); + assertCurrentPositionMediator(INVALID_TIME, newCurrentTimeMs); + } + + private void assertCurrentPositionMediator(long expectedSeekRequestTimeMs, + long expectedCurrentPositionMs) { + assertEquals("Seek request time", expectedSeekRequestTimeMs, mMediator.mSeekRequestTimeMs); + assertEquals("Current position", expectedCurrentPositionMs, mMediator.mCurrentPositionMs); + } +} diff --git a/tests/unit/src/com/android/tv/MainActivityTest.java b/tests/unit/src/com/android/tv/MainActivityTest.java new file mode 100644 index 00000000..ae789895 --- /dev/null +++ b/tests/unit/src/com/android/tv/MainActivityTest.java @@ -0,0 +1,106 @@ +/* + * 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; + +import android.test.suitebuilder.annotation.MediumTest; +import android.view.View; +import android.widget.TextView; + +import com.android.tv.data.Channel; +import com.android.tv.testing.testinput.TvTestInputConstants; +import com.android.tv.ui.ChannelBannerView; + +import java.util.List; + +/** + * Tests for {@link MainActivity}. + */ +@MediumTest +public class MainActivityTest extends BaseMainActivityTestCase { + + public MainActivityTest() { + super(MainActivity.class); + } + + public void testInitialConditions() { + waitUntilChannelLoadingFinish(); + List channelList = mActivity.getChannelDataManager().getChannelList(); + assertTrue("Expected at least one channel", channelList.size() > 0); + assertFalse("PIP disabled", mActivity.isPipEnabled()); + } + + public void testTuneToChannel() throws Throwable { + tuneToChannel(TvTestInputConstants.CH_2); + assertChannelBannerShown(true); + assertChannelName(TvTestInputConstants.CH_2.name); + } + + public void testShowProgramGuide() throws Throwable { + tuneToChannel(TvTestInputConstants.CH_2); + showProgramGuide(); + assertChannelBannerShown(false); + assertProgramGuide(true); + } + + private void showProgramGuide() throws Throwable { + // Run on UI thread so views can be modified + runTestOnUiThread(new Runnable() { + @Override + public void run() { + mActivity.getOverlayManager().showProgramGuide(); + } + }); + } + + private void assertChannelName(String displayName) { + TextView channelNameView = (TextView) mActivity.findViewById(R.id.channel_name); + assertEquals("Channel Name", displayName, channelNameView.getText()); + } + + private View assertProgramGuide(boolean isShown) { + return assertViewIsShown("Program Guide", R.id.program_guide, isShown); + } + + private ChannelBannerView assertChannelBannerShown(boolean isShown) { + View v = assertExpectedBannerSceneClassShown(ChannelBannerView.class, isShown); + return (ChannelBannerView) v; + } + + private View assertExpectedBannerSceneClassShown(Class expectedClass, + boolean expectedShown) throws AssertionError { + View v = assertViewIsShown(expectedClass.getSimpleName(), R.id.scene_transition_common, + expectedShown); + if (v != null) { + assertEquals(expectedClass, v.getClass()); + } + return v; + } + + private View assertViewIsShown(String viewName, int viewId, boolean expected) + throws AssertionError { + View view = mActivity.findViewById(viewId); + if (view == null) { + if (expected) { + throw new AssertionError("View " + viewName + " not found"); + } else { + return null; + } + } + assertEquals(viewName + " shown", expected, view.isShown()); + return view; + } + +} diff --git a/tests/unit/src/com/android/tv/TimeShiftManagerTest.java b/tests/unit/src/com/android/tv/TimeShiftManagerTest.java new file mode 100644 index 00000000..878d4293 --- /dev/null +++ b/tests/unit/src/com/android/tv/TimeShiftManagerTest.java @@ -0,0 +1,100 @@ +/* + * 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; + +import static com.android.tv.TimeShiftManager.TIME_SHIFT_ACTION_ID_FAST_FORWARD; +import static com.android.tv.TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT; +import static com.android.tv.TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS; +import static com.android.tv.TimeShiftManager.TIME_SHIFT_ACTION_ID_PAUSE; +import static com.android.tv.TimeShiftManager.TIME_SHIFT_ACTION_ID_PLAY; +import static com.android.tv.TimeShiftManager.TIME_SHIFT_ACTION_ID_REWIND; + +import android.test.suitebuilder.annotation.MediumTest; + +@MediumTest +public class TimeShiftManagerTest extends BaseMainActivityTestCase { + private TimeShiftManager mTimeShiftManager; + + public TimeShiftManagerTest() { + super(MainActivity.class); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + mTimeShiftManager = mActivity.getTimeShiftManager(); + } + + public void testDisableActions() { + enableAllActions(true); + assertActionState(true, true, true, true, true, true); + mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_PLAY, false); + assertActionState(false, true, true, true, true, true); + mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_PAUSE, false); + assertActionState(false, false, true, true, true, true); + mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_REWIND, false); + assertActionState(false, false, false, true, true, true); + mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_FAST_FORWARD, false); + assertActionState(false, false, false, false, true, true); + mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS, false); + assertActionState(false, false, false, false, false, true); + mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT, false); + assertActionState(false, false, false, false, false, false); + } + + public void testEnableActions() { + enableAllActions(false); + assertActionState(false, false, false, false, false, false); + mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_PLAY, true); + assertActionState(true, false, false, false, false, false); + mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_PAUSE, true); + assertActionState(true, true, false, false, false, false); + mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_REWIND, true); + assertActionState(true, true, true, false, false, false); + mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_FAST_FORWARD, true); + assertActionState(true, true, true, true, false, false); + mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS, true); + assertActionState(true, true, true, true, true, false); + mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT, true); + assertActionState(true, true, true, true, true, true); + } + + private void enableAllActions(boolean enabled) { + mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_PLAY, enabled); + mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_PAUSE, enabled); + mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_REWIND, enabled); + mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_FAST_FORWARD, enabled); + mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS, enabled); + mTimeShiftManager.enableAction(TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT, enabled); + } + + private void assertActionState(boolean playEnabled, boolean pauseEnabled, boolean rewindEnabled, + boolean fastForwardEnabled, boolean jumpToPreviousEnabled, boolean jumpToNextEnabled) { + assertEquals("Play Action", playEnabled, + mTimeShiftManager.isActionEnabled(TIME_SHIFT_ACTION_ID_PLAY)); + assertEquals("Pause Action", pauseEnabled, + mTimeShiftManager.isActionEnabled(TIME_SHIFT_ACTION_ID_PAUSE)); + assertEquals("Rewind Action", rewindEnabled, + mTimeShiftManager.isActionEnabled(TIME_SHIFT_ACTION_ID_REWIND)); + assertEquals("Fast Forward Action", fastForwardEnabled, + mTimeShiftManager.isActionEnabled(TIME_SHIFT_ACTION_ID_FAST_FORWARD)); + assertEquals("Jump To Previous Action", jumpToPreviousEnabled, + mTimeShiftManager.isActionEnabled(TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS)); + assertEquals("Jump To Next Action", jumpToNextEnabled, + mTimeShiftManager.isActionEnabled(TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT)); + } +} diff --git a/tests/unit/src/com/android/tv/data/ChannelDataManagerTest.java b/tests/unit/src/com/android/tv/data/ChannelDataManagerTest.java new file mode 100644 index 00000000..38ccdfb6 --- /dev/null +++ b/tests/unit/src/com/android/tv/data/ChannelDataManagerTest.java @@ -0,0 +1,646 @@ +/* + * 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.ContentProvider; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.database.ContentObserver; +import android.database.Cursor; +import android.media.tv.TvContract; +import android.media.tv.TvContract.Channels; +import android.net.Uri; +import android.os.HandlerThread; +import android.test.AndroidTestCase; +import android.test.MoreAsserts; +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.ChannelInfo; +import com.android.tv.testing.Constants; +import com.android.tv.util.TvInputManagerHelper; + +import org.mockito.Matchers; +import org.mockito.Mockito; + +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.ChannelDataManager} + * + * A test method may include tests for multiple methods to minimize the DB access. + */ +@SmallTest +public class ChannelDataManagerTest extends AndroidTestCase { + private static final boolean DEBUG = false; + private static final String TAG = "ChannelDataManagerTest"; + + // Wait time for expected success. + private static final long WAIT_TIME_OUT_MS = 1000L; + private static final String DUMMY_INPUT_ID = "dummy"; + // TODO: Use Channels.COLUMN_BROWSABLE and Channels.COLUMN_LOCKED instead. + private static final String COLUMN_BROWSABLE = "browsable"; + private static final String COLUMN_LOCKED = "locked"; + + private ChannelDataManager mChannelDataManager; + private HandlerThread mHandlerThread; + private TestChannelDataManagerListener mListener; + private FakeContentResolver mContentResolver; + private FakeContentProvider mContentProvider; + + @Override + protected void setUp() throws Exception { + super.setUp(); + assertTrue("More than 2 channels to test", Constants.UNIT_TEST_CHANNEL_COUNT > 2); + TvInputManagerHelper mockHelper = Mockito.mock(TvInputManagerHelper.class); + Mockito.when(mockHelper.hasTvInputInfo(Matchers.anyString())).thenReturn(true); + + mContentProvider = new FakeContentProvider(getContext()); + mContentResolver = new FakeContentResolver(); + mContentResolver.addProvider(TvContract.AUTHORITY, mContentProvider); + mHandlerThread = new HandlerThread(TAG); + mHandlerThread.start(); + mChannelDataManager = new ChannelDataManager( + getContext(), mockHelper, mContentResolver, mHandlerThread.getLooper()); + mListener = new TestChannelDataManagerListener(); + mChannelDataManager.addListener(mListener); + + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + mHandlerThread.quitSafely(); + mChannelDataManager.stop(); + } + + private void startAndWaitForComplete() throws Exception { + mChannelDataManager.start(); + try { + assertTrue(mListener.loadFinishedLatch.await(WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS)); + } catch (InterruptedException e) { + throw e; + } + } + + private void restart() throws Exception { + mChannelDataManager.stop(); + mListener.reset(); + startAndWaitForComplete(); + } + + public void testIsDbLoadFinished() throws Exception { + startAndWaitForComplete(); + assertTrue(mChannelDataManager.isDbLoadFinished()); + } + + /** + * Test for following methods + * - {@link ChannelDataManager#getChannelCount} + * - {@link ChannelDataManager#getChannelList} + * - {@link ChannelDataManager#getChannel} + */ + public void testGetChannels() throws Exception { + startAndWaitForComplete(); + + // Test {@link ChannelDataManager#getChannelCount} + assertEquals(Constants.UNIT_TEST_CHANNEL_COUNT, mChannelDataManager.getChannelCount()); + + // Test {@link ChannelDataManager#getChannelList} + List channelInfoList = new ArrayList<>(); + for (int i = 1; i <= Constants.UNIT_TEST_CHANNEL_COUNT; i++) { + channelInfoList.add(ChannelInfo.create(getContext(), i)); + } + List channelList = mChannelDataManager.getChannelList(); + for (Channel channel : channelList) { + boolean found = false; + for (ChannelInfo channelInfo : channelInfoList) { + if (TextUtils.equals(channelInfo.name, channel.getDisplayName()) + && TextUtils.equals(channelInfo.name, channel.getDisplayName())) { + found = true; + channelInfoList.remove(channelInfo); + break; + } + } + assertTrue("Cannot find (" + channel + ")", found); + } + + // Test {@link ChannelDataManager#getChannelIndex()} + for (Channel channel : channelList) { + assertEquals(channel, mChannelDataManager.getChannel(channel.getId())); + } + } + + /** + * Test for {@link ChannelDataManager#getChannelCount} when no channel is available. + */ + public void testGetChannels_noChannels() throws Exception { + mContentProvider.clear(); + startAndWaitForComplete(); + assertEquals(0, mChannelDataManager.getChannelCount()); + } + + /** + * Test for following methods and channel listener with notifying change. + * - {@link ChannelDataManager#updateBrowsable} + * - {@link ChannelDataManager#applyUpdatedValuesToDb} + */ + public void testBrowsable() throws Exception { + startAndWaitForComplete(); + + // Test if all channels are browable + List channelList = new ArrayList<>(mChannelDataManager.getChannelList()); + List browsableChannelList = mChannelDataManager.getBrowsableChannelList(); + for (Channel browsableChannel : browsableChannelList) { + boolean found = channelList.remove(browsableChannel); + assertTrue("Cannot find (" + browsableChannel + ")", found); + } + assertEquals(0, channelList.size()); + + // Prepare for next tests. + TestChannelDataManagerChannelListener channelListener = + new TestChannelDataManagerChannelListener(); + Channel channel1 = mChannelDataManager.getChannelList().get(0); + mChannelDataManager.addChannelListener(channel1.getId(), channelListener); + + // Test {@link ChannelDataManager#updateBrowsable} & notification. + mChannelDataManager.updateBrowsable(channel1.getId(), false, false); + assertTrue(mListener.channelBrowsableChangedCalled); + assertFalse(mChannelDataManager.getBrowsableChannelList().contains(channel1)); + MoreAsserts.assertContentsInAnyOrder(channelListener.updatedChannels, channel1); + channelListener.reset(); + + // Test {@link ChannelDataManager#applyUpdatedValuesToDb} + mChannelDataManager.applyUpdatedValuesToDb(); + restart(); + browsableChannelList = mChannelDataManager.getBrowsableChannelList(); + assertEquals(Constants.UNIT_TEST_CHANNEL_COUNT - 1, browsableChannelList.size()); + assertFalse(browsableChannelList.contains(channel1)); + } + + /** + * Test for following methods and channel listener without notifying change. + * - {@link ChannelDataManager#updateBrowsable} + * - {@link ChannelDataManager#applyUpdatedValuesToDb} + */ + public void testBrowsable_skipNotification() throws Exception { + startAndWaitForComplete(); + + // Prepare for next tests. + TestChannelDataManagerChannelListener channelListener = + new TestChannelDataManagerChannelListener(); + Channel channel1 = mChannelDataManager.getChannelList().get(0); + Channel channel2 = mChannelDataManager.getChannelList().get(1); + mChannelDataManager.addChannelListener(channel1.getId(), channelListener); + mChannelDataManager.addChannelListener(channel2.getId(), channelListener); + + // Test {@link ChannelDataManager#updateBrowsable} & skip notification. + mChannelDataManager.updateBrowsable(channel1.getId(), false, true); + mChannelDataManager.updateBrowsable(channel2.getId(), false, true); + mChannelDataManager.updateBrowsable(channel1.getId(), true, true); + assertFalse(mListener.channelBrowsableChangedCalled); + List browsableChannelList = mChannelDataManager.getBrowsableChannelList(); + assertTrue(browsableChannelList.contains(channel1)); + assertFalse(browsableChannelList.contains(channel2)); + + // Test {@link ChannelDataManager#applyUpdatedValuesToDb} + mChannelDataManager.applyUpdatedValuesToDb(); + restart(); + browsableChannelList = mChannelDataManager.getBrowsableChannelList(); + assertEquals(Constants.UNIT_TEST_CHANNEL_COUNT - 1, browsableChannelList.size()); + assertFalse(browsableChannelList.contains(channel2)); + } + + /** + * Test for following methods and channel listener. + * - {@link ChannelDataManager#updateLocked} + * - {@link ChannelDataManager#applyUpdatedValuesToDb} + */ + public void testLocked() throws Exception { + startAndWaitForComplete(); + + // Test if all channels aren't locked at the first time. + List channelList = mChannelDataManager.getChannelList(); + for (Channel channel : channelList) { + assertFalse(channel + " is locked", channel.isLocked()); + } + + // Prepare for next tests. + Channel channel = mChannelDataManager.getChannelList().get(0); + + // Test {@link ChannelDataManager#updateLocked} + mChannelDataManager.updateLocked(channel.getId(), true); + assertTrue(mChannelDataManager.getChannel(channel.getId()).isLocked()); + + // Test {@link ChannelDataManager#applyUpdatedValuesToDb}. + mChannelDataManager.applyUpdatedValuesToDb(); + restart(); + assertTrue(mChannelDataManager.getChannel(channel.getId()).isLocked()); + + // Cleanup + mChannelDataManager.updateLocked(channel.getId(), false); + } + + /** + * Test ChannelDataManager when channels in TvContract are updated, removed, or added. + */ + public void testChannelListChanged() throws Exception { + startAndWaitForComplete(); + + // Test channel add. + mListener.reset(); + long testChannelId = Constants.UNIT_TEST_CHANNEL_COUNT + 1; + ChannelInfo testChannelInfo = ChannelInfo.create(getContext(), (int) testChannelId); + testChannelId = Constants.UNIT_TEST_CHANNEL_COUNT + 1; + mContentProvider.simulateInsert(testChannelInfo); + assertTrue(mListener.channeListUpdatedLatch.await(WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS)); + assertEquals(Constants.UNIT_TEST_CHANNEL_COUNT + 1, mChannelDataManager.getChannelCount()); + + // Test channel update + mListener.reset(); + TestChannelDataManagerChannelListener channelListener = + new TestChannelDataManagerChannelListener(); + mChannelDataManager.addChannelListener(testChannelId, channelListener); + String newName = testChannelInfo.name + "_test"; + mContentProvider.simulateUpdate(testChannelId, newName); + assertTrue(mListener.channeListUpdatedLatch.await(WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS)); + assertTrue(channelListener.channelChangedLatch.await( + WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS)); + assertEquals(0, channelListener.removedChannels.size()); + assertEquals(1, channelListener.updatedChannels.size()); + Channel updatedChannel = channelListener.updatedChannels.get(0); + assertEquals(testChannelId, updatedChannel.getId()); + assertEquals(testChannelInfo.number, updatedChannel.getDisplayNumber()); + assertEquals(newName, updatedChannel.getDisplayName()); + assertEquals(Constants.UNIT_TEST_CHANNEL_COUNT + 1, + mChannelDataManager.getChannelCount()); + + // Test channel remove. + mListener.reset(); + channelListener.reset(); + mContentProvider.simulateDelete(testChannelId); + assertTrue(mListener.channeListUpdatedLatch.await(WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS)); + assertTrue(channelListener.channelChangedLatch.await( + WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS)); + assertEquals(1, channelListener.removedChannels.size()); + assertEquals(0, channelListener.updatedChannels.size()); + Channel removedChannel = channelListener.removedChannels.get(0); + assertEquals(newName, removedChannel.getDisplayName()); + assertEquals(testChannelInfo.number, removedChannel.getDisplayNumber()); + assertEquals(Constants.UNIT_TEST_CHANNEL_COUNT, mChannelDataManager.getChannelCount()); + } + + private class ChannelInfoWrapper { + public ChannelInfo channelInfo; + public boolean browsable; + public boolean locked; + public ChannelInfoWrapper(ChannelInfo channelInfo) { + this.channelInfo = channelInfo; + browsable = true; + locked = false; + } + } + + 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 + ", observer=" + observer + ")"); + } + // Do not call {@link ContentObserver#onChange} directly + // to run it on the {@link #mHandlerThread}. + if (observer != null) { + observer.dispatchChange(false, uri); + } else { + mChannelDataManager.getContentObserver().dispatchChange(false, uri); + } + } + } + + // This implements the minimal methods in content resolver + // and detailed assumptions are written in each method. + private class FakeContentProvider extends MockContentProvider { + private SparseArray mChannelInfoList = new SparseArray<>(); + + public FakeContentProvider(Context context) { + super(context); + for (int i = 1; i <= Constants.UNIT_TEST_CHANNEL_COUNT; i++) { + mChannelInfoList.put(i, + new ChannelInfoWrapper(ChannelInfo.create(getContext(), i))); + } + } + + /** + * Implementation of {@link ContentProvider#query}. + * This assumes that {@link ChannelDataManager} queries channels + * with empty {@code selection}. (i.e. channels are always queries for all) + */ + @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); + } + assertChannelUri(uri); + return new FakeCursor(projection); + } + + /** + * Implementation of {@link ContentProvider#update}. + * This assumes that {@link ChannelDataManager} update channels + * only for changing browsable and locked. + */ + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + if (DEBUG) Log.d(TAG, "update(uri=" + uri + ", selection=" + selection); + assertChannelUri(uri); + List channelIds = new ArrayList<>(); + try { + long channelId = ContentUris.parseId(uri); + channelIds.add(channelId); + } catch (NumberFormatException e) { + // Update for multiple channels. + if (TextUtils.isEmpty(selection)) { + for (int i = 0; i < mChannelInfoList.size(); i++) { + channelIds.add((long) mChannelInfoList.keyAt(i)); + } + } else { + // See {@link Utils#buildSelectionForIds} for the syntax. + String selectionForId = selection.substring( + selection.indexOf("(") + 1, selection.lastIndexOf(")")); + String[] ids = selectionForId.split(", "); + if (ids != null) { + for (String id : ids) { + channelIds.add(Long.parseLong(id)); + } + } + } + } + int updateCount = 0; + for (long channelId : channelIds) { + boolean updated = false; + ChannelInfoWrapper channel = mChannelInfoList.get((int) channelId); + if (channel == null) { + return 0; + } + if (values.containsKey(COLUMN_BROWSABLE)) { + updated = true; + channel.browsable = (values.getAsInteger(COLUMN_BROWSABLE) == 1); + } + if (values.containsKey(COLUMN_LOCKED)) { + updated = true; + channel.locked = (values.getAsInteger(COLUMN_LOCKED) == 1); + } + updateCount += updated ? 1 : 0; + } + if (updateCount > 0) { + if (channelIds.size() == 1) { + mContentResolver.notifyChange(uri, null); + } else { + mContentResolver.notifyChange(Channels.CONTENT_URI, null); + } + } else { + if (DEBUG) { + Log.d(TAG, "Update to channel(uri=" + uri + ") is ignored for " + values); + } + } + return updateCount; + } + + /** + * Simulates channel data insert. + * This assigns original network ID (the same with channel number) to channel ID. + */ + public void simulateInsert(ChannelInfo testChannelInfo) { + long channelId = testChannelInfo.originalNetworkId; + mChannelInfoList.put((int) channelId, + new ChannelInfoWrapper(ChannelInfo.create(getContext(), (int) channelId))); + mContentResolver.notifyChange(TvContract.buildChannelUri(channelId), null); + } + + /** + * Simulates channel data delete. + */ + public void simulateDelete(long channelId) { + mChannelInfoList.remove((int) channelId); + mContentResolver.notifyChange(TvContract.buildChannelUri(channelId), null); + } + + /** + * Simulates channel data update. + */ + public void simulateUpdate(long channelId, String newName) { + ChannelInfoWrapper channel = mChannelInfoList.get((int) channelId); + ChannelInfo.Builder builder = new ChannelInfo.Builder(channel.channelInfo); + builder.setName(newName); + channel.channelInfo = builder.build(); + mContentResolver.notifyChange(TvContract.buildChannelUri(channelId), null); + } + + private void assertChannelUri(Uri uri) { + assertTrue("Uri(" + uri + ") isn't channel uri", + uri.toString().startsWith(Channels.CONTENT_URI.toString())); + } + + public void clear() { + mChannelInfoList.clear(); + } + + public ChannelInfoWrapper get(int position) { + return mChannelInfoList.get(mChannelInfoList.keyAt(position)); + } + + public int getCount() { + return mChannelInfoList.size(); + } + + public long keyAt(int position) { + return mChannelInfoList.keyAt(position); + } + } + + private class FakeCursor extends MockCursor { + private String[] ALL_COLUMNS = { + Channels._ID, + Channels.COLUMN_DISPLAY_NAME, + Channels.COLUMN_DISPLAY_NUMBER, + Channels.COLUMN_INPUT_ID, + Channels.COLUMN_VIDEO_FORMAT, + Channels.COLUMN_ORIGINAL_NETWORK_ID, + COLUMN_BROWSABLE, + COLUMN_LOCKED}; + private String[] mColumns; + private int mPosition; + + public FakeCursor(String[] columns) { + mColumns = (columns == null) ? ALL_COLUMNS : columns; + mPosition = -1; + } + + @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 long getLong(int columnIndex) { + String columnName = getColumnName(columnIndex); + switch (columnName) { + case Channels._ID: + return mContentProvider.keyAt(mPosition); + } + if (DEBUG) { + Log.d(TAG, "Column (" + columnName + ") is ignored in getLong()"); + } + return 0; + } + + @Override + public String getString(int columnIndex) { + String columnName = getColumnName(columnIndex); + ChannelInfoWrapper channel = mContentProvider.get(mPosition); + switch (columnName) { + case Channels.COLUMN_DISPLAY_NAME: + return channel.channelInfo.name; + case Channels.COLUMN_DISPLAY_NUMBER: + return channel.channelInfo.number; + case Channels.COLUMN_INPUT_ID: + return DUMMY_INPUT_ID; + case Channels.COLUMN_VIDEO_FORMAT: + return channel.channelInfo.getVideoFormat(); + } + if (DEBUG) { + Log.d(TAG, "Column (" + columnName + ") is ignored in getString()"); + } + return null; + } + + @Override + public int getInt(int columnIndex) { + String columnName = getColumnName(columnIndex); + ChannelInfoWrapper channel = mContentProvider.get(mPosition); + switch (columnName) { + case Channels.COLUMN_ORIGINAL_NETWORK_ID: + return channel.channelInfo.originalNetworkId; + case COLUMN_BROWSABLE: + return channel.browsable ? 1 : 0; + case COLUMN_LOCKED: + return channel.locked ? 1 : 0; + } + if (DEBUG) { + Log.d(TAG, "Column (" + columnName + ") is ignored in getInt()"); + } + return 0; + } + + @Override + public int getCount() { + return mContentProvider.getCount(); + } + + @Override + public boolean moveToNext() { + return ++mPosition < mContentProvider.getCount(); + } + + @Override + public void close() { + // No-op. + } + } + + private class TestChannelDataManagerListener implements ChannelDataManager.Listener { + public CountDownLatch loadFinishedLatch = new CountDownLatch(1); + public CountDownLatch channeListUpdatedLatch = new CountDownLatch(1); + public boolean channelBrowsableChangedCalled; + + @Override + public void onLoadFinished() { + loadFinishedLatch.countDown(); + } + + @Override + public void onChannelListUpdated() { + channeListUpdatedLatch.countDown(); + } + + @Override + public void onChannelBrowsableChanged() { + channelBrowsableChangedCalled = true; + } + + public void reset() { + loadFinishedLatch = new CountDownLatch(1); + channeListUpdatedLatch = new CountDownLatch(1); + channelBrowsableChangedCalled = false; + } + } + + private class TestChannelDataManagerChannelListener + implements ChannelDataManager.ChannelListener { + public CountDownLatch channelChangedLatch = new CountDownLatch(1); + public List removedChannels = new ArrayList<>(); + public List updatedChannels = new ArrayList<>(); + + @Override + public void onChannelRemoved(Channel channel) { + removedChannels.add(channel); + channelChangedLatch.countDown(); + } + + @Override + public void onChannelUpdated(Channel channel) { + updatedChannels.add(channel); + channelChangedLatch.countDown(); + } + + public void reset() { + channelChangedLatch = new CountDownLatch(1); + removedChannels.clear(); + updatedChannels.clear(); + } + } +} diff --git a/tests/unit/src/com/android/tv/data/ChannelNumberTest.java b/tests/unit/src/com/android/tv/data/ChannelNumberTest.java new file mode 100644 index 00000000..9914f75e --- /dev/null +++ b/tests/unit/src/com/android/tv/data/ChannelNumberTest.java @@ -0,0 +1,87 @@ +/* + * 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 static com.android.tv.data.ChannelNumber.parseChannelNumber; + +import android.test.suitebuilder.annotation.SmallTest; + +import com.android.tv.testing.ComparableTester; + +import junit.framework.TestCase; + +/** + * Tests for {@link ChannelNumber}. + */ +@SmallTest +public class ChannelNumberTest extends TestCase { + + /** + * Test method for {@link ChannelNumber#ChannelNumber()}. + */ + public void testChannelNumber() { + assertChannelEquals(new ChannelNumber(), "", false, ""); + } + + /** + * Test method for + * {@link com.android.tv.data.ChannelNumber#parseChannelNumber(java.lang.String)}. + */ + public void testParseChannelNumber() { + assertNull(parseChannelNumber("")); + assertNull(parseChannelNumber(" ")); + assertChannelEquals(parseChannelNumber("1"), "1", false, ""); + assertChannelEquals(parseChannelNumber("1234 4321"), "1234", true, "4321"); + assertChannelEquals(parseChannelNumber("3-4"), "3", true, "4"); + assertChannelEquals(parseChannelNumber("5.6"), "5", true, "6"); + } + + /** + * Test method for {@link ChannelNumber#compareTo(com.android.tv.data.ChannelNumber)}. + */ + public void testCompareTo() { + new ComparableTester() + .addEquivelentGroup(parseChannelNumber("1"), parseChannelNumber("1")) + .addEquivelentGroup(parseChannelNumber("2")) + .addEquivelentGroup(parseChannelNumber("2 1"), parseChannelNumber("2.1"), + parseChannelNumber("2-1")) + .addEquivelentGroup(parseChannelNumber("2-2")) + .addEquivelentGroup(parseChannelNumber("2-10")) + .addEquivelentGroup(parseChannelNumber("3")) + .addEquivelentGroup(parseChannelNumber("10")) + .addEquivelentGroup(parseChannelNumber("100")) + .test(); + } + + /** + * Test method for {@link ChannelNumber#compare(java.lang.String, java.lang.String)}. + */ + public void testCompare() { + // Only need to test nulls, the reset is tested by testComparteTo + assertEquals("compareTo(null,null)", 0, ChannelNumber.compare(null, null)); + assertEquals("compareTo(1,1)", 0, ChannelNumber.compare("1", "1")); + assertEquals("compareTo(null,1)<0", true, ChannelNumber.compare(null, "1") < 0); + assertEquals("compareTo(1,null)>0", true, ChannelNumber.compare("1", null) > 0); + } + + private void assertChannelEquals(ChannelNumber actual, String expectedMajor, + boolean expectedHasDelimiter, String expectedMinor) { + assertEquals(actual + " major", actual.majorNumber, expectedMajor); + assertEquals(actual + " hasDelimiter", actual.hasDelimiter, expectedHasDelimiter); + assertEquals(actual + " minor", actual.minorNumber, expectedMinor); + } + +} diff --git a/tests/unit/src/com/android/tv/data/ChannelTest.java b/tests/unit/src/com/android/tv/data/ChannelTest.java new file mode 100644 index 00000000..dc41fda0 --- /dev/null +++ b/tests/unit/src/com/android/tv/data/ChannelTest.java @@ -0,0 +1,222 @@ +/* + * 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.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.test.AndroidTestCase; + +import com.android.tv.testing.ComparatorTester; +import com.android.tv.util.TvInputManagerHelper; + +import org.mockito.Matchers; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.util.Comparator; + +/** + * Tests for {@link Channel}. + */ +public class ChannelTest extends AndroidTestCase { + // Used for testing TV inputs with invalid input package. This could happen when a TV input is + // uninstalled while drawing an app link card. + private static final String INVALID_TV_INPUT_PACKAGE_NAME = + "com.android.tv.invalid_tv_input"; + // Used for testing TV inputs defined inside of Live Channels. + private static final String LIVE_CHANNELS_PACKAGE_NAME = "com.android.tv"; + // Used for testing a TV input which doesn't have its leanback launcher activity. + private static final String NONE_LEANBACK_TV_INPUT_PACKAGE_NAME = + "com.android.tv.none_leanback_tv_input"; + // Used for testing a TV input which has its leanback launcher activity. + private static final String LEANBACK_TV_INPUT_PACKAGE_NAME = + "com.android.tv.leanback_tv_input"; + private static final String TEST_APP_LINK_TEXT = "test_app_link_text"; + private static final ActivityInfo TEST_ACTIVITY_INFO = new ActivityInfo(); + + private Context mMockContext; + private Intent mInvalidIntent; + private Intent mValidIntent; + private Intent mLiveChannelsIntent; + private Intent mLeanbackTvInputIntent; + + public void setUp() throws Exception { + super.setUp(); + mInvalidIntent = new Intent(Intent.ACTION_VIEW); + mInvalidIntent.setComponent(new ComponentName(INVALID_TV_INPUT_PACKAGE_NAME, ".test")); + mValidIntent = new Intent(Intent.ACTION_VIEW); + mValidIntent.setComponent(new ComponentName(LEANBACK_TV_INPUT_PACKAGE_NAME, ".test")); + mLiveChannelsIntent = new Intent(Intent.ACTION_VIEW); + mLiveChannelsIntent.setComponent( + new ComponentName(LIVE_CHANNELS_PACKAGE_NAME, ".MainActivity")); + mLeanbackTvInputIntent = new Intent(Intent.ACTION_VIEW); + mLeanbackTvInputIntent.setComponent( + new ComponentName(LEANBACK_TV_INPUT_PACKAGE_NAME, ".test")); + + PackageManager mockPackageManager = Mockito.mock(PackageManager.class); + Mockito.when(mockPackageManager.getLeanbackLaunchIntentForPackage( + INVALID_TV_INPUT_PACKAGE_NAME)).thenReturn(null); + Mockito.when(mockPackageManager.getLeanbackLaunchIntentForPackage( + LIVE_CHANNELS_PACKAGE_NAME)).thenReturn(mLiveChannelsIntent); + Mockito.when(mockPackageManager.getLeanbackLaunchIntentForPackage( + NONE_LEANBACK_TV_INPUT_PACKAGE_NAME)).thenReturn(null); + Mockito.when(mockPackageManager.getLeanbackLaunchIntentForPackage( + LEANBACK_TV_INPUT_PACKAGE_NAME)).thenReturn(mLeanbackTvInputIntent); + + // Channel.getAppLinkIntent() calls initAppLinkTypeAndIntent() which calls + // Intent.resolveActivityInfo() which calls PackageManager.getActivityInfo(). + Mockito.doAnswer(new Answer() { + public ActivityInfo answer(InvocationOnMock invocation) { + // We only check the package name, since the class name can be changed + // when an intent is changed to an uri and created from the uri. + // (ex, ".className" -> "packageName.className") + return mValidIntent.getComponent().getPackageName().equals( + ((ComponentName)invocation.getArguments()[0]).getPackageName()) + ? TEST_ACTIVITY_INFO : null; + } + }).when(mockPackageManager).getActivityInfo(Mockito.any(), Mockito.anyInt()); + + mMockContext = Mockito.mock(Context.class); + Mockito.when(mMockContext.getApplicationContext()).thenReturn(mMockContext); + Mockito.when(mMockContext.getPackageName()).thenReturn(LIVE_CHANNELS_PACKAGE_NAME); + Mockito.when(mMockContext.getPackageManager()).thenReturn(mockPackageManager); + } + + public void testGetAppLinkType_NoText_NoIntent() { + assertAppLinkType(Channel.APP_LINK_TYPE_NONE, INVALID_TV_INPUT_PACKAGE_NAME, null, null); + assertAppLinkType(Channel.APP_LINK_TYPE_NONE, LIVE_CHANNELS_PACKAGE_NAME, null, null); + assertAppLinkType(Channel.APP_LINK_TYPE_NONE, NONE_LEANBACK_TV_INPUT_PACKAGE_NAME, null, + null); + assertAppLinkType(Channel.APP_LINK_TYPE_APP, LEANBACK_TV_INPUT_PACKAGE_NAME, null, null); + } + + public void testGetAppLinkType_NoText_InvalidIntent() { + assertAppLinkType(Channel.APP_LINK_TYPE_NONE, INVALID_TV_INPUT_PACKAGE_NAME, null, + mInvalidIntent); + assertAppLinkType(Channel.APP_LINK_TYPE_NONE, LIVE_CHANNELS_PACKAGE_NAME, null, + mInvalidIntent); + assertAppLinkType(Channel.APP_LINK_TYPE_NONE, NONE_LEANBACK_TV_INPUT_PACKAGE_NAME, null, + mInvalidIntent); + assertAppLinkType(Channel.APP_LINK_TYPE_APP, LEANBACK_TV_INPUT_PACKAGE_NAME, null, + mInvalidIntent); + } + + public void testGetAppLinkType_NoText_ValidIntent() { + assertAppLinkType(Channel.APP_LINK_TYPE_NONE, INVALID_TV_INPUT_PACKAGE_NAME, null, + mValidIntent); + assertAppLinkType(Channel.APP_LINK_TYPE_NONE, LIVE_CHANNELS_PACKAGE_NAME, null, + mValidIntent); + assertAppLinkType(Channel.APP_LINK_TYPE_NONE, NONE_LEANBACK_TV_INPUT_PACKAGE_NAME, null, + mValidIntent); + assertAppLinkType(Channel.APP_LINK_TYPE_APP, LEANBACK_TV_INPUT_PACKAGE_NAME, null, + mValidIntent); + } + + public void testGetAppLinkType_HasText_NoIntent() { + assertAppLinkType(Channel.APP_LINK_TYPE_NONE, INVALID_TV_INPUT_PACKAGE_NAME, + TEST_APP_LINK_TEXT, null); + assertAppLinkType(Channel.APP_LINK_TYPE_NONE, LIVE_CHANNELS_PACKAGE_NAME, + TEST_APP_LINK_TEXT, null); + assertAppLinkType(Channel.APP_LINK_TYPE_NONE, NONE_LEANBACK_TV_INPUT_PACKAGE_NAME, + TEST_APP_LINK_TEXT, null); + assertAppLinkType(Channel.APP_LINK_TYPE_APP, LEANBACK_TV_INPUT_PACKAGE_NAME, + TEST_APP_LINK_TEXT, null); + } + + public void testGetAppLinkType_HasText_InvalidIntent() { + assertAppLinkType(Channel.APP_LINK_TYPE_NONE, INVALID_TV_INPUT_PACKAGE_NAME, + TEST_APP_LINK_TEXT, mInvalidIntent); + assertAppLinkType(Channel.APP_LINK_TYPE_NONE, LIVE_CHANNELS_PACKAGE_NAME, + TEST_APP_LINK_TEXT, mInvalidIntent); + assertAppLinkType(Channel.APP_LINK_TYPE_NONE, NONE_LEANBACK_TV_INPUT_PACKAGE_NAME, + TEST_APP_LINK_TEXT, mInvalidIntent); + assertAppLinkType(Channel.APP_LINK_TYPE_APP, LEANBACK_TV_INPUT_PACKAGE_NAME, + TEST_APP_LINK_TEXT, mInvalidIntent); + } + + public void testGetAppLinkType_HasText_ValidIntent() { + assertAppLinkType(Channel.APP_LINK_TYPE_CHANNEL, INVALID_TV_INPUT_PACKAGE_NAME, + TEST_APP_LINK_TEXT, mValidIntent); + assertAppLinkType(Channel.APP_LINK_TYPE_CHANNEL, LIVE_CHANNELS_PACKAGE_NAME, + TEST_APP_LINK_TEXT, mValidIntent); + assertAppLinkType(Channel.APP_LINK_TYPE_CHANNEL, NONE_LEANBACK_TV_INPUT_PACKAGE_NAME, + TEST_APP_LINK_TEXT, mValidIntent); + assertAppLinkType(Channel.APP_LINK_TYPE_CHANNEL, LEANBACK_TV_INPUT_PACKAGE_NAME, + TEST_APP_LINK_TEXT, mValidIntent); + } + + private void assertAppLinkType(int expectedType, String inputPackageName, String appLinkText, + Intent appLinkIntent) { + Channel testChannel = new Channel.Builder() + .setPackageName(inputPackageName) + .setAppLinkText(appLinkText) + .setAppLinkIntentUri(appLinkIntent == null ? null : appLinkIntent.toUri(0)) + .build(); + assertEquals("Unexpected app-link type for for " + testChannel, + expectedType, testChannel.getAppLinkType(mMockContext)); + } + + public void testComparator() { + final String PARTNER_INPUT_ID = "partner"; + + TvInputManagerHelper manager = Mockito.mock(TvInputManagerHelper.class); + Mockito.when(manager.isPartnerInput(Matchers.anyString())).thenAnswer( + new Answer() { + @Override + public Boolean answer(InvocationOnMock invocation) throws Throwable { + String inputId = (String) invocation.getArguments()[0]; + return PARTNER_INPUT_ID.equals(inputId); + } + }); + Comparator comparator = new TestChannelComparator(manager); + ComparatorTester comparatorTester = + ComparatorTester.withoutEqualsTest(comparator); + comparatorTester.addComparableGroup( + new Channel.Builder().setInputId(PARTNER_INPUT_ID).setDisplayNumber("100").build()); + comparatorTester.addComparableGroup( + new Channel.Builder().setInputId("1").setDisplayNumber("2").build()); + comparatorTester.addComparableGroup( + new Channel.Builder().setInputId("2").setDisplayNumber("1.0").build()); + comparatorTester.addComparableGroup( + new Channel.Builder().setInputId("2").setDisplayNumber("1.62") + .setDisplayName("test1").build(), + new Channel.Builder().setInputId("2").setDisplayNumber("1.62") + .setDisplayName("test2").build(), + new Channel.Builder().setInputId("2").setDisplayNumber("1.62") + .setDisplayName("test3").build()); + comparatorTester.addComparableGroup( + new Channel.Builder().setInputId("2").setDisplayNumber("2.0").build()); + comparatorTester.addComparableGroup( + new Channel.Builder().setInputId("2").setDisplayNumber("12.2").build()); + comparatorTester.test(); + } + + private class TestChannelComparator extends Channel.DefaultComparator { + public TestChannelComparator(TvInputManagerHelper manager) { + super(null, manager); + } + + @Override + public String getInputLabelForChannel(Channel channel) { + return channel.getInputId(); + } + }; +} 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(); + } + } +} diff --git a/tests/unit/src/com/android/tv/data/ProgramTest.java b/tests/unit/src/com/android/tv/data/ProgramTest.java new file mode 100644 index 00000000..b4d78733 --- /dev/null +++ b/tests/unit/src/com/android/tv/data/ProgramTest.java @@ -0,0 +1,98 @@ +/* + * 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 static android.media.tv.TvContract.Programs.Genres.COMEDY; +import static android.media.tv.TvContract.Programs.Genres.FAMILY_KIDS; + +import junit.framework.TestCase; + +import java.util.Arrays; + +/** + * Tests for {@link Program}. + */ +public class ProgramTest extends TestCase { + + private static final int NOT_FOUND_GENRE = 987; + + private static final int FAMILY_GENRE_ID = GenreItems.getId(FAMILY_KIDS); + + private static final int COMEDY_GENRE_ID = GenreItems.getId(COMEDY); + + public void testBuild() { + Program program = new Program.Builder().build(); + assertEquals("isValid", false, program.isValid()); + } + + public void testNoGenres() { + Program program = new Program.Builder() + .setCanonicalGenres("") + .build(); + assertNullCanonicalGenres(program); + assertHasGenre(program, NOT_FOUND_GENRE, false); + assertHasGenre(program, FAMILY_GENRE_ID, false); + assertHasGenre(program, COMEDY_GENRE_ID, false); + assertHasGenre(program, GenreItems.ID_ALL_CHANNELS, true); + } + + public void testFamilyGenre() { + Program program = new Program.Builder() + .setCanonicalGenres(FAMILY_KIDS) + .build(); + assertCanonicalGenres(program, FAMILY_KIDS); + assertHasGenre(program, NOT_FOUND_GENRE, false); + assertHasGenre(program, FAMILY_GENRE_ID, true); + assertHasGenre(program, COMEDY_GENRE_ID, false); + assertHasGenre(program, GenreItems.ID_ALL_CHANNELS, true); + } + + public void testFamilyComedyGenre() { + Program program = new Program.Builder() + .setCanonicalGenres(FAMILY_KIDS + ", " + COMEDY) + .build(); + assertCanonicalGenres(program, FAMILY_KIDS, COMEDY); + assertHasGenre(program, NOT_FOUND_GENRE, false); + assertHasGenre(program, FAMILY_GENRE_ID, true); + assertHasGenre(program, COMEDY_GENRE_ID, true); + assertHasGenre(program, GenreItems.ID_ALL_CHANNELS, true); + } + + public void testOtherGenre() { + Program program = new Program.Builder() + .setCanonicalGenres("other") + .build(); + assertCanonicalGenres(program); + assertHasGenre(program, NOT_FOUND_GENRE, false); + assertHasGenre(program, FAMILY_GENRE_ID, false); + assertHasGenre(program, COMEDY_GENRE_ID, false); + assertHasGenre(program, GenreItems.ID_ALL_CHANNELS, true); + } + + private static void assertNullCanonicalGenres(Program program) { + String[] actual = program.getCanonicalGenres(); + assertNull("Expected null canonical genres but was " + Arrays.toString(actual), actual); + } + + private static void assertCanonicalGenres(Program program, String... expected) { + assertEquals("canonical genres", Arrays.asList(expected), + Arrays.asList(program.getCanonicalGenres())); + } + + private static void assertHasGenre(Program program, int genreId, boolean expected) { + assertEquals("hasGenre(" + genreId + ")", expected, program.hasGenre(genreId)); + } +} diff --git a/tests/unit/src/com/android/tv/menu/TvOptionsRowAdapterTest.java b/tests/unit/src/com/android/tv/menu/TvOptionsRowAdapterTest.java new file mode 100644 index 00000000..4ffd9fa9 --- /dev/null +++ b/tests/unit/src/com/android/tv/menu/TvOptionsRowAdapterTest.java @@ -0,0 +1,109 @@ +/* + * 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.menu; + +import android.media.tv.TvTrackInfo; +import android.os.SystemClock; +import android.test.suitebuilder.annotation.SmallTest; + +import com.android.tv.BaseMainActivityTestCase; +import com.android.tv.MainActivity; +import com.android.tv.customization.CustomAction; +import com.android.tv.testing.Constants; +import com.android.tv.testing.testinput.ChannelStateData; +import com.android.tv.testing.testinput.TvTestInputConstants; + +import java.util.Collections; +import java.util.List; + +/** + * Tests for {@link TvOptionsRowAdapter}. + */ +@SmallTest +public class TvOptionsRowAdapterTest extends BaseMainActivityTestCase { + private static final int WAIT_TRACK_SIZE_TIMEOUT_MS = 300; + public static final int TRACK_SIZE_CHECK_INTERVAL_MS = 10; + + + // TODO: Refactor TvOptionsRowAdapter so it does not rely on MainActivity + private TvOptionsRowAdapter mTvOptionsRowAdapter; + + public TvOptionsRowAdapterTest() { + super(MainActivity.class); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + mTvOptionsRowAdapter = new TvOptionsRowAdapter(mActivity, + Collections.emptyList()); + tuneToChannel(TvTestInputConstants.CH_1); + } + + public void testUpdateAudioAction_2tracks() { + mTvOptionsRowAdapter.update(); + ChannelStateData data = new ChannelStateData(); + data.mTvTrackInfos.add(Constants.GENERIC_AUDIO_TRACK); + updateThenTune(data, TvTestInputConstants.CH_2); + waitUntilTracksHaveSize(2); + + boolean result = mTvOptionsRowAdapter.updateActions(); + assertEquals("update Action had change", true, result); + assertEquals("Multi Audio enabled", true, + MenuAction.SELECT_AUDIO_LANGUAGE_ACTION.isEnabled()); + } + + public void testUpdateAudioAction_1track() { + mTvOptionsRowAdapter.update(); + ChannelStateData data = new ChannelStateData(); + data.mTvTrackInfos.clear(); + data.mTvTrackInfos.add(Constants.GENERIC_AUDIO_TRACK); + updateThenTune(data, TvTestInputConstants.CH_2); + waitUntilTracksHaveSize(1); + + boolean result = mTvOptionsRowAdapter.updateActions(); + assertEquals("update Action had change", true, result); + assertEquals("Multi Audio enabled", false, + MenuAction.SELECT_AUDIO_LANGUAGE_ACTION.isEnabled()); + } + + public void testUpdateAudioAction_noTracks() { + mTvOptionsRowAdapter.update(); + ChannelStateData data = new ChannelStateData(); + data.mTvTrackInfos.clear(); + updateThenTune(data, TvTestInputConstants.CH_2); + waitUntilTracksHaveSize(0); + + boolean result = mTvOptionsRowAdapter.updateActions(); + assertEquals("update Action had change", false, result); + assertEquals("Multi Audio enabled", false, + MenuAction.SELECT_AUDIO_LANGUAGE_ACTION.isEnabled()); + } + + private void waitUntilTracksHaveSize(int expected) { + long start = SystemClock.elapsedRealtime(); + while (SystemClock.elapsedRealtime() < start + WAIT_TRACK_SIZE_TIMEOUT_MS) { + getInstrumentation().waitForIdleSync(); + List tracks = mActivity.getTracks(TvTrackInfo.TYPE_AUDIO); + if (tracks != null && tracks.size() == expected) { + return; + } + SystemClock.sleep(TRACK_SIZE_CHECK_INTERVAL_MS); + } + fail("Waited for " + WAIT_TRACK_SIZE_TIMEOUT_MS + " milliseconds for track size to be " + + expected); + } +} diff --git a/tests/unit/src/com/android/tv/recommendation/ChannelRecordTest.java b/tests/unit/src/com/android/tv/recommendation/ChannelRecordTest.java new file mode 100644 index 00000000..9b0e2805 --- /dev/null +++ b/tests/unit/src/com/android/tv/recommendation/ChannelRecordTest.java @@ -0,0 +1,118 @@ +/* + * 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.recommendation; + +import android.test.AndroidTestCase; + +import java.util.Random; +import java.util.concurrent.TimeUnit; + +/** + * Unit tests for {@link ChannelRecord}. + */ +public class ChannelRecordTest extends AndroidTestCase { + private static final int CHANNEL_RECORD_MAX_HISTORY_SIZE = ChannelRecord.MAX_HISTORY_SIZE; + + private Random mRandom; + private ChannelRecord mChannelRecord; + private long mLatestWatchEndTimeMs; + + public void setUp() throws Exception { + super.setUp(); + mLatestWatchEndTimeMs = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1); + mChannelRecord = new ChannelRecord(getContext(), null, false); + mRandom = RecommendationUtils.createTestRandom(); + } + + public void testGetLastWatchEndTime_noHistory() { + assertEquals(0, mChannelRecord.getLastWatchEndTimeMs()); + } + + public void testGetLastWatchEndTime_oneHistory() { + addWatchLog(); + + assertEquals(mLatestWatchEndTimeMs, mChannelRecord.getLastWatchEndTimeMs()); + } + + public void testGetLastWatchEndTime_maxHistories() { + for (int i = 0; i < CHANNEL_RECORD_MAX_HISTORY_SIZE; ++i) { + addWatchLog(); + } + + assertEquals(mLatestWatchEndTimeMs, mChannelRecord.getLastWatchEndTimeMs()); + } + + public void testGetLastWatchEndTime_moreThanMaxHistories() { + for (int i = 0; i < CHANNEL_RECORD_MAX_HISTORY_SIZE + 1; ++i) { + addWatchLog(); + } + + assertEquals(mLatestWatchEndTimeMs, mChannelRecord.getLastWatchEndTimeMs()); + } + + public void testGetTotalWatchDuration_noHistory() { + assertEquals(0, mChannelRecord.getTotalWatchDurationMs()); + } + + public void testGetTotalWatchDuration_oneHistory() { + long durationMs = addWatchLog(); + + assertEquals(durationMs, mChannelRecord.getTotalWatchDurationMs()); + } + + public void testGetTotalWatchDuration_maxHistories() { + long totalWatchTimeMs = 0; + for (int i = 0; i < CHANNEL_RECORD_MAX_HISTORY_SIZE; ++i) { + long durationMs = addWatchLog(); + totalWatchTimeMs += durationMs; + } + + assertEquals(totalWatchTimeMs, mChannelRecord.getTotalWatchDurationMs()); + } + + public void testGetTotalWatchDuration_moreThanMaxHistories() { + long totalWatchTimeMs = 0; + long firstDurationMs = 0; + for (int i = 0; i < CHANNEL_RECORD_MAX_HISTORY_SIZE + 1; ++i) { + long durationMs = addWatchLog(); + totalWatchTimeMs += durationMs; + if (i == 0) { + firstDurationMs = durationMs; + } + } + + // Only latest CHANNEL_RECORD_MAX_HISTORY_SIZE logs are remained. + assertEquals(totalWatchTimeMs - firstDurationMs, mChannelRecord.getTotalWatchDurationMs()); + } + + /** + * Add new log history to channelRecord which its duration is lower than 1 minute. + * + * @return New watch log's duration time in milliseconds. + */ + private long addWatchLog() { + // Time hopping with random seconds. + mLatestWatchEndTimeMs += TimeUnit.SECONDS.toMillis(mRandom.nextInt(60) + 1); + + long durationMs = TimeUnit.SECONDS.toMillis(mRandom.nextInt(60) + 1); + mChannelRecord.logWatchHistory(new WatchedProgram(null, + mLatestWatchEndTimeMs, mLatestWatchEndTimeMs + durationMs)); + mLatestWatchEndTimeMs += durationMs; + + return durationMs; + } +} diff --git a/tests/unit/src/com/android/tv/recommendation/EvaluatorTestCase.java b/tests/unit/src/com/android/tv/recommendation/EvaluatorTestCase.java new file mode 100644 index 00000000..ee9fa95f --- /dev/null +++ b/tests/unit/src/com/android/tv/recommendation/EvaluatorTestCase.java @@ -0,0 +1,128 @@ +/* + * 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.recommendation; + +import android.test.AndroidTestCase; + +import com.android.tv.data.Channel; +import com.android.tv.recommendation.RecommendationUtils.ChannelRecordSortedMapHelper; +import com.android.tv.recommendation.Recommender.Evaluator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Base test case for Recommendation Evaluator Unit tests. + */ +public abstract class EvaluatorTestCase extends AndroidTestCase { + private static final long INVALID_CHANNEL_ID = -1; + + private ChannelRecordSortedMapHelper mChannelRecordSortedMap; + private RecommendationDataManager mDataManager; + + public T mEvaluator; + + public void setUp() throws Exception { + super.setUp(); + mChannelRecordSortedMap = new ChannelRecordSortedMapHelper(getContext()); + mDataManager = RecommendationUtils + .createMockRecommendationDataManager(mChannelRecordSortedMap); + Recommender mRecommender = new FakeRecommender(); + mEvaluator = createEvaluator(); + mEvaluator.setRecommender(mRecommender); + mChannelRecordSortedMap.setRecommender(mRecommender); + mChannelRecordSortedMap.resetRandom(RecommendationUtils.createTestRandom()); + } + + /** + * Each evaluator test has to create Evaluator in {@code mEvaluator}. + */ + public abstract T createEvaluator(); + + public void addChannels(int numberOfChannels) { + mChannelRecordSortedMap.addChannels(numberOfChannels); + } + + public Channel addChannel() { + return mChannelRecordSortedMap.addChannel(); + } + + public void addRandomWatchLogs(long watchStartTimeMs, long watchEndTimeMs, + long maxWatchDurationMs) { + assertTrue(mChannelRecordSortedMap.addRandomWatchLogs(watchStartTimeMs, watchEndTimeMs, + maxWatchDurationMs)); + } + + public void addWatchLog(long channelId, long watchStartTimeMs, long durationTimeMs) { + assertTrue(mChannelRecordSortedMap.addWatchLog(channelId, watchStartTimeMs, + durationTimeMs)); + } + + public List getChannelIdListSorted() { + return new ArrayList<>(mChannelRecordSortedMap.keySet()); + } + + public long getLatestWatchEndTimeMs() { + long latestWatchEndTimeMs = 0; + for (ChannelRecord channelRecord : mChannelRecordSortedMap.values()) { + latestWatchEndTimeMs = Math.max(latestWatchEndTimeMs, + channelRecord.getLastWatchEndTimeMs()); + } + return latestWatchEndTimeMs; + } + + /** + * Check whether scores of each channels are valid. + */ + protected void assertChannelScoresValid() { + assertEquals(Evaluator.NOT_RECOMMENDED, mEvaluator.evaluateChannel(INVALID_CHANNEL_ID)); + assertEquals(Evaluator.NOT_RECOMMENDED, + mEvaluator.evaluateChannel(mChannelRecordSortedMap.size())); + + for (long channelId : mChannelRecordSortedMap.keySet()) { + double score = mEvaluator.evaluateChannel(channelId); + assertTrue("Channel " + channelId + " score of " + score + "is not valid", + score == Evaluator.NOT_RECOMMENDED || (0.0 <= score && score <= 1.0)); + } + } + + /** + * Notify that loading channels and watch logs are finished. + */ + protected void notifyChannelAndWatchLogLoaded() { + mEvaluator.onChannelRecordListChanged(new ArrayList<>(mChannelRecordSortedMap.values())); + } + + private class FakeRecommender extends Recommender { + public FakeRecommender() { + super(new Recommender.Listener() { + @Override + public void onRecommenderReady() { + } + + @Override + public void onRecommendationChanged() { + } + }, true, mDataManager); + } + + @Override + public ChannelRecord getChannelRecord(long channelId) { + return mChannelRecordSortedMap.get(channelId); + } + } +} diff --git a/tests/unit/src/com/android/tv/recommendation/FavoriteChannelEvaluatorTest.java b/tests/unit/src/com/android/tv/recommendation/FavoriteChannelEvaluatorTest.java new file mode 100644 index 00000000..c33271bc --- /dev/null +++ b/tests/unit/src/com/android/tv/recommendation/FavoriteChannelEvaluatorTest.java @@ -0,0 +1,144 @@ +/* + * 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.recommendation; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Unit tests for {@link FavoriteChannelEvaluator}. + */ +public class FavoriteChannelEvaluatorTest extends EvaluatorTestCase { + private static final int DEFAULT_NUMBER_OF_CHANNELS = 4; + private static final long DEFAULT_WATCH_START_TIME_MS = + System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2); + private static final long DEFAULT_WATCH_END_TIME_MS = + System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1); + private static final long DEFAULT_MAX_WATCH_DURATION_MS = TimeUnit.HOURS.toMillis(1); + + public FavoriteChannelEvaluator createEvaluator() { + return new FavoriteChannelEvaluator(); + } + + public void testOneChannelWithNoWatchLog() { + long channelId = addChannel().getId(); + notifyChannelAndWatchLogLoaded(); + + assertEquals(Recommender.Evaluator.NOT_RECOMMENDED, + mEvaluator.evaluateChannel(channelId)); + } + + public void testOneChannelWithRandomWatchLogs() { + addChannel(); + addRandomWatchLogs(DEFAULT_WATCH_START_TIME_MS, DEFAULT_WATCH_END_TIME_MS, + DEFAULT_MAX_WATCH_DURATION_MS); + notifyChannelAndWatchLogLoaded(); + + assertChannelScoresValid(); + } + + public void testMultiChannelsWithNoWatchLog() { + addChannels(DEFAULT_NUMBER_OF_CHANNELS); + notifyChannelAndWatchLogLoaded(); + + List channelIdList = getChannelIdListSorted(); + for (long channelId : channelIdList) { + assertEquals(Recommender.Evaluator.NOT_RECOMMENDED, + mEvaluator.evaluateChannel(channelId)); + } + } + + public void testMultiChannelsWithRandomWatchLogs() { + addChannels(DEFAULT_NUMBER_OF_CHANNELS); + addRandomWatchLogs(DEFAULT_WATCH_START_TIME_MS, DEFAULT_WATCH_END_TIME_MS, + DEFAULT_MAX_WATCH_DURATION_MS); + notifyChannelAndWatchLogLoaded(); + + assertChannelScoresValid(); + } + + public void testMultiChannelsWithSimpleWatchLogs() { + addChannels(DEFAULT_NUMBER_OF_CHANNELS); + // For two channels which has ID x and y (x < y), the channel y is more watched + // than the channel x. (Duration is longer than channel x) + long latestWatchEndTimeMs = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2); + long durationMs = 0; + List channelIdList = getChannelIdListSorted(); + for (long channelId : channelIdList) { + durationMs += TimeUnit.MINUTES.toMillis(30); + addWatchLog(channelId, latestWatchEndTimeMs, durationMs); + latestWatchEndTimeMs += durationMs; + } + notifyChannelAndWatchLogLoaded(); + + assertChannelScoresValid(); + // Channel score must be increased as channel ID increased. + double previousScore = Recommender.Evaluator.NOT_RECOMMENDED; + for (long channelId : channelIdList) { + double score = mEvaluator.evaluateChannel(channelId); + assertTrue(previousScore <= score); + previousScore = score; + } + } + + public void testTwoChannelsWithSameWatchDuration() { + long channelOne = addChannel().getId(); + long channelTwo = addChannel().getId(); + addWatchLog(channelOne, System.currentTimeMillis() - TimeUnit.HOURS.toMillis(1), + TimeUnit.MINUTES.toMillis(30)); + addWatchLog(channelTwo, System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(30), + TimeUnit.MINUTES.toMillis(30)); + notifyChannelAndWatchLogLoaded(); + + assertTrue(mEvaluator.evaluateChannel(channelOne) == + mEvaluator.evaluateChannel(channelTwo)); + } + + public void testTwoChannelsWithDifferentWatchDuration() { + long channelOne = addChannel().getId(); + long channelTwo = addChannel().getId(); + addWatchLog(channelOne, System.currentTimeMillis() - TimeUnit.HOURS.toMillis(3), + TimeUnit.MINUTES.toMillis(30)); + addWatchLog(channelTwo, System.currentTimeMillis() - TimeUnit.HOURS.toMillis(2), + TimeUnit.HOURS.toMillis(1)); + notifyChannelAndWatchLogLoaded(); + + // Channel two was watched longer than channel one, so it's score is bigger. + assertTrue(mEvaluator.evaluateChannel(channelOne) < mEvaluator.evaluateChannel(channelTwo)); + + addWatchLog(channelOne, System.currentTimeMillis() - TimeUnit.HOURS.toMillis(1), + TimeUnit.HOURS.toMillis(1)); + + // Now, channel one was watched longer than channel two, so it's score is bigger. + assertTrue(mEvaluator.evaluateChannel(channelOne) > mEvaluator.evaluateChannel(channelTwo)); + } + + public void testScoreIncreasesWithNewWatchLog() { + long channelId = addChannel().getId(); + addRandomWatchLogs(DEFAULT_WATCH_START_TIME_MS, DEFAULT_WATCH_END_TIME_MS, + DEFAULT_MAX_WATCH_DURATION_MS); + notifyChannelAndWatchLogLoaded(); + + long latestWatchEndTimeMs = getLatestWatchEndTimeMs(); + double previousScore = mEvaluator.evaluateChannel(channelId); + + addWatchLog(channelId, latestWatchEndTimeMs, TimeUnit.MINUTES.toMillis(10)); + + // Score must be increased because total watch duration of the channel increases. + assertTrue(previousScore <= mEvaluator.evaluateChannel(channelId)); + } +} diff --git a/tests/unit/src/com/android/tv/recommendation/RecentChannelEvaluatorTest.java b/tests/unit/src/com/android/tv/recommendation/RecentChannelEvaluatorTest.java new file mode 100644 index 00000000..a888ceea --- /dev/null +++ b/tests/unit/src/com/android/tv/recommendation/RecentChannelEvaluatorTest.java @@ -0,0 +1,140 @@ +/* + * 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.recommendation; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * Unit tests for {@link RecentChannelEvaluator}. + */ +public class RecentChannelEvaluatorTest extends EvaluatorTestCase { + private static final int DEFAULT_NUMBER_OF_CHANNELS = 4; + private static final long DEFAULT_WATCH_START_TIME_MS = + System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2); + private static final long DEFAULT_WATCH_END_TIME_MS = + System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1); + private static final long DEFAULT_MAX_WATCH_DURATION_MS = TimeUnit.HOURS.toMillis(1); + + public RecentChannelEvaluator createEvaluator() { + return new RecentChannelEvaluator(); + } + + public void testOneChannelWithNoWatchLog() { + long channelId = addChannel().getId(); + notifyChannelAndWatchLogLoaded(); + + assertEquals(Recommender.Evaluator.NOT_RECOMMENDED, + mEvaluator.evaluateChannel(channelId)); + } + + public void testOneChannelWithRandomWatchLogs() { + addChannel(); + addRandomWatchLogs(DEFAULT_WATCH_START_TIME_MS, DEFAULT_WATCH_END_TIME_MS, + DEFAULT_MAX_WATCH_DURATION_MS); + notifyChannelAndWatchLogLoaded(); + + assertChannelScoresValid(); + } + + public void testMultiChannelsWithNoWatchLog() { + addChannels(DEFAULT_NUMBER_OF_CHANNELS); + notifyChannelAndWatchLogLoaded(); + + List channelIdList = getChannelIdListSorted(); + for (long channelId : channelIdList) { + assertEquals(Recommender.Evaluator.NOT_RECOMMENDED, + mEvaluator.evaluateChannel(channelId)); + } + } + + public void testMultiChannelsWithRandomWatchLogs() { + addChannels(DEFAULT_NUMBER_OF_CHANNELS); + addRandomWatchLogs(DEFAULT_WATCH_START_TIME_MS, DEFAULT_WATCH_END_TIME_MS, + DEFAULT_MAX_WATCH_DURATION_MS); + notifyChannelAndWatchLogLoaded(); + + assertChannelScoresValid(); + } + + public void testMultiChannelsWithSimpleWatchLogs() { + addChannels(DEFAULT_NUMBER_OF_CHANNELS); + // Every channel has one watch log with 1 hour. Also, for two channels + // which has ID x and y (x < y), the channel y is watched later than the channel x. + long latestWatchEndTimeMs = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2); + List channelIdList = getChannelIdListSorted(); + for (long channelId : channelIdList) { + addWatchLog(channelId, latestWatchEndTimeMs, TimeUnit.HOURS.toMillis(1)); + latestWatchEndTimeMs += TimeUnit.HOURS.toMillis(1); + } + notifyChannelAndWatchLogLoaded(); + + assertChannelScoresValid(); + // Channel score must be increased as channel ID increased. + double previousScore = Recommender.Evaluator.NOT_RECOMMENDED; + for (long channelId : channelIdList) { + double score = mEvaluator.evaluateChannel(channelId); + assertTrue(previousScore <= score); + previousScore = score; + } + } + + public void testScoreIncreasesWithNewWatchLog() { + addChannels(DEFAULT_NUMBER_OF_CHANNELS); + addRandomWatchLogs(DEFAULT_WATCH_START_TIME_MS, DEFAULT_WATCH_END_TIME_MS, + DEFAULT_MAX_WATCH_DURATION_MS); + notifyChannelAndWatchLogLoaded(); + + List channelIdList = getChannelIdListSorted(); + long latestWatchEndTimeMs = getLatestWatchEndTimeMs(); + for (long channelId : channelIdList) { + double previousScore = mEvaluator.evaluateChannel(channelId); + + long durationMs = TimeUnit.MINUTES.toMillis(10); + addWatchLog(channelId, latestWatchEndTimeMs, durationMs); + latestWatchEndTimeMs += durationMs; + + // Score must be increased because recentness of the log increases. + assertTrue(previousScore <= mEvaluator.evaluateChannel(channelId)); + } + } + + public void testScoreDecreasesWithIncrementOfWatchedLogUpdatedTime() { + addChannels(DEFAULT_NUMBER_OF_CHANNELS); + addRandomWatchLogs(DEFAULT_WATCH_START_TIME_MS, DEFAULT_WATCH_END_TIME_MS, + DEFAULT_MAX_WATCH_DURATION_MS); + notifyChannelAndWatchLogLoaded(); + + Map scores = new HashMap<>(); + List channelIdList = getChannelIdListSorted(); + long latestWatchedEndTimeMs = getLatestWatchEndTimeMs(); + + for (long channelId : channelIdList) { + scores.put(channelId, mEvaluator.evaluateChannel(channelId)); + } + + long newChannelId = addChannel().getId(); + addWatchLog(newChannelId, latestWatchedEndTimeMs, TimeUnit.MINUTES.toMillis(10)); + + for (long channelId : channelIdList) { + // Score must be decreased because LastWatchLogUpdateTime increases by new log. + assertTrue(mEvaluator.evaluateChannel(channelId) <= scores.get(channelId)); + } + } +} diff --git a/tests/unit/src/com/android/tv/recommendation/RecommendationUtils.java b/tests/unit/src/com/android/tv/recommendation/RecommendationUtils.java new file mode 100644 index 00000000..d275bfbb --- /dev/null +++ b/tests/unit/src/com/android/tv/recommendation/RecommendationUtils.java @@ -0,0 +1,180 @@ +/* + * 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.recommendation; + +import android.content.Context; +import android.util.Log; + +import com.android.tv.data.Channel; + +import org.mockito.Matchers; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Random; +import java.util.TreeMap; +import java.util.concurrent.TimeUnit; + +public class RecommendationUtils { + private static final String TAG = "RecommendationUtils"; + private static final long INVALID_CHANNEL_ID = -1; + private static final long DEFAULT_RANDOM_SEED = getSeed(); + + private static long getSeed() { + // Set random seed as the date to track failed test data easily. + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.getDefault()); + String today = dateFormat.format(new Date()); + Log.d(TAG, "Today's random seed is " + today); + return Long.valueOf(today); + } + + /** + * Return the Random class which is needed to make random data for testing. + * Default seed of the random is today's date. + */ + public static Random createTestRandom() { + return new Random(DEFAULT_RANDOM_SEED); + } + + /** + * Create a mock RecommendationDataManager backed by a {@link ChannelRecordSortedMapHelper}. + */ + public static RecommendationDataManager createMockRecommendationDataManager( + final ChannelRecordSortedMapHelper channelRecordSortedMap) { + RecommendationDataManager dataManager = Mockito.mock(RecommendationDataManager.class); + Mockito.doAnswer(new Answer() { + @Override + public Integer answer(InvocationOnMock invocation) throws Throwable { + return channelRecordSortedMap.size(); + } + }).when(dataManager).getChannelRecordCount(); + Mockito.doAnswer(new Answer>() { + @Override + public Collection answer(InvocationOnMock invocation) throws Throwable { + return channelRecordSortedMap.values(); + } + }).when(dataManager).getChannelRecords(); + Mockito.doAnswer(new Answer() { + @Override + public ChannelRecord answer(InvocationOnMock invocation) throws Throwable { + long channelId = (long) invocation.getArguments()[0]; + return channelRecordSortedMap.get(channelId); + } + }).when(dataManager).getChannelRecord(Matchers.anyLong()); + return dataManager; + } + + public static class ChannelRecordSortedMapHelper extends TreeMap { + private Context mContext; + private Recommender mRecommender; + private Random mRandom = createTestRandom(); + + public ChannelRecordSortedMapHelper(Context context) { + mContext = context; + } + + public void setRecommender(Recommender recommender) { + mRecommender = recommender; + } + + public void resetRandom(Random random) { + mRandom = random; + } + + /** + * Add new {@code numberOfChannels} channels by adding channel record to + * {@code channelRecordMap} with no history. + * This action corresponds to loading channels in the RecommendationDataManger. + */ + public void addChannels(int numberOfChannels) { + for (int i = 0; i < numberOfChannels; ++i) { + addChannel(); + } + } + + /** + * Add new one channel by adding channel record to {@code channelRecordMap} with no history. + * This action corresponds to loading one channel in the RecommendationDataManger. + * + * @return The new channel was made by this method. + */ + public Channel addChannel() { + long channelId = size(); + Channel channel = new Channel.Builder().setId(channelId).build(); + ChannelRecord channelRecord = new ChannelRecord(mContext, channel, false); + put(channelId, channelRecord); + return channel; + } + + /** + * Add the watch logs which its durationTime is under {@code maxWatchDurationMs}. + * Add until latest watch end time becomes bigger than {@code watchEndTimeMs}, + * starting from {@code watchStartTimeMs}. + * + * @return true if adding watch log success, otherwise false. + */ + public boolean addRandomWatchLogs(long watchStartTimeMs, long watchEndTimeMs, + long maxWatchDurationMs) { + long latestWatchEndTimeMs = watchStartTimeMs; + long previousChannelId = INVALID_CHANNEL_ID; + List channelIdList = new ArrayList<>(keySet()); + while (latestWatchEndTimeMs < watchEndTimeMs) { + long channelId = channelIdList.get(mRandom.nextInt(channelIdList.size())); + if (previousChannelId == channelId) { + // Time hopping with random minutes. + latestWatchEndTimeMs += TimeUnit.MINUTES.toMillis(mRandom.nextInt(30) + 1); + } + long watchedDurationMs = mRandom.nextInt((int) maxWatchDurationMs) + 1; + if (!addWatchLog(channelId, latestWatchEndTimeMs, watchedDurationMs)) { + return false; + } + latestWatchEndTimeMs += watchedDurationMs; + previousChannelId = channelId; + } + return true; + } + + /** + * Add new watch log to channel that id is {@code ChannelId}. Add watch log starts from + * {@code watchStartTimeMs} with duration {@code durationTimeMs}. If adding is finished, + * notify the recommender that there's a new watch log. + * + * @return true if adding watch log success, otherwise false. + */ + public boolean addWatchLog(long channelId, long watchStartTimeMs, long durationTimeMs) { + ChannelRecord channelRecord = get(channelId); + if (channelRecord == null || + watchStartTimeMs + durationTimeMs > System.currentTimeMillis()) { + return false; + } + + channelRecord.logWatchHistory(new WatchedProgram(null, watchStartTimeMs, + watchStartTimeMs + durationTimeMs)); + if (mRecommender != null) { + mRecommender.onNewWatchLog(channelRecord); + } + return true; + } + } +} diff --git a/tests/unit/src/com/android/tv/recommendation/RecommenderTest.java b/tests/unit/src/com/android/tv/recommendation/RecommenderTest.java new file mode 100644 index 00000000..4f16d168 --- /dev/null +++ b/tests/unit/src/com/android/tv/recommendation/RecommenderTest.java @@ -0,0 +1,324 @@ +/* + * 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.recommendation; + +import android.test.AndroidTestCase; +import android.test.MoreAsserts; + +import com.android.tv.data.Channel; +import com.android.tv.recommendation.RecommendationUtils.ChannelRecordSortedMapHelper; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class RecommenderTest extends AndroidTestCase { + private static final int DEFAULT_NUMBER_OF_CHANNELS = 5; + private static final long DEFAULT_WATCH_START_TIME_MS = + System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2); + private static final long DEFAULT_WATCH_END_TIME_MS = + System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1); + private static final long DEFAULT_MAX_WATCH_DURATION_MS = TimeUnit.HOURS.toMillis(1); + + private final Comparator CHANNEL_SORT_KEY_COMPARATOR = new Comparator() { + @Override + public int compare(Channel lhs, Channel rhs) { + return mRecommender.getChannelSortKey(lhs.getId()) + .compareTo(mRecommender.getChannelSortKey(rhs.getId())); + } + }; + private final Runnable START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS = new Runnable() { + @Override + public void run() { + // Add 4 channels in ChannelRecordMap for testing. Store the added channels to + // mChannels_1 ~ mChannels_4. They are sorted by channel id in increasing order. + mChannel_1 = mChannelRecordSortedMap.addChannel(); + mChannel_2 = mChannelRecordSortedMap.addChannel(); + mChannel_3 = mChannelRecordSortedMap.addChannel(); + mChannel_4 = mChannelRecordSortedMap.addChannel(); + } + }; + + private RecommendationDataManager mDataManager; + private Recommender mRecommender; + private FakeEvaluator mEvaluator; + private ChannelRecordSortedMapHelper mChannelRecordSortedMap; + private boolean mOnRecommenderReady; + private boolean mOnRecommendationChanged; + private Channel mChannel_1; + private Channel mChannel_2; + private Channel mChannel_3; + private Channel mChannel_4; + + public void setUp() throws Exception { + super.setUp(); + + mChannelRecordSortedMap = new ChannelRecordSortedMapHelper(getContext()); + mDataManager = RecommendationUtils + .createMockRecommendationDataManager(mChannelRecordSortedMap); + mChannelRecordSortedMap.resetRandom(RecommendationUtils.createTestRandom()); + } + + public void testRecommendChannels_includeRecommendedOnly_allChannelsHaveNoScore() { + createRecommender(true, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS); + + // Recommender doesn't recommend any channels because all channels are not recommended. + assertEquals(0, mRecommender.recommendChannels().size()); + assertEquals(0, mRecommender.recommendChannels(-5).size()); + assertEquals(0, mRecommender.recommendChannels(0).size()); + assertEquals(0, mRecommender.recommendChannels(3).size()); + assertEquals(0, mRecommender.recommendChannels(4).size()); + assertEquals(0, mRecommender.recommendChannels(5).size()); + } + + public void testRecommendChannels_notIncludeRecommendedOnly_allChannelsHaveNoScore() { + createRecommender(false, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS); + + // Recommender recommends every channel because it recommends not-recommended channels too. + assertEquals(4, mRecommender.recommendChannels().size()); + assertEquals(0, mRecommender.recommendChannels(-5).size()); + assertEquals(0, mRecommender.recommendChannels(0).size()); + assertEquals(3, mRecommender.recommendChannels(3).size()); + assertEquals(4, mRecommender.recommendChannels(4).size()); + assertEquals(4, mRecommender.recommendChannels(5).size()); + } + + public void testRecommendChannels_includeRecommendedOnly_allChannelsHaveScore() { + createRecommender(true, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS); + + setChannelScores_scoreIncreasesAsChannelIdIncreases(); + + // recommendChannels must be sorted by score in decreasing order. + // (i.e. sorted by channel ID in decreasing order in this case) + MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(), + mChannel_4, mChannel_3, mChannel_2, mChannel_1); + assertEquals(0, mRecommender.recommendChannels(-5).size()); + assertEquals(0, mRecommender.recommendChannels(0).size()); + MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(3), + mChannel_4, mChannel_3, mChannel_2); + MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(4), + mChannel_4, mChannel_3, mChannel_2, mChannel_1); + MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(5), + mChannel_4, mChannel_3, mChannel_2, mChannel_1); + } + + public void testRecommendChannels_notIncludeRecommendedOnly_allChannelsHaveScore() { + createRecommender(false, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS); + + setChannelScores_scoreIncreasesAsChannelIdIncreases(); + + // recommendChannels must be sorted by score in decreasing order. + // (i.e. sorted by channel ID in decreasing order in this case) + MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(), + mChannel_4, mChannel_3, mChannel_2, mChannel_1); + assertEquals(0, mRecommender.recommendChannels(-5).size()); + assertEquals(0, mRecommender.recommendChannels(0).size()); + MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(3), + mChannel_4, mChannel_3, mChannel_2); + MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(4), + mChannel_4, mChannel_3, mChannel_2, mChannel_1); + MoreAsserts.assertContentsInOrder(mRecommender.recommendChannels(5), + mChannel_4, mChannel_3, mChannel_2, mChannel_1); + } + + public void testRecommendChannels_includeRecommendedOnly_fewChannelsHaveScore() { + createRecommender(true, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS); + + mEvaluator.setChannelScore(mChannel_1.getId(), 1.0); + mEvaluator.setChannelScore(mChannel_2.getId(), 1.0); + + // Only two channels are recommended because recommender doesn't recommend other channels. + MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(), + mChannel_1, mChannel_2); + assertEquals(0, mRecommender.recommendChannels(-5).size()); + assertEquals(0, mRecommender.recommendChannels(0).size()); + MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(3), + mChannel_1, mChannel_2); + MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(4), + mChannel_1, mChannel_2); + MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(5), + mChannel_1, mChannel_2); + } + + public void testRecommendChannels_notIncludeRecommendedOnly_fewChannelsHaveScore() { + createRecommender(false, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS); + + mEvaluator.setChannelScore(mChannel_1.getId(), 1.0); + mEvaluator.setChannelScore(mChannel_2.getId(), 1.0); + + assertEquals(4, mRecommender.recommendChannels().size()); + MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels().subList(0, 2), + mChannel_1, mChannel_2); + + assertEquals(0, mRecommender.recommendChannels(-5).size()); + assertEquals(0, mRecommender.recommendChannels(0).size()); + + assertEquals(3, mRecommender.recommendChannels(3).size()); + MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(3).subList(0, 2), + mChannel_1, mChannel_2); + + assertEquals(4, mRecommender.recommendChannels(4).size()); + MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(4).subList(0, 2), + mChannel_1, mChannel_2); + + assertEquals(4, mRecommender.recommendChannels(5).size()); + MoreAsserts.assertContentsInAnyOrder(mRecommender.recommendChannels(5).subList(0, 2), + mChannel_1, mChannel_2); + } + + public void testGetChannelSortKey_recommendAllChannels() { + createRecommender(true, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS); + + setChannelScores_scoreIncreasesAsChannelIdIncreases(); + + List expectedChannelList = mRecommender.recommendChannels(); + List channelList = Arrays.asList(mChannel_1, mChannel_2, mChannel_3, mChannel_4); + Collections.sort(channelList, CHANNEL_SORT_KEY_COMPARATOR); + + // Recommended channel list and channel list sorted by sort key must be the same. + MoreAsserts.assertContentsInOrder(channelList, expectedChannelList.toArray()); + assertSortKeyNotInvalid(channelList); + } + + public void testGetChannelSortKey_recommendFewChannels() { + // Test with recommending 3 channels. + createRecommender(true, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS); + + setChannelScores_scoreIncreasesAsChannelIdIncreases(); + + List expectedChannelList = mRecommender.recommendChannels(3); + // A channel which is not recommended by the recommender has to get an invalid sort key. + assertEquals(Recommender.INVALID_CHANNEL_SORT_KEY, + mRecommender.getChannelSortKey(mChannel_1.getId())); + + List channelList = Arrays.asList(mChannel_2, mChannel_3, mChannel_4); + Collections.sort(channelList, CHANNEL_SORT_KEY_COMPARATOR); + + MoreAsserts.assertContentsInOrder(channelList, expectedChannelList.toArray()); + assertSortKeyNotInvalid(channelList); + } + + public void testListener_onRecommendationChanged() { + createRecommender(true, START_DATAMANAGER_RUNNABLE_ADD_FOUR_CHANNELS); + // FakeEvaluator doesn't recommend a channel with empty watch log. As every channel + // doesn't have a watch log, nothing is recommended and recommendation isn't changed. + assertFalse(mOnRecommendationChanged); + + // Set lastRecommendationUpdatedTimeUtcMs to check recommendation changed because, + // recommender has a minimum recommendation update period. + mRecommender.setLastRecommendationUpdatedTimeUtcMs( + System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(10)); + long latestWatchEndTimeMs = DEFAULT_WATCH_START_TIME_MS; + for (long channelId : mChannelRecordSortedMap.keySet()) { + mEvaluator.setChannelScore(channelId, 1.0); + // Add a log to recalculate the recommendation score. + assertTrue(mChannelRecordSortedMap.addWatchLog(channelId, latestWatchEndTimeMs, + TimeUnit.MINUTES.toMillis(10))); + latestWatchEndTimeMs += TimeUnit.MINUTES.toMillis(10); + } + + // onRecommendationChanged must be called, because recommend channels are not empty, + // by setting score to each channel. + assertTrue(mOnRecommendationChanged); + } + + public void testListener_onRecommenderReady() { + createRecommender(true, new Runnable() { + @Override + public void run() { + mChannelRecordSortedMap.addChannels(DEFAULT_NUMBER_OF_CHANNELS); + mChannelRecordSortedMap.addRandomWatchLogs(DEFAULT_WATCH_START_TIME_MS, + DEFAULT_WATCH_END_TIME_MS, DEFAULT_MAX_WATCH_DURATION_MS); + } + }); + + // After loading channels and watch logs are finished, recommender must be available to use. + assertTrue(mOnRecommenderReady); + } + + private void assertSortKeyNotInvalid(List channelList) { + for (Channel channel : channelList) { + MoreAsserts.assertNotEqual(Recommender.INVALID_CHANNEL_SORT_KEY, + mRecommender.getChannelSortKey(channel.getId())); + } + } + + private void createRecommender(boolean includeRecommendedOnly, + Runnable startDataManagerRunnable) { + mRecommender = new Recommender(new Recommender.Listener() { + @Override + public void onRecommenderReady() { + mOnRecommenderReady = true; + } + @Override + public void onRecommendationChanged() { + mOnRecommendationChanged = true; + } + }, includeRecommendedOnly, mDataManager); + + mEvaluator = new FakeEvaluator(); + mRecommender.registerEvaluator(mEvaluator); + mChannelRecordSortedMap.setRecommender(mRecommender); + + // When mRecommender is instantiated, its dataManager will be started, and load channels + // and watch history data if it is not started. + if (startDataManagerRunnable != null) { + startDataManagerRunnable.run(); + mRecommender.onChannelRecordChanged(); + } + // After loading channels and watch history data are finished, + // RecommendationDataManager calls listener.onChannelRecordLoaded() + // which will be mRecommender.onChannelRecordLoaded(). + mRecommender.onChannelRecordLoaded(); + } + + private List getChannelIdListSorted() { + return new ArrayList<>(mChannelRecordSortedMap.keySet()); + } + + private void setChannelScores_scoreIncreasesAsChannelIdIncreases() { + List channelIdList = getChannelIdListSorted(); + double score = Math.pow(0.5, channelIdList.size()); + for (long channelId : channelIdList) { + // Channel with smaller id has smaller score than channel with higher id. + mEvaluator.setChannelScore(channelId, score); + score *= 2.0; + } + } + + private class FakeEvaluator extends Recommender.Evaluator { + private Map mChannelScore = new HashMap<>(); + + @Override + public double evaluateChannel(long channelId) { + if (getRecommender().getChannelRecord(channelId) == null) { + return NOT_RECOMMENDED; + } + Double score = mChannelScore.get(channelId); + return score == null ? NOT_RECOMMENDED : score; + } + + public void setChannelScore(long channelId, double score) { + mChannelScore.put(channelId, score); + } + } +} diff --git a/tests/unit/src/com/android/tv/recommendation/RoutineWatchEvaluatorTest.java b/tests/unit/src/com/android/tv/recommendation/RoutineWatchEvaluatorTest.java new file mode 100644 index 00000000..e3c92859 --- /dev/null +++ b/tests/unit/src/com/android/tv/recommendation/RoutineWatchEvaluatorTest.java @@ -0,0 +1,205 @@ +/* + * 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.recommendation; + +import android.test.MoreAsserts; + +import com.android.tv.data.Program; +import com.android.tv.recommendation.RoutineWatchEvaluator.ProgramTime; + +import java.util.Calendar; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class RoutineWatchEvaluatorTest extends EvaluatorTestCase { + + public RoutineWatchEvaluator createEvaluator() { + return new RoutineWatchEvaluator(); + } + + public void testSplitTextToWords() { + assertSplitTextToWords(""); + assertSplitTextToWords("Google", "Google"); + assertSplitTextToWords("The Big Bang Theory", "The", "Big", "Bang", "Theory"); + assertSplitTextToWords("Hello, world!", "Hello", "world"); + assertSplitTextToWords("Adam's Rib", "Adam's", "Rib"); + assertSplitTextToWords("G.I. Joe", "G.I", "Joe"); + assertSplitTextToWords("A.I.", "A.I"); + } + + public void testCalculateMaximumMatchedWordSequenceLength() { + assertMaximumMatchedWordSequenceLength(0, "", "Google"); + assertMaximumMatchedWordSequenceLength(2, "The Big Bang Theory", "Big Bang"); + assertMaximumMatchedWordSequenceLength(2, "The Big Bang Theory", "Theory Of Big Bang"); + assertMaximumMatchedWordSequenceLength(4, "The Big Bang Theory", "The Big Bang Theory"); + assertMaximumMatchedWordSequenceLength(1, "Modern Family", "Family Guy"); + assertMaximumMatchedWordSequenceLength(1, "The Simpsons", "The Walking Dead"); + assertMaximumMatchedWordSequenceLength(3, "Game Of Thrones 1", "Game Of Thrones 6"); + assertMaximumMatchedWordSequenceLength(0, "Dexter", "Friends"); + } + + public void testProgramTime_createFromProgram() { + Calendar time = Calendar.getInstance(); + int todayDayOfWeek = time.get(Calendar.DAY_OF_WEEK); + // Value of DayOfWeek is between 1 and 7 (inclusive). + int tomorrowDayOfWeek = (todayDayOfWeek % 7) + 1; + + // Today 00:00 - 01:00. + ProgramTime programTimeToday0000_0100 = ProgramTime.createFromProgram( + createDummyProgram(todayAtHourMin(0, 0), TimeUnit.HOURS.toMillis(1))); + assertProgramTime(todayDayOfWeek, hourMinuteToSec(0, 0), hourMinuteToSec(1, 0), + programTimeToday0000_0100); + + // Today 23:30 - 24:30. + ProgramTime programTimeToday2330_2430 = ProgramTime.createFromProgram( + createDummyProgram(todayAtHourMin(23, 30), TimeUnit.HOURS.toMillis(1))); + assertProgramTime(todayDayOfWeek, hourMinuteToSec(23, 30), hourMinuteToSec(24, 30), + programTimeToday2330_2430); + + // Tomorrow 00:00 - 01:00. + ProgramTime programTimeTomorrow0000_0100 = ProgramTime.createFromProgram( + createDummyProgram(tomorrowAtHourMin(0, 0), TimeUnit.HOURS.toMillis(1))); + assertProgramTime(tomorrowDayOfWeek, hourMinuteToSec(0, 0), hourMinuteToSec(1, 0), + programTimeTomorrow0000_0100); + + // Tomorrow 23:30 - 24:30. + ProgramTime programTimeTomorrow2330_2430 = ProgramTime.createFromProgram( + createDummyProgram(tomorrowAtHourMin(23, 30), TimeUnit.HOURS.toMillis(1))); + assertProgramTime(tomorrowDayOfWeek, hourMinuteToSec(23, 30), hourMinuteToSec(24, 30), + programTimeTomorrow2330_2430); + + // Today 18:00 - Tomorrow 12:00. + ProgramTime programTimeToday1800_3600 = ProgramTime.createFromProgram( + createDummyProgram(todayAtHourMin(18, 0), TimeUnit.HOURS.toMillis(18))); + // Maximum duration of ProgramTime is 12 hours. + // So, this program looks like it ends at Tomorrow 06:00 (30:00). + assertProgramTime(todayDayOfWeek, hourMinuteToSec(18, 0), hourMinuteToSec(30, 0), + programTimeToday1800_3600); + } + + public void testCalculateOverlappedIntervalScore() { + // Today 21:00 - 24:00. + ProgramTime programTimeToday2100_2400 = ProgramTime.createFromProgram( + createDummyProgram(todayAtHourMin(21, 0), TimeUnit.HOURS.toMillis(3))); + // Today 22:00 - 01:00. + ProgramTime programTimeToday2200_0100 = ProgramTime.createFromProgram( + createDummyProgram(todayAtHourMin(22, 0), TimeUnit.HOURS.toMillis(3))); + // Tomorrow 00:00 - 03:00. + ProgramTime programTimeTomorrow0000_0300 = ProgramTime.createFromProgram( + createDummyProgram(tomorrowAtHourMin(0, 0), TimeUnit.HOURS.toMillis(3))); + // Tomorrow 20:00 - Tomorrow 23:00. + ProgramTime programTimeTomorrow2000_2300 = ProgramTime.createFromProgram( + createDummyProgram(tomorrowAtHourMin(20, 0), TimeUnit.HOURS.toMillis(3))); + + // Check intersection time and commutative law in all cases. + int oneHourInSec = hourMinuteToSec(1, 0); + assertOverlappedIntervalScore(2 * oneHourInSec, true, + programTimeToday2100_2400, programTimeToday2200_0100); + assertOverlappedIntervalScore(0, false, + programTimeToday2100_2400, programTimeTomorrow0000_0300); + assertOverlappedIntervalScore(2 * oneHourInSec, false, + programTimeToday2100_2400, programTimeTomorrow2000_2300); + assertOverlappedIntervalScore(oneHourInSec, true, + programTimeToday2200_0100, programTimeTomorrow0000_0300); + assertOverlappedIntervalScore(oneHourInSec, false, + programTimeToday2200_0100, programTimeTomorrow2000_2300); + assertOverlappedIntervalScore(0, false, + programTimeTomorrow0000_0300, programTimeTomorrow2000_2300); + } + + public void testGetTimeOfDayInSec() { + // Time was set as 00:00:00. So, getTimeOfDay must returns 0 (= 0 * 60 * 60 + 0 * 60 + 0). + assertEquals("TimeOfDayInSec", hourMinuteToSec(0, 0), + RoutineWatchEvaluator.getTimeOfDayInSec(todayAtHourMin(0, 0))); + + // Time was set as 23:59:59. So, getTimeOfDay must returns 23 * 60 + 60 + 59 * 60 + 59. + assertEquals("TimeOfDayInSec", hourMinuteSecondToSec(23, 59, 59), + RoutineWatchEvaluator.getTimeOfDayInSec(todayAtHourMinSec(23, 59, 59))); + } + + private void assertSplitTextToWords(String text, String... words) { + List wordList = RoutineWatchEvaluator.splitTextToWords(text); + MoreAsserts.assertContentsInOrder(wordList, words); + } + + private void assertMaximumMatchedWordSequenceLength(int expectedLength, + String text1, String text2) { + List wordList1 = RoutineWatchEvaluator.splitTextToWords(text1); + List wordList2 = RoutineWatchEvaluator.splitTextToWords(text2); + assertEquals("MaximumMatchedWordSequenceLength", expectedLength, + mEvaluator.calculateMaximumMatchedWordSequenceLength(wordList1, wordList2)); + assertEquals("MaximumMatchedWordSequenceLength", expectedLength, + mEvaluator.calculateMaximumMatchedWordSequenceLength(wordList2, wordList1)); + } + + private void assertProgramTime(int expectedWeekDay, int expectedStartTimeOfDayInSec, + int expectedEndTimeOfDayInSec, ProgramTime actualProgramTime) { + assertEquals("Weekday", expectedWeekDay, actualProgramTime.weekDay); + assertEquals("StartTimeOfDayInSec", + expectedStartTimeOfDayInSec, actualProgramTime.startTimeOfDayInSec); + assertEquals("EndTimeOfDayInSec", + expectedEndTimeOfDayInSec, actualProgramTime.endTimeOfDayInSec); + } + + private void assertOverlappedIntervalScore(int expectedSeconds, boolean overlappedOnSameDay, + ProgramTime t1, ProgramTime t2) { + double score = (double) expectedSeconds; + if (!overlappedOnSameDay) { + score *= RoutineWatchEvaluator.MULTIPLIER_FOR_UNMATCHED_DAY_OF_WEEK; + } + // Two tests for testing commutative law. + assertEquals("OverlappedIntervalScore", + score, mEvaluator.calculateOverlappedIntervalScore(t1, t2)); + assertEquals("OverlappedIntervalScore", + score, mEvaluator.calculateOverlappedIntervalScore(t2, t1)); + } + + private int hourMinuteToSec(int hour, int minute) { + return hourMinuteSecondToSec(hour, minute, 0); + } + + private int hourMinuteSecondToSec(int hour, int minute, int second) { + return hour * 60 * 60 + minute * 60 + second; + } + + private Calendar todayAtHourMin(int hour, int minute) { + return todayAtHourMinSec(hour, minute, 0); + } + + private Calendar todayAtHourMinSec(int hour, int minute, int second) { + Calendar time = Calendar.getInstance(); + time.set(Calendar.HOUR_OF_DAY, hour); + time.set(Calendar.MINUTE, minute); + time.set(Calendar.SECOND, second); + return time; + } + + private Calendar tomorrowAtHourMin(int hour, int minute) { + Calendar time = todayAtHourMin(hour, minute); + time.add(Calendar.DATE, 1); + return time; + } + + private Program createDummyProgram(Calendar startTime, long programDurationMs) { + long startTimeMs = startTime.getTimeInMillis(); + + return new Program.Builder() + .setStartTimeUtcMillis(startTimeMs) + .setEndTimeUtcMillis(startTimeMs + programDurationMs) + .build(); + } +} diff --git a/tests/unit/src/com/android/tv/tests/TvActivityTest.java b/tests/unit/src/com/android/tv/tests/TvActivityTest.java new file mode 100644 index 00000000..92998ab3 --- /dev/null +++ b/tests/unit/src/com/android/tv/tests/TvActivityTest.java @@ -0,0 +1,34 @@ +/* + * 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.tests; + +import android.test.ActivityInstrumentationTestCase2; +import android.test.suitebuilder.annotation.MediumTest; + +import com.android.tv.TvActivity; + +@MediumTest +public class TvActivityTest extends ActivityInstrumentationTestCase2 { + + public TvActivityTest() { + super(TvActivity.class); + } + + public void testLifeCycle() { + getActivity(); + } +} diff --git a/tests/unit/src/com/android/tv/ui/SetupViewTest.java b/tests/unit/src/com/android/tv/ui/SetupViewTest.java new file mode 100644 index 00000000..ce3b79fb --- /dev/null +++ b/tests/unit/src/com/android/tv/ui/SetupViewTest.java @@ -0,0 +1,86 @@ +/* + * 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.ui; + +import android.content.pm.ResolveInfo; +import android.media.tv.TvInputInfo; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; + +import com.android.tv.testing.ComparatorTester; +import com.android.tv.util.SetupUtils; +import com.android.tv.util.TestUtils; +import com.android.tv.util.TvInputManagerHelper; + +import java.util.Comparator; +import java.util.LinkedHashMap; + +import org.mockito.Matchers; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +/** + * Test for {@link SetupView} + */ +@SmallTest +public class SetupViewTest extends AndroidTestCase { + public void testComparator() throws Exception { + final LinkedHashMap INPUT_ID_TO_NEW_INPUT = new LinkedHashMap<>(); + INPUT_ID_TO_NEW_INPUT.put("2_new_input", true); + INPUT_ID_TO_NEW_INPUT.put("4_new_input", true); + INPUT_ID_TO_NEW_INPUT.put("0_old_input", false); + INPUT_ID_TO_NEW_INPUT.put("1_old_input", false); + INPUT_ID_TO_NEW_INPUT.put("3_old_input", false); + + SetupUtils setupUtils = Mockito.mock(SetupUtils.class); + Mockito.when(setupUtils.isNewInput(Matchers.anyString())).thenAnswer( + new Answer() { + @Override + public Boolean answer(InvocationOnMock invocation) throws Throwable { + String inputId = (String) invocation.getArguments()[0]; + return INPUT_ID_TO_NEW_INPUT.get(inputId); + } + } + ); + TvInputManagerHelper inputManager = Mockito.mock(TvInputManagerHelper.class); + Mockito.when(inputManager.getDefaultTvInputInfoComparator()).thenReturn( + new Comparator() { + @Override + public int compare(TvInputInfo lhs, TvInputInfo rhs) { + return lhs.getId().compareTo(rhs.getId()); + } + } + ); + SetupView.TvInputInfoComparator comparator = + new SetupView.TvInputInfoComparator(setupUtils, inputManager); + ComparatorTester comparatorTester = + ComparatorTester.withoutEqualsTest(comparator); + ResolveInfo resolveInfo = TestUtils.createResolveInfo("test", "test"); + for (String id : INPUT_ID_TO_NEW_INPUT.keySet()) { + // Put mock resolveInfo to prevent NPE in {@link TvInputInfo#toString} + TvInputInfo info1 = TestUtils.createTvInputInfo( + resolveInfo, id, "test1", TvInputInfo.TYPE_TUNER, false); + TvInputInfo info2 = TestUtils.createTvInputInfo( + resolveInfo, id, "test2", TvInputInfo.TYPE_DISPLAY_PORT, true); + TvInputInfo info3 = TestUtils.createTvInputInfo( + resolveInfo, id, "test", TvInputInfo.TYPE_HDMI, true); + comparatorTester.addComparableGroup(info1, info2, info3); + } + comparatorTester.test(); + } +} diff --git a/tests/unit/src/com/android/tv/util/FakeClock.java b/tests/unit/src/com/android/tv/util/FakeClock.java new file mode 100644 index 00000000..a4ab2e4f --- /dev/null +++ b/tests/unit/src/com/android/tv/util/FakeClock.java @@ -0,0 +1,34 @@ +/* + * 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.util; + +public class FakeClock implements Clock { + private long mCurrentTimeMillis; + + public FakeClock() { + mCurrentTimeMillis = System.currentTimeMillis(); + } + + public void setCurrentTimeMillis(long time) { + mCurrentTimeMillis = time; + } + + @Override + public long currentTimeMillis() { + return mCurrentTimeMillis; + } +} diff --git a/tests/unit/src/com/android/tv/util/ImageCacheTest.java b/tests/unit/src/com/android/tv/util/ImageCacheTest.java new file mode 100644 index 00000000..a73b79fe --- /dev/null +++ b/tests/unit/src/com/android/tv/util/ImageCacheTest.java @@ -0,0 +1,75 @@ +/* + * 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.util; + +import static com.android.tv.util.BitmapUtils.createScaledBitmapInfo; + +import android.graphics.Bitmap; + +import com.android.tv.util.BitmapUtils.ScaledBitmapInfo; + +import junit.framework.TestCase; + +/** + * Tests for {@link ImageCache}. + */ +public class ImageCacheTest extends TestCase { + + private static final Bitmap ORIG = Bitmap.createBitmap(100, 100, Bitmap.Config.RGB_565); + + private static final String KEY = "same"; + private static final ScaledBitmapInfo INFO_200 = createScaledBitmapInfo(KEY, ORIG, 200, 200); + private static final ScaledBitmapInfo INFO_100 = createScaledBitmapInfo(KEY, ORIG, 100, 100); + private static final ScaledBitmapInfo INFO_50 = createScaledBitmapInfo(KEY, ORIG, 50, 50); + private static final ScaledBitmapInfo INFO_25 = createScaledBitmapInfo(KEY, ORIG, 25, 25); + + private ImageCache mImageCache; + + @Override + protected void setUp() throws Exception { + super.setUp(); + mImageCache = ImageCache.newInstance(0.1f); + } + + //TODO: Empty the cache in the setup. Try using @VisibleForTesting + + public void testPutIfLarger_smaller() throws Exception { + + mImageCache.putIfNeeded( INFO_50); + assertSame("before", INFO_50, mImageCache.get(KEY)); + + mImageCache.putIfNeeded( INFO_25); + assertSame("after", INFO_50, mImageCache.get(KEY)); + } + + public void testPutIfLarger_larger() throws Exception { + mImageCache.putIfNeeded( INFO_50); + assertSame("before", INFO_50, mImageCache.get(KEY)); + + mImageCache.putIfNeeded(INFO_100); + assertSame("after", INFO_100, mImageCache.get(KEY)); + } + + public void testPutIfLarger_alreadyMax() throws Exception { + + mImageCache.putIfNeeded( INFO_100); + assertSame("before", INFO_100, mImageCache.get(KEY)); + + mImageCache.putIfNeeded( INFO_200); + assertSame("after", INFO_100, mImageCache.get(KEY)); + } +} diff --git a/tests/unit/src/com/android/tv/util/ScaledBitmapInfoTest.java b/tests/unit/src/com/android/tv/util/ScaledBitmapInfoTest.java new file mode 100644 index 00000000..ef707470 --- /dev/null +++ b/tests/unit/src/com/android/tv/util/ScaledBitmapInfoTest.java @@ -0,0 +1,52 @@ +package com.android.tv.util; + +import android.graphics.Bitmap; +import android.test.AndroidTestCase; + +import com.android.tv.util.BitmapUtils.ScaledBitmapInfo; + +/** + * Tests for {@link ScaledBitmapInfo}. + */ +public class ScaledBitmapInfoTest extends AndroidTestCase { + + private static final Bitmap B80x100 = Bitmap.createBitmap(80, 100, Bitmap.Config.RGB_565); + private static final Bitmap B960x1440 = Bitmap.createBitmap(960, 1440, Bitmap.Config.RGB_565); + + public void testSize_B100x100to50x50() { + ScaledBitmapInfo actual = BitmapUtils.createScaledBitmapInfo("B80x100", B80x100, 50, 50); + assertScaledBitmapSize(2, 40, 50, actual); + } + + public void testNeedsToReload_B100x100to50x50() { + ScaledBitmapInfo actual = BitmapUtils.createScaledBitmapInfo("B80x100", B80x100, 50, 50); + assertNeedsToReload(false, actual, 25, 25); + assertNeedsToReload(false, actual, 50, 50); + assertNeedsToReload(false, actual, 99, 99); + assertNeedsToReload(true, actual, 100, 100); + assertNeedsToReload(true, actual, 101, 101); + } + + /** + * Reproduces b/20488453. + */ + public void testBug20488453() { + ScaledBitmapInfo actual = BitmapUtils + .createScaledBitmapInfo("B960x1440", B960x1440, 284, 160); + assertScaledBitmapSize(8, 107, 160, actual); + assertNeedsToReload(false, actual, 284, 160); + } + + private static void assertNeedsToReload(boolean expected, ScaledBitmapInfo scaledBitmap, + int reqWidth, int reqHeight) { + assertEquals(scaledBitmap.id + " needToReload(" + reqWidth + "," + reqHeight + ")", + expected, scaledBitmap.needToReload(reqWidth, reqHeight)); + } + + private static void assertScaledBitmapSize(int expectedInSampleSize, int expectedWidth, + int expectedHeight, ScaledBitmapInfo actual) { + assertEquals(actual.id + " inSampleSize", expectedInSampleSize, actual.inSampleSize); + assertEquals(actual.id + " width", expectedWidth, actual.bitmap.getWidth()); + assertEquals(actual.id + " height", expectedHeight, actual.bitmap.getHeight()); + } +} diff --git a/tests/unit/src/com/android/tv/util/TestUtils.java b/tests/unit/src/com/android/tv/util/TestUtils.java new file mode 100644 index 00000000..872e8c51 --- /dev/null +++ b/tests/unit/src/com/android/tv/util/TestUtils.java @@ -0,0 +1,65 @@ +/* + * 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.util; + +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.media.tv.TvInputInfo; + +import com.android.tv.common.TvCommonConstants; + +import java.lang.reflect.Constructor; + +/** + * A class that includes convenience methods for testing. + */ +public class TestUtils { + public static TvInputInfo createTvInputInfo(ResolveInfo service, String id, String parentId, + int type, boolean isHardwareInput) throws Exception { + // Create a mock TvInputInfo by using private constructor + // TODO: Find better way to mock TvInputInfo. + // Note that mockito doesn't support mock/spy on final object. + if (!TvCommonConstants.IS_MNC_PREVIEW && !TvCommonConstants.IS_MNC_OR_HIGHER) { + return createTvInputInfoForLmp(service, id, parentId, type); + } + return createTvInputInfoForMnc(service, id, parentId, type, isHardwareInput); + } + + private static TvInputInfo createTvInputInfoForLmp(ResolveInfo service, String id, + String parentId, int type) throws Exception { + Constructor constructor = TvInputInfo.class.getDeclaredConstructor(new Class[]{ + ResolveInfo.class, String.class, String.class, int.class}); + constructor.setAccessible(true); + return constructor.newInstance(service, id, parentId, type); + } + + private static TvInputInfo createTvInputInfoForMnc(ResolveInfo service, String id, + String parentId, int type, boolean isHardwareInput) throws Exception { + Constructor constructor = TvInputInfo.class.getDeclaredConstructor(new Class[]{ + ResolveInfo.class, String.class, String.class, int.class, boolean.class}); + constructor.setAccessible(true); + return constructor.newInstance(service, id, parentId, type, isHardwareInput); + } + + public static ResolveInfo createResolveInfo(String packageName, String name) { + ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.serviceInfo = new ServiceInfo(); + resolveInfo.serviceInfo.packageName = packageName; + resolveInfo.serviceInfo.name = name; + return resolveInfo; + } +} diff --git a/tests/unit/src/com/android/tv/util/TvInputManagerHelperTest.java b/tests/unit/src/com/android/tv/util/TvInputManagerHelperTest.java new file mode 100644 index 00000000..6251d1a3 --- /dev/null +++ b/tests/unit/src/com/android/tv/util/TvInputManagerHelperTest.java @@ -0,0 +1,72 @@ +/* + * 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.util; + +import android.content.pm.ResolveInfo; +import android.media.tv.TvInputInfo; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; + +import com.android.tv.testing.ComparatorTester; + +import java.util.LinkedHashMap; + +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +/** + * Test for {@link TvInputManagerHelper} + */ +@SmallTest +public class TvInputManagerHelperTest extends AndroidTestCase { + public void testComparator() throws Exception { + final LinkedHashMap INPUT_ID_TO_PARTNER_INPUT = new LinkedHashMap<>(); + INPUT_ID_TO_PARTNER_INPUT.put("2_partner_input", true); + INPUT_ID_TO_PARTNER_INPUT.put("3_partner_input", true); + INPUT_ID_TO_PARTNER_INPUT.put("1_3rd_party_input", false); + INPUT_ID_TO_PARTNER_INPUT.put("4_3rd_party_input", false); + + TvInputManagerHelper manager = Mockito.mock(TvInputManagerHelper.class); + Mockito.doAnswer(new Answer() { + @Override + public Boolean answer(InvocationOnMock invocation) throws Throwable { + TvInputInfo info = (TvInputInfo) invocation.getArguments()[0]; + return INPUT_ID_TO_PARTNER_INPUT.get(info.getId()); + } + }).when(manager).isPartnerInput(Mockito.any()); + Mockito.doAnswer(new Answer() { + @Override + public String answer(InvocationOnMock invocation) throws Throwable { + TvInputInfo info = (TvInputInfo) invocation.getArguments()[0]; + return info.getId(); + } + }).when(manager).loadLabel(Mockito.any()); + + ComparatorTester comparatorTester = + ComparatorTester.withoutEqualsTest( + new TvInputManagerHelper.TvInputInfoComparator(manager)); + ResolveInfo resolveInfo1 = TestUtils.createResolveInfo("1_test", "1_test"); + ResolveInfo resolveInfo2 = TestUtils.createResolveInfo("2_test", "2_test"); + for (String inputId : INPUT_ID_TO_PARTNER_INPUT.keySet()) { + TvInputInfo info1 = TestUtils.createTvInputInfo(resolveInfo1, inputId, null, 0, false); + TvInputInfo info2 = TestUtils.createTvInputInfo(resolveInfo2, inputId, null, 0, false); + comparatorTester.addComparableGroup(info1, info2); + } + comparatorTester.test(); + } +} diff --git a/tests/unit/src/com/android/tv/util/TvTrackInfoUtilsTest.java b/tests/unit/src/com/android/tv/util/TvTrackInfoUtilsTest.java new file mode 100644 index 00000000..b657f49c --- /dev/null +++ b/tests/unit/src/com/android/tv/util/TvTrackInfoUtilsTest.java @@ -0,0 +1,98 @@ +/* + * 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.util; + +import static com.android.tv.util.TvTrackInfoUtils.getBestTrackInfo; + +import android.media.tv.TvTrackInfo; +import android.test.suitebuilder.annotation.SmallTest; + +import com.android.tv.testing.ComparatorTester; + +import junit.framework.TestCase; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * Tests for {@link com.android.tv.util.TvTrackInfoUtils}. + */ +@SmallTest +public class TvTrackInfoUtilsTest extends TestCase { + + private static final String UN_MATCHED_ID = "no matching ID"; + + private static final TvTrackInfo INFO_1_EN_1 = create("1", "en", 1); + + private static final TvTrackInfo INFO_2_EN_5 = create("2", "en", 5); + + private static final TvTrackInfo INFO_3_FR_5 = create("3", "fr", 5); + + private static TvTrackInfo create(String id, String fr, int audioChannelCount) { + return new TvTrackInfo.Builder(TvTrackInfo.TYPE_AUDIO, id) + .setLanguage(fr) + .setAudioChannelCount(audioChannelCount) + .build(); + } + + private static final List ALL = Arrays.asList(INFO_1_EN_1, INFO_2_EN_5, INFO_3_FR_5); + + public void testGetBestTrackInfo_empty() { + TvTrackInfo result = getBestTrackInfo(Collections.emptyList(), + UN_MATCHED_ID, "en", 1); + assertEquals("best track ", null, result); + } + + public void testGetBestTrackInfo_exactMatch() { + TvTrackInfo result = getBestTrackInfo(ALL, "1", "en", 1); + assertEquals("best track ", INFO_1_EN_1, result); + } + + public void testGetBestTrackInfo_langAndChannelCountMatch() { + TvTrackInfo result = getBestTrackInfo(ALL, UN_MATCHED_ID, "en", 5); + assertEquals("best track ", INFO_2_EN_5, result); + } + + public void testGetBestTrackInfo_languageOnlyMatch() { + TvTrackInfo result = getBestTrackInfo(ALL, UN_MATCHED_ID, "fr", 1); + assertEquals("best track ", INFO_3_FR_5, result); + } + + public void testGetBestTrackInfo_noMatches() { + TvTrackInfo result = getBestTrackInfo(ALL, UN_MATCHED_ID, "kr", 1); + assertEquals("best track ", INFO_1_EN_1, result); + } + + + public void testComparator() { + Comparator comparator = TvTrackInfoUtils.createComparator("1", "en", 1); + ComparatorTester.withoutEqualsTest(comparator) + // lang not match + .addComparableGroup(create("1", "kr", 1), create("2", "kr", 2), + create("1", "ja", 1), + create("1", "ch", 1)) + // lang match not count match + .addComparableGroup(create("2", "en", 2), create("3", "en", 3), + create("1", "en", 2)) + // lang and count match + .addComparableGroup(create("2", "en", 1), create("3", "en", 1)) + // all match + .addComparableGroup(create("1", "en", 1), create("1", "en", 1)) + .test(); + } +} diff --git a/tests/unit/src/com/android/tv/util/UtilsTest_GetDurationString.java b/tests/unit/src/com/android/tv/util/UtilsTest_GetDurationString.java new file mode 100644 index 00000000..1cdda744 --- /dev/null +++ b/tests/unit/src/com/android/tv/util/UtilsTest_GetDurationString.java @@ -0,0 +1,250 @@ +/* + * 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.util; + +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; + +import android.text.format.DateUtils; + +import java.lang.Exception; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.Locale; + +/** + * Tests for {@link com.android.tv.util.Utils#getDurationString}. + *

+ * This test uses deprecated flags {@link DateUtils#FORMAT_12HOUR} and + * {@link DateUtils#FORMAT_24HOUR} to run this test independent to system's 12/24h format. + * Note that changing system setting requires permission android.permission.WRITE_SETTINGS + * and it should be defined in TV app, not this test. + */ +@SmallTest +public class UtilsTest_GetDurationString extends AndroidTestCase { + // TODO: Mock Context so we can specify current time and locale for test. + private Locale mLocale; + private static final long DATE_2015_2_1_MS = getFeb2015InMillis(1, 0, 0); + + // All possible list for a paramter to test parameter independent result. + private static final boolean[] PARAM_USE_SHORT_FORMAT = {false, true}; + + @Override + protected void setUp() throws Exception { + super.setUp(); + // Set locale to US + mLocale = Locale.getDefault(); + Locale.setDefault(Locale.US); + } + + /** + * Return time in millis assuming that whose year is 2015 and month is Jan. + */ + private static long getJan2015InMillis(int date, int hour, int minutes) { + return new GregorianCalendar( + 2015, Calendar.JANUARY, date, hour, minutes).getTimeInMillis(); + } + + private static long getJan2015InMillis(int date, int hour) { + return getJan2015InMillis(date, hour, 0); + } + + /** + * Return time in millis assuming that whose year is 2015 and month is Feb. + */ + private static long getFeb2015InMillis(int date, int hour, int minutes) { + return new GregorianCalendar( + 2015, Calendar.FEBRUARY, date, hour, minutes).getTimeInMillis(); + } + + private static long getFeb2015InMillis(int date, int hour) { + return getFeb2015InMillis(date, hour, 0); + } + + public void testSameDateAndTime() { + assertEquals("3:00 AM", + Utils.getDurationString(getContext(), DATE_2015_2_1_MS, + getFeb2015InMillis(1, 3), getFeb2015InMillis(1, 3), false, + DateUtils.FORMAT_12HOUR)); + assertEquals("03:00", + Utils.getDurationString(getContext(), DATE_2015_2_1_MS, + getFeb2015InMillis(1, 3), getFeb2015InMillis(1, 3), false, + DateUtils.FORMAT_24HOUR)); + } + + public void testDurationWithinToday() { + assertEquals("12:00 – 3:00 AM", + Utils.getDurationString(getContext(), DATE_2015_2_1_MS, + DATE_2015_2_1_MS, getFeb2015InMillis(1, 3), false, + DateUtils.FORMAT_12HOUR)); + assertEquals("00:00 – 03:00", + Utils.getDurationString(getContext(), DATE_2015_2_1_MS, + DATE_2015_2_1_MS, getFeb2015InMillis(1, 3), false, + DateUtils.FORMAT_24HOUR)); + } + + public void testDurationFromYesterdayToToday() { + assertEquals("Jan 31, 3:00 AM – Feb 1, 4:00 AM", + Utils.getDurationString(getContext(), DATE_2015_2_1_MS, + getJan2015InMillis(31, 3), getFeb2015InMillis(1, 4), false, + DateUtils.FORMAT_12HOUR)); + assertEquals("Jan 31, 03:00 – Feb 1, 04:00", + Utils.getDurationString(getContext(), DATE_2015_2_1_MS, + getJan2015InMillis(31, 3), getFeb2015InMillis(1, 4), false, + DateUtils.FORMAT_24HOUR)); + assertEquals("1/31, 11:30 PM – 12:30 AM", + Utils.getDurationString(getContext(), DATE_2015_2_1_MS, + getJan2015InMillis(31, 23, 30), getFeb2015InMillis(1, 0, 30), true, + DateUtils.FORMAT_12HOUR)); + assertEquals("1/31, 23:30 – 00:30", + Utils.getDurationString(getContext(), DATE_2015_2_1_MS, + getJan2015InMillis(31, 23, 30), getFeb2015InMillis(1, 0, 30), true, + DateUtils.FORMAT_24HOUR)); + } + + public void testDurationFromTodayToTomorrow() { + assertEquals("Feb 1, 3:00 AM – Feb 2, 4:00 AM", + Utils.getDurationString(getContext(), DATE_2015_2_1_MS, + getFeb2015InMillis(1, 3), getFeb2015InMillis(2, 4), false, + DateUtils.FORMAT_12HOUR)); + assertEquals("Feb 1, 03:00 – Feb 2, 04:00", + Utils.getDurationString(getContext(), DATE_2015_2_1_MS, + getFeb2015InMillis(1, 3), getFeb2015InMillis(2, 4), false, + DateUtils.FORMAT_24HOUR)); + assertEquals("2/1, 3:00 AM – 2/2, 4:00 AM", + Utils.getDurationString(getContext(), DATE_2015_2_1_MS, + getFeb2015InMillis(1, 3), getFeb2015InMillis(2, 4), true, + DateUtils.FORMAT_12HOUR)); + assertEquals("2/1, 03:00 – 2/2, 04:00", + Utils.getDurationString(getContext(), DATE_2015_2_1_MS, + getFeb2015InMillis(1, 3), getFeb2015InMillis(2, 4), true, + DateUtils.FORMAT_24HOUR)); + + assertEquals("Feb 1, 11:30 PM – Feb 2, 12:30 AM", + Utils.getDurationString(getContext(), DATE_2015_2_1_MS, + getFeb2015InMillis(1, 23, 30), getFeb2015InMillis(2, 0, 30), false, + DateUtils.FORMAT_12HOUR)); + assertEquals("Feb 1, 23:30 – Feb 2, 00:30", + Utils.getDurationString(getContext(), DATE_2015_2_1_MS, + getFeb2015InMillis(1, 23, 30), getFeb2015InMillis(2, 0, 30), false, + DateUtils.FORMAT_24HOUR)); + assertEquals("11:30 PM – 12:30 AM", + Utils.getDurationString(getContext(), DATE_2015_2_1_MS, + getFeb2015InMillis(1, 23, 30), getFeb2015InMillis(2, 0, 30), true, + DateUtils.FORMAT_12HOUR)); + assertEquals("23:30 – 00:30", + Utils.getDurationString(getContext(), DATE_2015_2_1_MS, + getFeb2015InMillis(1, 23, 30), getFeb2015InMillis(2, 0, 30), true, + DateUtils.FORMAT_24HOUR)); + } + + public void testDurationWithinTomorrow() { + assertEquals("Feb 2, 2:00 – 4:00 AM", + Utils.getDurationString(getContext(), DATE_2015_2_1_MS, + getFeb2015InMillis(2, 2), getFeb2015InMillis(2, 4), false, + DateUtils.FORMAT_12HOUR)); + assertEquals("Feb 2, 02:00 – 04:00", + Utils.getDurationString(getContext(), DATE_2015_2_1_MS, + getFeb2015InMillis(2, 2), getFeb2015InMillis(2, 4), false, + DateUtils.FORMAT_24HOUR)); + assertEquals("2/2, 2:00 – 4:00 AM", + Utils.getDurationString(getContext(), DATE_2015_2_1_MS, + getFeb2015InMillis(2, 2), getFeb2015InMillis(2, 4), true, + DateUtils.FORMAT_12HOUR)); + assertEquals("2/2, 02:00 – 04:00", + Utils.getDurationString(getContext(), DATE_2015_2_1_MS, + getFeb2015InMillis(2, 2), getFeb2015InMillis(2, 4), true, + DateUtils.FORMAT_24HOUR)); + } + + public void testStartOfDay() { + assertEquals("12:00 – 1:00 AM", + Utils.getDurationString(getContext(), DATE_2015_2_1_MS, + DATE_2015_2_1_MS, getFeb2015InMillis(1, 1), false, + DateUtils.FORMAT_12HOUR)); + assertEquals("00:00 – 01:00", + Utils.getDurationString(getContext(), DATE_2015_2_1_MS, + DATE_2015_2_1_MS, getFeb2015InMillis(1, 1), false, + DateUtils.FORMAT_24HOUR)); + + assertEquals("Feb 2, 12:00 – 1:00 AM", + Utils.getDurationString(getContext(), DATE_2015_2_1_MS, + getFeb2015InMillis(2, 0), getFeb2015InMillis(2, 1), false, + DateUtils.FORMAT_12HOUR)); + assertEquals("Feb 2, 00:00 – 01:00", + Utils.getDurationString(getContext(), DATE_2015_2_1_MS, + getFeb2015InMillis(2, 0), getFeb2015InMillis(2, 1), false, + DateUtils.FORMAT_24HOUR)); + assertEquals("2/2, 12:00 – 1:00 AM", + Utils.getDurationString(getContext(), DATE_2015_2_1_MS, + getFeb2015InMillis(2, 0), getFeb2015InMillis(2, 1), true, + DateUtils.FORMAT_12HOUR)); + assertEquals("2/2, 00:00 – 01:00", + Utils.getDurationString(getContext(), DATE_2015_2_1_MS, + getFeb2015InMillis(2, 0), getFeb2015InMillis(2, 1), true, + DateUtils.FORMAT_24HOUR)); + } + + public void testEndOfDay() { + for (boolean useShortFormat : PARAM_USE_SHORT_FORMAT) { + assertEquals("11:00 PM – 12:00 AM", + Utils.getDurationString(getContext(), DATE_2015_2_1_MS, + getFeb2015InMillis(1, 23), getFeb2015InMillis(2, 0), useShortFormat, + DateUtils.FORMAT_12HOUR)); + assertEquals("23:00 – 00:00", + Utils.getDurationString(getContext(), DATE_2015_2_1_MS, + getFeb2015InMillis(1, 23), getFeb2015InMillis(2, 0), useShortFormat, + DateUtils.FORMAT_24HOUR)); + } + + assertEquals("Feb 2, 11:00 PM – 12:00 AM", + Utils.getDurationString(getContext(), DATE_2015_2_1_MS, + getFeb2015InMillis(2, 23), getFeb2015InMillis(3, 0), false, + DateUtils.FORMAT_12HOUR)); + assertEquals("Feb 2, 23:00 – 00:00", + Utils.getDurationString(getContext(), DATE_2015_2_1_MS, + getFeb2015InMillis(2, 23), getFeb2015InMillis(3, 0), false, + DateUtils.FORMAT_24HOUR)); + assertEquals("2/2, 11:00 PM – 12:00 AM", + Utils.getDurationString(getContext(), DATE_2015_2_1_MS, + getFeb2015InMillis(2, 23), getFeb2015InMillis(3, 0), true, + DateUtils.FORMAT_12HOUR)); + assertEquals("2/2, 23:00 – 00:00", + Utils.getDurationString(getContext(), DATE_2015_2_1_MS, + getFeb2015InMillis(2, 23), getFeb2015InMillis(3, 0), true, + DateUtils.FORMAT_24HOUR)); + } + + public void testMidnight() { + for (boolean useShortFormat : PARAM_USE_SHORT_FORMAT) { + assertEquals("12:00 AM", + Utils.getDurationString(getContext(), DATE_2015_2_1_MS, + DATE_2015_2_1_MS, DATE_2015_2_1_MS, useShortFormat, + DateUtils.FORMAT_12HOUR)); + assertEquals("00:00", + Utils.getDurationString(getContext(), DATE_2015_2_1_MS, + DATE_2015_2_1_MS, DATE_2015_2_1_MS, useShortFormat, + DateUtils.FORMAT_24HOUR)); + } + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + // Revive system locale. + Locale.setDefault(mLocale); + } +} diff --git a/tests/unit/src/com/android/tv/util/UtilsTest_IsInGivenDay.java b/tests/unit/src/com/android/tv/util/UtilsTest_IsInGivenDay.java new file mode 100644 index 00000000..160a2231 --- /dev/null +++ b/tests/unit/src/com/android/tv/util/UtilsTest_IsInGivenDay.java @@ -0,0 +1,61 @@ +/* + * 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.util; + +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; + +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +/** + * Tests for {@link com.android.tv.util.Utils#isInGivenDay}. + */ +@SmallTest +public class UtilsTest_IsInGivenDay extends AndroidTestCase { + public void testIsInGivenDay() { + assertTrue(Utils.isInGivenDay( + new GregorianCalendar(2015, Calendar.JANUARY, 1).getTimeInMillis(), + new GregorianCalendar(2015, Calendar.JANUARY, 1, 0, 30).getTimeInMillis())); + } + + public void testIsNotInGivenDay() { + assertFalse(Utils.isInGivenDay( + new GregorianCalendar(2015, Calendar.JANUARY, 1).getTimeInMillis(), + new GregorianCalendar(2015, Calendar.JANUARY, 2).getTimeInMillis())); + } + + public void testIfTimeZoneApplied() { + TimeZone timeZone = TimeZone.getDefault(); + + TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul")); + + // 2015.01.01 00:00 in KST = 2014.12.31 15:00 in UTC + long date2015StartMs = + new GregorianCalendar(2015, Calendar.JANUARY, 1).getTimeInMillis(); + + // 2015.01.01 10:00 in KST = 2015.01.01 01:00 in UTC + long date2015Start10AMMs = + new GregorianCalendar(2015, Calendar.JANUARY, 1, 10, 0).getTimeInMillis(); + + // Those two times aren't in the same day in UTC, but they are in KST. + assertTrue(Utils.isInGivenDay(date2015StartMs, date2015Start10AMMs)); + + TimeZone.setDefault(timeZone); + } +} -- cgit v1.2.3