/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.tv.data;

import static androidx.test.InstrumentationRegistry.getInstrumentation;
import static androidx.test.InstrumentationRegistry.getTargetContext;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;

import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.database.ContentObserver;
import android.database.Cursor;
import android.media.tv.TvContract;
import android.media.tv.TvContract.Channels;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.test.MoreAsserts;
import android.test.mock.MockContentProvider;
import android.test.mock.MockContentResolver;
import android.test.mock.MockCursor;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.tv.data.api.Channel;
import com.android.tv.testing.constants.Constants;
import com.android.tv.testing.data.ChannelInfo;
import com.android.tv.util.TvInputManagerHelper;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;

/**
 * Test for {@link ChannelDataManager}
 *
 * <p>A test method may include tests for multiple methods to minimize the DB access. Note that all
 * the methods of {@link ChannelDataManager} should be called from the UI thread.
 */
@SmallTest
@RunWith(AndroidJUnit4.class)
public class ChannelDataManagerTest {
    private static final boolean DEBUG = false;
    private static final String TAG = "ChannelDataManagerTest";

    // Wait time for expected success.
    private static final long WAIT_TIME_OUT_MS = 1000L;
    private static final String DUMMY_INPUT_ID = "dummy";
    private static final String COLUMN_BROWSABLE = "browsable";
    private static final String COLUMN_LOCKED = "locked";

    private ChannelDataManager mChannelDataManager;
    private TestChannelDataManagerListener mListener;
    private FakeContentResolver mContentResolver;
    private FakeContentProvider mContentProvider;

    @Before
    public void setUp() {
        assertWithMessage("More than 2 channels to test")
                .that(Constants.UNIT_TEST_CHANNEL_COUNT > 2)
                .isTrue();

        mContentProvider = new FakeContentProvider(getTargetContext());
        mContentResolver = new FakeContentResolver();
        mContentResolver.addProvider(TvContract.AUTHORITY, mContentProvider);
        mListener = new TestChannelDataManagerListener();
    getInstrumentation()
        .runOnMainSync(
            new Runnable() {
              @Override
              public void run() {
                TvInputManagerHelper mockHelper = Mockito.mock(TvInputManagerHelper.class);
                Mockito.when(mockHelper.hasTvInputInfo(ArgumentMatchers.anyString()))
                    .thenReturn(true);
                Context mockContext = Mockito.mock(Context.class);
                Mockito.when(mockContext.getContentResolver()).thenReturn(mContentResolver);
                Mockito.when(mockContext.checkSelfPermission(ArgumentMatchers.anyString()))
                    .thenAnswer(
                        invocation -> {
                          Object[] args = invocation.getArguments();
                          return getTargetContext().checkSelfPermission(((String) args[0]));
                        });

                mChannelDataManager =
                    new ChannelDataManager(
                        mockContext, mockHelper, AsyncTask.SERIAL_EXECUTOR, mContentResolver);
                mChannelDataManager.addListener(mListener);
              }
            });
    }

    @After
    public void tearDown() {
        getInstrumentation()
                .runOnMainSync(
                        new Runnable() {
                            @Override
                            public void run() {
                                mChannelDataManager.stop();
                            }
                        });
    }

    private void startAndWaitForComplete() throws InterruptedException {
        getInstrumentation()
                .runOnMainSync(
                        new Runnable() {
                            @Override
                            public void run() {
                                mChannelDataManager.start();
                            }
                        });
        assertThat(mListener.loadFinishedLatch.await(WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS))
                .isTrue();
    }

    private void restart() throws InterruptedException {
        getInstrumentation()
                .runOnMainSync(
                        new Runnable() {
                            @Override
                            public void run() {
                                mChannelDataManager.stop();
                                mListener.reset();
                            }
                        });
        startAndWaitForComplete();
    }

    @Test
    public void testIsDbLoadFinished() throws InterruptedException {
        startAndWaitForComplete();
        assertThat(mChannelDataManager.isDbLoadFinished()).isTrue();
    }

