| /* |
| * 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. |
| * <p/> |
| * 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<String> 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; |
| } |
| } |
| } |