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