/* * 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 android.content.ContentProvider; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; 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.test.AndroidTestCase; import android.test.MoreAsserts; import android.test.UiThreadTest; import android.test.mock.MockContentProvider; import android.test.mock.MockContentResolver; import android.test.mock.MockCursor; import android.test.suitebuilder.annotation.SmallTest; import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; import com.android.tv.testing.ChannelInfo; import com.android.tv.testing.Constants; import com.android.tv.testing.Utils; import com.android.tv.util.TvInputManagerHelper; import org.mockito.Matchers; import org.mockito.Mockito; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** * Test for {@link ChannelDataManager} * * 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 public class ChannelDataManagerTest extends AndroidTestCase { 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"; // TODO: Use Channels.COLUMN_BROWSABLE and Channels.COLUMN_LOCKED instead. 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; @Override protected void setUp() throws Exception { super.setUp(); assertTrue("More than 2 channels to test", Constants.UNIT_TEST_CHANNEL_COUNT > 2); mContentProvider = new FakeContentProvider(getContext()); mContentResolver = new FakeContentResolver(); mContentResolver.addProvider(TvContract.AUTHORITY, mContentProvider); mListener = new TestChannelDataManagerListener(); Utils.runOnMainSync(new Runnable() { @Override public void run() { TvInputManagerHelper mockHelper = Mockito.mock(TvInputManagerHelper.class); Mockito.when(mockHelper.hasTvInputInfo(Matchers.anyString())).thenReturn(true); mChannelDataManager = new ChannelDataManager(getContext(), mockHelper, mContentResolver); mChannelDataManager.addListener(mListener); } }); } @Override protected void tearDown() throws Exception { Utils.runOnMainSync(new Runnable() { @Override public void run() { mChannelDataManager.stop(); } }); super.tearDown(); } private void startAndWaitForComplete() throws Exception { mChannelDataManager.start(); try { assertTrue(mListener.loadFinishedLatch.await(WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS)); } catch (InterruptedException e) { throw e; } } private void restart() throws Exception { mChannelDataManager.stop(); mListener.reset(); startAndWaitForComplete(); } @UiThreadTest public void testIsDbLoadFinished() throws Exception { startAndWaitForComplete(); assertTrue(mChannelDataManager.isDbLoadFinished()); } /** * Test for following methods * - {@link ChannelDataManager#getChannelCount} * - {@link ChannelDataManager#getChannelList} * - {@link ChannelDataManager#getChannel} */ @UiThreadTest public void testGetChannels() throws Exception { startAndWaitForComplete(); // Test {@link ChannelDataManager#getChannelCount} assertEquals(Constants.UNIT_TEST_CHANNEL_COUNT, mChannelDataManager.getChannelCount()); // Test {@link ChannelDataManager#getChannelList} List channelInfoList = new ArrayList<>(); for (int i = 1; i <= Constants.UNIT_TEST_CHANNEL_COUNT; i++) { channelInfoList.add(ChannelInfo.create(getContext(), i)); } List channelList = mChannelDataManager.getChannelList(); for (Channel channel : channelList) { boolean found = false; for (ChannelInfo channelInfo : channelInfoList) { if (TextUtils.equals(channelInfo.name, channel.getDisplayName()) && TextUtils.equals(channelInfo.name, channel.getDisplayName())) { found = true; channelInfoList.remove(channelInfo); break; } } assertTrue("Cannot find (" + channel + ")", found); } // Test {@link ChannelDataManager#getChannelIndex()} for (Channel channel : channelList) { assertEquals(channel, mChannelDataManager.getChannel(channel.getId())); } } /** * Test for {@link ChannelDataManager#getChannelCount} when no channel is available. */ @UiThreadTest public void testGetChannels_noChannels() throws Exception { mContentProvider.clear(); startAndWaitForComplete(); assertEquals(0, mChannelDataManager.getChannelCount()); } /** * Test for following methods and channel listener with notifying change. * - {@link ChannelDataManager#updateBrowsable} * - {@link ChannelDataManager#applyUpdatedValuesToDb} */ @UiThreadTest public void testBrowsable() throws Exception { startAndWaitForComplete(); // Test if all channels are browsable List channelList = new ArrayList<>(mChannelDataManager.getChannelList()); List browsableChannelList = mChannelDataManager.getBrowsableChannelList(); for (Channel browsableChannel : browsableChannelList) { boolean found = channelList.remove(browsableChannel); assertTrue("Cannot find (" + browsableChannel + ")", found); } assertEquals(0, channelList.size()); // Prepare for next tests. TestChannelDataManagerChannelListener channelListener = new TestChannelDataManagerChannelListener(); Channel channel1 = mChannelDataManager.getChannelList().get(0); mChannelDataManager.addChannelListener(channel1.getId(), channelListener); // Test {@link ChannelDataManager#updateBrowsable} & notification. mChannelDataManager.updateBrowsable(channel1.getId(), false, false); assertTrue(mListener.channelBrowsableChangedCalled); assertFalse(mChannelDataManager.getBrowsableChannelList().contains(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(); assertEquals(Constants.UNIT_TEST_CHANNEL_COUNT - 1, browsableChannelList.size()); assertFalse(browsableChannelList.contains(channel1)); } /** * Test for following methods and channel listener without notifying change. * - {@link ChannelDataManager#updateBrowsable} * - {@link ChannelDataManager#applyUpdatedValuesToDb} */ @UiThreadTest public void testBrowsable_skipNotification() throws Exception { startAndWaitForComplete(); // Prepare for next tests. TestChannelDataManagerChannelListener channelListener = new TestChannelDataManagerChannelListener(); Channel channel1 = mChannelDataManager.getChannelList().get(0); Channel channel2 = mChannelDataManager.getChannelList().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); assertFalse(mListener.channelBrowsableChangedCalled); List browsableChannelList = mChannelDataManager.getBrowsableChannelList(); assertTrue(browsableChannelList.contains(channel1)); assertFalse(browsableChannelList.contains(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(); assertEquals(Constants.UNIT_TEST_CHANNEL_COUNT - 1, browsableChannelList.size()); assertFalse(browsableChannelList.contains(channel2)); } /** * Test for following methods and channel listener. * - {@link ChannelDataManager#updateLocked} * - {@link ChannelDataManager#applyUpdatedValuesToDb} */ @UiThreadTest public void testLocked() throws Exception { startAndWaitForComplete(); // Test if all channels aren't locked at the first time. List channelList = mChannelDataManager.getChannelList(); for (Channel channel : channelList) { assertFalse(channel + " is locked", channel.isLocked()); } // Prepare for next tests. Channel channel = mChannelDataManager.getChannelList().get(0); // Test {@link ChannelDataManager#updateLocked} mChannelDataManager.updateLocked(channel.getId(), true); assertTrue(mChannelDataManager.getChannel(channel.getId()).isLocked()); // Test {@link ChannelDataManager#applyUpdatedValuesToDb}. // Disable the update notification to avoid the unwanted call of "onLoadFinished". mContentResolver.mNotifyDisabled = true; mChannelDataManager.applyUpdatedValuesToDb(); restart(); assertTrue(mChannelDataManager.getChannel(channel.getId()).isLocked()); // Cleanup mChannelDataManager.updateLocked(channel.getId(), false); } /** * Test ChannelDataManager when channels in TvContract are updated, removed, or added. */ @UiThreadTest public void testChannelListChanged() throws Exception { startAndWaitForComplete(); // Test channel add. mListener.reset(); long testChannelId = Constants.UNIT_TEST_CHANNEL_COUNT + 1; ChannelInfo testChannelInfo = ChannelInfo.create(getContext(), (int) testChannelId); testChannelId = Constants.UNIT_TEST_CHANNEL_COUNT + 1; mContentProvider.simulateInsert(testChannelInfo); assertTrue( mListener.channelListUpdatedLatch.await(WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS)); assertEquals(Constants.UNIT_TEST_CHANNEL_COUNT + 1, mChannelDataManager.getChannelCount()); // Test channel update mListener.reset(); TestChannelDataManagerChannelListener channelListener = new TestChannelDataManagerChannelListener(); mChannelDataManager.addChannelListener(testChannelId, channelListener); String newName = testChannelInfo.name + "_test"; mContentProvider.simulateUpdate(testChannelId, newName); assertTrue( mListener.channelListUpdatedLatch.await(WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS)); assertTrue( channelListener.channelChangedLatch.await(WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS)); assertEquals(0, channelListener.removedChannels.size()); assertEquals(1, channelListener.updatedChannels.size()); Channel updatedChannel = channelListener.updatedChannels.get(0); assertEquals(testChannelId, updatedChannel.getId()); assertEquals(testChannelInfo.number, updatedChannel.getDisplayNumber()); assertEquals(newName, updatedChannel.getDisplayName()); assertEquals(Constants.UNIT_TEST_CHANNEL_COUNT + 1, mChannelDataManager.getChannelCount()); // Test channel remove. mListener.reset(); channelListener.reset(); mContentProvider.simulateDelete(testChannelId); assertTrue( mListener.channelListUpdatedLatch.await(WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS)); assertTrue( channelListener.channelChangedLatch.await(WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS)); assertEquals(1, channelListener.removedChannels.size()); assertEquals(0, channelListener.updatedChannels.size()); Channel removedChannel = channelListener.removedChannels.get(0); assertEquals(newName, removedChannel.getDisplayName()); assertEquals(testChannelInfo.number, removedChannel.getDisplayNumber()); assertEquals(Constants.UNIT_TEST_CHANNEL_COUNT, mChannelDataManager.getChannelCount()); } private 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 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(getContext(), i))); } } /** * 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 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(getContext(), (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) { assertTrue("Uri(" + uri + ") isn't channel uri", uri.toString().startsWith(Channels.CONTENT_URI.toString())); } 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[] ALL_COLUMNS = { 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) ? ALL_COLUMNS : 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); } 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(); } 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; } 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 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 class TestChannelDataManagerChannelListener implements ChannelDataManager.ChannelListener { public CountDownLatch channelChangedLatch = new CountDownLatch(1); public final List removedChannels = new ArrayList<>(); public final List 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(); } } }