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
17package com.android.tv.data;
18
19import android.content.Context;
20import android.content.Intent;
21import android.content.pm.PackageManager;
22import android.database.Cursor;
23import android.media.tv.TvContract;
24import android.media.tv.TvInputInfo;
25import android.net.Uri;
26import android.support.annotation.Nullable;
27import android.support.annotation.UiThread;
28import android.support.annotation.VisibleForTesting;
29import android.text.TextUtils;
30import android.util.Log;
31
32import com.android.tv.common.TvCommonConstants;
33import com.android.tv.util.ImageLoader;
34import com.android.tv.util.TvInputManagerHelper;
35import com.android.tv.util.Utils;
36
37import java.net.URISyntaxException;
38import java.util.Comparator;
39import java.util.HashMap;
40import java.util.Map;
41import java.util.Objects;
42
43/**
44 * A convenience class to create and insert channel entries into the database.
45 */
46public 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}