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