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.media.tv.TvInputInfo;
22 import android.media.tv.TvInputManager;
23 import android.media.tv.TvInputManager.TvInputCallback;
24 import android.os.Handler;
25 import android.support.annotation.VisibleForTesting;
26 import android.text.TextUtils;
27 import android.util.Log;
28 
29 import com.android.tv.common.SoftPreconditions;
30 import com.android.tv.parental.ContentRatingsManager;
31 import com.android.tv.parental.ParentalControlSettings;
32 
33 import java.util.ArrayList;
34 import java.util.Collections;
35 import java.util.Comparator;
36 import java.util.HashMap;
37 import java.util.HashSet;
38 import java.util.List;
39 import java.util.Map;
40 import java.util.Set;
41 
42 public class TvInputManagerHelper {
43     private static final String TAG = "TvInputManagerHelper";
44     private static final boolean DEBUG = false;
45 
46     // Hardcoded list for known bundled inputs not written by OEM/SOCs.
47     // Bundled (system) inputs not in the list will get the high priority
48     // so they and their channels come first in the UI.
49     private static final Set<String> BUNDLED_PACKAGE_SET = new HashSet<>();
50 
51     static {
52         BUNDLED_PACKAGE_SET.add("com.android.tv");
53         BUNDLED_PACKAGE_SET.add("com.android.usbtuner");
54     }
55 
56     private final Context mContext;
57     private final TvInputManager mTvInputManager;
58     private final Map<String, Integer> mInputStateMap = new HashMap<>();
59     private final Map<String, TvInputInfo> mInputMap = new HashMap<>();
60     private final Map<String, Boolean> mInputIdToPartnerInputMap = new HashMap<>();
61     private final TvInputCallback mInternalCallback = new TvInputCallback() {
62         @Override
63         public void onInputStateChanged(String inputId, int state) {
64             if (DEBUG) Log.d(TAG, "onInputStateChanged " + inputId + " state=" + state);
65             mInputStateMap.put(inputId, state);
66             for (TvInputCallback callback : mCallbacks) {
67                 callback.onInputStateChanged(inputId, state);
68             }
69         }
70 
71         @Override
72         public void onInputAdded(String inputId) {
73             if (DEBUG) Log.d(TAG, "onInputAdded " + inputId);
74             TvInputInfo info = mTvInputManager.getTvInputInfo(inputId);
75             if (info != null) {
76                 mInputMap.put(inputId, info);
77                 mInputStateMap.put(inputId, mTvInputManager.getInputState(inputId));
78                 mInputIdToPartnerInputMap.put(inputId, isPartnerInput(info));
79             }
80             mContentRatingsManager.update();
81             for (TvInputCallback callback : mCallbacks) {
82                 callback.onInputAdded(inputId);
83             }
84         }
85 
86         @Override
87         public void onInputRemoved(String inputId) {
88             if (DEBUG) Log.d(TAG, "onInputRemoved " + inputId);
89             mInputMap.remove(inputId);
90             mInputStateMap.remove(inputId);
91             mInputIdToPartnerInputMap.remove(inputId);
92             mContentRatingsManager.update();
93             for (TvInputCallback callback : mCallbacks) {
94                 callback.onInputRemoved(inputId);
95             }
96         }
97 
98         @Override
99         public void onInputUpdated(String inputId) {
100             if (DEBUG) Log.d(TAG, "onInputUpdated " + inputId);
101             TvInputInfo info = mTvInputManager.getTvInputInfo(inputId);
102             mInputMap.put(inputId, info);
103             for (TvInputCallback callback : mCallbacks) {
104                 callback.onInputUpdated(inputId);
105             }
106         }
107     };
108 
109     private final Handler mHandler = new Handler();
110     private boolean mStarted;
111     private final HashSet<TvInputCallback> mCallbacks = new HashSet<>();
112     private final ContentRatingsManager mContentRatingsManager;
113     private final ParentalControlSettings mParentalControlSettings;
114     private final Comparator<TvInputInfo> mTvInputInfoComparator;
115 
TvInputManagerHelper(Context context)116     public TvInputManagerHelper(Context context) {
117         mContext = context.getApplicationContext();
118         mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE);
119         mContentRatingsManager = new ContentRatingsManager(context);
120         mParentalControlSettings = new ParentalControlSettings(context);
121         mTvInputInfoComparator = new TvInputInfoComparator(this);
122     }
123 
start()124     public void start() {
125         if (mStarted) {
126             return;
127         }
128         if (DEBUG) Log.d(TAG, "start");
129         mStarted = true;
130         mTvInputManager.registerCallback(mInternalCallback, mHandler);
131         mInputMap.clear();
132         mInputStateMap.clear();
133         mInputIdToPartnerInputMap.clear();
134         for (TvInputInfo input : mTvInputManager.getTvInputList()) {
135             if (DEBUG) Log.d(TAG, "Input detected " + input);
136             String inputId = input.getId();
137             mInputMap.put(inputId, input);
138             int state = mTvInputManager.getInputState(inputId);
139             mInputStateMap.put(inputId, state);
140             mInputIdToPartnerInputMap.put(inputId, isPartnerInput(input));
141         }
142         SoftPreconditions.checkState(mInputStateMap.size() == mInputMap.size(), TAG,
143                 "mInputStateMap not the same size as mInputMap");
144         mContentRatingsManager.update();
145     }
146 
stop()147     public void stop() {
148         if (!mStarted) {
149             return;
150         }
151         mTvInputManager.unregisterCallback(mInternalCallback);
152         mStarted = false;
153         mInputStateMap.clear();
154         mInputMap.clear();
155         mInputIdToPartnerInputMap.clear();
156     }
157 
getTvInputInfos(boolean availableOnly, boolean tunerOnly)158     public List<TvInputInfo> getTvInputInfos(boolean availableOnly, boolean tunerOnly) {
159         ArrayList<TvInputInfo> list = new ArrayList<>();
160         for (Map.Entry<String, Integer> pair : mInputStateMap.entrySet()) {
161             if (availableOnly && pair.getValue() == TvInputManager.INPUT_STATE_DISCONNECTED) {
162                 continue;
163             }
164             TvInputInfo input = getTvInputInfo(pair.getKey());
165             if (tunerOnly && input.getType() != TvInputInfo.TYPE_TUNER) {
166                 continue;
167             }
168             list.add(input);
169         }
170         Collections.sort(list, mTvInputInfoComparator);
171         return list;
172     }
173 
174     /**
175      * Returns the default comparator for {@link TvInputInfo}.
176      * See {@link TvInputInfoComparator} for detail.
177      */
getDefaultTvInputInfoComparator()178     public Comparator<TvInputInfo> getDefaultTvInputInfoComparator() {
179         return mTvInputInfoComparator;
180     }
181 
182     /**
183      * Checks if the input is from a partner.
184      *
185      * It's visible for comparator test.
186      * Package private is enough for this method, but public is necessary to workaround mockito
187      * bug.
188      */
189     @VisibleForTesting
isPartnerInput(TvInputInfo inputInfo)190     public boolean isPartnerInput(TvInputInfo inputInfo) {
191         return isSystemInput(inputInfo) && !isBundledInput(inputInfo);
192     }
193 
194     /**
195      * Does the input have {@link ApplicationInfo#FLAG_SYSTEM} set.
196      */
isSystemInput(TvInputInfo inputInfo)197     public boolean isSystemInput(TvInputInfo inputInfo) {
198         return inputInfo != null
199                 && (inputInfo.getServiceInfo().applicationInfo.flags
200                     & ApplicationInfo.FLAG_SYSTEM) != 0;
201     }
202 
203     /**
204      * Is the input one known bundled inputs not written by OEM/SOCs.
205      */
isBundledInput(TvInputInfo inputInfo)206     public boolean isBundledInput(TvInputInfo inputInfo) {
207         return inputInfo != null
208                && BUNDLED_PACKAGE_SET.contains(
209                    inputInfo.getServiceInfo().applicationInfo.packageName);
210     }
211 
212     /**
213      * Returns if the given input is bundled and written by OEM/SOCs.
214      * This returns the cached result.
215      */
isPartnerInput(String inputId)216     public boolean isPartnerInput(String inputId) {
217         Boolean isPartnerInput = mInputIdToPartnerInputMap.get(inputId);
218         return (isPartnerInput != null) ? isPartnerInput : false;
219     }
220 
221     /**
222      * Loads label of {@code info}.
223      *
224      * It's visible for comparator test to mock TvInputInfo.
225      * Package private is enough for this method, but public is necessary to workaround mockito
226      * bug.
227      */
228     @VisibleForTesting
loadLabel(TvInputInfo info)229     public String loadLabel(TvInputInfo info) {
230         return info.loadLabel(mContext).toString();
231     }
232 
233     /**
234      * Returns if TV input exists with the input id.
235      */
hasTvInputInfo(String inputId)236     public boolean hasTvInputInfo(String inputId) {
237         SoftPreconditions.checkState(mStarted, TAG,
238                 "hasTvInputInfo() called before TvInputManagerHelper was started.");
239         if (!mStarted) {
240             return false;
241         }
242         return !TextUtils.isEmpty(inputId) && mInputMap.get(inputId) != null;
243     }
244 
getTvInputInfo(String inputId)245     public TvInputInfo getTvInputInfo(String inputId) {
246         SoftPreconditions.checkState(mStarted, TAG,
247                 "getTvInputInfo() called before TvInputManagerHelper was started.");
248         if (!mStarted) {
249             return null;
250         }
251         if (inputId == null) {
252             return null;
253         }
254         return mInputMap.get(inputId);
255     }
256 
getTvInputAppInfo(String inputId)257     public ApplicationInfo getTvInputAppInfo(String inputId) {
258         TvInputInfo info = getTvInputInfo(inputId);
259         return info == null ? null : info.getServiceInfo().applicationInfo;
260     }
261 
getTunerTvInputSize()262     public int getTunerTvInputSize() {
263         int size = 0;
264         for (TvInputInfo input : mInputMap.values()) {
265             if (input.getType() == TvInputInfo.TYPE_TUNER) {
266                 ++size;
267             }
268         }
269         return size;
270     }
271 
getInputState(TvInputInfo inputInfo)272     public int getInputState(TvInputInfo inputInfo) {
273         return getInputState(inputInfo.getId());
274     }
275 
getInputState(String inputId)276     public int getInputState(String inputId) {
277         SoftPreconditions.checkState(mStarted, TAG, "AvailabilityManager not started");
278         if (!mStarted) {
279             return TvInputManager.INPUT_STATE_DISCONNECTED;
280 
281         }
282         Integer state = mInputStateMap.get(inputId);
283         if (state == null) {
284             Log.w(TAG, "getInputState: no such input (id=" + inputId + ")");
285             return TvInputManager.INPUT_STATE_DISCONNECTED;
286         }
287         return state;
288     }
289 
addCallback(TvInputCallback callback)290     public void addCallback(TvInputCallback callback) {
291         mCallbacks.add(callback);
292     }
293 
removeCallback(TvInputCallback callback)294     public void removeCallback(TvInputCallback callback) {
295         mCallbacks.remove(callback);
296     }
297 
getParentalControlSettings()298     public ParentalControlSettings getParentalControlSettings() {
299         return mParentalControlSettings;
300     }
301 
302     /**
303      * Returns a ContentRatingsManager instance for a given application context.
304      */
getContentRatingsManager()305     public ContentRatingsManager getContentRatingsManager() {
306         return mContentRatingsManager;
307     }
308 
309     /**
310      * Default comparator for TvInputInfo.
311      *
312      * It's static class that accepts {@link TvInputManagerHelper} as parameter to test.
313      * To test comparator, we need to mock API in parent class such as {@link #isPartnerInput},
314      * but it's impossible for an inner class to use mocked methods.
315      * (i.e. Mockito's spy doesn't work)
316      */
317     @VisibleForTesting
318     static class TvInputInfoComparator implements Comparator<TvInputInfo> {
319         private final TvInputManagerHelper mInputManager;
320 
TvInputInfoComparator(TvInputManagerHelper inputManager)321         public TvInputInfoComparator(TvInputManagerHelper inputManager) {
322             mInputManager = inputManager;
323         }
324 
325         @Override
compare(TvInputInfo lhs, TvInputInfo rhs)326         public int compare(TvInputInfo lhs, TvInputInfo rhs) {
327             if (mInputManager.isPartnerInput(lhs) != mInputManager.isPartnerInput(rhs)) {
328                 return mInputManager.isPartnerInput(lhs) ? -1 : 1;
329             }
330             return mInputManager.loadLabel(lhs).compareTo(mInputManager.loadLabel(rhs));
331         }
332     }
333 }
334