    /**
     * Test for following methods - {@link ChannelDataManager#getChannelCount} - {@link
     * ChannelDataManager#getChannelList} - {@link ChannelDataManager#getChannel}
     */
    @Test
    public void testGetChannels() throws InterruptedException {
        startAndWaitForComplete();

        // Test {@link ChannelDataManager#getChannelCount}
        assertThat(mChannelDataManager.getChannelCount())
                .isEqualTo(Constants.UNIT_TEST_CHANNEL_COUNT);

        // Test {@link ChannelDataManager#getChannelList}
        List<ChannelInfo> channelInfoList = new ArrayList<>();
        for (int i = 1; i <= Constants.UNIT_TEST_CHANNEL_COUNT; i++) {
            channelInfoList.add(ChannelInfo.create(getTargetContext(), i));
        }
        List<Channel> channelList = mChannelDataManager.getChannelList();
        for (Channel channel : channelList) {
            boolean found = false;
            for (ChannelInfo channelInfo : channelInfoList) {
                if (TextUtils.equals(channelInfo.name, channel.getDisplayName())) {
                    found = true;
                    channelInfoList.remove(channelInfo);
                    break;
                }
            }
            assertWithMessage("Cannot find (" + channel + ")").that(found).isTrue();
        }

        // Test {@link ChannelDataManager#getChannelIndex()}
        for (Channel channel : channelList) {
            assertThat(mChannelDataManager.getChannel(channel.getId())).isEqualTo(channel);
        }
    }

    /** Test for {@link ChannelDataManager#getChannelCount} when no channel is available. */
    @Test
    public void testGetChannels_noChannels() throws InterruptedException {
        mContentProvider.clear();
        startAndWaitForComplete();
        assertThat(mChannelDataManager.getChannelCount()).isEqualTo(0);
    }

    /**
     * Test for following methods and channel listener with notifying change. - {@link
     * ChannelDataManager#updateBrowsable} - {@link ChannelDataManager#applyUpdatedValuesToDb}
     */
    @Test
    public void testBrowsable() throws InterruptedException {
        startAndWaitForComplete();

        // Test if all channels are browsable
        List<Channel> channelList = mChannelDataManager.getChannelList();
        List<Channel> browsableChannelList = mChannelDataManager.getBrowsableChannelList();
        for (Channel browsableChannel : browsableChannelList) {
            boolean found = channelList.remove(browsableChannel);
            assertWithMessage("Cannot find (" + browsableChannel + ")").that(found).isTrue();
        }
        assertThat(channelList).isEmpty();

        // Prepare for next tests.
        channelList = mChannelDataManager.getChannelList();
        TestChannelDataManagerChannelListener channelListener =
                new TestChannelDataManagerChannelListener();
        Channel channel1 = channelList.get(0);
        mChannelDataManager.addChannelListener(channel1.getId(), channelListener);

        // Test {@link ChannelDataManager#updateBrowsable} & notification.
        mChannelDataManager.updateBrowsable(channel1.getId(), false, false);
        assertThat(mListener.channelBrowsableChangedCalled).isTrue();
        assertThat(mChannelDataManager.getBrowsableChannelList()).doesNotContain(channel1);
        MoreAsserts.assertContentsInAnyOrder(channelListener.updatedChannels, channel1);
        channelListener.reset();

        // Test {@link ChannelDataManager#applyUpdatedValuesToDb}
        // Disable the update notification to avoid the unwanted call of "onLoadFinished".
        mContentResolver.mNotifyDisabled = true;
        mChannelDataManager.applyUpdatedValuesToDb();
        restart();
        browsableChannelList = mChannelDataManager.getBrowsableChannelList();
        assertThat(browsableChannelList).hasSize(Constants.UNIT_TEST_CHANNEL_COUNT - 1);
        assertThat(browsableChannelList).doesNotContain(channel1);
    }

