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