1/*
2 * Copyright 2018 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 */
16package androidx.tvprovider.media.tv;
17
18import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
19
20import android.annotation.TargetApi;
21import android.content.ContentValues;
22import android.content.Context;
23import android.content.Intent;
24import android.database.Cursor;
25import android.database.sqlite.SQLiteException;
26import android.graphics.Bitmap;
27import android.graphics.BitmapFactory;
28import android.media.tv.TvContract;
29import android.net.Uri;
30import android.text.TextUtils;
31import android.util.Log;
32
33import androidx.annotation.NonNull;
34import androidx.annotation.RestrictTo;
35import androidx.annotation.WorkerThread;
36import androidx.tvprovider.media.tv.TvContractCompat.Channels;
37import androidx.tvprovider.media.tv.TvContractCompat.Channels.Type;
38
39import java.io.FileNotFoundException;
40import java.net.URISyntaxException;
41import java.util.Objects;
42import java.util.Set;
43
44/**
45 * Since API 26, all TV apps may create preview channels and publish them to the home screen.
46 * We call these App Channels (as distinct from the Live Channels row on the home screen). To help
47 * you create App Channels, the support library provides a number of classes prefixed by the word
48 * Preview-.
49 *
50 * This is a convenience class for mapping your app's content into a
51 * {@link TvContractCompat TvProvider Channel} for publication. Use the provided {@link Builder}
52 * for creating your preview channel object. Once you create a preview channel, you can
53 * use {@link PreviewChannelHelper} to publish it and add {@link PreviewProgram programs} to it.
54 */
55@TargetApi(26)
56public class PreviewChannel {
57
58    private static final String TAG = "PreviewChannel";
59    private static final long INVALID_CHANNEL_ID = -1;
60    private static final int IS_BROWSABLE = 1;
61
62    private ContentValues mValues;
63    private volatile Bitmap mLogoImage;
64
65    private Uri mLogoUri;
66    private boolean mLogoChanged;
67
68    /**
69     * Logo is fetched when it is explicitly asked for. mLogoFetched prevents repeated calls in
70     * case there is no logo in fact.
71     */
72    private volatile boolean mLogoFetched;
73
74    private PreviewChannel(Builder builder) {
75        mValues = builder.mValues;
76        mLogoImage = builder.mLogoBitmap;
77        mLogoUri = builder.mLogoUri;
78        mLogoChanged = (mLogoImage != null || mLogoUri != null);
79    }
80
81    /**
82     * Used by {@link PreviewChannelHelper} to transduce a TvProvider channel row into a
83     * PreviewChannel Java object. You never need to use this method unless you want to convert
84     * database rows to PreviewChannel objects yourself.
85     * <p/>
86     * This method assumes the cursor was obtained using {@link androidx.tvprovider.media.tv
87     * .PreviewChannel.Columns#PROJECTION}. This way, all indices are known
88     * beforehand.
89     *
90     * @param cursor a cursor row from the TvProvider
91     * @return a PreviewChannel whose values come from the cursor row
92     */
93    public static PreviewChannel fromCursor(Cursor cursor) {
94        Builder builder = new Builder();
95        builder.setId(cursor.getInt(Columns.COL_ID));
96        builder.setPackageName(cursor.getString(Columns.COL_PACKAGE_NAME));
97        builder.setType(cursor.getString(Columns.COL_TYPE));
98        builder.setDisplayName(cursor.getString(Columns.COL_DISPLAY_NAME));
99        builder.setDescription(cursor.getString(Columns.COL_DESCRIPTION));
100        builder.setAppLinkIntentUri(Uri.parse(cursor.getString(Columns.COL_APP_LINK_INTENT_URI)));
101        builder.setInternalProviderId(cursor.getString(Columns.COL_INTERNAL_PROVIDER_ID));
102        builder.setInternalProviderData(cursor.getBlob(Columns.COL_INTERNAL_PROVIDER_DATA));
103        builder.setInternalProviderFlag1(cursor.getLong(Columns.COL_INTERNAL_PROVIDER_FLAG1));
104        builder.setInternalProviderFlag2(cursor.getLong(Columns.COL_INTERNAL_PROVIDER_FLAG2));
105        builder.setInternalProviderFlag3(cursor.getLong(Columns.COL_INTERNAL_PROVIDER_FLAG3));
106        builder.setInternalProviderFlag4(cursor.getLong(Columns.COL_INTERNAL_PROVIDER_FLAG4));
107        return builder.build();
108    }
109
110    /**
111     * @return the ID the system assigns to this preview channel upon publication.
112     */
113    public long getId() {
114        Long l = mValues.getAsLong(Channels._ID);
115        return l == null ? INVALID_CHANNEL_ID : l;
116    }
117
118    /**
119     * @return package name of the app that created this channel
120     */
121    public String getPackageName() {
122        return mValues.getAsString(Channels.COLUMN_PACKAGE_NAME);
123    }
124
125    /**
126     * @return what type of channel this is. For preview channels, the type is always
127     * TvContractCompat.Channels.TYPE_PREVIEW
128     */
129    @Type
130    public String getType() {
131        return mValues.getAsString(Channels.COLUMN_TYPE);
132    }
133
134    /**
135     * @return The name users see when this channel appears on the home screen
136     */
137    public CharSequence getDisplayName() {
138        return mValues.getAsString(Channels.COLUMN_DISPLAY_NAME);
139    }
140
141    /**
142     * @return The value of {@link Channels#COLUMN_DESCRIPTION} for the channel. A short text
143     * explaining what this channel contains.
144     */
145    public CharSequence getDescription() {
146        return mValues.getAsString(Channels.COLUMN_DESCRIPTION);
147    }
148
149    /**
150     * @return The value of {@link Channels#COLUMN_APP_LINK_INTENT_URI} for the channel.
151     */
152    public Uri getAppLinkIntentUri() {
153        String uri = mValues.getAsString(Channels.COLUMN_APP_LINK_INTENT_URI);
154        return uri == null ? null : Uri.parse(uri);
155    }
156
157    /**
158     * @return The value of {@link Channels#COLUMN_APP_LINK_INTENT_URI} for the program.
159     */
160    public Intent getAppLinkIntent() throws URISyntaxException {
161        String uri = mValues.getAsString(Channels.COLUMN_APP_LINK_INTENT_URI);
162        return uri == null ? null : Intent.parseUri(uri.toString(), Intent.URI_INTENT_SCHEME);
163    }
164
165    /**
166     * This method should be called on a worker thread since decoding Bitmap is an expensive
167     * operation and therefore should not be performed on the main thread.
168     *
169     * @return The logo associated with this preview channel
170     */
171    @WorkerThread
172    public Bitmap getLogo(Context context) {
173        if (!mLogoFetched && mLogoImage == null) {
174            try {
175                mLogoImage = BitmapFactory.decodeStream(
176                        context.getContentResolver().openInputStream(
177                                TvContract.buildChannelLogoUri(getId())
178                        ));
179            } catch (FileNotFoundException | SQLiteException e) {
180                Log.e(TAG, "Logo for preview channel (ID:" + getId() + ") not found.", e);
181            }
182            mLogoFetched = true;
183        }
184        return mLogoImage;
185    }
186
187    /**
188     * @hide
189     */
190    @RestrictTo(LIBRARY_GROUP)
191    boolean isLogoChanged() {
192        return mLogoChanged;
193    }
194
195    /**
196     * @hide
197     */
198    @RestrictTo(LIBRARY_GROUP)
199    Uri getLogoUri() {
200        return mLogoUri;
201    }
202
203    /**
204     * @return The value of {@link Channels#COLUMN_INTERNAL_PROVIDER_DATA} for the channel.
205     */
206    public byte[] getInternalProviderDataByteArray() {
207        return mValues.getAsByteArray(Channels.COLUMN_INTERNAL_PROVIDER_DATA);
208    }
209
210    /**
211     * @return The value of {@link Channels#COLUMN_INTERNAL_PROVIDER_FLAG1} for the channel.
212     */
213    public Long getInternalProviderFlag1() {
214        return mValues.getAsLong(Channels.COLUMN_INTERNAL_PROVIDER_FLAG1);
215    }
216
217    /**
218     * @return The value of {@link Channels#COLUMN_INTERNAL_PROVIDER_FLAG2} for the channel.
219     */
220    public Long getInternalProviderFlag2() {
221        return mValues.getAsLong(Channels.COLUMN_INTERNAL_PROVIDER_FLAG2);
222    }
223
224    /**
225     * @return The value of {@link Channels#COLUMN_INTERNAL_PROVIDER_FLAG3} for the channel.
226     */
227    public Long getInternalProviderFlag3() {
228        return mValues.getAsLong(Channels.COLUMN_INTERNAL_PROVIDER_FLAG3);
229    }
230
231    /**
232     * @return The value of {@link Channels#COLUMN_INTERNAL_PROVIDER_FLAG4} for the channel.
233     */
234    public Long getInternalProviderFlag4() {
235        return mValues.getAsLong(Channels.COLUMN_INTERNAL_PROVIDER_FLAG4);
236    }
237
238    /**
239     * @return The value of {@link Channels#COLUMN_INTERNAL_PROVIDER_ID} for the channel.
240     */
241    public String getInternalProviderId() {
242        return mValues.getAsString(Channels.COLUMN_INTERNAL_PROVIDER_ID);
243    }
244
245    /**
246     * @return The value of {@link Channels#COLUMN_BROWSABLE} for the channel. A preview channel
247     * is BROWABLE when it is visible on the TV home screen.
248     */
249    public boolean isBrowsable() {
250        Integer i = mValues.getAsInteger(Channels.COLUMN_BROWSABLE);
251        return i != null && i == IS_BROWSABLE;
252    }
253
254    @Override
255    public int hashCode() {
256        return mValues.hashCode();
257    }
258
259    @Override
260    public boolean equals(Object other) {
261        if (!(other instanceof PreviewChannel)) {
262            return false;
263        }
264        return mValues.equals(((PreviewChannel) other).mValues);
265    }
266
267    /**
268     * Indicates whether some other PreviewChannel has any set attribute that is different from
269     * this PreviewChannel's respective attributes. An attribute is considered "set" if its key
270     * is present in the ContentValues vector.
271     */
272    public boolean hasAnyUpdatedValues(PreviewChannel update) {
273        Set<String> updateKeys = update.mValues.keySet();
274        for (String key : updateKeys) {
275            Object updateValue = update.mValues.get(key);
276            Object currValue = mValues.get(key);
277            if (!Objects.deepEquals(updateValue, currValue)) {
278                return true;
279            }
280        }
281        return false;
282    }
283
284    @Override
285    public String toString() {
286        return "Channel{" + mValues.toString() + "}";
287    }
288
289    /**
290     * Used by {@link PreviewChannelHelper} to communicate PreviewChannel CRUD operations
291     * to the TvProvider. You never need to use this method unless you want to communicate to the
292     * TvProvider directly.
293     *
294     * @hide
295     */
296    @RestrictTo(LIBRARY_GROUP)
297    public ContentValues toContentValues() {
298        ContentValues values = new ContentValues(mValues);
299        return values;
300    }
301
302    /**
303     * @hide
304     */
305    @RestrictTo(LIBRARY_GROUP)
306    public static class Columns {
307        public static final String[] PROJECTION = {
308                Channels._ID,
309                Channels.COLUMN_PACKAGE_NAME,
310                Channels.COLUMN_TYPE,
311                Channels.COLUMN_DISPLAY_NAME,
312                Channels.COLUMN_DESCRIPTION,
313                Channels.COLUMN_APP_LINK_INTENT_URI,
314                Channels.COLUMN_INTERNAL_PROVIDER_ID,
315                Channels.COLUMN_INTERNAL_PROVIDER_DATA,
316                Channels.COLUMN_INTERNAL_PROVIDER_FLAG1,
317                Channels.COLUMN_INTERNAL_PROVIDER_FLAG2,
318                Channels.COLUMN_INTERNAL_PROVIDER_FLAG3,
319                Channels.COLUMN_INTERNAL_PROVIDER_FLAG4
320        };
321
322        public static final int COL_ID = 0;
323        public static final int COL_PACKAGE_NAME = 1;
324        public static final int COL_TYPE = 2;
325        public static final int COL_DISPLAY_NAME = 3;
326        public static final int COL_DESCRIPTION = 4;
327        public static final int COL_APP_LINK_INTENT_URI = 5;
328        public static final int COL_INTERNAL_PROVIDER_ID = 6;
329        public static final int COL_INTERNAL_PROVIDER_DATA = 7;
330        public static final int COL_INTERNAL_PROVIDER_FLAG1 = 8;
331        public static final int COL_INTERNAL_PROVIDER_FLAG2 = 9;
332        public static final int COL_INTERNAL_PROVIDER_FLAG3 = 10;
333        public static final int COL_INTERNAL_PROVIDER_FLAG4 = 11;
334
335        private Columns() {
336        }
337    }
338
339    /**
340     * This builder makes it easy to create a PreviewChannel object by allowing you to chain
341     * setters. Even though this builder provides a no-arg constructor, certain fields are
342     * required or the {@link #build()} method will throw an exception. The required fields are
343     * displayName and appLinkIntentUri; use the respective methods to set them.
344     */
345    public static final class Builder {
346        private ContentValues mValues;
347        private Bitmap mLogoBitmap;
348        private Uri mLogoUri;
349
350        public Builder() {
351            mValues = new ContentValues();
352        }
353
354        public Builder(PreviewChannel other) {
355            mValues = new ContentValues(other.mValues);
356        }
357
358        private Builder setId(long id) {
359            mValues.put(Channels._ID, id);
360            return this;
361        }
362
363        /**
364         * Sets the package name of the Channel.
365         *
366         * @param packageName The value of {@link Channels#COLUMN_PACKAGE_NAME} for the channel.
367         * @return This Builder object to allow for chaining of calls to builder methods.
368         * @hide
369         */
370        @RestrictTo(LIBRARY_GROUP)
371        Builder setPackageName(String packageName) {
372            mValues.put(Channels.COLUMN_PACKAGE_NAME, packageName);
373            return this;
374        }
375
376        // Private because this is always the same: setType(TvContractCompat.Channels.TYPE_PREVIEW)
377        private Builder setType(@Type String type) {
378            mValues.put(Channels.COLUMN_TYPE, type);
379            return this;
380        }
381
382        /**
383         * This is the name user sees when your channel appears on their TV home screen. For
384         * example "New Arrivals." This field is required.
385         *
386         * @return This Builder object to allow for chaining of calls to builder methods.
387         * @see TvContractCompat.Channels#COLUMN_DISPLAY_NAME
388         */
389        public Builder setDisplayName(CharSequence displayName) {
390            mValues.put(Channels.COLUMN_DISPLAY_NAME, displayName.toString());
391            return this;
392        }
393
394        /**
395         * It's good practice to include a general description of the programs in this channel.
396         *
397         * @return This Builder object to allow for chaining of calls to builder methods.
398         * @see TvContractCompat.Channels#COLUMN_DESCRIPTION
399         */
400        public Builder setDescription(CharSequence description) {
401            mValues.put(Channels.COLUMN_DESCRIPTION, description.toString());
402            return this;
403        }
404
405        /**
406         * When user clicks on this channel's logo, the system will send an Intent for your app to
407         * open an Activity with contents relevant to this channel. Hence, the Intent data you
408         * provide here must point to content relevant to this channel.
409         *
410         * @return This Builder object to allow for chaining of calls to builder methods.
411         */
412        public Builder setAppLinkIntent(Intent appLinkIntent) {
413            return setAppLinkIntentUri(Uri.parse(appLinkIntent.toUri(Intent.URI_INTENT_SCHEME)));
414        }
415
416        /**
417         * When user clicks on this channel's logo, the system will send an Intent for your app to
418         * open an Activity with contents relevant to this channel. Hence, the Uri you provide here
419         * must point to content relevant to this channel.
420         *
421         * @return This Builder object to allow for chaining of calls to builder methods.
422         * @see TvContractCompat.Channels#COLUMN_APP_LINK_INTENT_URI
423         */
424        public Builder setAppLinkIntentUri(Uri appLinkIntentUri) {
425            mValues.put(Channels.COLUMN_APP_LINK_INTENT_URI,
426                    null == appLinkIntentUri ? null : appLinkIntentUri.toString());
427            return this;
428        }
429
430        /**
431         * It is expected that your app or your server has its own internal representation
432         * (i.e. data structure) of channels. It is highly recommended that you store your
433         * app/server's channel ID here; so that you may easily relate this published preview
434         * channel with the corresponding channel from your server.
435         *
436         * The {@link PreviewChannelHelper#publishChannel(PreviewChannel) publish} method check this
437         * field to verify whether a preview channel being published would result in a duplicate.
438         * :
439         *
440         * @return This Builder object to allow for chaining of calls to builder methods.
441         * @see TvContractCompat.Channels#COLUMN_INTERNAL_PROVIDER_ID
442         */
443        public Builder setInternalProviderId(String internalProviderId) {
444            mValues.put(Channels.COLUMN_INTERNAL_PROVIDER_ID, internalProviderId);
445            return this;
446        }
447
448        /**
449         * This is one of the optional fields that your app may set. Use these fields at your
450         * discretion to help you remember important information about this channel.
451         *
452         * For example, if this channel needs a byte array that is expensive for your app to
453         * construct, you may choose to save it here.
454         *
455         * @return This Builder object to allow for chaining of calls to builder methods.
456         * @see TvContractCompat.Channels#COLUMN_INTERNAL_PROVIDER_DATA
457         */
458        public Builder setInternalProviderData(byte[] internalProviderData) {
459            mValues.put(Channels.COLUMN_INTERNAL_PROVIDER_DATA, internalProviderData);
460            return this;
461        }
462
463        /**
464         * This is one of the optional fields that your app may set. Use these fields at your
465         * discretion to help you remember important information about this channel.
466         *
467         * For example, you may use this flag to track additional data about this particular
468         * channel.
469         *
470         * @return This Builder object to allow for chaining of calls to builder methods.
471         * @see TvContractCompat.Channels#COLUMN_INTERNAL_PROVIDER_FLAG1
472         */
473        public Builder setInternalProviderFlag1(long flag) {
474            mValues.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG1, flag);
475            return this;
476        }
477
478        /**
479         * This is one of the optional fields that your app may set. Use these fields at your
480         * discretion to help you remember important information about this channel.
481         *
482         * For example, you may use this flag to track additional data about this particular
483         * channel.
484         *
485         * @return This Builder object to allow for chaining of calls to builder methods.
486         * @see TvContractCompat.Channels#COLUMN_INTERNAL_PROVIDER_FLAG2
487         */
488        public Builder setInternalProviderFlag2(long flag) {
489            mValues.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG2, flag);
490            return this;
491        }
492
493        /**
494         * This is one of the optional fields that your app may set. Use these fields at your
495         * discretion to help you remember important information about this channel.
496         *
497         * For example, you may use this flag to track additional data about this particular
498         * channel.
499         *
500         * @return This Builder object to allow for chaining of calls to builder methods.
501         * @see TvContractCompat.Channels#COLUMN_INTERNAL_PROVIDER_FLAG3
502         */
503        public Builder setInternalProviderFlag3(long flag) {
504            mValues.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG3, flag);
505            return this;
506        }
507
508        /**
509         * This is one of the optional fields that your app may set. Use these fields at your
510         * discretion to help you remember important information about this channel.
511         *
512         * For example, you may use this flag to track additional data about this particular
513         * channel.
514         *
515         * @return This Builder object to allow for chaining of calls to builder methods.
516         * @see TvContractCompat.Channels#COLUMN_INTERNAL_PROVIDER_FLAG4
517         */
518        public Builder setInternalProviderFlag4(long flag) {
519            mValues.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG4, flag);
520            return this;
521        }
522
523        /**
524         * A logo visually identifies your channel. Hence, you should consider adding a unique logo
525         * to every channel you create, so user can quickly identify your channel.
526         *
527         * @return This Builder object to allow for chaining of calls to builder methods.
528         */
529        public Builder setLogo(@NonNull Bitmap logoImage) {
530            mLogoBitmap = logoImage;
531            mLogoUri = null;
532            return this;
533        }
534
535        /**
536         * A logo visually identifies your channel. Hence, you should consider adding a unique logo
537         * to every channel you create, so user can quickly identify your channel.
538         *
539         * @return This Builder object to allow for chaining of calls to builder methods.
540         */
541        public Builder setLogo(@NonNull Uri logoUri) {
542            mLogoUri = logoUri;
543            mLogoBitmap = null;
544            return this;
545        }
546
547        /**
548         * Takes the values of the Builder object and creates a PreviewChannel object.
549         *
550         * @return PreviewChannel object with values from the Builder.
551         */
552        public PreviewChannel build() {
553            setType(Channels.TYPE_PREVIEW);
554
555            if (TextUtils.isEmpty(mValues.getAsString(Channels.COLUMN_DISPLAY_NAME))) {
556                throw new IllegalStateException("Need channel name."
557                        + " Use method setDisplayName(String) to set it.");
558            }
559
560            if (TextUtils.isEmpty(mValues.getAsString(Channels.COLUMN_APP_LINK_INTENT_URI))) {
561                throw new IllegalStateException("Need app link intent uri for channel."
562                        + " Use method setAppLinkIntent or setAppLinkIntentUri to set it.");
563            }
564
565            PreviewChannel previewChannel = new PreviewChannel(this);
566            return previewChannel;
567        }
568    }
569}
570