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