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