Home | History | Annotate | Download | only in util
      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 
    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      */
    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      */
    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      */
    174     public void addListener(Listener listener) {
    175         mListeners.add(listener);
    176     }
    177 
    178     /**
    179      * Removes a {@link PipInputManager.Listener}.
    180      */
    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      */
    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      */
    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      */
    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      */
    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 
    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          */
    308         void onPipInputStateUpdated();
    309 
    310         /**
    311          * Called when the list of PIP inputs is changed.
    312          */
    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 
    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          */
    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          */
    348         public boolean isAvailable() {
    349             return mAvailable;
    350         }
    351 
    352         /**
    353          * Returns true, if the input is a passthrough TV input.
    354          */
    355         public boolean isPassthrough() {
    356             return mIsPassthrough;
    357         }
    358 
    359         /**
    360          * Gets a channel to play in a PIP view.
    361          */
    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          */
    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          */
    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          */
    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 
    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