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;
18 
19 import android.media.tv.TvContract;
20 import android.media.tv.TvInputInfo;
21 import android.net.Uri;
22 import android.os.Handler;
23 import android.support.annotation.MainThread;
24 import android.support.annotation.Nullable;
25 import android.util.ArraySet;
26 import android.util.Log;
27 
28 import com.android.tv.common.SoftPreconditions;
29 import com.android.tv.data.Channel;
30 import com.android.tv.data.ChannelDataManager;
31 import com.android.tv.util.TvInputManagerHelper;
32 
33 import java.util.ArrayList;
34 import java.util.Collections;
35 import java.util.HashMap;
36 import java.util.List;
37 import java.util.Map;
38 import 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
45 public 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 = new ArraySet<>();
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 
ChannelTuner(ChannelDataManager channelDataManager, TvInputManagerHelper inputManager)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      */
start()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      */
stop()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      */
areAllChannelsLoaded()136     public boolean areAllChannelsLoaded() {
137         return mChannelDataManagerLoaded;
138     }
139 
140     /**
141      * Returns browsable channel lists.
142      */
getBrowsableChannelList()143     public List<Channel> getBrowsableChannelList() {
144         return Collections.unmodifiableList(mBrowsableChannels);
145     }
146 
147     /**
148      * Returns the number of browsable channels.
149      */
getBrowsableChannelCount()150     public int getBrowsableChannelCount() {
151         return mBrowsableChannels.size();
152     }
153 
154     /**
155      * Returns the current channel.
156      */
157     @Nullable
getCurrentChannel()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      */
setCurrentChannel(Channel currentChannel)168     public void setCurrentChannel(Channel currentChannel) {
169         mCurrentChannel = currentChannel;
170     }
171 
172     /**
173      * Returns the current channel's ID.
174      */
getCurrentChannelId()175     public long getCurrentChannelId() {
176         return mCurrentChannel != null ? mCurrentChannel.getId() : Channel.INVALID_ID;
177     }
178 
179     /**
180      * Returns the current channel's URI
181      */
getCurrentChannelUri()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
getCurrentInputInfo()197     public TvInputInfo getCurrentInputInfo() {
198         return mCurrentChannelInputInfo;
199     }
200 
201     /**
202      * Returns true, if the current channel is for a passthrough TV input.
203      */
isCurrentChannelPassthrough()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      */
moveToAdjacentBrowsableChannel(boolean up)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      */
getAdjacentBrowsableChannel(boolean up)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      */
findNearestBrowsableChannel(long channelId)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      */
moveToChannel(Channel channel)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      */
resetCurrentChannel()314     public void resetCurrentChannel() {
315         setCurrentChannelAndNotify(null);
316     }
317 
318     /**
319      * Adds {@link Listener}.
320      */
addListener(Listener listener)321     public void addListener(Listener listener) {
322         mListeners.add(listener);
323     }
324 
325     /**
326      * Removes {@link Listener}.
327      */
removeListener(Listener listener)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          */
onLoadFinished()336         void onLoadFinished();
337         /**
338          * Called when the browsable channel list is changed.
339          */
onBrowsableChannelListChanged()340         void onBrowsableChannelListChanged();
341         /**
342          * Called when the current channel is removed.
343          */
onCurrentChannelUnavailable(Channel channel)344         void onCurrentChannelUnavailable(Channel channel);
345         /**
346          * Called when the current channel is changed.
347          */
onChannelChanged(Channel previousChannel, Channel currentChannel)348         void onChannelChanged(Channel previousChannel, Channel currentChannel);
349     }
350 
setCurrentChannelAndNotify(Channel channel)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 
updateChannelData(List<Channel> channels)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 
updateBrowsableChannels()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