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