1/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package androidx.tvprovider.media.tv;
18
19import static org.junit.Assert.assertEquals;
20import static org.junit.Assert.assertNotNull;
21import static org.junit.Assert.assertTrue;
22
23import android.content.ContentResolver;
24import android.content.ContentValues;
25import android.content.Intent;
26import android.database.Cursor;
27import android.database.MatrixCursor;
28import android.media.tv.TvContentRating;
29import android.net.Uri;
30import android.support.test.InstrumentationRegistry;
31import android.support.test.filters.SdkSuppress;
32import android.support.test.filters.SmallTest;
33import android.support.test.runner.AndroidJUnit4;
34
35import androidx.tvprovider.media.tv.TvContractCompat.WatchNextPrograms;
36
37import org.junit.Before;
38import org.junit.Test;
39import org.junit.runner.RunWith;
40
41import java.util.Arrays;
42import java.util.Date;
43import java.util.Objects;
44
45/**
46 * Tests that watch next programs can be created using the Builder pattern and correctly obtain
47 * values from them.
48 */
49@SmallTest
50@SdkSuppress(minSdkVersion = 26)
51@RunWith(AndroidJUnit4.class)
52public class WatchNextProgramTest {
53
54    @Before
55    public void tearDown() {
56        if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) {
57            return;
58        }
59        ContentResolver resolver = InstrumentationRegistry.getContext().getContentResolver();
60        resolver.delete(WatchNextPrograms.CONTENT_URI, null, null);
61    }
62
63    @Test
64    public void testEmptyPreviewProgram() {
65        WatchNextProgram emptyProgram = new WatchNextProgram.Builder().build();
66        ContentValues contentValues = emptyProgram.toContentValues(true);
67        compareProgram(emptyProgram,
68                WatchNextProgram.fromCursor(getProgramCursor(Program.PROJECTION, contentValues)),
69                true);
70    }
71
72    @Test
73    public void testSampleProgram() {
74        WatchNextProgram sampleProgram = new WatchNextProgram.Builder()
75                .setTitle("Program Title")
76                .setDescription("This is a sample program")
77                .setEpisodeNumber(5)
78                .setSeasonNumber("The Final Season", 7)
79                .setThumbnailUri(Uri.parse("http://www.example.com/programs/poster.png"))
80                .build();
81        ContentValues contentValues = sampleProgram.toContentValues(true);
82        compareProgram(sampleProgram,
83                WatchNextProgram.fromCursor(
84                        getProgramCursor(WatchNextProgram.PROJECTION, contentValues)), true);
85
86        WatchNextProgram clonedSampleProgram = new WatchNextProgram.Builder(sampleProgram).build();
87        compareProgram(sampleProgram, clonedSampleProgram, true);
88    }
89
90    @Test
91    public void testFullyPopulatedProgram() {
92        WatchNextProgram fullyPopulatedProgram = createFullyPopulatedWatchNextProgram();
93        ContentValues contentValues = fullyPopulatedProgram.toContentValues(true);
94        compareProgram(fullyPopulatedProgram,
95                WatchNextProgram.fromCursor(
96                        getProgramCursor(WatchNextProgram.PROJECTION, contentValues)), true);
97
98        WatchNextProgram clonedFullyPopulatedProgram =
99                new WatchNextProgram.Builder(fullyPopulatedProgram).build();
100        compareProgram(fullyPopulatedProgram, clonedFullyPopulatedProgram, true);
101    }
102
103    @Test
104    public void testChannelWithSystemContentProvider() {
105        if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) {
106            return;
107        }
108        WatchNextProgram fullyPopulatedProgram = createFullyPopulatedWatchNextProgram();
109        ContentResolver resolver = InstrumentationRegistry.getContext().getContentResolver();
110        Uri watchNextProgramUri = resolver.insert(WatchNextPrograms.CONTENT_URI,
111                fullyPopulatedProgram.toContentValues());
112
113        WatchNextProgram programFromSystemDb =
114                loadWatchNextProgramFromContentProvider(resolver, watchNextProgramUri);
115        compareProgram(fullyPopulatedProgram, programFromSystemDb, false);
116    }
117
118    @Test
119    public void testWatchNextProgramUpdateWithContentProvider() {
120        if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) {
121            return;
122        }
123
124        WatchNextProgram fullyPopulatedProgram = createFullyPopulatedWatchNextProgram();
125        ContentResolver resolver = InstrumentationRegistry.getContext().getContentResolver();
126        Uri watchNextProgramUri = resolver.insert(WatchNextPrograms.CONTENT_URI,
127                fullyPopulatedProgram.toContentValues());
128
129        WatchNextProgram programFromSystemDb =
130                loadWatchNextProgramFromContentProvider(resolver, watchNextProgramUri);
131        compareProgram(fullyPopulatedProgram, programFromSystemDb, false);
132
133        // Update a field from a fully loaded watch-next program.
134        WatchNextProgram updatedProgram = new WatchNextProgram.Builder(programFromSystemDb)
135                .setInteractionCount(programFromSystemDb.getInteractionCount() + 1).build();
136        assertEquals(1, resolver.update(
137                watchNextProgramUri, updatedProgram.toContentValues(), null, null));
138        programFromSystemDb =
139                loadWatchNextProgramFromContentProvider(resolver, watchNextProgramUri);
140        compareProgram(updatedProgram, programFromSystemDb, false);
141
142        // Update a field with null from a fully loaded watch-next program.
143        updatedProgram = new WatchNextProgram.Builder(updatedProgram)
144                .setPreviewVideoUri(null).build();
145        assertEquals(1, resolver.update(
146                watchNextProgramUri, updatedProgram.toContentValues(), null, null));
147        programFromSystemDb = loadWatchNextProgramFromContentProvider(
148                resolver, watchNextProgramUri);
149        compareProgram(updatedProgram, programFromSystemDb, false);
150
151        // Update a field without referencing fully watch-next program.
152        ContentValues values = new PreviewProgram.Builder().setInteractionCount(1).build()
153                .toContentValues();
154        assertEquals(1, values.size());
155        assertEquals(1, resolver.update(watchNextProgramUri, values, null, null));
156        programFromSystemDb = loadWatchNextProgramFromContentProvider(
157                resolver, watchNextProgramUri);
158        WatchNextProgram expectedProgram = new WatchNextProgram.Builder(programFromSystemDb)
159                .setInteractionCount(1).build();
160        compareProgram(expectedProgram, programFromSystemDb, false);
161    }
162
163    @Test
164    public void testWatchNextProgramEquals() {
165        assertEquals(createFullyPopulatedWatchNextProgram(),
166                createFullyPopulatedWatchNextProgram());
167    }
168
169    private static WatchNextProgram loadWatchNextProgramFromContentProvider(
170            ContentResolver resolver, Uri watchNextProgramUri) {
171        try (Cursor cursor = resolver.query(watchNextProgramUri, null, null, null, null)) {
172            assertNotNull(cursor);
173            assertEquals(1, cursor.getCount());
174            cursor.moveToNext();
175            return WatchNextProgram.fromCursor(cursor);
176        }
177    }
178
179    @Test
180    public void testWatchNextProgramWithPartialData() {
181        WatchNextProgram previewProgram = new WatchNextProgram.Builder()
182                .setInternalProviderId("ID-4321")
183                .setPreviewVideoUri(Uri.parse("http://example.com/preview-video.mpg"))
184                .setLastPlaybackPositionMillis(0)
185                .setDurationMillis(60 * 1000)
186                .setIntentUri(Uri.parse(new Intent(Intent.ACTION_VIEW).toUri(
187                        Intent.URI_INTENT_SCHEME)))
188                .setTransient(false)
189                .setType(WatchNextPrograms.TYPE_TV_EPISODE)
190                .setPosterArtAspectRatio(WatchNextPrograms.ASPECT_RATIO_3_2)
191                .setThumbnailAspectRatio(WatchNextPrograms.ASPECT_RATIO_16_9)
192                .setLogoUri(Uri.parse("http://example.com/program-logo.mpg"))
193                .setAvailability(WatchNextPrograms.AVAILABILITY_FREE_WITH_SUBSCRIPTION)
194                .setStartingPrice("9.99 USD")
195                .setOfferPrice("3.99 USD")
196                .setReleaseDate(new Date(97, 2, 8))
197                .setLive(false)
198                .setInteractionType(WatchNextPrograms.INTERACTION_TYPE_VIEWS)
199                .setInteractionCount(99200)
200                .setAuthor("author_name")
201                .setReviewRatingStyle(WatchNextPrograms.REVIEW_RATING_STYLE_PERCENTAGE)
202                .setReviewRating("83.9")
203                .setId(10)
204                .setTitle("Recommended Video 1")
205                .setDescription("You should watch this!")
206                .setPosterArtUri(Uri.parse("http://example.com/poster.png"))
207                .setInternalProviderFlag2(0x0010010084108410L)
208                .build();
209
210        String[] partialProjection = {
211                WatchNextPrograms._ID,
212                WatchNextPrograms.COLUMN_TITLE,
213                WatchNextPrograms.COLUMN_SHORT_DESCRIPTION,
214                WatchNextPrograms.COLUMN_POSTER_ART_URI,
215                WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2,
216                WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_ID,
217                WatchNextPrograms.COLUMN_PREVIEW_VIDEO_URI,
218                WatchNextPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS,
219                WatchNextPrograms.COLUMN_DURATION_MILLIS,
220                WatchNextPrograms.COLUMN_INTENT_URI,
221                WatchNextPrograms.COLUMN_TRANSIENT,
222                WatchNextPrograms.COLUMN_TYPE,
223                WatchNextPrograms.COLUMN_POSTER_ART_ASPECT_RATIO,
224                WatchNextPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO,
225                WatchNextPrograms.COLUMN_LOGO_URI,
226                WatchNextPrograms.COLUMN_AVAILABILITY,
227                WatchNextPrograms.COLUMN_STARTING_PRICE,
228                WatchNextPrograms.COLUMN_OFFER_PRICE,
229                WatchNextPrograms.COLUMN_RELEASE_DATE,
230                WatchNextPrograms.COLUMN_ITEM_COUNT,
231                WatchNextPrograms.COLUMN_LIVE,
232                WatchNextPrograms.COLUMN_INTERACTION_TYPE,
233                WatchNextPrograms.COLUMN_INTERACTION_COUNT,
234                WatchNextPrograms.COLUMN_AUTHOR,
235                WatchNextPrograms.COLUMN_REVIEW_RATING_STYLE,
236                WatchNextPrograms.COLUMN_REVIEW_RATING,
237        };
238
239        ContentValues contentValues = previewProgram.toContentValues(true);
240        compareProgram(previewProgram,
241                WatchNextProgram.fromCursor(getProgramCursor(partialProjection, contentValues)),
242                true);
243
244        WatchNextProgram clonedFullyPopulatedProgram =
245                new WatchNextProgram.Builder(previewProgram).build();
246        compareProgram(previewProgram, clonedFullyPopulatedProgram, true);
247    }
248
249    private static WatchNextProgram createFullyPopulatedWatchNextProgram() {
250        return new WatchNextProgram.Builder()
251                .setTitle("Google")
252                .setInternalProviderId("ID-4321")
253                .setPreviewVideoUri(Uri.parse("http://example.com/preview-video.mpg"))
254                .setLastPlaybackPositionMillis(0)
255                .setDurationMillis(60 * 1000)
256                .setIntentUri(Uri.parse(new Intent(Intent.ACTION_VIEW).toUri(
257                        Intent.URI_INTENT_SCHEME)))
258                .setTransient(false)
259                .setType(WatchNextPrograms.TYPE_MOVIE)
260                .setWatchNextType(WatchNextPrograms.WATCH_NEXT_TYPE_CONTINUE)
261                .setPosterArtAspectRatio(WatchNextPrograms.ASPECT_RATIO_2_3)
262                .setThumbnailAspectRatio(WatchNextPrograms.ASPECT_RATIO_16_9)
263                .setLogoUri(Uri.parse("http://example.com/program-logo.mpg"))
264                .setAvailability(WatchNextPrograms.AVAILABILITY_AVAILABLE)
265                .setStartingPrice("12.99 USD")
266                .setOfferPrice("4.99 USD")
267                .setReleaseDate("1997")
268                .setItemCount(3)
269                .setLive(false)
270                .setInteractionType(WatchNextPrograms.INTERACTION_TYPE_LIKES)
271                .setInteractionCount(10200)
272                .setAuthor("author_name")
273                .setReviewRatingStyle(WatchNextPrograms.REVIEW_RATING_STYLE_STARS)
274                .setReviewRating("4.5")
275                .setSearchable(false)
276                .setThumbnailUri(Uri.parse("http://example.com/thumbnail.png"))
277                .setAudioLanguages(new String [] {"eng", "kor"})
278                .setCanonicalGenres(new String[] {TvContractCompat.Programs.Genres.MOVIES})
279                .setContentRatings(new TvContentRating[] {
280                        TvContentRating.createRating("com.android.tv", "US_TV", "US_TV_Y7")})
281                .setDescription("This is a sample program")
282                .setEpisodeNumber("Pilot", 0)
283                .setEpisodeTitle("Hello World")
284                .setLongDescription("This is a longer description than the previous description")
285                .setPosterArtUri(Uri.parse("http://example.com/poster.png"))
286                .setSeasonNumber("The Final Season", 7)
287                .setSeasonTitle("The Final Season")
288                .setVideoHeight(1080)
289                .setVideoWidth(1920)
290                .setInternalProviderFlag1(0x4)
291                .setInternalProviderFlag2(0x3)
292                .setInternalProviderFlag3(0x2)
293                .setInternalProviderFlag4(0x1)
294                .setBrowsable(true)
295                .setContentId("CID-8442")
296                .build();
297    }
298
299    private static void compareProgram(WatchNextProgram programA, WatchNextProgram programB,
300            boolean includeIdAndProtectedFields) {
301        assertTrue(Arrays.equals(programA.getAudioLanguages(), programB.getAudioLanguages()));
302        assertTrue(Arrays.deepEquals(programA.getCanonicalGenres(), programB.getCanonicalGenres()));
303        assertTrue(Arrays.deepEquals(programA.getContentRatings(), programB.getContentRatings()));
304        assertEquals(programA.getDescription(), programB.getDescription());
305        assertEquals(programA.getEpisodeNumber(), programB.getEpisodeNumber());
306        assertEquals(programA.getEpisodeTitle(), programB.getEpisodeTitle());
307        assertEquals(programA.getLongDescription(), programB.getLongDescription());
308        assertEquals(programA.getPosterArtUri(), programB.getPosterArtUri());
309        assertEquals(programA.getSeasonNumber(), programB.getSeasonNumber());
310        assertEquals(programA.getThumbnailUri(), programB.getThumbnailUri());
311        assertEquals(programA.getTitle(), programB.getTitle());
312        assertEquals(programA.getVideoHeight(), programB.getVideoHeight());
313        assertEquals(programA.getVideoWidth(), programB.getVideoWidth());
314        assertEquals(programA.isSearchable(), programB.isSearchable());
315        assertEquals(programA.getInternalProviderFlag1(), programB.getInternalProviderFlag1());
316        assertEquals(programA.getInternalProviderFlag2(), programB.getInternalProviderFlag2());
317        assertEquals(programA.getInternalProviderFlag3(), programB.getInternalProviderFlag3());
318        assertEquals(programA.getInternalProviderFlag4(), programB.getInternalProviderFlag4());
319        assertTrue(Objects.equals(programA.getSeasonTitle(), programB.getSeasonTitle()));
320        assertEquals(programA.getInternalProviderId(), programB.getInternalProviderId());
321        assertEquals(programA.getPreviewVideoUri(), programB.getPreviewVideoUri());
322        assertEquals(programA.getLastPlaybackPositionMillis(),
323                programB.getLastPlaybackPositionMillis());
324        assertEquals(programA.getDurationMillis(), programB.getDurationMillis());
325        assertEquals(programA.getIntentUri(), programB.getIntentUri());
326        assertEquals(programA.isTransient(), programB.isTransient());
327        assertEquals(programA.getType(), programB.getType());
328        assertEquals(programA.getWatchNextType(), programB.getWatchNextType());
329        assertEquals(programA.getPosterArtAspectRatio(), programB.getPosterArtAspectRatio());
330        assertEquals(programA.getThumbnailAspectRatio(), programB.getThumbnailAspectRatio());
331        assertEquals(programA.getLogoUri(), programB.getLogoUri());
332        assertEquals(programA.getAvailability(), programB.getAvailability());
333        assertEquals(programA.getStartingPrice(), programB.getStartingPrice());
334        assertEquals(programA.getOfferPrice(), programB.getOfferPrice());
335        assertEquals(programA.getReleaseDate(), programB.getReleaseDate());
336        assertEquals(programA.getItemCount(), programB.getItemCount());
337        assertEquals(programA.isLive(), programB.isLive());
338        assertEquals(programA.getInteractionType(), programB.getInteractionType());
339        assertEquals(programA.getInteractionCount(), programB.getInteractionCount());
340        assertEquals(programA.getAuthor(), programB.getAuthor());
341        assertEquals(programA.getReviewRatingStyle(), programB.getReviewRatingStyle());
342        assertEquals(programA.getReviewRating(), programB.getReviewRating());
343        assertEquals(programA.getContentId(), programB.getContentId());
344        if (includeIdAndProtectedFields) {
345            // Skip row ID since the one from system DB has the valid ID while the other does not.
346            assertEquals(programA.getId(), programB.getId());
347            // When we insert a channel using toContentValues() to the system, we drop some
348            // protected fields since they only can be modified by system apps.
349            assertEquals(programA.isBrowsable(), programB.isBrowsable());
350            assertEquals(programA.toContentValues(), programB.toContentValues());
351            assertEquals(programA, programB);
352        }
353    }
354
355    private static MatrixCursor getProgramCursor(String[] projection, ContentValues contentValues) {
356        MatrixCursor cursor = new MatrixCursor(projection);
357        MatrixCursor.RowBuilder builder = cursor.newRow();
358        for (String col : projection) {
359            if (col != null) {
360                builder.add(col, contentValues.get(col));
361            }
362        }
363        cursor.moveToFirst();
364        return cursor;
365    }
366}
367