/* * 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} * *

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 channelInfoList = new ArrayList<>(); for (int i = 1; i <= Constants.UNIT_TEST_CHANNEL_COUNT; i++) { channelInfoList.add(ChannelInfo.create(getTargetContext(), i)); } List 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 channelList = mChannelDataManager.getChannelList(); List 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 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 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 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 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 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 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(); } } }