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