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         mInputManager.addCallback(mTvInputCallback);
153         mChannelTuner.addListener(mChannelTunerListener);
154         initializePipInputList();
155     }
156 
157     /**
158      * Stops {@link PipInputManager}.
159      */
stop()160     public void stop() {
161         if (!mStarted) {
162             return;
163         }
164         mInputManager.removeCallback(mTvInputCallback);
165         mChannelTuner.removeListener(mChannelTunerListener);
166         mPipInputMap.clear();
167     }
168 
169     /**
170      * Adds a {@link PipInputManager.Listener}.
171      */
addListener(Listener listener)172     public void addListener(Listener listener) {
173         mListeners.add(listener);
174     }
175 
176     /**
177      * Removes a {@link PipInputManager.Listener}.
178      */
removeListener(Listener listener)179     public void removeListener(Listener listener) {
180         mListeners.remove(listener);
181     }
182 
183     /**
184      * Gets the size of inputs for PIP.
185      *
186      * <p>The hidden inputs are not counted.
187      *
188      * @param availableOnly If {@code true}, it counts only available PIP inputs. Please see {@link
189      *        PipInput#isAvailable()} for the details of availability.
190      */
getPipInputSize(boolean availableOnly)191     public int getPipInputSize(boolean availableOnly) {
192         int count = 0;
193         for (PipInput pipInput : mPipInputMap.values()) {
194             if (!pipInput.isHidden() && (!availableOnly || pipInput.mAvailable)) {
195                 ++count;
196             }
197             if (pipInput.isPassthrough()) {
198                 TvInputInfo info = pipInput.getInputInfo();
199                 // Do not count HDMI ports if a CEC device is directly connected to the port.
200                 if (info.getParentId() != null && !info.isConnectedToHdmiSwitch()) {
201                     --count;
202                 }
203             }
204         }
205         return count;
206     }
207 
208     /**
209      * Gets the list of inputs for PIP..
210      *
211      * <p>The hidden inputs are excluded.
212      *
213      * @param availableOnly If true, it returns only available PIP inputs. Please see {@link
214      *        PipInput#isAvailable()} for the details of availability.
215      */
getPipInputList(boolean availableOnly)216     public List<PipInput> getPipInputList(boolean availableOnly) {
217         List<PipInput> pipInputs = new ArrayList<>();
218         List<PipInput> removeInputs = new ArrayList<>();
219         for (PipInput pipInput : mPipInputMap.values()) {
220             if (!pipInput.isHidden() && (!availableOnly || pipInput.mAvailable)) {
221                 pipInputs.add(pipInput);
222             }
223             if (pipInput.isPassthrough()) {
224                 TvInputInfo info = pipInput.getInputInfo();
225                 // Do not show HDMI ports if a CEC device is directly connected to the port.
226                 if (info.getParentId() != null && !info.isConnectedToHdmiSwitch()) {
227                     removeInputs.add(mPipInputMap.get(info.getParentId()));
228                 }
229             }
230         }
231         if (!removeInputs.isEmpty()) {
232             pipInputs.removeAll(removeInputs);
233         }
234         Collections.sort(pipInputs, new Comparator<PipInput>() {
235             @Override
236             public int compare(PipInput lhs, PipInput rhs) {
237                 if (!lhs.mIsPassthrough) {
238                     return -1;
239                 }
240                 if (!rhs.mIsPassthrough) {
241                     return 1;
242                 }
243                 String a = lhs.getLabel();
244                 String b = rhs.getLabel();
245                 return a.compareTo(b);
246             }
247         });
248         return pipInputs;
249     }
250 
251     /**
252      * Returns an PIP input corresponding to {@code channel}.
253      */
getPipInput(Channel channel)254     public PipInput getPipInput(Channel channel) {
255         if (channel == null) {
256             return null;
257         }
258         if (channel.isPassthrough()) {
259             return mPipInputMap.get(channel.getInputId());
260         } else {
261             return mPipInputMap.get(TUNER_INPUT_ID);
262         }
263     }
264 
265     /**
266      * Returns true, if {@code channel1} and {@code channel2} belong to the same input. For example,
267      * two channels from different tuner inputs are also in the same input "Tuner" from PIP
268      * point of view.
269      */
areInSamePipInput(Channel channel1, Channel channel2)270     public boolean areInSamePipInput(Channel channel1, Channel channel2) {
271         PipInput input1 = getPipInput(channel1);
272         PipInput input2 = getPipInput(channel2);
273         return input1 != null && input2 != null
274                 && getPipInput(channel1).equals(getPipInput(channel2));
275     }
276 
initializePipInputList()277     private void initializePipInputList() {
278         boolean hasTunerInput = false;
279         for (TvInputInfo input : mInputManager.getTvInputInfos(false, false)) {
280             if (input.isPassthroughInput()) {
281                 boolean available = mInputManager.getInputState(input)
282                         == TvInputManager.INPUT_STATE_CONNECTED;
283                 mPipInputMap.put(input.getId(), new PipInput(input.getId(), available));
284             } else if (!hasTunerInput) {
285                 hasTunerInput = true;
286                 boolean available = mChannelTuner.getBrowsableChannelCount() != 0;
287                 mPipInputMap.put(TUNER_INPUT_ID, new PipInput(TUNER_INPUT_ID, available));
288             }
289         }
290         PipInput input = getPipInput(mChannelTuner.getCurrentChannel());
291         if (input != null) {
292             input.updateAvailability();
293         }
294         for (Listener l : mListeners) {
295             l.onPipInputListUpdated();
296         }
297     }
298 
299     /**
300      * Listeners to notify PIP input state changes.
301      */
302     public interface Listener {
303         /**
304          * Called when the state (availability) of PIP inputs is changed.
305          */
onPipInputStateUpdated()306         void onPipInputStateUpdated();
307 
308         /**
309          * Called when the list of PIP inputs is changed.
310          */
onPipInputListUpdated()311         void onPipInputListUpdated();
312     }
313 
314     /**
315      * Input class for PIP. It has useful methods for PIP handling.
316      */
317     public class PipInput {
318         private final String mInputId;
319         private final boolean mIsPassthrough;
320         private final TvInputInfo mInputInfo;
321         private boolean mAvailable;
322 
PipInput(String inputId, boolean available)323         private PipInput(String inputId, boolean available) {
324             mInputId = inputId;
325             mIsPassthrough = !mInputId.equals(TUNER_INPUT_ID);
326             if (mIsPassthrough) {
327                 mInputInfo = mInputManager.getTvInputInfo(mInputId);
328             } else {
329                 mInputInfo = null;
330             }
331             mAvailable = available;
332         }
333 
334         /**
335          * Returns the {@link TvInputInfo} object that matches to this PIP input.
336          */
getInputInfo()337         public TvInputInfo getInputInfo() {
338             return mInputInfo;
339         }
340 
341         /**
342          * Returns {@code true}, if the input is available for PIP. If a channel of an input is
343          * already played or an input is not connected state or there is no browsable channel, the
344          * input is unavailable.
345          */
isAvailable()346         public boolean isAvailable() {
347             return mAvailable;
348         }
349 
350         /**
351          * Returns true, if the input is a passthrough TV input.
352          */
isPassthrough()353         public boolean isPassthrough() {
354             return mIsPassthrough;
355         }
356 
357         /**
358          * Gets a channel to play in a PIP view.
359          */
getChannel()360         public Channel getChannel() {
361             if (mIsPassthrough) {
362                 return Channel.createPassthroughChannel(mInputId);
363             } else {
364                 return mChannelTuner.findNearestBrowsableChannel(
365                         Utils.getLastWatchedChannelId(mContext));
366             }
367         }
368 
369         /**
370          * Gets a label of the input.
371          */
getLabel()372         public String getLabel() {
373             if (mIsPassthrough) {
374                 return mInputInfo.loadLabel(mContext).toString();
375             } else {
376                 return mContext.getString(R.string.input_selector_tuner_label);
377             }
378         }
379 
380         /**
381          * Gets a long label including a customized label.
382          */
getLongLabel()383         public String getLongLabel() {
384             if (mIsPassthrough) {
385                 String customizedLabel = Utils.loadLabel(mContext, mInputInfo);
386                 String label = getLabel();
387                 if (label.equals(customizedLabel)) {
388                     return customizedLabel;
389                 }
390                 return customizedLabel + " (" + label + ")";
391             } else {
392                 return mContext.getString(R.string.input_long_label_for_tuner);
393             }
394         }
395 
396         /**
397          * Updates availability. It returns true, if availability is changed.
398          */
updateAvailability()399         private void updateAvailability() {
400             boolean available;
401             // current playing input cannot be available for PIP.
402             Channel currentChannel = mChannelTuner.getCurrentChannel();
403             if (mIsPassthrough) {
404                 if (currentChannel != null && currentChannel.getInputId().equals(mInputId)) {
405                     available = false;
406                 } else {
407                     available = mInputManager.getInputState(mInputId)
408                             == TvInputManager.INPUT_STATE_CONNECTED;
409                 }
410             } else {
411                 if (currentChannel != null && !currentChannel.isPassthrough()) {
412                     available = false;
413                 } else {
414                     available = mChannelTuner.getBrowsableChannelCount() > 0;
415                 }
416             }
417             if (mAvailable != available) {
418                 mAvailable = available;
419                 for (Listener l : mListeners) {
420                     l.onPipInputStateUpdated();
421                 }
422             }
423         }
424 
isHidden()425         private boolean isHidden() {
426             // mInputInfo is null for the tuner input and it's always visible.
427             return mInputInfo != null && mInputInfo.isHidden(mContext);
428         }
429     }
430 }
431