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 */ fromCursor(Cursor cursor)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('-'). */ normalizeDisplayNumber(String string)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 ChannelImpl()155 private ChannelImpl() { 156 // Do nothing. 157 } 158 159 @Override getId()160 public long getId() { 161 return mId; 162 } 163 164 @Override getUri()165 public Uri getUri() { 166 if (isPassthrough()) { 167 return TvContract.buildChannelUriForPassthroughInput(mInputId); 168 } else { 169 return TvContract.buildChannelUri(mId); 170 } 171 } 172 173 @Override getPackageName()174 public String getPackageName() { 175 return mPackageName; 176 } 177 178 @Override getInputId()179 public String getInputId() { 180 return mInputId; 181 } 182 183 @Override getType()184 public String getType() { 185 return mType; 186 } 187 188 @Override getDisplayNumber()189 public String getDisplayNumber() { 190 return mDisplayNumber; 191 } 192 193 @Override 194 @Nullable getDisplayName()195 public String getDisplayName() { 196 return mDisplayName; 197 } 198 199 @Override getDescription()200 public String getDescription() { 201 return mDescription; 202 } 203 204 @Override getVideoFormat()205 public String getVideoFormat() { 206 return mVideoFormat; 207 } 208 209 @Override isPassthrough()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 getDisplayText()219 public String getDisplayText() { 220 return TextUtils.isEmpty(mDisplayName) 221 ? mDisplayNumber 222 : mDisplayNumber + " " + mDisplayName; 223 } 224 225 @Override getAppLinkText()226 public String getAppLinkText() { 227 return mAppLinkText; 228 } 229 230 @Override getAppLinkColor()231 public int getAppLinkColor() { 232 return mAppLinkColor; 233 } 234 235 @Override getAppLinkIconUri()236 public String getAppLinkIconUri() { 237 return mAppLinkIconUri; 238 } 239 240 @Override getAppLinkPosterArtUri()241 public String getAppLinkPosterArtUri() { 242 return mAppLinkPosterArtUri; 243 } 244 245 @Override getAppLinkIntentUri()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 getLogoUri()252 public String getLogoUri() { 253 return mLogoUri; 254 } 255 256 @Override isRecordingProhibited()257 public boolean isRecordingProhibited() { 258 return mRecordingProhibited; 259 } 260 261 /** Checks whether this channel is physical tuner channel or not. */ 262 @Override isPhysicalTunerChannel()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 equals(Object o)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 hashCode()281 public int hashCode() { 282 return Objects.hash(mId, mInputId, mIsPassthrough); 283 } 284 285 @Override isBrowsable()286 public boolean isBrowsable() { 287 return mBrowsable; 288 } 289 290 /** Checks whether this channel is searchable or not. */ 291 @Override isSearchable()292 public boolean isSearchable() { 293 return mSearchable; 294 } 295 296 @Override isLocked()297 public boolean isLocked() { 298 return mLocked; 299 } 300 setBrowsable(boolean browsable)301 public void setBrowsable(boolean browsable) { 302 mBrowsable = browsable; 303 } 304 setLocked(boolean locked)305 public void setLocked(boolean locked) { 306 mLocked = locked; 307 } 308 309 /** Sets channel logo uri which is got from cloud. */ setLogoUri(String logoUri)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 hasSameReadOnlyInfo(Channel other)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 toString()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 copyFrom(Channel channel)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") copyFrom(ChannelImpl channel)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. */ createPassthroughChannel(Uri uri)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}. */ createPassthroughChannel(String 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. */ isValid(Channel channel)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 Builder()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 Builder(Channel other)470 public Builder(Channel other) { 471 mChannel = new ChannelImpl(); 472 mChannel.copyFrom(other); 473 } 474 475 @VisibleForTesting setId(long id)476 public Builder setId(long id) { 477 mChannel.mId = id; 478 return this; 479 } 480 481 @VisibleForTesting setPackageName(String packageName)482 public Builder setPackageName(String packageName) { 483 mChannel.mPackageName = packageName; 484 return this; 485 } 486 setInputId(String inputId)487 public Builder setInputId(String inputId) { 488 mChannel.mInputId = inputId; 489 return this; 490 } 491 setType(String type)492 public Builder setType(String type) { 493 mChannel.mType = type; 494 return this; 495 } 496 497 @VisibleForTesting setDisplayNumber(String displayNumber)498 public Builder setDisplayNumber(String displayNumber) { 499 mChannel.mDisplayNumber = normalizeDisplayNumber(displayNumber); 500 return this; 501 } 502 503 @VisibleForTesting setDisplayName(String displayName)504 public Builder setDisplayName(String displayName) { 505 mChannel.mDisplayName = displayName; 506 return this; 507 } 508 509 @VisibleForTesting setDescription(String description)510 public Builder setDescription(String description) { 511 mChannel.mDescription = description; 512 return this; 513 } 514 setVideoFormat(String videoFormat)515 public Builder setVideoFormat(String videoFormat) { 516 mChannel.mVideoFormat = videoFormat; 517 return this; 518 } 519 setBrowsable(boolean browsable)520 public Builder setBrowsable(boolean browsable) { 521 mChannel.mBrowsable = browsable; 522 return this; 523 } 524 setSearchable(boolean searchable)525 public Builder setSearchable(boolean searchable) { 526 mChannel.mSearchable = searchable; 527 return this; 528 } 529 setLocked(boolean locked)530 public Builder setLocked(boolean locked) { 531 mChannel.mLocked = locked; 532 return this; 533 } 534 setPassthrough(boolean isPassthrough)535 public Builder setPassthrough(boolean isPassthrough) { 536 mChannel.mIsPassthrough = isPassthrough; 537 return this; 538 } 539 540 @VisibleForTesting setAppLinkText(String appLinkText)541 public Builder setAppLinkText(String appLinkText) { 542 mChannel.mAppLinkText = appLinkText; 543 return this; 544 } 545 setAppLinkColor(int appLinkColor)546 public Builder setAppLinkColor(int appLinkColor) { 547 mChannel.mAppLinkColor = appLinkColor; 548 return this; 549 } 550 setAppLinkIconUri(String appLinkIconUri)551 public Builder setAppLinkIconUri(String appLinkIconUri) { 552 mChannel.mAppLinkIconUri = appLinkIconUri; 553 return this; 554 } 555 setAppLinkPosterArtUri(String appLinkPosterArtUri)556 public Builder setAppLinkPosterArtUri(String appLinkPosterArtUri) { 557 mChannel.mAppLinkPosterArtUri = appLinkPosterArtUri; 558 return this; 559 } 560 561 @VisibleForTesting setAppLinkIntentUri(String appLinkIntentUri)562 public Builder setAppLinkIntentUri(String appLinkIntentUri) { 563 mChannel.mAppLinkIntentUri = appLinkIntentUri; 564 return this; 565 } 566 setRecordingProhibited(boolean recordingProhibited)567 public Builder setRecordingProhibited(boolean recordingProhibited) { 568 mChannel.mRecordingProhibited = recordingProhibited; 569 return this; 570 } 571 build()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. */ prefetchImage(Context context, int type, int maxWidth, int maxHeight)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 loadBitmap( Context context, final int type, int maxWidth, int maxHeight, ImageLoader.ImageLoaderCallback callback)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 setChannelLogoExist(boolean exist)617 public void setChannelLogoExist(boolean exist) { 618 mChannelLogoExist = exist; 619 } 620 621 /** Returns if channel logo exists. */ channelLogoExists()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 */ getAppLinkType(Context context)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 */ getAppLinkIntent(Context context)644 public Intent getAppLinkIntent(Context context) { 645 if (mAppLinkType == APP_LINK_TYPE_NOT_SET) { 646 initAppLinkTypeAndIntent(context); 647 } 648 return mAppLinkIntent; 649 } 650 initAppLinkTypeAndIntent(Context context)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 getImageUriString(int type)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 DefaultComparator(Context context, TvInputManagerHelper inputManager)712 public DefaultComparator(Context context, TvInputManagerHelper inputManager) { 713 mContext = context; 714 mInputManager = inputManager; 715 } 716 setDetectDuplicatesEnabled(boolean detectDuplicatesEnabled)717 public void setDetectDuplicatesEnabled(boolean detectDuplicatesEnabled) { 718 mDetectDuplicatesEnabled = detectDuplicatesEnabled; 719 } 720 721 @SuppressWarnings("ReferenceEquality") 722 @Override compare(Channel lhs, Channel rhs)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 getInputLabelForChannel(Channel channel)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