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 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 */ 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 */ 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 */ 136 public boolean areAllChannelsLoaded() { 137 return mChannelDataManagerLoaded; 138 } 139 140 /** 141 * Returns browsable channel lists. 142 */ 143 public List<Channel> getBrowsableChannelList() { 144 return Collections.unmodifiableList(mBrowsableChannels); 145 } 146 147 /** 148 * Returns the number of browsable channels. 149 */ 150 public int getBrowsableChannelCount() { 151 return mBrowsableChannels.size(); 152 } 153 154 /** 155 * Returns the current channel. 156 */ 157 @Nullable 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 */ 168 public void setCurrentChannel(Channel currentChannel) { 169 mCurrentChannel = currentChannel; 170 } 171 172 /** 173 * Returns the current channel's ID. 174 */ 175 public long getCurrentChannelId() { 176 return mCurrentChannel != null ? mCurrentChannel.getId() : Channel.INVALID_ID; 177 } 178 179 /** 180 * Returns the current channel's URI 181 */ 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 197 public TvInputInfo getCurrentInputInfo() { 198 return mCurrentChannelInputInfo; 199 } 200 201 /** 202 * Returns true, if the current channel is for a passthrough TV input. 203 */ 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 */ 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 */ 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 */ 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 */ 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 */ 314 public void resetCurrentChannel() { 315 setCurrentChannelAndNotify(null); 316 } 317 318 /** 319 * Adds {@link Listener}. 320 */ 321 public void addListener(Listener listener) { 322 mListeners.add(listener); 323 } 324 325 /** 326 * Removes {@link Listener}. 327 */ 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 */ 336 void onLoadFinished(); 337 /** 338 * Called when the browsable channel list is changed. 339 */ 340 void onBrowsableChannelListChanged(); 341 /** 342 * Called when the current channel is removed. 343 */ 344 void onCurrentChannelUnavailable(Channel channel); 345 /** 346 * Called when the current channel is changed. 347 */ 348 void onChannelChanged(Channel previousChannel, Channel currentChannel); 349 } 350 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 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 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