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
17package com.android.tv.util;
18
19import android.content.Context;
20import android.media.tv.TvInputInfo;
21import android.media.tv.TvInputManager;
22import android.media.tv.TvInputManager.TvInputCallback;
23import android.util.ArraySet;
24import android.util.Log;
25
26import com.android.tv.ChannelTuner;
27import com.android.tv.R;
28import com.android.tv.data.Channel;
29
30import java.util.ArrayList;
31import java.util.Collections;
32import java.util.Comparator;
33import java.util.HashMap;
34import java.util.List;
35import java.util.Map;
36import 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 */
42public 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        mInputManager.addCallback(mTvInputCallback);
153        mChannelTuner.addListener(mChannelTunerListener);
154        initializePipInputList();
155    }
156
157    /**
158     * Stops {@link PipInputManager}.
159     */
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     */
172    public void addListener(Listener listener) {
173        mListeners.add(listener);
174    }
175
176    /**
177     * Removes a {@link PipInputManager.Listener}.
178     */
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     */
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     */
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     */
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     */
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
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         */
306        void onPipInputStateUpdated();
307
308        /**
309         * Called when the list of PIP inputs is changed.
310         */
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
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         */
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         */
346        public boolean isAvailable() {
347            return mAvailable;
348        }
349
350        /**
351         * Returns true, if the input is a passthrough TV input.
352         */
353        public boolean isPassthrough() {
354            return mIsPassthrough;
355        }
356
357        /**
358         * Gets a channel to play in a PIP view.
359         */
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         */
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         */
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         */
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
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