1 /* 2 * Copyright (C) 2015 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 17 package com.android.tv.data; 18 19 import android.content.Context; 20 import android.content.Intent; 21 import android.content.pm.PackageManager; 22 import android.database.Cursor; 23 import android.media.tv.TvContract; 24 import android.media.tv.TvInputInfo; 25 import android.net.Uri; 26 import android.support.annotation.Nullable; 27 import android.support.annotation.UiThread; 28 import android.support.annotation.VisibleForTesting; 29 import android.text.TextUtils; 30 import android.util.Log; 31 32 import com.android.tv.common.TvCommonConstants; 33 import com.android.tv.util.ImageLoader; 34 import com.android.tv.util.TvInputManagerHelper; 35 import com.android.tv.util.Utils; 36 37 import java.net.URISyntaxException; 38 import java.util.Comparator; 39 import java.util.HashMap; 40 import java.util.Map; 41 import java.util.Objects; 42 43 /** 44 * A convenience class to create and insert channel entries into the database. 45 */ 46 public final class Channel { 47 private static final String TAG = "Channel"; 48 49 public static final long INVALID_ID = -1; 50 public static final int LOAD_IMAGE_TYPE_CHANNEL_LOGO = 1; 51 public static final int LOAD_IMAGE_TYPE_APP_LINK_ICON = 2; 52 public static final int LOAD_IMAGE_TYPE_APP_LINK_POSTER_ART = 3; 53 54 /** 55 * When a TIS doesn't provide any information about app link, and it doesn't have a leanback 56 * launch intent, there will be no app link card for the TIS. 57 */ 58 public static final int APP_LINK_TYPE_NONE = -1; 59 /** 60 * When a TIS provide a specific app link information, the app link card will be 61 * {@code APP_LINK_TYPE_CHANNEL} which contains all the provided information. 62 */ 63 public static final int APP_LINK_TYPE_CHANNEL = 1; 64 /** 65 * When a TIS doesn't provide a specific app link information, but the app has a leanback launch 66 * intent, the app link card will be {@code APP_LINK_TYPE_APP} which launches the application. 67 */ 68 public static final int APP_LINK_TYPE_APP = 2; 69 70 private static final int APP_LINK_TYPE_NOT_SET = 0; 71 private static final String INVALID_PACKAGE_NAME = "packageName"; 72 73 public static final String[] PROJECTION = { 74 // Columns must match what is read in Channel.fromCursor() 75 TvContract.Channels._ID, 76 TvContract.Channels.COLUMN_PACKAGE_NAME, 77 TvContract.Channels.COLUMN_INPUT_ID, 78 TvContract.Channels.COLUMN_TYPE, 79 TvContract.Channels.COLUMN_DISPLAY_NUMBER, 80 TvContract.Channels.COLUMN_DISPLAY_NAME, 81 TvContract.Channels.COLUMN_DESCRIPTION, 82 TvContract.Channels.COLUMN_VIDEO_FORMAT, 83 TvContract.Channels.COLUMN_BROWSABLE, 84 TvContract.Channels.COLUMN_LOCKED, 85 TvContract.Channels.COLUMN_APP_LINK_TEXT, 86 TvContract.Channels.COLUMN_APP_LINK_COLOR, 87 TvContract.Channels.COLUMN_APP_LINK_ICON_URI, 88 TvContract.Channels.COLUMN_APP_LINK_POSTER_ART_URI, 89 TvContract.Channels.COLUMN_APP_LINK_INTENT_URI, 90 }; 91 92 /** 93 * Creates {@code Channel} object from cursor. 94 * 95 * <p>The query that created the cursor MUST use {@link #PROJECTION} 96 * 97 */ 98 public static Channel fromCursor(Cursor cursor) { 99 // Columns read must match the order of {@link #PROJECTION} 100 Channel channel = new Channel(); 101 int index = 0; 102 channel.mId = cursor.getLong(index++); 103 channel.mPackageName = Utils.intern(cursor.getString(index++)); 104 channel.mInputId = Utils.intern(cursor.getString(index++)); 105 channel.mType = Utils.intern(cursor.getString(index++)); 106 channel.mDisplayNumber = cursor.getString(index++); 107 channel.mDisplayName = cursor.getString(index++); 108 channel.mDescription = cursor.getString(index++); 109 channel.mVideoFormat = Utils.intern(cursor.getString(index++)); 110 channel.mBrowsable = cursor.getInt(index++) == 1; 111 channel.mLocked = cursor.getInt(index++) == 1; 112 channel.mAppLinkText = cursor.getString(index++); 113 channel.mAppLinkColor = cursor.getInt(index++); 114 channel.mAppLinkIconUri = cursor.getString(index++); 115 channel.mAppLinkPosterArtUri = cursor.getString(index++); 116 channel.mAppLinkIntentUri = cursor.getString(index++); 117 return channel; 118 } 119 120 /** 121 * Creates a {@link Channel} object from the DVR database. 122 */ 123 public static Channel fromDvrCursor(Cursor c) { 124 Channel channel = new Channel(); 125 int index = -1; 126 channel.mDvrId = c.getLong(++index); 127 return channel; 128 } 129 130 /** ID of this channel. Matches to BaseColumns._ID. */ 131 private long mId; 132 133 private String mPackageName; 134 private String mInputId; 135 private String mType; 136 private String mDisplayNumber; 137 private String mDisplayName; 138 private String mDescription; 139 private String mVideoFormat; 140 private boolean mBrowsable; 141 private boolean mLocked; 142 private boolean mIsPassthrough; 143 private String mAppLinkText; 144 private int mAppLinkColor; 145 private String mAppLinkIconUri; 146 private String mAppLinkPosterArtUri; 147 private String mAppLinkIntentUri; 148 private Intent mAppLinkIntent; 149 private int mAppLinkType; 150 151 private long mDvrId; 152 153 private Channel() { 154 // Do nothing. 155 } 156 157 public long getId() { 158 return mId; 159 } 160 161 public Uri getUri() { 162 if (isPassthrough()) { 163 return TvContract.buildChannelUriForPassthroughInput(mInputId); 164 } else { 165 return TvContract.buildChannelUri(mId); 166 } 167 } 168 169 public String getPackageName() { 170 return mPackageName; 171 } 172 173 public String getInputId() { 174 return mInputId; 175 } 176 177 public String getType() { 178 return mType; 179 } 180 181 public String getDisplayNumber() { 182 return mDisplayNumber; 183 } 184 185 @Nullable 186 public String getDisplayName() { 187 return mDisplayName; 188 } 189 190 @VisibleForTesting 191 public String getDescription() { 192 return mDescription; 193 } 194 195 public String getVideoFormat() { 196 return mVideoFormat; 197 } 198 199 public boolean isPassthrough() { 200 return mIsPassthrough; 201 } 202 203 /** 204 * Gets identification text for displaying or debugging. 205 * It's made from Channels' display number plus their display name. 206 */ 207 public String getDisplayText() { 208 return TextUtils.isEmpty(mDisplayName) ? mDisplayNumber 209 : mDisplayNumber + " " + mDisplayName; 210 } 211 212 public String getAppLinkText() { 213 return mAppLinkText; 214 } 215 216 public int getAppLinkColor() { 217 return mAppLinkColor; 218 } 219 220 public String getAppLinkIconUri() { 221 return mAppLinkIconUri; 222 } 223 224 public String getAppLinkPosterArtUri() { 225 return mAppLinkPosterArtUri; 226 } 227 228 public String getAppLinkIntentUri() { 229 return mAppLinkIntentUri; 230 } 231 232 /** 233 * Returns an ID in DVR database. 234 */ 235 public long getDvrId() { 236 return mDvrId; 237 } 238 239 /** 240 * Checks whether this channel is physical tuner channel or not. 241 */ 242 public boolean isPhysicalTunerChannel() { 243 return !TextUtils.isEmpty(mType) && !TvContract.Channels.TYPE_OTHER.equals(mType); 244 } 245 246 /** 247 * Checks if two channels equal by checking ids. 248 */ 249 @Override 250 public boolean equals(Object o) { 251 if (!(o instanceof Channel)) { 252 return false; 253 } 254 Channel other = (Channel) o; 255 // All pass-through TV channels have INVALID_ID value for mId. 256 return mId == other.mId && TextUtils.equals(mInputId, other.mInputId) 257 && mIsPassthrough == other.mIsPassthrough; 258 } 259 260 @Override 261 public int hashCode() { 262 return Objects.hash(mId, mInputId, mIsPassthrough); 263 } 264 265 public boolean isBrowsable() { 266 return mBrowsable; 267 } 268 269 public boolean isLocked() { 270 return mLocked; 271 } 272 273 public void setBrowsable(boolean browsable) { 274 mBrowsable = browsable; 275 } 276 277 public void setLocked(boolean locked) { 278 mLocked = locked; 279 } 280 281 /** 282 * Check whether {@code other} has same read-only channel info as this. But, it cannot check two 283 * channels have same logos. It also excludes browsable and locked, because two fields are 284 * changed by TV app. 285 */ 286 public boolean hasSameReadOnlyInfo(Channel other) { 287 return other != null 288 && Objects.equals(mId, other.mId) 289 && Objects.equals(mPackageName, other.mPackageName) 290 && Objects.equals(mInputId, other.mInputId) 291 && Objects.equals(mType, other.mType) 292 && Objects.equals(mDisplayNumber, other.mDisplayNumber) 293 && Objects.equals(mDisplayName, other.mDisplayName) 294 && Objects.equals(mDescription, other.mDescription) 295 && Objects.equals(mVideoFormat, other.mVideoFormat) 296 && mIsPassthrough == other.mIsPassthrough 297 && Objects.equals(mAppLinkText, other.mAppLinkText) 298 && mAppLinkColor == other.mAppLinkColor 299 && Objects.equals(mAppLinkIconUri, other.mAppLinkIconUri) 300 && Objects.equals(mAppLinkPosterArtUri, other.mAppLinkPosterArtUri) 301 && Objects.equals(mAppLinkIntentUri, other.mAppLinkIntentUri); 302 } 303 304 @Override 305 public String toString() { 306 return "Channel{" 307 + "id=" + mId 308 + ", packageName=" + mPackageName 309 + ", inputId=" + mInputId 310 + ", type=" + mType 311 + ", displayNumber=" + mDisplayNumber 312 + ", displayName=" + mDisplayName 313 + ", description=" + mDescription 314 + ", videoFormat=" + mVideoFormat 315 + ", isPassthrough=" + mIsPassthrough 316 + ", browsable=" + mBrowsable 317 + ", locked=" + mLocked 318 + ", appLinkText=" + mAppLinkText + "}"; 319 } 320 321 void copyFrom(Channel other) { 322 if (this == other) { 323 return; 324 } 325 mId = other.mId; 326 mPackageName = other.mPackageName; 327 mInputId = other.mInputId; 328 mType = other.mType; 329 mDisplayNumber = other.mDisplayNumber; 330 mDisplayName = other.mDisplayName; 331 mDescription = other.mDescription; 332 mVideoFormat = other.mVideoFormat; 333 mIsPassthrough = other.mIsPassthrough; 334 mBrowsable = other.mBrowsable; 335 mLocked = other.mLocked; 336 mAppLinkText = other.mAppLinkText; 337 mAppLinkColor = other.mAppLinkColor; 338 mAppLinkIconUri = other.mAppLinkIconUri; 339 mAppLinkPosterArtUri = other.mAppLinkPosterArtUri; 340 mAppLinkIntentUri = other.mAppLinkIntentUri; 341 mAppLinkIntent = other.mAppLinkIntent; 342 mAppLinkType = other.mAppLinkType; 343 } 344 345 /** 346 * Creates a channel for a passthrough TV input. 347 */ 348 public static Channel createPassthroughChannel(Uri uri) { 349 if (!TvContract.isChannelUriForPassthroughInput(uri)) { 350 throw new IllegalArgumentException("URI is not a passthrough channel URI"); 351 } 352 String inputId = uri.getPathSegments().get(1); 353 return createPassthroughChannel(inputId); 354 } 355 356 /** 357 * Creates a channel for a passthrough TV input with {@code inputId}. 358 */ 359 public static Channel createPassthroughChannel(String inputId) { 360 return new Builder() 361 .setInputId(inputId) 362 .setPassthrough(true) 363 .build(); 364 } 365 366 /** 367 * Checks whether the channel is valid or not. 368 */ 369 public static boolean isValid(Channel channel) { 370 return channel != null && (channel.mId != INVALID_ID || channel.mIsPassthrough); 371 } 372 373 /** 374 * Builder class for {@code Channel}. 375 * Suppress using this outside of ChannelDataManager 376 * so Channels could be managed by ChannelDataManager. 377 */ 378 public static final class Builder { 379 private final Channel mChannel; 380 381 public Builder() { 382 mChannel = new Channel(); 383 // Fill initial data. 384 mChannel.mId = INVALID_ID; 385 mChannel.mPackageName = INVALID_PACKAGE_NAME; 386 mChannel.mInputId = "inputId"; 387 mChannel.mType = "type"; 388 mChannel.mDisplayNumber = "0"; 389 mChannel.mDisplayName = "name"; 390 mChannel.mDescription = "description"; 391 mChannel.mBrowsable = true; 392 mChannel.mLocked = false; 393 mChannel.mIsPassthrough = false; 394 } 395 396 public Builder(Channel other) { 397 mChannel = new Channel(); 398 mChannel.copyFrom(other); 399 } 400 401 @VisibleForTesting 402 public Builder setId(long id) { 403 mChannel.mId = id; 404 return this; 405 } 406 407 @VisibleForTesting 408 public Builder setPackageName(String packageName) { 409 mChannel.mPackageName = packageName; 410 return this; 411 } 412 413 public Builder setInputId(String inputId) { 414 mChannel.mInputId = inputId; 415 return this; 416 } 417 418 public Builder setType(String type) { 419 mChannel.mType = type; 420 return this; 421 } 422 423 @VisibleForTesting 424 public Builder setDisplayNumber(String displayNumber) { 425 mChannel.mDisplayNumber = displayNumber; 426 return this; 427 } 428 429 @VisibleForTesting 430 public Builder setDisplayName(String displayName) { 431 mChannel.mDisplayName = displayName; 432 return this; 433 } 434 435 @VisibleForTesting 436 public Builder setDescription(String description) { 437 mChannel.mDescription = description; 438 return this; 439 } 440 441 public Builder setVideoFormat(String videoFormat) { 442 mChannel.mVideoFormat = videoFormat; 443 return this; 444 } 445 446 public Builder setBrowsable(boolean browsable) { 447 mChannel.mBrowsable = browsable; 448 return this; 449 } 450 451 public Builder setLocked(boolean locked) { 452 mChannel.mLocked = locked; 453 return this; 454 } 455 456 public Builder setPassthrough(boolean isPassthrough) { 457 mChannel.mIsPassthrough = isPassthrough; 458 return this; 459 } 460 461 @VisibleForTesting 462 public Builder setAppLinkText(String appLinkText) { 463 mChannel.mAppLinkText = appLinkText; 464 return this; 465 } 466 467 public Builder setAppLinkColor(int appLinkColor) { 468 mChannel.mAppLinkColor = appLinkColor; 469 return this; 470 } 471 472 public Builder setAppLinkIconUri(String appLinkIconUri) { 473 mChannel.mAppLinkIconUri = appLinkIconUri; 474 return this; 475 } 476 477 public Builder setAppLinkPosterArtUri(String appLinkPosterArtUri) { 478 mChannel.mAppLinkPosterArtUri = appLinkPosterArtUri; 479 return this; 480 } 481 482 @VisibleForTesting 483 public Builder setAppLinkIntentUri(String appLinkIntentUri) { 484 mChannel.mAppLinkIntentUri = appLinkIntentUri; 485 return this; 486 } 487 488 public Channel build() { 489 Channel channel = new Channel(); 490 channel.copyFrom(mChannel); 491 return channel; 492 } 493 } 494 495 /** 496 * Prefetches the images for this channel. 497 */ 498 public void prefetchImage(Context context, int type, int maxWidth, int maxHeight) { 499 String uriString = getImageUriString(type); 500 if (!TextUtils.isEmpty(uriString)) { 501 ImageLoader.prefetchBitmap(context, uriString, maxWidth, maxHeight); 502 } 503 } 504 505 /** 506 * Loads the bitmap of this channel and returns it via {@code callback}. 507 * The loaded bitmap will be cached and resized with given params. 508 * <p> 509 * Note that it may directly call {@code callback} if the bitmap is already loaded. 510 * 511 * @param context A context. 512 * @param type The type of bitmap which will be loaded. It should be one of follows: 513 * {@link #LOAD_IMAGE_TYPE_CHANNEL_LOGO}, {@link #LOAD_IMAGE_TYPE_APP_LINK_ICON}, or 514 * {@link #LOAD_IMAGE_TYPE_APP_LINK_POSTER_ART}. 515 * @param maxWidth The max width of the loaded bitmap. 516 * @param maxHeight The max height of the loaded bitmap. 517 * @param callback A callback which will be called after the loading finished. 518 */ 519 @UiThread 520 public void loadBitmap(Context context, final int type, int maxWidth, int maxHeight, 521 ImageLoader.ImageLoaderCallback callback) { 522 String uriString = getImageUriString(type); 523 ImageLoader.loadBitmap(context, uriString, maxWidth, maxHeight, callback); 524 } 525 526 /** 527 * Returns the type of app link for this channel. 528 * It returns {@link #APP_LINK_TYPE_CHANNEL} if the channel has a non null app link text and 529 * a valid app link intent, it returns {@link #APP_LINK_TYPE_APP} if the input service which 530 * holds the channel has leanback launch intent, and it returns {@link #APP_LINK_TYPE_NONE} 531 * otherwise. 532 */ 533 public int getAppLinkType(Context context) { 534 if (mAppLinkType == APP_LINK_TYPE_NOT_SET) { 535 initAppLinkTypeAndIntent(context); 536 } 537 return mAppLinkType; 538 } 539 540 /** 541 * Returns the app link intent for this channel. 542 * If the type of app link is {@link #APP_LINK_TYPE_NONE}, it returns {@code null}. 543 */ 544 public Intent getAppLinkIntent(Context context) { 545 if (mAppLinkType == APP_LINK_TYPE_NOT_SET) { 546 initAppLinkTypeAndIntent(context); 547 } 548 return mAppLinkIntent; 549 } 550 551 private void initAppLinkTypeAndIntent(Context context) { 552 mAppLinkType = APP_LINK_TYPE_NONE; 553 mAppLinkIntent = null; 554 PackageManager pm = context.getPackageManager(); 555 if (!TextUtils.isEmpty(mAppLinkText) && !TextUtils.isEmpty(mAppLinkIntentUri)) { 556 try { 557 Intent intent = Intent.parseUri(mAppLinkIntentUri, Intent.URI_INTENT_SCHEME); 558 if (intent.resolveActivityInfo(pm, 0) != null) { 559 mAppLinkIntent = intent; 560 mAppLinkIntent.putExtra(TvCommonConstants.EXTRA_APP_LINK_CHANNEL_URI, 561 getUri().toString()); 562 mAppLinkType = APP_LINK_TYPE_CHANNEL; 563 return; 564 } else { 565 Log.w(TAG, "No activity exists to handle : " + mAppLinkIntentUri); 566 } 567 } catch (URISyntaxException e) { 568 Log.w(TAG, "Unable to set app link for " + mAppLinkIntentUri, e); 569 // Do nothing. 570 } 571 } 572 if (mPackageName.equals(context.getApplicationContext().getPackageName())) { 573 return; 574 } 575 mAppLinkIntent = pm.getLeanbackLaunchIntentForPackage(mPackageName); 576 if (mAppLinkIntent != null) { 577 mAppLinkIntent.putExtra(TvCommonConstants.EXTRA_APP_LINK_CHANNEL_URI, 578 getUri().toString()); 579 mAppLinkType = APP_LINK_TYPE_APP; 580 } 581 } 582 583 private String getImageUriString(int type) { 584 switch (type) { 585 case LOAD_IMAGE_TYPE_CHANNEL_LOGO: 586 return TvContract.buildChannelLogoUri(mId).toString(); 587 case LOAD_IMAGE_TYPE_APP_LINK_ICON: 588 return mAppLinkIconUri; 589 case LOAD_IMAGE_TYPE_APP_LINK_POSTER_ART: 590 return mAppLinkPosterArtUri; 591 } 592 return null; 593 } 594 595 public static class DefaultComparator implements Comparator<Channel> { 596 private final Context mContext; 597 private final TvInputManagerHelper mInputManager; 598 private final Map<String, String> mInputIdToLabelMap = new HashMap<>(); 599 private boolean mDetectDuplicatesEnabled; 600 601 public DefaultComparator(Context context, TvInputManagerHelper inputManager) { 602 mContext = context; 603 mInputManager = inputManager; 604 } 605 606 public void setDetectDuplicatesEnabled(boolean detectDuplicatesEnabled) { 607 mDetectDuplicatesEnabled = detectDuplicatesEnabled; 608 } 609 610 @Override 611 public int compare(Channel lhs, Channel rhs) { 612 if (lhs == rhs) { 613 return 0; 614 } 615 // Put channels from OEM/SOC inputs first. 616 boolean lhsIsPartner = mInputManager.isPartnerInput(lhs.getInputId()); 617 boolean rhsIsPartner = mInputManager.isPartnerInput(rhs.getInputId()); 618 if (lhsIsPartner != rhsIsPartner) { 619 return lhsIsPartner ? -1 : 1; 620 } 621 // Compare the input labels. 622 String lhsLabel = getInputLabelForChannel(lhs); 623 String rhsLabel = getInputLabelForChannel(rhs); 624 int result = lhsLabel == null ? (rhsLabel == null ? 0 : 1) : rhsLabel == null ? -1 625 : lhsLabel.compareTo(rhsLabel); 626 if (result != 0) { 627 return result; 628 } 629 // Compare the input IDs. The input IDs cannot be null. 630 result = lhs.getInputId().compareTo(rhs.getInputId()); 631 if (result != 0) { 632 return result; 633 } 634 // Compare the channel numbers if both channels belong to the same input. 635 result = ChannelNumber.compare(lhs.getDisplayNumber(), rhs.getDisplayNumber()); 636 if (mDetectDuplicatesEnabled && result == 0) { 637 Log.w(TAG, "Duplicate channels detected! - \"" 638 + lhs.getDisplayText() + "\" and \"" + rhs.getDisplayText() + "\""); 639 } 640 return result; 641 } 642 643 @VisibleForTesting 644 String getInputLabelForChannel(Channel channel) { 645 String label = mInputIdToLabelMap.get(channel.getInputId()); 646 if (label == null) { 647 TvInputInfo info = mInputManager.getTvInputInfo(channel.getInputId()); 648 if (info != null) { 649 label = Utils.loadLabel(mContext, info); 650 if (label != null) { 651 mInputIdToLabelMap.put(channel.getInputId(), label); 652 } 653 } 654 } 655 return label; 656 } 657 } 658 } 659