    /**
     * Test for following methods and channel listener without notifying change. - {@link
     * ChannelDataManager#updateBrowsable} - {@link ChannelDataManager#applyUpdatedValuesToDb}
     */
    @Test
    public void testBrowsable_skipNotification() throws InterruptedException {
        startAndWaitForComplete();

        List<Channel> channels = mChannelDataManager.getChannelList();
        // Prepare for next tests.
        TestChannelDataManagerChannelListener channelListener =
                new TestChannelDataManagerChannelListener();
        Channel channel1 = channels.get(0);
        Channel channel2 = channels.get(1);
        mChannelDataManager.addChannelListener(channel1.getId(), channelListener);
        mChannelDataManager.addChannelListener(channel2.getId(), channelListener);

        // Test {@link ChannelDataManager#updateBrowsable} & skip notification.
        mChannelDataManager.updateBrowsable(channel1.getId(), false, true);
        mChannelDataManager.updateBrowsable(channel2.getId(), false, true);
        mChannelDataManager.updateBrowsable(channel1.getId(), true, true);
        assertThat(mListener.channelBrowsableChangedCalled).isFalse();
        List<Channel> browsableChannelList = mChannelDataManager.getBrowsableChannelList();
        assertThat(browsableChannelList).contains(channel1);
        assertThat(browsableChannelList).doesNotContain(channel2);

        // Test {@link ChannelDataManager#applyUpdatedValuesToDb}
        // Disable the update notification to avoid the unwanted call of "onLoadFinished".
        mContentResolver.mNotifyDisabled = true;
        mChannelDataManager.applyUpdatedValuesToDb();
        restart();
        browsableChannelList = mChannelDataManager.getBrowsableChannelList();
        assertThat(browsableChannelList).hasSize(Constants.UNIT_TEST_CHANNEL_COUNT - 1);
        assertThat(browsableChannelList).doesNotContain(channel2);
    }

    /**
     * Test for following methods and channel listener. - {@link ChannelDataManager#updateLocked} -
     * {@link ChannelDataManager#applyUpdatedValuesToDb}
     */
    @Test
    public void testLocked() throws InterruptedException {
        startAndWaitForComplete();

        // Test if all channels aren't locked at the first time.
        List<Channel> channelList = mChannelDataManager.getChannelList();
        for (Channel channel : channelList) {
            assertWithMessage(channel + " is locked").that(channel.isLocked()).isFalse();
        }

        // Prepare for next tests.
        Channel channel = mChannelDataManager.getChannelList().get(0);

        // Test {@link ChannelDataManager#updateLocked}
        mChannelDataManager.updateLocked(channel.getId(), true);
        assertThat(mChannelDataManager.getChannel(channel.getId()).isLocked()).isTrue();

        // Test {@link ChannelDataManager#applyUpdatedValuesToDb}.
        // Disable the update notification to avoid the unwanted call of "onLoadFinished".
        mContentResolver.mNotifyDisabled = true;
        mChannelDataManager.applyUpdatedValuesToDb();
        restart();
        assertThat(mChannelDataManager.getChannel(channel.getId()).isLocked()).isTrue();

        // Cleanup
        mChannelDataManager.updateLocked(channel.getId(), false);
    }

    /** Test ChannelDataManager when channels in TvContract are updated, removed, or added. */
    @Test
    public void testChannelListChanged() throws InterruptedException {
        startAndWaitForComplete();

        // Test channel add.
        mListener.reset();
        long testChannelId = Constants.UNIT_TEST_CHANNEL_COUNT + 1;
        ChannelInfo testChannelInfo = ChannelInfo.create(getTargetContext(), (int) testChannelId);
        testChannelId = Constants.UNIT_TEST_CHANNEL_COUNT + 1;
        mContentProvider.simulateInsert(testChannelInfo);
        assertThat(mListener.channelListUpdatedLatch.await(WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS))
                .isTrue();
        assertThat(mChannelDataManager.getChannelCount())
                .isEqualTo(Constants.UNIT_TEST_CHANNEL_COUNT + 1);

        // Test channel update
        mListener.reset();
        TestChannelDataManagerChannelListener channelListener =
                new TestChannelDataManagerChannelListener();
        mChannelDataManager.addChannelListener(testChannelId, channelListener);
        String newName = testChannelInfo.name + "_test";
        mContentProvider.simulateUpdate(testChannelId, newName);
        assertThat(mListener.channelListUpdatedLatch.await(WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS))
                .isTrue();
        assertThat(
                        channelListener.channelChangedLatch.await(
                                WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS))
                .isTrue();
        assertThat(channelListener.removedChannels).isEmpty();
        assertThat(channelListener.updatedChannels).hasSize(1);
        Channel updatedChannel = channelListener.updatedChannels.get(0);
        assertThat(updatedChannel.getId()).isEqualTo(testChannelId);
        assertThat(updatedChannel.getDisplayNumber()).isEqualTo(testChannelInfo.number);
        assertThat(updatedChannel.getDisplayName()).isEqualTo(newName);
        assertThat(mChannelDataManager.getChannelCount())
                .isEqualTo(Constants.UNIT_TEST_CHANNEL_COUNT + 1);

        // Test channel remove.
        mListener.reset();
        channelListener.reset();
        mContentProvider.simulateDelete(testChannelId);
        assertThat(mListener.channelListUpdatedLatch.await(WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS))
                .isTrue();
        assertThat(
                        channelListener.channelChangedLatch.await(
                                WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS))
                .isTrue();
        assertThat(channelListener.removedChannels).hasSize(1);
        assertThat(channelListener.updatedChannels).isEmpty();
        Channel removedChannel = channelListener.removedChannels.get(0);
        assertThat(removedChannel.getDisplayName()).isEqualTo(newName);
        assertThat(removedChannel.getDisplayNumber()).isEqualTo(testChannelInfo.number);
        assertThat(mChannelDataManager.getChannelCount())
                .isEqualTo(Constants.UNIT_TEST_CHANNEL_COUNT);
    }

