aboutsummaryrefslogtreecommitdiff
path: root/tests/unit/src/com/android
diff options
context:
space:
mode:
authorNick Chalko <nchalko@google.com>2015-08-03 15:39:56 -0700
committerNick Chalko <nchalko@google.com>2015-08-03 15:53:37 -0700
commit816a4be1a0f34f6a48877c8afd3dbbca19eac435 (patch)
tree4f18dda269764494942f5313acc93db4a35d47db /tests/unit/src/com/android
parent6edd2b09e5d16a29c703a5fcbd2e88c5cf5e55b7 (diff)
downloadTV-816a4be1a0f34f6a48877c8afd3dbbca19eac435.tar.gz
Migrate Live Channels App Src to AOSP branch
Bug: 21625152 Change-Id: I07e2830b27440556dc757e6340b4f77d1c0cbc66
Diffstat (limited to 'tests/unit/src/com/android')
-rw-r--r--tests/unit/src/com/android/tv/BaseMainActivityTestCase.java135
-rw-r--r--tests/unit/src/com/android/tv/CurrentPositionMediatorTest.java79
-rw-r--r--tests/unit/src/com/android/tv/MainActivityTest.java106
-rw-r--r--tests/unit/src/com/android/tv/TimeShiftManagerTest.java100
-rw-r--r--tests/unit/src/com/android/tv/data/ChannelDataManagerTest.java646
-rw-r--r--tests/unit/src/com/android/tv/data/ChannelNumberTest.java87
-rw-r--r--tests/unit/src/com/android/tv/data/ChannelTest.java222
-rw-r--r--tests/unit/src/com/android/tv/data/ProgramDataManagerTest.java533
-rw-r--r--tests/unit/src/com/android/tv/data/ProgramTest.java98
-rw-r--r--tests/unit/src/com/android/tv/menu/TvOptionsRowAdapterTest.java109
-rw-r--r--tests/unit/src/com/android/tv/recommendation/ChannelRecordTest.java118
-rw-r--r--tests/unit/src/com/android/tv/recommendation/EvaluatorTestCase.java128
-rw-r--r--tests/unit/src/com/android/tv/recommendation/FavoriteChannelEvaluatorTest.java144
-rw-r--r--tests/unit/src/com/android/tv/recommendation/RecentChannelEvaluatorTest.java140
-rw-r--r--tests/unit/src/com/android/tv/recommendation/RecommendationUtils.java180
-rw-r--r--tests/unit/src/com/android/tv/recommendation/RecommenderTest.java324
-rw-r--r--tests/unit/src/com/android/tv/recommendation/RoutineWatchEvaluatorTest.java205
-rw-r--r--tests/unit/src/com/android/tv/tests/TvActivityTest.java34
-rw-r--r--tests/unit/src/com/android/tv/ui/SetupViewTest.java86
-rw-r--r--tests/unit/src/com/android/tv/util/FakeClock.java34
-rw-r--r--tests/unit/src/com/android/tv/util/ImageCacheTest.java75
-rw-r--r--tests/unit/src/com/android/tv/util/ScaledBitmapInfoTest.java52
-rw-r--r--tests/unit/src/com/android/tv/util/TestUtils.java65
-rw-r--r--tests/unit/src/com/android/tv/util/TvInputManagerHelperTest.java72
-rw-r--r--tests/unit/src/com/android/tv/util/TvTrackInfoUtilsTest.java98
-rw-r--r--tests/unit/src/com/android/tv/util/UtilsTest_GetDurationString.java250
-rw-r--r--tests/unit/src/com/android/tv/util/UtilsTest_IsInGivenDay.java61
27 files changed, 4181 insertions, 0 deletions
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<MainActivity> {
+ 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<MainActivity> 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<Channel> 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<Channel> 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<ChannelBannerView> 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<ChannelInfo> channelInfoList = new ArrayList<>();
+ for (int i = 1; i <= Constants.UNIT_TEST_CHANNEL_COUNT; i++) {
+ channelInfoList.add(ChannelInfo.create(getContext(), i));
+ }
+ List<Channel> 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<Channel> channelList = new ArrayList<>(mChannelDataManager.getChannelList());
+ List<Channel> 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<Channel> 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<Channel> 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<ChannelInfoWrapper> 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<Long> 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<Channel> removedChannels = new ArrayList<>();
+ public List<Channel> 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<ChannelNumber>()
+ .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<ActivityInfo>() {
+ 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.<ComponentName>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<Boolean>() {
+ @Override
+ public Boolean answer(InvocationOnMock invocation) throws Throwable {
+ String inputId = (String) invocation.getArguments()[0];
+ return PARTNER_INPUT_ID.equals(inputId);
+ }
+ });
+ Comparator<Channel> comparator = new TestChannelComparator(manager);
+ ComparatorTester<Channel> 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.
+ *
+ * <p>
+ * {@link ProgramDataManager#getCurrentProgram(long)},
+ * {@link ProgramDataManager#getPrograms(long, long)},
+ * {@link ProgramDataManager#setPrefetchTimeRange(long)}.
+ * </p>
+ */
+ public void testGetPrograms() throws Exception {
+ // Initial setup to test {@link ProgramDataManager#setPrefetchTimeRange(long)}.
+ long preventSnapDelayMs = ProgramDataManager.PROGRAM_GUIDE_SNAP_TIME_MS * 2;
+ long prefetchTimeRangeStartMs = System.currentTimeMillis() + preventSnapDelayMs;
+ mClock.setCurrentTimeMillis(prefetchTimeRangeStartMs + preventSnapDelayMs);
+ mProgramDataManager.setPrefetchTimeRange(prefetchTimeRangeStartMs);
+
+ startAndWaitForComplete();
+
+ for (long channelId = 1; channelId <= Constants.UNIT_TEST_CHANNEL_COUNT; channelId++) {
+ Program currentProgram = mProgramDataManager.getCurrentProgram(channelId);
+ // Test {@link ProgramDataManager#getCurrentProgram(long)}.
+ assertTrue(currentProgram.getStartTimeUtcMillis() <= mClock.currentTimeMillis()
+ && mClock.currentTimeMillis() <= currentProgram.getEndTimeUtcMillis());
+
+ // Test {@link ProgramDataManager#getPrograms(long)}.
+ // Case #1: Normal case
+ List<Program> programs =
+ mProgramDataManager.getPrograms(channelId, mClock.currentTimeMillis());
+ ProgramInfo stub = ProgramInfo.create();
+ int index = stub.getIndex(mClock.currentTimeMillis(), channelId);
+ for (Program program : programs) {
+ ProgramInfo programInfoAt = stub.build(getContext(), index);
+ long startTimeMs = stub.getStartTimeMs(index, channelId);
+ assertTrue(program.toString() + " differ from " + programInfoAt,
+ equals(programInfoAt, startTimeMs, program));
+ index++;
+ }
+ // Case #2: Corner cases where there's a program that starts at the start of the range.
+ long startTimeMs = programs.get(0).getStartTimeUtcMillis();
+ programs = mProgramDataManager.getPrograms(channelId, startTimeMs);
+ assertEquals(startTimeMs, programs.get(0).getStartTimeUtcMillis());
+
+ // Test {@link ProgramDataManager#setPrefetchTimeRange(long)}.
+ programs = mProgramDataManager.getPrograms(channelId,
+ prefetchTimeRangeStartMs - TimeUnit.HOURS.toMillis(1));
+ for (Program program : programs) {
+ assertTrue(program.getEndTimeUtcMillis() >= prefetchTimeRangeStartMs);
+ }
+ }
+ }
+
+ /**
+ * Test for following methods.
+ *
+ * <p>
+ * {@link ProgramDataManager#addOnCurrentProgramUpdatedListener},
+ * {@link ProgramDataManager#removeOnCurrentProgramUpdatedListener}.
+ * </p>
+ */
+ public void testCurrentProgramListener() throws Exception {
+ final long testChannelId = 1;
+ ProgramInfo stub = ProgramInfo.create();
+ int index = stub.getIndex(mClock.currentTimeMillis(), testChannelId);
+ // Set current time to few seconds before the current program ends,
+ // so we can see if callback is called as expected.
+ long nextProgramStartTimeMs = stub.getStartTimeMs(index + 1, testChannelId);
+ ProgramInfo nextProgramInfo = stub.build(getContext(), index + 1);
+ mClock.setCurrentTimeMillis(nextProgramStartTimeMs - (WAIT_TIME_OUT_MS / 2));
+
+ startAndWaitForComplete();
+ // Note that changing current time doesn't affect the current program
+ // because current program is updated after waiting for the program's duration.
+ // See {@link ProgramDataManager#updateCurrentProgram}.
+ mClock.setCurrentTimeMillis(mClock.currentTimeMillis() + WAIT_TIME_OUT_MS);
+ TestProgramDataManagerOnCurrentProgramUpdatedListener listener =
+ new TestProgramDataManagerOnCurrentProgramUpdatedListener();
+ mProgramDataManager.addOnCurrentProgramUpdatedListener(testChannelId, listener);
+ assertTrue(listener.currentProgramUpdatedLatch.await(WAIT_TIME_OUT_MS,
+ TimeUnit.MILLISECONDS));
+ assertEquals(testChannelId, listener.updatedChannelId);
+ assertTrue(ProgramDataManagerTest.equals(
+ nextProgramInfo, nextProgramStartTimeMs,
+ mProgramDataManager.getCurrentProgram(testChannelId)));
+ assertEquals(listener.updatedProgram, mProgramDataManager.getCurrentProgram(testChannelId));
+ }
+
+ /**
+ * Test if program data is refreshed after the program insertion.
+ */
+ public void testContentProviderUpdate() throws Exception {
+ final long testChannelId = 1;
+ startAndWaitForComplete();
+ // Force program data manager to update program data whenever it's changes.
+ mProgramDataManager.setProgramPrefetchUpdateWait(0);
+ mListener.reset();
+ List<Program> programList =
+ mProgramDataManager.getPrograms(testChannelId, mClock.currentTimeMillis());
+ assertNotNull(programList);
+ long lastProgramEndTime = programList.get(programList.size() - 1).getEndTimeUtcMillis();
+ // Make change in content provider
+ mContentProvider.simulateAppend(testChannelId);
+ assertTrue(mListener.programUpdatedLatch.await(WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS));
+ programList = mProgramDataManager.getPrograms(testChannelId, mClock.currentTimeMillis());
+ assertTrue(lastProgramEndTime
+ < programList.get(programList.size() - 1).getEndTimeUtcMillis());
+ }
+
+ /**
+ * Test for {@link ProgramDataManager#setPauseProgramUpdate(boolean)}.
+ */
+ public void testSetPauseProgramUpdate() throws Exception {
+ final long testChannelId = 1;
+ startAndWaitForComplete();
+ // Force program data manager to update program data whenever it's changes.
+ mProgramDataManager.setProgramPrefetchUpdateWait(0);
+ mListener.reset();
+ mProgramDataManager.setPauseProgramUpdate(true);
+ mContentProvider.simulateAppend(testChannelId);
+ assertFalse(mListener.programUpdatedLatch.await(FAILURE_TIME_OUT_MS,
+ TimeUnit.MILLISECONDS));
+ }
+
+ private class FakeContentResolver extends MockContentResolver {
+ @Override
+ public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) {
+ super.notifyChange(uri, observer, syncToNetwork);
+ if (DEBUG) {
+ Log.d(TAG, "onChanged(uri=" + uri + ")");
+ }
+ if (observer != null) {
+ observer.dispatchChange(false, uri);
+ } else {
+ mProgramDataManager.getContentObserver().dispatchChange(false, uri);
+ }
+ }
+ }
+
+ private static class ProgramInfoWrapper {
+ private final int index;
+ private final long startTimeMs;
+ private final ProgramInfo programInfo;
+ public ProgramInfoWrapper(int index, long startTimeMs, ProgramInfo programInfo) {
+ this.index = index;
+ this.startTimeMs = startTimeMs;
+ this.programInfo = programInfo;
+ }
+ }
+
+ // This implements the minimal methods in content resolver
+ // and detailed assumptions are written in each method.
+ private class FakeContentProvider extends MockContentProvider {
+ private SparseArray<List<ProgramInfoWrapper>> mProgramInfoList = new SparseArray<>();
+
+ /**
+ * Constructor for FakeContentProvider
+ * <p>
+ * This initializes program info assuming that
+ * channel IDs are 1, 2, 3, ... {@link Constants#UNIT_TEST_CHANNEL_COUNT}.
+ * </p>
+ */
+ public FakeContentProvider(Context context) {
+ super(context);
+ long startTimeMs = Utils.floorTime(
+ mClock.currentTimeMillis() - ProgramDataManager.PROGRAM_GUIDE_SNAP_TIME_MS,
+ ProgramDataManager.PROGRAM_GUIDE_SNAP_TIME_MS);
+ long endTimeMs = startTimeMs + (ProgramDataManager.PROGRAM_GUIDE_MAX_TIME_RANGE / 2);
+ for (int i = 1; i <= Constants.UNIT_TEST_CHANNEL_COUNT; i++) {
+ List<ProgramInfoWrapper> programInfoList = new ArrayList<>();
+ ProgramInfo stub = ProgramInfo.create();
+ int index = stub.getIndex(startTimeMs, i);
+ long programStartTimeMs = stub.getStartTimeMs(index, i);
+ while (programStartTimeMs < endTimeMs) {
+ ProgramInfo programAt = stub.build(getContext(), index);
+ programInfoList.add(
+ new ProgramInfoWrapper(index, programStartTimeMs, programAt));
+ index++;
+ programStartTimeMs += programAt.durationMs;
+ }
+ mProgramInfoList.put(i, programInfoList);
+ }
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder) {
+ if (DEBUG) {
+ Log.d(TAG, "dump query");
+ Log.d(TAG, " uri=" + uri);
+ if (projection == null || projection.length == 0) {
+ Log.d(TAG, " projection=" + projection);
+ } else {
+ for (int i = 0; i < projection.length; i++) {
+ Log.d(TAG, " projection=" + projection[i]);
+ }
+ }
+ Log.d(TAG," selection=" + selection);
+ }
+ long startTimeMs = Long.parseLong(uri.getQueryParameter(PARAM_START_TIME));
+ long endTimeMs = Long.parseLong(uri.getQueryParameter(PARAM_END_TIME));
+ if (startTimeMs == 0 || endTimeMs == 0) {
+ throw new UnsupportedOperationException();
+ }
+ assertProgramUri(uri);
+ long channelId;
+ try {
+ channelId = Long.parseLong(uri.getQueryParameter(PARAM_CHANNEL));
+ } catch (NumberFormatException e) {
+ channelId = -1;
+ }
+ return new FakeCursor(projection, channelId, startTimeMs, endTimeMs);
+ }
+
+ /**
+ * Simulate program data appends at the end of the existing programs.
+ * This appends programs until the maximum program query range
+ * ({@link ProgramDataManager#PROGRAM_GUIDE_MAX_TIME_RANGE})
+ * where we started with the inserting half of it.
+ */
+ public void simulateAppend(long channelId) {
+ long endTimeMs =
+ mClock.currentTimeMillis() + ProgramDataManager.PROGRAM_GUIDE_MAX_TIME_RANGE;
+ List<ProgramInfoWrapper> programList = mProgramInfoList.get((int) channelId);
+ if (mProgramInfoList == null) {
+ return;
+ }
+ ProgramInfo stub = ProgramInfo.create();
+ ProgramInfoWrapper last = programList.get(programList.size() - 1);
+ while (last.startTimeMs < endTimeMs) {
+ ProgramInfo nextProgramInfo = stub.build(getContext(), last.index + 1);
+ ProgramInfoWrapper next = new ProgramInfoWrapper(last.index + 1,
+ last.startTimeMs + last.programInfo.durationMs, nextProgramInfo);
+ programList.add(next);
+ last = next;
+ }
+ mContentResolver.notifyChange(TvContract.Programs.CONTENT_URI, null);
+ }
+
+ private void assertProgramUri(Uri uri) {
+ assertTrue("Uri(" + uri + ") isn't channel uri",
+ uri.toString().startsWith(TvContract.Programs.CONTENT_URI.toString()));
+ }
+
+ public ProgramInfoWrapper get(long channelId, int position) {
+ List<ProgramInfoWrapper> programList = mProgramInfoList.get((int) channelId);
+ if (programList == null || position >= programList.size()) {
+ return null;
+ }
+ return programList.get(position);
+ }
+ }
+
+ private class FakeCursor extends MockCursor {
+ private String[] ALL_COLUMNS = {
+ TvContract.Programs.COLUMN_CHANNEL_ID,
+ TvContract.Programs.COLUMN_TITLE,
+ TvContract.Programs.COLUMN_SHORT_DESCRIPTION,
+ TvContract.Programs.COLUMN_EPISODE_TITLE,
+ TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS,
+ TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS};
+ private final String[] mColumns;
+ private final boolean mIsQueryForSingleChannel;
+ private final long mStartTimeMs;
+ private final long mEndTimeMs;
+ private final int mCount;
+ private long mChannelId;
+ private int mProgramPosition;
+ private ProgramInfoWrapper mCurrentProgram;
+
+ /**
+ * Constructor
+ * @param columns the same as projection passed from {@link FakeContentProvider#query}.
+ * Can be null for query all.
+ * @param channelId channel ID to query programs belongs to the specified channel.
+ * Can be negative to indicate all channels.
+ * @param startTimeMs start of the time range to query programs.
+ * @param endTimeMs end of the time range to query programs.
+ */
+ public FakeCursor(String[] columns, long channelId, long startTimeMs, long endTimeMs) {
+ mColumns = (columns == null) ? ALL_COLUMNS : columns;
+ mIsQueryForSingleChannel = (channelId > 0);
+ mChannelId = channelId;
+ mProgramPosition = -1;
+ mStartTimeMs = startTimeMs;
+ mEndTimeMs = endTimeMs;
+ int count = 0;
+ while (moveToNext()) {
+ count++;
+ }
+ mCount = count;
+ // Rewind channel Id and program index.
+ mChannelId = channelId;
+ mProgramPosition = -1;
+ if (DEBUG) {
+ Log.d(TAG, "FakeCursor(columns=" + columns + ", channelId=" + channelId
+ + ", startTimeMs=" + startTimeMs + ", endTimeMs=" + endTimeMs
+ + ") has mCount=" + mCount);
+ }
+ }
+
+ @Override
+ public String getColumnName(int columnIndex) {
+ return mColumns[columnIndex];
+ }
+
+ @Override
+ public int getColumnIndex(String columnName) {
+ for (int i = 0; i < mColumns.length; i++) {
+ if (mColumns[i].equalsIgnoreCase(columnName)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public int getInt(int columnIndex) {
+ if (DEBUG) {
+ Log.d(TAG, "Column (" + getColumnName(columnIndex) + ") is ignored in getInt()");
+ }
+ return 0;
+ }
+
+ @Override
+ public long getLong(int columnIndex) {
+ String columnName = getColumnName(columnIndex);
+ switch (columnName) {
+ case TvContract.Programs.COLUMN_CHANNEL_ID:
+ return mChannelId;
+ case TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS:
+ return mCurrentProgram.startTimeMs;
+ case TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS:
+ return mCurrentProgram.startTimeMs + mCurrentProgram.programInfo.durationMs;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Column (" + columnName + ") is ignored in getLong()");
+ }
+ return 0;
+ }
+
+ @Override
+ public String getString(int columnIndex) {
+ String columnName = getColumnName(columnIndex);
+ switch (columnName) {
+ case TvContract.Programs.COLUMN_TITLE:
+ return mCurrentProgram.programInfo.title;
+ case TvContract.Programs.COLUMN_SHORT_DESCRIPTION:
+ return mCurrentProgram.programInfo.description;
+ case TvContract.Programs.COLUMN_EPISODE_TITLE:
+ return mCurrentProgram.programInfo.episode;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Column (" + columnName + ") is ignored in getString()");
+ }
+ return null;
+ }
+
+ @Override
+ public int getCount() {
+ return mCount;
+ }
+
+ @Override
+ public boolean moveToNext() {
+ while (true) {
+ ProgramInfoWrapper program = mContentProvider.get(mChannelId, ++mProgramPosition);
+ if (program == null || program.startTimeMs >= mEndTimeMs) {
+ if (mIsQueryForSingleChannel) {
+ return false;
+ } else {
+ if (++mChannelId > Constants.UNIT_TEST_CHANNEL_COUNT) {
+ return false;
+ }
+ mProgramPosition = -1;
+ }
+ } else if (program.startTimeMs + program.programInfo.durationMs >= mStartTimeMs) {
+ mCurrentProgram = program;
+ break;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public void close() {
+ // No-op.
+ }
+ }
+
+ private class TestProgramDataManagerListener implements ProgramDataManager.Listener {
+ public CountDownLatch programUpdatedLatch = new CountDownLatch(1);
+
+ @Override
+ public void onProgramUpdated() {
+ programUpdatedLatch.countDown();
+ }
+
+ public void reset() {
+ programUpdatedLatch = new CountDownLatch(1);
+ }
+ }
+
+ private class TestProgramDataManagerOnCurrentProgramUpdatedListener implements
+ OnCurrentProgramUpdatedListener {
+ public CountDownLatch currentProgramUpdatedLatch = new CountDownLatch(1);
+ public long updatedChannelId = -1;
+ public Program updatedProgram = null;
+
+ @Override
+ public void onCurrentProgramUpdated(long channelId, Program program) {
+ updatedChannelId = channelId;
+ updatedProgram = program;
+ currentProgramUpdatedLatch.countDown();
+ }
+ }
+}
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.<CustomAction>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<TvTrackInfo> 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<T extends Evaluator> 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<Long> 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<FavoriteChannelEvaluator> {
+ 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<Long> 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<Long> 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<RecentChannelEvaluator> {
+ 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<Long> 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<Long> 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<Long> 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<Long, Double> scores = new HashMap<>();
+ List<Long> 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<Integer>() {
+ @Override
+ public Integer answer(InvocationOnMock invocation) throws Throwable {
+ return channelRecordSortedMap.size();
+ }
+ }).when(dataManager).getChannelRecordCount();
+ Mockito.doAnswer(new Answer<Collection<ChannelRecord>>() {
+ @Override
+ public Collection<ChannelRecord> answer(InvocationOnMock invocation) throws Throwable {
+ return channelRecordSortedMap.values();
+ }
+ }).when(dataManager).getChannelRecords();
+ Mockito.doAnswer(new Answer<ChannelRecord>() {
+ @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<Long, ChannelRecord> {
+ 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<Long> 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> CHANNEL_SORT_KEY_COMPARATOR = new Comparator<Channel>() {
+ @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<Channel> expectedChannelList = mRecommender.recommendChannels();
+ List<Channel> 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<Channel> 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<Channel> 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<Channel> 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<Long> getChannelIdListSorted() {
+ return new ArrayList<>(mChannelRecordSortedMap.keySet());
+ }
+
+ private void setChannelScores_scoreIncreasesAsChannelIdIncreases() {
+ List<Long> 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<Long, Double> 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<RoutineWatchEvaluator> {
+
+ 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<String> wordList = RoutineWatchEvaluator.splitTextToWords(text);
+ MoreAsserts.assertContentsInOrder(wordList, words);
+ }
+
+ private void assertMaximumMatchedWordSequenceLength(int expectedLength,
+ String text1, String text2) {
+ List<String> wordList1 = RoutineWatchEvaluator.splitTextToWords(text1);
+ List<String> 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<TvActivity> {
+
+ 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<String, Boolean> 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<Boolean>() {
+ @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<TvInputInfo>() {
+ @Override
+ public int compare(TvInputInfo lhs, TvInputInfo rhs) {
+ return lhs.getId().compareTo(rhs.getId());
+ }
+ }
+ );
+ SetupView.TvInputInfoComparator comparator =
+ new SetupView.TvInputInfoComparator(setupUtils, inputManager);
+ ComparatorTester<TvInputInfo> 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 <a href="http://b/20488453">b/20488453</a>.
+ */
+ 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<TvInputInfo> 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<TvInputInfo> 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<String, Boolean> 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<Boolean>() {
+ @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.<TvInputInfo>any());
+ Mockito.doAnswer(new Answer<String>() {
+ @Override
+ public String answer(InvocationOnMock invocation) throws Throwable {
+ TvInputInfo info = (TvInputInfo) invocation.getArguments()[0];
+ return info.getId();
+ }
+ }).when(manager).loadLabel(Mockito.<TvInputInfo>any());
+
+ ComparatorTester<TvInputInfo> 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<TvTrackInfo> ALL = Arrays.asList(INFO_1_EN_1, INFO_2_EN_5, INFO_3_FR_5);
+
+ public void testGetBestTrackInfo_empty() {
+ TvTrackInfo result = getBestTrackInfo(Collections.<TvTrackInfo>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<TvTrackInfo> 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}.
+ * <p/>
+ * 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);
+ }
+}