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