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