    private static class ChannelInfoWrapper {
        public ChannelInfo channelInfo;
        public boolean browsable;
        public boolean locked;

        public ChannelInfoWrapper(ChannelInfo channelInfo) {
            this.channelInfo = channelInfo;
            browsable = true;
            locked = false;
        }
    }

    private class FakeContentResolver extends MockContentResolver {
        boolean mNotifyDisabled;

        @Override
        public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) {
            super.notifyChange(uri, observer, syncToNetwork);
            if (DEBUG) {
                Log.d(
                        TAG,
                        "onChanged(uri="
                                + uri
                                + ", observer="
                                + observer
                                + ") - Notification "
                                + (mNotifyDisabled ? "disabled" : "enabled"));
            }
            if (mNotifyDisabled) {
                return;
            }
            // Do not call {@link ContentObserver#onChange} directly to run it on the correct
            // thread.
            if (observer != null) {
                observer.dispatchChange(false, uri);
            } else {
                mChannelDataManager.getContentObserver().dispatchChange(false, uri);
            }
        }
    }

    // This implements the minimal methods in content resolver
    // and detailed assumptions are written in each method.
    private class FakeContentProvider extends MockContentProvider {
        private final SparseArray<ChannelInfoWrapper> mChannelInfoList = new SparseArray<>();

        public FakeContentProvider(Context context) {
            super(context);
            for (int i = 1; i <= Constants.UNIT_TEST_CHANNEL_COUNT; i++) {
                mChannelInfoList.put(
                        i, new ChannelInfoWrapper(ChannelInfo.create(getTargetContext(), i)));
            }
        }

        @Override
        public AssetFileDescriptor openTypedAssetFile(Uri url, String mimeType, Bundle opts) {
            try {
                return getTargetContext().getContentResolver().openAssetFileDescriptor(url, "r");
            } catch (FileNotFoundException e) {
                return null;
            }
        }

        /**
         * Implementation of {@link ContentProvider#query}. This assumes that {@link
         * ChannelDataManager} queries channels with empty {@code selection}. (i.e. channels are
         * always queries for all)
         */
        @Override
        public Cursor query(
                Uri uri,
                String[] projection,
                String selection,
                String[] selectionArgs,
                String sortOrder) {
            if (DEBUG) {
                Log.d(TAG, "dump query");
                Log.d(TAG, "  uri=" + uri);
                Log.d(TAG, "  projection=" + Arrays.toString(projection));
                Log.d(TAG, "  selection=" + selection);
            }
            assertChannelUri(uri);
            return new FakeCursor(projection);
        }

        /**
         * Implementation of {@link ContentProvider#update}. This assumes that {@link
         * ChannelDataManager} update channels only for changing browsable and locked.
         */
        @Override
        public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
            if (DEBUG) Log.d(TAG, "update(uri=" + uri + ", selection=" + selection);
            assertChannelUri(uri);
            List<Long> channelIds = new ArrayList<>();
            try {
                long channelId = ContentUris.parseId(uri);
                channelIds.add(channelId);
            } catch (NumberFormatException e) {
                // Update for multiple channels.
                if (TextUtils.isEmpty(selection)) {
                    for (int i = 0; i < mChannelInfoList.size(); i++) {
                        channelIds.add((long) mChannelInfoList.keyAt(i));
                    }
                } else {
                    // See {@link Utils#buildSelectionForIds} for the syntax.
                    String selectionForId =
                            selection.substring(
                                    selection.indexOf("(") + 1, selection.lastIndexOf(")"));
                    String[] ids = selectionForId.split(", ");
                    if (ids != null) {
                        for (String id : ids) {
                            channelIds.add(Long.parseLong(id));
                        }
                    }
                }
            }
            int updateCount = 0;
            for (long channelId : channelIds) {
                boolean updated = false;
                ChannelInfoWrapper channel = mChannelInfoList.get((int) channelId);
                if (channel == null) {
                    return 0;
                }
                if (values.containsKey(COLUMN_BROWSABLE)) {
                    updated = true;
                    channel.browsable = (values.getAsInteger(COLUMN_BROWSABLE) == 1);
                }
                if (values.containsKey(COLUMN_LOCKED)) {
                    updated = true;
                    channel.locked = (values.getAsInteger(COLUMN_LOCKED) == 1);
                }
                updateCount += updated ? 1 : 0;
            }
            if (updateCount > 0) {
                if (channelIds.size() == 1) {
                    mContentResolver.notifyChange(uri, null);
                } else {
                    mContentResolver.notifyChange(Channels.CONTENT_URI, null);
                }
            } else {
                if (DEBUG) {
                    Log.d(TAG, "Update to channel(uri=" + uri + ") is ignored for " + values);
                }
            }
            return updateCount;
        }

        /**
         * Simulates channel data insert. This assigns original network ID (the same with channel
         * number) to channel ID.
         */
        public void simulateInsert(ChannelInfo testChannelInfo) {
            long channelId = testChannelInfo.originalNetworkId;
            mChannelInfoList.put(
                    (int) channelId,
                    new ChannelInfoWrapper(
                            ChannelInfo.create(getTargetContext(), (int) channelId)));
            mContentResolver.notifyChange(TvContract.buildChannelUri(channelId), null);
        }

        /** Simulates channel data delete. */
        public void simulateDelete(long channelId) {
            mChannelInfoList.remove((int) channelId);
            mContentResolver.notifyChange(TvContract.buildChannelUri(channelId), null);
        }

        /** Simulates channel data update. */
        public void simulateUpdate(long channelId, String newName) {
            ChannelInfoWrapper channel = mChannelInfoList.get((int) channelId);
            ChannelInfo.Builder builder = new ChannelInfo.Builder(channel.channelInfo);
            builder.setName(newName);
            channel.channelInfo = builder.build();
            mContentResolver.notifyChange(TvContract.buildChannelUri(channelId), null);
        }

        private void assertChannelUri(Uri uri) {
            assertWithMessage("Uri(" + uri + ") isn't channel uri")
                    .that(uri.toString().startsWith(Channels.CONTENT_URI.toString()))
                    .isTrue();
        }

        public void clear() {
            mChannelInfoList.clear();
        }

        public ChannelInfoWrapper get(int position) {
            return mChannelInfoList.get(mChannelInfoList.keyAt(position));
        }

        public int getCount() {
            return mChannelInfoList.size();
        }

        public long keyAt(int position) {
            return mChannelInfoList.keyAt(position);
        }
    }

    private class FakeCursor extends MockCursor {
        private final String[] allColumns = {
            Channels._ID,
            Channels.COLUMN_DISPLAY_NAME,
            Channels.COLUMN_DISPLAY_NUMBER,
            Channels.COLUMN_INPUT_ID,
            Channels.COLUMN_VIDEO_FORMAT,
            Channels.COLUMN_ORIGINAL_NETWORK_ID,
            COLUMN_BROWSABLE,
            COLUMN_LOCKED
        };
        private final String[] mColumns;
        private int mPosition;

        public FakeCursor(String[] columns) {
            mColumns = (columns == null) ? allColumns : columns;
            mPosition = -1;
        }

        @Override
        public String getColumnName(int columnIndex) {
            return mColumns[columnIndex];
        }

        @Override
        public int getColumnIndex(String columnName) {
            for (int i = 0; i < mColumns.length; i++) {
                if (mColumns[i].equalsIgnoreCase(columnName)) {
                    return i;
                }
            }
            return -1;
        }

        @Override
        public long getLong(int columnIndex) {
            String columnName = getColumnName(columnIndex);
            switch (columnName) {
                case Channels._ID:
                    return mContentProvider.keyAt(mPosition);
                default: // fall out
            }
            if (DEBUG) {
                Log.d(TAG, "Column (" + columnName + ") is ignored in getLong()");
            }
            return 0;
        }

        @Override
        public String getString(int columnIndex) {
            String columnName = getColumnName(columnIndex);
            ChannelInfoWrapper channel = mContentProvider.get(mPosition);
            switch (columnName) {
                case Channels.COLUMN_DISPLAY_NAME:
                    return channel.channelInfo.name;
                case Channels.COLUMN_DISPLAY_NUMBER:
                    return channel.channelInfo.number;
                case Channels.COLUMN_INPUT_ID:
                    return DUMMY_INPUT_ID;
                case Channels.COLUMN_VIDEO_FORMAT:
                    return channel.channelInfo.getVideoFormat();
                default: // fall out
            }
            if (DEBUG) {
                Log.d(TAG, "Column (" + columnName + ") is ignored in getString()");
            }
            return null;
        }

        @Override
        public int getInt(int columnIndex) {
            String columnName = getColumnName(columnIndex);
            ChannelInfoWrapper channel = mContentProvider.get(mPosition);
            switch (columnName) {
                case Channels.COLUMN_ORIGINAL_NETWORK_ID:
                    return channel.channelInfo.originalNetworkId;
                case COLUMN_BROWSABLE:
                    return channel.browsable ? 1 : 0;
                case COLUMN_LOCKED:
                    return channel.locked ? 1 : 0;
                default: // fall out
            }
            if (DEBUG) {
                Log.d(TAG, "Column (" + columnName + ") is ignored in getInt()");
            }
            return 0;
        }

        @Override
        public int getCount() {
            return mContentProvider.getCount();
        }

        @Override
        public boolean moveToNext() {
            return ++mPosition < mContentProvider.getCount();
        }

        @Override
        public void close() {
            // No-op.
        }
    }

    private static class TestChannelDataManagerListener implements ChannelDataManager.Listener {
        public CountDownLatch loadFinishedLatch = new CountDownLatch(1);
        public CountDownLatch channelListUpdatedLatch = new CountDownLatch(1);
        public boolean channelBrowsableChangedCalled;

        @Override
        public void onLoadFinished() {
            loadFinishedLatch.countDown();
        }

        @Override
        public void onChannelListUpdated() {
            channelListUpdatedLatch.countDown();
        }

        @Override
        public void onChannelBrowsableChanged() {
            channelBrowsableChangedCalled = true;
        }

        public void reset() {
            loadFinishedLatch = new CountDownLatch(1);
            channelListUpdatedLatch = new CountDownLatch(1);
            channelBrowsableChangedCalled = false;
        }
    }

    private static class TestChannelDataManagerChannelListener
            implements ChannelDataManager.ChannelListener {
        public CountDownLatch channelChangedLatch = new CountDownLatch(1);
        public final List<Channel> removedChannels = new ArrayList<>();
        public final List<Channel> updatedChannels = new ArrayList<>();

        @Override
        public void onChannelRemoved(Channel channel) {
            removedChannels.add(channel);
            channelChangedLatch.countDown();
        }

        @Override
        public void onChannelUpdated(Channel channel) {
            updatedChannels.add(channel);
            channelChangedLatch.countDown();
        }

        public void reset() {
            channelChangedLatch = new CountDownLatch(1);
            removedChannels.clear();
            updatedChannels.clear();
        }
    }
}
