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