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