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