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