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(mChannelDataManagerListener::onLoadFinished);
101         }
102     }
103 
104     /** Stops ChannelTuner. */
stop()105     public void stop() {
106         if (!mStarted) {
107             return;
108         }
109         mStarted = false;
110         mHandler.removeCallbacksAndMessages(null);
111         mChannelDataManager.removeListener(mChannelDataManagerListener);
112         mCurrentChannel = null;
113         mChannels.clear();
114         mBrowsableChannels.clear();
115         mChannelMap.clear();
116         mChannelIndexMap.clear();
117         mChannelDataManagerLoaded = false;
118     }
119 
120     /** Returns true, if all the channels are loaded. */
areAllChannelsLoaded()121     public boolean areAllChannelsLoaded() {
122         return mChannelDataManagerLoaded;
123     }
124 
125     /** Returns browsable channel lists. */
getBrowsableChannelList()126     public List<Channel> getBrowsableChannelList() {
127         return Collections.unmodifiableList(mBrowsableChannels);
128     }
129 
130     /** Returns the number of browsable channels. */
getBrowsableChannelCount()131     public int getBrowsableChannelCount() {
132         return mBrowsableChannels.size();
133     }
134 
135     /** Returns the current channel. */
136     @Nullable
getCurrentChannel()137     public Channel getCurrentChannel() {
138         return mCurrentChannel;
139     }
140 
141     /**
142      * Sets the current channel. Call this method only when setting the current channel without
143      * actually tuning to it.
144      *
145      * @param currentChannel The new current channel to set to.
146      */
setCurrentChannel(Channel currentChannel)147     public void setCurrentChannel(Channel currentChannel) {
148         mCurrentChannel = currentChannel;
149     }
150 
151     /** Returns the current channel's ID. */
getCurrentChannelId()152     public long getCurrentChannelId() {
153         return mCurrentChannel != null ? mCurrentChannel.getId() : Channel.INVALID_ID;
154     }
155 
156     /** Returns the current channel's URI */
getCurrentChannelUri()157     public Uri getCurrentChannelUri() {
158         if (mCurrentChannel == null) {
159             return null;
160         }
161         if (mCurrentChannel.isPassthrough()) {
162             return TvContract.buildChannelUriForPassthroughInput(mCurrentChannel.getInputId());
163         } else {
164             return TvContract.buildChannelUri(mCurrentChannel.getId());
165         }
166     }
167 
168     /** Returns the current {@link TvInputInfo}. */
169     @Nullable
getCurrentInputInfo()170     public TvInputInfo getCurrentInputInfo() {
171         return mCurrentChannelInputInfo;
172     }
173 
174     /** Returns true, if the current channel is for a passthrough TV input. */
isCurrentChannelPassthrough()175     public boolean isCurrentChannelPassthrough() {
176         return mCurrentChannel != null && mCurrentChannel.isPassthrough();
177     }
178 
179     /**
180      * Moves the current channel to the next (or previous) browsable channel.
181      *
182      * @return true, if the channel is changed to the adjacent channel. If there is no browsable
183      *     channel, it returns false.
184      */
moveToAdjacentBrowsableChannel(boolean up)185     public boolean moveToAdjacentBrowsableChannel(boolean up) {
186         Channel channel = getAdjacentBrowsableChannel(up);
187         if (channel == null) {
188             return false;
189         }
190         setCurrentChannelAndNotify(mChannelMap.get(channel.getId()));
191         return true;
192     }
193 
194     /**
195      * Returns a next browsable channel. It doesn't change the current channel unlike {@link
196      * #moveToAdjacentBrowsableChannel}.
197      */
getAdjacentBrowsableChannel(boolean up)198     public Channel getAdjacentBrowsableChannel(boolean up) {
199         if (isCurrentChannelPassthrough() || getBrowsableChannelCount() == 0) {
200             return null;
201         }
202         int channelIndex;
203         if (mCurrentChannel == null) {
204             channelIndex = 0;
205             Channel channel = mChannels.get(channelIndex);
206             if (channel.isBrowsable()) {
207                 return channel;
208             }
209         } else {
210             channelIndex = mChannelIndexMap.get(mCurrentChannel.getId());
211         }
212         int size = mChannels.size();
213         for (int i = 0; i < size; ++i) {
214             int nextChannelIndex = up ? channelIndex + 1 + i : channelIndex - 1 - i + size;
215             if (nextChannelIndex >= size) {
216                 nextChannelIndex -= size;
217             }
218             Channel channel = mChannels.get(nextChannelIndex);
219             if (channel.isBrowsable()) {
220                 return channel;
221             }
222         }
223         Log.e(TAG, "This code should not be reached");
224         return null;
225     }
226 
227     /**
228      * Finds the nearest browsable channel from a channel with {@code channelId}. If the channel
229      * with {@code channelId} is browsable, the channel will be returned.
230      */
findNearestBrowsableChannel(long channelId)231     public Channel findNearestBrowsableChannel(long channelId) {
232         if (getBrowsableChannelCount() == 0) {
233             return null;
234         }
235         Channel channel = mChannelMap.get(channelId);
236         if (channel == null) {
237             return mBrowsableChannels.get(0);
238         } else if (channel.isBrowsable()) {
239             return channel;
240         }
241         int index = mChannelIndexMap.get(channelId);
242         int size = mChannels.size();
243         for (int i = 1; i <= size / 2; ++i) {
244             Channel upChannel = mChannels.get((index + i) % size);
245             if (upChannel.isBrowsable()) {
246                 return upChannel;
247             }
248             Channel downChannel = mChannels.get((index - i + size) % size);
249             if (downChannel.isBrowsable()) {
250                 return downChannel;
251             }
252         }
253         throw new IllegalStateException(
254                 "This code should be unreachable in findNearestBrowsableChannel");
255     }
256 
257     /**
258      * Moves the current channel to {@code channel}. It can move to a non-browsable channel as well
259      * as a browsable channel.
260      *
261      * @return true, the channel change is success. But, if the channel doesn't exist, the channel
262      *     change will be failed and it will return false.
263      */
moveToChannel(Channel channel)264     public boolean moveToChannel(Channel channel) {
265         if (channel == null) {
266             return false;
267         }
268         if (channel.isPassthrough()) {
269             setCurrentChannelAndNotify(channel);
270             return true;
271         }
272         SoftPreconditions.checkState(mChannelDataManagerLoaded, TAG, "Channel data is not loaded");
273         Channel newChannel = mChannelMap.get(channel.getId());
274         if (newChannel != null) {
275             setCurrentChannelAndNotify(newChannel);
276             return true;
277         }
278         return false;
279     }
280 
281     /** Resets the current channel to {@code null}. */
resetCurrentChannel()282     public void resetCurrentChannel() {
283         setCurrentChannelAndNotify(null);
284     }
285 
286     /** Adds {@link Listener}. */
addListener(Listener listener)287     public void addListener(Listener listener) {
288         mListeners.add(listener);
289     }
290 
291     /** Removes {@link Listener}. */
removeListener(Listener listener)292     public void removeListener(Listener listener) {
293         mListeners.remove(listener);
294     }
295 
296     public interface Listener {
297         /** Called when all the channels are loaded. */
onLoadFinished()298         void onLoadFinished();
299         /** Called when the browsable channel list is changed. */
onBrowsableChannelListChanged()300         void onBrowsableChannelListChanged();
301         /** Called when the current channel is removed. */
onCurrentChannelUnavailable(Channel channel)302         void onCurrentChannelUnavailable(Channel channel);
303         /** Called when the current channel is changed. */
onChannelChanged(Channel previousChannel, Channel currentChannel)304         void onChannelChanged(Channel previousChannel, Channel currentChannel);
305     }
306 
setCurrentChannelAndNotify(Channel channel)307     private void setCurrentChannelAndNotify(Channel channel) {
308         if (mCurrentChannel == channel
309                 || (channel != null && channel.hasSameReadOnlyInfo(mCurrentChannel))) {
310             return;
311         }
312         Channel previousChannel = mCurrentChannel;
313         mCurrentChannel = channel;
314         if (mCurrentChannel != null) {
315             mCurrentChannelInputInfo = mInputManager.getTvInputInfo(mCurrentChannel.getInputId());
316         }
317         for (Listener l : mListeners) {
318             l.onChannelChanged(previousChannel, mCurrentChannel);
319         }
320     }
321 
updateChannelData(List<Channel> channels)322     private void updateChannelData(List<Channel> channels) {
323         mChannels.clear();
324         mChannels.addAll(channels);
325 
326         mChannelMap.clear();
327         mChannelIndexMap.clear();
328         for (int i = 0; i < channels.size(); ++i) {
329             Channel channel = channels.get(i);
330             long channelId = channel.getId();
331             mChannelMap.put(channelId, channel);
332             mChannelIndexMap.put(channelId, i);
333         }
334         updateBrowsableChannels();
335 
336         if (mCurrentChannel != null && !mCurrentChannel.isPassthrough()) {
337             Channel prevChannel = mCurrentChannel;
338             setCurrentChannelAndNotify(mChannelMap.get(mCurrentChannel.getId()));
339             if (mCurrentChannel == null) {
340                 for (Listener l : mListeners) {
341                     l.onCurrentChannelUnavailable(prevChannel);
342                 }
343             }
344         }
345         // TODO: Do not call onBrowsableChannelListChanged, when only non-browsable
346         // channels are changed.
347         for (Listener l : mListeners) {
348             l.onBrowsableChannelListChanged();
349         }
350     }
351 
updateBrowsableChannels()352     private void updateBrowsableChannels() {
353         mBrowsableChannels.clear();
354         for (Channel channel : mChannels) {
355             if (channel.isBrowsable()) {
356                 mBrowsableChannels.add(channel);
357             }
358         }
359     }
360 }
361