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 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}. */ 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. */ 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. */ 127 public boolean areAllChannelsLoaded() { 128 return mChannelDataManagerLoaded; 129 } 130 131 /** Returns browsable channel lists. */ 132 public List<Channel> getBrowsableChannelList() { 133 return Collections.unmodifiableList(mBrowsableChannels); 134 } 135 136 /** Returns the number of browsable channels. */ 137 public int getBrowsableChannelCount() { 138 return mBrowsableChannels.size(); 139 } 140 141 /** Returns the current channel. */ 142 @Nullable 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 */ 153 public void setCurrentChannel(Channel currentChannel) { 154 mCurrentChannel = currentChannel; 155 } 156 157 /** Returns the current channel's ID. */ 158 public long getCurrentChannelId() { 159 return mCurrentChannel != null ? mCurrentChannel.getId() : Channel.INVALID_ID; 160 } 161 162 /** Returns the current channel's URI */ 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 176 public TvInputInfo getCurrentInputInfo() { 177 return mCurrentChannelInputInfo; 178 } 179 180 /** Returns true, if the current channel is for a passthrough TV input. */ 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 */ 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 */ 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 */ 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 */ 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}. */ 288 public void resetCurrentChannel() { 289 setCurrentChannelAndNotify(null); 290 } 291 292 /** Adds {@link Listener}. */ 293 public void addListener(Listener listener) { 294 mListeners.add(listener); 295 } 296 297 /** Removes {@link 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. */ 304 void onLoadFinished(); 305 /** Called when the browsable channel list is changed. */ 306 void onBrowsableChannelListChanged(); 307 /** Called when the current channel is removed. */ 308 void onCurrentChannelUnavailable(Channel channel); 309 /** Called when the current channel is changed. */ 310 void onChannelChanged(Channel previousChannel, Channel currentChannel); 311 } 312 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 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 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