1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tv.data;
18 
19 import static android.os.Looper.getMainLooper;
20 
21 import static com.google.common.truth.Truth.assertThat;
22 import static com.google.common.truth.Truth.assertWithMessage;
23 
24 import static org.robolectric.Shadows.shadowOf;
25 
26 import android.content.ContentResolver;
27 import android.media.tv.TvContract;
28 
29 import com.android.tv.common.flags.impl.DefaultBackendKnobsFlags;
30 import com.android.tv.data.api.Program;
31 import com.android.tv.perf.stub.StubPerformanceMonitor;
32 import com.android.tv.testing.FakeTvInputManagerHelper;
33 import com.android.tv.testing.TestSingletonApp;
34 import com.android.tv.testing.constants.ConfigConstants;
35 import com.android.tv.testing.constants.Constants;
36 import com.android.tv.testing.data.ProgramInfo;
37 import com.android.tv.testing.data.ProgramUtils;
38 import com.android.tv.testing.fakes.FakeClock;
39 import com.android.tv.testing.fakes.FakeTvProvider;
40 import com.android.tv.testing.robo.ContentProviders;
41 import com.android.tv.testing.testdata.TestData;
42 
43 import org.junit.After;
44 import org.junit.Before;
45 import org.junit.Test;
46 import org.junit.runner.RunWith;
47 import org.robolectric.RobolectricTestRunner;
48 import org.robolectric.RuntimeEnvironment;
49 import org.robolectric.android.util.concurrent.RoboExecutorService;
50 import org.robolectric.annotation.Config;
51 
52 import java.util.List;
53 import java.util.concurrent.CountDownLatch;
54 import java.util.concurrent.TimeUnit;
55 
56 /** Test for {@link ProgramDataManager} */
57 @RunWith(RobolectricTestRunner.class)
58 @Config(sdk = ConfigConstants.SDK, application = TestSingletonApp.class)
59 public class ProgramDataManagerTest {
60 
61     // Wait time for expected success.
62     private static final long WAIT_TIME_OUT_MS = 1000L;
63     // Wait time for expected failure.
64     private static final long FAILURE_TIME_OUT_MS = 300L;
65 
66     private ProgramDataManager mProgramDataManager;
67     private FakeClock mClock;
68     private TestProgramDataManagerCallback mCallback;
69 
70     @Before
setUp()71     public void setUp() {
72         mClock = FakeClock.createWithCurrentTime();
73         mCallback = new TestProgramDataManagerCallback();
74         ContentProviders.register(FakeTvProvider.class, TvContract.AUTHORITY);
75         TestData.DEFAULT_10_CHANNELS.init(
76                 RuntimeEnvironment.application, mClock, TimeUnit.DAYS.toMillis(1));
77         FakeTvInputManagerHelper tvInputManagerHelper =
78                 new FakeTvInputManagerHelper(RuntimeEnvironment.application);
79         RoboExecutorService executor = new RoboExecutorService();
80         ContentResolver contentResolver = RuntimeEnvironment.application.getContentResolver();
81         ChannelDataManager channelDataManager =
82                 new ChannelDataManager(
83                         RuntimeEnvironment.application,
84                         tvInputManagerHelper,
85                         executor,
86                         contentResolver);
87         mProgramDataManager =
88                 new ProgramDataManager(
89                         RuntimeEnvironment.application,
90                         executor,
91                         RuntimeEnvironment.application.getContentResolver(),
92                         mClock,
93                         getMainLooper(),
94                         new DefaultBackendKnobsFlags(),
95                         new StubPerformanceMonitor(),
96                         channelDataManager,
97                         tvInputManagerHelper);
98 
99         mProgramDataManager.setPrefetchEnabled(true);
100         mProgramDataManager.addCallback(mCallback);
101     }
102 
103     @After
tearDown()104     public void tearDown() {
105         mProgramDataManager.stop();
106     }
107 
startAndWaitForComplete()108     private void startAndWaitForComplete() throws InterruptedException {
109         mProgramDataManager.start();
110         shadowOf(getMainLooper()).idle();
111         assertThat(mCallback.channelUpdatedLatch.await(WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS))
112                 .isTrue();
113     }
114 
115     /** Test for {@link ProgramInfo#getIndex} and {@link ProgramInfo#getStartTimeMs}. */
116     @Test
testProgramUtils()117     public void testProgramUtils() {
118         ProgramInfo stub = ProgramInfo.create();
119         for (long channelId = 1; channelId < Constants.UNIT_TEST_CHANNEL_COUNT; channelId++) {
120             int index = stub.getIndex(mClock.currentTimeMillis(), channelId);
121             long startTimeMs = stub.getStartTimeMs(index, channelId);
122             ProgramInfo programAt = stub.build(RuntimeEnvironment.application, index);
123             assertThat(startTimeMs).isAtMost(mClock.currentTimeMillis());
124             assertThat(mClock.currentTimeMillis()).isLessThan(startTimeMs + programAt.durationMs);
125         }
126     }
127 
128     /**
129      * Test for following methods.
130      *
131      * <p>{@link ProgramDataManager#getCurrentProgram(long)}, {@link
132      * ProgramDataManager#getPrograms(long, long)}, {@link
133      * ProgramDataManager#setPrefetchTimeRange(long)}.
134      */
135     @Test
testGetPrograms()136     public void testGetPrograms() throws InterruptedException {
137         // Initial setup to test {@link ProgramDataManager#setPrefetchTimeRange(long)}.
138         long preventSnapDelayMs = ProgramDataManager.PROGRAM_GUIDE_SNAP_TIME_MS * 2;
139         long prefetchTimeRangeStartMs = System.currentTimeMillis() + preventSnapDelayMs;
140         mClock.setCurrentTimeMillis(prefetchTimeRangeStartMs + preventSnapDelayMs);
141         mProgramDataManager.setPrefetchTimeRange(prefetchTimeRangeStartMs);
142 
143         startAndWaitForComplete();
144 
145         for (long channelId = 1; channelId <= Constants.UNIT_TEST_CHANNEL_COUNT; channelId++) {
146             Program currentProgram = mProgramDataManager.getCurrentProgram(channelId);
147             // Test {@link ProgramDataManager#getCurrentProgram(long)}.
148             assertThat(currentProgram).isNotNull();
149             assertWithMessage("currentProgramStartTime")
150                     .that(currentProgram.getStartTimeUtcMillis())
151                     .isLessThan(mClock.currentTimeMillis());
152             assertWithMessage("currentProgramEndTime")
153                     .that(currentProgram.getEndTimeUtcMillis())
154                     .isGreaterThan(mClock.currentTimeMillis());
155 
156             // Test {@link ProgramDataManager#getPrograms(long)}.
157             // Case #1: Normal case
158             List<Program> programs =
159                     mProgramDataManager.getPrograms(channelId, mClock.currentTimeMillis());
160             ProgramInfo stub = ProgramInfo.create();
161             int index = stub.getIndex(mClock.currentTimeMillis(), channelId);
162             for (Program program : programs) {
163                 ProgramInfo programInfoAt = stub.build(RuntimeEnvironment.application, index);
164                 long startTimeMs = stub.getStartTimeMs(index, channelId);
165                 assertProgramEquals(startTimeMs, programInfoAt, program);
166                 index++;
167             }
168             // Case #2: Corner cases where there's a program that starts at the start of the range.
169             long startTimeMs = programs.get(0).getStartTimeUtcMillis();
170             programs = mProgramDataManager.getPrograms(channelId, startTimeMs);
171             assertThat(programs.get(0).getStartTimeUtcMillis()).isEqualTo(startTimeMs);
172 
173             // Test {@link ProgramDataManager#setPrefetchTimeRange(long)}.
174             programs =
175                     mProgramDataManager.getPrograms(
176                             channelId, prefetchTimeRangeStartMs - TimeUnit.HOURS.toMillis(1));
177             for (Program program : programs) {
178                 assertThat(program.getEndTimeUtcMillis()).isAtLeast(prefetchTimeRangeStartMs);
179             }
180         }
181     }
182 
183     /**
184      * Test for following methods.
185      *
186      * <p>{@link ProgramDataManager#addOnCurrentProgramUpdatedListener}, {@link
187      * ProgramDataManager#removeOnCurrentProgramUpdatedListener}.
188      */
189     @Test
testCurrentProgramListener()190     public void testCurrentProgramListener() throws InterruptedException {
191         final long testChannelId = 1;
192         ProgramInfo stub = ProgramInfo.create();
193         int index = stub.getIndex(mClock.currentTimeMillis(), testChannelId);
194         // Set current time to few seconds before the current program ends,
195         // so we can see if callback is called as expected.
196         long nextProgramStartTimeMs = stub.getStartTimeMs(index + 1, testChannelId);
197         ProgramInfo nextProgramInfo = stub.build(RuntimeEnvironment.application, index + 1);
198         mClock.setCurrentTimeMillis(nextProgramStartTimeMs - (WAIT_TIME_OUT_MS / 2));
199 
200         startAndWaitForComplete();
201         // Note that changing current time doesn't affect the current program
202         // because current program is updated after waiting for the program's duration.
203         // See {@link ProgramDataManager#updateCurrentProgram}.
204         TestProgramDataManagerOnCurrentProgramUpdatedListener listener =
205                 new TestProgramDataManagerOnCurrentProgramUpdatedListener();
206         mClock.setCurrentTimeMillis(mClock.currentTimeMillis() + WAIT_TIME_OUT_MS);
207         mProgramDataManager.addOnCurrentProgramUpdatedListener(testChannelId, listener);
208         shadowOf(getMainLooper()).runToEndOfTasks();
209         assertThat(
210                         listener.currentProgramUpdatedLatch.await(
211                                 WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS))
212                 .isTrue();
213         assertThat(listener.updatedChannelId).isEqualTo(testChannelId);
214         Program currentProgram = mProgramDataManager.getCurrentProgram(testChannelId);
215         assertProgramEquals(nextProgramStartTimeMs, nextProgramInfo, currentProgram);
216         assertThat(currentProgram).isEqualTo(listener.updatedProgram);
217     }
218 
219     /** Test if program data is refreshed after the program insertion. */
220     @Test
testContentProviderUpdate()221     public void testContentProviderUpdate() throws InterruptedException {
222         final long testChannelId = 1;
223         startAndWaitForComplete();
224         // Force program data manager to update program data whenever it's changes.
225         mProgramDataManager.setProgramPrefetchUpdateWait(0);
226         mCallback.reset();
227         List<Program> programList =
228                 mProgramDataManager.getPrograms(testChannelId, mClock.currentTimeMillis());
229         assertThat(programList).isNotNull();
230         long lastProgramEndTime = programList.get(programList.size() - 1).getEndTimeUtcMillis();
231         // Make change in content provider
232         ProgramUtils.populatePrograms(
233                 RuntimeEnvironment.application,
234                 TvContract.buildChannelUri(testChannelId),
235                 ProgramInfo.create(),
236                 mClock,
237                 TimeUnit.DAYS.toMillis(2));
238         shadowOf(getMainLooper()).runToEndOfTasks();
239         assertThat(mCallback.programUpdatedLatch.await(WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS))
240                 .isTrue();
241         programList = mProgramDataManager.getPrograms(testChannelId, mClock.currentTimeMillis());
242         assertThat(lastProgramEndTime)
243                 .isLessThan(programList.get(programList.size() - 1).getEndTimeUtcMillis());
244     }
245 
246     /** Test for {@link ProgramDataManager#setPauseProgramUpdate(boolean)}. */
247     @Test
testSetPauseProgramUpdate()248     public void testSetPauseProgramUpdate() throws InterruptedException {
249         final long testChannelId = 1;
250         startAndWaitForComplete();
251         // Force program data manager to update program data whenever it's changes.
252         mProgramDataManager.setProgramPrefetchUpdateWait(0);
253         mCallback.reset();
254         mProgramDataManager.setPauseProgramUpdate(true);
255         ProgramUtils.populatePrograms(
256                 RuntimeEnvironment.application,
257                 TvContract.buildChannelUri(testChannelId),
258                 ProgramInfo.create(),
259                 mClock,
260                 TimeUnit.DAYS.toMillis(2));
261         shadowOf(getMainLooper()).runToEndOfTasks();
262         assertThat(mCallback.programUpdatedLatch.await(FAILURE_TIME_OUT_MS, TimeUnit.MILLISECONDS))
263                 .isFalse();
264     }
265 
assertProgramEquals( long expectedStartTime, ProgramInfo expectedInfo, Program actualProgram)266     public static void assertProgramEquals(
267             long expectedStartTime, ProgramInfo expectedInfo, Program actualProgram) {
268         assertWithMessage("title").that(actualProgram.getTitle()).isEqualTo(expectedInfo.title);
269         assertWithMessage("episode")
270                 .that(actualProgram.getEpisodeTitle())
271                 .isEqualTo(expectedInfo.episode);
272         assertWithMessage("description")
273                 .that(actualProgram.getDescription())
274                 .isEqualTo(expectedInfo.description);
275         assertWithMessage("startTime")
276                 .that(actualProgram.getStartTimeUtcMillis())
277                 .isEqualTo(expectedStartTime);
278         assertWithMessage("endTime")
279                 .that(actualProgram.getEndTimeUtcMillis())
280                 .isEqualTo(expectedStartTime + expectedInfo.durationMs);
281     }
282 
283     private static class TestProgramDataManagerCallback implements ProgramDataManager.Callback {
284         public CountDownLatch programUpdatedLatch = new CountDownLatch(1);
285         public CountDownLatch channelUpdatedLatch = new CountDownLatch(1);
286 
287         @Override
onProgramUpdated()288         public void onProgramUpdated() {
289             programUpdatedLatch.countDown();
290         }
291 
292         @Override
onChannelUpdated()293         public void onChannelUpdated() {
294             channelUpdatedLatch.countDown();
295         }
296 
reset()297         public void reset() {
298             programUpdatedLatch = new CountDownLatch(1);
299             channelUpdatedLatch = new CountDownLatch(1);
300         }
301     }
302 
303     private static class TestProgramDataManagerOnCurrentProgramUpdatedListener
304             implements OnCurrentProgramUpdatedListener {
305         public final CountDownLatch currentProgramUpdatedLatch = new CountDownLatch(1);
306         public long updatedChannelId = -1;
307         public Program updatedProgram = null;
308 
309         @Override
onCurrentProgramUpdated(long channelId, Program program)310         public void onCurrentProgramUpdated(long channelId, Program program) {
311             updatedChannelId = channelId;
312             updatedProgram = program;
313             currentProgramUpdatedLatch.countDown();
314         }
315     }
316 }
317