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