ChannelTuner.java revision 816a4be1a0f34f6a48877c8afd3dbbca19eac435
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.content.Context; 20import android.database.Cursor; 21import android.media.tv.TvContract; 22import android.net.Uri; 23import android.os.Handler; 24import android.util.Log; 25 26import com.android.tv.data.Channel; 27import com.android.tv.data.ChannelDataManager; 28 29import java.util.ArrayList; 30import java.util.Collections; 31import java.util.HashMap; 32import java.util.HashSet; 33import java.util.List; 34import java.util.Map; 35import java.util.Set; 36 37/** 38 * It manages the current tuned channel among browsable channels. And it determines the next channel 39 * by channel up/down. But, it doesn't actually tune through TvView. 40 */ 41public class ChannelTuner { 42 private static final String TAG = "ChannelTuner"; 43 44 private static final int INVALID_INDEX = -1; 45 46 private final Context mContext; 47 private boolean mStarted; 48 private boolean mChannelDataManagerLoaded; 49 private final List<Channel> mChannels = new ArrayList<>(); 50 private final List<Channel> mBrowsableChannels = new ArrayList<>(); 51 private final Map<Long, Channel> mChannelMap = new HashMap<>(); 52 // TODO: need to check that mChannelIndexMap can be removed, once mCurrentChannelIndex 53 // is changed to mCurrentChannel(Id). 54 private final Map<Long, Integer> mChannelIndexMap = new HashMap<>(); 55 56 private final Handler mHandler = new Handler(); 57 private final ChannelDataManager mChannelDataManager; 58 private final Set<Listener> mListeners = new HashSet<>(); 59 private Channel mCurrentChannel; 60 61 private final ChannelDataManager.Listener mChannelDataManagerListener = 62 new ChannelDataManager.Listener() { 63 @Override 64 public void onLoadFinished() { 65 mChannelDataManagerLoaded = true; 66 updateChannelData(mChannelDataManager.getChannelList()); 67 for (Listener l : mListeners) { 68 l.onLoadFinished(); 69 } 70 } 71 72 @Override 73 public void onChannelListUpdated() { 74 updateChannelData(mChannelDataManager.getChannelList()); 75 } 76 77 @Override 78 public void onChannelBrowsableChanged() { 79 updateBrowsableChannels(); 80 for (Listener l : mListeners) { 81 l.onBrowsableChannelListChanged(); 82 } 83 } 84 }; 85 86 public ChannelTuner(Context context, ChannelDataManager channelDataManager) { 87 mContext = context; 88 mChannelDataManager = channelDataManager; 89 } 90 91 /** 92 * Starts ChannelTuner. It cannot be called twice before calling {@link #stop}. 93 */ 94 public void start() { 95 if (mStarted) { 96 throw new IllegalStateException("start is called twice"); 97 } 98 mStarted = true; 99 mChannelDataManager.addListener(mChannelDataManagerListener); 100 if (mChannelDataManager.isDbLoadFinished()) { 101 mHandler.post(new Runnable() { 102 @Override 103 public void run() { 104 mChannelDataManagerListener.onLoadFinished(); 105 } 106 }); 107 } 108 } 109 110 /** 111 * Stops ChannelTuner. 112 */ 113 public void stop() { 114 if (!mStarted) { 115 return; 116 } 117 mStarted = false; 118 mHandler.removeCallbacksAndMessages(null); 119 mChannelDataManager.removeListener(mChannelDataManagerListener); 120 mCurrentChannel = null; 121 mChannels.clear(); 122 mBrowsableChannels.clear(); 123 mChannelMap.clear(); 124 mChannelIndexMap.clear(); 125 mChannelDataManagerLoaded = false; 126 } 127 128 /** 129 * Returns true, if all the channels are loaded. 130 */ 131 public boolean areAllChannelsLoaded() { 132 return mChannelDataManagerLoaded; 133 } 134 135 /** 136 * Returns browsable channel lists. 137 */ 138 public List<Channel> getBrowsableChannelList() { 139 return Collections.unmodifiableList(mBrowsableChannels); 140 } 141 142 /** 143 * Returns the number of browsable channels. 144 */ 145 public int getBrowsableChannelCount() { 146 return mBrowsableChannels.size(); 147 } 148 149 /** 150 * Returns the current channel. 151 */ 152 public Channel getCurrentChannel() { 153 return mCurrentChannel; 154 } 155 156 /** 157 * Sets the current channel. Call this method only when setting the current channel without 158 * actually tuning to it. 159 * 160 * @param currentChannel The new current channel to set to. 161 */ 162 public void setCurrentChannel(Channel currentChannel) { 163 mCurrentChannel = currentChannel; 164 } 165 166 /** 167 * Returns the current channel's ID. 168 */ 169 public long getCurrentChannelId() { 170 return mCurrentChannel != null ? mCurrentChannel.getId() : Channel.INVALID_ID; 171 } 172 173 /** 174 * Returns the current channel's URI 175 */ 176 public Uri getCurrentChannelUri() { 177 if (mCurrentChannel == null) { 178 return null; 179 } 180 if (mCurrentChannel.isPassthrough()) { 181 return TvContract.buildChannelUriForPassthroughInput(mCurrentChannel.getInputId()); 182 } else { 183 return TvContract.buildChannelUri(mCurrentChannel.getId()); 184 } 185 } 186 187 /** 188 * Returns true, if the current channel is for a passthrough TV input. 189 */ 190 public boolean isCurrentChannelPassthrough() { 191 return mCurrentChannel != null && mCurrentChannel.isPassthrough(); 192 } 193 194 /** 195 * Moves the current channel to the next (or previous) browsable channel. 196 * 197 * @return true, if the channel is changed to the adjacent channel. If there is no 198 * browsable channel, it returns false. 199 */ 200 public boolean moveToAdjacentBrowsableChannel(boolean up) { 201 Channel channel = getAdjacentBrowsableChannel(up); 202 if (channel == null) { 203 return false; 204 } 205 setCurrentChannelAndNotify(mChannelMap.get(channel.getId())); 206 return true; 207 } 208 209 /** 210 * Returns a next browsable channel. It doesn't change the current channel unlike 211 * {@link #moveToAdjacentBrowsableChannel}. 212 */ 213 public Channel getAdjacentBrowsableChannel(boolean up) { 214 if (isCurrentChannelPassthrough() || getBrowsableChannelCount() == 0) { 215 return null; 216 } 217 int channelIndex; 218 if (mCurrentChannel == null) { 219 channelIndex = 0; 220 Channel channel = mChannels.get(channelIndex); 221 if (channel.isBrowsable()) { 222 return channel; 223 } 224 } else { 225 channelIndex = mChannelIndexMap.get(mCurrentChannel.getId()); 226 } 227 int size = mChannels.size(); 228 for (int i = 0; i < size; ++i) { 229 int nextChannelIndex = up ? channelIndex + 1 + i 230 : channelIndex - 1 - i + size; 231 if (nextChannelIndex >= size) { 232 nextChannelIndex -= size; 233 } 234 Channel channel = mChannels.get(nextChannelIndex); 235 if (channel.isBrowsable()) { 236 return channel; 237 } 238 } 239 Log.e(TAG, "This code should not be reached"); 240 return null; 241 } 242 243 /** 244 * Finds the nearest browsable channel from a channel with {@code channelId}. If the channel 245 * with {@code channelId} is browsable, the channel will be returned. 246 */ 247 public Channel findNearestBrowsableChannel(long channelId) { 248 if (getBrowsableChannelCount() == 0) { 249 return null; 250 } 251 Channel channel = mChannelMap.get(channelId); 252 if (channel == null) { 253 return mBrowsableChannels.get(0); 254 } else if (channel.isBrowsable()) { 255 return channel; 256 } 257 int index = mChannelIndexMap.get(channelId); 258 int size = mChannels.size(); 259 for (int i = 1; i <= size / 2; ++i) { 260 Channel upChannel = mChannels.get((index + i) % size); 261 if (upChannel.isBrowsable()) { 262 return upChannel; 263 } 264 Channel downChannel = mChannels.get((index - i + size) % size); 265 if (downChannel.isBrowsable()) { 266 return downChannel; 267 } 268 } 269 throw new IllegalStateException( 270 "This code should be unreachable in findNearestBrowsableChannel"); 271 } 272 273 /** 274 * Moves the current channel to {@code channel}. It can move to a non-browsable channel as well 275 * as a browsable channel. 276 * 277 * @return true, the channel change is success. But, if the channel doesn't exist, the channel 278 * change will be failed and it will return false. 279 */ 280 public boolean moveToChannel(Channel channel) { 281 if (channel == null) { 282 return false; 283 } 284 if (channel.isPassthrough()) { 285 setCurrentChannelAndNotify(channel); 286 return true; 287 } 288 Channel newChannel = mChannelMap.get(channel.getId()); 289 if (newChannel != null) { 290 setCurrentChannelAndNotify(newChannel); 291 return true; 292 } else if (!mChannelDataManagerLoaded) { 293 return loadChannel(channel.getId()) != null; 294 } 295 return false; 296 } 297 298 /** 299 * Resets the current channel to {@code null}. 300 */ 301 public void resetCurrentChannel() { 302 setCurrentChannelAndNotify(null); 303 } 304 305 /** 306 * Adds {@link Listener}. 307 */ 308 public void addListener(Listener listener) { 309 mListeners.add(listener); 310 } 311 312 /** 313 * Removes {@link Listener}. 314 */ 315 public void removeListener(Listener listener) { 316 mListeners.remove(listener); 317 } 318 319 public interface Listener { 320 /** 321 * Called when all the channels are loaded. 322 */ 323 void onLoadFinished(); 324 /** 325 * Called when the browsable channel list is changed. 326 */ 327 void onBrowsableChannelListChanged(); 328 /** 329 * Called when the current channel is removed. 330 */ 331 void onCurrentChannelUnavailable(Channel channel); 332 /** 333 * Called when the current channel is changed. 334 */ 335 void onChannelChanged(Channel previousChannel, Channel currentChannel); 336 } 337 338 private void setCurrentChannelAndNotify(Channel channel) { 339 if (mCurrentChannel == channel 340 || (channel != null && channel.hasSameReadOnlyInfo(mCurrentChannel))) { 341 return; 342 } 343 Channel previousChannel = mCurrentChannel; 344 mCurrentChannel = channel; 345 for (Listener l : mListeners) { 346 l.onChannelChanged(previousChannel, mCurrentChannel); 347 } 348 } 349 350 private void updateChannelData(List<Channel> channels) { 351 mChannels.clear(); 352 mChannels.addAll(channels); 353 354 mChannelMap.clear(); 355 mChannelIndexMap.clear(); 356 for (int i = 0; i < channels.size(); ++i) { 357 Channel channel = channels.get(i); 358 long channelId = channel.getId(); 359 mChannelMap.put(channelId, channel); 360 mChannelIndexMap.put(channelId, i); 361 } 362 updateBrowsableChannels(); 363 364 if (mCurrentChannel != null && !mCurrentChannel.isPassthrough()) { 365 Channel prevChannel = mCurrentChannel; 366 setCurrentChannelAndNotify(mChannelMap.get(mCurrentChannel.getId())); 367 if (mCurrentChannel == null) { 368 for (Listener l : mListeners) { 369 l.onCurrentChannelUnavailable(prevChannel); 370 } 371 } 372 } 373 // TODO: Do not call onBrowsableChannelListChanged, when only non-browsable 374 // channels are changed. 375 for (Listener l : mListeners) { 376 l.onBrowsableChannelListChanged(); 377 } 378 } 379 380 private void updateBrowsableChannels() { 381 mBrowsableChannels.clear(); 382 for (Channel channel : mChannels) { 383 if (channel.isBrowsable()) { 384 mBrowsableChannels.add(channel); 385 } 386 } 387 } 388 389 /** 390 * Loads and returns a channel which has the given channel ID. 391 * 392 * @param channelId The ID of the channel to be loaded. 393 * @return a channel if it has been loaded. {@code null} if the channel is not found. 394 */ 395 public Channel loadChannel(long channelId) { 396 if (channelId < 0) { 397 return null; 398 } 399 if (mChannelDataManagerLoaded) { 400 return mChannelMap.get(channelId); 401 } 402 Channel channel = mChannelMap.get(channelId); 403 if (channel != null) { 404 return channel; 405 } 406 407 Uri uri = TvContract.buildChannelUri(channelId); 408 try (Cursor c = mContext.getContentResolver().query(uri, Channel.PROJECTION, 409 null, null, null)) { 410 if (c != null && c.moveToNext()) { 411 channel = Channel.fromCursor(c); 412 List<Channel> channels = new ArrayList<>(mChannels); 413 channels.add(channel); 414 updateChannelData(channels); 415 return channel; 416 } 417 } 418 return null; 419 } 420} 421