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 import com.android.tv.common.CommonConstants;
32 import com.android.tv.common.util.CommonUtils;
33 import com.android.tv.data.api.Channel;
34 import com.android.tv.util.TvInputManagerHelper;
35 import com.android.tv.util.Utils;
36 import com.android.tv.util.images.ImageLoader;
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 /** A convenience class to create and insert channel entries into the database. */
44 public final class ChannelImpl implements Channel {
45     private static final String TAG = "ChannelImpl";
46 
47     /** Compares the channel numbers of channels which belong to the same input. */
48     public static final Comparator<Channel> CHANNEL_NUMBER_COMPARATOR =
49             (Channel lhs, Channel rhs) ->
50                     ChannelNumber.compare(lhs.getDisplayNumber(), rhs.getDisplayNumber());
51 
52     private static final int APP_LINK_TYPE_NOT_SET = 0;
53     private static final String INVALID_PACKAGE_NAME = "packageName";
54 
55     public static final String[] PROJECTION = {
56         // Columns must match what is read in ChannelImpl.fromCursor()
57         TvContract.Channels._ID,
58         TvContract.Channels.COLUMN_PACKAGE_NAME,
59         TvContract.Channels.COLUMN_INPUT_ID,
60         TvContract.Channels.COLUMN_TYPE,
61         TvContract.Channels.COLUMN_DISPLAY_NUMBER,
62         TvContract.Channels.COLUMN_DISPLAY_NAME,
63         TvContract.Channels.COLUMN_DESCRIPTION,
64         TvContract.Channels.COLUMN_VIDEO_FORMAT,
65         TvContract.Channels.COLUMN_BROWSABLE,
66         TvContract.Channels.COLUMN_SEARCHABLE,
67         TvContract.Channels.COLUMN_LOCKED,
68         TvContract.Channels.COLUMN_APP_LINK_TEXT,
69         TvContract.Channels.COLUMN_APP_LINK_COLOR,
70         TvContract.Channels.COLUMN_APP_LINK_ICON_URI,
71         TvContract.Channels.COLUMN_APP_LINK_POSTER_ART_URI,
72         TvContract.Channels.COLUMN_APP_LINK_INTENT_URI,
73         TvContract.Channels.COLUMN_NETWORK_AFFILIATION,
74         TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG2, // Only used in bundled input
75     };
76 
77     /**
78      * Creates {@code ChannelImpl} object from cursor.
79      *
80      * <p>The query that created the cursor MUST use {@link #PROJECTION}
81      */
fromCursor(Cursor cursor)82     public static ChannelImpl fromCursor(Cursor cursor) {
83         // Columns read must match the order of {@link #PROJECTION}
84         ChannelImpl channel = new ChannelImpl();
85         int index = 0;
86         channel.mId = cursor.getLong(index++);
87         channel.mPackageName = Utils.intern(cursor.getString(index++));
88         channel.mInputId = Utils.intern(cursor.getString(index++));
89         channel.mType = Utils.intern(cursor.getString(index++));
90         channel.mDisplayNumber = normalizeDisplayNumber(cursor.getString(index++));
91         channel.mDisplayName = cursor.getString(index++);
92         channel.mDescription = cursor.getString(index++);
93         channel.mVideoFormat = Utils.intern(cursor.getString(index++));
94         channel.mBrowsable = cursor.getInt(index++) == 1;
95         channel.mSearchable = cursor.getInt(index++) == 1;
96         channel.mLocked = cursor.getInt(index++) == 1;
97         channel.mAppLinkText = cursor.getString(index++);
98         channel.mAppLinkColor = cursor.getInt(index++);
99         channel.mAppLinkIconUri = cursor.getString(index++);
100         channel.mAppLinkPosterArtUri = cursor.getString(index++);
101         channel.mAppLinkIntentUri = cursor.getString(index++);
102         channel.mNetworkAffiliation = cursor.getString(index++);
103         if (CommonUtils.isBundledInput(channel.mInputId)) {
104             channel.mRecordingProhibited = cursor.getInt(index++) != 0;
105         }
106         return channel;
107     }
108 
109     /** Replaces the channel number separator with dash('-'). */
normalizeDisplayNumber(String string)110     public static String normalizeDisplayNumber(String string) {
111         if (!TextUtils.isEmpty(string)) {
112             int length = string.length();
113             for (int i = 0; i < length; i++) {
114                 char c = string.charAt(i);
115                 if (c == '.'
116                         || Character.isWhitespace(c)
117                         || Character.getType(c) == Character.DASH_PUNCTUATION) {
118                     StringBuilder sb = new StringBuilder(string);
119                     sb.setCharAt(i, CHANNEL_NUMBER_DELIMITER);
120                     return sb.toString();
121                 }
122             }
123         }
124         return string;
125     }
126 
127     /** ID of this channel. Matches to BaseColumns._ID. */
128     private long mId;
129 
130     private String mPackageName;
131     private String mInputId;
132     private String mType;
133     private String mDisplayNumber;
134     private String mDisplayName;
135     private String mDescription;
136     private String mVideoFormat;
137     private boolean mBrowsable;
138     private boolean mSearchable;
139     private boolean mLocked;
140     private boolean mIsPassthrough;
141     private String mAppLinkText;
142     private int mAppLinkColor;
143     private String mAppLinkIconUri;
144     private String mAppLinkPosterArtUri;
145     private String mAppLinkIntentUri;
146     private Intent mAppLinkIntent;
147     private String mNetworkAffiliation;
148     private int mAppLinkType;
149     private String mLogoUri;
150     private boolean mRecordingProhibited;
151 
152     private boolean mChannelLogoExist;
153 
ChannelImpl()154     private ChannelImpl() {
155         // Do nothing.
156     }
157 
158     @Override
getId()159     public long getId() {
160         return mId;
161     }
162 
163     @Override
getUri()164     public Uri getUri() {
165         if (isPassthrough()) {
166             return TvContract.buildChannelUriForPassthroughInput(mInputId);
167         } else {
168             return TvContract.buildChannelUri(mId);
169         }
170     }
171 
172     @Override
getPackageName()173     public String getPackageName() {
174         return mPackageName;
175     }
176 
177     @Override
getInputId()178     public String getInputId() {
179         return mInputId;
180     }
181 
182     @Override
getType()183     public String getType() {
184         return mType;
185     }
186 
187     @Override
getDisplayNumber()188     public String getDisplayNumber() {
189         return mDisplayNumber;
190     }
191 
192     @Override
193     @Nullable
getDisplayName()194     public String getDisplayName() {
195         return mDisplayName;
196     }
197 
198     @Override
getDescription()199     public String getDescription() {
200         return mDescription;
201     }
202 
203     @Override
getVideoFormat()204     public String getVideoFormat() {
205         return mVideoFormat;
206     }
207 
208     @Override
isPassthrough()209     public boolean isPassthrough() {
210         return mIsPassthrough;
211     }
212 
213     /**
214      * Gets identification text for displaying or debugging. It's made from Channels' display number
215      * plus their display name.
216      */
217     @Override
getDisplayText()218     public String getDisplayText() {
219         return TextUtils.isEmpty(mDisplayName)
220                 ? mDisplayNumber
221                 : mDisplayNumber + " " + mDisplayName;
222     }
223 
224     @Override
getAppLinkText()225     public String getAppLinkText() {
226         return mAppLinkText;
227     }
228 
229     @Override
getAppLinkColor()230     public int getAppLinkColor() {
231         return mAppLinkColor;
232     }
233 
234     @Override
getAppLinkIconUri()235     public String getAppLinkIconUri() {
236         return mAppLinkIconUri;
237     }
238 
239     @Override
getAppLinkPosterArtUri()240     public String getAppLinkPosterArtUri() {
241         return mAppLinkPosterArtUri;
242     }
243 
244     @Override
getAppLinkIntentUri()245     public String getAppLinkIntentUri() {
246         return mAppLinkIntentUri;
247     }
248 
249     @Override
getNetworkAffiliation()250     public String getNetworkAffiliation() {
251         return mNetworkAffiliation;
252     }
253 
254     /** Returns channel logo uri which is got from cloud, it's used only for ChannelLogoFetcher. */
255     @Override
getLogoUri()256     public String getLogoUri() {
257         return mLogoUri;
258     }
259 
260     @Override
isRecordingProhibited()261     public boolean isRecordingProhibited() {
262         return mRecordingProhibited;
263     }
264 
265     /** Checks whether this channel is physical tuner channel or not. */
266     @Override
isPhysicalTunerChannel()267     public boolean isPhysicalTunerChannel() {
268         return !TextUtils.isEmpty(mType) && !TvContract.Channels.TYPE_OTHER.equals(mType);
269     }
270 
271     /** Checks if two channels equal by checking ids. */
272     @Override
equals(Object o)273     public boolean equals(Object o) {
274         if (!(o instanceof ChannelImpl)) {
275             return false;
276         }
277         ChannelImpl other = (ChannelImpl) o;
278         // All pass-through TV channels have INVALID_ID value for mId.
279         return mId == other.mId
280                 && TextUtils.equals(mInputId, other.mInputId)
281                 && mIsPassthrough == other.mIsPassthrough;
282     }
283 
284     @Override
hashCode()285     public int hashCode() {
286         return Objects.hash(mId, mInputId, mIsPassthrough);
287     }
288 
289     @Override
isBrowsable()290     public boolean isBrowsable() {
291         return mBrowsable;
292     }
293 
294     /** Checks whether this channel is searchable or not. */
295     @Override
isSearchable()296     public boolean isSearchable() {
297         return mSearchable;
298     }
299 
300     @Override
isLocked()301     public boolean isLocked() {
302         return mLocked;
303     }
304 
setBrowsable(boolean browsable)305     public void setBrowsable(boolean browsable) {
306         mBrowsable = browsable;
307     }
308 
setLocked(boolean locked)309     public void setLocked(boolean locked) {
310         mLocked = locked;
311     }
312 
313     /** Sets channel logo uri which is got from cloud. */
setLogoUri(String logoUri)314     public void setLogoUri(String logoUri) {
315         mLogoUri = logoUri;
316     }
317 
318     @Override
setNetworkAffiliation(String networkAffiliation)319     public void setNetworkAffiliation(String networkAffiliation) {
320         mNetworkAffiliation = networkAffiliation;
321     }
322 
323     /**
324      * Check whether {@code other} has same read-only channel info as this. But, it cannot check two
325      * channels have same logos. It also excludes browsable and locked, because two fields are
326      * changed by TV app.
327      */
328     @Override
hasSameReadOnlyInfo(Channel other)329     public boolean hasSameReadOnlyInfo(Channel other) {
330         return other != null
331                 && Objects.equals(mId, other.getId())
332                 && Objects.equals(mPackageName, other.getPackageName())
333                 && Objects.equals(mInputId, other.getInputId())
334                 && Objects.equals(mType, other.getType())
335                 && Objects.equals(mDisplayNumber, other.getDisplayNumber())
336                 && Objects.equals(mDisplayName, other.getDisplayName())
337                 && Objects.equals(mDescription, other.getDescription())
338                 && Objects.equals(mVideoFormat, other.getVideoFormat())
339                 && mIsPassthrough == other.isPassthrough()
340                 && Objects.equals(mAppLinkText, other.getAppLinkText())
341                 && mAppLinkColor == other.getAppLinkColor()
342                 && Objects.equals(mAppLinkIconUri, other.getAppLinkIconUri())
343                 && Objects.equals(mAppLinkPosterArtUri, other.getAppLinkPosterArtUri())
344                 && Objects.equals(mAppLinkIntentUri, other.getAppLinkIntentUri())
345                 && Objects.equals(mRecordingProhibited, other.isRecordingProhibited());
346     }
347 
348     @Override
toString()349     public String toString() {
350         return "Channel{"
351                 + "id="
352                 + mId
353                 + ", packageName="
354                 + mPackageName
355                 + ", inputId="
356                 + mInputId
357                 + ", type="
358                 + mType
359                 + ", displayNumber="
360                 + mDisplayNumber
361                 + ", displayName="
362                 + mDisplayName
363                 + ", description="
364                 + mDescription
365                 + ", videoFormat="
366                 + mVideoFormat
367                 + ", isPassthrough="
368                 + mIsPassthrough
369                 + ", browsable="
370                 + mBrowsable
371                 + ", searchable="
372                 + mSearchable
373                 + ", locked="
374                 + mLocked
375                 + ", appLinkText="
376                 + mAppLinkText
377                 + ", recordingProhibited="
378                 + mRecordingProhibited
379                 + "}";
380     }
381 
382     @Override
copyFrom(Channel channel)383     public void copyFrom(Channel channel) {
384         if (channel instanceof ChannelImpl) {
385             copyFrom((ChannelImpl) channel);
386         } else {
387             // copy what we can
388             mId = channel.getId();
389             mPackageName = channel.getPackageName();
390             mInputId = channel.getInputId();
391             mType = channel.getType();
392             mDisplayNumber = channel.getDisplayNumber();
393             mDisplayName = channel.getDisplayName();
394             mDescription = channel.getDescription();
395             mVideoFormat = channel.getVideoFormat();
396             mIsPassthrough = channel.isPassthrough();
397             mBrowsable = channel.isBrowsable();
398             mSearchable = channel.isSearchable();
399             mLocked = channel.isLocked();
400             mAppLinkText = channel.getAppLinkText();
401             mAppLinkColor = channel.getAppLinkColor();
402             mAppLinkIconUri = channel.getAppLinkIconUri();
403             mAppLinkPosterArtUri = channel.getAppLinkPosterArtUri();
404             mAppLinkIntentUri = channel.getAppLinkIntentUri();
405             mNetworkAffiliation = channel.getNetworkAffiliation();
406             mRecordingProhibited = channel.isRecordingProhibited();
407             mChannelLogoExist = channel.channelLogoExists();
408             mNetworkAffiliation = channel.getNetworkAffiliation();
409         }
410     }
411 
412     @SuppressWarnings("ReferenceEquality")
copyFrom(ChannelImpl channel)413     public void copyFrom(ChannelImpl channel) {
414         ChannelImpl other = (ChannelImpl) channel;
415         if (this == other) {
416             return;
417         }
418         mId = other.mId;
419         mPackageName = other.mPackageName;
420         mInputId = other.mInputId;
421         mType = other.mType;
422         mDisplayNumber = other.mDisplayNumber;
423         mDisplayName = other.mDisplayName;
424         mDescription = other.mDescription;
425         mVideoFormat = other.mVideoFormat;
426         mIsPassthrough = other.mIsPassthrough;
427         mBrowsable = other.mBrowsable;
428         mSearchable = other.mSearchable;
429         mLocked = other.mLocked;
430         mAppLinkText = other.mAppLinkText;
431         mAppLinkColor = other.mAppLinkColor;
432         mAppLinkIconUri = other.mAppLinkIconUri;
433         mAppLinkPosterArtUri = other.mAppLinkPosterArtUri;
434         mAppLinkIntentUri = other.mAppLinkIntentUri;
435         mNetworkAffiliation = channel.mNetworkAffiliation;
436         mAppLinkIntent = other.mAppLinkIntent;
437         mAppLinkType = other.mAppLinkType;
438         mRecordingProhibited = other.mRecordingProhibited;
439         mChannelLogoExist = other.mChannelLogoExist;
440     }
441 
442     /** Creates a channel for a passthrough TV input. */
createPassthroughChannel(Uri uri)443     public static ChannelImpl createPassthroughChannel(Uri uri) {
444         if (!TvContract.isChannelUriForPassthroughInput(uri)) {
445             throw new IllegalArgumentException("URI is not a passthrough channel URI");
446         }
447         String inputId = uri.getPathSegments().get(1);
448         return createPassthroughChannel(inputId);
449     }
450 
451     /** Creates a channel for a passthrough TV input with {@code inputId}. */
createPassthroughChannel(String inputId)452     public static ChannelImpl createPassthroughChannel(String inputId) {
453         return new Builder().setInputId(inputId).setPassthrough(true).build();
454     }
455 
456     /** Checks whether the channel is valid or not. */
isValid(Channel channel)457     public static boolean isValid(Channel channel) {
458         return channel != null && (channel.getId() != INVALID_ID || channel.isPassthrough());
459     }
460 
461     /**
462      * Builder class for {@code ChannelImpl}. Suppress using this outside of ChannelDataManager so
463      * Channels could be managed by ChannelDataManager.
464      */
465     public static final class Builder {
466         private final ChannelImpl mChannel;
467 
Builder()468         public Builder() {
469             mChannel = new ChannelImpl();
470             // Fill initial data.
471             mChannel.mId = INVALID_ID;
472             mChannel.mPackageName = INVALID_PACKAGE_NAME;
473             mChannel.mInputId = "inputId";
474             mChannel.mType = "type";
475             mChannel.mDisplayNumber = "0";
476             mChannel.mDisplayName = "name";
477             mChannel.mDescription = "description";
478             mChannel.mBrowsable = true;
479             mChannel.mSearchable = true;
480         }
481 
Builder(Channel other)482         public Builder(Channel other) {
483             mChannel = new ChannelImpl();
484             mChannel.copyFrom(other);
485         }
486 
487         @VisibleForTesting
setId(long id)488         public Builder setId(long id) {
489             mChannel.mId = id;
490             return this;
491         }
492 
493         @VisibleForTesting
setPackageName(String packageName)494         public Builder setPackageName(String packageName) {
495             mChannel.mPackageName = packageName;
496             return this;
497         }
498 
setInputId(String inputId)499         public Builder setInputId(String inputId) {
500             mChannel.mInputId = inputId;
501             return this;
502         }
503 
setType(String type)504         public Builder setType(String type) {
505             mChannel.mType = type;
506             return this;
507         }
508 
509         @VisibleForTesting
setDisplayNumber(String displayNumber)510         public Builder setDisplayNumber(String displayNumber) {
511             mChannel.mDisplayNumber = normalizeDisplayNumber(displayNumber);
512             return this;
513         }
514 
515         @VisibleForTesting
setDisplayName(String displayName)516         public Builder setDisplayName(String displayName) {
517             mChannel.mDisplayName = displayName;
518             return this;
519         }
520 
521         @VisibleForTesting
setDescription(String description)522         public Builder setDescription(String description) {
523             mChannel.mDescription = description;
524             return this;
525         }
526 
setVideoFormat(String videoFormat)527         public Builder setVideoFormat(String videoFormat) {
528             mChannel.mVideoFormat = videoFormat;
529             return this;
530         }
531 
setBrowsable(boolean browsable)532         public Builder setBrowsable(boolean browsable) {
533             mChannel.mBrowsable = browsable;
534             return this;
535         }
536 
setSearchable(boolean searchable)537         public Builder setSearchable(boolean searchable) {
538             mChannel.mSearchable = searchable;
539             return this;
540         }
541 
setLocked(boolean locked)542         public Builder setLocked(boolean locked) {
543             mChannel.mLocked = locked;
544             return this;
545         }
546 
setPassthrough(boolean isPassthrough)547         public Builder setPassthrough(boolean isPassthrough) {
548             mChannel.mIsPassthrough = isPassthrough;
549             return this;
550         }
551 
552         @VisibleForTesting
setAppLinkText(String appLinkText)553         public Builder setAppLinkText(String appLinkText) {
554             mChannel.mAppLinkText = appLinkText;
555             return this;
556         }
557 
558         @VisibleForTesting
setNetworkAffiliation(String networkAffiliation)559         public Builder setNetworkAffiliation(String networkAffiliation) {
560             mChannel.mNetworkAffiliation = networkAffiliation;
561             return this;
562         }
563 
setAppLinkColor(int appLinkColor)564         public Builder setAppLinkColor(int appLinkColor) {
565             mChannel.mAppLinkColor = appLinkColor;
566             return this;
567         }
568 
setAppLinkIconUri(String appLinkIconUri)569         public Builder setAppLinkIconUri(String appLinkIconUri) {
570             mChannel.mAppLinkIconUri = appLinkIconUri;
571             return this;
572         }
573 
setAppLinkPosterArtUri(String appLinkPosterArtUri)574         public Builder setAppLinkPosterArtUri(String appLinkPosterArtUri) {
575             mChannel.mAppLinkPosterArtUri = appLinkPosterArtUri;
576             return this;
577         }
578 
579         @VisibleForTesting
setAppLinkIntentUri(String appLinkIntentUri)580         public Builder setAppLinkIntentUri(String appLinkIntentUri) {
581             mChannel.mAppLinkIntentUri = appLinkIntentUri;
582             return this;
583         }
584 
setRecordingProhibited(boolean recordingProhibited)585         public Builder setRecordingProhibited(boolean recordingProhibited) {
586             mChannel.mRecordingProhibited = recordingProhibited;
587             return this;
588         }
589 
build()590         public ChannelImpl build() {
591             ChannelImpl channel = new ChannelImpl();
592             channel.copyFrom(mChannel);
593             return channel;
594         }
595     }
596 
597     /** Prefetches the images for this channel. */
prefetchImage(Context context, int type, int maxWidth, int maxHeight)598     public void prefetchImage(Context context, int type, int maxWidth, int maxHeight) {
599         String uriString = getImageUriString(type);
600         if (!TextUtils.isEmpty(uriString)) {
601             ImageLoader.prefetchBitmap(context, uriString, maxWidth, maxHeight);
602         }
603     }
604 
605     /**
606      * Loads the bitmap of this channel and returns it via {@code callback}. The loaded bitmap will
607      * be cached and resized with given params.
608      *
609      * <p>Note that it may directly call {@code callback} if the bitmap is already loaded.
610      *
611      * @param context A context.
612      * @param type The type of bitmap which will be loaded. It should be one of follows: {@link
613      *     Channel#LOAD_IMAGE_TYPE_CHANNEL_LOGO}, {@link Channel#LOAD_IMAGE_TYPE_APP_LINK_ICON}, or
614      *     {@link Channel#LOAD_IMAGE_TYPE_APP_LINK_POSTER_ART}.
615      * @param maxWidth The max width of the loaded bitmap.
616      * @param maxHeight The max height of the loaded bitmap.
617      * @param callback A callback which will be called after the loading finished.
618      */
619     @UiThread
loadBitmap( Context context, final int type, int maxWidth, int maxHeight, ImageLoader.ImageLoaderCallback callback)620     public void loadBitmap(
621             Context context,
622             final int type,
623             int maxWidth,
624             int maxHeight,
625             ImageLoader.ImageLoaderCallback callback) {
626         String uriString = getImageUriString(type);
627         ImageLoader.loadBitmap(context, uriString, maxWidth, maxHeight, callback);
628     }
629 
630     /**
631      * Sets if the channel logo exists. This method should be only called from {@link
632      * ChannelDataManager}.
633      */
634     @Override
setChannelLogoExist(boolean exist)635     public void setChannelLogoExist(boolean exist) {
636         mChannelLogoExist = exist;
637     }
638 
639     /** Returns if channel logo exists. */
channelLogoExists()640     public boolean channelLogoExists() {
641         return mChannelLogoExist;
642     }
643 
644     /**
645      * Returns the type of app link for this channel. It returns {@link
646      * Channel#APP_LINK_TYPE_CHANNEL} if the channel has a non null app link text and a valid app
647      * link intent, it returns {@link Channel#APP_LINK_TYPE_APP} if the input service which holds
648      * the channel has leanback launch intent, and it returns {@link Channel#APP_LINK_TYPE_NONE}
649      * otherwise.
650      */
getAppLinkType(Context context)651     public int getAppLinkType(Context context) {
652         if (mAppLinkType == APP_LINK_TYPE_NOT_SET) {
653             initAppLinkTypeAndIntent(context);
654         }
655         return mAppLinkType;
656     }
657 
658     /**
659      * Returns the app link intent for this channel. If the type of app link is {@link
660      * Channel#APP_LINK_TYPE_NONE}, it returns {@code null}.
661      */
getAppLinkIntent(Context context)662     public Intent getAppLinkIntent(Context context) {
663         if (mAppLinkType == APP_LINK_TYPE_NOT_SET) {
664             initAppLinkTypeAndIntent(context);
665         }
666         return mAppLinkIntent;
667     }
668 
initAppLinkTypeAndIntent(Context context)669     private void initAppLinkTypeAndIntent(Context context) {
670         mAppLinkType = APP_LINK_TYPE_NONE;
671         mAppLinkIntent = null;
672         PackageManager pm = context.getPackageManager();
673         if (!TextUtils.isEmpty(mAppLinkText) && !TextUtils.isEmpty(mAppLinkIntentUri)) {
674             try {
675                 Intent intent = Intent.parseUri(mAppLinkIntentUri, Intent.URI_INTENT_SCHEME);
676                 if (intent.resolveActivityInfo(pm, 0) != null) {
677                     mAppLinkIntent = intent;
678                     mAppLinkIntent.putExtra(
679                             CommonConstants.EXTRA_APP_LINK_CHANNEL_URI, getUri().toString());
680                     mAppLinkType = APP_LINK_TYPE_CHANNEL;
681                     return;
682                 } else {
683                     Log.w(TAG, "No activity exists to handle : " + mAppLinkIntentUri);
684                 }
685             } catch (URISyntaxException e) {
686                 Log.w(TAG, "Unable to set app link for " + mAppLinkIntentUri, e);
687                 // Do nothing.
688             }
689         }
690         if (mPackageName.equals(context.getApplicationContext().getPackageName())) {
691             return;
692         }
693         mAppLinkIntent = pm.getLeanbackLaunchIntentForPackage(mPackageName);
694         if (mAppLinkIntent != null) {
695             mAppLinkIntent.putExtra(
696                     CommonConstants.EXTRA_APP_LINK_CHANNEL_URI, getUri().toString());
697             mAppLinkType = APP_LINK_TYPE_APP;
698         }
699     }
700 
getImageUriString(int type)701     private String getImageUriString(int type) {
702         switch (type) {
703             case LOAD_IMAGE_TYPE_CHANNEL_LOGO:
704                 return TvContract.buildChannelLogoUri(mId).toString();
705             case LOAD_IMAGE_TYPE_APP_LINK_ICON:
706                 return mAppLinkIconUri;
707             case LOAD_IMAGE_TYPE_APP_LINK_POSTER_ART:
708                 return mAppLinkPosterArtUri;
709         }
710         return null;
711     }
712 
713     /**
714      * Default Channel ordering.
715      *
716      * <p>Ordering
717      * <li>{@link TvInputManagerHelper#isPartnerInput(String)}
718      * <li>{@link #getInputLabelForChannel(Channel)}
719      * <li>{@link #getInputId()}
720      * <li>{@link ChannelNumber#compare(String, String)}
721      * <li>
722      * </ol>
723      */
724     public static class DefaultComparator implements Comparator<Channel> {
725         private final Context mContext;
726         private final TvInputManagerHelper mInputManager;
727         private final Map<String, String> mInputIdToLabelMap = new HashMap<>();
728         private boolean mDetectDuplicatesEnabled;
729 
DefaultComparator(Context context, TvInputManagerHelper inputManager)730         public DefaultComparator(Context context, TvInputManagerHelper inputManager) {
731             mContext = context;
732             mInputManager = inputManager;
733         }
734 
setDetectDuplicatesEnabled(boolean detectDuplicatesEnabled)735         public void setDetectDuplicatesEnabled(boolean detectDuplicatesEnabled) {
736             mDetectDuplicatesEnabled = detectDuplicatesEnabled;
737         }
738 
739         @SuppressWarnings("ReferenceEquality")
740         @Override
compare(Channel lhs, Channel rhs)741         public int compare(Channel lhs, Channel rhs) {
742             if (lhs == rhs) {
743                 return 0;
744             }
745             // Put channels from OEM/SOC inputs first.
746             boolean lhsIsPartner = mInputManager.isPartnerInput(lhs.getInputId());
747             boolean rhsIsPartner = mInputManager.isPartnerInput(rhs.getInputId());
748             if (lhsIsPartner != rhsIsPartner) {
749                 return lhsIsPartner ? -1 : 1;
750             }
751             // Compare the input labels.
752             String lhsLabel = getInputLabelForChannel(lhs);
753             String rhsLabel = getInputLabelForChannel(rhs);
754             int result =
755                     lhsLabel == null
756                             ? (rhsLabel == null ? 0 : 1)
757                             : rhsLabel == null ? -1 : lhsLabel.compareTo(rhsLabel);
758             if (result != 0) {
759                 return result;
760             }
761             // Compare the input IDs. The input IDs cannot be null.
762             result = lhs.getInputId().compareTo(rhs.getInputId());
763             if (result != 0) {
764                 return result;
765             }
766             // Compare the channel numbers if both channels belong to the same input.
767             result = ChannelNumber.compare(lhs.getDisplayNumber(), rhs.getDisplayNumber());
768             if (mDetectDuplicatesEnabled && result == 0) {
769                 Log.w(
770                         TAG,
771                         "Duplicate channels detected! - \""
772                                 + lhs.getDisplayText()
773                                 + "\" and \""
774                                 + rhs.getDisplayText()
775                                 + "\"");
776             }
777             return result;
778         }
779 
780         @VisibleForTesting
getInputLabelForChannel(Channel channel)781         String getInputLabelForChannel(Channel channel) {
782             String label = mInputIdToLabelMap.get(channel.getInputId());
783             if (label == null) {
784                 TvInputInfo info = mInputManager.getTvInputInfo(channel.getInputId());
785                 if (info != null) {
786                     label = Utils.loadLabel(mContext, info);
787                     if (label != null) {
788                         mInputIdToLabelMap.put(channel.getInputId(), label);
789                     }
790                 }
791             }
792             return label;
793         }
794     }
795 }
796