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