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