diff options
Diffstat (limited to 'tests/common/src/com/android/tv/testing')
47 files changed, 5182 insertions, 570 deletions
diff --git a/tests/common/src/com/android/tv/testing/ChannelNumberSubject.java b/tests/common/src/com/android/tv/testing/ChannelNumberSubject.java new file mode 100644 index 00000000..ba4662ee --- /dev/null +++ b/tests/common/src/com/android/tv/testing/ChannelNumberSubject.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2017 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.testing; + +import android.support.annotation.Nullable; +import com.android.tv.data.ChannelNumber; +import com.google.common.truth.ComparableSubject; +import com.google.common.truth.FailureMetadata; +import com.google.common.truth.Subject; +import com.google.common.truth.Truth; + +/** Propositions for {@link ChannelNumber} subjects. */ +public final class ChannelNumberSubject + extends ComparableSubject<ChannelNumberSubject, ChannelNumber> { + private static final Subject.Factory<ChannelNumberSubject, ChannelNumber> FACTORY = + ChannelNumberSubject::new; + + public static Subject.Factory<ChannelNumberSubject, ChannelNumber> channelNumbers() { + return FACTORY; + } + + public static ChannelNumberSubject assertThat(@Nullable ChannelNumber actual) { + return Truth.assertAbout(channelNumbers()).that(actual); + } + + public ChannelNumberSubject(FailureMetadata failureMetadata, @Nullable ChannelNumber subject) { + super(failureMetadata, subject); + } + + public void displaysAs(int major) { + if (!getSubject().majorNumber.equals(Integer.toString(major)) + || getSubject().hasDelimiter) { + fail("displaysAs", major); + } + } + + public void displaysAs(int major, int minor) { + if (!getSubject().majorNumber.equals(Integer.toString(major)) + || !getSubject().minorNumber.equals(Integer.toString(minor)) + || !getSubject().hasDelimiter) { + fail("displaysAs", major + "-" + minor); + } + } + + public void isEmpty() { + if (!getSubject().majorNumber.isEmpty() + || !getSubject().minorNumber.isEmpty() + || getSubject().hasDelimiter) { + fail("isEmpty"); + } + } +} diff --git a/tests/common/src/com/android/tv/testing/ComparableTester.java b/tests/common/src/com/android/tv/testing/ComparableTester.java index fe6e72f5..4328deb9 100644 --- a/tests/common/src/com/android/tv/testing/ComparableTester.java +++ b/tests/common/src/com/android/tv/testing/ComparableTester.java @@ -16,22 +16,19 @@ package com.android.tv.testing; -import junit.framework.Assert; - import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; +import junit.framework.Assert; /** * Tester for {@link java.lang.Comparable}s. * - * <p> - * To use, create a new {@link ComparableTester} and add comparable groups - * where each group contains objects that are - * {@link java.util.Comparator#compare(Object, Object)} == 0 to each other. - * Groups are added in order asserting that all earlier groups have compare < 0 - * for all later groups. + * <p>To use, create a new {@link ComparableTester} and add comparable groups where each group + * contains objects that are {@link java.util.Comparator#compare(Object, Object)} == 0 to each + * other. Groups are added in order asserting that all earlier groups have compare < 0 for all later + * groups. * * <pre>{@code * new ComparableTester<String>() @@ -39,8 +36,7 @@ import java.util.List; * .addEquivalentGroup("World", "wORLD") * .addEquivalentGroup("ZEBRA") * .test(); - * } - * </pre> + * }</pre> * * @param <T> the type of objects to compare. */ @@ -74,8 +70,8 @@ public class ComparableTester<T extends Comparable<T>> { assertMore(more, less, moreGroup, lessGroup); } - private void assertLess(int left, int right, Collection<T> leftGroup, - Collection<T> rightGroup) { + private void assertLess( + int left, int right, Collection<T> leftGroup, Collection<T> rightGroup) { int leftSub = 0; for (T leftItem : leftGroup) { int rightSub = 0; @@ -83,14 +79,22 @@ public class ComparableTester<T extends Comparable<T>> { for (T rightItem : rightGroup) { String rightName = "Item[" + right + "," + (rightSub++) + "]"; Assert.assertEquals( - leftName + " " + leftItem + " compareTo " + rightName + " " + rightItem - + " is <0", true, leftItem.compareTo(rightItem) < 0); + leftName + + " " + + leftItem + + " compareTo " + + rightName + + " " + + rightItem + + " is <0", + true, + leftItem.compareTo(rightItem) < 0); } } } - private void assertMore(int left, int right, Collection<T> leftGroup, - Collection<T> rightGroup) { + private void assertMore( + int left, int right, Collection<T> leftGroup, Collection<T> rightGroup) { int leftSub = 0; for (T leftItem : leftGroup) { int rightSub = 0; @@ -98,8 +102,16 @@ public class ComparableTester<T extends Comparable<T>> { for (T rightItem : rightGroup) { String rightName = "Item[" + right + "," + (rightSub++) + "]"; Assert.assertEquals( - leftName + " " + leftItem + " compareTo " + rightName + " " + rightItem - + " is >0", true, leftItem.compareTo(rightItem) > 0); + leftName + + " " + + leftItem + + " compareTo " + + rightName + + " " + + rightItem + + " is >0", + true, + leftItem.compareTo(rightItem) > 0); } } } diff --git a/tests/common/src/com/android/tv/testing/ComparatorTester.java b/tests/common/src/com/android/tv/testing/ComparatorTester.java index 3774532f..6ebd8b4e 100644 --- a/tests/common/src/com/android/tv/testing/ComparatorTester.java +++ b/tests/common/src/com/android/tv/testing/ComparatorTester.java @@ -27,12 +27,9 @@ import java.util.List; /** * Tester for {@link Comparator} relationships between groups of T. * - * <p> - * To use, create a new {@link ComparatorTester} and add comparable groups - * where each group contains objects that are - * {@link Comparator#compare(Object, Object)} == 0 to each other. - * Groups are added in order asserting that all earlier groups have compare < 0 - * for all later groups. + * <p>To use, create a new {@link ComparatorTester} and add comparable groups where each group + * contains objects that are {@link Comparator#compare(Object, Object)} == 0 to each other. Groups + * are added in order asserting that all earlier groups have compare < 0 for all later groups. * * <pre>{@code * ComparatorTester @@ -41,8 +38,7 @@ import java.util.List; * .addComparableGroup("World", "wORLD") * .addComparableGroup("ZEBRA") * .test(); - * } - * </pre> + * }</pre> * * @param <T> the type of objects to compare. */ @@ -52,7 +48,6 @@ public class ComparatorTester<T> { private final Comparator<T> comparator; - public static <T> ComparatorTester<T> withoutEqualsTest(Comparator<T> comparator) { return new ComparatorTester<>(comparator); } @@ -80,7 +75,7 @@ public class ComparatorTester<T> { assertOrder(i, j, currentGroup, rhs); } } - //TODO: also test equals + // TODO: also test equals } private void assertOrder(int less, int more, List<T> lessGroup, List<T> moreGroup) { @@ -88,30 +83,48 @@ public class ComparatorTester<T> { assertMore(more, less, moreGroup, lessGroup); } - private void assertLess(int left, int right, Collection<T> leftGroup, - Collection<T> rightGroup) { + private void assertLess( + int left, int right, Collection<T> leftGroup, Collection<T> rightGroup) { int leftSub = 0; for (T leftItem : leftGroup) { int rightSub = 0; for (T rightItem : rightGroup) { String leftName = "Item[" + left + "," + (leftSub++) + "]"; String rName = "Item[" + right + "," + (rightSub++) + "]"; - assertEquals(leftName + " " + leftItem + " compareTo " + rName + " " + rightItem - + " is <0", true, comparator.compare(leftItem, rightItem) < 0); + assertEquals( + leftName + + " " + + leftItem + + " compareTo " + + rName + + " " + + rightItem + + " is <0", + true, + comparator.compare(leftItem, rightItem) < 0); } } } - private void assertMore(int left, int right, Collection<T> leftGroup, - Collection<T> rightGroup) { + private void assertMore( + int left, int right, Collection<T> leftGroup, Collection<T> rightGroup) { int leftSub = 0; for (T leftItem : leftGroup) { int rightSub = 0; for (T rightItem : rightGroup) { String leftName = "Item[" + left + "," + (leftSub++) + "]"; String rName = "Item[" + right + "," + (rightSub++) + "]"; - assertEquals(leftName + " " + leftItem + " compareTo " + rName + " " + rightItem - + " is >0", true, comparator.compare(leftItem, rightItem) > 0); + assertEquals( + leftName + + " " + + leftItem + + " compareTo " + + rName + + " " + + rightItem + + " is >0", + true, + comparator.compare(leftItem, rightItem) > 0); } } } @@ -120,9 +133,11 @@ public class ComparatorTester<T> { // Test everything against everything in both directions, including against itself. for (T leftItem : group) { for (T rightItem : group) { - assertEquals(leftItem + " compareTo " + rightItem, 0, + assertEquals( + leftItem + " compareTo " + rightItem, + 0, comparator.compare(leftItem, rightItem)); } } } -}
\ No newline at end of file +} diff --git a/tests/common/src/com/android/tv/testing/Constants.java b/tests/common/src/com/android/tv/testing/Constants.java deleted file mode 100644 index 4c9cb5fb..00000000 --- a/tests/common/src/com/android/tv/testing/Constants.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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.testing; - -import android.media.tv.TvTrackInfo; - -/** - * Constants for testing. - */ -public final class Constants { - public static final int FUNC_TEST_CHANNEL_COUNT = 100; - public static final int UNIT_TEST_CHANNEL_COUNT = 4; - public static final int JANK_TEST_CHANNEL_COUNT = 500; // TODO: increase to 1000 see b/23526997 - - public static final TvTrackInfo EN_STEREO_AUDIO_TRACK = new TvTrackInfo.Builder( - TvTrackInfo.TYPE_AUDIO, "English Stereo Audio").setLanguage("en") - .setAudioChannelCount(2).build(); - public static final TvTrackInfo GENERIC_AUDIO_TRACK = new TvTrackInfo.Builder( - TvTrackInfo.TYPE_AUDIO, "Generic Audio").build(); - - public static final TvTrackInfo FHD1080P50_VIDEO_TRACK = new TvTrackInfo.Builder( - TvTrackInfo.TYPE_VIDEO, "FHD Video").setVideoHeight(1080).setVideoWidth(1920) - .setVideoFrameRate(50).build(); - public static final TvTrackInfo SVGA_VIDEO_TRACK = new TvTrackInfo.Builder( - TvTrackInfo.TYPE_VIDEO, "SVGA Video").setVideoHeight(600).setVideoWidth(800).build(); - - private Constants() { - } -} diff --git a/tests/common/src/com/android/tv/testing/DbTestingUtils.java b/tests/common/src/com/android/tv/testing/DbTestingUtils.java new file mode 100644 index 00000000..53e26ca7 --- /dev/null +++ b/tests/common/src/com/android/tv/testing/DbTestingUtils.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2017 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.testing; + +import android.database.Cursor; +import java.util.ArrayList; +import java.util.List; + +/** Static utilities for testing using databases. */ +public final class DbTestingUtils { + + public static List<List<String>> toList(Cursor cursor) { + ArrayList<List<String>> result = new ArrayList<>(); + int colCount = cursor.getColumnCount(); + while (cursor.moveToNext()) { + List<String> row = new ArrayList<>(colCount); + for (int i = 0; i < colCount; i++) { + row.add(cursor.getString(i)); + } + result.add(row); + } + return result; + } + + private DbTestingUtils() {} +} diff --git a/tests/common/src/com/android/tv/testing/EpgTestData.java b/tests/common/src/com/android/tv/testing/EpgTestData.java new file mode 100644 index 00000000..49a92181 --- /dev/null +++ b/tests/common/src/com/android/tv/testing/EpgTestData.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2017 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.testing; + +import com.android.tv.data.ChannelImpl; +import com.android.tv.data.Lineup; +import com.android.tv.data.Program; +import com.android.tv.data.api.Channel; +import com.google.common.base.Function; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.Iterables; +import com.google.common.collect.ListMultimap; +import java.util.concurrent.TimeUnit; + +/** EPG data for use in tests. */ +public abstract class EpgTestData { + + public static final android.support.media.tv.Channel CHANNEL_10 = + new android.support.media.tv.Channel.Builder() + .setDisplayName("Channel TEN") + .setDisplayNumber("10") + .build(); + public static final android.support.media.tv.Channel CHANNEL_11 = + new android.support.media.tv.Channel.Builder() + .setDisplayName("Channel Eleven") + .setDisplayNumber("11") + .build(); + public static final android.support.media.tv.Channel CHANNEL_90_2 = + new android.support.media.tv.Channel.Builder() + .setDisplayName("Channel Ninety dot Two") + .setDisplayNumber("90.2") + .build(); + + public static final Lineup LINEUP_1 = + new Lineup( + "lineup1", + Lineup.LINEUP_SATELLITE, + "Lineup one", + "Location one", + ImmutableList.of("1", "2.2")); + public static final Lineup LINEUP_2 = + new Lineup( + "lineup2", + Lineup.LINEUP_SATELLITE, + "Lineup two", + "Location two", + ImmutableList.of("1", "2.3")); + + public static final Lineup LINEUP_90210 = + new Lineup( + "test90210", + Lineup.LINEUP_BROADCAST_DIGITAL, + "Test 90210", + "Beverly Hills", + ImmutableList.of("90.2", "10")); + + // Programs start and end times are set relative to 0. + // Then when loaded they are offset by the {@link #getStartTimeMs}. + // Start and end time may be negative meaning they happen before "now". + + public static final Program PROGRAM_1 = + new Program.Builder() + .setTitle("Program 1") + .setStartTimeUtcMillis(0) + .setEndTimeUtcMillis(TimeUnit.MINUTES.toMillis(30)) + .build(); + + public static final Program PROGRAM_2 = + new Program.Builder() + .setTitle("Program 2") + .setStartTimeUtcMillis(TimeUnit.MINUTES.toMillis(30)) + .setEndTimeUtcMillis(TimeUnit.MINUTES.toMillis(60)) + .build(); + + public static final EpgTestData DATA_90210 = + new EpgTestData() { + + // Thursday, June 1, 2017 4:00:00 PM GMT-07:00 + private final long testStartTimeMs = 1496358000000L; + + @Override + public ListMultimap<String, Lineup> getLineups() { + ImmutableListMultimap.Builder<String, Lineup> builder = + ImmutableListMultimap.builder(); + return builder.putAll("90210", LINEUP_1, LINEUP_2, LINEUP_90210).build(); + } + + @Override + public ListMultimap<String, Channel> getLineupChannels() { + ImmutableListMultimap.Builder<String, Channel> builder = + ImmutableListMultimap.builder(); + return builder.putAll( + LINEUP_90210.getId(), toTvChannels(CHANNEL_90_2, CHANNEL_10)) + .putAll(LINEUP_1.getId(), toTvChannels(CHANNEL_10, CHANNEL_11)) + .build(); + } + + @Override + public ListMultimap<String, Program> getEpgPrograms() { + ImmutableListMultimap.Builder<String, Program> builder = + ImmutableListMultimap.builder(); + return builder.putAll( + CHANNEL_10.getDisplayNumber(), + EpgTestData.updateTime(getStartTimeMs(), PROGRAM_1)) + .putAll( + CHANNEL_11.getDisplayNumber(), + EpgTestData.updateTime(getStartTimeMs(), PROGRAM_2)) + .build(); + } + + @Override + public long getStartTimeMs() { + return testStartTimeMs; + } + }; + + public abstract ListMultimap<String, Lineup> getLineups(); + + public abstract ListMultimap<String, Channel> getLineupChannels(); + + public abstract ListMultimap<String, Program> getEpgPrograms(); + + /** The starting time for this test data */ + public abstract long getStartTimeMs(); + + /** + * Loads test data + * + * <p> + * + * <ul> + * <li>Sets clock to {@link #getStartTimeMs()} and boot time to 12 hours before that + * <li>Loads lineups + * <li>Loads lineupChannels + * <li>Loads epgPrograms + * </ul> + */ + public final void loadData(FakeClock clock, FakeEpgReader epgReader) { + clock.setBootTimeMillis(getStartTimeMs() + TimeUnit.HOURS.toMillis(-12)); + clock.setCurrentTimeMillis(getStartTimeMs()); + epgReader.zip2lineups.putAll(getLineups()); + epgReader.lineup2Channels.putAll(getLineupChannels()); + epgReader.epgChannelId2Programs.putAll(getEpgPrograms()); + } + + public final void loadData(TestSingletonApp testSingletonApp) { + loadData(testSingletonApp.fakeClock, testSingletonApp.epgReader); + } + + private static Iterable<Channel> toTvChannels(android.support.media.tv.Channel... channels) { + return Iterables.transform( + ImmutableList.copyOf(channels), + new Function<android.support.media.tv.Channel, Channel>() { + @Override + public Channel apply(android.support.media.tv.Channel original) { + return toTvChannel(original); + } + }); + } + + public static Channel toTvChannel(android.support.media.tv.Channel original) { + return new ChannelImpl.Builder() + .setDisplayName(original.getDisplayName()) + .setDisplayNumber(original.getDisplayNumber()) + // TODO implement the reset + .build(); + } + + /** Add time to the startTime and stopTime of each program */ + private static Iterable<Program> updateTime(long time, Program... programs) { + return Iterables.transform( + ImmutableList.copyOf(programs), + new Function<Program, Program>() { + @Override + public Program apply(Program p) { + return new Program.Builder(p) + .setStartTimeUtcMillis(p.getStartTimeUtcMillis() + time) + .setEndTimeUtcMillis(p.getEndTimeUtcMillis() + time) + .build(); + } + }); + } +} diff --git a/tests/common/src/com/android/tv/testing/FakeClock.java b/tests/common/src/com/android/tv/testing/FakeClock.java index d88e53a8..f5941939 100644 --- a/tests/common/src/com/android/tv/testing/FakeClock.java +++ b/tests/common/src/com/android/tv/testing/FakeClock.java @@ -16,8 +16,7 @@ package com.android.tv.testing; -import com.android.tv.util.Clock; - +import com.android.tv.common.util.Clock; import java.util.concurrent.TimeUnit; /** @@ -27,21 +26,20 @@ import java.util.concurrent.TimeUnit; * {@link #sleep(long)} is called. */ public class FakeClock implements Clock { - /** - * Creates a fake clock with the time set to now and the boot time set to now - 100,000. - */ + /** Creates a fake clock with the time set to now and the boot time set to now - 100,000. */ public static FakeClock createWithCurrentTime() { long now = System.currentTimeMillis(); return new FakeClock(now, now - 100_000); } - /** - * Creates a fake clock with the time set to zero. - */ + /** Creates a fake clock with the time set to zero. */ public static FakeClock createWithTimeOne() { return new FakeClock(1L, 0L); } - + /** Creates a fake clock with the time set to {@code time}. */ + public static FakeClock createWithTime(long time) { + return new FakeClock(time, 0L); + } private long mCurrentTimeMillis; @@ -95,9 +93,12 @@ public class FakeClock implements Clock { return mCurrentTimeMillis - mBootTimeMillis; } - /** - * Sleep does not block it just updates the current time. - */ + @Override + public long uptimeMillis() { + return elapsedRealtime(); + } + + /** Sleep does not block it just updates the current time. */ @Override public void sleep(long ms) { // TODO: implement blocking if needed. diff --git a/tests/common/src/com/android/tv/testing/FakeEpgFetcher.java b/tests/common/src/com/android/tv/testing/FakeEpgFetcher.java new file mode 100644 index 00000000..d1018a5c --- /dev/null +++ b/tests/common/src/com/android/tv/testing/FakeEpgFetcher.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2017 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.testing; + +import android.app.job.JobParameters; +import android.app.job.JobService; +import com.android.tv.data.epg.EpgFetcher; + +/** Fake {@link EpgFetcher} for testing. */ +public class FakeEpgFetcher implements EpgFetcher { + public boolean fetchStarted = false; + + @Override + public void startRoutineService() {} + + @Override + public void fetchImmediatelyIfNeeded() {} + + @Override + public void fetchImmediately() { + fetchStarted = true; + } + + @Override + public void onChannelScanStarted() {} + + @Override + public void onChannelScanFinished() {} + + @Override + public boolean executeFetchTaskIfPossible(JobService jobService, JobParameters params) { + return false; + } + + @Override + public void stopFetchingJob() { + fetchStarted = false; + } + + public void reset() { + fetchStarted = false; + } +} diff --git a/tests/common/src/com/android/tv/testing/FakeEpgReader.java b/tests/common/src/com/android/tv/testing/FakeEpgReader.java new file mode 100644 index 00000000..710ada55 --- /dev/null +++ b/tests/common/src/com/android/tv/testing/FakeEpgReader.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2017 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.testing; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Range; +import com.android.tv.data.ChannelImpl; +import com.android.tv.data.ChannelNumber; +import com.android.tv.data.Lineup; +import com.android.tv.data.Program; +import com.android.tv.data.api.Channel; +import com.android.tv.data.epg.EpgReader; +import com.android.tv.dvr.data.SeriesInfo; +import com.google.common.base.Function; +import com.google.common.base.Predicate; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import com.google.common.collect.LinkedListMultimap; +import com.google.common.collect.ListMultimap; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** Fake {@link EpgReader} for testing. */ +public final class FakeEpgReader implements EpgReader { + public final ListMultimap<String, Lineup> zip2lineups = LinkedListMultimap.create(2); + public final ListMultimap<String, Channel> lineup2Channels = LinkedListMultimap.create(2); + public final ListMultimap<String, Program> epgChannelId2Programs = LinkedListMultimap.create(2); + public final FakeClock fakeClock; + + public FakeEpgReader(FakeClock fakeClock) { + this.fakeClock = fakeClock; + } + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public long getEpgTimestamp() { + return fakeClock.currentTimeMillis(); + } + + @Override + public void setRegionCode(String regionCode) {} + + @Override + public List<Lineup> getLineups(@NonNull String postalCode) { + return zip2lineups.get(postalCode); + } + + @Override + public List<String> getChannelNumbers(@NonNull String lineupId) { + return null; + } + + @Override + public Set<EpgChannel> getChannels(Set<Channel> inputChannels, @NonNull String lineupId) { + Set<EpgChannel> result = new HashSet<>(); + List<Channel> lineupChannels = lineup2Channels.get(lineupId); + for (Channel channel : lineupChannels) { + Channel match = + Iterables.find( + inputChannels, + new Predicate<Channel>() { + @Override + public boolean apply(@Nullable Channel inputChannel) { + return ChannelNumber.equivalent( + inputChannel.getDisplayNumber(), + channel.getDisplayNumber()); + } + }, + null); + if (match != null) { + ChannelImpl updatedChannel = new ChannelImpl.Builder(match).build(); + updatedChannel.setLogoUri(channel.getLogoUri()); + result.add(EpgChannel.createEpgChannel(updatedChannel, channel.getDisplayNumber())); + } + } + return result; + } + + @Override + public void preloadChannels(@NonNull String lineupId) {} + + @Override + public void clearCachedChannels(@NonNull String lineupId) {} + + @Override + public List<Program> getPrograms(EpgChannel epgChannel) { + return ImmutableList.copyOf( + Iterables.transform( + epgChannelId2Programs.get(epgChannel.getEpgChannelId()), + updateWith(epgChannel))); + } + + @Override + public Map<EpgChannel, Collection<Program>> getPrograms( + @NonNull Set<EpgChannel> epgChannels, long duration) { + Range<Long> validRange = + Range.create( + fakeClock.currentTimeMillis(), fakeClock.currentTimeMillis() + duration); + ImmutableMap.Builder<EpgChannel, Collection<Program>> mapBuilder = ImmutableMap.builder(); + for (EpgChannel epgChannel : epgChannels) { + Iterable<Program> programs = getPrograms(epgChannel); + + mapBuilder.put( + epgChannel, + ImmutableList.copyOf(Iterables.filter(programs, isProgramDuring(validRange)))); + } + return mapBuilder.build(); + } + + protected Function<Program, Program> updateWith(final EpgChannel channel) { + return new Function<Program, Program>() { + @Nullable + @Override + public Program apply(@Nullable Program program) { + return new Program.Builder(program) + .setChannelId(channel.getChannel().getId()) + .setPackageName(channel.getChannel().getPackageName()) + .build(); + } + }; + } + + /** + * True if the start time or the end time is {@link Range#contains contained (inclusive)} in the + * range + */ + protected Predicate<Program> isProgramDuring(final Range<Long> validRange) { + return new Predicate<Program>() { + @Override + public boolean apply(@Nullable Program program) { + return validRange.contains(program.getStartTimeUtcMillis()) + || validRange.contains(program.getEndTimeUtcMillis()); + } + }; + } + + @Override + public SeriesInfo getSeriesInfo(@NonNull String seriesId) { + return null; + } +} diff --git a/tests/common/src/com/android/tv/testing/FakeRemoteConfig.java b/tests/common/src/com/android/tv/testing/FakeRemoteConfig.java new file mode 100644 index 00000000..89e6a0a2 --- /dev/null +++ b/tests/common/src/com/android/tv/testing/FakeRemoteConfig.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2017 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.testing; + +import android.text.TextUtils; +import com.android.tv.common.config.api.RemoteConfig; +import java.util.HashMap; +import java.util.Map; + +/** Fake {@link RemoteConfig} suitable for testing. */ +public class FakeRemoteConfig implements RemoteConfig { + public final Map<String, String> values = new HashMap(); + + @Override + public void fetch(OnRemoteConfigUpdatedListener listener) {} + + @Override + public String getString(String key) { + return values.get(key); + } + + @Override + public boolean getBoolean(String key) { + String value = values.get(key); + return TextUtils.isEmpty(value) ? false : Boolean.valueOf(key); + } + + @Override + public long getLong(String key) { + return getLong(key, 0); + } + + @Override + public long getLong(String key, long defaultValue) { + if (values.containsKey(key)) { + String value = values.get(key); + return TextUtils.isEmpty(value) ? defaultValue : Long.valueOf(value); + } + return defaultValue; + } +} diff --git a/tests/common/src/com/android/tv/testing/FakeTvInputManager.java b/tests/common/src/com/android/tv/testing/FakeTvInputManager.java new file mode 100644 index 00000000..397b4052 --- /dev/null +++ b/tests/common/src/com/android/tv/testing/FakeTvInputManager.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2017 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.testing; + +import android.media.tv.TvContentRatingSystemInfo; +import android.media.tv.TvInputInfo; +import android.media.tv.TvInputManager; +import android.os.Handler; +import com.android.tv.util.TvInputManagerHelper; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** Fake implementation for testing. */ +public class FakeTvInputManager implements TvInputManagerHelper.TvInputManagerInterface { + + private final Map<String, Integer> mInputStateMap = new HashMap<>(); + private final Map<String, TvInputInfo> mInputMap = new HashMap<>(); + private final Map<TvInputManager.TvInputCallback, Handler> mCallbacks = new HashMap<>(); + + public void add(TvInputInfo inputInfo, int state) { + final String inputId = inputInfo.getId(); + if (mInputStateMap.containsKey(inputId)) { + throw new IllegalArgumentException("inputId " + inputId); + } + mInputMap.put(inputId, inputInfo); + mInputStateMap.put(inputId, state); + for (final Map.Entry<TvInputManager.TvInputCallback, Handler> e : mCallbacks.entrySet()) { + e.getValue() + .post( + new Runnable() { + @Override + public void run() { + e.getKey().onInputAdded(inputId); + } + }); + } + } + + public void setInputState(final String inputId, final int state) { + if (!mInputStateMap.containsKey(inputId)) { + throw new IllegalArgumentException("inputId " + inputId); + } + mInputStateMap.put(inputId, state); + for (final Map.Entry<TvInputManager.TvInputCallback, Handler> e : mCallbacks.entrySet()) { + e.getValue() + .post( + new Runnable() { + @Override + public void run() { + e.getKey().onInputStateChanged(inputId, state); + } + }); + } + } + + public void remove(final String inputId) { + mInputMap.remove(inputId); + mInputStateMap.remove(inputId); + for (final Map.Entry<TvInputManager.TvInputCallback, Handler> e : mCallbacks.entrySet()) { + e.getValue() + .post( + new Runnable() { + @Override + public void run() { + e.getKey().onInputRemoved(inputId); + } + }); + } + } + + @Override + public TvInputInfo getTvInputInfo(String inputId) { + return mInputMap.get(inputId); + } + + @Override + public Integer getInputState(String inputId) { + return mInputStateMap.get(inputId); + } + + @Override + public void registerCallback(TvInputManager.TvInputCallback internalCallback, Handler handler) { + mCallbacks.put(internalCallback, handler); + } + + @Override + public void unregisterCallback(TvInputManager.TvInputCallback internalCallback) { + mCallbacks.remove(internalCallback); + } + + @Override + public List<TvInputInfo> getTvInputList() { + return new ArrayList(mInputMap.values()); + } + + @Override + public List<TvContentRatingSystemInfo> getTvContentRatingSystemList() { + // TODO implement + return new ArrayList<>(); + } +} diff --git a/tests/common/src/com/android/tv/testing/FakeTvInputManagerHelper.java b/tests/common/src/com/android/tv/testing/FakeTvInputManagerHelper.java new file mode 100644 index 00000000..85bdcf04 --- /dev/null +++ b/tests/common/src/com/android/tv/testing/FakeTvInputManagerHelper.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2017 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.testing; + +import android.content.Context; +import com.android.tv.util.TvInputManagerHelper; + +/** Fake TvInputManagerHelper. */ +public class FakeTvInputManagerHelper extends TvInputManagerHelper { + + public FakeTvInputManagerHelper(Context context) { + super(context, new FakeTvInputManager()); + } + + public FakeTvInputManager getFakeTvInputManager() { + return (FakeTvInputManager) mTvInputManager; + } +} diff --git a/tests/common/src/com/android/tv/testing/FakeTvProvider.java b/tests/common/src/com/android/tv/testing/FakeTvProvider.java new file mode 100644 index 00000000..24c26f39 --- /dev/null +++ b/tests/common/src/com/android/tv/testing/FakeTvProvider.java @@ -0,0 +1,2605 @@ +/* + * Copyright (C) 2017 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.testing; + +import android.annotation.SuppressLint; +import android.content.ContentProvider; +import android.content.ContentProviderOperation; +import android.content.ContentProviderResult; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.OperationApplicationException; +import android.content.SharedPreferences; +import android.content.UriMatcher; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteQueryBuilder; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.media.tv.TvContract; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.ParcelFileDescriptor; +import android.os.ParcelFileDescriptor.AutoCloseInputStream; +import android.preference.PreferenceManager; +import android.provider.BaseColumns; +import android.support.annotation.VisibleForTesting; +import android.support.media.tv.TvContractCompat; +import android.support.media.tv.TvContractCompat.BaseTvColumns; +import android.support.media.tv.TvContractCompat.Channels; +import android.support.media.tv.TvContractCompat.PreviewPrograms; +import android.support.media.tv.TvContractCompat.Programs; +import android.support.media.tv.TvContractCompat.Programs.Genres; +import android.support.media.tv.TvContractCompat.RecordedPrograms; +import android.support.media.tv.TvContractCompat.WatchNextPrograms; +import android.text.TextUtils; +import android.util.Log; +import com.android.tv.util.SqlParams; +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Fake TV content provider suitable for unit tests. The contract between this provider and + * applications is defined in {@link TvContractCompat}. + */ +// TODO(b/62143348): remove when error prone check fixed +@SuppressWarnings({"AndroidApiChecker", "TryWithResources"}) +public class FakeTvProvider extends ContentProvider { + // TODO either make this a shadow or move it to the support library + + private static final boolean DEBUG = false; + private static final String TAG = "TvProvider"; + + static final int DATABASE_VERSION = 34; + static final String SHARED_PREF_BLOCKED_PACKAGES_KEY = "blocked_packages"; + static final String CHANNELS_TABLE = "channels"; + static final String PROGRAMS_TABLE = "programs"; + static final String RECORDED_PROGRAMS_TABLE = "recorded_programs"; + static final String PREVIEW_PROGRAMS_TABLE = "preview_programs"; + static final String WATCH_NEXT_PROGRAMS_TABLE = "watch_next_programs"; + static final String WATCHED_PROGRAMS_TABLE = "watched_programs"; + static final String PROGRAMS_TABLE_PACKAGE_NAME_INDEX = "programs_package_name_index"; + static final String PROGRAMS_TABLE_CHANNEL_ID_INDEX = "programs_channel_id_index"; + static final String PROGRAMS_TABLE_START_TIME_INDEX = "programs_start_time_index"; + static final String PROGRAMS_TABLE_END_TIME_INDEX = "programs_end_time_index"; + static final String WATCHED_PROGRAMS_TABLE_CHANNEL_ID_INDEX = + "watched_programs_channel_id_index"; + // The internal column in the watched programs table to indicate whether the current log entry + // is consolidated or not. Unconsolidated entries may have columns with missing data. + static final String WATCHED_PROGRAMS_COLUMN_CONSOLIDATED = "consolidated"; + static final String CHANNELS_COLUMN_LOGO = "logo"; + private static final String DATABASE_NAME = "tv.db"; + private static final String DELETED_CHANNELS_TABLE = "deleted_channels"; // Deprecated + private static final String DEFAULT_PROGRAMS_SORT_ORDER = + Programs.COLUMN_START_TIME_UTC_MILLIS + " ASC"; + private static final String DEFAULT_WATCHED_PROGRAMS_SORT_ORDER = + WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS + " DESC"; + private static final String CHANNELS_TABLE_INNER_JOIN_PROGRAMS_TABLE = + CHANNELS_TABLE + + " INNER JOIN " + + PROGRAMS_TABLE + + " ON (" + + CHANNELS_TABLE + + "." + + Channels._ID + + "=" + + PROGRAMS_TABLE + + "." + + Programs.COLUMN_CHANNEL_ID + + ")"; + + // Operation names for createSqlParams(). + private static final String OP_QUERY = "query"; + private static final String OP_UPDATE = "update"; + private static final String OP_DELETE = "delete"; + + private static final UriMatcher sUriMatcher; + private static final int MATCH_CHANNEL = 1; + private static final int MATCH_CHANNEL_ID = 2; + private static final int MATCH_CHANNEL_ID_LOGO = 3; + private static final int MATCH_PASSTHROUGH_ID = 4; + private static final int MATCH_PROGRAM = 5; + private static final int MATCH_PROGRAM_ID = 6; + private static final int MATCH_WATCHED_PROGRAM = 7; + private static final int MATCH_WATCHED_PROGRAM_ID = 8; + private static final int MATCH_RECORDED_PROGRAM = 9; + private static final int MATCH_RECORDED_PROGRAM_ID = 10; + private static final int MATCH_PREVIEW_PROGRAM = 11; + private static final int MATCH_PREVIEW_PROGRAM_ID = 12; + private static final int MATCH_WATCH_NEXT_PROGRAM = 13; + private static final int MATCH_WATCH_NEXT_PROGRAM_ID = 14; + + private static final int MAX_LOGO_IMAGE_SIZE = 256; + + private static final String EMPTY_STRING = ""; + + private static final Map<String, String> sChannelProjectionMap; + private static final Map<String, String> sProgramProjectionMap; + private static final Map<String, String> sWatchedProgramProjectionMap; + private static final Map<String, String> sRecordedProgramProjectionMap; + private static final Map<String, String> sPreviewProgramProjectionMap; + private static final Map<String, String> sWatchNextProgramProjectionMap; + + // TvContract hidden + private static final String PARAM_PACKAGE = "package"; + private static final String PARAM_PREVIEW = "preview"; + + private static boolean sInitialized; + + static { + sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); + sUriMatcher.addURI(TvContractCompat.AUTHORITY, "channel", MATCH_CHANNEL); + sUriMatcher.addURI(TvContractCompat.AUTHORITY, "channel/#", MATCH_CHANNEL_ID); + sUriMatcher.addURI(TvContractCompat.AUTHORITY, "channel/#/logo", MATCH_CHANNEL_ID_LOGO); + sUriMatcher.addURI(TvContractCompat.AUTHORITY, "passthrough/*", MATCH_PASSTHROUGH_ID); + sUriMatcher.addURI(TvContractCompat.AUTHORITY, "program", MATCH_PROGRAM); + sUriMatcher.addURI(TvContractCompat.AUTHORITY, "program/#", MATCH_PROGRAM_ID); + sUriMatcher.addURI(TvContractCompat.AUTHORITY, "watched_program", MATCH_WATCHED_PROGRAM); + sUriMatcher.addURI( + TvContractCompat.AUTHORITY, "watched_program/#", MATCH_WATCHED_PROGRAM_ID); + sUriMatcher.addURI(TvContractCompat.AUTHORITY, "recorded_program", MATCH_RECORDED_PROGRAM); + sUriMatcher.addURI( + TvContractCompat.AUTHORITY, "recorded_program/#", MATCH_RECORDED_PROGRAM_ID); + sUriMatcher.addURI(TvContractCompat.AUTHORITY, "preview_program", MATCH_PREVIEW_PROGRAM); + sUriMatcher.addURI( + TvContractCompat.AUTHORITY, "preview_program/#", MATCH_PREVIEW_PROGRAM_ID); + sUriMatcher.addURI( + TvContractCompat.AUTHORITY, "watch_next_program", MATCH_WATCH_NEXT_PROGRAM); + sUriMatcher.addURI( + TvContractCompat.AUTHORITY, "watch_next_program/#", MATCH_WATCH_NEXT_PROGRAM_ID); + + sChannelProjectionMap = new HashMap<>(); + sChannelProjectionMap.put(Channels._ID, CHANNELS_TABLE + "." + Channels._ID); + sChannelProjectionMap.put( + Channels.COLUMN_PACKAGE_NAME, CHANNELS_TABLE + "." + Channels.COLUMN_PACKAGE_NAME); + sChannelProjectionMap.put( + Channels.COLUMN_INPUT_ID, CHANNELS_TABLE + "." + Channels.COLUMN_INPUT_ID); + sChannelProjectionMap.put( + Channels.COLUMN_TYPE, CHANNELS_TABLE + "." + Channels.COLUMN_TYPE); + sChannelProjectionMap.put( + Channels.COLUMN_SERVICE_TYPE, CHANNELS_TABLE + "." + Channels.COLUMN_SERVICE_TYPE); + sChannelProjectionMap.put( + Channels.COLUMN_ORIGINAL_NETWORK_ID, + CHANNELS_TABLE + "." + Channels.COLUMN_ORIGINAL_NETWORK_ID); + sChannelProjectionMap.put( + Channels.COLUMN_TRANSPORT_STREAM_ID, + CHANNELS_TABLE + "." + Channels.COLUMN_TRANSPORT_STREAM_ID); + sChannelProjectionMap.put( + Channels.COLUMN_SERVICE_ID, CHANNELS_TABLE + "." + Channels.COLUMN_SERVICE_ID); + sChannelProjectionMap.put( + Channels.COLUMN_DISPLAY_NUMBER, + CHANNELS_TABLE + "." + Channels.COLUMN_DISPLAY_NUMBER); + sChannelProjectionMap.put( + Channels.COLUMN_DISPLAY_NAME, CHANNELS_TABLE + "." + Channels.COLUMN_DISPLAY_NAME); + sChannelProjectionMap.put( + Channels.COLUMN_NETWORK_AFFILIATION, + CHANNELS_TABLE + "." + Channels.COLUMN_NETWORK_AFFILIATION); + sChannelProjectionMap.put( + Channels.COLUMN_DESCRIPTION, CHANNELS_TABLE + "." + Channels.COLUMN_DESCRIPTION); + sChannelProjectionMap.put( + Channels.COLUMN_VIDEO_FORMAT, CHANNELS_TABLE + "." + Channels.COLUMN_VIDEO_FORMAT); + sChannelProjectionMap.put( + Channels.COLUMN_BROWSABLE, CHANNELS_TABLE + "." + Channels.COLUMN_BROWSABLE); + sChannelProjectionMap.put( + Channels.COLUMN_SEARCHABLE, CHANNELS_TABLE + "." + Channels.COLUMN_SEARCHABLE); + sChannelProjectionMap.put( + Channels.COLUMN_LOCKED, CHANNELS_TABLE + "." + Channels.COLUMN_LOCKED); + sChannelProjectionMap.put( + Channels.COLUMN_APP_LINK_ICON_URI, + CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_ICON_URI); + sChannelProjectionMap.put( + Channels.COLUMN_APP_LINK_POSTER_ART_URI, + CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_POSTER_ART_URI); + sChannelProjectionMap.put( + Channels.COLUMN_APP_LINK_TEXT, + CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_TEXT); + sChannelProjectionMap.put( + Channels.COLUMN_APP_LINK_COLOR, + CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_COLOR); + sChannelProjectionMap.put( + Channels.COLUMN_APP_LINK_INTENT_URI, + CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_INTENT_URI); + sChannelProjectionMap.put( + Channels.COLUMN_INTERNAL_PROVIDER_DATA, + CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_DATA); + sChannelProjectionMap.put( + Channels.COLUMN_INTERNAL_PROVIDER_FLAG1, + CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG1); + sChannelProjectionMap.put( + Channels.COLUMN_INTERNAL_PROVIDER_FLAG2, + CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG2); + sChannelProjectionMap.put( + Channels.COLUMN_INTERNAL_PROVIDER_FLAG3, + CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG3); + sChannelProjectionMap.put( + Channels.COLUMN_INTERNAL_PROVIDER_FLAG4, + CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG4); + sChannelProjectionMap.put( + Channels.COLUMN_VERSION_NUMBER, + CHANNELS_TABLE + "." + Channels.COLUMN_VERSION_NUMBER); + sChannelProjectionMap.put( + Channels.COLUMN_TRANSIENT, CHANNELS_TABLE + "." + Channels.COLUMN_TRANSIENT); + sChannelProjectionMap.put( + Channels.COLUMN_INTERNAL_PROVIDER_ID, + CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_ID); + + sProgramProjectionMap = new HashMap<>(); + sProgramProjectionMap.put(Programs._ID, Programs._ID); + sProgramProjectionMap.put(Programs.COLUMN_PACKAGE_NAME, Programs.COLUMN_PACKAGE_NAME); + sProgramProjectionMap.put(Programs.COLUMN_CHANNEL_ID, Programs.COLUMN_CHANNEL_ID); + sProgramProjectionMap.put(Programs.COLUMN_TITLE, Programs.COLUMN_TITLE); + // COLUMN_SEASON_NUMBER is deprecated. Return COLUMN_SEASON_DISPLAY_NUMBER instead. + sProgramProjectionMap.put( + Programs.COLUMN_SEASON_NUMBER, + Programs.COLUMN_SEASON_DISPLAY_NUMBER + " AS " + Programs.COLUMN_SEASON_NUMBER); + sProgramProjectionMap.put( + Programs.COLUMN_SEASON_DISPLAY_NUMBER, Programs.COLUMN_SEASON_DISPLAY_NUMBER); + sProgramProjectionMap.put(Programs.COLUMN_SEASON_TITLE, Programs.COLUMN_SEASON_TITLE); + // COLUMN_EPISODE_NUMBER is deprecated. Return COLUMN_EPISODE_DISPLAY_NUMBER instead. + sProgramProjectionMap.put( + Programs.COLUMN_EPISODE_NUMBER, + Programs.COLUMN_EPISODE_DISPLAY_NUMBER + " AS " + Programs.COLUMN_EPISODE_NUMBER); + sProgramProjectionMap.put( + Programs.COLUMN_EPISODE_DISPLAY_NUMBER, Programs.COLUMN_EPISODE_DISPLAY_NUMBER); + sProgramProjectionMap.put(Programs.COLUMN_EPISODE_TITLE, Programs.COLUMN_EPISODE_TITLE); + sProgramProjectionMap.put( + Programs.COLUMN_START_TIME_UTC_MILLIS, Programs.COLUMN_START_TIME_UTC_MILLIS); + sProgramProjectionMap.put( + Programs.COLUMN_END_TIME_UTC_MILLIS, Programs.COLUMN_END_TIME_UTC_MILLIS); + sProgramProjectionMap.put(Programs.COLUMN_BROADCAST_GENRE, Programs.COLUMN_BROADCAST_GENRE); + sProgramProjectionMap.put(Programs.COLUMN_CANONICAL_GENRE, Programs.COLUMN_CANONICAL_GENRE); + sProgramProjectionMap.put( + Programs.COLUMN_SHORT_DESCRIPTION, Programs.COLUMN_SHORT_DESCRIPTION); + sProgramProjectionMap.put( + Programs.COLUMN_LONG_DESCRIPTION, Programs.COLUMN_LONG_DESCRIPTION); + sProgramProjectionMap.put(Programs.COLUMN_VIDEO_WIDTH, Programs.COLUMN_VIDEO_WIDTH); + sProgramProjectionMap.put(Programs.COLUMN_VIDEO_HEIGHT, Programs.COLUMN_VIDEO_HEIGHT); + sProgramProjectionMap.put(Programs.COLUMN_AUDIO_LANGUAGE, Programs.COLUMN_AUDIO_LANGUAGE); + sProgramProjectionMap.put(Programs.COLUMN_CONTENT_RATING, Programs.COLUMN_CONTENT_RATING); + sProgramProjectionMap.put(Programs.COLUMN_POSTER_ART_URI, Programs.COLUMN_POSTER_ART_URI); + sProgramProjectionMap.put(Programs.COLUMN_THUMBNAIL_URI, Programs.COLUMN_THUMBNAIL_URI); + sProgramProjectionMap.put(Programs.COLUMN_SEARCHABLE, Programs.COLUMN_SEARCHABLE); + sProgramProjectionMap.put( + Programs.COLUMN_RECORDING_PROHIBITED, Programs.COLUMN_RECORDING_PROHIBITED); + sProgramProjectionMap.put( + Programs.COLUMN_INTERNAL_PROVIDER_DATA, Programs.COLUMN_INTERNAL_PROVIDER_DATA); + sProgramProjectionMap.put( + Programs.COLUMN_INTERNAL_PROVIDER_FLAG1, Programs.COLUMN_INTERNAL_PROVIDER_FLAG1); + sProgramProjectionMap.put( + Programs.COLUMN_INTERNAL_PROVIDER_FLAG2, Programs.COLUMN_INTERNAL_PROVIDER_FLAG2); + sProgramProjectionMap.put( + Programs.COLUMN_INTERNAL_PROVIDER_FLAG3, Programs.COLUMN_INTERNAL_PROVIDER_FLAG3); + sProgramProjectionMap.put( + Programs.COLUMN_INTERNAL_PROVIDER_FLAG4, Programs.COLUMN_INTERNAL_PROVIDER_FLAG4); + sProgramProjectionMap.put(Programs.COLUMN_VERSION_NUMBER, Programs.COLUMN_VERSION_NUMBER); + sProgramProjectionMap.put( + Programs.COLUMN_REVIEW_RATING_STYLE, Programs.COLUMN_REVIEW_RATING_STYLE); + sProgramProjectionMap.put(Programs.COLUMN_REVIEW_RATING, Programs.COLUMN_REVIEW_RATING); + + sWatchedProgramProjectionMap = new HashMap<>(); + sWatchedProgramProjectionMap.put(WatchedPrograms._ID, WatchedPrograms._ID); + sWatchedProgramProjectionMap.put( + WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS, + WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS); + sWatchedProgramProjectionMap.put( + WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS, + WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS); + sWatchedProgramProjectionMap.put( + WatchedPrograms.COLUMN_CHANNEL_ID, WatchedPrograms.COLUMN_CHANNEL_ID); + sWatchedProgramProjectionMap.put( + WatchedPrograms.COLUMN_TITLE, WatchedPrograms.COLUMN_TITLE); + sWatchedProgramProjectionMap.put( + WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS, + WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS); + sWatchedProgramProjectionMap.put( + WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS, + WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS); + sWatchedProgramProjectionMap.put( + WatchedPrograms.COLUMN_DESCRIPTION, WatchedPrograms.COLUMN_DESCRIPTION); + sWatchedProgramProjectionMap.put( + WatchedPrograms.COLUMN_INTERNAL_TUNE_PARAMS, + WatchedPrograms.COLUMN_INTERNAL_TUNE_PARAMS); + sWatchedProgramProjectionMap.put( + WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN, + WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN); + sWatchedProgramProjectionMap.put( + WATCHED_PROGRAMS_COLUMN_CONSOLIDATED, WATCHED_PROGRAMS_COLUMN_CONSOLIDATED); + + sRecordedProgramProjectionMap = new HashMap<>(); + sRecordedProgramProjectionMap.put(RecordedPrograms._ID, RecordedPrograms._ID); + sRecordedProgramProjectionMap.put( + RecordedPrograms.COLUMN_PACKAGE_NAME, RecordedPrograms.COLUMN_PACKAGE_NAME); + sRecordedProgramProjectionMap.put( + RecordedPrograms.COLUMN_INPUT_ID, RecordedPrograms.COLUMN_INPUT_ID); + sRecordedProgramProjectionMap.put( + RecordedPrograms.COLUMN_CHANNEL_ID, RecordedPrograms.COLUMN_CHANNEL_ID); + sRecordedProgramProjectionMap.put( + RecordedPrograms.COLUMN_TITLE, RecordedPrograms.COLUMN_TITLE); + sRecordedProgramProjectionMap.put( + RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER, + RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER); + sRecordedProgramProjectionMap.put( + RecordedPrograms.COLUMN_SEASON_TITLE, RecordedPrograms.COLUMN_SEASON_TITLE); + sRecordedProgramProjectionMap.put( + RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER, + RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER); + sRecordedProgramProjectionMap.put( + RecordedPrograms.COLUMN_EPISODE_TITLE, RecordedPrograms.COLUMN_EPISODE_TITLE); + sRecordedProgramProjectionMap.put( + RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS, + RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS); + sRecordedProgramProjectionMap.put( + RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, + RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS); + sRecordedProgramProjectionMap.put( + RecordedPrograms.COLUMN_BROADCAST_GENRE, RecordedPrograms.COLUMN_BROADCAST_GENRE); + sRecordedProgramProjectionMap.put( + RecordedPrograms.COLUMN_CANONICAL_GENRE, RecordedPrograms.COLUMN_CANONICAL_GENRE); + sRecordedProgramProjectionMap.put( + RecordedPrograms.COLUMN_SHORT_DESCRIPTION, + RecordedPrograms.COLUMN_SHORT_DESCRIPTION); + sRecordedProgramProjectionMap.put( + RecordedPrograms.COLUMN_LONG_DESCRIPTION, RecordedPrograms.COLUMN_LONG_DESCRIPTION); + sRecordedProgramProjectionMap.put( + RecordedPrograms.COLUMN_VIDEO_WIDTH, RecordedPrograms.COLUMN_VIDEO_WIDTH); + sRecordedProgramProjectionMap.put( + RecordedPrograms.COLUMN_VIDEO_HEIGHT, RecordedPrograms.COLUMN_VIDEO_HEIGHT); + sRecordedProgramProjectionMap.put( + RecordedPrograms.COLUMN_AUDIO_LANGUAGE, RecordedPrograms.COLUMN_AUDIO_LANGUAGE); + sRecordedProgramProjectionMap.put( + RecordedPrograms.COLUMN_CONTENT_RATING, RecordedPrograms.COLUMN_CONTENT_RATING); + sRecordedProgramProjectionMap.put( + RecordedPrograms.COLUMN_POSTER_ART_URI, RecordedPrograms.COLUMN_POSTER_ART_URI); + sRecordedProgramProjectionMap.put( + RecordedPrograms.COLUMN_THUMBNAIL_URI, RecordedPrograms.COLUMN_THUMBNAIL_URI); + sRecordedProgramProjectionMap.put( + RecordedPrograms.COLUMN_SEARCHABLE, RecordedPrograms.COLUMN_SEARCHABLE); + sRecordedProgramProjectionMap.put( + RecordedPrograms.COLUMN_RECORDING_DATA_URI, + RecordedPrograms.COLUMN_RECORDING_DATA_URI); + sRecordedProgramProjectionMap.put( + RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, + RecordedPrograms.COLUMN_RECORDING_DATA_BYTES); + sRecordedProgramProjectionMap.put( + RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, + RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS); + sRecordedProgramProjectionMap.put( + RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS, + RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS); + sRecordedProgramProjectionMap.put( + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA, + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA); + sRecordedProgramProjectionMap.put( + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1, + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1); + sRecordedProgramProjectionMap.put( + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2, + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2); + sRecordedProgramProjectionMap.put( + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3, + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3); + sRecordedProgramProjectionMap.put( + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4, + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4); + sRecordedProgramProjectionMap.put( + RecordedPrograms.COLUMN_VERSION_NUMBER, RecordedPrograms.COLUMN_VERSION_NUMBER); + sRecordedProgramProjectionMap.put( + RecordedPrograms.COLUMN_REVIEW_RATING_STYLE, + RecordedPrograms.COLUMN_REVIEW_RATING_STYLE); + sRecordedProgramProjectionMap.put( + RecordedPrograms.COLUMN_REVIEW_RATING, RecordedPrograms.COLUMN_REVIEW_RATING); + + sPreviewProgramProjectionMap = new HashMap<>(); + sPreviewProgramProjectionMap.put(PreviewPrograms._ID, PreviewPrograms._ID); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_PACKAGE_NAME, PreviewPrograms.COLUMN_PACKAGE_NAME); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_CHANNEL_ID, PreviewPrograms.COLUMN_CHANNEL_ID); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_TITLE, PreviewPrograms.COLUMN_TITLE); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_SEASON_DISPLAY_NUMBER, + PreviewPrograms.COLUMN_SEASON_DISPLAY_NUMBER); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_SEASON_TITLE, PreviewPrograms.COLUMN_SEASON_TITLE); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_EPISODE_DISPLAY_NUMBER, + PreviewPrograms.COLUMN_EPISODE_DISPLAY_NUMBER); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_EPISODE_TITLE, PreviewPrograms.COLUMN_EPISODE_TITLE); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_CANONICAL_GENRE, PreviewPrograms.COLUMN_CANONICAL_GENRE); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_SHORT_DESCRIPTION, PreviewPrograms.COLUMN_SHORT_DESCRIPTION); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_LONG_DESCRIPTION, PreviewPrograms.COLUMN_LONG_DESCRIPTION); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_VIDEO_WIDTH, PreviewPrograms.COLUMN_VIDEO_WIDTH); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_VIDEO_HEIGHT, PreviewPrograms.COLUMN_VIDEO_HEIGHT); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_AUDIO_LANGUAGE, PreviewPrograms.COLUMN_AUDIO_LANGUAGE); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_CONTENT_RATING, PreviewPrograms.COLUMN_CONTENT_RATING); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_POSTER_ART_URI, PreviewPrograms.COLUMN_POSTER_ART_URI); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_THUMBNAIL_URI, PreviewPrograms.COLUMN_THUMBNAIL_URI); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_SEARCHABLE, PreviewPrograms.COLUMN_SEARCHABLE); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_DATA, + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_DATA); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1, + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2, + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3, + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4, + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_VERSION_NUMBER, PreviewPrograms.COLUMN_VERSION_NUMBER); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_ID, + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_ID); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_PREVIEW_VIDEO_URI, PreviewPrograms.COLUMN_PREVIEW_VIDEO_URI); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS, + PreviewPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_DURATION_MILLIS, PreviewPrograms.COLUMN_DURATION_MILLIS); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_INTENT_URI, PreviewPrograms.COLUMN_INTENT_URI); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_WEIGHT, PreviewPrograms.COLUMN_WEIGHT); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_TRANSIENT, PreviewPrograms.COLUMN_TRANSIENT); + sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_TYPE, PreviewPrograms.COLUMN_TYPE); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_POSTER_ART_ASPECT_RATIO, + PreviewPrograms.COLUMN_POSTER_ART_ASPECT_RATIO); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO, + PreviewPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_LOGO_URI, PreviewPrograms.COLUMN_LOGO_URI); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_AVAILABILITY, PreviewPrograms.COLUMN_AVAILABILITY); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_STARTING_PRICE, PreviewPrograms.COLUMN_STARTING_PRICE); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_OFFER_PRICE, PreviewPrograms.COLUMN_OFFER_PRICE); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_RELEASE_DATE, PreviewPrograms.COLUMN_RELEASE_DATE); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_ITEM_COUNT, PreviewPrograms.COLUMN_ITEM_COUNT); + sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_LIVE, PreviewPrograms.COLUMN_LIVE); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_INTERACTION_TYPE, PreviewPrograms.COLUMN_INTERACTION_TYPE); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_INTERACTION_COUNT, PreviewPrograms.COLUMN_INTERACTION_COUNT); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_AUTHOR, PreviewPrograms.COLUMN_AUTHOR); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_REVIEW_RATING_STYLE, + PreviewPrograms.COLUMN_REVIEW_RATING_STYLE); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_REVIEW_RATING, PreviewPrograms.COLUMN_REVIEW_RATING); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_BROWSABLE, PreviewPrograms.COLUMN_BROWSABLE); + sPreviewProgramProjectionMap.put( + PreviewPrograms.COLUMN_CONTENT_ID, PreviewPrograms.COLUMN_CONTENT_ID); + + sWatchNextProgramProjectionMap = new HashMap<>(); + sWatchNextProgramProjectionMap.put(WatchNextPrograms._ID, WatchNextPrograms._ID); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_PACKAGE_NAME, WatchNextPrograms.COLUMN_PACKAGE_NAME); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_TITLE, WatchNextPrograms.COLUMN_TITLE); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_SEASON_DISPLAY_NUMBER, + WatchNextPrograms.COLUMN_SEASON_DISPLAY_NUMBER); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_SEASON_TITLE, WatchNextPrograms.COLUMN_SEASON_TITLE); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_EPISODE_DISPLAY_NUMBER, + WatchNextPrograms.COLUMN_EPISODE_DISPLAY_NUMBER); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_EPISODE_TITLE, WatchNextPrograms.COLUMN_EPISODE_TITLE); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_CANONICAL_GENRE, WatchNextPrograms.COLUMN_CANONICAL_GENRE); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_SHORT_DESCRIPTION, + WatchNextPrograms.COLUMN_SHORT_DESCRIPTION); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_LONG_DESCRIPTION, + WatchNextPrograms.COLUMN_LONG_DESCRIPTION); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_VIDEO_WIDTH, WatchNextPrograms.COLUMN_VIDEO_WIDTH); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_VIDEO_HEIGHT, WatchNextPrograms.COLUMN_VIDEO_HEIGHT); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_AUDIO_LANGUAGE, WatchNextPrograms.COLUMN_AUDIO_LANGUAGE); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_CONTENT_RATING, WatchNextPrograms.COLUMN_CONTENT_RATING); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_POSTER_ART_URI, WatchNextPrograms.COLUMN_POSTER_ART_URI); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_THUMBNAIL_URI, WatchNextPrograms.COLUMN_THUMBNAIL_URI); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_SEARCHABLE, WatchNextPrograms.COLUMN_SEARCHABLE); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_DATA, + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_DATA); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1, + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2, + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3, + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4, + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_VERSION_NUMBER, WatchNextPrograms.COLUMN_VERSION_NUMBER); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_ID, + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_ID); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_PREVIEW_VIDEO_URI, + WatchNextPrograms.COLUMN_PREVIEW_VIDEO_URI); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS, + WatchNextPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_DURATION_MILLIS, WatchNextPrograms.COLUMN_DURATION_MILLIS); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_INTENT_URI, WatchNextPrograms.COLUMN_INTENT_URI); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_TRANSIENT, WatchNextPrograms.COLUMN_TRANSIENT); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_TYPE, WatchNextPrograms.COLUMN_TYPE); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_WATCH_NEXT_TYPE, WatchNextPrograms.COLUMN_WATCH_NEXT_TYPE); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_POSTER_ART_ASPECT_RATIO, + WatchNextPrograms.COLUMN_POSTER_ART_ASPECT_RATIO); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO, + WatchNextPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_LOGO_URI, WatchNextPrograms.COLUMN_LOGO_URI); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_AVAILABILITY, WatchNextPrograms.COLUMN_AVAILABILITY); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_STARTING_PRICE, WatchNextPrograms.COLUMN_STARTING_PRICE); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_OFFER_PRICE, WatchNextPrograms.COLUMN_OFFER_PRICE); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_RELEASE_DATE, WatchNextPrograms.COLUMN_RELEASE_DATE); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_ITEM_COUNT, WatchNextPrograms.COLUMN_ITEM_COUNT); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_LIVE, WatchNextPrograms.COLUMN_LIVE); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_INTERACTION_TYPE, + WatchNextPrograms.COLUMN_INTERACTION_TYPE); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_INTERACTION_COUNT, + WatchNextPrograms.COLUMN_INTERACTION_COUNT); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_AUTHOR, WatchNextPrograms.COLUMN_AUTHOR); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_REVIEW_RATING_STYLE, + WatchNextPrograms.COLUMN_REVIEW_RATING_STYLE); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_REVIEW_RATING, WatchNextPrograms.COLUMN_REVIEW_RATING); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_BROWSABLE, WatchNextPrograms.COLUMN_BROWSABLE); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_CONTENT_ID, WatchNextPrograms.COLUMN_CONTENT_ID); + sWatchNextProgramProjectionMap.put( + WatchNextPrograms.COLUMN_LAST_ENGAGEMENT_TIME_UTC_MILLIS, + WatchNextPrograms.COLUMN_LAST_ENGAGEMENT_TIME_UTC_MILLIS); + } + + // Mapping from broadcast genre to canonical genre. + private static Map<String, String> sGenreMap; + + private static final String PERMISSION_READ_TV_LISTINGS = "android.permission.READ_TV_LISTINGS"; + + private static final String PERMISSION_ACCESS_ALL_EPG_DATA = + "com.android.providers.tv.permission.ACCESS_ALL_EPG_DATA"; + + private static final String PERMISSION_ACCESS_WATCHED_PROGRAMS = + "com.android.providers.tv.permission.ACCESS_WATCHED_PROGRAMS"; + + private static final String CREATE_RECORDED_PROGRAMS_TABLE_SQL = + "CREATE TABLE " + + RECORDED_PROGRAMS_TABLE + + " (" + + RecordedPrograms._ID + + " INTEGER PRIMARY KEY AUTOINCREMENT," + + RecordedPrograms.COLUMN_PACKAGE_NAME + + " TEXT NOT NULL," + + RecordedPrograms.COLUMN_INPUT_ID + + " TEXT NOT NULL," + + RecordedPrograms.COLUMN_CHANNEL_ID + + " INTEGER," + + RecordedPrograms.COLUMN_TITLE + + " TEXT," + + RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER + + " TEXT," + + RecordedPrograms.COLUMN_SEASON_TITLE + + " TEXT," + + RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER + + " TEXT," + + RecordedPrograms.COLUMN_EPISODE_TITLE + + " TEXT," + + RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS + + " INTEGER," + + RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS + + " INTEGER," + + RecordedPrograms.COLUMN_BROADCAST_GENRE + + " TEXT," + + RecordedPrograms.COLUMN_CANONICAL_GENRE + + " TEXT," + + RecordedPrograms.COLUMN_SHORT_DESCRIPTION + + " TEXT," + + RecordedPrograms.COLUMN_LONG_DESCRIPTION + + " TEXT," + + RecordedPrograms.COLUMN_VIDEO_WIDTH + + " INTEGER," + + RecordedPrograms.COLUMN_VIDEO_HEIGHT + + " INTEGER," + + RecordedPrograms.COLUMN_AUDIO_LANGUAGE + + " TEXT," + + RecordedPrograms.COLUMN_CONTENT_RATING + + " TEXT," + + RecordedPrograms.COLUMN_POSTER_ART_URI + + " TEXT," + + RecordedPrograms.COLUMN_THUMBNAIL_URI + + " TEXT," + + RecordedPrograms.COLUMN_SEARCHABLE + + " INTEGER NOT NULL DEFAULT 1," + + RecordedPrograms.COLUMN_RECORDING_DATA_URI + + " TEXT," + + RecordedPrograms.COLUMN_RECORDING_DATA_BYTES + + " INTEGER," + + RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS + + " INTEGER," + + RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS + + " INTEGER," + + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA + + " BLOB," + + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1 + + " INTEGER," + + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2 + + " INTEGER," + + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3 + + " INTEGER," + + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4 + + " INTEGER," + + RecordedPrograms.COLUMN_VERSION_NUMBER + + " INTEGER," + + RecordedPrograms.COLUMN_REVIEW_RATING_STYLE + + " INTEGER," + + RecordedPrograms.COLUMN_REVIEW_RATING + + " TEXT," + + "FOREIGN KEY(" + + RecordedPrograms.COLUMN_CHANNEL_ID + + ") " + + "REFERENCES " + + CHANNELS_TABLE + + "(" + + Channels._ID + + ") " + + "ON UPDATE CASCADE ON DELETE SET NULL);"; + + private static final String CREATE_PREVIEW_PROGRAMS_TABLE_SQL = + "CREATE TABLE " + + PREVIEW_PROGRAMS_TABLE + + " (" + + PreviewPrograms._ID + + " INTEGER PRIMARY KEY AUTOINCREMENT," + + PreviewPrograms.COLUMN_PACKAGE_NAME + + " TEXT NOT NULL," + + PreviewPrograms.COLUMN_CHANNEL_ID + + " INTEGER," + + PreviewPrograms.COLUMN_TITLE + + " TEXT," + + PreviewPrograms.COLUMN_SEASON_DISPLAY_NUMBER + + " TEXT," + + PreviewPrograms.COLUMN_SEASON_TITLE + + " TEXT," + + PreviewPrograms.COLUMN_EPISODE_DISPLAY_NUMBER + + " TEXT," + + PreviewPrograms.COLUMN_EPISODE_TITLE + + " TEXT," + + PreviewPrograms.COLUMN_CANONICAL_GENRE + + " TEXT," + + PreviewPrograms.COLUMN_SHORT_DESCRIPTION + + " TEXT," + + PreviewPrograms.COLUMN_LONG_DESCRIPTION + + " TEXT," + + PreviewPrograms.COLUMN_VIDEO_WIDTH + + " INTEGER," + + PreviewPrograms.COLUMN_VIDEO_HEIGHT + + " INTEGER," + + PreviewPrograms.COLUMN_AUDIO_LANGUAGE + + " TEXT," + + PreviewPrograms.COLUMN_CONTENT_RATING + + " TEXT," + + PreviewPrograms.COLUMN_POSTER_ART_URI + + " TEXT," + + PreviewPrograms.COLUMN_THUMBNAIL_URI + + " TEXT," + + PreviewPrograms.COLUMN_SEARCHABLE + + " INTEGER NOT NULL DEFAULT 1," + + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_DATA + + " BLOB," + + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1 + + " INTEGER," + + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2 + + " INTEGER," + + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3 + + " INTEGER," + + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4 + + " INTEGER," + + PreviewPrograms.COLUMN_VERSION_NUMBER + + " INTEGER," + + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_ID + + " TEXT," + + PreviewPrograms.COLUMN_PREVIEW_VIDEO_URI + + " TEXT," + + PreviewPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS + + " INTEGER," + + PreviewPrograms.COLUMN_DURATION_MILLIS + + " INTEGER," + + PreviewPrograms.COLUMN_INTENT_URI + + " TEXT," + + PreviewPrograms.COLUMN_WEIGHT + + " INTEGER," + + PreviewPrograms.COLUMN_TRANSIENT + + " INTEGER NOT NULL DEFAULT 0," + + PreviewPrograms.COLUMN_TYPE + + " INTEGER NOT NULL," + + PreviewPrograms.COLUMN_POSTER_ART_ASPECT_RATIO + + " INTEGER," + + PreviewPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO + + " INTEGER," + + PreviewPrograms.COLUMN_LOGO_URI + + " TEXT," + + PreviewPrograms.COLUMN_AVAILABILITY + + " INTERGER," + + PreviewPrograms.COLUMN_STARTING_PRICE + + " TEXT," + + PreviewPrograms.COLUMN_OFFER_PRICE + + " TEXT," + + PreviewPrograms.COLUMN_RELEASE_DATE + + " TEXT," + + PreviewPrograms.COLUMN_ITEM_COUNT + + " INTEGER," + + PreviewPrograms.COLUMN_LIVE + + " INTEGER NOT NULL DEFAULT 0," + + PreviewPrograms.COLUMN_INTERACTION_TYPE + + " INTEGER," + + PreviewPrograms.COLUMN_INTERACTION_COUNT + + " INTEGER," + + PreviewPrograms.COLUMN_AUTHOR + + " TEXT," + + PreviewPrograms.COLUMN_REVIEW_RATING_STYLE + + " INTEGER," + + PreviewPrograms.COLUMN_REVIEW_RATING + + " TEXT," + + PreviewPrograms.COLUMN_BROWSABLE + + " INTEGER NOT NULL DEFAULT 1," + + PreviewPrograms.COLUMN_CONTENT_ID + + " TEXT," + + "FOREIGN KEY(" + + PreviewPrograms.COLUMN_CHANNEL_ID + + "," + + PreviewPrograms.COLUMN_PACKAGE_NAME + + ") REFERENCES " + + CHANNELS_TABLE + + "(" + + Channels._ID + + "," + + Channels.COLUMN_PACKAGE_NAME + + ") ON UPDATE CASCADE ON DELETE CASCADE" + + ");"; + private static final String CREATE_PREVIEW_PROGRAMS_PACKAGE_NAME_INDEX_SQL = + "CREATE INDEX preview_programs_package_name_index ON " + + PREVIEW_PROGRAMS_TABLE + + "(" + + PreviewPrograms.COLUMN_PACKAGE_NAME + + ");"; + private static final String CREATE_PREVIEW_PROGRAMS_CHANNEL_ID_INDEX_SQL = + "CREATE INDEX preview_programs_id_index ON " + + PREVIEW_PROGRAMS_TABLE + + "(" + + PreviewPrograms.COLUMN_CHANNEL_ID + + ");"; + private static final String CREATE_WATCH_NEXT_PROGRAMS_TABLE_SQL = + "CREATE TABLE " + + WATCH_NEXT_PROGRAMS_TABLE + + " (" + + WatchNextPrograms._ID + + " INTEGER PRIMARY KEY AUTOINCREMENT," + + WatchNextPrograms.COLUMN_PACKAGE_NAME + + " TEXT NOT NULL," + + WatchNextPrograms.COLUMN_TITLE + + " TEXT," + + WatchNextPrograms.COLUMN_SEASON_DISPLAY_NUMBER + + " TEXT," + + WatchNextPrograms.COLUMN_SEASON_TITLE + + " TEXT," + + WatchNextPrograms.COLUMN_EPISODE_DISPLAY_NUMBER + + " TEXT," + + WatchNextPrograms.COLUMN_EPISODE_TITLE + + " TEXT," + + WatchNextPrograms.COLUMN_CANONICAL_GENRE + + " TEXT," + + WatchNextPrograms.COLUMN_SHORT_DESCRIPTION + + " TEXT," + + WatchNextPrograms.COLUMN_LONG_DESCRIPTION + + " TEXT," + + WatchNextPrograms.COLUMN_VIDEO_WIDTH + + " INTEGER," + + WatchNextPrograms.COLUMN_VIDEO_HEIGHT + + " INTEGER," + + WatchNextPrograms.COLUMN_AUDIO_LANGUAGE + + " TEXT," + + WatchNextPrograms.COLUMN_CONTENT_RATING + + " TEXT," + + WatchNextPrograms.COLUMN_POSTER_ART_URI + + " TEXT," + + WatchNextPrograms.COLUMN_THUMBNAIL_URI + + " TEXT," + + WatchNextPrograms.COLUMN_SEARCHABLE + + " INTEGER NOT NULL DEFAULT 1," + + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_DATA + + " BLOB," + + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1 + + " INTEGER," + + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2 + + " INTEGER," + + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3 + + " INTEGER," + + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4 + + " INTEGER," + + WatchNextPrograms.COLUMN_VERSION_NUMBER + + " INTEGER," + + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_ID + + " TEXT," + + WatchNextPrograms.COLUMN_PREVIEW_VIDEO_URI + + " TEXT," + + WatchNextPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS + + " INTEGER," + + WatchNextPrograms.COLUMN_DURATION_MILLIS + + " INTEGER," + + WatchNextPrograms.COLUMN_INTENT_URI + + " TEXT," + + WatchNextPrograms.COLUMN_TRANSIENT + + " INTEGER NOT NULL DEFAULT 0," + + WatchNextPrograms.COLUMN_TYPE + + " INTEGER NOT NULL," + + WatchNextPrograms.COLUMN_WATCH_NEXT_TYPE + + " INTEGER," + + WatchNextPrograms.COLUMN_POSTER_ART_ASPECT_RATIO + + " INTEGER," + + WatchNextPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO + + " INTEGER," + + WatchNextPrograms.COLUMN_LOGO_URI + + " TEXT," + + WatchNextPrograms.COLUMN_AVAILABILITY + + " INTEGER," + + WatchNextPrograms.COLUMN_STARTING_PRICE + + " TEXT," + + WatchNextPrograms.COLUMN_OFFER_PRICE + + " TEXT," + + WatchNextPrograms.COLUMN_RELEASE_DATE + + " TEXT," + + WatchNextPrograms.COLUMN_ITEM_COUNT + + " INTEGER," + + WatchNextPrograms.COLUMN_LIVE + + " INTEGER NOT NULL DEFAULT 0," + + WatchNextPrograms.COLUMN_INTERACTION_TYPE + + " INTEGER," + + WatchNextPrograms.COLUMN_INTERACTION_COUNT + + " INTEGER," + + WatchNextPrograms.COLUMN_AUTHOR + + " TEXT," + + WatchNextPrograms.COLUMN_REVIEW_RATING_STYLE + + " INTEGER," + + WatchNextPrograms.COLUMN_REVIEW_RATING + + " TEXT," + + WatchNextPrograms.COLUMN_BROWSABLE + + " INTEGER NOT NULL DEFAULT 1," + + WatchNextPrograms.COLUMN_CONTENT_ID + + " TEXT," + + WatchNextPrograms.COLUMN_LAST_ENGAGEMENT_TIME_UTC_MILLIS + + " INTEGER" + + ");"; + private static final String CREATE_WATCH_NEXT_PROGRAMS_PACKAGE_NAME_INDEX_SQL = + "CREATE INDEX watch_next_programs_package_name_index ON " + + WATCH_NEXT_PROGRAMS_TABLE + + "(" + + WatchNextPrograms.COLUMN_PACKAGE_NAME + + ");"; + + private String mCallingPackage = "com.android.tv"; + + static class DatabaseHelper extends SQLiteOpenHelper { + private Context mContext; + + public static synchronized DatabaseHelper createInstance(Context context) { + return new DatabaseHelper(context); + } + + private DatabaseHelper(Context context) { + this(context, DATABASE_NAME, DATABASE_VERSION); + } + + @VisibleForTesting + DatabaseHelper(Context context, String databaseName, int databaseVersion) { + super(context, databaseName, null, databaseVersion); + mContext = context; + } + + @Override + public void onConfigure(SQLiteDatabase db) { + db.setForeignKeyConstraintsEnabled(true); + } + + @Override + public void onCreate(SQLiteDatabase db) { + if (DEBUG) { + Log.d(TAG, "Creating database"); + } + // Set up the database schema. + db.execSQL( + "CREATE TABLE " + + CHANNELS_TABLE + + " (" + + Channels._ID + + " INTEGER PRIMARY KEY AUTOINCREMENT," + + Channels.COLUMN_PACKAGE_NAME + + " TEXT NOT NULL," + + Channels.COLUMN_INPUT_ID + + " TEXT NOT NULL," + + Channels.COLUMN_TYPE + + " TEXT NOT NULL DEFAULT '" + + Channels.TYPE_OTHER + + "'," + + Channels.COLUMN_SERVICE_TYPE + + " TEXT NOT NULL DEFAULT '" + + Channels.SERVICE_TYPE_AUDIO_VIDEO + + "'," + + Channels.COLUMN_ORIGINAL_NETWORK_ID + + " INTEGER NOT NULL DEFAULT 0," + + Channels.COLUMN_TRANSPORT_STREAM_ID + + " INTEGER NOT NULL DEFAULT 0," + + Channels.COLUMN_SERVICE_ID + + " INTEGER NOT NULL DEFAULT 0," + + Channels.COLUMN_DISPLAY_NUMBER + + " TEXT," + + Channels.COLUMN_DISPLAY_NAME + + " TEXT," + + Channels.COLUMN_NETWORK_AFFILIATION + + " TEXT," + + Channels.COLUMN_DESCRIPTION + + " TEXT," + + Channels.COLUMN_VIDEO_FORMAT + + " TEXT," + + Channels.COLUMN_BROWSABLE + + " INTEGER NOT NULL DEFAULT 0," + + Channels.COLUMN_SEARCHABLE + + " INTEGER NOT NULL DEFAULT 1," + + Channels.COLUMN_LOCKED + + " INTEGER NOT NULL DEFAULT 0," + + Channels.COLUMN_APP_LINK_ICON_URI + + " TEXT," + + Channels.COLUMN_APP_LINK_POSTER_ART_URI + + " TEXT," + + Channels.COLUMN_APP_LINK_TEXT + + " TEXT," + + Channels.COLUMN_APP_LINK_COLOR + + " INTEGER," + + Channels.COLUMN_APP_LINK_INTENT_URI + + " TEXT," + + Channels.COLUMN_INTERNAL_PROVIDER_DATA + + " BLOB," + + Channels.COLUMN_INTERNAL_PROVIDER_FLAG1 + + " INTEGER," + + Channels.COLUMN_INTERNAL_PROVIDER_FLAG2 + + " INTEGER," + + Channels.COLUMN_INTERNAL_PROVIDER_FLAG3 + + " INTEGER," + + Channels.COLUMN_INTERNAL_PROVIDER_FLAG4 + + " INTEGER," + + CHANNELS_COLUMN_LOGO + + " BLOB," + + Channels.COLUMN_VERSION_NUMBER + + " INTEGER," + + Channels.COLUMN_TRANSIENT + + " INTEGER NOT NULL DEFAULT 0," + + Channels.COLUMN_INTERNAL_PROVIDER_ID + + " TEXT," + // Needed for foreign keys in other tables. + + "UNIQUE(" + + Channels._ID + + "," + + Channels.COLUMN_PACKAGE_NAME + + ")" + + ");"); + db.execSQL( + "CREATE TABLE " + + PROGRAMS_TABLE + + " (" + + Programs._ID + + " INTEGER PRIMARY KEY AUTOINCREMENT," + + Programs.COLUMN_PACKAGE_NAME + + " TEXT NOT NULL," + + Programs.COLUMN_CHANNEL_ID + + " INTEGER," + + Programs.COLUMN_TITLE + + " TEXT," + + Programs.COLUMN_SEASON_DISPLAY_NUMBER + + " TEXT," + + Programs.COLUMN_SEASON_TITLE + + " TEXT," + + Programs.COLUMN_EPISODE_DISPLAY_NUMBER + + " TEXT," + + Programs.COLUMN_EPISODE_TITLE + + " TEXT," + + Programs.COLUMN_START_TIME_UTC_MILLIS + + " INTEGER," + + Programs.COLUMN_END_TIME_UTC_MILLIS + + " INTEGER," + + Programs.COLUMN_BROADCAST_GENRE + + " TEXT," + + Programs.COLUMN_CANONICAL_GENRE + + " TEXT," + + Programs.COLUMN_SHORT_DESCRIPTION + + " TEXT," + + Programs.COLUMN_LONG_DESCRIPTION + + " TEXT," + + Programs.COLUMN_VIDEO_WIDTH + + " INTEGER," + + Programs.COLUMN_VIDEO_HEIGHT + + " INTEGER," + + Programs.COLUMN_AUDIO_LANGUAGE + + " TEXT," + + Programs.COLUMN_CONTENT_RATING + + " TEXT," + + Programs.COLUMN_POSTER_ART_URI + + " TEXT," + + Programs.COLUMN_THUMBNAIL_URI + + " TEXT," + + Programs.COLUMN_SEARCHABLE + + " INTEGER NOT NULL DEFAULT 1," + + Programs.COLUMN_RECORDING_PROHIBITED + + " INTEGER NOT NULL DEFAULT 0," + + Programs.COLUMN_INTERNAL_PROVIDER_DATA + + " BLOB," + + Programs.COLUMN_INTERNAL_PROVIDER_FLAG1 + + " INTEGER," + + Programs.COLUMN_INTERNAL_PROVIDER_FLAG2 + + " INTEGER," + + Programs.COLUMN_INTERNAL_PROVIDER_FLAG3 + + " INTEGER," + + Programs.COLUMN_INTERNAL_PROVIDER_FLAG4 + + " INTEGER," + + Programs.COLUMN_REVIEW_RATING_STYLE + + " INTEGER," + + Programs.COLUMN_REVIEW_RATING + + " TEXT," + + Programs.COLUMN_VERSION_NUMBER + + " INTEGER," + + "FOREIGN KEY(" + + Programs.COLUMN_CHANNEL_ID + + "," + + Programs.COLUMN_PACKAGE_NAME + + ") REFERENCES " + + CHANNELS_TABLE + + "(" + + Channels._ID + + "," + + Channels.COLUMN_PACKAGE_NAME + + ") ON UPDATE CASCADE ON DELETE CASCADE" + + ");"); + db.execSQL( + "CREATE INDEX " + + PROGRAMS_TABLE_PACKAGE_NAME_INDEX + + " ON " + + PROGRAMS_TABLE + + "(" + + Programs.COLUMN_PACKAGE_NAME + + ");"); + db.execSQL( + "CREATE INDEX " + + PROGRAMS_TABLE_CHANNEL_ID_INDEX + + " ON " + + PROGRAMS_TABLE + + "(" + + Programs.COLUMN_CHANNEL_ID + + ");"); + db.execSQL( + "CREATE INDEX " + + PROGRAMS_TABLE_START_TIME_INDEX + + " ON " + + PROGRAMS_TABLE + + "(" + + Programs.COLUMN_START_TIME_UTC_MILLIS + + ");"); + db.execSQL( + "CREATE INDEX " + + PROGRAMS_TABLE_END_TIME_INDEX + + " ON " + + PROGRAMS_TABLE + + "(" + + Programs.COLUMN_END_TIME_UTC_MILLIS + + ");"); + db.execSQL( + "CREATE TABLE " + + WATCHED_PROGRAMS_TABLE + + " (" + + WatchedPrograms._ID + + " INTEGER PRIMARY KEY AUTOINCREMENT," + + WatchedPrograms.COLUMN_PACKAGE_NAME + + " TEXT NOT NULL," + + WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS + + " INTEGER NOT NULL DEFAULT 0," + + WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS + + " INTEGER NOT NULL DEFAULT 0," + + WatchedPrograms.COLUMN_CHANNEL_ID + + " INTEGER," + + WatchedPrograms.COLUMN_TITLE + + " TEXT," + + WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS + + " INTEGER," + + WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS + + " INTEGER," + + WatchedPrograms.COLUMN_DESCRIPTION + + " TEXT," + + WatchedPrograms.COLUMN_INTERNAL_TUNE_PARAMS + + " TEXT," + + WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN + + " TEXT NOT NULL," + + WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + + " INTEGER NOT NULL DEFAULT 0," + + "FOREIGN KEY(" + + WatchedPrograms.COLUMN_CHANNEL_ID + + "," + + WatchedPrograms.COLUMN_PACKAGE_NAME + + ") REFERENCES " + + CHANNELS_TABLE + + "(" + + Channels._ID + + "," + + Channels.COLUMN_PACKAGE_NAME + + ") ON UPDATE CASCADE ON DELETE CASCADE" + + ");"); + db.execSQL( + "CREATE INDEX " + + WATCHED_PROGRAMS_TABLE_CHANNEL_ID_INDEX + + " ON " + + WATCHED_PROGRAMS_TABLE + + "(" + + WatchedPrograms.COLUMN_CHANNEL_ID + + ");"); + db.execSQL(CREATE_RECORDED_PROGRAMS_TABLE_SQL); + db.execSQL(CREATE_PREVIEW_PROGRAMS_TABLE_SQL); + db.execSQL(CREATE_PREVIEW_PROGRAMS_PACKAGE_NAME_INDEX_SQL); + db.execSQL(CREATE_PREVIEW_PROGRAMS_CHANNEL_ID_INDEX_SQL); + db.execSQL(CREATE_WATCH_NEXT_PROGRAMS_TABLE_SQL); + db.execSQL(CREATE_WATCH_NEXT_PROGRAMS_PACKAGE_NAME_INDEX_SQL); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion < 23) { + Log.i( + TAG, + "Upgrading from version " + + oldVersion + + " to " + + newVersion + + ", data will be lost!"); + db.execSQL("DROP TABLE IF EXISTS " + DELETED_CHANNELS_TABLE); + db.execSQL("DROP TABLE IF EXISTS " + WATCHED_PROGRAMS_TABLE); + db.execSQL("DROP TABLE IF EXISTS " + PROGRAMS_TABLE); + db.execSQL("DROP TABLE IF EXISTS " + CHANNELS_TABLE); + + onCreate(db); + return; + } + + Log.i(TAG, "Upgrading from version " + oldVersion + " to " + newVersion + "."); + if (oldVersion <= 23) { + db.execSQL( + "ALTER TABLE " + + CHANNELS_TABLE + + " ADD " + + Channels.COLUMN_INTERNAL_PROVIDER_FLAG1 + + " INTEGER;"); + db.execSQL( + "ALTER TABLE " + + CHANNELS_TABLE + + " ADD " + + Channels.COLUMN_INTERNAL_PROVIDER_FLAG2 + + " INTEGER;"); + db.execSQL( + "ALTER TABLE " + + CHANNELS_TABLE + + " ADD " + + Channels.COLUMN_INTERNAL_PROVIDER_FLAG3 + + " INTEGER;"); + db.execSQL( + "ALTER TABLE " + + CHANNELS_TABLE + + " ADD " + + Channels.COLUMN_INTERNAL_PROVIDER_FLAG4 + + " INTEGER;"); + } + if (oldVersion <= 24) { + db.execSQL( + "ALTER TABLE " + + PROGRAMS_TABLE + + " ADD " + + Programs.COLUMN_INTERNAL_PROVIDER_FLAG1 + + " INTEGER;"); + db.execSQL( + "ALTER TABLE " + + PROGRAMS_TABLE + + " ADD " + + Programs.COLUMN_INTERNAL_PROVIDER_FLAG2 + + " INTEGER;"); + db.execSQL( + "ALTER TABLE " + + PROGRAMS_TABLE + + " ADD " + + Programs.COLUMN_INTERNAL_PROVIDER_FLAG3 + + " INTEGER;"); + db.execSQL( + "ALTER TABLE " + + PROGRAMS_TABLE + + " ADD " + + Programs.COLUMN_INTERNAL_PROVIDER_FLAG4 + + " INTEGER;"); + } + if (oldVersion <= 25) { + db.execSQL( + "ALTER TABLE " + + CHANNELS_TABLE + + " ADD " + + Channels.COLUMN_APP_LINK_ICON_URI + + " TEXT;"); + db.execSQL( + "ALTER TABLE " + + CHANNELS_TABLE + + " ADD " + + Channels.COLUMN_APP_LINK_POSTER_ART_URI + + " TEXT;"); + db.execSQL( + "ALTER TABLE " + + CHANNELS_TABLE + + " ADD " + + Channels.COLUMN_APP_LINK_TEXT + + " TEXT;"); + db.execSQL( + "ALTER TABLE " + + CHANNELS_TABLE + + " ADD " + + Channels.COLUMN_APP_LINK_COLOR + + " INTEGER;"); + db.execSQL( + "ALTER TABLE " + + CHANNELS_TABLE + + " ADD " + + Channels.COLUMN_APP_LINK_INTENT_URI + + " TEXT;"); + db.execSQL( + "ALTER TABLE " + + PROGRAMS_TABLE + + " ADD " + + Programs.COLUMN_SEARCHABLE + + " INTEGER NOT NULL DEFAULT 1;"); + } + if (oldVersion <= 28) { + db.execSQL( + "ALTER TABLE " + + PROGRAMS_TABLE + + " ADD " + + Programs.COLUMN_SEASON_TITLE + + " TEXT;"); + migrateIntegerColumnToTextColumn( + db, + PROGRAMS_TABLE, + Programs.COLUMN_SEASON_NUMBER, + Programs.COLUMN_SEASON_DISPLAY_NUMBER); + migrateIntegerColumnToTextColumn( + db, + PROGRAMS_TABLE, + Programs.COLUMN_EPISODE_NUMBER, + Programs.COLUMN_EPISODE_DISPLAY_NUMBER); + } + if (oldVersion <= 29) { + db.execSQL("DROP TABLE IF EXISTS " + RECORDED_PROGRAMS_TABLE); + db.execSQL(CREATE_RECORDED_PROGRAMS_TABLE_SQL); + } + if (oldVersion <= 30) { + db.execSQL( + "ALTER TABLE " + + PROGRAMS_TABLE + + " ADD " + + Programs.COLUMN_RECORDING_PROHIBITED + + " INTEGER NOT NULL DEFAULT 0;"); + } + if (oldVersion <= 32) { + db.execSQL( + "ALTER TABLE " + + CHANNELS_TABLE + + " ADD " + + Channels.COLUMN_TRANSIENT + + " INTEGER NOT NULL DEFAULT 0;"); + db.execSQL( + "ALTER TABLE " + + CHANNELS_TABLE + + " ADD " + + Channels.COLUMN_INTERNAL_PROVIDER_ID + + " TEXT;"); + db.execSQL( + "ALTER TABLE " + + PROGRAMS_TABLE + + " ADD " + + Programs.COLUMN_REVIEW_RATING_STYLE + + " INTEGER;"); + db.execSQL( + "ALTER TABLE " + + PROGRAMS_TABLE + + " ADD " + + Programs.COLUMN_REVIEW_RATING + + " TEXT;"); + if (oldVersion > 29) { + db.execSQL( + "ALTER TABLE " + + RECORDED_PROGRAMS_TABLE + + " ADD " + + RecordedPrograms.COLUMN_REVIEW_RATING_STYLE + + " INTEGER;"); + db.execSQL( + "ALTER TABLE " + + RECORDED_PROGRAMS_TABLE + + " ADD " + + RecordedPrograms.COLUMN_REVIEW_RATING + + " TEXT;"); + } + } + if (oldVersion <= 33) { + db.execSQL("DROP TABLE IF EXISTS " + PREVIEW_PROGRAMS_TABLE); + db.execSQL("DROP TABLE IF EXISTS " + WATCH_NEXT_PROGRAMS_TABLE); + db.execSQL(CREATE_PREVIEW_PROGRAMS_TABLE_SQL); + db.execSQL(CREATE_PREVIEW_PROGRAMS_PACKAGE_NAME_INDEX_SQL); + db.execSQL(CREATE_PREVIEW_PROGRAMS_CHANNEL_ID_INDEX_SQL); + db.execSQL(CREATE_WATCH_NEXT_PROGRAMS_TABLE_SQL); + db.execSQL(CREATE_WATCH_NEXT_PROGRAMS_PACKAGE_NAME_INDEX_SQL); + } + Log.i(TAG, "Upgrading from version " + oldVersion + " to " + newVersion + " is done."); + } + + @Override + public void onOpen(SQLiteDatabase db) { + // Call a static method on the TvProvider because changes to sInitialized must + // be guarded by a lock on the class. + initOnOpenIfNeeded(mContext, db); + } + + private static void migrateIntegerColumnToTextColumn( + SQLiteDatabase db, String table, String integerColumn, String textColumn) { + db.execSQL("ALTER TABLE " + table + " ADD " + textColumn + " TEXT;"); + db.execSQL( + "UPDATE " + + table + + " SET " + + textColumn + + " = CAST(" + + integerColumn + + " AS TEXT);"); + } + } + + private DatabaseHelper mOpenHelper; + private static SharedPreferences sBlockedPackagesSharedPreference; + private static Map<String, Boolean> sBlockedPackages; + + @Override + public boolean onCreate() { + if (DEBUG) { + Log.d(TAG, "Creating TvProvider"); + } + mOpenHelper = DatabaseHelper.createInstance(getContext()); + return true; + } + + @VisibleForTesting + String getCallingPackage_() { + return mCallingPackage; + } + + public void setCallingPackage(String packageName) { + mCallingPackage = packageName; + } + + void setOpenHelper(DatabaseHelper helper) { + mOpenHelper = helper; + } + + @Override + public String getType(Uri uri) { + switch (sUriMatcher.match(uri)) { + case MATCH_CHANNEL: + return Channels.CONTENT_TYPE; + case MATCH_CHANNEL_ID: + return Channels.CONTENT_ITEM_TYPE; + case MATCH_CHANNEL_ID_LOGO: + return "image/png"; + case MATCH_PASSTHROUGH_ID: + return Channels.CONTENT_ITEM_TYPE; + case MATCH_PROGRAM: + return Programs.CONTENT_TYPE; + case MATCH_PROGRAM_ID: + return Programs.CONTENT_ITEM_TYPE; + case MATCH_WATCHED_PROGRAM: + return WatchedPrograms.CONTENT_TYPE; + case MATCH_WATCHED_PROGRAM_ID: + return WatchedPrograms.CONTENT_ITEM_TYPE; + case MATCH_RECORDED_PROGRAM: + return RecordedPrograms.CONTENT_TYPE; + case MATCH_RECORDED_PROGRAM_ID: + return RecordedPrograms.CONTENT_ITEM_TYPE; + case MATCH_PREVIEW_PROGRAM: + return PreviewPrograms.CONTENT_TYPE; + case MATCH_PREVIEW_PROGRAM_ID: + return PreviewPrograms.CONTENT_ITEM_TYPE; + case MATCH_WATCH_NEXT_PROGRAM: + return WatchNextPrograms.CONTENT_TYPE; + case MATCH_WATCH_NEXT_PROGRAM_ID: + return WatchNextPrograms.CONTENT_ITEM_TYPE; + default: + throw new IllegalArgumentException("Unknown URI " + uri); + } + } + + @Override + public Bundle call(String method, String arg, Bundle extras) { + throw new UnsupportedOperationException(); + } + + @Override + public Cursor query( + Uri uri, + String[] projection, + String selection, + String[] selectionArgs, + String sortOrder) { + ensureInitialized(); + boolean needsToValidateSortOrder = !callerHasAccessAllEpgDataPermission(); + SqlParams params = createSqlParams(OP_QUERY, uri, selection, selectionArgs); + + SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); + queryBuilder.setStrict(needsToValidateSortOrder); + queryBuilder.setTables(params.getTables()); + String orderBy = null; + Map<String, String> projectionMap; + switch (params.getTables()) { + case PROGRAMS_TABLE: + projectionMap = sProgramProjectionMap; + orderBy = DEFAULT_PROGRAMS_SORT_ORDER; + break; + case WATCHED_PROGRAMS_TABLE: + projectionMap = sWatchedProgramProjectionMap; + orderBy = DEFAULT_WATCHED_PROGRAMS_SORT_ORDER; + break; + case RECORDED_PROGRAMS_TABLE: + projectionMap = sRecordedProgramProjectionMap; + break; + case PREVIEW_PROGRAMS_TABLE: + projectionMap = sPreviewProgramProjectionMap; + break; + case WATCH_NEXT_PROGRAMS_TABLE: + projectionMap = sWatchNextProgramProjectionMap; + break; + default: + projectionMap = sChannelProjectionMap; + break; + } + queryBuilder.setProjectionMap(createProjectionMapForQuery(projection, projectionMap)); + if (needsToValidateSortOrder) { + validateSortOrder(sortOrder, projectionMap.keySet()); + } + + // Use the default sort order only if no sort order is specified. + if (!TextUtils.isEmpty(sortOrder)) { + orderBy = sortOrder; + } + + // Get the database and run the query. + SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + Cursor c = + queryBuilder.query( + db, + projection, + params.getSelection(), + params.getSelectionArgs(), + null, + null, + orderBy); + + // Tell the cursor what URI to watch, so it knows when its source data changes. + c.setNotificationUri(getContext().getContentResolver(), uri); + return c; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + ensureInitialized(); + switch (sUriMatcher.match(uri)) { + case MATCH_CHANNEL: + // Preview channels are not necessarily associated with TV input service. + // Therefore, we fill a fake ID to meet not null restriction for preview channels. + if (values.get(Channels.COLUMN_INPUT_ID) == null + && TvContractCompat.PARAM_CHANNEL.equals( + values.get(Channels.COLUMN_TYPE))) { + values.put(Channels.COLUMN_INPUT_ID, EMPTY_STRING); + } + filterContentValues(values, sChannelProjectionMap); + return insertChannel(uri, values); + case MATCH_PROGRAM: + filterContentValues(values, sProgramProjectionMap); + return insertProgram(uri, values); + case MATCH_WATCHED_PROGRAM: + return insertWatchedProgram(uri, values); + case MATCH_RECORDED_PROGRAM: + filterContentValues(values, sRecordedProgramProjectionMap); + return insertRecordedProgram(uri, values); + case MATCH_PREVIEW_PROGRAM: + filterContentValues(values, sPreviewProgramProjectionMap); + return insertPreviewProgram(uri, values); + case MATCH_WATCH_NEXT_PROGRAM: + filterContentValues(values, sWatchNextProgramProjectionMap); + return insertWatchNextProgram(uri, values); + case MATCH_CHANNEL_ID: + case MATCH_CHANNEL_ID_LOGO: + case MATCH_PASSTHROUGH_ID: + case MATCH_PROGRAM_ID: + case MATCH_WATCHED_PROGRAM_ID: + case MATCH_RECORDED_PROGRAM_ID: + case MATCH_PREVIEW_PROGRAM_ID: + throw new UnsupportedOperationException("Cannot insert into that URI: " + uri); + default: + throw new IllegalArgumentException("Unknown URI " + uri); + } + } + + private Uri insertChannel(Uri uri, ContentValues values) { + if (TextUtils.equals( + values.getAsString(Channels.COLUMN_TYPE), TvContractCompat.Channels.TYPE_PREVIEW)) { + blockIllegalAccessFromBlockedPackage(); + } + // Mark the owner package of this channel. + values.put(Channels.COLUMN_PACKAGE_NAME, getCallingPackage_()); + blockIllegalAccessToChannelsSystemColumns(values); + + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + long rowId = db.insert(CHANNELS_TABLE, null, values); + if (rowId > 0) { + Uri channelUri = TvContractCompat.buildChannelUri(rowId); + notifyChange(channelUri); + return channelUri; + } + + throw new SQLException("Failed to insert row into " + uri); + } + + private Uri insertProgram(Uri uri, ContentValues values) { + if (!callerHasAccessAllEpgDataPermission() + || !values.containsKey(Programs.COLUMN_PACKAGE_NAME)) { + // Mark the owner package of this program. System app with a proper permission may + // change the owner of the program. + values.put(Programs.COLUMN_PACKAGE_NAME, getCallingPackage_()); + } + + checkAndConvertGenre(values); + checkAndConvertDeprecatedColumns(values); + + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + long rowId = db.insert(PROGRAMS_TABLE, null, values); + if (rowId > 0) { + Uri programUri = TvContractCompat.buildProgramUri(rowId); + notifyChange(programUri); + return programUri; + } + + throw new SQLException("Failed to insert row into " + uri); + } + + private Uri insertWatchedProgram(Uri uri, ContentValues values) { + if (DEBUG) { + Log.d(TAG, "insertWatchedProgram(uri=" + uri + ", values={" + values + "})"); + } + Long watchStartTime = values.getAsLong(WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS); + Long watchEndTime = values.getAsLong(WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS); + // The system sends only two kinds of watch events: + // 1. The user tunes to a new channel. (COLUMN_WATCH_START_TIME_UTC_MILLIS) + // 2. The user stops watching. (COLUMN_WATCH_END_TIME_UTC_MILLIS) + if (watchStartTime != null && watchEndTime == null) { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + long rowId = db.insert(WATCHED_PROGRAMS_TABLE, null, values); + if (rowId > 0) { + return ContentUris.withAppendedId(WatchedPrograms.CONTENT_URI, rowId); + } + Log.w(TAG, "Failed to insert row for " + values + ". Channel does not exist."); + return null; + } else if (watchStartTime == null && watchEndTime != null) { + return null; + } + // All the other cases are invalid. + throw new IllegalArgumentException( + "Only one of COLUMN_WATCH_START_TIME_UTC_MILLIS and" + + " COLUMN_WATCH_END_TIME_UTC_MILLIS should be specified"); + } + + private Uri insertRecordedProgram(Uri uri, ContentValues values) { + // Mark the owner package of this program. + values.put(Programs.COLUMN_PACKAGE_NAME, getCallingPackage_()); + + checkAndConvertGenre(values); + + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + long rowId = db.insert(RECORDED_PROGRAMS_TABLE, null, values); + if (rowId > 0) { + Uri recordedProgramUri = TvContractCompat.buildRecordedProgramUri(rowId); + notifyChange(recordedProgramUri); + return recordedProgramUri; + } + + throw new SQLException("Failed to insert row into " + uri); + } + + private Uri insertPreviewProgram(Uri uri, ContentValues values) { + if (!values.containsKey(PreviewPrograms.COLUMN_TYPE)) { + throw new IllegalArgumentException( + "Missing the required column: " + PreviewPrograms.COLUMN_TYPE); + } + blockIllegalAccessFromBlockedPackage(); + // Mark the owner package of this program. + values.put(Programs.COLUMN_PACKAGE_NAME, getCallingPackage_()); + blockIllegalAccessToPreviewProgramsSystemColumns(values); + + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + long rowId = db.insert(PREVIEW_PROGRAMS_TABLE, null, values); + if (rowId > 0) { + Uri previewProgramUri = TvContractCompat.buildPreviewProgramUri(rowId); + notifyChange(previewProgramUri); + return previewProgramUri; + } + + throw new SQLException("Failed to insert row into " + uri); + } + + private Uri insertWatchNextProgram(Uri uri, ContentValues values) { + if (!values.containsKey(WatchNextPrograms.COLUMN_TYPE)) { + throw new IllegalArgumentException( + "Missing the required column: " + WatchNextPrograms.COLUMN_TYPE); + } + blockIllegalAccessFromBlockedPackage(); + if (!callerHasAccessAllEpgDataPermission() + || !values.containsKey(Programs.COLUMN_PACKAGE_NAME)) { + // Mark the owner package of this program. System app with a proper permission may + // change the owner of the program. + values.put(Programs.COLUMN_PACKAGE_NAME, getCallingPackage_()); + } + blockIllegalAccessToPreviewProgramsSystemColumns(values); + + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + long rowId = db.insert(WATCH_NEXT_PROGRAMS_TABLE, null, values); + if (rowId > 0) { + Uri watchNextProgramUri = TvContractCompat.buildWatchNextProgramUri(rowId); + notifyChange(watchNextProgramUri); + return watchNextProgramUri; + } + + throw new SQLException("Failed to insert row into " + uri); + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + SqlParams params = createSqlParams(OP_DELETE, uri, selection, selectionArgs); + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + int count; + switch (sUriMatcher.match(uri)) { + case MATCH_CHANNEL_ID_LOGO: + ContentValues values = new ContentValues(); + values.putNull(CHANNELS_COLUMN_LOGO); + count = + db.update( + params.getTables(), + values, + params.getSelection(), + params.getSelectionArgs()); + break; + case MATCH_CHANNEL: + case MATCH_PROGRAM: + case MATCH_WATCHED_PROGRAM: + case MATCH_RECORDED_PROGRAM: + case MATCH_PREVIEW_PROGRAM: + case MATCH_WATCH_NEXT_PROGRAM: + case MATCH_CHANNEL_ID: + case MATCH_PASSTHROUGH_ID: + case MATCH_PROGRAM_ID: + case MATCH_WATCHED_PROGRAM_ID: + case MATCH_RECORDED_PROGRAM_ID: + case MATCH_PREVIEW_PROGRAM_ID: + case MATCH_WATCH_NEXT_PROGRAM_ID: + count = + db.delete( + params.getTables(), + params.getSelection(), + params.getSelectionArgs()); + break; + default: + throw new IllegalArgumentException("Unknown URI " + uri); + } + if (count > 0) { + notifyChange(uri); + } + return count; + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + SqlParams params = createSqlParams(OP_UPDATE, uri, selection, selectionArgs); + blockIllegalAccessToIdAndPackageName(uri, values); + boolean containImmutableColumn = false; + if (params.getTables().equals(CHANNELS_TABLE)) { + filterContentValues(values, sChannelProjectionMap); + containImmutableColumn = disallowModifyChannelType(values, params); + if (containImmutableColumn && sUriMatcher.match(uri) != MATCH_CHANNEL_ID) { + Log.i(TAG, "Updating failed. Attempt to change immutable column for channels."); + return 0; + } + blockIllegalAccessToChannelsSystemColumns(values); + } else if (params.getTables().equals(PROGRAMS_TABLE)) { + filterContentValues(values, sProgramProjectionMap); + checkAndConvertGenre(values); + checkAndConvertDeprecatedColumns(values); + } else if (params.getTables().equals(RECORDED_PROGRAMS_TABLE)) { + filterContentValues(values, sRecordedProgramProjectionMap); + checkAndConvertGenre(values); + } else if (params.getTables().equals(PREVIEW_PROGRAMS_TABLE)) { + filterContentValues(values, sPreviewProgramProjectionMap); + containImmutableColumn = disallowModifyChannelId(values, params); + if (containImmutableColumn && PreviewPrograms.CONTENT_URI.equals(uri)) { + Log.i( + TAG, + "Updating failed. Attempt to change unmodifiable column for " + + "preview programs."); + return 0; + } + blockIllegalAccessToPreviewProgramsSystemColumns(values); + } else if (params.getTables().equals(WATCH_NEXT_PROGRAMS_TABLE)) { + filterContentValues(values, sWatchNextProgramProjectionMap); + blockIllegalAccessToPreviewProgramsSystemColumns(values); + } + if (values.size() == 0) { + // All values may be filtered out, no need to update + return 0; + } + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + int count = + db.update( + params.getTables(), + values, + params.getSelection(), + params.getSelectionArgs()); + if (count > 0) { + notifyChange(uri); + } else if (containImmutableColumn) { + Log.i( + TAG, + "Updating failed. The item may not exist or attempt to change " + + "immutable column."); + } + return count; + } + + private synchronized void ensureInitialized() { + if (!sInitialized) { + // Database is not accessed before and the projection maps and the blocked package list + // are not updated yet. Gets database here to make it initialized. + mOpenHelper.getReadableDatabase(); + } + } + + private static synchronized void initOnOpenIfNeeded(Context context, SQLiteDatabase db) { + if (!sInitialized) { + updateProjectionMap(db, CHANNELS_TABLE, sChannelProjectionMap); + updateProjectionMap(db, PROGRAMS_TABLE, sProgramProjectionMap); + updateProjectionMap(db, WATCHED_PROGRAMS_TABLE, sWatchedProgramProjectionMap); + updateProjectionMap(db, RECORDED_PROGRAMS_TABLE, sRecordedProgramProjectionMap); + updateProjectionMap(db, PREVIEW_PROGRAMS_TABLE, sPreviewProgramProjectionMap); + updateProjectionMap(db, WATCH_NEXT_PROGRAMS_TABLE, sWatchNextProgramProjectionMap); + sBlockedPackagesSharedPreference = + PreferenceManager.getDefaultSharedPreferences(context); + sBlockedPackages = new ConcurrentHashMap<>(); + for (String packageName : + sBlockedPackagesSharedPreference.getStringSet( + SHARED_PREF_BLOCKED_PACKAGES_KEY, new HashSet<>())) { + sBlockedPackages.put(packageName, true); + } + sInitialized = true; + } + } + + private static void updateProjectionMap( + SQLiteDatabase db, String tableName, Map<String, String> projectionMap) { + try (Cursor cursor = db.rawQuery("SELECT * FROM " + tableName + " LIMIT 0", null)) { + for (String columnName : cursor.getColumnNames()) { + if (!projectionMap.containsKey(columnName)) { + projectionMap.put(columnName, tableName + '.' + columnName); + } + } + } + } + + private Map<String, String> createProjectionMapForQuery( + String[] projection, Map<String, String> projectionMap) { + if (projection == null) { + return projectionMap; + } + Map<String, String> columnProjectionMap = new HashMap<>(); + for (String columnName : projection) { + // Value NULL will be provided if the requested column does not exist in the database. + columnProjectionMap.put( + columnName, projectionMap.getOrDefault(columnName, "NULL as " + columnName)); + } + return columnProjectionMap; + } + + private void filterContentValues(ContentValues values, Map<String, String> projectionMap) { + Iterator<String> iter = values.keySet().iterator(); + while (iter.hasNext()) { + String columnName = iter.next(); + if (!projectionMap.containsKey(columnName)) { + iter.remove(); + } + } + } + + private SqlParams createSqlParams( + String operation, Uri uri, String selection, String[] selectionArgs) { + int match = sUriMatcher.match(uri); + SqlParams params = new SqlParams(null, selection, selectionArgs); + + // Control access to EPG data (excluding watched programs) when the caller doesn't have all + // access. + String prefix = match == MATCH_CHANNEL ? CHANNELS_TABLE + "." : ""; + if (!callerHasAccessAllEpgDataPermission() + && match != MATCH_WATCHED_PROGRAM + && match != MATCH_WATCHED_PROGRAM_ID) { + if (!TextUtils.isEmpty(selection)) { + throw new SecurityException("Selection not allowed for " + uri); + } + // Limit the operation only to the data that the calling package owns except for when + // the caller tries to read TV listings and has the appropriate permission. + if (operation.equals(OP_QUERY) && callerHasReadTvListingsPermission()) { + params.setWhere( + prefix + + BaseTvColumns.COLUMN_PACKAGE_NAME + + "=? OR " + + Channels.COLUMN_SEARCHABLE + + "=?", + getCallingPackage_(), + "1"); + } else { + params.setWhere( + prefix + BaseTvColumns.COLUMN_PACKAGE_NAME + "=?", getCallingPackage_()); + } + } + String packageName = uri.getQueryParameter(PARAM_PACKAGE); + if (packageName != null) { + params.appendWhere(prefix + BaseTvColumns.COLUMN_PACKAGE_NAME + "=?", packageName); + } + + switch (match) { + case MATCH_CHANNEL: + String genre = uri.getQueryParameter(TvContractCompat.PARAM_CANONICAL_GENRE); + if (genre == null) { + params.setTables(CHANNELS_TABLE); + } else { + if (!operation.equals(OP_QUERY)) { + throw new SecurityException( + capitalize(operation) + " not allowed for " + uri); + } + if (!Genres.isCanonical(genre)) { + throw new IllegalArgumentException("Not a canonical genre : " + genre); + } + params.setTables(CHANNELS_TABLE_INNER_JOIN_PROGRAMS_TABLE); + String curTime = String.valueOf(System.currentTimeMillis()); + params.appendWhere( + "LIKE(?, " + + Programs.COLUMN_CANONICAL_GENRE + + ") AND " + + Programs.COLUMN_START_TIME_UTC_MILLIS + + "<=? AND " + + Programs.COLUMN_END_TIME_UTC_MILLIS + + ">=?", + "%" + genre + "%", + curTime, + curTime); + } + String inputId = uri.getQueryParameter(TvContractCompat.PARAM_INPUT); + if (inputId != null) { + params.appendWhere(Channels.COLUMN_INPUT_ID + "=?", inputId); + } + boolean browsableOnly = + uri.getBooleanQueryParameter(TvContractCompat.PARAM_BROWSABLE_ONLY, false); + if (browsableOnly) { + params.appendWhere(Channels.COLUMN_BROWSABLE + "=1"); + } + String preview = uri.getQueryParameter(PARAM_PREVIEW); + if (preview != null) { + String previewSelection = + Channels.COLUMN_TYPE + + (preview.equals(String.valueOf(true)) ? "=?" : "!=?"); + params.appendWhere(previewSelection, Channels.TYPE_PREVIEW); + } + break; + case MATCH_CHANNEL_ID: + params.setTables(CHANNELS_TABLE); + params.appendWhere(Channels._ID + "=?", uri.getLastPathSegment()); + break; + case MATCH_PROGRAM: + params.setTables(PROGRAMS_TABLE); + String paramChannelId = uri.getQueryParameter(TvContractCompat.PARAM_CHANNEL); + if (paramChannelId != null) { + String channelId = String.valueOf(Long.parseLong(paramChannelId)); + params.appendWhere(Programs.COLUMN_CHANNEL_ID + "=?", channelId); + } + String paramStartTime = uri.getQueryParameter(TvContractCompat.PARAM_START_TIME); + String paramEndTime = uri.getQueryParameter(TvContractCompat.PARAM_END_TIME); + if (paramStartTime != null && paramEndTime != null) { + String startTime = String.valueOf(Long.parseLong(paramStartTime)); + String endTime = String.valueOf(Long.parseLong(paramEndTime)); + params.appendWhere( + Programs.COLUMN_START_TIME_UTC_MILLIS + + "<=? AND " + + Programs.COLUMN_END_TIME_UTC_MILLIS + + ">=? AND ?<=?", + endTime, + startTime, + startTime, + endTime); + } + break; + case MATCH_PROGRAM_ID: + params.setTables(PROGRAMS_TABLE); + params.appendWhere(Programs._ID + "=?", uri.getLastPathSegment()); + break; + case MATCH_WATCHED_PROGRAM: + if (!callerHasAccessWatchedProgramsPermission()) { + throw new SecurityException("Access not allowed for " + uri); + } + params.setTables(WATCHED_PROGRAMS_TABLE); + params.appendWhere(WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=?", "1"); + break; + case MATCH_WATCHED_PROGRAM_ID: + if (!callerHasAccessWatchedProgramsPermission()) { + throw new SecurityException("Access not allowed for " + uri); + } + params.setTables(WATCHED_PROGRAMS_TABLE); + params.appendWhere(WatchedPrograms._ID + "=?", uri.getLastPathSegment()); + params.appendWhere(WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=?", "1"); + break; + case MATCH_RECORDED_PROGRAM_ID: + params.appendWhere(RecordedPrograms._ID + "=?", uri.getLastPathSegment()); + // fall-through + case MATCH_RECORDED_PROGRAM: + params.setTables(RECORDED_PROGRAMS_TABLE); + paramChannelId = uri.getQueryParameter(TvContractCompat.PARAM_CHANNEL); + if (paramChannelId != null) { + String channelId = String.valueOf(Long.parseLong(paramChannelId)); + params.appendWhere(Programs.COLUMN_CHANNEL_ID + "=?", channelId); + } + break; + case MATCH_PREVIEW_PROGRAM_ID: + params.appendWhere(PreviewPrograms._ID + "=?", uri.getLastPathSegment()); + // fall-through + case MATCH_PREVIEW_PROGRAM: + params.setTables(PREVIEW_PROGRAMS_TABLE); + paramChannelId = uri.getQueryParameter(TvContractCompat.PARAM_CHANNEL); + if (paramChannelId != null) { + String channelId = String.valueOf(Long.parseLong(paramChannelId)); + params.appendWhere(PreviewPrograms.COLUMN_CHANNEL_ID + "=?", channelId); + } + break; + case MATCH_WATCH_NEXT_PROGRAM_ID: + params.appendWhere(WatchNextPrograms._ID + "=?", uri.getLastPathSegment()); + // fall-through + case MATCH_WATCH_NEXT_PROGRAM: + params.setTables(WATCH_NEXT_PROGRAMS_TABLE); + break; + case MATCH_CHANNEL_ID_LOGO: + if (operation.equals(OP_DELETE)) { + params.setTables(CHANNELS_TABLE); + params.appendWhere(Channels._ID + "=?", uri.getPathSegments().get(1)); + break; + } + // fall-through + case MATCH_PASSTHROUGH_ID: + throw new UnsupportedOperationException(operation + " not permmitted on " + uri); + default: + throw new IllegalArgumentException("Unknown URI " + uri); + } + return params; + } + + private static String capitalize(String str) { + return Character.toUpperCase(str.charAt(0)) + str.substring(1); + } + + @SuppressLint("DefaultLocale") + private void checkAndConvertGenre(ContentValues values) { + String canonicalGenres = values.getAsString(Programs.COLUMN_CANONICAL_GENRE); + + if (!TextUtils.isEmpty(canonicalGenres)) { + // Check if the canonical genres are valid. If not, clear them. + String[] genres = Genres.decode(canonicalGenres); + for (String genre : genres) { + if (!Genres.isCanonical(genre)) { + values.putNull(Programs.COLUMN_CANONICAL_GENRE); + canonicalGenres = null; + break; + } + } + } + + if (TextUtils.isEmpty(canonicalGenres)) { + // If the canonical genre is not set, try to map the broadcast genre to the canonical + // genre. + String broadcastGenres = values.getAsString(Programs.COLUMN_BROADCAST_GENRE); + if (!TextUtils.isEmpty(broadcastGenres)) { + Set<String> genreSet = new HashSet<>(); + String[] genres = Genres.decode(broadcastGenres); + for (String genre : genres) { + String canonicalGenre = sGenreMap.get(genre.toUpperCase()); + if (Genres.isCanonical(canonicalGenre)) { + genreSet.add(canonicalGenre); + } + } + if (genreSet.size() > 0) { + values.put( + Programs.COLUMN_CANONICAL_GENRE, + Genres.encode(genreSet.toArray(new String[genreSet.size()]))); + } + } + } + } + + private void checkAndConvertDeprecatedColumns(ContentValues values) { + if (values.containsKey(Programs.COLUMN_SEASON_NUMBER)) { + if (!values.containsKey(Programs.COLUMN_SEASON_DISPLAY_NUMBER)) { + values.put( + Programs.COLUMN_SEASON_DISPLAY_NUMBER, + values.getAsInteger(Programs.COLUMN_SEASON_NUMBER)); + } + values.remove(Programs.COLUMN_SEASON_NUMBER); + } + if (values.containsKey(Programs.COLUMN_EPISODE_NUMBER)) { + if (!values.containsKey(Programs.COLUMN_EPISODE_DISPLAY_NUMBER)) { + values.put( + Programs.COLUMN_EPISODE_DISPLAY_NUMBER, + values.getAsInteger(Programs.COLUMN_EPISODE_NUMBER)); + } + values.remove(Programs.COLUMN_EPISODE_NUMBER); + } + } + + // We might have more than one thread trying to make its way through applyBatch() so the + // notification coalescing needs to be thread-local to work correctly. + private final ThreadLocal<Set<Uri>> mTLBatchNotifications = new ThreadLocal<>(); + + private Set<Uri> getBatchNotificationsSet() { + return mTLBatchNotifications.get(); + } + + private void setBatchNotificationsSet(Set<Uri> batchNotifications) { + mTLBatchNotifications.set(batchNotifications); + } + + @Override + public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) + throws OperationApplicationException { + setBatchNotificationsSet(new HashSet<Uri>()); + Context context = getContext(); + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + ContentProviderResult[] results = super.applyBatch(operations); + db.setTransactionSuccessful(); + return results; + } finally { + db.endTransaction(); + final Set<Uri> notifications = getBatchNotificationsSet(); + setBatchNotificationsSet(null); + for (final Uri uri : notifications) { + context.getContentResolver().notifyChange(uri, null); + } + } + } + + @Override + public int bulkInsert(Uri uri, ContentValues[] values) { + setBatchNotificationsSet(new HashSet<Uri>()); + Context context = getContext(); + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + int result = super.bulkInsert(uri, values); + db.setTransactionSuccessful(); + return result; + } finally { + db.endTransaction(); + final Set<Uri> notifications = getBatchNotificationsSet(); + setBatchNotificationsSet(null); + for (final Uri notificationUri : notifications) { + context.getContentResolver().notifyChange(notificationUri, null); + } + } + } + + private void notifyChange(Uri uri) { + final Set<Uri> batchNotifications = getBatchNotificationsSet(); + if (batchNotifications != null) { + batchNotifications.add(uri); + } else { + getContext().getContentResolver().notifyChange(uri, null); + } + } + + private boolean callerHasReadTvListingsPermission() { + return getContext().checkCallingOrSelfPermission(PERMISSION_READ_TV_LISTINGS) + == PackageManager.PERMISSION_GRANTED; + } + + private boolean callerHasAccessAllEpgDataPermission() { + return getContext().checkCallingOrSelfPermission(PERMISSION_ACCESS_ALL_EPG_DATA) + == PackageManager.PERMISSION_GRANTED; + } + + private boolean callerHasAccessWatchedProgramsPermission() { + return getContext().checkCallingOrSelfPermission(PERMISSION_ACCESS_WATCHED_PROGRAMS) + == PackageManager.PERMISSION_GRANTED; + } + + private boolean callerHasModifyParentalControlsPermission() { + return getContext() + .checkCallingOrSelfPermission( + android.Manifest.permission.MODIFY_PARENTAL_CONTROLS) + == PackageManager.PERMISSION_GRANTED; + } + + private void blockIllegalAccessToIdAndPackageName(Uri uri, ContentValues values) { + if (values.containsKey(BaseColumns._ID)) { + int match = sUriMatcher.match(uri); + switch (match) { + case MATCH_CHANNEL_ID: + case MATCH_PROGRAM_ID: + case MATCH_PREVIEW_PROGRAM_ID: + case MATCH_RECORDED_PROGRAM_ID: + case MATCH_WATCH_NEXT_PROGRAM_ID: + case MATCH_WATCHED_PROGRAM_ID: + if (TextUtils.equals( + values.getAsString(BaseColumns._ID), uri.getLastPathSegment())) { + break; + } + // fall through + default: + throw new IllegalArgumentException("Not allowed to change ID."); + } + } + if (values.containsKey(BaseTvColumns.COLUMN_PACKAGE_NAME) + && !callerHasAccessAllEpgDataPermission() + && !TextUtils.equals( + values.getAsString(BaseTvColumns.COLUMN_PACKAGE_NAME), + getCallingPackage_())) { + throw new SecurityException("Not allowed to change package name."); + } + } + + private void blockIllegalAccessToChannelsSystemColumns(ContentValues values) { + if (values.containsKey(Channels.COLUMN_LOCKED) + && !callerHasModifyParentalControlsPermission()) { + throw new SecurityException("Not allowed to access Channels.COLUMN_LOCKED"); + } + Boolean hasAccessAllEpgDataPermission = null; + if (values.containsKey(Channels.COLUMN_BROWSABLE)) { + hasAccessAllEpgDataPermission = callerHasAccessAllEpgDataPermission(); + if (!hasAccessAllEpgDataPermission) { + throw new SecurityException("Not allowed to access Channels.COLUMN_BROWSABLE"); + } + } + } + + private void blockIllegalAccessToPreviewProgramsSystemColumns(ContentValues values) { + if (values.containsKey(PreviewPrograms.COLUMN_BROWSABLE) + && !callerHasAccessAllEpgDataPermission()) { + throw new SecurityException("Not allowed to access Programs.COLUMN_BROWSABLE"); + } + } + + private void blockIllegalAccessFromBlockedPackage() { + String callingPackageName = getCallingPackage_(); + if (sBlockedPackages.containsKey(callingPackageName)) { + throw new SecurityException( + "Not allowed to access " + + TvContractCompat.AUTHORITY + + ", " + + callingPackageName + + " is blocked"); + } + } + + private boolean disallowModifyChannelType(ContentValues values, SqlParams params) { + if (values.containsKey(Channels.COLUMN_TYPE)) { + params.appendWhere( + Channels.COLUMN_TYPE + "=?", values.getAsString(Channels.COLUMN_TYPE)); + return true; + } + return false; + } + + private boolean disallowModifyChannelId(ContentValues values, SqlParams params) { + if (values.containsKey(PreviewPrograms.COLUMN_CHANNEL_ID)) { + params.appendWhere( + PreviewPrograms.COLUMN_CHANNEL_ID + "=?", + values.getAsString(PreviewPrograms.COLUMN_CHANNEL_ID)); + return true; + } + return false; + } + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { + switch (sUriMatcher.match(uri)) { + case MATCH_CHANNEL_ID_LOGO: + return openLogoFile(uri, mode); + default: + throw new FileNotFoundException(uri.toString()); + } + } + + private ParcelFileDescriptor openLogoFile(Uri uri, String mode) throws FileNotFoundException { + long channelId = Long.parseLong(uri.getPathSegments().get(1)); + + SqlParams params = + new SqlParams(CHANNELS_TABLE, Channels._ID + "=?", String.valueOf(channelId)); + if (!callerHasAccessAllEpgDataPermission()) { + if (callerHasReadTvListingsPermission()) { + params.appendWhere( + Channels.COLUMN_PACKAGE_NAME + "=? OR " + Channels.COLUMN_SEARCHABLE + "=?", + getCallingPackage_(), + "1"); + } else { + params.appendWhere(Channels.COLUMN_PACKAGE_NAME + "=?", getCallingPackage_()); + } + } + + SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); + queryBuilder.setTables(params.getTables()); + + // We don't write the database here. + SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + if (mode.equals("r")) { + String sql = + queryBuilder.buildQuery( + new String[] {CHANNELS_COLUMN_LOGO}, + params.getSelection(), + null, + null, + null, + null); + ParcelFileDescriptor fd = + DatabaseUtils.blobFileDescriptorForQuery(db, sql, params.getSelectionArgs()); + if (fd == null) { + throw new FileNotFoundException(uri.toString()); + } + return fd; + } else { + try (Cursor cursor = + queryBuilder.query( + db, + new String[] {Channels._ID}, + params.getSelection(), + params.getSelectionArgs(), + null, + null, + null)) { + if (cursor.getCount() < 1) { + // Fails early if corresponding channel does not exist. + // PipeMonitor may still fail to update DB later. + throw new FileNotFoundException(uri.toString()); + } + } + + try { + ParcelFileDescriptor[] pipeFds = ParcelFileDescriptor.createPipe(); + PipeMonitor pipeMonitor = new PipeMonitor(pipeFds[0], channelId, params); + pipeMonitor.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + return pipeFds[1]; + } catch (IOException ioe) { + FileNotFoundException fne = new FileNotFoundException(uri.toString()); + fne.initCause(ioe); + throw fne; + } + } + } + + /** + * Validates the sort order based on the given field set. + * + * @throws IllegalArgumentException if there is any unknown field. + */ + @SuppressLint("DefaultLocale") + private static void validateSortOrder(String sortOrder, Set<String> possibleFields) { + if (TextUtils.isEmpty(sortOrder) || possibleFields.isEmpty()) { + return; + } + String[] orders = sortOrder.split(","); + for (String order : orders) { + String field = + order.replaceAll("\\s+", " ") + .trim() + .toLowerCase() + .replace(" asc", "") + .replace(" desc", ""); + if (!possibleFields.contains(field)) { + throw new IllegalArgumentException("Illegal field in sort order " + order); + } + } + } + + private class PipeMonitor extends AsyncTask<Void, Void, Void> { + private final ParcelFileDescriptor mPfd; + private final long mChannelId; + private final SqlParams mParams; + + private PipeMonitor(ParcelFileDescriptor pfd, long channelId, SqlParams params) { + mPfd = pfd; + mChannelId = channelId; + mParams = params; + } + + @Override + protected Void doInBackground(Void... params) { + int count = 0; + try (AutoCloseInputStream is = new AutoCloseInputStream(mPfd); + ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + Bitmap bitmap = BitmapFactory.decodeStream(is); + if (bitmap == null) { + Log.e(TAG, "Failed to decode logo image for channel ID " + mChannelId); + return null; + } + + float scaleFactor = + Math.min( + 1f, + ((float) MAX_LOGO_IMAGE_SIZE) + / Math.max(bitmap.getWidth(), bitmap.getHeight())); + if (scaleFactor < 1f) { + bitmap = + Bitmap.createScaledBitmap( + bitmap, + (int) (bitmap.getWidth() * scaleFactor), + (int) (bitmap.getHeight() * scaleFactor), + false); + } + bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos); + byte[] bytes = baos.toByteArray(); + + ContentValues values = new ContentValues(); + values.put(CHANNELS_COLUMN_LOGO, bytes); + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + count = + db.update( + mParams.getTables(), + values, + mParams.getSelection(), + mParams.getSelectionArgs()); + if (count > 0) { + Uri uri = TvContractCompat.buildChannelLogoUri(mChannelId); + notifyChange(uri); + } + } catch (IOException e) { + Log.e(TAG, "Failed to write logo for channel ID " + mChannelId, e); + + } finally { + if (count == 0) { + try { + mPfd.closeWithError("Failed to write logo for channel ID " + mChannelId); + } catch (IOException ioe) { + Log.e(TAG, "Failed to close pipe", ioe); + } + } + } + return null; + } + } + + /** + * Column definitions for the TV programs that the user watched. Applications do not have access + * to this table. + * + * <p> + * + * <p>By default, the query results will be sorted by {@link + * WatchedPrograms#COLUMN_WATCH_START_TIME_UTC_MILLIS} in descending order. + * + * @hide + */ + public static final class WatchedPrograms implements BaseTvColumns { + + /** The content:// style URI for this table. */ + public static final Uri CONTENT_URI = + Uri.parse("content://" + TvContract.AUTHORITY + "/watched_program"); + + /** The MIME type of a directory of watched programs. */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/watched_program"; + + /** The MIME type of a single item in this table. */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/watched_program"; + + /** + * The UTC time that the user started watching this TV program, in milliseconds since the + * epoch. + * + * <p> + * + * <p>Type: INTEGER (long) + */ + public static final String COLUMN_WATCH_START_TIME_UTC_MILLIS = + "watch_start_time_utc_millis"; + + /** + * The UTC time that the user stopped watching this TV program, in milliseconds since the + * epoch. + * + * <p> + * + * <p>Type: INTEGER (long) + */ + public static final String COLUMN_WATCH_END_TIME_UTC_MILLIS = "watch_end_time_utc_millis"; + + /** + * The ID of the TV channel that provides this TV program. + * + * <p> + * + * <p>This is a required field. + * + * <p> + * + * <p>Type: INTEGER (long) + */ + public static final String COLUMN_CHANNEL_ID = "channel_id"; + + /** + * The title of this TV program. + * + * <p> + * + * <p>Type: TEXT + */ + public static final String COLUMN_TITLE = "title"; + + /** + * The start time of this TV program, in milliseconds since the epoch. + * + * <p> + * + * <p>Type: INTEGER (long) + */ + public static final String COLUMN_START_TIME_UTC_MILLIS = "start_time_utc_millis"; + + /** + * The end time of this TV program, in milliseconds since the epoch. + * + * <p> + * + * <p>Type: INTEGER (long) + */ + public static final String COLUMN_END_TIME_UTC_MILLIS = "end_time_utc_millis"; + + /** + * The description of this TV program. + * + * <p> + * + * <p>Type: TEXT + */ + public static final String COLUMN_DESCRIPTION = "description"; + + /** + * Extra parameters given to {@link TvInputService.Session#tune(Uri, android.os.Bundle) + * TvInputService.Session.tune(Uri, android.os.Bundle)} when tuning to the channel that + * provides this TV program. (Used internally.) + * + * <p> + * + * <p>This column contains an encoded string that represents comma-separated key-value pairs + * of the tune parameters. (Ex. "[key1]=[value1], [key2]=[value2]"). '%' is used as an + * escape character for '%', '=', and ','. + * + * <p> + * + * <p>Type: TEXT + */ + public static final String COLUMN_INTERNAL_TUNE_PARAMS = "tune_params"; + + /** + * The session token of this TV program. (Used internally.) + * + * <p> + * + * <p>This contains a String representation of {@link IBinder} for {@link + * TvInputService.Session} that provides the current TV program. It is used internally to + * distinguish watched programs entries from different TV input sessions. + * + * <p> + * + * <p>Type: TEXT + */ + public static final String COLUMN_INTERNAL_SESSION_TOKEN = "session_token"; + + private WatchedPrograms() {} + } +} diff --git a/tests/common/src/com/android/tv/testing/SingletonProvider.java b/tests/common/src/com/android/tv/testing/SingletonProvider.java new file mode 100644 index 00000000..d9c2d409 --- /dev/null +++ b/tests/common/src/com/android/tv/testing/SingletonProvider.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2017 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.testing; + +import javax.inject.Provider; + +/** A Provider that always returns the same instance. */ +public class SingletonProvider<T> implements Provider<T> { + private final T t; + + private SingletonProvider(T t) { + this.t = t; + } + + @Override + public T get() { + return t; + } + + public static <S, T extends S> Provider<S> create(T t) { + return new SingletonProvider<S>(t); + } +} diff --git a/tests/common/src/com/android/tv/testing/TestSingletonApp.java b/tests/common/src/com/android/tv/testing/TestSingletonApp.java new file mode 100644 index 00000000..f55ed8d4 --- /dev/null +++ b/tests/common/src/com/android/tv/testing/TestSingletonApp.java @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2017 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.testing; + +import android.app.Application; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.media.tv.TvInputManager; +import android.os.AsyncTask; +import com.android.tv.InputSessionManager; +import com.android.tv.MainActivityWrapper; +import com.android.tv.TvSingletons; +import com.android.tv.analytics.Analytics; +import com.android.tv.analytics.Tracker; +import com.android.tv.common.BaseApplication; +import com.android.tv.common.config.api.RemoteConfig; +import com.android.tv.common.experiments.ExperimentLoader; +import com.android.tv.common.recording.RecordingStorageStatusManager; +import com.android.tv.common.util.Clock; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.data.PreviewDataManager; +import com.android.tv.data.ProgramDataManager; +import com.android.tv.data.epg.EpgFetcher; +import com.android.tv.data.epg.EpgReader; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.DvrScheduleManager; +import com.android.tv.dvr.DvrWatchedPositionManager; +import com.android.tv.dvr.recorder.RecordingScheduler; +import com.android.tv.perf.PerformanceMonitor; +import com.android.tv.perf.StubPerformanceMonitor; +import com.android.tv.testing.dvr.DvrDataManagerInMemoryImpl; +import com.android.tv.testing.testdata.TestData; +import com.android.tv.tuner.TunerInputController; +import com.android.tv.util.SetupUtils; +import com.android.tv.util.TvInputManagerHelper; +import com.android.tv.util.account.AccountHelper; +import java.util.concurrent.Executor; +import javax.inject.Provider; + +/** Test application for Live TV. */ +public class TestSingletonApp extends Application implements TvSingletons { + public final FakeClock fakeClock = FakeClock.createWithCurrentTime(); + public final FakeEpgReader epgReader = new FakeEpgReader(fakeClock); + public final FakeRemoteConfig remoteConfig = new FakeRemoteConfig(); + public final FakeEpgFetcher epgFetcher = new FakeEpgFetcher(); + + public FakeTvInputManagerHelper tvInputManagerHelper; + public SetupUtils setupUtils; + public DvrManager dvrManager; + public DvrDataManager mDvrDataManager; + + private final Provider<EpgReader> mEpgReaderProvider = SingletonProvider.create(epgReader); + private TunerInputController mTunerInputController; + private PerformanceMonitor mPerformanceMonitor; + private ChannelDataManager mChannelDataManager; + + @Override + public void onCreate() { + super.onCreate(); + mTunerInputController = + new TunerInputController( + ComponentName.unflattenFromString(getEmbeddedTunerInputId())); + + tvInputManagerHelper = new FakeTvInputManagerHelper(this); + setupUtils = SetupUtils.createForTvSingletons(this); + tvInputManagerHelper.start(); + mChannelDataManager = new ChannelDataManager(this, tvInputManagerHelper); + mChannelDataManager.start(); + mDvrDataManager = new DvrDataManagerInMemoryImpl(this, fakeClock); + // HACK reset the singleton for tests + BaseApplication.sSingletons = this; + } + + public void loadTestData(TestData testData, long durationMs) { + tvInputManagerHelper + .getFakeTvInputManager() + .add(testData.getTvInputInfo(), TvInputManager.INPUT_STATE_CONNECTED); + testData.init(this, fakeClock, durationMs); + } + + @Override + public Analytics getAnalytics() { + return null; + } + + @Override + public void handleInputCountChanged() {} + + @Override + public ChannelDataManager getChannelDataManager() { + return mChannelDataManager; + } + + @Override + public boolean isChannelDataManagerLoadFinished() { + return false; + } + + @Override + public ProgramDataManager getProgramDataManager() { + return null; + } + + @Override + public boolean isProgramDataManagerCurrentProgramsLoadFinished() { + return false; + } + + @Override + public PreviewDataManager getPreviewDataManager() { + return null; + } + + @Override + public DvrDataManager getDvrDataManager() { + return mDvrDataManager; + } + + @Override + public DvrScheduleManager getDvrScheduleManager() { + return null; + } + + @Override + public DvrManager getDvrManager() { + return dvrManager; + } + + @Override + public RecordingScheduler getRecordingScheduler() { + return null; + } + + @Override + public DvrWatchedPositionManager getDvrWatchedPositionManager() { + return null; + } + + @Override + public InputSessionManager getInputSessionManager() { + return null; + } + + @Override + public Tracker getTracker() { + return null; + } + + @Override + public TvInputManagerHelper getTvInputManagerHelper() { + return tvInputManagerHelper; + } + + @Override + public Provider<EpgReader> providesEpgReader() { + return mEpgReaderProvider; + } + + @Override + public EpgFetcher getEpgFetcher() { + return epgFetcher; + } + + @Override + public SetupUtils getSetupUtils() { + return setupUtils; + } + + @Override + public TunerInputController getTunerInputController() { + return mTunerInputController; + } + + @Override + public ExperimentLoader getExperimentLoader() { + return new ExperimentLoader(); + } + + @Override + public MainActivityWrapper getMainActivityWrapper() { + return null; + } + + @Override + public AccountHelper getAccountHelper() { + return null; + } + + @Override + public Clock getClock() { + return fakeClock; + } + + @Override + public RecordingStorageStatusManager getRecordingStorageStatusManager() { + return null; + } + + @Override + public RemoteConfig getRemoteConfig() { + return remoteConfig; + } + + @Override + public Intent getTunerSetupIntent(Context context) { + return null; + } + + @Override + public boolean isRunningInMainProcess() { + return false; + } + + @Override + public PerformanceMonitor getPerformanceMonitor() { + if (mPerformanceMonitor == null) { + mPerformanceMonitor = new StubPerformanceMonitor(); + } + return mPerformanceMonitor; + } + + @Override + public String getEmbeddedTunerInputId() { + return "com.android.tv/.tuner.tvinput.TunerTvInputService"; + } + + @Override + public Executor getDbExecutor() { + return AsyncTask.SERIAL_EXECUTOR; + } +} diff --git a/tests/common/src/com/android/tv/testing/activities/BaseMainActivityTestCase.java b/tests/common/src/com/android/tv/testing/activities/BaseMainActivityTestCase.java new file mode 100644 index 00000000..666f8181 --- /dev/null +++ b/tests/common/src/com/android/tv/testing/activities/BaseMainActivityTestCase.java @@ -0,0 +1,141 @@ +/* + * 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.testing.activities; + +import static android.support.test.InstrumentationRegistry.getInstrumentation; + +import android.content.Context; +import android.os.SystemClock; +import android.support.test.rule.ActivityTestRule; +import android.text.TextUtils; +import com.android.tv.MainActivity; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.data.api.Channel; +import com.android.tv.testing.data.ChannelInfo; +import com.android.tv.testing.testinput.ChannelStateData; +import com.android.tv.testing.testinput.TestInputControlConnection; +import com.android.tv.testing.testinput.TestInputControlUtils; +import com.android.tv.testing.testinput.TvTestInputConstants; +import java.util.List; +import org.junit.Before; +import org.junit.Rule; + +/** Base TestCase for tests that need a {@link MainActivity}. */ +public abstract class BaseMainActivityTestCase { + private static final String TAG = "BaseMainActivityTest"; + private static final int CHANNEL_LOADING_CHECK_INTERVAL_MS = 10; + + @Rule + public ActivityTestRule<MainActivity> mActivityTestRule = + new ActivityTestRule<>(MainActivity.class); + + protected final TestInputControlConnection mConnection = new TestInputControlConnection(); + + protected MainActivity mActivity; + + @Before + public void setUp() { + mActivity = mActivityTestRule.getActivity(); + // TODO: ensure the SampleInputs are setup. + getInstrumentation() + .getTargetContext() + .bindService( + TestInputControlUtils.createIntent(), + mConnection, + Context.BIND_AUTO_CREATE); + } + + @Before + public void tearDown() { + if (mConnection.isBound()) { + getInstrumentation().getTargetContext().unbindService(mConnection); + } + } + + /** + * Tune to {@code channel}. + * + * @param channel the channel to tune to. + */ + protected void tuneToChannel(final Channel channel) { + // Run on UI thread so views can be modified + getInstrumentation() + .runOnMainSync( + new Runnable() { + @Override + public void run() { + mActivity.tuneToChannel(channel); + } + }); + } + + /** Sleep until @{@link ChannelDataManager#isDbLoadFinished()} is true. */ + protected void waitUntilChannelLoadingFinish() { + ChannelDataManager channelDataManager = mActivity.getChannelDataManager(); + while (!channelDataManager.isDbLoadFinished()) { + getInstrumentation().waitForIdleSync(); + SystemClock.sleep(CHANNEL_LOADING_CHECK_INTERVAL_MS); + } + } + + /** + * Tune to the channel with {@code name}. + * + * @param name the name of the channel to find. + */ + protected void tuneToChannel(String name) { + Channel c = findChannelWithName(name); + tuneToChannel(c); + } + + /** Tune to channel. */ + protected void tuneToChannel(ChannelInfo channel) { + tuneToChannel(channel.name); + } + + /** + * Update the channel state to {@code data} then tune to that channel. + * + * @param data the state to update the channel with. + * @param channel the channel to tune to + */ + protected void updateThenTune(ChannelStateData data, ChannelInfo channel) { + if (channel.equals(TvTestInputConstants.CH_1_DEFAULT_DONT_MODIFY)) { + throw new IllegalArgumentException( + "By convention " + + TvTestInputConstants.CH_1_DEFAULT_DONT_MODIFY.name + + " should not be modified."); + } + mConnection.updateChannelState(channel, data); + tuneToChannel(channel); + } + + private Channel findChannelWithName(String displayName) { + waitUntilChannelLoadingFinish(); + Channel channel = null; + List<Channel> channelList = mActivity.getChannelDataManager().getChannelList(); + for (Channel c : channelList) { + if (TextUtils.equals(c.getDisplayName(), displayName)) { + channel = c; + break; + } + } + if (channel == null) { + throw new AssertionError("'" + displayName + "' channel not found"); + } + return channel; + } +} diff --git a/tests/common/src/com/android/tv/testing/constants/ConfigConstants.java b/tests/common/src/com/android/tv/testing/constants/ConfigConstants.java new file mode 100644 index 00000000..890c51e0 --- /dev/null +++ b/tests/common/src/com/android/tv/testing/constants/ConfigConstants.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2017 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.testing.constants; + +import android.os.Build; + +/** Constants for Robolectic Config. */ +public final class ConfigConstants { + + public static final String MANIFEST = "vendor/unbundled_google/packages/TV/AndroidManifest.xml"; + public static final int SDK = Build.VERSION_CODES.M; + + private ConfigConstants() {} +} diff --git a/tests/common/src/com/android/tv/testing/constants/Constants.java b/tests/common/src/com/android/tv/testing/constants/Constants.java new file mode 100644 index 00000000..09e1ada1 --- /dev/null +++ b/tests/common/src/com/android/tv/testing/constants/Constants.java @@ -0,0 +1,47 @@ +/* + * 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.testing.constants; + +import android.media.tv.TvTrackInfo; + +/** Constants for testing. */ +public final class Constants { + public static final int FUNC_TEST_CHANNEL_COUNT = 100; + public static final int UNIT_TEST_CHANNEL_COUNT = 4; + public static final int JANK_TEST_CHANNEL_COUNT = 500; // TODO: increase to 1000 see b/23526997 + + public static final TvTrackInfo EN_STEREO_AUDIO_TRACK = + new TvTrackInfo.Builder(TvTrackInfo.TYPE_AUDIO, "English Stereo Audio") + .setLanguage("en") + .setAudioChannelCount(2) + .build(); + public static final TvTrackInfo GENERIC_AUDIO_TRACK = + new TvTrackInfo.Builder(TvTrackInfo.TYPE_AUDIO, "Generic Audio").build(); + + public static final TvTrackInfo FHD1080P50_VIDEO_TRACK = + new TvTrackInfo.Builder(TvTrackInfo.TYPE_VIDEO, "FHD Video") + .setVideoHeight(1080) + .setVideoWidth(1920) + .setVideoFrameRate(50) + .build(); + public static final TvTrackInfo SVGA_VIDEO_TRACK = + new TvTrackInfo.Builder(TvTrackInfo.TYPE_VIDEO, "SVGA Video") + .setVideoHeight(600) + .setVideoWidth(800) + .build(); + + private Constants() {} +} diff --git a/tests/common/src/com/android/tv/testing/TvContentRatingConstants.java b/tests/common/src/com/android/tv/testing/constants/TvContentRatingConstants.java index c4c96fed..e1a3d906 100644 --- a/tests/common/src/com/android/tv/testing/TvContentRatingConstants.java +++ b/tests/common/src/com/android/tv/testing/constants/TvContentRatingConstants.java @@ -14,20 +14,21 @@ * limitations under the License. */ -package com.android.tv.testing; +package com.android.tv.testing.constants; import android.media.tv.TvContentRating; -/** - * Constants for the content rating strings. - */ +/** Constants for the content rating strings. */ public final class TvContentRatingConstants { /** * A content rating object. * * <p>Domain: com.android.tv + * * <p>Rating system: US_TV + * * <p>Rating: US_TV_Y7 + * * <p>Sub ratings: US_TV_FV */ public static final TvContentRating CONTENT_RATING_US_TV_Y7_US_TV_FV = @@ -39,7 +40,9 @@ public final class TvContentRatingConstants { * A content rating object. * * <p>Domain: com.android.tv + * * <p>Rating system: US_TV + * * <p>Rating: US_TV_MA */ public static final TvContentRating CONTENT_RATING_US_TV_MA = @@ -51,11 +54,14 @@ public final class TvContentRatingConstants { * A content rating object. * * <p>Domain: com.android.tv + * * <p>Rating system: US_TV + * * <p>Rating: US_TV_PG + * * <p>Sub ratings: US_TV_L, US_TV_S */ public static final TvContentRating CONTENT_RATING_US_TV_PG_US_TV_L_US_TV_S = - TvContentRating.createRating("com.android.tv", "US_TV", "US_TV_PG", "US_TV_L", - "US_TV_S"); + TvContentRating.createRating( + "com.android.tv", "US_TV", "US_TV_PG", "US_TV_L", "US_TV_S"); } diff --git a/tests/common/src/com/android/tv/testing/ChannelInfo.java b/tests/common/src/com/android/tv/testing/data/ChannelInfo.java index 946c0b55..e39c057d 100644 --- a/tests/common/src/com/android/tv/testing/ChannelInfo.java +++ b/tests/common/src/com/android/tv/testing/data/ChannelInfo.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.testing; +package com.android.tv.testing.data; import android.content.ContentResolver; import android.content.Context; @@ -23,14 +23,12 @@ import android.media.tv.TvContract; import android.net.Uri; import android.support.annotation.Nullable; import android.util.SparseArray; - import java.util.Objects; -/** - * Channel Information. - */ +/** Channel Information. */ public final class ChannelInfo { private static final SparseArray<String> VIDEO_HEIGHT_TO_FORMAT_MAP = new SparseArray<>(); + static { VIDEO_HEIGHT_TO_FORMAT_MAP.put(480, TvContract.Channels.VIDEO_FORMAT_480P); VIDEO_HEIGHT_TO_FORMAT_MAP.put(576, TvContract.Channels.VIDEO_FORMAT_576P); @@ -41,9 +39,9 @@ public final class ChannelInfo { } public static final String[] PROJECTION = { - TvContract.Channels.COLUMN_DISPLAY_NUMBER, - TvContract.Channels.COLUMN_DISPLAY_NAME, - TvContract.Channels.COLUMN_ORIGINAL_NETWORK_ID, + TvContract.Channels.COLUMN_DISPLAY_NUMBER, + TvContract.Channels.COLUMN_DISPLAY_NAME, + TvContract.Channels.COLUMN_ORIGINAL_NETWORK_ID, }; public final String number; @@ -67,14 +65,15 @@ public final class ChannelInfo { * Create a channel info for TVTestInput. * * @param context a context to insert logo. It can be null if logo isn't needed. - * @param channelNumber a channel number to be use as an identifier. - * {@link #originalNetworkId} will be assigned the same value, too. + * @param channelNumber a channel number to be use as an identifier. {@link #originalNetworkId} + * will be assigned the same value, too. */ public static ChannelInfo create(@Nullable Context context, int channelNumber) { - Builder builder = new Builder() - .setNumber(String.valueOf(channelNumber)) - .setName("Channel " + channelNumber) - .setOriginalNetworkId(channelNumber); + Builder builder = + new Builder() + .setNumber(String.valueOf(channelNumber)) + .setName("Channel " + channelNumber) + .setOriginalNetworkId(channelNumber); if (context != null) { // tests/input/tools/get_test_logos.sh only stores 1000 logos. builder.setLogoUrl(getUriStringForChannelLogo(context, channelNumber)); @@ -88,7 +87,9 @@ public final class ChannelInfo { .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) .authority(context.getPackageName()) .path("drawable") - .appendPath("ch_" + index + "_logo").build().toString(); + .appendPath("ch_" + index + "_logo") + .build() + .toString(); } public static ChannelInfo fromCursor(Cursor c) { @@ -109,10 +110,22 @@ public final class ChannelInfo { return builder.build(); } - private ChannelInfo(String number, String name, String logoUrl, int originalNetworkId, - int videoWidth, int videoHeight, float videoPixelAspectRatio, int audioChannel, - int audioLanguageCount, boolean hasClosedCaption, ProgramInfo program, - String appLinkText, int appLinkColor, String appLinkIconUri, String appLinkPosterArtUri, + private ChannelInfo( + String number, + String name, + String logoUrl, + int originalNetworkId, + int videoWidth, + int videoHeight, + float videoPixelAspectRatio, + int audioChannel, + int audioLanguageCount, + boolean hasClosedCaption, + ProgramInfo program, + String appLinkText, + int appLinkColor, + String appLinkIconUri, + String appLinkPosterArtUri, String appLinkIntentUri) { this.number = number; this.name = name; @@ -139,20 +152,35 @@ public final class ChannelInfo { @Override public String toString() { return "Channel{" - + "number=" + number - + ", name=" + name - + ", logoUri=" + logoUrl - + ", originalNetworkId=" + originalNetworkId - + ", videoWidth=" + videoWidth - + ", videoHeight=" + videoHeight - + ", audioChannel=" + audioChannel - + ", audioLanguageCount=" + audioLanguageCount - + ", hasClosedCaption=" + hasClosedCaption - + ", appLinkText=" + appLinkText - + ", appLinkColor=" + appLinkColor - + ", appLinkIconUri=" + appLinkIconUri - + ", appLinkPosterArtUri=" + appLinkPosterArtUri - + ", appLinkIntentUri=" + appLinkIntentUri + "}"; + + "number=" + + number + + ", name=" + + name + + ", logoUri=" + + logoUrl + + ", originalNetworkId=" + + originalNetworkId + + ", videoWidth=" + + videoWidth + + ", videoHeight=" + + videoHeight + + ", audioChannel=" + + audioChannel + + ", audioLanguageCount=" + + audioLanguageCount + + ", hasClosedCaption=" + + hasClosedCaption + + ", appLinkText=" + + appLinkText + + ", appLinkColor=" + + appLinkColor + + ", appLinkIconUri=" + + appLinkIconUri + + ", appLinkPosterArtUri=" + + appLinkPosterArtUri + + ", appLinkIntentUri=" + + appLinkIntentUri + + "}"; } @Override @@ -164,21 +192,21 @@ public final class ChannelInfo { return false; } ChannelInfo that = (ChannelInfo) o; - return Objects.equals(originalNetworkId, that.originalNetworkId) && - Objects.equals(videoWidth, that.videoWidth) && - Objects.equals(videoHeight, that.videoHeight) && - Objects.equals(audioChannel, that.audioChannel) && - Objects.equals(audioLanguageCount, that.audioLanguageCount) && - Objects.equals(hasClosedCaption, that.hasClosedCaption) && - Objects.equals(appLinkColor, that.appLinkColor) && - Objects.equals(number, that.number) && - Objects.equals(name, that.name) && - Objects.equals(logoUrl, that.logoUrl) && - Objects.equals(program, that.program) && - Objects.equals(appLinkText, that.appLinkText) && - Objects.equals(appLinkIconUri, that.appLinkIconUri) && - Objects.equals(appLinkPosterArtUri, that.appLinkPosterArtUri) && - Objects.equals(appLinkIntentUri, that.appLinkIntentUri); + return Objects.equals(originalNetworkId, that.originalNetworkId) + && Objects.equals(videoWidth, that.videoWidth) + && Objects.equals(videoHeight, that.videoHeight) + && Objects.equals(audioChannel, that.audioChannel) + && Objects.equals(audioLanguageCount, that.audioLanguageCount) + && Objects.equals(hasClosedCaption, that.hasClosedCaption) + && Objects.equals(appLinkColor, that.appLinkColor) + && Objects.equals(number, that.number) + && Objects.equals(name, that.name) + && Objects.equals(logoUrl, that.logoUrl) + && Objects.equals(program, that.program) + && Objects.equals(appLinkText, that.appLinkText) + && Objects.equals(appLinkIconUri, that.appLinkIconUri) + && Objects.equals(appLinkPosterArtUri, that.appLinkPosterArtUri) + && Objects.equals(appLinkIntentUri, that.appLinkIntentUri); } @Override @@ -186,17 +214,15 @@ public final class ChannelInfo { return Objects.hash(number, name, originalNetworkId); } - /** - * Builder class for {@code ChannelInfo}. - */ + /** Builder class for {@code ChannelInfo}. */ public static class Builder { private String mNumber; private String mName; private String mLogoUrl = null; private int mOriginalNetworkId; - private int mVideoWidth = 1920; // Width for HD video. - private int mVideoHeight = 1080; // Height for HD video. - private float mVideoPixelAspectRatio = 1.0f; //default value + private int mVideoWidth = 1920; // Width for HD video. + private int mVideoHeight = 1080; // Height for HD video. + private float mVideoPixelAspectRatio = 1.0f; // default value private int mAudioChannel; private int mAudioLanguageCount; private boolean mHasClosedCaption; @@ -207,8 +233,7 @@ public final class ChannelInfo { private String mAppLinkPosterArtUri; private String mAppLinkIntentUri; - public Builder() { - } + public Builder() {} public Builder(ChannelInfo other) { mNumber = other.number; @@ -305,11 +330,23 @@ public final class ChannelInfo { } public ChannelInfo build() { - return new ChannelInfo(mNumber, mName, mLogoUrl, mOriginalNetworkId, - mVideoWidth, mVideoHeight, mVideoPixelAspectRatio, mAudioChannel, - mAudioLanguageCount, mHasClosedCaption, mProgram, mAppLinkText, mAppLinkColor, - mAppLinkIconUri, mAppLinkPosterArtUri, mAppLinkIntentUri); - + return new ChannelInfo( + mNumber, + mName, + mLogoUrl, + mOriginalNetworkId, + mVideoWidth, + mVideoHeight, + mVideoPixelAspectRatio, + mAudioChannel, + mAudioLanguageCount, + mHasClosedCaption, + mProgram, + mAppLinkText, + mAppLinkColor, + mAppLinkIconUri, + mAppLinkPosterArtUri, + mAppLinkIntentUri); } } } diff --git a/tests/common/src/com/android/tv/testing/ChannelUtils.java b/tests/common/src/com/android/tv/testing/data/ChannelUtils.java index bfb766d6..920c7087 100644 --- a/tests/common/src/com/android/tv/testing/ChannelUtils.java +++ b/tests/common/src/com/android/tv/testing/data/ChannelUtils.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.tv.testing; +package com.android.tv.testing.data; import android.content.ContentResolver; import android.content.ContentValues; @@ -27,24 +27,22 @@ import android.support.annotation.WorkerThread; import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; - import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -/** - * Static helper methods for working with {@link android.media.tv.TvContract}. - */ +/** Static helper methods for working with {@link android.media.tv.TvContract}. */ public class ChannelUtils { private static final String TAG = "ChannelUtils"; private static final boolean DEBUG = false; /** - * Query and return the map of (channel_id, ChannelInfo). - * See: {@link ChannelInfo#fromCursor(Cursor)}. + * Query and return the map of (channel_id, ChannelInfo). See: {@link + * com.android.tv.testing.data.ChannelInfo#fromCursor(Cursor)}. */ @WorkerThread public static Map<Long, ChannelInfo> queryChannelInfoMapForTvInput( @@ -55,8 +53,8 @@ public class ChannelUtils { String[] projections = new String[ChannelInfo.PROJECTION.length + 1]; projections[0] = Channels._ID; System.arraycopy(ChannelInfo.PROJECTION, 0, projections, 1, ChannelInfo.PROJECTION.length); - try (Cursor cursor = context.getContentResolver() - .query(uri, projections, null, null, null)) { + try (Cursor cursor = + context.getContentResolver().query(uri, projections, null, null, null)) { if (cursor != null) { while (cursor.moveToNext()) { map.put(cursor.getLong(0), ChannelInfo.fromCursor(cursor)); @@ -113,10 +111,14 @@ public class ChannelUtils { Long rowId = existingChannelsMap.get(channel.originalNetworkId); Uri uri; if (rowId == null) { - if (DEBUG) Log.d(TAG, "Inserting "+ channel); + if (DEBUG) { + Log.d(TAG, "Inserting " + channel); + } uri = resolver.insert(TvContract.Channels.CONTENT_URI, values); } else { - if (DEBUG) Log.d(TAG, "Updating "+ channel); + if (DEBUG) { + Log.d(TAG, "Updating " + channel); + } uri = TvContract.buildChannelUri(rowId); resolver.update(uri, values, null, null); existingChannelsMap.remove(channel.originalNetworkId); @@ -149,6 +151,14 @@ public class ChannelUtils { // Prevent instantiation. } + public static List<ChannelInfo> createChannelInfos(Context context, int channelCount) { + List<ChannelInfo> channels = new ArrayList<>(); + for (int i = 1; i <= channelCount; i++) { + channels.add(ChannelInfo.create(context, i)); + } + return channels; + } + public static class InsertLogosTask extends AsyncTask<Map<Uri, String>, Void, Void> { private final Context mContext; diff --git a/tests/common/src/com/android/tv/testing/ProgramInfo.java b/tests/common/src/com/android/tv/testing/data/ProgramInfo.java index b1aaea6b..6d801425 100644 --- a/tests/common/src/com/android/tv/testing/ProgramInfo.java +++ b/tests/common/src/com/android/tv/testing/data/ProgramInfo.java @@ -14,80 +14,83 @@ * limitations under the License. */ -package com.android.tv.testing; +package com.android.tv.testing.data; import android.content.Context; import android.database.Cursor; import android.media.tv.TvContentRating; import android.media.tv.TvContract; - +import com.android.tv.testing.R; +import com.android.tv.testing.utils.Utils; +import java.util.Arrays; import java.util.Objects; import java.util.concurrent.TimeUnit; public final class ProgramInfo { - /** - * If this is specify for title, it will be generated by adding index. - */ + /** If this is specify for title, it will be generated by adding index. */ public static final String GEN_TITLE = ""; /** - * If this is specify for episode title, it will be generated by adding index. - * Also, season and episode numbers would be generated, too. - * see: {@link #build} for detail. + * If this is specify for episode title, it will be generated by adding index. Also, season and + * episode numbers would be generated, too. see: {@link #build} for detail. */ public static final String GEN_EPISODE = ""; + private static final int SEASON_MAX = 10; private static final int EPISODE_MAX = 12; /** - * If this is specify for poster art, - * it will be selected one of {@link #POSTER_ARTS_RES} in order. + * If this is specify for poster art, it will be selected one of {@link #POSTER_ARTS_RES} in + * order. */ public static final String GEN_POSTER = "GEN"; + private static final int[] POSTER_ARTS_RES = { - 0, - R.drawable.blue, - R.drawable.red_large, - R.drawable.green, - R.drawable.red, - R.drawable.green_large, - R.drawable.blue_small}; + 0, + R.drawable.blue, + R.drawable.red_large, + R.drawable.green, + R.drawable.red, + R.drawable.green_large, + R.drawable.blue_small + }; /** - * If this is specified for duration, - * it will be selected one of {@link #DURATIONS_MS} in order. + * If this is specified for duration, it will be selected one of {@link #DURATIONS_MS} in order. */ public static final int GEN_DURATION = -1; + private static final long[] DURATIONS_MS = { - TimeUnit.MINUTES.toMillis(15), - TimeUnit.MINUTES.toMillis(45), - TimeUnit.MINUTES.toMillis(90), - TimeUnit.MINUTES.toMillis(60), - TimeUnit.MINUTES.toMillis(30), - TimeUnit.MINUTES.toMillis(45), - TimeUnit.MINUTES.toMillis(60), - TimeUnit.MINUTES.toMillis(90), - TimeUnit.HOURS.toMillis(5)}; - private static long DURATIONS_SUM_MS; + TimeUnit.MINUTES.toMillis(15), + TimeUnit.MINUTES.toMillis(45), + TimeUnit.MINUTES.toMillis(90), + TimeUnit.MINUTES.toMillis(60), + TimeUnit.MINUTES.toMillis(30), + TimeUnit.MINUTES.toMillis(45), + TimeUnit.MINUTES.toMillis(60), + TimeUnit.MINUTES.toMillis(90), + TimeUnit.HOURS.toMillis(5) + }; + private static long durationsSumMs; + static { - DURATIONS_SUM_MS = 0; + durationsSumMs = 0; for (long duration : DURATIONS_MS) { - DURATIONS_SUM_MS += duration; + durationsSumMs += duration; } } - /** - * If this is specified for genre, - * it will be selected one of {@link #GENRES} in order. - */ + /** If this is specified for genre, it will be selected one of {@link #GENRES} in order. */ public static final String GEN_GENRE = "GEN"; + private static final String[] GENRES = { - "", - TvContract.Programs.Genres.SPORTS, - TvContract.Programs.Genres.NEWS, - TvContract.Programs.Genres.SHOPPING, - TvContract.Programs.Genres.DRAMA, - TvContract.Programs.Genres.ENTERTAINMENT}; + "", + TvContract.Programs.Genres.SPORTS, + TvContract.Programs.Genres.NEWS, + TvContract.Programs.Genres.SHOPPING, + TvContract.Programs.Genres.DRAMA, + TvContract.Programs.Genres.ENTERTAINMENT + }; public final String title; public final String episode; @@ -118,9 +121,17 @@ public final class ProgramInfo { return builder.build(); } - public ProgramInfo(String title, String episode, int seasonNumber, int episodeNumber, - String posterArtUri, String description, long durationMs, - TvContentRating[] contentRatings, String genre, String resourceUri) { + public ProgramInfo( + String title, + String episode, + int seasonNumber, + int episodeNumber, + String posterArtUri, + String description, + long durationMs, + TvContentRating[] contentRatings, + String genre, + String resourceUri) { this.title = title; this.episode = episode; this.seasonNumber = seasonNumber; @@ -141,8 +152,9 @@ public final class ProgramInfo { } /** - * Get index of the program whose start time equals or less than {@code timeMs} and - * end time more than {@code timeMs}. + * Get index of the program whose start time equals or less than {@code timeMs} and end time + * more than {@code timeMs}. + * * @param timeMs target time in millis to find a program. * @param channelId used to add complexity to the index between two consequence channels. */ @@ -151,8 +163,8 @@ public final class ProgramInfo { return Math.max((int) (timeMs / durationMs), 0); } long startTimeMs = channelId * DURATIONS_MS[((int) (channelId % DURATIONS_MS.length))]; - int index = (int) ((timeMs - startTimeMs) / DURATIONS_SUM_MS) * DURATIONS_MS.length; - startTimeMs += (index / DURATIONS_MS.length) * DURATIONS_SUM_MS; + int index = (int) ((timeMs - startTimeMs) / durationsSumMs) * DURATIONS_MS.length; + startTimeMs += (index / DURATIONS_MS.length) * durationsSumMs; while (startTimeMs + DURATIONS_MS[index % DURATIONS_MS.length] < timeMs) { startTimeMs += DURATIONS_MS[index % DURATIONS_MS.length]; index++; @@ -162,14 +174,16 @@ public final class ProgramInfo { /** * Returns the start time for the program with the position. + * * @param index index returned by {@link #getIndex} */ public long getStartTimeMs(int index, long channelId) { if (durationMs != GEN_DURATION) { return index * durationMs; } - long startTimeMs = channelId * DURATIONS_MS[((int) (channelId % DURATIONS_MS.length))] - + (index / DURATIONS_MS.length) * DURATIONS_SUM_MS; + long startTimeMs = + channelId * DURATIONS_MS[((int) (channelId % DURATIONS_MS.length))] + + (index / DURATIONS_MS.length) * durationsSumMs; for (int i = 0; i < index % DURATIONS_MS.length; i++) { startTimeMs += DURATIONS_MS[i]; } @@ -177,9 +191,9 @@ public final class ProgramInfo { } /** - * Return complete {@link ProgramInfo} with the generated value. - * See: {@link #GEN_TITLE}, {@link #GEN_EPISODE}, {@link #GEN_POSTER}, {@link #GEN_DURATION}, - * {@link #GEN_GENRE}. + * Return complete {@link ProgramInfo} with the generated value. See: {@link #GEN_TITLE}, {@link + * #GEN_EPISODE}, {@link #GEN_POSTER}, {@link #GEN_DURATION}, {@link #GEN_GENRE}. + * * @param index index returned by {@link #getIndex} */ public ProgramInfo build(Context context, int index) { @@ -196,8 +210,8 @@ public final class ProgramInfo { episode != null ? (index % SEASON_MAX + 1) : seasonNumber, episode != null ? (index % EPISODE_MAX + 1) : episodeNumber, GEN_POSTER.equals(posterArtUri) - ? Utils.getUriStringForResource(context, - POSTER_ARTS_RES[index % POSTER_ARTS_RES.length]) + ? Utils.getUriStringForResource( + context, POSTER_ARTS_RES[index % POSTER_ARTS_RES.length]) : posterArtUri, description, durationMs == GEN_DURATION ? DURATIONS_MS[index % DURATIONS_MS.length] : durationMs, @@ -208,9 +222,13 @@ public final class ProgramInfo { @Override public String toString() { - return "ProgramInfo{title=" + title - + ", episode=" + episode - + ", durationMs=" + durationMs + "}"; + return "ProgramInfo{title=" + + title + + ", episode=" + + episode + + ", durationMs=" + + durationMs + + "}"; } @Override @@ -222,16 +240,16 @@ public final class ProgramInfo { return false; } ProgramInfo that = (ProgramInfo) o; - return Objects.equals(seasonNumber, that.seasonNumber) && - Objects.equals(episodeNumber, that.episodeNumber) && - Objects.equals(durationMs, that.durationMs) && - Objects.equals(title, that.title) && - Objects.equals(episode, that.episode) && - Objects.equals(posterArtUri, that.posterArtUri) && - Objects.equals(description, that.description) && - Objects.equals(genre, that.genre) && - Objects.equals(contentRatings, that.contentRatings) && - Objects.equals(resourceUri, that.resourceUri); + return Objects.equals(seasonNumber, that.seasonNumber) + && Objects.equals(episodeNumber, that.episodeNumber) + && Objects.equals(durationMs, that.durationMs) + && Objects.equals(title, that.title) + && Objects.equals(episode, that.episode) + && Objects.equals(posterArtUri, that.posterArtUri) + && Objects.equals(description, that.description) + && Objects.equals(genre, that.genre) + && Arrays.equals(contentRatings, that.contentRatings) + && Objects.equals(resourceUri, that.resourceUri); } @Override @@ -302,8 +320,17 @@ public final class ProgramInfo { } public ProgramInfo build() { - return new ProgramInfo(mTitle, mEpisode, mSeasonNumber, mEpisodeNumber, mPosterArtUri, - mDescription, mDurationMs, mContentRatings, mGenre, mResourceUri); + return new ProgramInfo( + mTitle, + mEpisode, + mSeasonNumber, + mEpisodeNumber, + mPosterArtUri, + mDescription, + mDurationMs, + mContentRatings, + mGenre, + mResourceUri); } } } diff --git a/tests/common/src/com/android/tv/testing/ProgramUtils.java b/tests/common/src/com/android/tv/testing/data/ProgramUtils.java index 08c6a033..21647719 100644 --- a/tests/common/src/com/android/tv/testing/ProgramUtils.java +++ b/tests/common/src/com/android/tv/testing/data/ProgramUtils.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.testing; +package com.android.tv.testing.data; import android.content.ContentUris; import android.content.ContentValues; @@ -25,36 +25,57 @@ import android.media.tv.TvContract; import android.media.tv.TvContract.Programs; import android.net.Uri; import android.util.Log; - import com.android.tv.common.TvContentRatingCache; - +import com.android.tv.common.util.Clock; import java.util.ArrayList; +import java.util.Map; import java.util.concurrent.TimeUnit; -public class ProgramUtils { +/** Static utilities for using Programs in tests */ +public final class ProgramUtils { private static final String TAG = "ProgramUtils"; private static final boolean DEBUG = false; - // Populate program data for a week. - private static final long PROGRAM_INSERT_DURATION_MS = TimeUnit.DAYS.toMillis(7); + /** Populate program data for a week */ + public static final long PROGRAM_INSERT_DURATION_MS = TimeUnit.DAYS.toMillis(7); + private static final int MAX_DB_INSERT_COUNT_AT_ONCE = 500; /** - * Populate programs by repeating given program information. - * This method will populate programs without any gap nor overlapping - * starting from the current time. + * Populate programs by repeating given program information. This method will populate programs + * without any gap nor overlapping starting from the current time. */ - public static void populatePrograms(Context context, Uri channelUri, ProgramInfo program) { + public static void populatePrograms( + Context context, Uri channelUri, ProgramInfo program, Clock clock) { + populatePrograms(context, channelUri, program, clock, PROGRAM_INSERT_DURATION_MS); + } + + public static void populatePrograms( + Context context, + Uri channelUri, + ProgramInfo program, + Clock clock, + long programInsertDurationMs) { + long currentTimeMs = clock.currentTimeMillis(); + long targetEndTimeMs = currentTimeMs + programInsertDurationMs; + populatePrograms(context, channelUri, program, currentTimeMs, targetEndTimeMs); + } + + public static void populatePrograms( + Context context, + Uri channelUri, + ProgramInfo program, + long currentTimeMs, + long targetEndTimeMs) { ContentValues values = new ContentValues(); long channelId = ContentUris.parseId(channelUri); values.put(Programs.COLUMN_CHANNEL_ID, channelId); values.put(Programs.COLUMN_SHORT_DESCRIPTION, program.description); - values.put(Programs.COLUMN_CONTENT_RATING, + values.put( + Programs.COLUMN_CONTENT_RATING, TvContentRatingCache.contentRatingsToString(program.contentRatings)); - long currentTimeMs = System.currentTimeMillis(); - long targetEndTimeMs = currentTimeMs + PROGRAM_INSERT_DURATION_MS; long timeMs = getLastProgramEndTimeMs(context, channelUri, currentTimeMs, targetEndTimeMs); if (timeMs <= 0) { timeMs = currentTimeMs; @@ -81,11 +102,12 @@ public class ProgramUtils { list.add(new ContentValues(values)); timeMs += programAt.durationMs; - if (list.size() >= MAX_DB_INSERT_COUNT_AT_ONCE - || timeMs >= targetEndTimeMs) { + if (list.size() >= MAX_DB_INSERT_COUNT_AT_ONCE || timeMs >= targetEndTimeMs) { try { - context.getContentResolver().bulkInsert(Programs.CONTENT_URI, - list.toArray(new ContentValues[list.size()])); + context.getContentResolver() + .bulkInsert( + Programs.CONTENT_URI, + list.toArray(new ContentValues[list.size()])); } catch (SQLiteException e) { Log.e(TAG, "Can't insert EPG.", e); return; @@ -110,4 +132,16 @@ public class ProgramUtils { } private ProgramUtils() {} + + public static void updateProgramForAllChannelsOf( + Context context, String inputId, Clock clock, long durationMs) { + // Reload channels so we have the ids. + Map<Long, ChannelInfo> channelIdToInfoMap = + ChannelUtils.queryChannelInfoMapForTvInput(context, inputId); + for (Long channelId : channelIdToInfoMap.keySet()) { + ProgramInfo programInfo = ProgramInfo.create(); + populatePrograms( + context, TvContract.buildChannelUri(channelId), programInfo, clock, durationMs); + } + } } diff --git a/tests/common/src/com/android/tv/testing/dvr/DvrDataManagerInMemoryImpl.java b/tests/common/src/com/android/tv/testing/dvr/DvrDataManagerInMemoryImpl.java new file mode 100644 index 00000000..b8a055c7 --- /dev/null +++ b/tests/common/src/com/android/tv/testing/dvr/DvrDataManagerInMemoryImpl.java @@ -0,0 +1,327 @@ +/* + * 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.testing.dvr; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.util.Log; +import android.util.Range; +import com.android.tv.common.SoftPreconditions; +import com.android.tv.common.util.Clock; +import com.android.tv.dvr.BaseDvrDataManager; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording.RecordingState; +import com.android.tv.dvr.data.SeriesRecording; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +/** A DVR Data manager that stores values in memory suitable for testing. */ +public final class DvrDataManagerInMemoryImpl extends BaseDvrDataManager { + private static final String TAG = "DvrDataManagerInMemory"; + private final AtomicLong mNextId = new AtomicLong(1); + private final Map<Long, ScheduledRecording> mScheduledRecordings = new HashMap<>(); + private final Map<Long, RecordedProgram> mRecordedPrograms = new HashMap<>(); + private final Map<Long, SeriesRecording> mSeriesRecordings = new HashMap<>(); + + public DvrDataManagerInMemoryImpl(Context context, Clock clock) { + super(context, clock); + } + + @Override + public boolean isInitialized() { + return true; + } + + @Override + public boolean isDvrScheduleLoadFinished() { + return true; + } + + @Override + public boolean isRecordedProgramLoadFinished() { + return true; + } + + private List<ScheduledRecording> getScheduledRecordingsPrograms() { + return new ArrayList<>(mScheduledRecordings.values()); + } + + @Override + public List<RecordedProgram> getRecordedPrograms() { + return new ArrayList<>(mRecordedPrograms.values()); + } + + @Override + public List<ScheduledRecording> getAllScheduledRecordings() { + return new ArrayList<>(mScheduledRecordings.values()); + } + + @Override + public List<SeriesRecording> getSeriesRecordings() { + return new ArrayList<>(mSeriesRecordings.values()); + } + + @Override + public List<SeriesRecording> getSeriesRecordings(String inputId) { + List<SeriesRecording> result = new ArrayList<>(); + for (SeriesRecording r : mSeriesRecordings.values()) { + if (TextUtils.equals(r.getInputId(), inputId)) { + result.add(r); + } + } + return result; + } + + @Override + public long getNextScheduledStartTimeAfter(long startTime) { + + List<ScheduledRecording> temp = getNonStartedScheduledRecordings(); + Collections.sort(temp, ScheduledRecording.START_TIME_COMPARATOR); + for (ScheduledRecording r : temp) { + if (r.getStartTimeMs() > startTime) { + return r.getStartTimeMs(); + } + } + return DvrDataManager.NEXT_START_TIME_NOT_FOUND; + } + + @Override + public List<ScheduledRecording> getScheduledRecordings( + Range<Long> period, @RecordingState int state) { + List<ScheduledRecording> temp = getScheduledRecordingsPrograms(); + List<ScheduledRecording> result = new ArrayList<>(); + for (ScheduledRecording r : temp) { + if (r.isOverLapping(period) && r.getState() == state) { + result.add(r); + } + } + return result; + } + + @Override + public List<ScheduledRecording> getScheduledRecordings(long seriesRecordingId) { + List<ScheduledRecording> result = new ArrayList<>(); + for (ScheduledRecording r : mScheduledRecordings.values()) { + if (r.getSeriesRecordingId() == seriesRecordingId) { + result.add(r); + } + } + return result; + } + + @Override + public List<ScheduledRecording> getScheduledRecordings(String inputId) { + List<ScheduledRecording> result = new ArrayList<>(); + for (ScheduledRecording r : mScheduledRecordings.values()) { + if (TextUtils.equals(r.getInputId(), inputId)) { + result.add(r); + } + } + return result; + } + + /** Add a new scheduled recording. */ + @Override + public void addScheduledRecording(ScheduledRecording... scheduledRecordings) { + addScheduledRecording(false, scheduledRecordings); + } + + public void addScheduledRecording(boolean keepIds, ScheduledRecording... scheduledRecordings) { + for (ScheduledRecording r : scheduledRecordings) { + addScheduledRecordingInternal(r, keepIds); + } + } + + public void addRecordedProgram(RecordedProgram recordedProgram) { + addRecordedProgramInternal(recordedProgram, false); + } + + public void updateRecordedProgram(RecordedProgram r) { + long id = r.getId(); + if (mRecordedPrograms.containsKey(id)) { + mRecordedPrograms.put(id, r); + notifyRecordedProgramsChanged(r); + } else { + throw new IllegalArgumentException("Recording not found:" + r); + } + } + + public void removeRecordedProgram(RecordedProgram scheduledRecording) { + mRecordedPrograms.remove(scheduledRecording.getId()); + notifyRecordedProgramsRemoved(scheduledRecording); + } + + public ScheduledRecording addScheduledRecordingInternal(ScheduledRecording scheduledRecording) { + return addScheduledRecordingInternal(scheduledRecording, false); + } + + public ScheduledRecording addScheduledRecordingInternal( + ScheduledRecording scheduledRecording, boolean keepId) { + if (!keepId) { + SoftPreconditions.checkState( + scheduledRecording.getId() == ScheduledRecording.ID_NOT_SET, + TAG, + "expected id of " + + ScheduledRecording.ID_NOT_SET + + " but was " + + scheduledRecording); + scheduledRecording = + ScheduledRecording.buildFrom(scheduledRecording) + .setId(mNextId.incrementAndGet()) + .build(); + } + mScheduledRecordings.put(scheduledRecording.getId(), scheduledRecording); + notifyScheduledRecordingAdded(scheduledRecording); + return scheduledRecording; + } + + public RecordedProgram addRecordedProgramInternal( + RecordedProgram recordedProgram, boolean keepId) { + if (!keepId) { + SoftPreconditions.checkState( + recordedProgram.getId() == RecordedProgram.ID_NOT_SET, + TAG, + "expected id of " + RecordedProgram.ID_NOT_SET + " but was " + recordedProgram); + recordedProgram = + RecordedProgram + .buildFrom(recordedProgram) + .setId(mNextId.incrementAndGet()) + .build(); + } + mRecordedPrograms.put(recordedProgram.getId(), recordedProgram); + notifyRecordedProgramsAdded(recordedProgram); + return recordedProgram; + } + + @Override + public void addSeriesRecording(SeriesRecording... seriesRecordings) { + for (SeriesRecording r : seriesRecordings) { + mSeriesRecordings.put(r.getId(), r); + } + notifySeriesRecordingAdded(seriesRecordings); + } + + @Override + public void removeScheduledRecording(ScheduledRecording... scheduledRecordings) { + for (ScheduledRecording r : scheduledRecordings) { + mScheduledRecordings.remove(r.getId()); + } + notifyScheduledRecordingRemoved(scheduledRecordings); + } + + @Override + public void removeScheduledRecording(boolean forceRemove, ScheduledRecording... schedule) { + removeScheduledRecording(schedule); + } + + @Override + public void removeSeriesRecording(SeriesRecording... seriesRecordings) { + for (SeriesRecording r : seriesRecordings) { + mSeriesRecordings.remove(r.getId()); + } + notifySeriesRecordingRemoved(seriesRecordings); + } + + @Override + public void updateScheduledRecording(ScheduledRecording... scheduledRecordings) { + for (ScheduledRecording r : scheduledRecordings) { + long id = r.getId(); + if (mScheduledRecordings.containsKey(id)) { + mScheduledRecordings.put(id, r); + } else { + Log.d(TAG, "Recording not found:" + r); + } + } + notifyScheduledRecordingStatusChanged(scheduledRecordings); + } + + @Override + public void updateSeriesRecording(SeriesRecording... seriesRecordings) { + for (SeriesRecording r : seriesRecordings) { + long id = r.getId(); + if (mSeriesRecordings.containsKey(id)) { + mSeriesRecordings.put(id, r); + } else { + throw new IllegalArgumentException("Recording not found:" + r); + } + } + notifySeriesRecordingChanged(seriesRecordings); + } + + @Nullable + @Override + public ScheduledRecording getScheduledRecording(long id) { + return mScheduledRecordings.get(id); + } + + @Nullable + @Override + public ScheduledRecording getScheduledRecordingForProgramId(long programId) { + for (ScheduledRecording r : mScheduledRecordings.values()) { + if (r.getProgramId() == programId) { + return r; + } + } + return null; + } + + @Nullable + @Override + public SeriesRecording getSeriesRecording(long seriesRecordingId) { + return mSeriesRecordings.get(seriesRecordingId); + } + + @Nullable + @Override + public SeriesRecording getSeriesRecording(String seriesId) { + for (SeriesRecording r : mSeriesRecordings.values()) { + if (r.getSeriesId().equals(seriesId)) { + return r; + } + } + return null; + } + + @Nullable + @Override + public RecordedProgram getRecordedProgram(long recordingId) { + return mRecordedPrograms.get(recordingId); + } + + @Override + @NonNull + protected List<ScheduledRecording> getRecordingsWithState(int... states) { + ArrayList<ScheduledRecording> result = new ArrayList<>(); + for (ScheduledRecording r : mScheduledRecordings.values()) { + for (int state : states) { + if (r.getState() == state) { + result.add(r); + break; + } + } + } + return result; + } +} diff --git a/tests/common/src/com/android/tv/testing/dvr/RecordingTestUtils.java b/tests/common/src/com/android/tv/testing/dvr/RecordingTestUtils.java index a9bfa97a..72bac8fc 100644 --- a/tests/common/src/com/android/tv/testing/dvr/RecordingTestUtils.java +++ b/tests/common/src/com/android/tv/testing/dvr/RecordingTestUtils.java @@ -17,40 +17,37 @@ package com.android.tv.testing.dvr; import com.android.tv.dvr.data.ScheduledRecording; - import junit.framework.Assert; -/** - * Static utils for using {@link ScheduledRecording} in tests. - */ +/** Static utils for using {@link ScheduledRecording} in tests. */ public final class RecordingTestUtils { private static final String INPUT_ID = "input_id"; private static final int CHANNEL_ID = 273; - public static ScheduledRecording createTestRecordingWithIdAndPeriod(long id, String inputId, - long channelId, long startTime, long endTime) { + public static ScheduledRecording createTestRecordingWithIdAndPeriod( + long id, String inputId, long channelId, long startTime, long endTime) { return ScheduledRecording.builder(inputId, channelId, startTime, endTime) .setId(id) .setChannelId(channelId) .build(); } - public static ScheduledRecording createTestRecordingWithPeriod(String inputId, - long channelId, long startTime, long endTime) { - return createTestRecordingWithIdAndPeriod(ScheduledRecording.ID_NOT_SET, inputId, channelId, - startTime, endTime); + public static ScheduledRecording createTestRecordingWithPeriod( + String inputId, long channelId, long startTime, long endTime) { + return createTestRecordingWithIdAndPeriod( + ScheduledRecording.ID_NOT_SET, inputId, channelId, startTime, endTime); } - public static ScheduledRecording createTestRecordingWithPriorityAndPeriod(long channelId, - long priority, long startTime, long endTime) { + public static ScheduledRecording createTestRecordingWithPriorityAndPeriod( + long channelId, long priority, long startTime, long endTime) { return ScheduledRecording.builder(INPUT_ID, CHANNEL_ID, startTime, endTime) .setChannelId(channelId) .setPriority(priority) .build(); } - public static ScheduledRecording createTestRecordingWithIdAndPriorityAndPeriod(long id, - long channelId, long priority, long startTime, long endTime) { + public static ScheduledRecording createTestRecordingWithIdAndPriorityAndPeriod( + long id, long channelId, long priority, long startTime, long endTime) { return ScheduledRecording.builder(INPUT_ID, CHANNEL_ID, startTime, endTime) .setId(id) .setChannelId(channelId) @@ -58,11 +55,12 @@ public final class RecordingTestUtils { .build(); } - public static ScheduledRecording normalizePriority(ScheduledRecording orig){ + public static ScheduledRecording normalizePriority(ScheduledRecording orig) { return ScheduledRecording.buildFrom(orig).setPriority(orig.getId()).build(); } - public static void assertRecordingEquals(ScheduledRecording expected, ScheduledRecording actual) { + public static void assertRecordingEquals( + ScheduledRecording expected, ScheduledRecording actual) { Assert.assertEquals("id", expected.getId(), actual.getId()); Assert.assertEquals("channel", expected.getChannelId(), actual.getChannelId()); Assert.assertEquals("programId", expected.getProgramId(), actual.getProgramId()); @@ -70,9 +68,11 @@ public final class RecordingTestUtils { Assert.assertEquals("start time", expected.getStartTimeMs(), actual.getStartTimeMs()); Assert.assertEquals("end time", expected.getEndTimeMs(), actual.getEndTimeMs()); Assert.assertEquals("state", expected.getState(), actual.getState()); - Assert.assertEquals("parent series recording", expected.getSeriesRecordingId(), + Assert.assertEquals( + "parent series recording", + expected.getSeriesRecordingId(), actual.getSeriesRecordingId()); } - private RecordingTestUtils() { } + private RecordingTestUtils() {} } diff --git a/tests/common/src/com/android/tv/testing/robo/ContentProviders.java b/tests/common/src/com/android/tv/testing/robo/ContentProviders.java new file mode 100644 index 00000000..aaaa11df --- /dev/null +++ b/tests/common/src/com/android/tv/testing/robo/ContentProviders.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2018 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.testing.robo; + +import android.content.ContentProvider; +import android.content.pm.ProviderInfo; +import org.robolectric.Robolectric; +import org.robolectric.android.controller.ContentProviderController; +import org.robolectric.shadows.ShadowContentResolver; + +/** Static utilities for using content providers in tests. */ +public final class ContentProviders { + + /** Builds creates and register a ContentProvider with the given authority. */ + public static <T extends ContentProvider> T register(Class<T> providerClass, String authority) { + ProviderInfo info = new ProviderInfo(); + info.authority = authority; + ContentProviderController<T> contentProviderController = + Robolectric.buildContentProvider(providerClass); + T provider = contentProviderController.create(info).get(); + provider.onCreate(); + ShadowContentResolver.registerProviderInternal(authority, provider); + return provider; + } + + private ContentProviders() {} +} diff --git a/tests/common/src/com/android/tv/testing/robo/RobotTestAppHelper.java b/tests/common/src/com/android/tv/testing/robo/RobotTestAppHelper.java new file mode 100644 index 00000000..9eb79298 --- /dev/null +++ b/tests/common/src/com/android/tv/testing/robo/RobotTestAppHelper.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2018 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.testing.robo; + +import android.media.tv.TvContract; +import com.android.tv.testing.FakeTvProvider; +import com.android.tv.testing.TestSingletonApp; +import com.android.tv.testing.testdata.TestData; +import java.util.concurrent.TimeUnit; +import org.robolectric.Robolectric; + +/** Static utilities for using {@link TestSingletonApp} in roboletric tests. */ +public final class RobotTestAppHelper { + + public static void loadTestData(TestSingletonApp app, TestData testData) { + ContentProviders.register(FakeTvProvider.class, TvContract.AUTHORITY); + app.loadTestData(testData, TimeUnit.DAYS.toMillis(1)); + Robolectric.flushBackgroundThreadScheduler(); + Robolectric.flushForegroundThreadScheduler(); + } + + private RobotTestAppHelper() {} +} diff --git a/tests/common/src/com/android/tv/testing/shadows/ShadowMediaSession.java b/tests/common/src/com/android/tv/testing/shadows/ShadowMediaSession.java new file mode 100644 index 00000000..5a2c41e6 --- /dev/null +++ b/tests/common/src/com/android/tv/testing/shadows/ShadowMediaSession.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2017 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.testing.shadows; + +import android.app.PendingIntent; +import android.content.Context; +import android.media.MediaMetadata; +import android.media.session.MediaSession; +import android.media.session.PlaybackState; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; + +/** Shadow {@link MediaSession}. */ +@Implements(MediaSession.class) +public class ShadowMediaSession { + + public MediaSession.Callback mCallback; + public PendingIntent mMediaButtonReceiver; + public PendingIntent mSessionActivity; + public PlaybackState mPlaybackState; + public MediaMetadata mMediaMetadata; + public int mFlags; + public boolean mActive; + public boolean mReleased; + + /** Stand-in for the MediaSession constructor with the same parameters. */ + public void __constructor__(Context context, String tag, int userID) { + // This empty method prevents the real MediaSession constructor from being called. + } + + @Implementation + public void setCallback(MediaSession.Callback callback) { + mCallback = callback; + } + + @Implementation + public void setMediaButtonReceiver(PendingIntent mbr) { + mMediaButtonReceiver = mbr; + } + + @Implementation + public void setSessionActivity(PendingIntent activity) { + mSessionActivity = activity; + } + + @Implementation + public void setPlaybackState(PlaybackState state) { + mPlaybackState = state; + } + + @Implementation + public void setMetadata(MediaMetadata metadata) { + mMediaMetadata = metadata; + } + + @Implementation + public void setFlags(int flags) { + mFlags = flags; + } + + @Implementation + public boolean isActive() { + return mActive; + } + + @Implementation + public void setActive(boolean active) { + mActive = active; + } + + @Implementation + public void release() { + mReleased = true; + } +} diff --git a/tests/common/src/com/android/tv/testing/testdata/TestData.java b/tests/common/src/com/android/tv/testing/testdata/TestData.java new file mode 100644 index 00000000..e7e52348 --- /dev/null +++ b/tests/common/src/com/android/tv/testing/testdata/TestData.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2018 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.testing.testdata; + +import android.content.Context; +import android.media.tv.TvInputInfo; +import com.android.tv.common.util.Clock; +import com.android.tv.testing.data.ChannelInfo; +import com.android.tv.testing.data.ChannelUtils; +import com.android.tv.testing.data.ProgramUtils; +import com.android.tv.testing.utils.TestUtils; +import java.util.List; + +/** + * A set of test data. + * + * <p>contains: + * + * <ul> + * <li>InputID + * <li>Channel List + * </ul> + * + * Call {@link #init(Context)}, to update the TvProvider data base with the given values. + */ +public abstract class TestData { + private List<ChannelInfo> channelList; + + protected abstract List<ChannelInfo> createChannels(Context context); + + public void init(Context context, Clock clock, long durationMs) { + channelList = createChannels(context); + ChannelUtils.updateChannels(context, getInputId(), channelList); + ProgramUtils.updateProgramForAllChannelsOf(context, getInputId(), clock, durationMs); + } + + public abstract TvInputInfo getTvInputInfo(); + + public final String getInputId() { + return getTvInputInfo().getId(); + } + + public static final TestData DEFAULT_10_CHANNELS = + new TestData() { + private TvInputInfo mTvInputInfo = createTvInputInfo(); + + private TvInputInfo createTvInputInfo() { + try { + return TestUtils.createTvInputInfo( + TestUtils.createResolveInfo( + "com.android.tv.testing.testdata", + "com.android.tv.testing.testdata.Default10Channels"), + "com.android.tv.testing.testdata/.Default10Channels", + null, + TvInputInfo.TYPE_TUNER, + true); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + protected List<ChannelInfo> createChannels(Context context) { + return ChannelUtils.createChannelInfos(context, 10); + } + + @Override + public TvInputInfo getTvInputInfo() { + return mTvInputInfo; + } + }; +} diff --git a/tests/common/src/com/android/tv/testing/testinput/ChannelState.java b/tests/common/src/com/android/tv/testing/testinput/ChannelState.java index 1b8f63cd..3e8bab33 100644 --- a/tests/common/src/com/android/tv/testing/testinput/ChannelState.java +++ b/tests/common/src/com/android/tv/testing/testinput/ChannelState.java @@ -16,24 +16,16 @@ package com.android.tv.testing.testinput; import android.media.tv.TvTrackInfo; - -import com.android.tv.testing.Constants; - +import com.android.tv.testing.constants.Constants; import java.util.Collections; import java.util.List; -/** - * Versioned state information for a channel. - */ +/** Versioned state information for a channel. */ public class ChannelState { - /** - * The video track a channel has by default. - */ + /** The video track a channel has by default. */ public static final TvTrackInfo DEFAULT_VIDEO_TRACK = Constants.FHD1080P50_VIDEO_TRACK; - /** - * The video track a channel has by default. - */ + /** The video track a channel has by default. */ public static final TvTrackInfo DEFAULT_AUDIO_TRACK = Constants.EN_STEREO_AUDIO_TRACK; /** * The channel is "tuned" and video available. @@ -47,27 +39,23 @@ public class ChannelState { * Default ChannelState with version @{value #CHANNEL_VERSION_DEFAULT} and default {@link * ChannelStateData}. */ - public static final ChannelState DEFAULT = new ChannelState(CHANNEL_VERSION_DEFAULT, - new ChannelStateData()); + public static final ChannelState DEFAULT = + new ChannelState(CHANNEL_VERSION_DEFAULT, new ChannelStateData()); + private final int mVersion; private final ChannelStateData mData; - private ChannelState(int version, ChannelStateData channelStateData) { mVersion = version; mData = channelStateData; } - /** - * Returns the id of the selected audio track, or null if none is selected. - */ + /** Returns the id of the selected audio track, or null if none is selected. */ public String getSelectedAudioTrackId() { return mData.mSelectedAudioTrackId; } - /** - * Returns the id of the selected audio track, or null if none is selected. - */ + /** Returns the id of the selected audio track, or null if none is selected. */ public String getSelectedVideoTrackId() { return mData.mSelectedVideoTrackId; } @@ -82,9 +70,8 @@ public class ChannelState { } /** - * Tune status is either {@link #TUNE_STATUS_VIDEO_AVAILABLE} or a {@link - * android.media.tv.TvInputService.Session#notifyVideoUnavailable(int) video unavailable - * reason} + * Tune status is either {@link #TUNE_STATUS_VIDEO_AVAILABLE} or a {@link + * android.media.tv.TvInputService.Session#notifyVideoUnavailable(int) video unavailable reason} */ public int getTuneStatus() { return mData.mTuneStatus; diff --git a/tests/common/src/com/android/tv/testing/testinput/ChannelStateData.java b/tests/common/src/com/android/tv/testing/testinput/ChannelStateData.java index 9bac9d12..cdeb1f5c 100644 --- a/tests/common/src/com/android/tv/testing/testinput/ChannelStateData.java +++ b/tests/common/src/com/android/tv/testing/testinput/ChannelStateData.java @@ -19,25 +19,23 @@ package com.android.tv.testing.testinput; import android.media.tv.TvTrackInfo; import android.os.Parcel; import android.os.Parcelable; - import java.util.ArrayList; import java.util.List; -/** - * Mutable unversioned channel state. - */ +/** Mutable unversioned channel state. */ public final class ChannelStateData implements Parcelable { - public static final Creator<ChannelStateData> CREATOR = new Creator<ChannelStateData>() { - @Override - public ChannelStateData createFromParcel(Parcel in) { - return new ChannelStateData(in); - } + public static final Creator<ChannelStateData> CREATOR = + new Creator<ChannelStateData>() { + @Override + public ChannelStateData createFromParcel(Parcel in) { + return new ChannelStateData(in); + } - @Override - public ChannelStateData[] newArray(int size) { - return new ChannelStateData[size]; - } - }; + @Override + public ChannelStateData[] newArray(int size) { + return new ChannelStateData[size]; + } + }; public final List<TvTrackInfo> mTvTrackInfos = new ArrayList<>(); public int mTuneStatus = ChannelState.TUNE_STATUS_VIDEO_AVAILABLE; @@ -71,9 +69,6 @@ public final class ChannelStateData implements Parcelable { @Override public String toString() { - return "{" - + "tune=" + mTuneStatus - + ", tracks=" + mTvTrackInfos - + "}"; + return "{" + "tune=" + mTuneStatus + ", tracks=" + mTvTrackInfos + "}"; } } diff --git a/tests/common/src/com/android/tv/testing/testinput/TestInputControlConnection.java b/tests/common/src/com/android/tv/testing/testinput/TestInputControlConnection.java index 9b3f8835..071b1d3c 100644 --- a/tests/common/src/com/android/tv/testing/testinput/TestInputControlConnection.java +++ b/tests/common/src/com/android/tv/testing/testinput/TestInputControlConnection.java @@ -21,8 +21,7 @@ import android.os.IBinder; import android.os.RemoteException; import android.os.SystemClock; import android.util.Log; - -import com.android.tv.testing.ChannelInfo; +import com.android.tv.testing.data.ChannelInfo; /** * Connection for controlling the Test TV Input Service. @@ -47,9 +46,7 @@ public class TestInputControlConnection implements ServiceConnection { mControl = null; } - /** - * Is the service currently connected. - */ + /** Is the service currently connected. */ public boolean isBound() { return mControl != null; } @@ -58,7 +55,7 @@ public class TestInputControlConnection implements ServiceConnection { * Update the state of the channel. * * @param channel the channel to update. - * @param data the new state for the channel. + * @param data the new state for the channel. */ public void updateChannelState(ChannelInfo channel, ChannelStateData data) { waitUntilBound(); @@ -69,9 +66,7 @@ public class TestInputControlConnection implements ServiceConnection { } } - /** - * Sleep until {@link #isBound()} is true; - */ + /** Sleep until {@link #isBound()} is true; */ public void waitUntilBound() { while (!isBound()) { SystemClock.sleep(BOUND_CHECK_INTERVAL_MS); diff --git a/tests/common/src/com/android/tv/testing/testinput/TestInputControlUtils.java b/tests/common/src/com/android/tv/testing/testinput/TestInputControlUtils.java index 54aacf20..330afa9b 100644 --- a/tests/common/src/com/android/tv/testing/testinput/TestInputControlUtils.java +++ b/tests/common/src/com/android/tv/testing/testinput/TestInputControlUtils.java @@ -18,16 +18,16 @@ package com.android.tv.testing.testinput; import android.content.ComponentName; import android.content.Intent; -/** - * Static utils for {@link ITestInputControl}. - */ +/** Static utils for {@link ITestInputControl}. */ public final class TestInputControlUtils { public static Intent createIntent() { - return new Intent().setComponent(new ComponentName("com.android.tv.testinput", - "com.android.tv.testinput.TestInputControlService")); + return new Intent() + .setComponent( + new ComponentName( + "com.android.tv.testinput", + "com.android.tv.testinput.TestInputControlService")); } - private TestInputControlUtils() { - } + private TestInputControlUtils() {} } diff --git a/tests/common/src/com/android/tv/testing/testinput/TvTestInputConstants.java b/tests/common/src/com/android/tv/testing/testinput/TvTestInputConstants.java index 498addfd..27d3036c 100644 --- a/tests/common/src/com/android/tv/testing/testinput/TvTestInputConstants.java +++ b/tests/common/src/com/android/tv/testing/testinput/TvTestInputConstants.java @@ -15,25 +15,22 @@ */ package com.android.tv.testing.testinput; -import com.android.tv.testing.ChannelInfo; +import com.android.tv.testing.data.ChannelInfo; -/** - * Constants for interacting with TvTestInput. - */ +/** Constants for interacting with TvTestInput. */ public final class TvTestInputConstants { /** * Channel 1. * - * <p> By convention Channel 1 should not be changed. Test often start by tuning to this - * channel. + * <p>By convention Channel 1 should not be changed. Test often start by tuning to this channel. */ public static final ChannelInfo CH_1_DEFAULT_DONT_MODIFY = ChannelInfo.create(null, 1); /** * Channel 2. * - * <p> By convention the state of Channel 2 is changed by tests. Testcases should explicitly - * set the state of this channel before using it in tests. + * <p>By convention the state of Channel 2 is changed by tests. Testcases should explicitly set + * the state of this channel before using it in tests. */ public static final ChannelInfo CH_2 = ChannelInfo.create(null, 2); } diff --git a/tests/common/src/com/android/tv/testing/uihelper/BaseUiDeviceHelper.java b/tests/common/src/com/android/tv/testing/uihelper/BaseUiDeviceHelper.java index 3a2f5509..21b05d67 100644 --- a/tests/common/src/com/android/tv/testing/uihelper/BaseUiDeviceHelper.java +++ b/tests/common/src/com/android/tv/testing/uihelper/BaseUiDeviceHelper.java @@ -18,9 +18,7 @@ package com.android.tv.testing.uihelper; import android.content.res.Resources; import android.support.test.uiautomator.UiDevice; -/** - * Base class for building UiAutomator Helper classes. - */ +/** Base class for building UiAutomator Helper classes. */ public abstract class BaseUiDeviceHelper { protected final UiDevice mUiDevice; protected final Resources mTargetResources; diff --git a/tests/common/src/com/android/tv/testing/uihelper/ByResource.java b/tests/common/src/com/android/tv/testing/uihelper/ByResource.java index a76ee1d3..47b8d9f9 100644 --- a/tests/common/src/com/android/tv/testing/uihelper/ByResource.java +++ b/tests/common/src/com/android/tv/testing/uihelper/ByResource.java @@ -19,9 +19,7 @@ import android.content.res.Resources; import android.support.test.uiautomator.By; import android.support.test.uiautomator.BySelector; -/** - * Convenience methods for creating {@link BySelector}s using resource ids. - */ +/** Convenience methods for creating {@link BySelector}s using resource ids. */ public final class ByResource { /** @@ -44,6 +42,5 @@ public final class ByResource { return By.text(text); } - private ByResource() { - } + private ByResource() {} } diff --git a/tests/common/src/com/android/tv/testing/uihelper/Constants.java b/tests/common/src/com/android/tv/testing/uihelper/Constants.java index 8dd8e14a..4b522914 100644 --- a/tests/common/src/com/android/tv/testing/uihelper/Constants.java +++ b/tests/common/src/com/android/tv/testing/uihelper/Constants.java @@ -17,6 +17,7 @@ package com.android.tv.testing.uihelper; import android.support.test.uiautomator.By; import android.support.test.uiautomator.BySelector; +import com.android.tv.common.CommonConstants; public final class Constants { @@ -24,7 +25,7 @@ public final class Constants { public static final int MIN_EXTRA_TIMEOUT = 10; public static final long MAX_SHOW_DELAY_MILLIS = 200; public static final long MAX_FOCUSED_DELAY_MILLIS = 1000; - public static final String TV_APP_PACKAGE = "com.android.tv"; + public static final String TV_APP_PACKAGE = CommonConstants.BASE_PACKAGE; public static final BySelector TV_VIEW = By.res(TV_APP_PACKAGE, "main_tunable_tv_view"); public static final BySelector CHANNEL_BANNER = By.res(TV_APP_PACKAGE, "channel_banner_view"); public static final BySelector KEYPAD_CHANNEL_SWITCH = By.res(TV_APP_PACKAGE, "channel_number"); @@ -35,6 +36,5 @@ public final class Constants { public static final BySelector DVR_SCHEDULES = By.res(TV_APP_PACKAGE, "dvr_schedules"); public static final BySelector FOCUSED_VIEW = By.focused(true); - private Constants() { - } + private Constants() {} } diff --git a/tests/common/src/com/android/tv/testing/uihelper/DialogHelper.java b/tests/common/src/com/android/tv/testing/uihelper/DialogHelper.java index 9e4040a8..2ac4b648 100644 --- a/tests/common/src/com/android/tv/testing/uihelper/DialogHelper.java +++ b/tests/common/src/com/android/tv/testing/uihelper/DialogHelper.java @@ -24,12 +24,9 @@ import android.content.res.Resources; import android.support.test.uiautomator.BySelector; import android.support.test.uiautomator.UiDevice; import android.support.test.uiautomator.Until; - import com.android.tv.R; -/** - * Helper for testing {@link DialogFragment}s. - */ +/** Helper for testing {@link DialogFragment}s. */ public class DialogHelper extends BaseUiDeviceHelper { private final BySelector byPinDialog; @@ -39,7 +36,9 @@ public class DialogHelper extends BaseUiDeviceHelper { } public void assertWaitForPinDialogOpen() { - assertWaitForCondition(mUiDevice, Until.hasObject(byPinDialog), + assertWaitForCondition( + mUiDevice, + Until.hasObject(byPinDialog), Constants.MAX_SHOW_DELAY_MILLIS + mTargetResources.getInteger(R.integer.pin_dialog_anim_duration)); } diff --git a/tests/common/src/com/android/tv/testing/uihelper/LiveChannelsUiDeviceHelper.java b/tests/common/src/com/android/tv/testing/uihelper/LiveChannelsUiDeviceHelper.java index 1dc0f020..4b7c1f89 100644 --- a/tests/common/src/com/android/tv/testing/uihelper/LiveChannelsUiDeviceHelper.java +++ b/tests/common/src/com/android/tv/testing/uihelper/LiveChannelsUiDeviceHelper.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2017 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.testing.uihelper; import static com.android.tv.testing.uihelper.UiDeviceAsserts.waitForCondition; @@ -11,31 +26,28 @@ import android.support.test.uiautomator.BySelector; import android.support.test.uiautomator.UiDevice; import android.support.test.uiautomator.Until; import android.util.Log; - -import com.android.tv.testing.Utils; - +import com.android.tv.common.CommonConstants; +import com.android.tv.testing.utils.Utils; import junit.framework.Assert; -/** - * Helper for testing the Live TV Application. - */ +/** Helper for testing the Live TV Application. */ public class LiveChannelsUiDeviceHelper extends BaseUiDeviceHelper { private static final String TAG = "LiveChannelsUiDevice"; private static final int APPLICATION_START_TIMEOUT_MSEC = 5000; private final Context mContext; - public LiveChannelsUiDeviceHelper(UiDevice uiDevice, Resources targetResources, - Context context) { + public LiveChannelsUiDeviceHelper( + UiDevice uiDevice, Resources targetResources, Context context) { super(uiDevice, targetResources); mContext = context; } public void assertAppStarted() { assertTrue("TvActivity should be enabled.", Utils.isTvActivityEnabled(mContext)); - Intent intent = mContext.getPackageManager() - .getLaunchIntentForPackage(Constants.TV_APP_PACKAGE); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); // Clear out any previous instances + Intent intent = + mContext.getPackageManager().getLaunchIntentForPackage(Constants.TV_APP_PACKAGE); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); // Clear out any previous instances mContext.startActivity(intent); // Wait for idle state before checking the channel banner because waitForCondition() has // timeout. @@ -43,8 +55,10 @@ public class LiveChannelsUiDeviceHelper extends BaseUiDeviceHelper { // Make sure that the activity is resumed. waitForCondition(mUiDevice, Until.hasObject(Constants.TV_VIEW)); - Assert.assertTrue(Constants.TV_APP_PACKAGE + " did not start", mUiDevice - .wait(Until.hasObject(By.pkg(Constants.TV_APP_PACKAGE).depth(0)), + Assert.assertTrue( + Constants.TV_APP_PACKAGE + " did not start", + mUiDevice.wait( + Until.hasObject(By.pkg(Constants.TV_APP_PACKAGE).depth(0)), APPLICATION_START_TIMEOUT_MSEC)); BySelector welcome = ByResource.id(mTargetResources, com.android.tv.R.id.intro); if (mUiDevice.hasObject(welcome)) { @@ -54,9 +68,9 @@ public class LiveChannelsUiDeviceHelper extends BaseUiDeviceHelper { } public void assertAppStopped() { - while(mUiDevice.hasObject(By.pkg(Constants.TV_APP_PACKAGE).depth(0))) { + while (mUiDevice.hasObject(By.pkg(CommonConstants.BASE_PACKAGE).depth(0))) { mUiDevice.pressBack(); mUiDevice.waitForIdle(); } } -}
\ No newline at end of file +} diff --git a/tests/common/src/com/android/tv/testing/uihelper/MenuHelper.java b/tests/common/src/com/android/tv/testing/uihelper/MenuHelper.java index 80d53242..c8ea85ac 100644 --- a/tests/common/src/com/android/tv/testing/uihelper/MenuHelper.java +++ b/tests/common/src/com/android/tv/testing/uihelper/MenuHelper.java @@ -25,33 +25,30 @@ import android.support.test.uiautomator.Direction; import android.support.test.uiautomator.UiDevice; import android.support.test.uiautomator.UiObject2; import android.support.test.uiautomator.Until; - import com.android.tv.R; - import junit.framework.Assert; -/** - * Helper for testing {@link com.android.tv.menu.Menu}. - */ +/** Helper for testing {@link com.android.tv.menu.Menu}. */ public class MenuHelper extends BaseUiDeviceHelper { private final BySelector byChannels; public MenuHelper(UiDevice uiDevice, Resources targetResources) { super(uiDevice, targetResources); - byChannels = ByResource.id(mTargetResources, R.id.item_list) - .hasDescendant(ByResource.text(mTargetResources, R.string.menu_title_channels)); + byChannels = + ByResource.id(mTargetResources, R.id.item_list) + .hasDescendant( + ByResource.text(mTargetResources, R.string.menu_title_channels)); } public BySelector getByChannels() { return byChannels; } - /** - * Navigate to the menu item with the text {@code itemTextResId} in the row with text - * {@code rowTitleResId}. - * <p> - * Fails if the menu item can not be navigated to. + * Navigate to the menu item with the text {@code itemTextResId} in the row with text {@code + * rowTitleResId}. + * + * <p>Fails if the menu item can not be navigated to. * * @param rowTitleResId the resource id of the string in the desired row title. * @param itemTextResId the resource id of the string in the desired item. @@ -62,17 +59,20 @@ public class MenuHelper extends BaseUiDeviceHelper { BySelector byListView = ByResource.id(mTargetResources, R.id.list_view); UiObject2 listView = row.findObject(byListView); Assert.assertNotNull( - "Menu row '" + mTargetResources.getString(rowTitleResId) + "' does not have a " - + byListView, listView); + "Menu row '" + + mTargetResources.getString(rowTitleResId) + + "' does not have a " + + byListView, + listView); return assertNavigateToRowItem(listView, itemTextResId); } /** * Navigate to the menu row with the text title {@code rowTitleResId}. - * <p> - * Fails if the menu row can not be navigated to. - * We can't navigate to the Play controls row with this method, because the row doesn't have the - * title when it is selected. Use {@link #assertNavigateToPlayControlsRow} for the row instead. + * + * <p>Fails if the menu row can not be navigated to. We can't navigate to the Play controls row + * with this method, because the row doesn't have the title when it is selected. Use {@link + * #assertNavigateToPlayControlsRow} for the row instead. * * @param rowTitleResId the resource id of the string in the desired row title. * @return the row navigated to. @@ -82,14 +82,17 @@ public class MenuHelper extends BaseUiDeviceHelper { UiObject2 menu = mUiDevice.findObject(MENU); // TODO: handle play controls. They have a different dom structure and navigation sometimes // can get stuck on that row. - return UiDeviceAsserts.assertNavigateTo(mUiDevice, menu, - By.hasDescendant(ByResource.text(mTargetResources, rowTitleResId)), Direction.DOWN); + return UiDeviceAsserts.assertNavigateTo( + mUiDevice, + menu, + By.hasDescendant(ByResource.text(mTargetResources, rowTitleResId)), + Direction.DOWN); } /** * Navigate to the Play controls row. - * <p> - * Fails if the row can not be navigated to. + * + * <p>Fails if the row can not be navigated to. * * @see #assertNavigateToRow */ @@ -103,27 +106,28 @@ public class MenuHelper extends BaseUiDeviceHelper { /** * Navigate to the menu item in the given {@code row} with the text {@code itemTextResId} . - * <p> - * Fails if the menu item can not be navigated to. * - * @param row the container to look for menu items in. + * <p>Fails if the menu item can not be navigated to. + * + * @param row the container to look for menu items in. * @param itemTextResId the resource id of the string in the desired item. * @return the item navigated to. */ public UiObject2 assertNavigateToRowItem(UiObject2 row, int itemTextResId) { - return UiDeviceAsserts.assertNavigateTo(mUiDevice, row, + return UiDeviceAsserts.assertNavigateTo( + mUiDevice, + row, By.hasDescendant(ByResource.text(mTargetResources, itemTextResId)), Direction.RIGHT); } public UiObject2 assertPressOptionsSettings() { - return assertPressMenuItem(R.string.menu_title_options, - R.string.options_item_settings); + return assertPressMenuItem(R.string.menu_title_options, R.string.options_item_settings); } public UiObject2 assertPressOptionsClosedCaptions() { - return assertPressMenuItem(R.string.menu_title_options, - R.string.options_item_closed_caption); + return assertPressMenuItem( + R.string.menu_title_options, R.string.options_item_closed_caption); } public UiObject2 assertPressOptionsDisplayMode() { @@ -135,20 +139,19 @@ public class MenuHelper extends BaseUiDeviceHelper { } public UiObject2 assertPressProgramGuide() { - return assertPressMenuItem(R.string.menu_title_channels, - R.string.channels_item_program_guide); + return assertPressMenuItem( + R.string.menu_title_channels, R.string.channels_item_program_guide); } public UiObject2 assertPressDvrLibrary() { - return assertPressMenuItem(R.string.menu_title_channels, - R.string.channels_item_dvr); + return assertPressMenuItem(R.string.menu_title_channels, R.string.channels_item_dvr); } /** - * Navigate to the menu item with the text {@code itemTextResId} in the row with text - * {@code rowTitleResId}. - * <p> - * Fails if the menu item can not be navigated to. + * Navigate to the menu item with the text {@code itemTextResId} in the row with text {@code + * rowTitleResId}. + * + * <p>Fails if the menu item can not be navigated to. * * @param rowTitleResId the resource id of the string in the desired row title. * @param itemTextResId the resource id of the string in the desired item. @@ -161,17 +164,15 @@ public class MenuHelper extends BaseUiDeviceHelper { return item; } - /** - * Waits until the menu is visible. - */ + /** Waits until the menu is visible. */ public void assertWaitForMenu() { UiDeviceAsserts.assertWaitForCondition(mUiDevice, Until.hasObject(MENU)); } /** - * Show the menu. - * <p> - * Fails if the menu does not appear in {@link Constants#MAX_SHOW_DELAY_MILLIS}. + * Show the menu. + * + * <p>Fails if the menu does not appear in {@link Constants#MAX_SHOW_DELAY_MILLIS}. */ public void showMenu() { if (!mUiDevice.hasObject(MENU)) { diff --git a/tests/common/src/com/android/tv/testing/uihelper/SidePanelHelper.java b/tests/common/src/com/android/tv/testing/uihelper/SidePanelHelper.java index 98a19a41..ba015260 100644 --- a/tests/common/src/com/android/tv/testing/uihelper/SidePanelHelper.java +++ b/tests/common/src/com/android/tv/testing/uihelper/SidePanelHelper.java @@ -22,15 +22,11 @@ import android.support.test.uiautomator.BySelector; import android.support.test.uiautomator.Direction; import android.support.test.uiautomator.UiDevice; import android.support.test.uiautomator.UiObject2; - import com.android.tv.R; import com.android.tv.ui.sidepanel.SideFragment; - import junit.framework.Assert; -/** - * Helper for testing {@link SideFragment}s. - */ +/** Helper for testing {@link SideFragment}s. */ public class SidePanelHelper extends BaseUiDeviceHelper { public SidePanelHelper(UiDevice uiDevice, Resources targetResources) { @@ -54,6 +50,7 @@ public class SidePanelHelper extends BaseUiDeviceHelper { String title = mTargetResources.getString(resId); return assertNavigateToItem(title, direction); } + public UiObject2 assertNavigateToItem(String title) { return assertNavigateToItem(title, Direction.DOWN); } @@ -63,7 +60,7 @@ public class SidePanelHelper extends BaseUiDeviceHelper { UiObject2 sidePanelList = mUiDevice.findObject(sidePanelSelector); Assert.assertNotNull(sidePanelSelector + " not found", sidePanelList); - return UiDeviceAsserts.assertNavigateTo(mUiDevice, sidePanelList, - By.hasDescendant(By.text(title)), direction); + return UiDeviceAsserts.assertNavigateTo( + mUiDevice, sidePanelList, By.hasDescendant(By.text(title)), direction); } } diff --git a/tests/common/src/com/android/tv/testing/uihelper/UiDeviceAsserts.java b/tests/common/src/com/android/tv/testing/uihelper/UiDeviceAsserts.java index c096d7d2..28ea163e 100644 --- a/tests/common/src/com/android/tv/testing/uihelper/UiDeviceAsserts.java +++ b/tests/common/src/com/android/tv/testing/uihelper/UiDeviceAsserts.java @@ -27,12 +27,9 @@ import android.support.test.uiautomator.SearchCondition; import android.support.test.uiautomator.UiDevice; import android.support.test.uiautomator.UiObject2; import android.support.test.uiautomator.Until; - import junit.framework.Assert; -/** - * Asserts for {@link UiDevice}s. - */ +/** Asserts for {@link UiDevice}s. */ public final class UiDeviceAsserts { public static void assertHas(UiDevice uiDevice, BySelector bySelector, boolean expected) { @@ -46,25 +43,25 @@ public final class UiDeviceAsserts { } /** - * Assert that {@code searchCondition} becomes true within - * {@value Constants#MAX_SHOW_DELAY_MILLIS} milliseconds. + * Assert that {@code searchCondition} becomes true within {@value + * Constants#MAX_SHOW_DELAY_MILLIS} milliseconds. * - * @param uiDevice the device under test. + * @param uiDevice the device under test. * @param searchCondition the condition to wait for. */ - public static void assertWaitForCondition(UiDevice uiDevice, - SearchCondition<Boolean> searchCondition) { + public static void assertWaitForCondition( + UiDevice uiDevice, SearchCondition<Boolean> searchCondition) { assertWaitForCondition(uiDevice, searchCondition, Constants.MAX_SHOW_DELAY_MILLIS); } /** * Assert that {@code searchCondition} becomes true within {@code timeout} milliseconds. * - * @param uiDevice the device under test. + * @param uiDevice the device under test. * @param searchCondition the condition to wait for. */ - public static void assertWaitForCondition(UiDevice uiDevice, - SearchCondition<Boolean> searchCondition, long timeout) { + public static void assertWaitForCondition( + UiDevice uiDevice, SearchCondition<Boolean> searchCondition, long timeout) { boolean result = waitForCondition(uiDevice, searchCondition, timeout); assertTrue(searchCondition + " not true after " + timeout / 1000.0 + " seconds.", result); } @@ -72,52 +69,55 @@ public final class UiDeviceAsserts { /** * Wait until {@code searchCondition} becomes true. * - * @param uiDevice The device under test. + * @param uiDevice The device under test. * @param searchCondition The condition to wait for. * @return {@code true} if the condition is met, otherwise {@code false}. */ - public static boolean waitForCondition(UiDevice uiDevice, - SearchCondition<Boolean> searchCondition) { + public static boolean waitForCondition( + UiDevice uiDevice, SearchCondition<Boolean> searchCondition) { return waitForCondition(uiDevice, searchCondition, Constants.MAX_SHOW_DELAY_MILLIS); } - private static boolean waitForCondition(UiDevice uiDevice, - SearchCondition<Boolean> searchCondition, long timeout) { - long adjustedTimeout = timeout + Math.max(Constants.MIN_EXTRA_TIMEOUT, - (long) (timeout * Constants.EXTRA_TIMEOUT_PERCENT)); + private static boolean waitForCondition( + UiDevice uiDevice, SearchCondition<Boolean> searchCondition, long timeout) { + long adjustedTimeout = + timeout + + Math.max( + Constants.MIN_EXTRA_TIMEOUT, + (long) (timeout * Constants.EXTRA_TIMEOUT_PERCENT)); return uiDevice.wait(searchCondition, adjustedTimeout); } /** * Navigates through the focus items in a container returning the container child that has a * descendant matching the {@code selector}. - * <p> - * The navigation starts in the {@code direction} specified and - * {@link Direction#reverse(Direction) reverses} once if needed. Fails if there is not a - * focused - * descendant, or if after completing both directions no focused child has a descendant - * matching + * + * <p>The navigation starts in the {@code direction} specified and {@link + * Direction#reverse(Direction) reverses} once if needed. Fails if there is not a focused + * descendant, or if after completing both directions no focused child has a descendant matching * {@code selector}. - * <p> - * Fails if the menu item can not be navigated to. * - * @param uiDevice the device under test. + * <p>Fails if the menu item can not be navigated to. + * + * @param uiDevice the device under test. * @param container contains children to navigate over. - * @param selector the selector for the object to navigate to. + * @param selector the selector for the object to navigate to. * @param direction the direction to start navigating. * @return the object navigated to. */ - public static UiObject2 assertNavigateTo(UiDevice uiDevice, UiObject2 container, - BySelector selector, Direction direction) { + public static UiObject2 assertNavigateTo( + UiDevice uiDevice, UiObject2 container, BySelector selector, Direction direction) { int count = 0; while (count < 2) { BySelector hasFocusedDescendant = By.hasDescendant(FOCUSED_VIEW); UiObject2 focusedChild = null; - SearchCondition<Boolean> untilHasFocusedDescendant = Until - .hasObject(hasFocusedDescendant); + SearchCondition<Boolean> untilHasFocusedDescendant = + Until.hasObject(hasFocusedDescendant); - boolean result = container.wait(untilHasFocusedDescendant, - UiObject2Asserts.getAdjustedTimeout(Constants.MAX_SHOW_DELAY_MILLIS)); + boolean result = + container.wait( + untilHasFocusedDescendant, + UiObject2Asserts.getAdjustedTimeout(Constants.MAX_SHOW_DELAY_MILLIS)); if (!result) { // HACK: Try direction anyways because play control does not always have a // focused item. @@ -147,6 +147,5 @@ public final class UiDeviceAsserts { return null; } - private UiDeviceAsserts() { - } + private UiDeviceAsserts() {} } diff --git a/tests/common/src/com/android/tv/testing/uihelper/UiDeviceUtils.java b/tests/common/src/com/android/tv/testing/uihelper/UiDeviceUtils.java index 98eff906..d5545023 100644 --- a/tests/common/src/com/android/tv/testing/uihelper/UiDeviceUtils.java +++ b/tests/common/src/com/android/tv/testing/uihelper/UiDeviceUtils.java @@ -15,21 +15,11 @@ */ package com.android.tv.testing.uihelper; -import static junit.framework.Assert.assertTrue; - -import android.app.Instrumentation; -import android.app.UiAutomation; -import android.os.Build; -import android.os.SystemClock; -import android.support.test.uiautomator.Configurator; import android.support.test.uiautomator.Direction; import android.support.test.uiautomator.UiDevice; -import android.view.InputDevice; import android.view.KeyEvent; -/** - * Static utility methods for {@link UiDevice}. - */ +/** Static utility methods for {@link UiDevice}. */ public final class UiDeviceUtils { public static void pressDpad(UiDevice uiDevice, Direction direction) { @@ -51,7 +41,6 @@ public final class UiDeviceUtils { } } - public static void pressKeys(UiDevice uiDevice, int... keyCodes) { for (int k : keyCodes) { uiDevice.pressKeyCode(k); @@ -60,8 +49,8 @@ public final class UiDeviceUtils { /** * Parses the string and sends the corresponding individual key presses. - * <p> - * <b>Note:</b> only handles 0-9, '.', and '-'. + * + * <p><b>Note:</b> only handles 0-9, '.', and '-'. */ public static void pressKeys(UiDevice uiDevice, String keys) { for (char c : keys.toCharArray()) { @@ -77,59 +66,5 @@ public final class UiDeviceUtils { } } - /** - * Sends the DPAD Center key presses with the {@code repeat} count. - * TODO: Remove instrumentation argument once migrated to JUnit4. - */ - public static void pressDPadCenter(Instrumentation instrumentation, int repeat) { - pressKey(instrumentation, KeyEvent.KEYCODE_DPAD_CENTER, repeat); - } - - private static void pressKey(Instrumentation instrumentation, int keyCode, int repeat) { - UiDevice.getInstance(instrumentation).waitForIdle(); - for (int i = 0; i < repeat; ++i) { - assertPressKeyDown(instrumentation, keyCode, false); - if (i < repeat - 1) { - assertPressKeyUp(instrumentation, keyCode, false); - } - } - // Send last key event synchronously. - assertPressKeyUp(instrumentation, keyCode, true); - } - - private static void assertPressKeyDown(Instrumentation instrumentation, int keyCode, - boolean sync) { - assertPressKey(instrumentation, KeyEvent.ACTION_DOWN, keyCode, sync); - } - - private static void assertPressKeyUp(Instrumentation instrumentation, int keyCode, - boolean sync) { - assertPressKey(instrumentation, KeyEvent.ACTION_UP, keyCode, sync); - } - - private static void assertPressKey(Instrumentation instrumentation, int action, int keyCode, - boolean sync) { - long eventTime = SystemClock.uptimeMillis(); - KeyEvent event = new KeyEvent(eventTime, eventTime, action, keyCode, 0, 0, -1, 0, 0, - InputDevice.SOURCE_KEYBOARD); - assertTrue("Failed to inject key up event:" + event, - injectEvent(instrumentation, event, sync)); - } - - private static boolean injectEvent(Instrumentation instrumentation, KeyEvent event, - boolean sync) { - return getUiAutomation(instrumentation).injectInputEvent(event, sync); - } - - private static UiAutomation getUiAutomation(Instrumentation instrumentation) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - int flags = Configurator.getInstance().getUiAutomationFlags(); - return instrumentation.getUiAutomation(flags); - } else { - return instrumentation.getUiAutomation(); - } - } - - private UiDeviceUtils() { - } + private UiDeviceUtils() {} } diff --git a/tests/common/src/com/android/tv/testing/uihelper/UiObject2Asserts.java b/tests/common/src/com/android/tv/testing/uihelper/UiObject2Asserts.java index aba29f0e..ee02d7f7 100644 --- a/tests/common/src/com/android/tv/testing/uihelper/UiObject2Asserts.java +++ b/tests/common/src/com/android/tv/testing/uihelper/UiObject2Asserts.java @@ -20,41 +20,40 @@ import static junit.framework.Assert.assertTrue; import android.support.test.uiautomator.SearchCondition; import android.support.test.uiautomator.UiObject2; -/** - * Asserts for {@link UiObject2}s. - */ +/** Asserts for {@link UiObject2}s. */ public final class UiObject2Asserts { /** - * Assert that {@code searchCondition} becomes true within - * {@value Constants#MAX_SHOW_DELAY_MILLIS} milliseconds. + * Assert that {@code searchCondition} becomes true within {@value + * Constants#MAX_SHOW_DELAY_MILLIS} milliseconds. * - * @param uiObject the device under test. + * @param uiObject the device under test. * @param searchCondition the condition to wait for. */ - public static void assertWaitForCondition(UiObject2 uiObject, - SearchCondition<Boolean> searchCondition) { + public static void assertWaitForCondition( + UiObject2 uiObject, SearchCondition<Boolean> searchCondition) { assertWaitForCondition(uiObject, searchCondition, Constants.MAX_SHOW_DELAY_MILLIS); } /** * Assert that {@code searchCondition} becomes true within {@code timeout} milliseconds. * - * @param uiObject the device under test. + * @param uiObject the device under test. * @param searchCondition the condition to wait for. */ - public static void assertWaitForCondition(UiObject2 uiObject, - SearchCondition<Boolean> searchCondition, long timeout) { + public static void assertWaitForCondition( + UiObject2 uiObject, SearchCondition<Boolean> searchCondition, long timeout) { long adjustedTimeout = getAdjustedTimeout(timeout); boolean result = uiObject.wait(searchCondition, adjustedTimeout); assertTrue(searchCondition + " not true after " + timeout / 1000.0 + " seconds.", result); } public static long getAdjustedTimeout(long timeout) { - return timeout + Math.max( - Constants.MIN_EXTRA_TIMEOUT, (long) (timeout * Constants.EXTRA_TIMEOUT_PERCENT)); + return timeout + + Math.max( + Constants.MIN_EXTRA_TIMEOUT, + (long) (timeout * Constants.EXTRA_TIMEOUT_PERCENT)); } - private UiObject2Asserts() { - } + private UiObject2Asserts() {} } diff --git a/tests/common/src/com/android/tv/testing/uihelper/UiObject2Utils.java b/tests/common/src/com/android/tv/testing/uihelper/UiObject2Utils.java index 2a997a67..2f3779c5 100644 --- a/tests/common/src/com/android/tv/testing/uihelper/UiObject2Utils.java +++ b/tests/common/src/com/android/tv/testing/uihelper/UiObject2Utils.java @@ -19,9 +19,7 @@ import android.graphics.Point; import android.support.test.uiautomator.Direction; import android.support.test.uiautomator.UiObject2; -/** - * Static utility methods for {@link UiObject2}s. - */ +/** Static utility methods for {@link UiObject2}s. */ public class UiObject2Utils { public static boolean hasSiblingInDirection(UiObject2 theUiObject, Direction direction) { @@ -56,6 +54,5 @@ public class UiObject2Utils { return false; } - private UiObject2Utils() { - } + private UiObject2Utils() {} } diff --git a/tests/common/src/com/android/tv/testing/utils/TestUtils.java b/tests/common/src/com/android/tv/testing/utils/TestUtils.java new file mode 100644 index 00000000..6604c9ad --- /dev/null +++ b/tests/common/src/com/android/tv/testing/utils/TestUtils.java @@ -0,0 +1,193 @@ +/* + * 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.testing.utils; + +import android.content.pm.ApplicationInfo; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.graphics.drawable.Icon; +import android.hardware.hdmi.HdmiDeviceInfo; +import android.media.tv.TvInputInfo; +import android.os.Build; +import android.os.Bundle; +import java.lang.reflect.Constructor; + +/** A class that includes convenience methods for testing. */ +public class TestUtils { + /** Creates a {@link TvInputInfo}. */ + public static TvInputInfo createTvInputInfo( + ResolveInfo service, String id, String parentId, int type, boolean isHardwareInput) + throws Exception { + return createTvInputInfo(service, id, parentId, type, isHardwareInput, false, 0); + } + + /** + * Creates a {@link TvInputInfo}. + * + * <p>If this is called on MNC, {@code canRecord} and {@code tunerCount} are ignored. + */ + public static TvInputInfo createTvInputInfo( + ResolveInfo service, + String id, + String parentId, + int type, + boolean isHardwareInput, + boolean canRecord, + int tunerCount) + throws Exception { + // Create a mock TvInputInfo by using private constructor + // Note that mockito doesn't support mock/spy on final object. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + return createTvInputInfoForO( + service, id, parentId, type, isHardwareInput, canRecord, tunerCount); + + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return createTvInputInfoForNyc( + service, id, parentId, type, isHardwareInput, canRecord, tunerCount); + } + return createTvInputInfoForMnc(service, id, parentId, type, isHardwareInput); + } + + /** + * private TvInputInfo(ResolveInfo service, String id, int type, boolean isHardwareInput, + * CharSequence label, int labelResId, Icon icon, Icon iconStandby, Icon iconDisconnected, + * String setupActivity, boolean canRecord, int tunerCount, HdmiDeviceInfo hdmiDeviceInfo, + * boolean isConnectedToHdmiSwitch, String parentId, Bundle extras) { + */ + private static TvInputInfo createTvInputInfoForO( + ResolveInfo service, + String id, + String parentId, + int type, + boolean isHardwareInput, + boolean canRecord, + int tunerCount) + throws Exception { + Constructor<TvInputInfo> constructor = + TvInputInfo.class.getDeclaredConstructor( + ResolveInfo.class, + String.class, + int.class, + boolean.class, + CharSequence.class, + int.class, + Icon.class, + Icon.class, + Icon.class, + String.class, + boolean.class, + int.class, + HdmiDeviceInfo.class, + boolean.class, + String.class, + Bundle.class); + constructor.setAccessible(true); + return constructor.newInstance( + service, + id, + type, + isHardwareInput, + null, + 0, + null, + null, + null, + null, + canRecord, + tunerCount, + null, + false, + parentId, + null); + } + + /** + * private TvInputInfo(ResolveInfo service, String id, int type, boolean isHardwareInput, + * CharSequence label, int labelResId, Icon icon, Icon iconStandby, Icon iconDisconnected, + * String setupActivity, String settingsActivity, boolean canRecord, int tunerCount, + * HdmiDeviceInfo hdmiDeviceInfo, boolean isConnectedToHdmiSwitch, String parentId, Bundle + * extras) { + */ + private static TvInputInfo createTvInputInfoForNyc( + ResolveInfo service, + String id, + String parentId, + int type, + boolean isHardwareInput, + boolean canRecord, + int tunerCount) + throws Exception { + Constructor<TvInputInfo> constructor = + TvInputInfo.class.getDeclaredConstructor( + ResolveInfo.class, + String.class, + int.class, + boolean.class, + CharSequence.class, + int.class, + Icon.class, + Icon.class, + Icon.class, + String.class, + String.class, + boolean.class, + int.class, + HdmiDeviceInfo.class, + boolean.class, + String.class, + Bundle.class); + constructor.setAccessible(true); + return constructor.newInstance( + service, + id, + type, + isHardwareInput, + null, + 0, + null, + null, + null, + null, + null, + canRecord, + tunerCount, + null, + false, + parentId, + null); + } + + private static TvInputInfo createTvInputInfoForMnc( + ResolveInfo service, String id, String parentId, int type, boolean isHardwareInput) + throws Exception { + Constructor<TvInputInfo> constructor = + TvInputInfo.class.getDeclaredConstructor( + ResolveInfo.class, String.class, String.class, int.class, boolean.class); + constructor.setAccessible(true); + return constructor.newInstance(service, id, parentId, type, isHardwareInput); + } + + public static ResolveInfo createResolveInfo(String packageName, String name) { + ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.serviceInfo = new ServiceInfo(); + resolveInfo.serviceInfo.packageName = packageName; + resolveInfo.serviceInfo.name = name; + resolveInfo.serviceInfo.metaData = new Bundle(); + resolveInfo.serviceInfo.applicationInfo = new ApplicationInfo(); + return resolveInfo; + } +} diff --git a/tests/common/src/com/android/tv/testing/Utils.java b/tests/common/src/com/android/tv/testing/utils/Utils.java index b2b4036e..a116db0b 100644 --- a/tests/common/src/com/android/tv/testing/Utils.java +++ b/tests/common/src/com/android/tv/testing/utils/Utils.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.testing; +package com.android.tv.testing.utils; import android.content.ComponentName; import android.content.ContentResolver; @@ -26,9 +26,8 @@ import android.media.tv.TvInputInfo; import android.media.tv.TvInputManager; import android.net.Uri; import android.util.Log; - -import com.android.tv.common.TvCommonUtils; - +import com.android.tv.common.CommonConstants; +import com.android.tv.common.util.CommonUtils; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -40,12 +39,10 @@ import java.util.Random; /** * An utility class for testing. * - * <p>This class is also used to check whether TV app is running in tests or not. - * - * @see TvCommonUtils#isRunningInTest + * @see CommonUtils#isRunningInTest */ public final class Utils { - private static final String TAG ="Utils"; + private static final String TAG = "Utils"; private static final long DEFAULT_RANDOM_SEED = getSeed(); @@ -55,10 +52,12 @@ public final class Utils { } Resources res = context.getResources(); return new Uri.Builder() - .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) - .authority(res.getResourcePackageName(resId)) - .path(res.getResourceTypeName(resId)) - .appendPath(res.getResourceEntryName(resId)).build().toString(); + .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + .authority(res.getResourcePackageName(resId)) + .path(res.getResourceTypeName(resId)) + .appendPath(res.getResourceEntryName(resId)) + .build() + .toString(); } public static void copy(InputStream is, OutputStream os) throws IOException { @@ -91,8 +90,8 @@ public final class Utils { } /** - * Return the Random class which is needed to make random data for testing. - * Default seed of the random is today's date. + * Return the Random class which is needed to make random data for testing. Default seed of the + * random is today's date. */ public static Random createTestRandom() { return new Random(DEFAULT_RANDOM_SEED); @@ -106,17 +105,15 @@ public final class Utils { return Long.valueOf(today); } - private Utils() {} - - /** - * Checks whether TvActivity is enabled or not. - */ + /** Checks whether TvActivity is enabled or not. */ public static boolean isTvActivityEnabled(Context context) { PackageManager pm = context.getPackageManager(); - ComponentName name = new ComponentName("com.android.tv", - "com.android.tv.TvActivity"); + ComponentName name = + new ComponentName(CommonConstants.BASE_PACKAGE, "com.android.tv.TvActivity"); int enabled = pm.getComponentEnabledSetting(name); return enabled == PackageManager.COMPONENT_ENABLED_STATE_ENABLED || enabled == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT; } + + private Utils() {} } |