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