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