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