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