• 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      * 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      */
fromCursor(Cursor cursor)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      */
normalizeDisplayNumber(String string)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 
Channel()186     private Channel() {
187         // Do nothing.
188     }
189 
getId()190     public long getId() {
191         return mId;
192     }
193 
getUri()194     public Uri getUri() {
195         if (isPassthrough()) {
196             return TvContract.buildChannelUriForPassthroughInput(mInputId);
197         } else {
198             return TvContract.buildChannelUri(mId);
199         }
200     }
201 
getPackageName()202     public String getPackageName() {
203         return mPackageName;
204     }
205 
getInputId()206     public String getInputId() {
207         return mInputId;
208     }
209 
getType()210     public String getType() {
211         return mType;
212     }
213 
getDisplayNumber()214     public String getDisplayNumber() {
215         return mDisplayNumber;
216     }
217 
218     @Nullable
getDisplayName()219     public String getDisplayName() {
220         return mDisplayName;
221     }
222 
getDescription()223     public String getDescription() {
224         return mDescription;
225     }
226 
getVideoFormat()227     public String getVideoFormat() {
228         return mVideoFormat;
229     }
230 
isPassthrough()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      */
getDisplayText()239     public String getDisplayText() {
240         return TextUtils.isEmpty(mDisplayName) ? mDisplayNumber
241                 : mDisplayNumber + " " + mDisplayName;
242     }
243 
getAppLinkText()244     public String getAppLinkText() {
245         return mAppLinkText;
246     }
247 
getAppLinkColor()248     public int getAppLinkColor() {
249         return mAppLinkColor;
250     }
251 
getAppLinkIconUri()252     public String getAppLinkIconUri() {
253         return mAppLinkIconUri;
254     }
255 
getAppLinkPosterArtUri()256     public String getAppLinkPosterArtUri() {
257         return mAppLinkPosterArtUri;
258     }
259 
getAppLinkIntentUri()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      */
getLogoUri()267     public String getLogoUri() {
268         return mLogoUri;
269     }
270 
isRecordingProhibited()271     public boolean isRecordingProhibited() {
272         return mRecordingProhibited;
273     }
274 
275     /**
276      * Checks whether this channel is physical tuner channel or not.
277      */
isPhysicalTunerChannel()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
equals(Object o)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
hashCode()297     public int hashCode() {
298         return Objects.hash(mId, mInputId, mIsPassthrough);
299     }
300 
isBrowsable()301     public boolean isBrowsable() {
302         return mBrowsable;
303     }
304 
305     /** Checks whether this channel is searchable or not. */
isSearchable()306     public boolean isSearchable() {
307         return mSearchable;
308     }
309 
isLocked()310     public boolean isLocked() {
311         return mLocked;
312     }
313 
setBrowsable(boolean browsable)314     public void setBrowsable(boolean browsable) {
315         mBrowsable = browsable;
316     }
317 
setLocked(boolean locked)318     public void setLocked(boolean locked) {
319         mLocked = locked;
320     }
321 
322     /**
323      * Sets channel logo uri which is got from cloud.
324      */
setLogoUri(String logoUri)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      */
hasSameReadOnlyInfo(Channel other)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
toString()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 
copyFrom(Channel other)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      */
createPassthroughChannel(Uri uri)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      */
createPassthroughChannel(String inputId)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      */
isValid(Channel channel)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 
Builder()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 
Builder(Channel other)449         public Builder(Channel other) {
450             mChannel = new Channel();
451             mChannel.copyFrom(other);
452         }
453 
454         @VisibleForTesting
setId(long id)455         public Builder setId(long id) {
456             mChannel.mId = id;
457             return this;
458         }
459 
460         @VisibleForTesting
setPackageName(String packageName)461         public Builder setPackageName(String packageName) {
462             mChannel.mPackageName = packageName;
463             return this;
464         }
465 
setInputId(String inputId)466         public Builder setInputId(String inputId) {
467             mChannel.mInputId = inputId;
468             return this;
469         }
470 
setType(String type)471         public Builder setType(String type) {
472             mChannel.mType = type;
473             return this;
474         }
475 
476         @VisibleForTesting
setDisplayNumber(String displayNumber)477         public Builder setDisplayNumber(String displayNumber) {
478             mChannel.mDisplayNumber = normalizeDisplayNumber(displayNumber);
479             return this;
480         }
481 
482         @VisibleForTesting
setDisplayName(String displayName)483         public Builder setDisplayName(String displayName) {
484             mChannel.mDisplayName = displayName;
485             return this;
486         }
487 
488         @VisibleForTesting
setDescription(String description)489         public Builder setDescription(String description) {
490             mChannel.mDescription = description;
491             return this;
492         }
493 
setVideoFormat(String videoFormat)494         public Builder setVideoFormat(String videoFormat) {
495             mChannel.mVideoFormat = videoFormat;
496             return this;
497         }
498 
setBrowsable(boolean browsable)499         public Builder setBrowsable(boolean browsable) {
500             mChannel.mBrowsable = browsable;
501             return this;
502         }
503 
setSearchable(boolean searchable)504         public Builder setSearchable(boolean searchable) {
505             mChannel.mSearchable = searchable;
506             return this;
507         }
508 
setLocked(boolean locked)509         public Builder setLocked(boolean locked) {
510             mChannel.mLocked = locked;
511             return this;
512         }
513 
setPassthrough(boolean isPassthrough)514         public Builder setPassthrough(boolean isPassthrough) {
515             mChannel.mIsPassthrough = isPassthrough;
516             return this;
517         }
518 
519         @VisibleForTesting
setAppLinkText(String appLinkText)520         public Builder setAppLinkText(String appLinkText) {
521             mChannel.mAppLinkText = appLinkText;
522             return this;
523         }
524 
setAppLinkColor(int appLinkColor)525         public Builder setAppLinkColor(int appLinkColor) {
526             mChannel.mAppLinkColor = appLinkColor;
527             return this;
528         }
529 
setAppLinkIconUri(String appLinkIconUri)530         public Builder setAppLinkIconUri(String appLinkIconUri) {
531             mChannel.mAppLinkIconUri = appLinkIconUri;
532             return this;
533         }
534 
setAppLinkPosterArtUri(String appLinkPosterArtUri)535         public Builder setAppLinkPosterArtUri(String appLinkPosterArtUri) {
536             mChannel.mAppLinkPosterArtUri = appLinkPosterArtUri;
537             return this;
538         }
539 
540         @VisibleForTesting
setAppLinkIntentUri(String appLinkIntentUri)541         public Builder setAppLinkIntentUri(String appLinkIntentUri) {
542             mChannel.mAppLinkIntentUri = appLinkIntentUri;
543             return this;
544         }
545 
setRecordingProhibited(boolean recordingProhibited)546         public Builder setRecordingProhibited(boolean recordingProhibited) {
547             mChannel.mRecordingProhibited = recordingProhibited;
548             return this;
549         }
550 
build()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      */
prefetchImage(Context context, int type, int maxWidth, int maxHeight)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
loadBitmap(Context context, final int type, int maxWidth, int maxHeight, ImageLoader.ImageLoaderCallback callback)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      */
setChannelLogoExist(boolean exist)593     void setChannelLogoExist(boolean exist) {
594         mChannelLogoExist = exist;
595     }
596 
597     /**
598      * Returns if channel logo exists.
599      */
channelLogoExists()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      */
getAppLinkType(Context context)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      */
getAppLinkIntent(Context context)622     public Intent getAppLinkIntent(Context context) {
623         if (mAppLinkType == APP_LINK_TYPE_NOT_SET) {
624             initAppLinkTypeAndIntent(context);
625         }
626         return mAppLinkIntent;
627     }
628 
initAppLinkTypeAndIntent(Context context)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 
getImageUriString(int type)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 
DefaultComparator(Context context, TvInputManagerHelper inputManager)679         public DefaultComparator(Context context, TvInputManagerHelper inputManager) {
680             mContext = context;
681             mInputManager = inputManager;
682         }
683 
setDetectDuplicatesEnabled(boolean detectDuplicatesEnabled)684         public void setDetectDuplicatesEnabled(boolean detectDuplicatesEnabled) {
685             mDetectDuplicatesEnabled = detectDuplicatesEnabled;
686         }
687 
688         @Override
compare(Channel lhs, Channel rhs)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
getInputLabelForChannel(Channel channel)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 }