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 androidx.test.InstrumentationRegistry.getInstrumentation;
20 import static androidx.test.InstrumentationRegistry.getTargetContext;
21 import static com.google.common.truth.Truth.assertThat;
22 import static com.google.common.truth.Truth.assertWithMessage;
23 
24 import android.content.ContentProvider;
25 import android.content.ContentUris;
26 import android.content.ContentValues;
27 import android.content.Context;
28 import android.content.res.AssetFileDescriptor;
29 import android.database.ContentObserver;
30 import android.database.Cursor;
31 import android.media.tv.TvContract;
32 import android.media.tv.TvContract.Channels;
33 import android.net.Uri;
34 import android.os.AsyncTask;
35 import android.os.Bundle;
36 import android.test.MoreAsserts;
37 import android.test.mock.MockContentProvider;
38 import android.test.mock.MockContentResolver;
39 import android.test.mock.MockCursor;
40 import android.text.TextUtils;
41 import android.util.Log;
42 import android.util.SparseArray;
43 import androidx.test.filters.SmallTest;
44 import androidx.test.runner.AndroidJUnit4;
45 import com.android.tv.data.api.Channel;
46 import com.android.tv.testing.constants.Constants;
47 import com.android.tv.testing.data.ChannelInfo;
48 import com.android.tv.util.TvInputManagerHelper;
49 import java.io.FileNotFoundException;
50 import java.util.ArrayList;
51 import java.util.Arrays;
52 import java.util.List;
53 import java.util.concurrent.CountDownLatch;
54 import java.util.concurrent.TimeUnit;
55 import org.junit.After;
56 import org.junit.Before;
57 import org.junit.Test;
58 import org.junit.runner.RunWith;
59 import org.mockito.ArgumentMatchers;
60 import org.mockito.Mockito;
61 
62 /**
63  * Test for {@link ChannelDataManager}
64  *
65  * <p>A test method may include tests for multiple methods to minimize the DB access. Note that all
66  * the methods of {@link ChannelDataManager} should be called from the UI thread.
67  */
68 @SmallTest
69 @RunWith(AndroidJUnit4.class)
70 public class ChannelDataManagerTest {
71     private static final boolean DEBUG = false;
72     private static final String TAG = "ChannelDataManagerTest";
73 
74     // Wait time for expected success.
75     private static final long WAIT_TIME_OUT_MS = 1000L;
76     private static final String DUMMY_INPUT_ID = "dummy";
77     private static final String COLUMN_BROWSABLE = "browsable";
78     private static final String COLUMN_LOCKED = "locked";
79 
80     private ChannelDataManager mChannelDataManager;
81     private TestChannelDataManagerListener mListener;
82     private FakeContentResolver mContentResolver;
83     private FakeContentProvider mContentProvider;
84 
85     @Before
setUp()86     public void setUp() {
87         assertWithMessage("More than 2 channels to test")
88                 .that(Constants.UNIT_TEST_CHANNEL_COUNT > 2)
89                 .isTrue();
90 
91         mContentProvider = new FakeContentProvider(getTargetContext());
92         mContentResolver = new FakeContentResolver();
93         mContentResolver.addProvider(TvContract.AUTHORITY, mContentProvider);
94         mListener = new TestChannelDataManagerListener();
95     getInstrumentation()
96         .runOnMainSync(
97             new Runnable() {
98               @Override
99               public void run() {
100                 TvInputManagerHelper mockHelper = Mockito.mock(TvInputManagerHelper.class);
101                 Mockito.when(mockHelper.hasTvInputInfo(ArgumentMatchers.anyString()))
102                     .thenReturn(true);
103                 Context mockContext = Mockito.mock(Context.class);
104                 Mockito.when(mockContext.getContentResolver()).thenReturn(mContentResolver);
105                 Mockito.when(mockContext.checkSelfPermission(ArgumentMatchers.anyString()))
106                     .thenAnswer(
107                         invocation -> {
108                           Object[] args = invocation.getArguments();
109                           return getTargetContext().checkSelfPermission(((String) args[0]));
110                         });
111 
112                 mChannelDataManager =
113                     new ChannelDataManager(
114                         mockContext, mockHelper, AsyncTask.SERIAL_EXECUTOR, mContentResolver);
115                 mChannelDataManager.addListener(mListener);
116               }
117             });
118     }
119 
120     @After
tearDown()121     public void tearDown() {
122         getInstrumentation()
123                 .runOnMainSync(
124                         new Runnable() {
125                             @Override
126                             public void run() {
127                                 mChannelDataManager.stop();
128                             }
129                         });
130     }
131 
startAndWaitForComplete()132     private void startAndWaitForComplete() throws InterruptedException {
133         getInstrumentation()
134                 .runOnMainSync(
135                         new Runnable() {
136                             @Override
137                             public void run() {
138                                 mChannelDataManager.start();
139                             }
140                         });
141         assertThat(mListener.loadFinishedLatch.await(WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS))
142                 .isTrue();
143     }
144 
restart()145     private void restart() throws InterruptedException {
146         getInstrumentation()
147                 .runOnMainSync(
148                         new Runnable() {
149                             @Override
150                             public void run() {
151                                 mChannelDataManager.stop();
152                                 mListener.reset();
153                             }
154                         });
155         startAndWaitForComplete();
156     }
157 
158     @Test
testIsDbLoadFinished()159     public void testIsDbLoadFinished() throws InterruptedException {
160         startAndWaitForComplete();
161         assertThat(mChannelDataManager.isDbLoadFinished()).isTrue();
162     }
163 
164     /**
165      * Test for following methods - {@link ChannelDataManager#getChannelCount} - {@link
166      * ChannelDataManager#getChannelList} - {@link ChannelDataManager#getChannel}
167      */
168     @Test
testGetChannels()169     public void testGetChannels() throws InterruptedException {
170         startAndWaitForComplete();
171 
172         // Test {@link ChannelDataManager#getChannelCount}
173         assertThat(mChannelDataManager.getChannelCount())
174                 .isEqualTo(Constants.UNIT_TEST_CHANNEL_COUNT);
175 
176         // Test {@link ChannelDataManager#getChannelList}
177         List<ChannelInfo> channelInfoList = new ArrayList<>();
178         for (int i = 1; i <= Constants.UNIT_TEST_CHANNEL_COUNT; i++) {
179             channelInfoList.add(ChannelInfo.create(getTargetContext(), i));
180         }
181         List<Channel> channelList = mChannelDataManager.getChannelList();
182         for (Channel channel : channelList) {
183             boolean found = false;
184             for (ChannelInfo channelInfo : channelInfoList) {
185                 if (TextUtils.equals(channelInfo.name, channel.getDisplayName())) {
186                     found = true;
187                     channelInfoList.remove(channelInfo);
188                     break;
189                 }
190             }
191             assertWithMessage("Cannot find (" + channel + ")").that(found).isTrue();
192         }
193 
194         // Test {@link ChannelDataManager#getChannelIndex()}
195         for (Channel channel : channelList) {
196             assertThat(mChannelDataManager.getChannel(channel.getId())).isEqualTo(channel);
197         }
198     }
199 
200     /** Test for {@link ChannelDataManager#getChannelCount} when no channel is available. */
201     @Test
testGetChannels_noChannels()202     public void testGetChannels_noChannels() throws InterruptedException {
203         mContentProvider.clear();
204         startAndWaitForComplete();
205         assertThat(mChannelDataManager.getChannelCount()).isEqualTo(0);
206     }
207 
208     /**
209      * Test for following methods and channel listener with notifying change. - {@link
210      * ChannelDataManager#updateBrowsable} - {@link ChannelDataManager#applyUpdatedValuesToDb}
211      */
212     @Test
testBrowsable()213     public void testBrowsable() throws InterruptedException {
214         startAndWaitForComplete();
215 
216         // Test if all channels are browsable
217         List<Channel> channelList = mChannelDataManager.getChannelList();
218         List<Channel> browsableChannelList = mChannelDataManager.getBrowsableChannelList();
219         for (Channel browsableChannel : browsableChannelList) {
220             boolean found = channelList.remove(browsableChannel);
221             assertWithMessage("Cannot find (" + browsableChannel + ")").that(found).isTrue();
222         }
223         assertThat(channelList).isEmpty();
224 
225         // Prepare for next tests.
226         channelList = mChannelDataManager.getChannelList();
227         TestChannelDataManagerChannelListener channelListener =
228                 new TestChannelDataManagerChannelListener();
229         Channel channel1 = channelList.get(0);
230         mChannelDataManager.addChannelListener(channel1.getId(), channelListener);
231 
232         // Test {@link ChannelDataManager#updateBrowsable} & notification.
233         mChannelDataManager.updateBrowsable(channel1.getId(), false, false);
234         assertThat(mListener.channelBrowsableChangedCalled).isTrue();
235         assertThat(mChannelDataManager.getBrowsableChannelList()).doesNotContain(channel1);
236         MoreAsserts.assertContentsInAnyOrder(channelListener.updatedChannels, channel1);
237         channelListener.reset();
238 
239         // Test {@link ChannelDataManager#applyUpdatedValuesToDb}
240         // Disable the update notification to avoid the unwanted call of "onLoadFinished".
241         mContentResolver.mNotifyDisabled = true;
242         mChannelDataManager.applyUpdatedValuesToDb();
243         restart();
244         browsableChannelList = mChannelDataManager.getBrowsableChannelList();
245         assertThat(browsableChannelList).hasSize(Constants.UNIT_TEST_CHANNEL_COUNT - 1);
246         assertThat(browsableChannelList).doesNotContain(channel1);
247     }
248 
249     /**
250      * Test for following methods and channel listener without notifying change. - {@link
251      * ChannelDataManager#updateBrowsable} - {@link ChannelDataManager#applyUpdatedValuesToDb}
252      */
253     @Test
testBrowsable_skipNotification()254     public void testBrowsable_skipNotification() throws InterruptedException {
255         startAndWaitForComplete();
256 
257         List<Channel> channels = mChannelDataManager.getChannelList();
258         // Prepare for next tests.
259         TestChannelDataManagerChannelListener channelListener =
260                 new TestChannelDataManagerChannelListener();
261         Channel channel1 = channels.get(0);
262         Channel channel2 = channels.get(1);
263         mChannelDataManager.addChannelListener(channel1.getId(), channelListener);
264         mChannelDataManager.addChannelListener(channel2.getId(), channelListener);
265 
266         // Test {@link ChannelDataManager#updateBrowsable} & skip notification.
267         mChannelDataManager.updateBrowsable(channel1.getId(), false, true);
268         mChannelDataManager.updateBrowsable(channel2.getId(), false, true);
269         mChannelDataManager.updateBrowsable(channel1.getId(), true, true);
270         assertThat(mListener.channelBrowsableChangedCalled).isFalse();
271         List<Channel> browsableChannelList = mChannelDataManager.getBrowsableChannelList();
272         assertThat(browsableChannelList).contains(channel1);
273         assertThat(browsableChannelList).doesNotContain(channel2);
274 
275         // Test {@link ChannelDataManager#applyUpdatedValuesToDb}
276         // Disable the update notification to avoid the unwanted call of "onLoadFinished".
277         mContentResolver.mNotifyDisabled = true;
278         mChannelDataManager.applyUpdatedValuesToDb();
279         restart();
280         browsableChannelList = mChannelDataManager.getBrowsableChannelList();
281         assertThat(browsableChannelList).hasSize(Constants.UNIT_TEST_CHANNEL_COUNT - 1);
282         assertThat(browsableChannelList).doesNotContain(channel2);
283     }
284 
285     /**
286      * Test for following methods and channel listener. - {@link ChannelDataManager#updateLocked} -
287      * {@link ChannelDataManager#applyUpdatedValuesToDb}
288      */
289     @Test
testLocked()290     public void testLocked() throws InterruptedException {
291         startAndWaitForComplete();
292 
293         // Test if all channels aren't locked at the first time.
294         List<Channel> channelList = mChannelDataManager.getChannelList();
295         for (Channel channel : channelList) {
296             assertWithMessage(channel + " is locked").that(channel.isLocked()).isFalse();
297         }
298 
299         // Prepare for next tests.
300         Channel channel = mChannelDataManager.getChannelList().get(0);
301 
302         // Test {@link ChannelDataManager#updateLocked}
303         mChannelDataManager.updateLocked(channel.getId(), true);
304         assertThat(mChannelDataManager.getChannel(channel.getId()).isLocked()).isTrue();
305 
306         // Test {@link ChannelDataManager#applyUpdatedValuesToDb}.
307         // Disable the update notification to avoid the unwanted call of "onLoadFinished".
308         mContentResolver.mNotifyDisabled = true;
309         mChannelDataManager.applyUpdatedValuesToDb();
310         restart();
311         assertThat(mChannelDataManager.getChannel(channel.getId()).isLocked()).isTrue();
312 
313         // Cleanup
314         mChannelDataManager.updateLocked(channel.getId(), false);
315     }
316 
317     /** Test ChannelDataManager when channels in TvContract are updated, removed, or added. */
318     @Test
testChannelListChanged()319     public void testChannelListChanged() throws InterruptedException {
320         startAndWaitForComplete();
321 
322         // Test channel add.
323         mListener.reset();
324         long testChannelId = Constants.UNIT_TEST_CHANNEL_COUNT + 1;
325         ChannelInfo testChannelInfo = ChannelInfo.create(getTargetContext(), (int) testChannelId);
326         testChannelId = Constants.UNIT_TEST_CHANNEL_COUNT + 1;
327         mContentProvider.simulateInsert(testChannelInfo);
328         assertThat(mListener.channelListUpdatedLatch.await(WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS))
329                 .isTrue();
330         assertThat(mChannelDataManager.getChannelCount())
331                 .isEqualTo(Constants.UNIT_TEST_CHANNEL_COUNT + 1);
332 
333         // Test channel update
334         mListener.reset();
335         TestChannelDataManagerChannelListener channelListener =
336                 new TestChannelDataManagerChannelListener();
337         mChannelDataManager.addChannelListener(testChannelId, channelListener);
338         String newName = testChannelInfo.name + "_test";
339         mContentProvider.simulateUpdate(testChannelId, newName);
340         assertThat(mListener.channelListUpdatedLatch.await(WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS))
341                 .isTrue();
342         assertThat(
343                         channelListener.channelChangedLatch.await(
344                                 WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS))
345                 .isTrue();
346         assertThat(channelListener.removedChannels).isEmpty();
347         assertThat(channelListener.updatedChannels).hasSize(1);
348         Channel updatedChannel = channelListener.updatedChannels.get(0);
349         assertThat(updatedChannel.getId()).isEqualTo(testChannelId);
350         assertThat(updatedChannel.getDisplayNumber()).isEqualTo(testChannelInfo.number);
351         assertThat(updatedChannel.getDisplayName()).isEqualTo(newName);
352         assertThat(mChannelDataManager.getChannelCount())
353                 .isEqualTo(Constants.UNIT_TEST_CHANNEL_COUNT + 1);
354 
355         // Test channel remove.
356         mListener.reset();
357         channelListener.reset();
358         mContentProvider.simulateDelete(testChannelId);
359         assertThat(mListener.channelListUpdatedLatch.await(WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS))
360                 .isTrue();
361         assertThat(
362                         channelListener.channelChangedLatch.await(
363                                 WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS))
364                 .isTrue();
365         assertThat(channelListener.removedChannels).hasSize(1);
366         assertThat(channelListener.updatedChannels).isEmpty();
367         Channel removedChannel = channelListener.removedChannels.get(0);
368         assertThat(removedChannel.getDisplayName()).isEqualTo(newName);
369         assertThat(removedChannel.getDisplayNumber()).isEqualTo(testChannelInfo.number);
370         assertThat(mChannelDataManager.getChannelCount())
371                 .isEqualTo(Constants.UNIT_TEST_CHANNEL_COUNT);
372     }
373 
374     private static class ChannelInfoWrapper {
375         public ChannelInfo channelInfo;
376         public boolean browsable;
377         public boolean locked;
378 
ChannelInfoWrapper(ChannelInfo channelInfo)379         public ChannelInfoWrapper(ChannelInfo channelInfo) {
380             this.channelInfo = channelInfo;
381             browsable = true;
382             locked = false;
383         }
384     }
385 
386     private class FakeContentResolver extends MockContentResolver {
387         boolean mNotifyDisabled;
388 
389         @Override
notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork)390         public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) {
391             super.notifyChange(uri, observer, syncToNetwork);
392             if (DEBUG) {
393                 Log.d(
394                         TAG,
395                         "onChanged(uri="
396                                 + uri
397                                 + ", observer="
398                                 + observer
399                                 + ") - Notification "
400                                 + (mNotifyDisabled ? "disabled" : "enabled"));
401             }
402             if (mNotifyDisabled) {
403                 return;
404             }
405             // Do not call {@link ContentObserver#onChange} directly to run it on the correct
406             // thread.
407             if (observer != null) {
408                 observer.dispatchChange(false, uri);
409             } else {
410                 mChannelDataManager.getContentObserver().dispatchChange(false, uri);
411             }
412         }
413     }
414 
415     // This implements the minimal methods in content resolver
416     // and detailed assumptions are written in each method.
417     private class FakeContentProvider extends MockContentProvider {
418         private final SparseArray<ChannelInfoWrapper> mChannelInfoList = new SparseArray<>();
419 
FakeContentProvider(Context context)420         public FakeContentProvider(Context context) {
421             super(context);
422             for (int i = 1; i <= Constants.UNIT_TEST_CHANNEL_COUNT; i++) {
423                 mChannelInfoList.put(
424                         i, new ChannelInfoWrapper(ChannelInfo.create(getTargetContext(), i)));
425             }
426         }
427 
428         @Override
openTypedAssetFile(Uri url, String mimeType, Bundle opts)429         public AssetFileDescriptor openTypedAssetFile(Uri url, String mimeType, Bundle opts) {
430             try {
431                 return getTargetContext().getContentResolver().openAssetFileDescriptor(url, "r");
432             } catch (FileNotFoundException e) {
433                 return null;
434             }
435         }
436 
437         /**
438          * Implementation of {@link ContentProvider#query}. This assumes that {@link
439          * ChannelDataManager} queries channels with empty {@code selection}. (i.e. channels are
440          * always queries for all)
441          */
442         @Override
query( Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)443         public Cursor query(
444                 Uri uri,
445                 String[] projection,
446                 String selection,
447                 String[] selectionArgs,
448                 String sortOrder) {
449             if (DEBUG) {
450                 Log.d(TAG, "dump query");
451                 Log.d(TAG, "  uri=" + uri);
452                 Log.d(TAG, "  projection=" + Arrays.toString(projection));
453                 Log.d(TAG, "  selection=" + selection);
454             }
455             assertChannelUri(uri);
456             return new FakeCursor(projection);
457         }
458 
459         /**
460          * Implementation of {@link ContentProvider#update}. This assumes that {@link
461          * ChannelDataManager} update channels only for changing browsable and locked.
462          */
463         @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)464         public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
465             if (DEBUG) Log.d(TAG, "update(uri=" + uri + ", selection=" + selection);
466             assertChannelUri(uri);
467             List<Long> channelIds = new ArrayList<>();
468             try {
469                 long channelId = ContentUris.parseId(uri);
470                 channelIds.add(channelId);
471             } catch (NumberFormatException e) {
472                 // Update for multiple channels.
473                 if (TextUtils.isEmpty(selection)) {
474                     for (int i = 0; i < mChannelInfoList.size(); i++) {
475                         channelIds.add((long) mChannelInfoList.keyAt(i));
476                     }
477                 } else {
478                     // See {@link Utils#buildSelectionForIds} for the syntax.
479                     String selectionForId =
480                             selection.substring(
481                                     selection.indexOf("(") + 1, selection.lastIndexOf(")"));
482                     String[] ids = selectionForId.split(", ");
483                     if (ids != null) {
484                         for (String id : ids) {
485                             channelIds.add(Long.parseLong(id));
486                         }
487                     }
488                 }
489             }
490             int updateCount = 0;
491             for (long channelId : channelIds) {
492                 boolean updated = false;
493                 ChannelInfoWrapper channel = mChannelInfoList.get((int) channelId);
494                 if (channel == null) {
495                     return 0;
496                 }
497                 if (values.containsKey(COLUMN_BROWSABLE)) {
498                     updated = true;
499                     channel.browsable = (values.getAsInteger(COLUMN_BROWSABLE) == 1);
500                 }
501                 if (values.containsKey(COLUMN_LOCKED)) {
502                     updated = true;
503                     channel.locked = (values.getAsInteger(COLUMN_LOCKED) == 1);
504                 }
505                 updateCount += updated ? 1 : 0;
506             }
507             if (updateCount > 0) {
508                 if (channelIds.size() == 1) {
509                     mContentResolver.notifyChange(uri, null);
510                 } else {
511                     mContentResolver.notifyChange(Channels.CONTENT_URI, null);
512                 }
513             } else {
514                 if (DEBUG) {
515                     Log.d(TAG, "Update to channel(uri=" + uri + ") is ignored for " + values);
516                 }
517             }
518             return updateCount;
519         }
520 
521         /**
522          * Simulates channel data insert. This assigns original network ID (the same with channel
523          * number) to channel ID.
524          */
simulateInsert(ChannelInfo testChannelInfo)525         public void simulateInsert(ChannelInfo testChannelInfo) {
526             long channelId = testChannelInfo.originalNetworkId;
527             mChannelInfoList.put(
528                     (int) channelId,
529                     new ChannelInfoWrapper(
530                             ChannelInfo.create(getTargetContext(), (int) channelId)));
531             mContentResolver.notifyChange(TvContract.buildChannelUri(channelId), null);
532         }
533 
534         /** Simulates channel data delete. */
simulateDelete(long channelId)535         public void simulateDelete(long channelId) {
536             mChannelInfoList.remove((int) channelId);
537             mContentResolver.notifyChange(TvContract.buildChannelUri(channelId), null);
538         }
539 
540         /** Simulates channel data update. */
simulateUpdate(long channelId, String newName)541         public void simulateUpdate(long channelId, String newName) {
542             ChannelInfoWrapper channel = mChannelInfoList.get((int) channelId);
543             ChannelInfo.Builder builder = new ChannelInfo.Builder(channel.channelInfo);
544             builder.setName(newName);
545             channel.channelInfo = builder.build();
546             mContentResolver.notifyChange(TvContract.buildChannelUri(channelId), null);
547         }
548 
assertChannelUri(Uri uri)549         private void assertChannelUri(Uri uri) {
550             assertWithMessage("Uri(" + uri + ") isn't channel uri")
551                     .that(uri.toString().startsWith(Channels.CONTENT_URI.toString()))
552                     .isTrue();
553         }
554 
clear()555         public void clear() {
556             mChannelInfoList.clear();
557         }
558 
get(int position)559         public ChannelInfoWrapper get(int position) {
560             return mChannelInfoList.get(mChannelInfoList.keyAt(position));
561         }
562 
getCount()563         public int getCount() {
564             return mChannelInfoList.size();
565         }
566 
keyAt(int position)567         public long keyAt(int position) {
568             return mChannelInfoList.keyAt(position);
569         }
570     }
571 
572     private class FakeCursor extends MockCursor {
573         private final String[] allColumns = {
574             Channels._ID,
575             Channels.COLUMN_DISPLAY_NAME,
576             Channels.COLUMN_DISPLAY_NUMBER,
577             Channels.COLUMN_INPUT_ID,
578             Channels.COLUMN_VIDEO_FORMAT,
579             Channels.COLUMN_ORIGINAL_NETWORK_ID,
580             COLUMN_BROWSABLE,
581             COLUMN_LOCKED
582         };
583         private final String[] mColumns;
584         private int mPosition;
585 
FakeCursor(String[] columns)586         public FakeCursor(String[] columns) {
587             mColumns = (columns == null) ? allColumns : columns;
588             mPosition = -1;
589         }
590 
591         @Override
getColumnName(int columnIndex)592         public String getColumnName(int columnIndex) {
593             return mColumns[columnIndex];
594         }
595 
596         @Override
getColumnIndex(String columnName)597         public int getColumnIndex(String columnName) {
598             for (int i = 0; i < mColumns.length; i++) {
599                 if (mColumns[i].equalsIgnoreCase(columnName)) {
600                     return i;
601                 }
602             }
603             return -1;
604         }
605 
606         @Override
getLong(int columnIndex)607         public long getLong(int columnIndex) {
608             String columnName = getColumnName(columnIndex);
609             switch (columnName) {
610                 case Channels._ID:
611                     return mContentProvider.keyAt(mPosition);
612                 default: // fall out
613             }
614             if (DEBUG) {
615                 Log.d(TAG, "Column (" + columnName + ") is ignored in getLong()");
616             }
617             return 0;
618         }
619 
620         @Override
getString(int columnIndex)621         public String getString(int columnIndex) {
622             String columnName = getColumnName(columnIndex);
623             ChannelInfoWrapper channel = mContentProvider.get(mPosition);
624             switch (columnName) {
625                 case Channels.COLUMN_DISPLAY_NAME:
626                     return channel.channelInfo.name;
627                 case Channels.COLUMN_DISPLAY_NUMBER:
628                     return channel.channelInfo.number;
629                 case Channels.COLUMN_INPUT_ID:
630                     return DUMMY_INPUT_ID;
631                 case Channels.COLUMN_VIDEO_FORMAT:
632                     return channel.channelInfo.getVideoFormat();
633                 default: // fall out
634             }
635             if (DEBUG) {
636                 Log.d(TAG, "Column (" + columnName + ") is ignored in getString()");
637             }
638             return null;
639         }
640 
641         @Override
getInt(int columnIndex)642         public int getInt(int columnIndex) {
643             String columnName = getColumnName(columnIndex);
644             ChannelInfoWrapper channel = mContentProvider.get(mPosition);
645             switch (columnName) {
646                 case Channels.COLUMN_ORIGINAL_NETWORK_ID:
647                     return channel.channelInfo.originalNetworkId;
648                 case COLUMN_BROWSABLE:
649                     return channel.browsable ? 1 : 0;
650                 case COLUMN_LOCKED:
651                     return channel.locked ? 1 : 0;
652                 default: // fall out
653             }
654             if (DEBUG) {
655                 Log.d(TAG, "Column (" + columnName + ") is ignored in getInt()");
656             }
657             return 0;
658         }
659 
660         @Override
getCount()661         public int getCount() {
662             return mContentProvider.getCount();
663         }
664 
665         @Override
moveToNext()666         public boolean moveToNext() {
667             return ++mPosition < mContentProvider.getCount();
668         }
669 
670         @Override
close()671         public void close() {
672             // No-op.
673         }
674     }
675 
676     private static class TestChannelDataManagerListener implements ChannelDataManager.Listener {
677         public CountDownLatch loadFinishedLatch = new CountDownLatch(1);
678         public CountDownLatch channelListUpdatedLatch = new CountDownLatch(1);
679         public boolean channelBrowsableChangedCalled;
680 
681         @Override
onLoadFinished()682         public void onLoadFinished() {
683             loadFinishedLatch.countDown();
684         }
685 
686         @Override
onChannelListUpdated()687         public void onChannelListUpdated() {
688             channelListUpdatedLatch.countDown();
689         }
690 
691         @Override
onChannelBrowsableChanged()692         public void onChannelBrowsableChanged() {
693             channelBrowsableChangedCalled = true;
694         }
695 
reset()696         public void reset() {
697             loadFinishedLatch = new CountDownLatch(1);
698             channelListUpdatedLatch = new CountDownLatch(1);
699             channelBrowsableChangedCalled = false;
700         }
701     }
702 
703     private static class TestChannelDataManagerChannelListener
704             implements ChannelDataManager.ChannelListener {
705         public CountDownLatch channelChangedLatch = new CountDownLatch(1);
706         public final List<Channel> removedChannels = new ArrayList<>();
707         public final List<Channel> updatedChannels = new ArrayList<>();
708 
709         @Override
onChannelRemoved(Channel channel)710         public void onChannelRemoved(Channel channel) {
711             removedChannels.add(channel);
712             channelChangedLatch.countDown();
713         }
714 
715         @Override
onChannelUpdated(Channel channel)716         public void onChannelUpdated(Channel channel) {
717             updatedChannels.add(channel);
718             channelChangedLatch.countDown();
719         }
720 
reset()721         public void reset() {
722             channelChangedLatch = new CountDownLatch(1);
723             removedChannels.clear();
724             updatedChannels.clear();
725         }
726     }
727 }
728