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.media.tv.TvInputInfo; 21 import android.media.tv.TvInputManager; 22 import android.media.tv.TvInputManager.TvInputCallback; 23 import android.util.ArraySet; 24 import android.util.Log; 25 26 import com.android.tv.ChannelTuner; 27 import com.android.tv.R; 28 import com.android.tv.data.Channel; 29 30 import java.util.ArrayList; 31 import java.util.Collections; 32 import java.util.Comparator; 33 import java.util.HashMap; 34 import java.util.List; 35 import java.util.Map; 36 import java.util.Set; 37 38 /** 39 * A class that manages inputs for PIP. All tuner inputs are represented to one tuner input for PIP. 40 * Hidden inputs should not be visible to the users. 41 */ 42 public class PipInputManager { 43 private static final String TAG = "PipInputManager"; 44 45 // Tuner inputs aren't distinguished each other in PipInput. They are handled as one input. 46 // Therefore, we define a fake input id for the unified input. 47 private static final String TUNER_INPUT_ID = "tuner_input_id"; 48 49 private final Context mContext; 50 private final TvInputManagerHelper mInputManager; 51 private final ChannelTuner mChannelTuner; 52 private boolean mStarted; 53 private final Map<String, PipInput> mPipInputMap = new HashMap<>(); // inputId -> PipInput 54 private final Set<Listener> mListeners = new ArraySet<>(); 55 56 private final TvInputCallback mTvInputCallback = new TvInputCallback() { 57 @Override 58 public void onInputAdded(String inputId) { 59 TvInputInfo input = mInputManager.getTvInputInfo(inputId); 60 if (input.isPassthroughInput()) { 61 boolean available = mInputManager.getInputState(input) 62 == TvInputManager.INPUT_STATE_CONNECTED; 63 mPipInputMap.put(inputId, new PipInput(inputId, available)); 64 } else if (!mPipInputMap.containsKey(TUNER_INPUT_ID)) { 65 boolean available = mChannelTuner.getBrowsableChannelCount() != 0; 66 mPipInputMap.put(TUNER_INPUT_ID, new PipInput(TUNER_INPUT_ID, available)); 67 } else { 68 return; 69 } 70 for (Listener l : mListeners) { 71 l.onPipInputListUpdated(); 72 } 73 } 74 75 @Override 76 public void onInputRemoved(String inputId) { 77 PipInput pipInput = mPipInputMap.remove(inputId); 78 if (pipInput == null) { 79 if (!mPipInputMap.containsKey(TUNER_INPUT_ID)) { 80 Log.w(TAG, "A TV input (" + inputId + ") isn't tracked in PipInputManager"); 81 return; 82 } 83 if (mInputManager.getTunerTvInputSize() > 0) { 84 return; 85 } 86 mPipInputMap.remove(TUNER_INPUT_ID); 87 } 88 for (Listener l : mListeners) { 89 l.onPipInputListUpdated(); 90 } 91 } 92 93 @Override 94 public void onInputStateChanged(String inputId, int state) { 95 PipInput pipInput = mPipInputMap.get(inputId); 96 if (pipInput == null) { 97 // For tuner input, state change is handled in mChannelTunerListener. 98 return; 99 } 100 pipInput.updateAvailability(); 101 } 102 }; 103 104 private final ChannelTuner.Listener mChannelTunerListener = new ChannelTuner.Listener() { 105 @Override 106 public void onLoadFinished() { } 107 108 @Override 109 public void onCurrentChannelUnavailable(Channel channel) { } 110 111 @Override 112 public void onBrowsableChannelListChanged() { 113 PipInput tunerInput = mPipInputMap.get(TUNER_INPUT_ID); 114 if (tunerInput == null) { 115 return; 116 } 117 tunerInput.updateAvailability(); 118 } 119 120 @Override 121 public void onChannelChanged(Channel previousChannel, Channel currentChannel) { 122 if (previousChannel != null && currentChannel != null 123 && !previousChannel.isPassthrough() && !currentChannel.isPassthrough()) { 124 // Channel change between channels for tuner inputs. 125 return; 126 } 127 PipInput previousMainInput = getPipInput(previousChannel); 128 if (previousMainInput != null) { 129 previousMainInput.updateAvailability(); 130 } 131 PipInput currentMainInput = getPipInput(currentChannel); 132 if (currentMainInput != null) { 133 currentMainInput.updateAvailability(); 134 } 135 } 136 }; 137 PipInputManager(Context context, TvInputManagerHelper inputManager, ChannelTuner channelTuner)138 public PipInputManager(Context context, TvInputManagerHelper inputManager, 139 ChannelTuner channelTuner) { 140 mContext = context; 141 mInputManager = inputManager; 142 mChannelTuner = channelTuner; 143 } 144 145 /** 146 * Starts {@link PipInputManager}. 147 */ start()148 public void start() { 149 if (mStarted) { 150 return; 151 } 152 mStarted = true; 153 mInputManager.addCallback(mTvInputCallback); 154 mChannelTuner.addListener(mChannelTunerListener); 155 initializePipInputList(); 156 } 157 158 /** 159 * Stops {@link PipInputManager}. 160 */ stop()161 public void stop() { 162 if (!mStarted) { 163 return; 164 } 165 mStarted = false; 166 mInputManager.removeCallback(mTvInputCallback); 167 mChannelTuner.removeListener(mChannelTunerListener); 168 mPipInputMap.clear(); 169 } 170 171 /** 172 * Adds a {@link PipInputManager.Listener}. 173 */ addListener(Listener listener)174 public void addListener(Listener listener) { 175 mListeners.add(listener); 176 } 177 178 /** 179 * Removes a {@link PipInputManager.Listener}. 180 */ removeListener(Listener listener)181 public void removeListener(Listener listener) { 182 mListeners.remove(listener); 183 } 184 185 /** 186 * Gets the size of inputs for PIP. 187 * 188 * <p>The hidden inputs are not counted. 189 * 190 * @param availableOnly If {@code true}, it counts only available PIP inputs. Please see {@link 191 * PipInput#isAvailable()} for the details of availability. 192 */ getPipInputSize(boolean availableOnly)193 public int getPipInputSize(boolean availableOnly) { 194 int count = 0; 195 for (PipInput pipInput : mPipInputMap.values()) { 196 if (!pipInput.isHidden() && (!availableOnly || pipInput.mAvailable)) { 197 ++count; 198 } 199 if (pipInput.isPassthrough()) { 200 TvInputInfo info = pipInput.getInputInfo(); 201 // Do not count HDMI ports if a CEC device is directly connected to the port. 202 if (info.getParentId() != null && !info.isConnectedToHdmiSwitch()) { 203 --count; 204 } 205 } 206 } 207 return count; 208 } 209 210 /** 211 * Gets the list of inputs for PIP.. 212 * 213 * <p>The hidden inputs are excluded. 214 * 215 * @param availableOnly If true, it returns only available PIP inputs. Please see {@link 216 * PipInput#isAvailable()} for the details of availability. 217 */ getPipInputList(boolean availableOnly)218 public List<PipInput> getPipInputList(boolean availableOnly) { 219 List<PipInput> pipInputs = new ArrayList<>(); 220 List<PipInput> removeInputs = new ArrayList<>(); 221 for (PipInput pipInput : mPipInputMap.values()) { 222 if (!pipInput.isHidden() && (!availableOnly || pipInput.mAvailable)) { 223 pipInputs.add(pipInput); 224 } 225 if (pipInput.isPassthrough()) { 226 TvInputInfo info = pipInput.getInputInfo(); 227 // Do not show HDMI ports if a CEC device is directly connected to the port. 228 if (info.getParentId() != null && !info.isConnectedToHdmiSwitch()) { 229 removeInputs.add(mPipInputMap.get(info.getParentId())); 230 } 231 } 232 } 233 if (!removeInputs.isEmpty()) { 234 pipInputs.removeAll(removeInputs); 235 } 236 Collections.sort(pipInputs, new Comparator<PipInput>() { 237 @Override 238 public int compare(PipInput lhs, PipInput rhs) { 239 if (!lhs.mIsPassthrough) { 240 return -1; 241 } 242 if (!rhs.mIsPassthrough) { 243 return 1; 244 } 245 String a = lhs.getLabel(); 246 String b = rhs.getLabel(); 247 return a.compareTo(b); 248 } 249 }); 250 return pipInputs; 251 } 252 253 /** 254 * Returns an PIP input corresponding to {@code channel}. 255 */ getPipInput(Channel channel)256 public PipInput getPipInput(Channel channel) { 257 if (channel == null) { 258 return null; 259 } 260 if (channel.isPassthrough()) { 261 return mPipInputMap.get(channel.getInputId()); 262 } else { 263 return mPipInputMap.get(TUNER_INPUT_ID); 264 } 265 } 266 267 /** 268 * Returns true, if {@code channel1} and {@code channel2} belong to the same input. For example, 269 * two channels from different tuner inputs are also in the same input "Tuner" from PIP 270 * point of view. 271 */ areInSamePipInput(Channel channel1, Channel channel2)272 public boolean areInSamePipInput(Channel channel1, Channel channel2) { 273 PipInput input1 = getPipInput(channel1); 274 PipInput input2 = getPipInput(channel2); 275 return input1 != null && input2 != null 276 && getPipInput(channel1).equals(getPipInput(channel2)); 277 } 278 initializePipInputList()279 private void initializePipInputList() { 280 boolean hasTunerInput = false; 281 for (TvInputInfo input : mInputManager.getTvInputInfos(false, false)) { 282 if (input.isPassthroughInput()) { 283 boolean available = mInputManager.getInputState(input) 284 == TvInputManager.INPUT_STATE_CONNECTED; 285 mPipInputMap.put(input.getId(), new PipInput(input.getId(), available)); 286 } else if (!hasTunerInput) { 287 hasTunerInput = true; 288 boolean available = mChannelTuner.getBrowsableChannelCount() != 0; 289 mPipInputMap.put(TUNER_INPUT_ID, new PipInput(TUNER_INPUT_ID, available)); 290 } 291 } 292 PipInput input = getPipInput(mChannelTuner.getCurrentChannel()); 293 if (input != null) { 294 input.updateAvailability(); 295 } 296 for (Listener l : mListeners) { 297 l.onPipInputListUpdated(); 298 } 299 } 300 301 /** 302 * Listeners to notify PIP input state changes. 303 */ 304 public interface Listener { 305 /** 306 * Called when the state (availability) of PIP inputs is changed. 307 */ onPipInputStateUpdated()308 void onPipInputStateUpdated(); 309 310 /** 311 * Called when the list of PIP inputs is changed. 312 */ onPipInputListUpdated()313 void onPipInputListUpdated(); 314 } 315 316 /** 317 * Input class for PIP. It has useful methods for PIP handling. 318 */ 319 public class PipInput { 320 private final String mInputId; 321 private final boolean mIsPassthrough; 322 private final TvInputInfo mInputInfo; 323 private boolean mAvailable; 324 PipInput(String inputId, boolean available)325 private PipInput(String inputId, boolean available) { 326 mInputId = inputId; 327 mIsPassthrough = !mInputId.equals(TUNER_INPUT_ID); 328 if (mIsPassthrough) { 329 mInputInfo = mInputManager.getTvInputInfo(mInputId); 330 } else { 331 mInputInfo = null; 332 } 333 mAvailable = available; 334 } 335 336 /** 337 * Returns the {@link TvInputInfo} object that matches to this PIP input. 338 */ getInputInfo()339 public TvInputInfo getInputInfo() { 340 return mInputInfo; 341 } 342 343 /** 344 * Returns {@code true}, if the input is available for PIP. If a channel of an input is 345 * already played or an input is not connected state or there is no browsable channel, the 346 * input is unavailable. 347 */ isAvailable()348 public boolean isAvailable() { 349 return mAvailable; 350 } 351 352 /** 353 * Returns true, if the input is a passthrough TV input. 354 */ isPassthrough()355 public boolean isPassthrough() { 356 return mIsPassthrough; 357 } 358 359 /** 360 * Gets a channel to play in a PIP view. 361 */ getChannel()362 public Channel getChannel() { 363 if (mIsPassthrough) { 364 return Channel.createPassthroughChannel(mInputId); 365 } else { 366 return mChannelTuner.findNearestBrowsableChannel( 367 Utils.getLastWatchedChannelId(mContext)); 368 } 369 } 370 371 /** 372 * Gets a label of the input. 373 */ getLabel()374 public String getLabel() { 375 if (mIsPassthrough) { 376 return mInputInfo.loadLabel(mContext).toString(); 377 } else { 378 return mContext.getString(R.string.input_selector_tuner_label); 379 } 380 } 381 382 /** 383 * Gets a long label including a customized label. 384 */ getLongLabel()385 public String getLongLabel() { 386 if (mIsPassthrough) { 387 String customizedLabel = Utils.loadLabel(mContext, mInputInfo); 388 String label = getLabel(); 389 if (label.equals(customizedLabel)) { 390 return customizedLabel; 391 } 392 return customizedLabel + " (" + label + ")"; 393 } else { 394 return mContext.getString(R.string.input_long_label_for_tuner); 395 } 396 } 397 398 /** 399 * Updates availability. It returns true, if availability is changed. 400 */ updateAvailability()401 private void updateAvailability() { 402 boolean available; 403 // current playing input cannot be available for PIP. 404 Channel currentChannel = mChannelTuner.getCurrentChannel(); 405 if (mIsPassthrough) { 406 if (currentChannel != null && currentChannel.getInputId().equals(mInputId)) { 407 available = false; 408 } else { 409 available = mInputManager.getInputState(mInputId) 410 == TvInputManager.INPUT_STATE_CONNECTED; 411 } 412 } else { 413 if (currentChannel != null && !currentChannel.isPassthrough()) { 414 available = false; 415 } else { 416 available = mChannelTuner.getBrowsableChannelCount() > 0; 417 } 418 } 419 if (mAvailable != available) { 420 mAvailable = available; 421 for (Listener l : mListeners) { 422 l.onPipInputStateUpdated(); 423 } 424 } 425 } 426 isHidden()427 private boolean isHidden() { 428 // mInputInfo is null for the tuner input and it's always visible. 429 return mInputInfo != null && mInputInfo.isHidden(mContext); 430 } 431 } 432 } 433