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 
createProjection()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      */
fromCursor(Cursor cursor)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      */
fromDvrCursor(Cursor c)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 
Channel()179     private Channel() {
180         // Do nothing.
181     }
182 
getId()183     public long getId() {
184         return mId;
185     }
186 
getUri()187     public Uri getUri() {
188         if (isPassthrough()) {
189             return TvContract.buildChannelUriForPassthroughInput(mInputId);
190         } else {
191             return TvContract.buildChannelUri(mId);
192         }
193     }
194 
getPackageName()195     public String getPackageName() {
196         return mPackageName;
197     }
198 
getInputId()199     public String getInputId() {
200         return mInputId;
201     }
202 
getType()203     public String getType() {
204         return mType;
205     }
206 
getDisplayNumber()207     public String getDisplayNumber() {
208         return mDisplayNumber;
209     }
210 
211     @Nullable
getDisplayName()212     public String getDisplayName() {
213         return mDisplayName;
214     }
215 
216     @VisibleForTesting
getDescription()217     public String getDescription() {
218         return mDescription;
219     }
220 
getVideoFormat()221     public String getVideoFormat() {
222         return mVideoFormat;
223     }
224 
isPassthrough()225     public boolean isPassthrough() {
226         return mIsPassthrough;
227     }
228 
getAppLinkText()229     public String getAppLinkText() {
230         return mAppLinkText;
231     }
232 
getAppLinkColor()233     public int getAppLinkColor() {
234         return mAppLinkColor;
235     }
236 
getAppLinkIconUri()237     public String getAppLinkIconUri() {
238         return mAppLinkIconUri;
239     }
240 
getAppLinkPosterArtUri()241     public String getAppLinkPosterArtUri() {
242         return mAppLinkPosterArtUri;
243     }
244 
getAppLinkIntentUri()245     public String getAppLinkIntentUri() {
246         return mAppLinkIntentUri;
247     }
248 
249     /**
250      * Returns an ID in DVR database.
251      */
getDvrId()252     public long getDvrId() {
253         return mDvrId;
254     }
255 
256     /**
257      * Checks whether this channel is physical tuner channel or not.
258      */
isPhysicalTunerChannel()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
equals(Object o)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
hashCode()278     public int hashCode() {
279         return Objects.hash(mId, mInputId, mIsPassthrough);
280     }
281 
isBrowsable()282     public boolean isBrowsable() {
283         return mBrowsable;
284     }
285 
isLocked()286     public boolean isLocked() {
287         return mLocked;
288     }
289 
setBrowsable(boolean browsable)290     public void setBrowsable(boolean browsable) {
291         mBrowsable = browsable;
292     }
293 
setLocked(boolean locked)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      */
hasSameReadOnlyInfo(Channel other)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
toString()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 
copyFrom(Channel other)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      */
createPassthroughChannel(Uri uri)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      */
createPassthroughChannel(String inputId)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      */
isValid(Channel channel)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 
Builder()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 
Builder(Channel other)413         public Builder(Channel other) {
414             mChannel = new Channel();
415             mChannel.copyFrom(other);
416         }
417 
418         @VisibleForTesting
setId(long id)419         public Builder setId(long id) {
420             mChannel.mId = id;
421             return this;
422         }
423 
424         @VisibleForTesting
setPackageName(String packageName)425         public Builder setPackageName(String packageName) {
426             mChannel.mPackageName = packageName;
427             return this;
428         }
429 
setInputId(String inputId)430         public Builder setInputId(String inputId) {
431             mChannel.mInputId = inputId;
432             return this;
433         }
434 
setType(String type)435         public Builder setType(String type) {
436             mChannel.mType = type;
437             return this;
438         }
439 
440         @VisibleForTesting
setDisplayNumber(String displayNumber)441         public Builder setDisplayNumber(String displayNumber) {
442             mChannel.mDisplayNumber = displayNumber;
443             return this;
444         }
445 
446         @VisibleForTesting
setDisplayName(String displayName)447         public Builder setDisplayName(String displayName) {
448             mChannel.mDisplayName = displayName;
449             return this;
450         }
451 
452         @VisibleForTesting
setDescription(String description)453         public Builder setDescription(String description) {
454             mChannel.mDescription = description;
455             return this;
456         }
457 
setVideoFormat(String videoFormat)458         public Builder setVideoFormat(String videoFormat) {
459             mChannel.mVideoFormat = videoFormat;
460             return this;
461         }
462 
setBrowsable(boolean browsable)463         public Builder setBrowsable(boolean browsable) {
464             mChannel.mBrowsable = browsable;
465             return this;
466         }
467 
setLocked(boolean locked)468         public Builder setLocked(boolean locked) {
469             mChannel.mLocked = locked;
470             return this;
471         }
472 
setPassthrough(boolean isPassthrough)473         public Builder setPassthrough(boolean isPassthrough) {
474             mChannel.mIsPassthrough = isPassthrough;
475             return this;
476         }
477 
478         @VisibleForTesting
setAppLinkText(String appLinkText)479         public Builder setAppLinkText(String appLinkText) {
480             mChannel.mAppLinkText = appLinkText;
481             return this;
482         }
483 
setAppLinkColor(int appLinkColor)484         public Builder setAppLinkColor(int appLinkColor) {
485             mChannel.mAppLinkColor = appLinkColor;
486             return this;
487         }
488 
setAppLinkIconUri(String appLinkIconUri)489         public Builder setAppLinkIconUri(String appLinkIconUri) {
490             mChannel.mAppLinkIconUri = appLinkIconUri;
491             return this;
492         }
493 
setAppLinkPosterArtUri(String appLinkPosterArtUri)494         public Builder setAppLinkPosterArtUri(String appLinkPosterArtUri) {
495             mChannel.mAppLinkPosterArtUri = appLinkPosterArtUri;
496             return this;
497         }
498 
499         @VisibleForTesting
setAppLinkIntentUri(String appLinkIntentUri)500         public Builder setAppLinkIntentUri(String appLinkIntentUri) {
501             mChannel.mAppLinkIntentUri = appLinkIntentUri;
502             return this;
503         }
504 
build()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      */
prefetchImage(Context context, int type, int maxWidth, int maxHeight)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
loadBitmap(Context context, final int type, int maxWidth, int maxHeight, ImageLoader.ImageLoaderCallback callback)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      */
getAppLinkType(Context context)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      */
getAppLinkIntent(Context context)561     public Intent getAppLinkIntent(Context context) {
562         if (mAppLinkType == APP_LINK_TYPE_NOT_SET) {
563             initAppLinkTypeAndIntent(context);
564         }
565         return mAppLinkIntent;
566     }
567 
initAppLinkTypeAndIntent(Context context)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 
getImageUriString(int type)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 
DefaultComparator(Context context, TvInputManagerHelper inputManager)616         public DefaultComparator(Context context, TvInputManagerHelper inputManager) {
617             mContext = context;
618             mInputManager = inputManager;
619         }
620 
setDetectDuplicatesEnabled(boolean detectDuplicatesEnabled)621         public void setDetectDuplicatesEnabled(boolean detectDuplicatesEnabled) {
622             mDetectDuplicatesEnabled = detectDuplicatesEnabled;
623         }
624 
625         @Override
compare(Channel lhs, Channel rhs)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
getInputLabelForChannel(Channel channel)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