/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tv.util; import android.content.Context; import android.content.pm.ApplicationInfo; import android.media.tv.TvInputInfo; import android.media.tv.TvInputManager; import android.media.tv.TvInputManager.TvInputCallback; import android.os.Handler; import android.support.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.Log; import com.android.tv.Features; import com.android.tv.common.SoftPreconditions; import com.android.tv.parental.ContentRatingsManager; import com.android.tv.parental.ParentalControlSettings; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; public class TvInputManagerHelper { private static final String TAG = "TvInputManagerHelper"; private static final boolean DEBUG = false; private static final String[] PARTNER_TUNER_INPUT_PREFIX_BLACKLIST = { }; private final Context mContext; private final TvInputManager mTvInputManager; private final Map mInputStateMap = new HashMap<>(); private final Map mInputMap = new HashMap<>(); private final Map mInputIdToPartnerInputMap = new HashMap<>(); private final TvInputCallback mInternalCallback = new TvInputCallback() { @Override public void onInputStateChanged(String inputId, int state) { if (DEBUG) Log.d(TAG, "onInputStateChanged " + inputId + " state=" + state); if (isInBlackList(inputId)) { return; } mInputStateMap.put(inputId, state); for (TvInputCallback callback : mCallbacks) { callback.onInputStateChanged(inputId, state); } } @Override public void onInputAdded(String inputId) { if (DEBUG) Log.d(TAG, "onInputAdded " + inputId); if (isInBlackList(inputId)) { return; } TvInputInfo info = mTvInputManager.getTvInputInfo(inputId); if (info != null) { mInputMap.put(inputId, info); mInputStateMap.put(inputId, mTvInputManager.getInputState(inputId)); mInputIdToPartnerInputMap.put(inputId, isPartnerInput(info)); } mContentRatingsManager.update(); for (TvInputCallback callback : mCallbacks) { callback.onInputAdded(inputId); } } @Override public void onInputRemoved(String inputId) { if (DEBUG) Log.d(TAG, "onInputRemoved " + inputId); mInputMap.remove(inputId); mInputStateMap.remove(inputId); mInputIdToPartnerInputMap.remove(inputId); mContentRatingsManager.update(); for (TvInputCallback callback : mCallbacks) { callback.onInputRemoved(inputId); } ImageCache.getInstance().remove(ImageLoader.LoadTvInputLogoTask.getTvInputLogoKey( inputId)); } @Override public void onInputUpdated(String inputId) { if (DEBUG) Log.d(TAG, "onInputUpdated " + inputId); if (isInBlackList(inputId)) { return; } TvInputInfo info = mTvInputManager.getTvInputInfo(inputId); mInputMap.put(inputId, info); for (TvInputCallback callback : mCallbacks) { callback.onInputUpdated(inputId); } ImageCache.getInstance().remove(ImageLoader.LoadTvInputLogoTask.getTvInputLogoKey( inputId)); } @Override public void onTvInputInfoUpdated(TvInputInfo inputInfo) { if (DEBUG) Log.d(TAG, "onTvInputInfoUpdated " + inputInfo); mInputMap.put(inputInfo.getId(), inputInfo); for (TvInputCallback callback : mCallbacks) { callback.onTvInputInfoUpdated(inputInfo); } ImageCache.getInstance().remove(ImageLoader.LoadTvInputLogoTask.getTvInputLogoKey( inputInfo.getId())); } }; private final Handler mHandler = new Handler(); private boolean mStarted; private final HashSet mCallbacks = new HashSet<>(); private final ContentRatingsManager mContentRatingsManager; private final ParentalControlSettings mParentalControlSettings; private final Comparator mTvInputInfoComparator; public TvInputManagerHelper(Context context) { mContext = context.getApplicationContext(); mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE); mContentRatingsManager = new ContentRatingsManager(context); mParentalControlSettings = new ParentalControlSettings(context); mTvInputInfoComparator = new TvInputInfoComparator(this); } public void start() { if (mStarted) { return; } if (DEBUG) Log.d(TAG, "start"); mStarted = true; mTvInputManager.registerCallback(mInternalCallback, mHandler); mInputMap.clear(); mInputStateMap.clear(); mInputIdToPartnerInputMap.clear(); for (TvInputInfo input : mTvInputManager.getTvInputList()) { if (DEBUG) Log.d(TAG, "Input detected " + input); String inputId = input.getId(); if (isInBlackList(inputId)) { continue; } mInputMap.put(inputId, input); int state = mTvInputManager.getInputState(inputId); mInputStateMap.put(inputId, state); mInputIdToPartnerInputMap.put(inputId, isPartnerInput(input)); } SoftPreconditions.checkState(mInputStateMap.size() == mInputMap.size(), TAG, "mInputStateMap not the same size as mInputMap"); mContentRatingsManager.update(); } public void stop() { if (!mStarted) { return; } mTvInputManager.unregisterCallback(mInternalCallback); mStarted = false; mInputStateMap.clear(); mInputMap.clear(); mInputIdToPartnerInputMap.clear(); } public List getTvInputInfos(boolean availableOnly, boolean tunerOnly) { ArrayList list = new ArrayList<>(); for (Map.Entry pair : mInputStateMap.entrySet()) { if (availableOnly && pair.getValue() == TvInputManager.INPUT_STATE_DISCONNECTED) { continue; } TvInputInfo input = getTvInputInfo(pair.getKey()); if (tunerOnly && input.getType() != TvInputInfo.TYPE_TUNER) { continue; } list.add(input); } Collections.sort(list, mTvInputInfoComparator); return list; } /** * Returns the default comparator for {@link TvInputInfo}. * See {@link TvInputInfoComparator} for detail. */ public Comparator getDefaultTvInputInfoComparator() { return mTvInputInfoComparator; } /** * Checks if the input is from a partner. * * It's visible for comparator test. * Package private is enough for this method, but public is necessary to workaround mockito * bug. */ @VisibleForTesting public boolean isPartnerInput(TvInputInfo inputInfo) { return isSystemInput(inputInfo) && !isBundledInput(inputInfo); } /** * Does the input have {@link ApplicationInfo#FLAG_SYSTEM} set. */ public boolean isSystemInput(TvInputInfo inputInfo) { return inputInfo != null && (inputInfo.getServiceInfo().applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; } /** * Is the input one known bundled inputs not written by OEM/SOCs. */ public boolean isBundledInput(TvInputInfo inputInfo) { return inputInfo != null && Utils.isInBundledPackageSet(inputInfo.getServiceInfo() .applicationInfo.packageName); } /** * Returns if the given input is bundled and written by OEM/SOCs. * This returns the cached result. */ public boolean isPartnerInput(String inputId) { Boolean isPartnerInput = mInputIdToPartnerInputMap.get(inputId); return (isPartnerInput != null) ? isPartnerInput : false; } /** * Loads label of {@code info}. * * It's visible for comparator test to mock TvInputInfo. * Package private is enough for this method, but public is necessary to workaround mockito * bug. */ @VisibleForTesting public String loadLabel(TvInputInfo info) { return info.loadLabel(mContext).toString(); } /** * Returns if TV input exists with the input id. */ public boolean hasTvInputInfo(String inputId) { SoftPreconditions.checkState(mStarted, TAG, "hasTvInputInfo() called before TvInputManagerHelper was started."); return mStarted && !TextUtils.isEmpty(inputId) && mInputMap.get(inputId) != null; } public TvInputInfo getTvInputInfo(String inputId) { SoftPreconditions.checkState(mStarted, TAG, "getTvInputInfo() called before TvInputManagerHelper was started."); if (!mStarted) { return null; } if (inputId == null) { return null; } return mInputMap.get(inputId); } public ApplicationInfo getTvInputAppInfo(String inputId) { TvInputInfo info = getTvInputInfo(inputId); return info == null ? null : info.getServiceInfo().applicationInfo; } public int getTunerTvInputSize() { int size = 0; for (TvInputInfo input : mInputMap.values()) { if (input.getType() == TvInputInfo.TYPE_TUNER) { ++size; } } return size; } public int getInputState(TvInputInfo inputInfo) { return getInputState(inputInfo.getId()); } public int getInputState(String inputId) { SoftPreconditions.checkState(mStarted, TAG, "AvailabilityManager not started"); if (!mStarted) { return TvInputManager.INPUT_STATE_DISCONNECTED; } Integer state = mInputStateMap.get(inputId); if (state == null) { Log.w(TAG, "getInputState: no such input (id=" + inputId + ")"); return TvInputManager.INPUT_STATE_DISCONNECTED; } return state; } public void addCallback(TvInputCallback callback) { mCallbacks.add(callback); } public void removeCallback(TvInputCallback callback) { mCallbacks.remove(callback); } public ParentalControlSettings getParentalControlSettings() { return mParentalControlSettings; } /** * Returns a ContentRatingsManager instance for a given application context. */ public ContentRatingsManager getContentRatingsManager() { return mContentRatingsManager; } private boolean isInBlackList(String inputId) { if (!Features.USE_PARTNER_INPUT_BLACKLIST.isEnabled(mContext)) { return false; } for (String disabledTunerInputPrefix : PARTNER_TUNER_INPUT_PREFIX_BLACKLIST) { if (inputId.contains(disabledTunerInputPrefix)) { return true; } } return false; } /** * Default comparator for TvInputInfo. * * It's static class that accepts {@link TvInputManagerHelper} as parameter to test. * To test comparator, we need to mock API in parent class such as {@link #isPartnerInput}, * but it's impossible for an inner class to use mocked methods. * (i.e. Mockito's spy doesn't work) */ @VisibleForTesting static class TvInputInfoComparator implements Comparator { private final TvInputManagerHelper mInputManager; public TvInputInfoComparator(TvInputManagerHelper inputManager) { mInputManager = inputManager; } @Override public int compare(TvInputInfo lhs, TvInputInfo rhs) { if (mInputManager.isPartnerInput(lhs) != mInputManager.isPartnerInput(rhs)) { return mInputManager.isPartnerInput(lhs) ? -1 : 1; } return mInputManager.loadLabel(lhs).compareTo(mInputManager.loadLabel(rhs)); } } }