/* * 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.media.tv.TvInputInfo; import android.media.tv.TvInputManager; import android.media.tv.TvInputManager.TvInputCallback; import android.util.Log; import com.android.tv.ChannelTuner; import com.android.tv.R; import com.android.tv.data.Channel; 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; import java.util.Set; /** * A class that manages inputs for PIP. All tuner inputs are represented to one tuner input for PIP. */ public class PipInputManager { private static final String TAG = "PipInputManager"; // Tuner inputs aren't distinguished each other in PipInput. They are handled as one input. // Therefore, we define a fake input id for the unified input. private static final String TUNER_INPUT_ID = "tuner_input_id"; private final Context mContext; private final TvInputManagerHelper mInputManager; private final ChannelTuner mChannelTuner; private boolean mStarted; private final Map mPipInputMap = new HashMap<>(); // inputId -> PipInput private final Set mListeners = new HashSet<>(); private final TvInputCallback mTvInputCallback = new TvInputCallback() { @Override public void onInputAdded(String inputId) { TvInputInfo input = mInputManager.getTvInputInfo(inputId); if (input.isPassthroughInput()) { boolean available = mInputManager.getInputState(input) == TvInputManager.INPUT_STATE_CONNECTED; mPipInputMap.put(inputId, new PipInput(inputId, available)); } else if (!mPipInputMap.containsKey(TUNER_INPUT_ID)) { boolean available = mChannelTuner.getBrowsableChannelCount() != 0; mPipInputMap.put(TUNER_INPUT_ID, new PipInput(TUNER_INPUT_ID, available)); } else { return; } for (Listener l : mListeners) { l.onPipInputListUpdated(); } } @Override public void onInputRemoved(String inputId) { PipInput pipInput = mPipInputMap.remove(inputId); if (pipInput == null) { if (!mPipInputMap.containsKey(TUNER_INPUT_ID)) { Log.w(TAG, "A TV input (" + inputId + ") isn't tracked in PipInputManager"); return; } if (mInputManager.getTunerTvInputSize() > 0) { return; } mPipInputMap.remove(TUNER_INPUT_ID); } for (Listener l : mListeners) { l.onPipInputListUpdated(); } } @Override public void onInputStateChanged(String inputId, int state) { PipInput pipInput = mPipInputMap.get(inputId); if (pipInput == null) { // For tuner input, state change is handled in mChannelTunerListener. return; } pipInput.updateAvailability(); } }; private final ChannelTuner.Listener mChannelTunerListener = new ChannelTuner.Listener() { @Override public void onLoadFinished() { } @Override public void onCurrentChannelUnavailable(Channel channel) { } @Override public void onBrowsableChannelListChanged() { PipInput tunerInput = mPipInputMap.get(TUNER_INPUT_ID); if (tunerInput == null) { return; } tunerInput.updateAvailability(); } @Override public void onChannelChanged(Channel previousChannel, Channel currentChannel) { if (previousChannel != null && currentChannel != null && !previousChannel.isPassthrough() && !currentChannel.isPassthrough()) { // Channel change between channels for tuner inputs. return; } PipInput previousMainInput = getPipInput(previousChannel); if (previousMainInput != null) { previousMainInput.updateAvailability(); } PipInput currentMainInput = getPipInput(currentChannel); if (currentMainInput != null) { currentMainInput.updateAvailability(); } } }; public PipInputManager(Context context, TvInputManagerHelper inputManager, ChannelTuner channelTuner) { mContext = context; mInputManager = inputManager; mChannelTuner = channelTuner; } /** * Starts {@link PipInputManager}. */ public void start() { if (mStarted) { return; } mInputManager.addCallback(mTvInputCallback); mChannelTuner.addListener(mChannelTunerListener); initializePipInputList(); } /** * Stops {@link PipInputManager}. */ public void stop() { if (!mStarted) { return; } mInputManager.removeCallback(mTvInputCallback); mChannelTuner.removeListener(mChannelTunerListener); mPipInputMap.clear(); } /** * Adds a {@link PipInputManager.Listener}. */ public void addListener(Listener listener) { mListeners.add(listener); } /** * Removes a {@link PipInputManager.Listener}. */ public void removeListener(Listener listener) { mListeners.remove(listener); } /** * Gets the size of inputs for PIP. * * @param availableOnly If true, it counts only available PIP inputs. Please see {@link * PipInput#isAvailable()} for the details of availability. */ public int getPipInputSize(boolean availableOnly) { if (availableOnly) { int count = 0; for (PipInput pipInput : mPipInputMap.values()) { if (pipInput.isAvailable()) { ++count; } } return count; } else { return mPipInputMap.size(); } } /** * Gets the list of inputs for PIP. * * @param availableOnly If true, it returns only available PIP inputs. Please see {@link * PipInput#isAvailable()} for the details of availability. */ public List getPipInputList(boolean availableOnly) { List pipInputs; if (availableOnly) { pipInputs = new ArrayList<>(); for (PipInput pipInput : mPipInputMap.values()) { if (pipInput.mAvailable) { pipInputs.add(pipInput); } } } else { pipInputs = new ArrayList<>(mPipInputMap.values()); } Collections.sort(pipInputs, new Comparator() { @Override public int compare(PipInput lhs, PipInput rhs) { if (!lhs.mIsPassthrough) { return -1; } if (!rhs.mIsPassthrough) { return 1; } String a = lhs.getLabel(); String b = rhs.getLabel(); return a.compareTo(b); } }); return pipInputs; } /** * Returns an PIP input corresponding to {@code channel}. */ public PipInput getPipInput(Channel channel) { if (channel == null) { return null; } if (channel.isPassthrough()) { return mPipInputMap.get(channel.getInputId()); } else { return mPipInputMap.get(TUNER_INPUT_ID); } } /** * Returns true, if {@code channel1} and {@code channel2} belong to the same input. For example, * two channels from different tuner inputs are also in the same input "Tuner" from PIP * point of view. */ public boolean areInSamePipInput(Channel channel1, Channel channel2) { PipInput input1 = getPipInput(channel1); PipInput input2 = getPipInput(channel2); return input1 != null && input2 != null && getPipInput(channel1).equals(getPipInput(channel2)); } private void initializePipInputList() { boolean hasTunerInput = false; for (TvInputInfo input : mInputManager.getTvInputInfos(false, false)) { if (input.isPassthroughInput()) { boolean available = mInputManager.getInputState(input) == TvInputManager.INPUT_STATE_CONNECTED; mPipInputMap.put(input.getId(), new PipInput(input.getId(), available)); } else if (!hasTunerInput) { hasTunerInput = true; boolean available = mChannelTuner.getBrowsableChannelCount() != 0; mPipInputMap.put(TUNER_INPUT_ID, new PipInput(TUNER_INPUT_ID, available)); } } PipInput input = getPipInput(mChannelTuner.getCurrentChannel()); if (input != null) { input.updateAvailability(); } for (Listener l : mListeners) { l.onPipInputListUpdated(); } } /** * Listeners to notify PIP input state changes. */ public interface Listener { /** * Called when the state (availability) of PIP inputs is changed. */ void onPipInputStateUpdated(); /** * Called when the list of PIP inputs is changed. */ void onPipInputListUpdated(); } /** * Input class for PIP. It has useful methods for PIP handling. */ public class PipInput { private final String mInputId; private final boolean mIsPassthrough; private final TvInputInfo mInputInfo; private boolean mAvailable; private PipInput(String inputId, boolean available) { mInputId = inputId; mIsPassthrough = !mInputId.equals(TUNER_INPUT_ID); if (mIsPassthrough) { mInputInfo = mInputManager.getTvInputInfo(mInputId); } else { mInputInfo = null; } mAvailable = available; } /** * Returns the {@link TvInputInfo} object that matches to this PIP input. */ public TvInputInfo getInputInfo() { return mInputInfo; } /** * Returns true, if the input is available for PIP. If a channel of an input is already * played or an input is not connected state or there is no browsable channel, the input * is unavailable. */ public boolean isAvailable() { return mAvailable; } /** * Returns true, if the input is a passthrough TV input. */ public boolean isPassthrough() { return mIsPassthrough; } /** * Gets a channel to play in a PIP view. */ public Channel getChannel() { if (mIsPassthrough) { return Channel.createPassthroughChannel(mInputId); } else { return mChannelTuner.findNearestBrowsableChannel( Utils.getLastWatchedChannelId(mContext)); } } /** * Gets a label of the input. */ public String getLabel() { if (mIsPassthrough) { return mInputInfo.loadLabel(mContext).toString(); } else { return mContext.getString(R.string.input_selector_tuner_label); } } /** * Gets a long label including a customized label. */ public String getLongLabel() { if (mIsPassthrough) { String customizedLabel = Utils.loadLabel(mContext, mInputInfo); String label = getLabel(); if (label.equals(customizedLabel)) { return customizedLabel; } return customizedLabel + " (" + label + ")"; } else { return mContext.getString(R.string.input_long_label_for_tuner); } } /** * Updates availability. It returns true, if availability is changed. */ private void updateAvailability() { boolean available; // current playing input cannot be available for PIP. Channel currentChannel = mChannelTuner.getCurrentChannel(); if (mIsPassthrough) { if (currentChannel != null && currentChannel.getInputId().equals(mInputId)) { available = false; } else { available = mInputManager.getInputState(mInputId) == TvInputManager.INPUT_STATE_CONNECTED; } } else { if (currentChannel != null && !currentChannel.isPassthrough()) { available = false; } else { available = mChannelTuner.getBrowsableChannelCount() > 0; } } if (mAvailable != available) { mAvailable = available; for (Listener l : mListeners) { l.onPipInputStateUpdated(); } } } } }