ChannelTuner.java revision ba5845f23b8fbc985890f892961abc8b39886611
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;
18
19import android.media.tv.TvContract;
20import android.media.tv.TvInputInfo;
21import android.net.Uri;
22import android.os.Handler;
23import android.support.annotation.MainThread;
24import android.support.annotation.Nullable;
25import android.util.Log;
26
27import com.android.tv.common.CollectionUtils;
28import com.android.tv.data.Channel;
29import com.android.tv.data.ChannelDataManager;
30import com.android.tv.util.SoftPreconditions;
31import com.android.tv.util.TvInputManagerHelper;
32
33import java.util.ArrayList;
34import java.util.Collections;
35import java.util.HashMap;
36import java.util.List;
37import java.util.Map;
38import java.util.Set;
39
40/**
41 * It manages the current tuned channel among browsable channels. And it determines the next channel
42 * by channel up/down. But, it doesn't actually tune through TvView.
43 */
44@MainThread
45public class ChannelTuner {
46    private static final String TAG = "ChannelTuner";
47
48    private boolean mStarted;
49    private boolean mChannelDataManagerLoaded;
50    private final List<Channel> mChannels = new ArrayList<>();
51    private final List<Channel> mBrowsableChannels = new ArrayList<>();
52    private final Map<Long, Channel> mChannelMap = new HashMap<>();
53    // TODO: need to check that mChannelIndexMap can be removed, once mCurrentChannelIndex
54    // is changed to mCurrentChannel(Id).
55    private final Map<Long, Integer> mChannelIndexMap = new HashMap<>();
56
57    private final Handler mHandler = new Handler();
58    private final ChannelDataManager mChannelDataManager;
59    private final Set<Listener> mListeners = CollectionUtils.createSmallSet();
60    @Nullable
61    private Channel mCurrentChannel;
62    private final TvInputManagerHelper mInputManager;
63    @Nullable
64    private TvInputInfo mCurrentChannelInputInfo;
65
66    private final ChannelDataManager.Listener mChannelDataManagerListener =
67            new ChannelDataManager.Listener() {
68                @Override
69                public void onLoadFinished() {
70                    mChannelDataManagerLoaded = true;
71                    updateChannelData(mChannelDataManager.getChannelList());
72                    for (Listener l : mListeners) {
73                        l.onLoadFinished();
74                    }
75                }
76
77                @Override
78                public void onChannelListUpdated() {
79                    updateChannelData(mChannelDataManager.getChannelList());
80                }
81
82                @Override
83                public void onChannelBrowsableChanged() {
84                    updateBrowsableChannels();
85                    for (Listener l : mListeners) {
86                        l.onBrowsableChannelListChanged();
87                    }
88                }
89    };
90
91    public ChannelTuner(ChannelDataManager channelDataManager, TvInputManagerHelper inputManager) {
92        mChannelDataManager = channelDataManager;
93        mInputManager = inputManager;
94    }
95
96    /**
97     * Starts ChannelTuner. It cannot be called twice before calling {@link #stop}.
98     */
99    public void start() {
100        if (mStarted) {
101            throw new IllegalStateException("start is called twice");
102        }
103        mStarted = true;
104        mChannelDataManager.addListener(mChannelDataManagerListener);
105        if (mChannelDataManager.isDbLoadFinished()) {
106            mHandler.post(new Runnable() {
107                @Override
108                public void run() {
109                    mChannelDataManagerListener.onLoadFinished();
110                }
111            });
112        }
113    }
114
115    /**
116     * Stops ChannelTuner.
117     */
118    public void stop() {
119        if (!mStarted) {
120            return;
121        }
122        mStarted = false;
123        mHandler.removeCallbacksAndMessages(null);
124        mChannelDataManager.removeListener(mChannelDataManagerListener);
125        mCurrentChannel = null;
126        mChannels.clear();
127        mBrowsableChannels.clear();
128        mChannelMap.clear();
129        mChannelIndexMap.clear();
130        mChannelDataManagerLoaded = false;
131    }
132
133    /**
134     * Returns true, if all the channels are loaded.
135     */
136    public boolean areAllChannelsLoaded() {
137        return mChannelDataManagerLoaded;
138    }
139
140    /**
141     * Returns browsable channel lists.
142     */
143    public List<Channel> getBrowsableChannelList() {
144        return Collections.unmodifiableList(mBrowsableChannels);
145    }
146
147    /**
148     * Returns the number of browsable channels.
149     */
150    public int getBrowsableChannelCount() {
151        return mBrowsableChannels.size();
152    }
153
154    /**
155     * Returns the current channel.
156     */
157    @Nullable
158    public Channel getCurrentChannel() {
159        return mCurrentChannel;
160    }
161
162    /**
163     * Sets the current channel. Call this method only when setting the current channel without
164     * actually tuning to it.
165     *
166     * @param currentChannel The new current channel to set to.
167     */
168    public void setCurrentChannel(Channel currentChannel) {
169        mCurrentChannel = currentChannel;
170    }
171
172    /**
173     * Returns the current channel's ID.
174     */
175    public long getCurrentChannelId() {
176        return mCurrentChannel != null ? mCurrentChannel.getId() : Channel.INVALID_ID;
177    }
178
179    /**
180     * Returns the current channel's URI
181     */
182    public Uri getCurrentChannelUri() {
183        if (mCurrentChannel == null) {
184            return null;
185        }
186        if (mCurrentChannel.isPassthrough()) {
187            return TvContract.buildChannelUriForPassthroughInput(mCurrentChannel.getInputId());
188        } else {
189            return TvContract.buildChannelUri(mCurrentChannel.getId());
190        }
191    }
192
193    /**
194     * Returns the current {@link TvInputInfo}.
195     */
196    @Nullable
197    public TvInputInfo getCurrentInputInfo() {
198        return mCurrentChannelInputInfo;
199    }
200
201    /**
202     * Returns true, if the current channel is for a passthrough TV input.
203     */
204    public boolean isCurrentChannelPassthrough() {
205        return mCurrentChannel != null && mCurrentChannel.isPassthrough();
206    }
207
208    /**
209     * Moves the current channel to the next (or previous) browsable channel.
210     *
211     * @return true, if the channel is changed to the adjacent channel. If there is no
212     *         browsable channel, it returns false.
213     */
214    public boolean moveToAdjacentBrowsableChannel(boolean up) {
215        Channel channel = getAdjacentBrowsableChannel(up);
216        if (channel == null) {
217            return false;
218        }
219        setCurrentChannelAndNotify(mChannelMap.get(channel.getId()));
220        return true;
221    }
222
223    /**
224     * Returns a next browsable channel. It doesn't change the current channel unlike
225     * {@link #moveToAdjacentBrowsableChannel}.
226     */
227    public Channel getAdjacentBrowsableChannel(boolean up) {
228        if (isCurrentChannelPassthrough() || getBrowsableChannelCount() == 0) {
229            return null;
230        }
231        int channelIndex;
232        if (mCurrentChannel == null) {
233            channelIndex = 0;
234            Channel channel = mChannels.get(channelIndex);
235            if (channel.isBrowsable()) {
236                return channel;
237            }
238        } else {
239            channelIndex = mChannelIndexMap.get(mCurrentChannel.getId());
240        }
241        int size = mChannels.size();
242        for (int i = 0; i < size; ++i) {
243            int nextChannelIndex = up ? channelIndex + 1 + i
244                    : channelIndex - 1 - i + size;
245            if (nextChannelIndex >= size) {
246                nextChannelIndex -= size;
247            }
248            Channel channel = mChannels.get(nextChannelIndex);
249            if (channel.isBrowsable()) {
250                return channel;
251            }
252        }
253        Log.e(TAG, "This code should not be reached");
254        return null;
255    }
256
257    /**
258     * Finds the nearest browsable channel from a channel with {@code channelId}. If the channel
259     * with {@code channelId} is browsable, the channel will be returned.
260     */
261    public Channel findNearestBrowsableChannel(long channelId) {
262        if (getBrowsableChannelCount() == 0) {
263            return null;
264        }
265        Channel channel = mChannelMap.get(channelId);
266        if (channel == null) {
267            return mBrowsableChannels.get(0);
268        } else if (channel.isBrowsable()) {
269            return channel;
270        }
271        int index = mChannelIndexMap.get(channelId);
272        int size = mChannels.size();
273        for (int i = 1; i <= size / 2; ++i) {
274            Channel upChannel = mChannels.get((index + i) % size);
275            if (upChannel.isBrowsable()) {
276                return upChannel;
277            }
278            Channel downChannel = mChannels.get((index - i + size) % size);
279            if (downChannel.isBrowsable()) {
280                return downChannel;
281            }
282        }
283        throw new IllegalStateException(
284                "This code should be unreachable in findNearestBrowsableChannel");
285    }
286
287    /**
288     * Moves the current channel to {@code channel}. It can move to a non-browsable channel as well
289     * as a browsable channel.
290     *
291     * @return true, the channel change is success. But, if the channel doesn't exist, the channel
292     *         change will be failed and it will return false.
293     */
294    public boolean moveToChannel(Channel channel) {
295        if (channel == null) {
296            return false;
297        }
298        if (channel.isPassthrough()) {
299            setCurrentChannelAndNotify(channel);
300            return true;
301        }
302        SoftPreconditions.checkState(mChannelDataManagerLoaded, TAG, "Channel data is not loaded");
303        Channel newChannel = mChannelMap.get(channel.getId());
304        if (newChannel != null) {
305            setCurrentChannelAndNotify(newChannel);
306            return true;
307        }
308        return false;
309    }
310
311    /**
312     * Resets the current channel to {@code null}.
313     */
314    public void resetCurrentChannel() {
315        setCurrentChannelAndNotify(null);
316    }
317
318    /**
319     * Adds {@link Listener}.
320     */
321    public void addListener(Listener listener) {
322        mListeners.add(listener);
323    }
324
325    /**
326     * Removes {@link Listener}.
327     */
328    public void removeListener(Listener listener) {
329        mListeners.remove(listener);
330    }
331
332    public interface Listener {
333        /**
334         * Called when all the channels are loaded.
335         */
336        void onLoadFinished();
337        /**
338         * Called when the browsable channel list is changed.
339         */
340        void onBrowsableChannelListChanged();
341        /**
342         * Called when the current channel is removed.
343         */
344        void onCurrentChannelUnavailable(Channel channel);
345        /**
346         * Called when the current channel is changed.
347         */
348        void onChannelChanged(Channel previousChannel, Channel currentChannel);
349    }
350
351    private void setCurrentChannelAndNotify(Channel channel) {
352        if (mCurrentChannel == channel
353                || (channel != null && channel.hasSameReadOnlyInfo(mCurrentChannel))) {
354            return;
355        }
356        Channel previousChannel = mCurrentChannel;
357        mCurrentChannel = channel;
358        if (mCurrentChannel != null) {
359            mCurrentChannelInputInfo = mInputManager.getTvInputInfo(mCurrentChannel.getInputId());
360        }
361        for (Listener l : mListeners) {
362            l.onChannelChanged(previousChannel, mCurrentChannel);
363        }
364    }
365
366    private void updateChannelData(List<Channel> channels) {
367        mChannels.clear();
368        mChannels.addAll(channels);
369
370        mChannelMap.clear();
371        mChannelIndexMap.clear();
372        for (int i = 0; i < channels.size(); ++i) {
373            Channel channel = channels.get(i);
374            long channelId = channel.getId();
375            mChannelMap.put(channelId, channel);
376            mChannelIndexMap.put(channelId, i);
377        }
378        updateBrowsableChannels();
379
380        if (mCurrentChannel != null && !mCurrentChannel.isPassthrough()) {
381            Channel prevChannel = mCurrentChannel;
382            setCurrentChannelAndNotify(mChannelMap.get(mCurrentChannel.getId()));
383            if (mCurrentChannel == null) {
384                for (Listener l : mListeners) {
385                    l.onCurrentChannelUnavailable(prevChannel);
386                }
387            }
388        }
389        // TODO: Do not call onBrowsableChannelListChanged, when only non-browsable
390        // channels are changed.
391        for (Listener l : mListeners) {
392            l.onBrowsableChannelListChanged();
393        }
394    }
395
396    private void updateBrowsableChannels() {
397        mBrowsableChannels.clear();
398        for (Channel channel : mChannels) {
399            if (channel.isBrowsable()) {
400                mBrowsableChannels.add(channel);
401            }
402        }
403    }
404}
405