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.util;
18 
19 import android.content.Context;
20 import android.content.pm.ApplicationInfo;
21 import android.content.pm.PackageManager;
22 import android.database.ContentObserver;
23 import android.graphics.drawable.Drawable;
24 import android.hardware.hdmi.HdmiDeviceInfo;
25 import android.media.tv.TvContentRatingSystemInfo;
26 import android.media.tv.TvInputInfo;
27 import android.media.tv.TvInputManager;
28 import android.media.tv.TvInputManager.TvInputCallback;
29 import android.net.Uri;
30 import android.os.Handler;
31 import android.provider.Settings;
32 import android.provider.Settings.SettingNotFoundException;
33 import android.support.annotation.Nullable;
34 import android.support.annotation.UiThread;
35 import android.support.annotation.VisibleForTesting;
36 import android.text.TextUtils;
37 import android.util.ArrayMap;
38 import android.util.Log;
39 import com.android.tv.common.SoftPreconditions;
40 import com.android.tv.common.compat.TvInputInfoCompat;
41 import com.android.tv.common.dagger.annotations.ApplicationContext;
42 import com.android.tv.common.util.CommonUtils;
43 import com.android.tv.common.util.SystemProperties;
44 import com.android.tv.features.TvFeatures;
45 import com.android.tv.parental.ContentRatingsManager;
46 import com.android.tv.parental.ParentalControlSettings;
47 import com.android.tv.util.images.ImageCache;
48 import com.android.tv.util.images.ImageLoader;
49 import com.google.common.collect.Ordering;
50 import com.android.tv.common.flags.LegacyFlags;
51 import java.util.ArrayList;
52 import java.util.Arrays;
53 import java.util.Collections;
54 import java.util.Comparator;
55 import java.util.HashMap;
56 import java.util.HashSet;
57 import java.util.List;
58 import java.util.Map;
59 import javax.inject.Inject;
60 import javax.inject.Singleton;
61 
62 /** Helper class for {@link TvInputManager}. */
63 @UiThread
64 @Singleton
65 public class TvInputManagerHelper {
66     private static final String TAG = "TvInputManagerHelper";
67     private static final boolean DEBUG = false;
68 
69     public interface TvInputManagerInterface {
getTvInputInfo(String inputId)70         TvInputInfo getTvInputInfo(String inputId);
71 
getInputState(String inputId)72         Integer getInputState(String inputId);
73 
registerCallback(TvInputCallback internalCallback, Handler handler)74         void registerCallback(TvInputCallback internalCallback, Handler handler);
75 
unregisterCallback(TvInputCallback internalCallback)76         void unregisterCallback(TvInputCallback internalCallback);
77 
getTvInputList()78         List<TvInputInfo> getTvInputList();
79 
getTvContentRatingSystemList()80         List<TvContentRatingSystemInfo> getTvContentRatingSystemList();
81     }
82 
83     private static final class TvInputManagerImpl implements TvInputManagerInterface {
84         private final TvInputManager delegate;
85 
TvInputManagerImpl(TvInputManager delegate)86         private TvInputManagerImpl(TvInputManager delegate) {
87             this.delegate = delegate;
88         }
89 
90         @Override
getTvInputInfo(String inputId)91         public TvInputInfo getTvInputInfo(String inputId) {
92             return delegate.getTvInputInfo(inputId);
93         }
94 
95         @Override
getInputState(String inputId)96         public Integer getInputState(String inputId) {
97             return delegate.getInputState(inputId);
98         }
99 
100         @Override
registerCallback(TvInputCallback internalCallback, Handler handler)101         public void registerCallback(TvInputCallback internalCallback, Handler handler) {
102             delegate.registerCallback(internalCallback, handler);
103         }
104 
105         @Override
unregisterCallback(TvInputCallback internalCallback)106         public void unregisterCallback(TvInputCallback internalCallback) {
107             delegate.unregisterCallback(internalCallback);
108         }
109 
110         @Override
getTvInputList()111         public List<TvInputInfo> getTvInputList() {
112             return delegate.getTvInputList();
113         }
114 
115         @Override
getTvContentRatingSystemList()116         public List<TvContentRatingSystemInfo> getTvContentRatingSystemList() {
117             return delegate.getTvContentRatingSystemList();
118         }
119     }
120 
121     /** Types of HDMI device and bundled tuner. */
122     public static final int TYPE_CEC_DEVICE = -2;
123 
124     public static final int TYPE_BUNDLED_TUNER = -3;
125     public static final int TYPE_CEC_DEVICE_RECORDER = -4;
126     public static final int TYPE_CEC_DEVICE_PLAYBACK = -5;
127     public static final int TYPE_MHL_MOBILE = -6;
128 
129     private static final String PERMISSION_ACCESS_ALL_EPG_DATA =
130             "com.android.providers.tv.permission.ACCESS_ALL_EPG_DATA";
131     private static final String[] mPhysicalTunerBlockList = {
132         "com.google.android.videos", // Play Movies
133     };
134     private static final String META_LABEL_SORT_KEY = "input_sort_key";
135 
136     private static final String TV_INPUT_ALLOW_3RD_PARTY_INPUTS = "tv_input_allow_3rd_party_inputs";
137 
138     private static final String[] SYSTEM_INPUT_ID_BLOCKLIST = {
139         "com.google.android.videos/" // Play Movies
140     };
141 
142     /** The default tv input priority to show. */
143     private static final ArrayList<Integer> DEFAULT_TV_INPUT_PRIORITY = new ArrayList<>();
144 
145     static {
146         DEFAULT_TV_INPUT_PRIORITY.add(TYPE_BUNDLED_TUNER);
147         DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_TUNER);
148         DEFAULT_TV_INPUT_PRIORITY.add(TYPE_CEC_DEVICE);
149         DEFAULT_TV_INPUT_PRIORITY.add(TYPE_CEC_DEVICE_RECORDER);
150         DEFAULT_TV_INPUT_PRIORITY.add(TYPE_CEC_DEVICE_PLAYBACK);
151         DEFAULT_TV_INPUT_PRIORITY.add(TYPE_MHL_MOBILE);
152         DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_HDMI);
153         DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_DVI);
154         DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_COMPONENT);
155         DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_SVIDEO);
156         DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_COMPOSITE);
157         DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_DISPLAY_PORT);
158         DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_VGA);
159         DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_SCART);
160         DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_OTHER);
161     }
162 
163     private static final String[] PARTNER_TUNER_INPUT_PREFIX_BLOCKLIST = {
164         /* Begin_AOSP_Comment_Out
165         // Disabled partner's tuner input prefix list.
166         "com.mediatek.tvinput/.dtv"
167         End_AOSP_Comment_Out */
168     };
169 
170     private static final String[] TESTABLE_INPUTS = {
171         "com.android.tv.testinput/.TestTvInputService"
172     };
173 
174     private final Context mContext;
175     private final PackageManager mPackageManager;
176     protected final TvInputManagerInterface mTvInputManager;
177     private final Map<String, Integer> mInputStateMap = new HashMap<>();
178     private final Map<String, TvInputInfoCompat> mInputMap = new HashMap<>();
179     private final Map<String, String> mTvInputLabels = new ArrayMap<>();
180     private final Map<String, String> mTvInputCustomLabels = new ArrayMap<>();
181     private final Map<String, Boolean> mInputIdToPartnerInputMap = new HashMap<>();
182 
183     private final Map<String, CharSequence> mTvInputApplicationLabels = new ArrayMap<>();
184     private final Map<String, Drawable> mTvInputApplicationIcons = new ArrayMap<>();
185     private final Map<String, Drawable> mTvInputApplicationBanners = new ArrayMap<>();
186 
187     private final ContentObserver mContentObserver;
188 
189     private final TvInputCallback mInternalCallback =
190             new TvInputCallback() {
191                 @Override
192                 public void onInputStateChanged(String inputId, int state) {
193                     if (DEBUG) Log.d(TAG, "onInputStateChanged " + inputId + " state=" + state);
194                     TvInputInfo info = mInputMap.get(inputId).getTvInputInfo();
195                     if (info == null || isInputBlocked(info)) {
196                         return;
197                     }
198                     mInputStateMap.put(inputId, state);
199                     for (TvInputCallback callback : mCallbacks) {
200                         callback.onInputStateChanged(inputId, state);
201                     }
202                 }
203 
204                 @Override
205                 public void onInputAdded(String inputId) {
206                     if (DEBUG) Log.d(TAG, "onInputAdded " + inputId);
207                     TvInputInfo info = mTvInputManager.getTvInputInfo(inputId);
208                     if (info == null || isInputBlocked(info)) {
209                         return;
210                     }
211                     if (info != null) {
212                         mInputMap.put(inputId, new TvInputInfoCompat(mContext, info));
213                         CharSequence label = info.loadLabel(mContext);
214                         // in tests the label may be missing just use the input id
215                         mTvInputLabels.put(inputId, label != null ? label.toString() : inputId);
216                         CharSequence inputCustomLabel = info.loadCustomLabel(mContext);
217                         if (inputCustomLabel != null) {
218                             mTvInputCustomLabels.put(inputId, inputCustomLabel.toString());
219                         }
220                         mInputStateMap.put(inputId, mTvInputManager.getInputState(inputId));
221                         mInputIdToPartnerInputMap.put(inputId, isPartnerInput(info));
222                     }
223                     mContentRatingsManager.update();
224                     for (TvInputCallback callback : mCallbacks) {
225                         callback.onInputAdded(inputId);
226                     }
227                 }
228 
229                 @Override
230                 public void onInputRemoved(String inputId) {
231                     if (DEBUG) Log.d(TAG, "onInputRemoved " + inputId);
232                     mInputMap.remove(inputId);
233                     mTvInputLabels.remove(inputId);
234                     mTvInputCustomLabels.remove(inputId);
235                     mTvInputApplicationLabels.remove(inputId);
236                     mTvInputApplicationIcons.remove(inputId);
237                     mTvInputApplicationBanners.remove(inputId);
238                     mInputStateMap.remove(inputId);
239                     mInputIdToPartnerInputMap.remove(inputId);
240                     mContentRatingsManager.update();
241                     for (TvInputCallback callback : mCallbacks) {
242                         callback.onInputRemoved(inputId);
243                     }
244                     ImageCache.getInstance()
245                             .remove(ImageLoader.LoadTvInputLogoTask.getTvInputLogoKey(inputId));
246                 }
247 
248                 @Override
249                 public void onInputUpdated(String inputId) {
250                     if (DEBUG) Log.d(TAG, "onInputUpdated " + inputId);
251                     TvInputInfo info = mTvInputManager.getTvInputInfo(inputId);
252                     if (info == null || isInputBlocked(info)) {
253                         return;
254                     }
255                     mInputMap.put(inputId, new TvInputInfoCompat(mContext, info));
256                     mTvInputLabels.put(inputId, info.loadLabel(mContext).toString());
257                     CharSequence inputCustomLabel = info.loadCustomLabel(mContext);
258                     if (inputCustomLabel != null) {
259                         mTvInputCustomLabels.put(inputId, inputCustomLabel.toString());
260                     }
261                     mTvInputApplicationLabels.remove(inputId);
262                     mTvInputApplicationIcons.remove(inputId);
263                     mTvInputApplicationBanners.remove(inputId);
264                     for (TvInputCallback callback : mCallbacks) {
265                         callback.onInputUpdated(inputId);
266                     }
267                     ImageCache.getInstance()
268                             .remove(ImageLoader.LoadTvInputLogoTask.getTvInputLogoKey(inputId));
269                 }
270 
271                 @Override
272                 public void onTvInputInfoUpdated(TvInputInfo inputInfo) {
273                     if (DEBUG) Log.d(TAG, "onTvInputInfoUpdated " + inputInfo);
274                     if (isInputBlocked(inputInfo)) {
275                         return;
276                     }
277                     mInputMap.put(inputInfo.getId(), new TvInputInfoCompat(mContext, inputInfo));
278                     mTvInputLabels.put(inputInfo.getId(), inputInfo.loadLabel(mContext).toString());
279                     CharSequence inputCustomLabel = inputInfo.loadCustomLabel(mContext);
280                     if (inputCustomLabel != null) {
281                         mTvInputCustomLabels.put(inputInfo.getId(), inputCustomLabel.toString());
282                     }
283                     for (TvInputCallback callback : mCallbacks) {
284                         callback.onTvInputInfoUpdated(inputInfo);
285                     }
286                     ImageCache.getInstance()
287                             .remove(
288                                     ImageLoader.LoadTvInputLogoTask.getTvInputLogoKey(
289                                             inputInfo.getId()));
290                 }
291             };
292 
293     private final Handler mHandler = new Handler();
294     private boolean mStarted;
295     private final HashSet<TvInputCallback> mCallbacks = new HashSet<>();
296     private final ContentRatingsManager mContentRatingsManager;
297     private final ParentalControlSettings mParentalControlSettings;
298     private final Comparator<TvInputInfo> mTvInputInfoComparator;
299     private boolean mAllow3rdPartyInputs;
300 
301     @Inject
TvInputManagerHelper(@pplicationContext Context context, LegacyFlags legacyFlags)302     public TvInputManagerHelper(@ApplicationContext Context context, LegacyFlags legacyFlags) {
303         this(context, createTvInputManagerWrapper(context), legacyFlags);
304     }
305 
306     @Nullable
createTvInputManagerWrapper(Context context)307     protected static TvInputManagerImpl createTvInputManagerWrapper(Context context) {
308         TvInputManager tvInputManager =
309                 (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE);
310         return tvInputManager == null ? null : new TvInputManagerImpl(tvInputManager);
311     }
312 
313     @VisibleForTesting
TvInputManagerHelper( Context context, @Nullable TvInputManagerInterface tvInputManager, LegacyFlags legacyFlags)314     protected TvInputManagerHelper(
315             Context context,
316             @Nullable TvInputManagerInterface tvInputManager,
317             LegacyFlags legacyFlags) {
318         mContext = context.getApplicationContext();
319         mPackageManager = context.getPackageManager();
320         mTvInputManager = tvInputManager;
321         mContentRatingsManager = new ContentRatingsManager(context, tvInputManager);
322         mParentalControlSettings = new ParentalControlSettings(context, legacyFlags);
323         mTvInputInfoComparator = new InputComparatorInternal(this);
324         mContentObserver =
325                 new ContentObserver(mHandler) {
326                     @Override
327                     public void onChange(boolean selfChange, Uri uri) {
328                         String option = uri.getLastPathSegment();
329                         if (option == null || !option.equals(TV_INPUT_ALLOW_3RD_PARTY_INPUTS)) {
330                             return;
331                         }
332                         boolean previousSetting = mAllow3rdPartyInputs;
333                         updateAllow3rdPartyInputs();
334                         if (previousSetting == mAllow3rdPartyInputs) {
335                             return;
336                         }
337                         initInputMaps();
338                     }
339                 };
340     }
341 
start()342     public void start() {
343         if (!hasTvInputManager()) {
344             // Not a TV device
345             return;
346         }
347         if (mStarted) {
348             return;
349         }
350         if (DEBUG) Log.d(TAG, "start");
351         mStarted = true;
352         mContext.getContentResolver()
353                 .registerContentObserver(
354                         Settings.Global.getUriFor(TV_INPUT_ALLOW_3RD_PARTY_INPUTS),
355                         true,
356                         mContentObserver);
357         updateAllow3rdPartyInputs();
358         mTvInputManager.registerCallback(mInternalCallback, mHandler);
359         initInputMaps();
360     }
361 
stop()362     public void stop() {
363         if (!mStarted) {
364             return;
365         }
366         mTvInputManager.unregisterCallback(mInternalCallback);
367         mContext.getContentResolver().unregisterContentObserver(mContentObserver);
368         mStarted = false;
369         mInputStateMap.clear();
370         mInputMap.clear();
371         mTvInputLabels.clear();
372         mTvInputCustomLabels.clear();
373         mTvInputApplicationLabels.clear();
374         mTvInputApplicationIcons.clear();
375         mTvInputApplicationBanners.clear();
376         mInputIdToPartnerInputMap.clear();
377     }
378 
379     /** Clears the TvInput labels map. */
clearTvInputLabels()380     public void clearTvInputLabels() {
381         mTvInputLabels.clear();
382         mTvInputCustomLabels.clear();
383         mTvInputApplicationLabels.clear();
384     }
385 
getTvInputInfos(boolean availableOnly, boolean tunerOnly)386     public List<TvInputInfo> getTvInputInfos(boolean availableOnly, boolean tunerOnly) {
387         ArrayList<TvInputInfo> list = new ArrayList<>();
388         for (Map.Entry<String, Integer> pair : mInputStateMap.entrySet()) {
389             if (availableOnly && pair.getValue() == TvInputManager.INPUT_STATE_DISCONNECTED) {
390                 continue;
391             }
392             TvInputInfo input = getTvInputInfo(pair.getKey());
393             if (input == null || isInputBlocked(input)) {
394                 continue;
395             }
396             if (tunerOnly && input.getType() != TvInputInfo.TYPE_TUNER) {
397                 continue;
398             }
399             list.add(input);
400         }
401         Collections.sort(list, mTvInputInfoComparator);
402         return list;
403     }
404 
405     /**
406      * Returns the default comparator for {@link TvInputInfo}. See {@link InputComparatorInternal}
407      * for detail.
408      */
getDefaultTvInputInfoComparator()409     public Comparator<TvInputInfo> getDefaultTvInputInfoComparator() {
410         return mTvInputInfoComparator;
411     }
412 
413     /**
414      * Checks if the input is from a partner.
415      *
416      * <p>It's visible for comparator test. Package private is enough for this method, but public is
417      * necessary to workaround mockito bug.
418      */
419     @VisibleForTesting
isPartnerInput(TvInputInfo inputInfo)420     public boolean isPartnerInput(TvInputInfo inputInfo) {
421         return isSystemInput(inputInfo) && !isBundledInput(inputInfo);
422     }
423 
424     /** Does the input have {@link ApplicationInfo#FLAG_SYSTEM} set. */
isSystemInput(TvInputInfo inputInfo)425     public boolean isSystemInput(TvInputInfo inputInfo) {
426         return inputInfo != null
427                 && (inputInfo.getServiceInfo().applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM)
428                         != 0;
429     }
430 
431     /** Is the input one known bundled inputs not written by OEM/SOCs. */
isBundledInput(TvInputInfo inputInfo)432     public boolean isBundledInput(TvInputInfo inputInfo) {
433         return inputInfo != null
434                 && CommonUtils.isInBundledPackageSet(
435                         inputInfo.getServiceInfo().applicationInfo.packageName);
436     }
437 
438     /**
439      * Returns if the given input is bundled and written by OEM/SOCs. This returns the cached
440      * result.
441      */
isPartnerInput(String inputId)442     public boolean isPartnerInput(String inputId) {
443         Boolean isPartnerInput = mInputIdToPartnerInputMap.get(inputId);
444         return (isPartnerInput != null) ? isPartnerInput : false;
445     }
446 
447     /**
448      * Is (Context.TV_INPUT_SERVICE) available.
449      *
450      * <p>This is only available on TV devices.
451      */
hasTvInputManager()452     public boolean hasTvInputManager() {
453         return mTvInputManager != null;
454     }
455 
456     /** Loads label of {@code info}. */
457     @Nullable
loadLabel(TvInputInfo info)458     public String loadLabel(TvInputInfo info) {
459         String label = mTvInputLabels.get(info.getId());
460         if (label == null) {
461             CharSequence labelSequence = info.loadLabel(mContext);
462             label = labelSequence == null ? null : labelSequence.toString();
463             mTvInputLabels.put(info.getId(), label);
464         }
465         return label;
466     }
467 
468     /** Loads custom label of {@code info} */
loadCustomLabel(TvInputInfo info)469     public String loadCustomLabel(TvInputInfo info) {
470         String customLabel = mTvInputCustomLabels.get(info.getId());
471         if (customLabel == null) {
472             CharSequence customLabelCharSequence = info.loadCustomLabel(mContext);
473             if (customLabelCharSequence != null) {
474                 customLabel = customLabelCharSequence.toString();
475                 mTvInputCustomLabels.put(info.getId(), customLabel);
476             }
477         }
478         return customLabel;
479     }
480 
481     /** Gets the tv input application's label. */
getTvInputApplicationLabel(CharSequence inputId)482     public CharSequence getTvInputApplicationLabel(CharSequence inputId) {
483         return mTvInputApplicationLabels.get(inputId);
484     }
485 
486     /** Stores the tv input application's label. */
setTvInputApplicationLabel(String inputId, CharSequence label)487     public void setTvInputApplicationLabel(String inputId, CharSequence label) {
488         mTvInputApplicationLabels.put(inputId, label);
489     }
490 
491     /** Gets the tv input application's icon. */
getTvInputApplicationIcon(String inputId)492     public Drawable getTvInputApplicationIcon(String inputId) {
493         return mTvInputApplicationIcons.get(inputId);
494     }
495 
496     /** Stores the tv input application's icon. */
setTvInputApplicationIcon(String inputId, Drawable icon)497     public void setTvInputApplicationIcon(String inputId, Drawable icon) {
498         mTvInputApplicationIcons.put(inputId, icon);
499     }
500 
501     /** Gets the tv input application's banner. */
getTvInputApplicationBanner(String inputId)502     public Drawable getTvInputApplicationBanner(String inputId) {
503         return mTvInputApplicationBanners.get(inputId);
504     }
505 
506     /** Stores the tv input application's banner. */
setTvInputApplicationBanner(String inputId, Drawable banner)507     public void setTvInputApplicationBanner(String inputId, Drawable banner) {
508         mTvInputApplicationBanners.put(inputId, banner);
509     }
510 
511     /** Returns if TV input exists with the input id. */
hasTvInputInfo(String inputId)512     public boolean hasTvInputInfo(String inputId) {
513         SoftPreconditions.checkState(
514                 mStarted, TAG, "hasTvInputInfo() called before TvInputManagerHelper was started.");
515         return mStarted && !TextUtils.isEmpty(inputId) && mInputMap.get(inputId) != null;
516     }
517 
518     @Nullable
getTvInputInfo(String inputId)519     public TvInputInfo getTvInputInfo(String inputId) {
520         TvInputInfoCompat inputInfo = getTvInputInfoCompat(inputId);
521         return inputInfo == null ? null : inputInfo.getTvInputInfo();
522     }
523 
524     @Nullable
getTvInputInfoCompat(String inputId)525     public TvInputInfoCompat getTvInputInfoCompat(String inputId) {
526         SoftPreconditions.checkState(
527                 mStarted, TAG, "getTvInputInfo() called before TvInputManagerHelper was started.");
528         if (!mStarted) {
529             return null;
530         }
531         if (inputId == null) {
532             return null;
533         }
534         return mInputMap.get(inputId);
535     }
536 
getTvInputAppInfo(String inputId)537     public ApplicationInfo getTvInputAppInfo(String inputId) {
538         TvInputInfo info = getTvInputInfo(inputId);
539         return info == null ? null : info.getServiceInfo().applicationInfo;
540     }
541 
getTunerTvInputSize()542     public int getTunerTvInputSize() {
543         int size = 0;
544         for (TvInputInfoCompat input : mInputMap.values()) {
545             if (input.getType() == TvInputInfo.TYPE_TUNER) {
546                 ++size;
547             }
548         }
549         return size;
550     }
551     /**
552      * Returns TvInputInfo's input state.
553      *
554      * @param inputInfo
555      * @return An Integer which stands for the input state {@link
556      *     TvInputManager.INPUT_STATE_DISCONNECTED} if inputInfo is null
557      */
getInputState(@ullable TvInputInfo inputInfo)558     public int getInputState(@Nullable TvInputInfo inputInfo) {
559         return inputInfo == null
560                 ? TvInputManager.INPUT_STATE_DISCONNECTED
561                 : getInputState(inputInfo.getId());
562     }
563 
getInputState(String inputId)564     public int getInputState(String inputId) {
565         SoftPreconditions.checkState(mStarted, TAG, "AvailabilityManager not started");
566         if (!mStarted) {
567             return TvInputManager.INPUT_STATE_DISCONNECTED;
568         }
569         Integer state = mInputStateMap.get(inputId);
570         if (state == null) {
571             Log.w(TAG, "getInputState: no such input (id=" + inputId + ")");
572             return TvInputManager.INPUT_STATE_DISCONNECTED;
573         }
574         return state;
575     }
576 
addCallback(TvInputCallback callback)577     public void addCallback(TvInputCallback callback) {
578         mCallbacks.add(callback);
579     }
580 
removeCallback(TvInputCallback callback)581     public void removeCallback(TvInputCallback callback) {
582         mCallbacks.remove(callback);
583     }
584 
getParentalControlSettings()585     public ParentalControlSettings getParentalControlSettings() {
586         return mParentalControlSettings;
587     }
588 
589     /** Returns a ContentRatingsManager instance for a given application context. */
getContentRatingsManager()590     public ContentRatingsManager getContentRatingsManager() {
591         return mContentRatingsManager;
592     }
593 
getInputSortKey(TvInputInfo input)594     private int getInputSortKey(TvInputInfo input) {
595         return input.getServiceInfo().metaData.getInt(META_LABEL_SORT_KEY, Integer.MAX_VALUE);
596     }
597 
isInputPhysicalTuner(TvInputInfo input)598     private boolean isInputPhysicalTuner(TvInputInfo input) {
599         String packageName = input.getServiceInfo().packageName;
600         if (Arrays.asList(mPhysicalTunerBlockList).contains(packageName)) {
601             return false;
602         }
603 
604         if (input.createSetupIntent() == null) {
605             return false;
606         } else {
607             boolean mayBeTunerInput =
608                     mPackageManager.checkPermission(
609                                     PERMISSION_ACCESS_ALL_EPG_DATA,
610                                     input.getServiceInfo().packageName)
611                             == PackageManager.PERMISSION_GRANTED;
612             if (!mayBeTunerInput) {
613                 try {
614                     ApplicationInfo ai =
615                             mPackageManager.getApplicationInfo(
616                                     input.getServiceInfo().packageName, 0);
617                     if ((ai.flags
618                                     & (ApplicationInfo.FLAG_SYSTEM
619                                             | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP))
620                             == 0) {
621                         return false;
622                     }
623                 } catch (PackageManager.NameNotFoundException e) {
624                     return false;
625                 }
626             }
627         }
628         return true;
629     }
630 
isBlocked(String inputId)631     private boolean isBlocked(String inputId) {
632         if (TvFeatures.USE_PARTNER_INPUT_BLOCKLIST.isEnabled(mContext)) {
633             for (String disabledTunerInputPrefix : PARTNER_TUNER_INPUT_PREFIX_BLOCKLIST) {
634                 if (inputId.contains(disabledTunerInputPrefix)) {
635                     return true;
636                 }
637             }
638         }
639         if (CommonUtils.isRoboTest()) return false;
640         if (CommonUtils.isRunningInTest()) {
641             for (String testableInput : TESTABLE_INPUTS) {
642                 if (testableInput.equals(inputId)) {
643                     return false;
644                 }
645             }
646             return true;
647         }
648         return false;
649     }
650 
initInputMaps()651     private void initInputMaps() {
652         mInputMap.clear();
653         mTvInputLabels.clear();
654         mTvInputCustomLabels.clear();
655         mTvInputApplicationLabels.clear();
656         mTvInputApplicationIcons.clear();
657         mTvInputApplicationBanners.clear();
658         mInputStateMap.clear();
659         mInputIdToPartnerInputMap.clear();
660         for (TvInputInfo input : mTvInputManager.getTvInputList()) {
661             if (DEBUG) {
662                 Log.d(TAG, "Input detected " + input);
663             }
664             String inputId = input.getId();
665             if (isInputBlocked(input)) {
666                 continue;
667             }
668             mInputMap.put(inputId, new TvInputInfoCompat(mContext, input));
669             int state = mTvInputManager.getInputState(inputId);
670             mInputStateMap.put(inputId, state);
671             mInputIdToPartnerInputMap.put(inputId, isPartnerInput(input));
672         }
673         SoftPreconditions.checkState(
674                 mInputStateMap.size() == mInputMap.size(),
675                 TAG,
676                 "mInputStateMap not the same size as mInputMap");
677     }
678 
updateAllow3rdPartyInputs()679     private void updateAllow3rdPartyInputs() {
680         int setting;
681         try {
682             setting =
683                     Settings.Global.getInt(
684                             mContext.getContentResolver(), TV_INPUT_ALLOW_3RD_PARTY_INPUTS);
685         } catch (SettingNotFoundException e) {
686             mAllow3rdPartyInputs = SystemProperties.ALLOW_THIRD_PARTY_INPUTS.getValue();
687             return;
688         }
689         mAllow3rdPartyInputs = setting == 1;
690     }
691 
isInputBlocked(TvInputInfo info)692     private boolean isInputBlocked(TvInputInfo info) {
693         if (!mAllow3rdPartyInputs) {
694             if (!isSystemInput(info)) {
695                 return true;
696             }
697             for (String id : SYSTEM_INPUT_ID_BLOCKLIST) {
698                 if (info.getId().startsWith(id)) {
699                     return true;
700                 }
701             }
702         }
703         return isBlocked(info.getId());
704     }
705 
706     /**
707      * Default comparator for TvInputInfo.
708      *
709      * <p>It's static class that accepts {@link TvInputManagerHelper} as parameter to test. To test
710      * comparator, we need to mock API in parent class such as {@link #isPartnerInput}, but it's
711      * impossible for an inner class to use mocked methods. (i.e. Mockito's spy doesn't work)
712      */
713     @VisibleForTesting
714     static class InputComparatorInternal implements Comparator<TvInputInfo> {
715         private final TvInputManagerHelper mInputManager;
716         private static final Ordering<Comparable> NULL_FIRST_STRING_ORDERING =
717                 Ordering.natural().nullsFirst();
718 
InputComparatorInternal(TvInputManagerHelper inputManager)719         public InputComparatorInternal(TvInputManagerHelper inputManager) {
720             mInputManager = inputManager;
721         }
722 
723         @Override
compare(TvInputInfo lhs, TvInputInfo rhs)724         public int compare(TvInputInfo lhs, TvInputInfo rhs) {
725             if (mInputManager.isPartnerInput(lhs) != mInputManager.isPartnerInput(rhs)) {
726                 return mInputManager.isPartnerInput(lhs) ? -1 : 1;
727             }
728             return NULL_FIRST_STRING_ORDERING.compare(
729                     mInputManager.loadLabel(lhs), mInputManager.loadLabel(rhs));
730         }
731     }
732 
733     /**
734      * A comparator used for {@link com.android.tv.ui.SelectInputView} to show the list of TV
735      * inputs.
736      */
737     public static class HardwareInputComparator implements Comparator<TvInputInfo> {
738         private Map<Integer, Integer> mTypePriorities = new HashMap<>();
739         private final TvInputManagerHelper mTvInputManagerHelper;
740         private final Context mContext;
741 
HardwareInputComparator(Context context, TvInputManagerHelper tvInputManagerHelper)742         public HardwareInputComparator(Context context, TvInputManagerHelper tvInputManagerHelper) {
743             mContext = context;
744             mTvInputManagerHelper = tvInputManagerHelper;
745             setupDeviceTypePriorities();
746         }
747 
748         @Override
compare(TvInputInfo lhs, TvInputInfo rhs)749         public int compare(TvInputInfo lhs, TvInputInfo rhs) {
750             if (lhs == null) {
751                 return (rhs == null) ? 0 : 1;
752             }
753             if (rhs == null) {
754                 return -1;
755             }
756 
757             boolean enabledL =
758                     (mTvInputManagerHelper.getInputState(lhs)
759                             != TvInputManager.INPUT_STATE_DISCONNECTED);
760             boolean enabledR =
761                     (mTvInputManagerHelper.getInputState(rhs)
762                             != TvInputManager.INPUT_STATE_DISCONNECTED);
763             if (enabledL != enabledR) {
764                 return enabledL ? -1 : 1;
765             }
766 
767             int priorityL = getPriority(lhs);
768             int priorityR = getPriority(rhs);
769             if (priorityL != priorityR) {
770                 return priorityL - priorityR;
771             }
772 
773             if (lhs.getType() == TvInputInfo.TYPE_TUNER
774                     && rhs.getType() == TvInputInfo.TYPE_TUNER) {
775                 boolean isPhysicalL = mTvInputManagerHelper.isInputPhysicalTuner(lhs);
776                 boolean isPhysicalR = mTvInputManagerHelper.isInputPhysicalTuner(rhs);
777                 if (isPhysicalL != isPhysicalR) {
778                     return isPhysicalL ? -1 : 1;
779                 }
780             }
781 
782             int sortKeyL = mTvInputManagerHelper.getInputSortKey(lhs);
783             int sortKeyR = mTvInputManagerHelper.getInputSortKey(rhs);
784             if (sortKeyL != sortKeyR) {
785                 return sortKeyR - sortKeyL;
786             }
787 
788             String parentLabelL =
789                     lhs.getParentId() != null
790                             ? getLabel(mTvInputManagerHelper.getTvInputInfo(lhs.getParentId()))
791                             : getLabel(mTvInputManagerHelper.getTvInputInfo(lhs.getId()));
792             String parentLabelR =
793                     rhs.getParentId() != null
794                             ? getLabel(mTvInputManagerHelper.getTvInputInfo(rhs.getParentId()))
795                             : getLabel(mTvInputManagerHelper.getTvInputInfo(rhs.getId()));
796 
797             if (!TextUtils.equals(parentLabelL, parentLabelR)) {
798                 return parentLabelL.compareToIgnoreCase(parentLabelR);
799             }
800             return getLabel(lhs).compareToIgnoreCase(getLabel(rhs));
801         }
802 
getLabel(TvInputInfo input)803         private String getLabel(TvInputInfo input) {
804             if (input == null) {
805                 return "";
806             }
807             String label = mTvInputManagerHelper.loadCustomLabel(input);
808             if (TextUtils.isEmpty(label)) {
809                 label = mTvInputManagerHelper.loadLabel(input);
810             }
811             return label == null ? "" : label;
812         }
813 
getPriority(TvInputInfo info)814         private int getPriority(TvInputInfo info) {
815             Integer priority = null;
816             if (mTypePriorities != null) {
817                 priority = mTypePriorities.get(getTvInputTypeForPriority(info));
818             }
819             if (priority != null) {
820                 return priority;
821             }
822             return Integer.MAX_VALUE;
823         }
824 
setupDeviceTypePriorities()825         private void setupDeviceTypePriorities() {
826             mTypePriorities = Partner.getInstance(mContext).getInputsOrderMap();
827 
828             // Fill in any missing priorities in the map we got from the OEM
829             int priority = mTypePriorities.size();
830             for (int type : DEFAULT_TV_INPUT_PRIORITY) {
831                 if (!mTypePriorities.containsKey(type)) {
832                     mTypePriorities.put(type, priority++);
833                 }
834             }
835         }
836 
getTvInputTypeForPriority(TvInputInfo info)837         private int getTvInputTypeForPriority(TvInputInfo info) {
838             if (info.getHdmiDeviceInfo() != null) {
839                 if (info.getHdmiDeviceInfo().isCecDevice()) {
840                     switch (info.getHdmiDeviceInfo().getDeviceType()) {
841                         case HdmiDeviceInfo.DEVICE_RECORDER:
842                             return TYPE_CEC_DEVICE_RECORDER;
843                         case HdmiDeviceInfo.DEVICE_PLAYBACK:
844                             return TYPE_CEC_DEVICE_PLAYBACK;
845                         default:
846                             return TYPE_CEC_DEVICE;
847                     }
848                 } else if (info.getHdmiDeviceInfo().isMhlDevice()) {
849                     return TYPE_MHL_MOBILE;
850                 }
851             }
852             return info.getType();
853         }
854     }
855 }
856