/* * Copyright 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 androidx.tvprovider.media.tv; import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; import android.annotation.TargetApi; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.database.sqlite.SQLiteException; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.media.tv.TvContract; import android.net.Uri; import android.text.TextUtils; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.RestrictTo; import androidx.annotation.WorkerThread; import androidx.tvprovider.media.tv.TvContractCompat.Channels; import androidx.tvprovider.media.tv.TvContractCompat.Channels.Type; import java.io.FileNotFoundException; import java.net.URISyntaxException; import java.util.Objects; import java.util.Set; /** * Since API 26, all TV apps may create preview channels and publish them to the home screen. * We call these App Channels (as distinct from the Live Channels row on the home screen). To help * you create App Channels, the support library provides a number of classes prefixed by the word * Preview-. * * This is a convenience class for mapping your app's content into a * {@link TvContractCompat TvProvider Channel} for publication. Use the provided {@link Builder} * for creating your preview channel object. Once you create a preview channel, you can * use {@link PreviewChannelHelper} to publish it and add {@link PreviewProgram programs} to it. */ @TargetApi(26) public class PreviewChannel { private static final String TAG = "PreviewChannel"; private static final long INVALID_CHANNEL_ID = -1; private static final int IS_BROWSABLE = 1; private ContentValues mValues; private volatile Bitmap mLogoImage; private Uri mLogoUri; private boolean mLogoChanged; /** * Logo is fetched when it is explicitly asked for. mLogoFetched prevents repeated calls in * case there is no logo in fact. */ private volatile boolean mLogoFetched; private PreviewChannel(Builder builder) { mValues = builder.mValues; mLogoImage = builder.mLogoBitmap; mLogoUri = builder.mLogoUri; mLogoChanged = (mLogoImage != null || mLogoUri != null); } /** * Used by {@link PreviewChannelHelper} to transduce a TvProvider channel row into a * PreviewChannel Java object. You never need to use this method unless you want to convert * database rows to PreviewChannel objects yourself. *

* This method assumes the cursor was obtained using {@link androidx.tvprovider.media.tv * .PreviewChannel.Columns#PROJECTION}. This way, all indices are known * beforehand. * * @param cursor a cursor row from the TvProvider * @return a PreviewChannel whose values come from the cursor row */ public static PreviewChannel fromCursor(Cursor cursor) { Builder builder = new Builder(); builder.setId(cursor.getInt(Columns.COL_ID)); builder.setPackageName(cursor.getString(Columns.COL_PACKAGE_NAME)); builder.setType(cursor.getString(Columns.COL_TYPE)); builder.setDisplayName(cursor.getString(Columns.COL_DISPLAY_NAME)); builder.setDescription(cursor.getString(Columns.COL_DESCRIPTION)); builder.setAppLinkIntentUri(Uri.parse(cursor.getString(Columns.COL_APP_LINK_INTENT_URI))); builder.setInternalProviderId(cursor.getString(Columns.COL_INTERNAL_PROVIDER_ID)); builder.setInternalProviderData(cursor.getBlob(Columns.COL_INTERNAL_PROVIDER_DATA)); builder.setInternalProviderFlag1(cursor.getLong(Columns.COL_INTERNAL_PROVIDER_FLAG1)); builder.setInternalProviderFlag2(cursor.getLong(Columns.COL_INTERNAL_PROVIDER_FLAG2)); builder.setInternalProviderFlag3(cursor.getLong(Columns.COL_INTERNAL_PROVIDER_FLAG3)); builder.setInternalProviderFlag4(cursor.getLong(Columns.COL_INTERNAL_PROVIDER_FLAG4)); return builder.build(); } /** * @return the ID the system assigns to this preview channel upon publication. */ public long getId() { Long l = mValues.getAsLong(Channels._ID); return l == null ? INVALID_CHANNEL_ID : l; } /** * @return package name of the app that created this channel */ public String getPackageName() { return mValues.getAsString(Channels.COLUMN_PACKAGE_NAME); } /** * @return what type of channel this is. For preview channels, the type is always * TvContractCompat.Channels.TYPE_PREVIEW */ @Type public String getType() { return mValues.getAsString(Channels.COLUMN_TYPE); } /** * @return The name users see when this channel appears on the home screen */ public CharSequence getDisplayName() { return mValues.getAsString(Channels.COLUMN_DISPLAY_NAME); } /** * @return The value of {@link Channels#COLUMN_DESCRIPTION} for the channel. A short text * explaining what this channel contains. */ public CharSequence getDescription() { return mValues.getAsString(Channels.COLUMN_DESCRIPTION); } /** * @return The value of {@link Channels#COLUMN_APP_LINK_INTENT_URI} for the channel. */ public Uri getAppLinkIntentUri() { String uri = mValues.getAsString(Channels.COLUMN_APP_LINK_INTENT_URI); return uri == null ? null : Uri.parse(uri); } /** * @return The value of {@link Channels#COLUMN_APP_LINK_INTENT_URI} for the program. */ public Intent getAppLinkIntent() throws URISyntaxException { String uri = mValues.getAsString(Channels.COLUMN_APP_LINK_INTENT_URI); return uri == null ? null : Intent.parseUri(uri.toString(), Intent.URI_INTENT_SCHEME); } /** * This method should be called on a worker thread since decoding Bitmap is an expensive * operation and therefore should not be performed on the main thread. * * @return The logo associated with this preview channel */ @WorkerThread public Bitmap getLogo(Context context) { if (!mLogoFetched && mLogoImage == null) { try { mLogoImage = BitmapFactory.decodeStream( context.getContentResolver().openInputStream( TvContract.buildChannelLogoUri(getId()) )); } catch (FileNotFoundException | SQLiteException e) { Log.e(TAG, "Logo for preview channel (ID:" + getId() + ") not found.", e); } mLogoFetched = true; } return mLogoImage; } /** * @hide */ @RestrictTo(LIBRARY_GROUP) boolean isLogoChanged() { return mLogoChanged; } /** * @hide */ @RestrictTo(LIBRARY_GROUP) Uri getLogoUri() { return mLogoUri; } /** * @return The value of {@link Channels#COLUMN_INTERNAL_PROVIDER_DATA} for the channel. */ public byte[] getInternalProviderDataByteArray() { return mValues.getAsByteArray(Channels.COLUMN_INTERNAL_PROVIDER_DATA); } /** * @return The value of {@link Channels#COLUMN_INTERNAL_PROVIDER_FLAG1} for the channel. */ public Long getInternalProviderFlag1() { return mValues.getAsLong(Channels.COLUMN_INTERNAL_PROVIDER_FLAG1); } /** * @return The value of {@link Channels#COLUMN_INTERNAL_PROVIDER_FLAG2} for the channel. */ public Long getInternalProviderFlag2() { return mValues.getAsLong(Channels.COLUMN_INTERNAL_PROVIDER_FLAG2); } /** * @return The value of {@link Channels#COLUMN_INTERNAL_PROVIDER_FLAG3} for the channel. */ public Long getInternalProviderFlag3() { return mValues.getAsLong(Channels.COLUMN_INTERNAL_PROVIDER_FLAG3); } /** * @return The value of {@link Channels#COLUMN_INTERNAL_PROVIDER_FLAG4} for the channel. */ public Long getInternalProviderFlag4() { return mValues.getAsLong(Channels.COLUMN_INTERNAL_PROVIDER_FLAG4); } /** * @return The value of {@link Channels#COLUMN_INTERNAL_PROVIDER_ID} for the channel. */ public String getInternalProviderId() { return mValues.getAsString(Channels.COLUMN_INTERNAL_PROVIDER_ID); } /** * @return The value of {@link Channels#COLUMN_BROWSABLE} for the channel. A preview channel * is BROWABLE when it is visible on the TV home screen. */ public boolean isBrowsable() { Integer i = mValues.getAsInteger(Channels.COLUMN_BROWSABLE); return i != null && i == IS_BROWSABLE; } @Override public int hashCode() { return mValues.hashCode(); } @Override public boolean equals(Object other) { if (!(other instanceof PreviewChannel)) { return false; } return mValues.equals(((PreviewChannel) other).mValues); } /** * Indicates whether some other PreviewChannel has any set attribute that is different from * this PreviewChannel's respective attributes. An attribute is considered "set" if its key * is present in the ContentValues vector. */ public boolean hasAnyUpdatedValues(PreviewChannel update) { Set updateKeys = update.mValues.keySet(); for (String key : updateKeys) { Object updateValue = update.mValues.get(key); Object currValue = mValues.get(key); if (!Objects.deepEquals(updateValue, currValue)) { return true; } } return false; } @Override public String toString() { return "Channel{" + mValues.toString() + "}"; } /** * Used by {@link PreviewChannelHelper} to communicate PreviewChannel CRUD operations * to the TvProvider. You never need to use this method unless you want to communicate to the * TvProvider directly. * * @hide */ @RestrictTo(LIBRARY_GROUP) public ContentValues toContentValues() { ContentValues values = new ContentValues(mValues); return values; } /** * @hide */ @RestrictTo(LIBRARY_GROUP) public static class Columns { public static final String[] PROJECTION = { Channels._ID, Channels.COLUMN_PACKAGE_NAME, Channels.COLUMN_TYPE, Channels.COLUMN_DISPLAY_NAME, Channels.COLUMN_DESCRIPTION, Channels.COLUMN_APP_LINK_INTENT_URI, Channels.COLUMN_INTERNAL_PROVIDER_ID, Channels.COLUMN_INTERNAL_PROVIDER_DATA, Channels.COLUMN_INTERNAL_PROVIDER_FLAG1, Channels.COLUMN_INTERNAL_PROVIDER_FLAG2, Channels.COLUMN_INTERNAL_PROVIDER_FLAG3, Channels.COLUMN_INTERNAL_PROVIDER_FLAG4 }; public static final int COL_ID = 0; public static final int COL_PACKAGE_NAME = 1; public static final int COL_TYPE = 2; public static final int COL_DISPLAY_NAME = 3; public static final int COL_DESCRIPTION = 4; public static final int COL_APP_LINK_INTENT_URI = 5; public static final int COL_INTERNAL_PROVIDER_ID = 6; public static final int COL_INTERNAL_PROVIDER_DATA = 7; public static final int COL_INTERNAL_PROVIDER_FLAG1 = 8; public static final int COL_INTERNAL_PROVIDER_FLAG2 = 9; public static final int COL_INTERNAL_PROVIDER_FLAG3 = 10; public static final int COL_INTERNAL_PROVIDER_FLAG4 = 11; private Columns() { } } /** * This builder makes it easy to create a PreviewChannel object by allowing you to chain * setters. Even though this builder provides a no-arg constructor, certain fields are * required or the {@link #build()} method will throw an exception. The required fields are * displayName and appLinkIntentUri; use the respective methods to set them. */ public static final class Builder { private ContentValues mValues; private Bitmap mLogoBitmap; private Uri mLogoUri; public Builder() { mValues = new ContentValues(); } public Builder(PreviewChannel other) { mValues = new ContentValues(other.mValues); } private Builder setId(long id) { mValues.put(Channels._ID, id); return this; } /** * Sets the package name of the Channel. * * @param packageName The value of {@link Channels#COLUMN_PACKAGE_NAME} for the channel. * @return This Builder object to allow for chaining of calls to builder methods. * @hide */ @RestrictTo(LIBRARY_GROUP) Builder setPackageName(String packageName) { mValues.put(Channels.COLUMN_PACKAGE_NAME, packageName); return this; } // Private because this is always the same: setType(TvContractCompat.Channels.TYPE_PREVIEW) private Builder setType(@Type String type) { mValues.put(Channels.COLUMN_TYPE, type); return this; } /** * This is the name user sees when your channel appears on their TV home screen. For * example "New Arrivals." This field is required. * * @return This Builder object to allow for chaining of calls to builder methods. * @see TvContractCompat.Channels#COLUMN_DISPLAY_NAME */ public Builder setDisplayName(CharSequence displayName) { mValues.put(Channels.COLUMN_DISPLAY_NAME, displayName.toString()); return this; } /** * It's good practice to include a general description of the programs in this channel. * * @return This Builder object to allow for chaining of calls to builder methods. * @see TvContractCompat.Channels#COLUMN_DESCRIPTION */ public Builder setDescription(CharSequence description) { mValues.put(Channels.COLUMN_DESCRIPTION, description.toString()); return this; } /** * When user clicks on this channel's logo, the system will send an Intent for your app to * open an Activity with contents relevant to this channel. Hence, the Intent data you * provide here must point to content relevant to this channel. * * @return This Builder object to allow for chaining of calls to builder methods. */ public Builder setAppLinkIntent(Intent appLinkIntent) { return setAppLinkIntentUri(Uri.parse(appLinkIntent.toUri(Intent.URI_INTENT_SCHEME))); } /** * When user clicks on this channel's logo, the system will send an Intent for your app to * open an Activity with contents relevant to this channel. Hence, the Uri you provide here * must point to content relevant to this channel. * * @return This Builder object to allow for chaining of calls to builder methods. * @see TvContractCompat.Channels#COLUMN_APP_LINK_INTENT_URI */ public Builder setAppLinkIntentUri(Uri appLinkIntentUri) { mValues.put(Channels.COLUMN_APP_LINK_INTENT_URI, null == appLinkIntentUri ? null : appLinkIntentUri.toString()); return this; } /** * It is expected that your app or your server has its own internal representation * (i.e. data structure) of channels. It is highly recommended that you store your * app/server's channel ID here; so that you may easily relate this published preview * channel with the corresponding channel from your server. * * The {@link PreviewChannelHelper#publishChannel(PreviewChannel) publish} method check this * field to verify whether a preview channel being published would result in a duplicate. * : * * @return This Builder object to allow for chaining of calls to builder methods. * @see TvContractCompat.Channels#COLUMN_INTERNAL_PROVIDER_ID */ public Builder setInternalProviderId(String internalProviderId) { mValues.put(Channels.COLUMN_INTERNAL_PROVIDER_ID, internalProviderId); return this; } /** * This is one of the optional fields that your app may set. Use these fields at your * discretion to help you remember important information about this channel. * * For example, if this channel needs a byte array that is expensive for your app to * construct, you may choose to save it here. * * @return This Builder object to allow for chaining of calls to builder methods. * @see TvContractCompat.Channels#COLUMN_INTERNAL_PROVIDER_DATA */ public Builder setInternalProviderData(byte[] internalProviderData) { mValues.put(Channels.COLUMN_INTERNAL_PROVIDER_DATA, internalProviderData); return this; } /** * This is one of the optional fields that your app may set. Use these fields at your * discretion to help you remember important information about this channel. * * For example, you may use this flag to track additional data about this particular * channel. * * @return This Builder object to allow for chaining of calls to builder methods. * @see TvContractCompat.Channels#COLUMN_INTERNAL_PROVIDER_FLAG1 */ public Builder setInternalProviderFlag1(long flag) { mValues.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG1, flag); return this; } /** * This is one of the optional fields that your app may set. Use these fields at your * discretion to help you remember important information about this channel. * * For example, you may use this flag to track additional data about this particular * channel. * * @return This Builder object to allow for chaining of calls to builder methods. * @see TvContractCompat.Channels#COLUMN_INTERNAL_PROVIDER_FLAG2 */ public Builder setInternalProviderFlag2(long flag) { mValues.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG2, flag); return this; } /** * This is one of the optional fields that your app may set. Use these fields at your * discretion to help you remember important information about this channel. * * For example, you may use this flag to track additional data about this particular * channel. * * @return This Builder object to allow for chaining of calls to builder methods. * @see TvContractCompat.Channels#COLUMN_INTERNAL_PROVIDER_FLAG3 */ public Builder setInternalProviderFlag3(long flag) { mValues.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG3, flag); return this; } /** * This is one of the optional fields that your app may set. Use these fields at your * discretion to help you remember important information about this channel. * * For example, you may use this flag to track additional data about this particular * channel. * * @return This Builder object to allow for chaining of calls to builder methods. * @see TvContractCompat.Channels#COLUMN_INTERNAL_PROVIDER_FLAG4 */ public Builder setInternalProviderFlag4(long flag) { mValues.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG4, flag); return this; } /** * A logo visually identifies your channel. Hence, you should consider adding a unique logo * to every channel you create, so user can quickly identify your channel. * * @return This Builder object to allow for chaining of calls to builder methods. */ public Builder setLogo(@NonNull Bitmap logoImage) { mLogoBitmap = logoImage; mLogoUri = null; return this; } /** * A logo visually identifies your channel. Hence, you should consider adding a unique logo * to every channel you create, so user can quickly identify your channel. * * @return This Builder object to allow for chaining of calls to builder methods. */ public Builder setLogo(@NonNull Uri logoUri) { mLogoUri = logoUri; mLogoBitmap = null; return this; } /** * Takes the values of the Builder object and creates a PreviewChannel object. * * @return PreviewChannel object with values from the Builder. */ public PreviewChannel build() { setType(Channels.TYPE_PREVIEW); if (TextUtils.isEmpty(mValues.getAsString(Channels.COLUMN_DISPLAY_NAME))) { throw new IllegalStateException("Need channel name." + " Use method setDisplayName(String) to set it."); } if (TextUtils.isEmpty(mValues.getAsString(Channels.COLUMN_APP_LINK_INTENT_URI))) { throw new IllegalStateException("Need app link intent uri for channel." + " Use method setAppLinkIntent or setAppLinkIntentUri to set it."); } PreviewChannel previewChannel = new PreviewChannel(this); return previewChannel; } } }