| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.tv; |
| |
| import android.media.tv.TvContract; |
| import android.media.tv.TvInputInfo; |
| import android.net.Uri; |
| import android.os.Handler; |
| import android.support.annotation.MainThread; |
| import android.support.annotation.Nullable; |
| import android.util.ArraySet; |
| import android.util.Log; |
| import com.android.tv.common.SoftPreconditions; |
| import com.android.tv.data.ChannelDataManager; |
| import com.android.tv.data.api.Channel; |
| import com.android.tv.util.TvInputManagerHelper; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| /** |
| * It manages the current tuned channel among browsable channels. And it determines the next channel |
| * by channel up/down. But, it doesn't actually tune through TvView. |
| */ |
| @MainThread |
| public class ChannelTuner { |
| private static final String TAG = "ChannelTuner"; |
| |
| private boolean mStarted; |
| private boolean mChannelDataManagerLoaded; |
| private final List<Channel> mChannels = new ArrayList<>(); |
| private final List<Channel> mBrowsableChannels = new ArrayList<>(); |
| private final Map<Long, Channel> mChannelMap = new HashMap<>(); |
| // TODO: need to check that mChannelIndexMap can be removed, once mCurrentChannelIndex |
| // is changed to mCurrentChannel(Id). |
| private final Map<Long, Integer> mChannelIndexMap = new HashMap<>(); |
| |
| private final Handler mHandler = new Handler(); |
| private final ChannelDataManager mChannelDataManager; |
| private final Set<Listener> mListeners = new ArraySet<>(); |
| @Nullable private Channel mCurrentChannel; |
| private final TvInputManagerHelper mInputManager; |
| @Nullable private TvInputInfo mCurrentChannelInputInfo; |
| |
| private final ChannelDataManager.Listener mChannelDataManagerListener = |
| new ChannelDataManager.Listener() { |
| @Override |
| public void onLoadFinished() { |
| mChannelDataManagerLoaded = true; |
| updateChannelData(mChannelDataManager.getChannelList()); |
| for (Listener l : mListeners) { |
| l.onLoadFinished(); |
| } |
| } |
| |
| @Override |
| public void onChannelListUpdated() { |
| updateChannelData(mChannelDataManager.getChannelList()); |
| } |
| |
| @Override |
| public void onChannelBrowsableChanged() { |
| updateBrowsableChannels(); |
| for (Listener l : mListeners) { |
| l.onBrowsableChannelListChanged(); |
| } |
| } |
| }; |
| |
| public ChannelTuner(ChannelDataManager channelDataManager, TvInputManagerHelper inputManager) { |
| mChannelDataManager = channelDataManager; |
| mInputManager = inputManager; |
| } |
| |
| /** Starts ChannelTuner. It cannot be called twice before calling {@link #stop}. */ |
| public void start() { |
| if (mStarted) { |
| throw new IllegalStateException("start is called twice"); |
| } |
| mStarted = true; |
| mChannelDataManager.addListener(mChannelDataManagerListener); |
| if (mChannelDataManager.isDbLoadFinished()) { |
| mHandler.post(mChannelDataManagerListener::onLoadFinished); |
| } |
| } |
| |
| /** Stops ChannelTuner. */ |
| public void stop() { |
| if (!mStarted) { |
| return; |
| } |
| mStarted = false; |
| mHandler.removeCallbacksAndMessages(null); |
| mChannelDataManager.removeListener(mChannelDataManagerListener); |
| mCurrentChannel = null; |
| mChannels.clear(); |
| mBrowsableChannels.clear(); |
| mChannelMap.clear(); |
| mChannelIndexMap.clear(); |
| mChannelDataManagerLoaded = false; |
| } |
| |
| /** Returns true, if all the channels are loaded. */ |
| public boolean areAllChannelsLoaded() { |
| return mChannelDataManagerLoaded; |
| } |
| |
| /** Returns browsable channel lists. */ |
| public List<Channel> getBrowsableChannelList() { |
| return Collections.unmodifiableList(mBrowsableChannels); |
| } |
| |
| /** Returns the number of browsable channels. */ |
| public int getBrowsableChannelCount() { |
| return mBrowsableChannels.size(); |
| } |
| |
| /** Returns the current channel. */ |
| @Nullable |
| public Channel getCurrentChannel() { |
| return mCurrentChannel; |
| } |
| |
| /** |
| * Sets the current channel. Call this method only when setting the current channel without |
| * actually tuning to it. |
| * |
| * @param currentChannel The new current channel to set to. |
| */ |
| public void setCurrentChannel(Channel currentChannel) { |
| mCurrentChannel = currentChannel; |
| } |
| |
| /** Returns the current channel's ID. */ |
| public long getCurrentChannelId() { |
| return mCurrentChannel != null ? mCurrentChannel.getId() : Channel.INVALID_ID; |
| } |
| |
| /** Returns the current channel's URI */ |
| public Uri getCurrentChannelUri() { |
| if (mCurrentChannel == null) { |
| return null; |
| } |
| if (mCurrentChannel.isPassthrough()) { |
| return TvContract.buildChannelUriForPassthroughInput(mCurrentChannel.getInputId()); |
| } else { |
| return TvContract.buildChannelUri(mCurrentChannel.getId()); |
| } |
| } |
| |
| /** Returns the current {@link TvInputInfo}. */ |
| @Nullable |
| public TvInputInfo getCurrentInputInfo() { |
| return mCurrentChannelInputInfo; |
| } |
| |
| /** Returns true, if the current channel is for a passthrough TV input. */ |
| public boolean isCurrentChannelPassthrough() { |
| return mCurrentChannel != null && mCurrentChannel.isPassthrough(); |
| } |
| |
| /** |
| * Moves the current channel to the next (or previous) browsable channel. |
| * |
| * @return true, if the channel is changed to the adjacent channel. If there is no browsable |
| * channel, it returns false. |
| */ |
| public boolean moveToAdjacentBrowsableChannel(boolean up) { |
| Channel channel = getAdjacentBrowsableChannel(up); |
| if (channel == null) { |
| return false; |
| } |
| setCurrentChannelAndNotify(mChannelMap.get(channel.getId())); |
| return true; |
| } |
| |
| /** |
| * Returns a next browsable channel. It doesn't change the current channel unlike {@link |
| * #moveToAdjacentBrowsableChannel}. |
| */ |
| public Channel getAdjacentBrowsableChannel(boolean up) { |
| if (isCurrentChannelPassthrough() || getBrowsableChannelCount() == 0) { |
| return null; |
| } |
| int channelIndex; |
| if (mCurrentChannel == null) { |
| channelIndex = 0; |
| Channel channel = mChannels.get(channelIndex); |
| if (channel.isBrowsable()) { |
| return channel; |
| } |
| } else { |
| channelIndex = mChannelIndexMap.get(mCurrentChannel.getId()); |
| } |
| int size = mChannels.size(); |
| for (int i = 0; i < size; ++i) { |
| int nextChannelIndex = up ? channelIndex + 1 + i : channelIndex - 1 - i + size; |
| if (nextChannelIndex >= size) { |
| nextChannelIndex -= size; |
| } |
| Channel channel = mChannels.get(nextChannelIndex); |
| if (channel.isBrowsable()) { |
| return channel; |
| } |
| } |
| Log.e(TAG, "This code should not be reached"); |
| return null; |
| } |
| |
| /** |
| * Finds the nearest browsable channel from a channel with {@code channelId}. If the channel |
| * with {@code channelId} is browsable, the channel will be returned. |
| */ |
| public Channel findNearestBrowsableChannel(long channelId) { |
| if (getBrowsableChannelCount() == 0) { |
| return null; |
| } |
| Channel channel = mChannelMap.get(channelId); |
| if (channel == null) { |
| return mBrowsableChannels.get(0); |
| } else if (channel.isBrowsable()) { |
| return channel; |
| } |
| int index = mChannelIndexMap.get(channelId); |
| int size = mChannels.size(); |
| for (int i = 1; i <= size / 2; ++i) { |
| Channel upChannel = mChannels.get((index + i) % size); |
| if (upChannel.isBrowsable()) { |
| return upChannel; |
| } |
| Channel downChannel = mChannels.get((index - i + size) % size); |
| if (downChannel.isBrowsable()) { |
| return downChannel; |
| } |
| } |
| throw new IllegalStateException( |
| "This code should be unreachable in findNearestBrowsableChannel"); |
| } |
| |
| /** |
| * Moves the current channel to {@code channel}. It can move to a non-browsable channel as well |
| * as a browsable channel. |
| * |
| * @return true, the channel change is success. But, if the channel doesn't exist, the channel |
| * change will be failed and it will return false. |
| */ |
| public boolean moveToChannel(Channel channel) { |
| if (channel == null) { |
| return false; |
| } |
| if (channel.isPassthrough()) { |
| setCurrentChannelAndNotify(channel); |
| return true; |
| } |
| SoftPreconditions.checkState(mChannelDataManagerLoaded, TAG, "Channel data is not loaded"); |
| Channel newChannel = mChannelMap.get(channel.getId()); |
| if (newChannel != null) { |
| setCurrentChannelAndNotify(newChannel); |
| return true; |
| } |
| return false; |
| } |
| |
| /** Resets the current channel to {@code null}. */ |
| public void resetCurrentChannel() { |
| setCurrentChannelAndNotify(null); |
| } |
| |
| /** Adds {@link Listener}. */ |
| public void addListener(Listener listener) { |
| mListeners.add(listener); |
| } |
| |
| /** Removes {@link Listener}. */ |
| public void removeListener(Listener listener) { |
| mListeners.remove(listener); |
| } |
| |
| public interface Listener { |
| /** Called when all the channels are loaded. */ |
| void onLoadFinished(); |
| /** Called when the browsable channel list is changed. */ |
| void onBrowsableChannelListChanged(); |
| /** Called when the current channel is removed. */ |
| void onCurrentChannelUnavailable(Channel channel); |
| /** Called when the current channel is changed. */ |
| void onChannelChanged(Channel previousChannel, Channel currentChannel); |
| } |
| |
| private void setCurrentChannelAndNotify(Channel channel) { |
| if (mCurrentChannel == channel |
| || (channel != null && channel.hasSameReadOnlyInfo(mCurrentChannel))) { |
| return; |
| } |
| Channel previousChannel = mCurrentChannel; |
| mCurrentChannel = channel; |
| if (mCurrentChannel != null) { |
| mCurrentChannelInputInfo = mInputManager.getTvInputInfo(mCurrentChannel.getInputId()); |
| } |
| for (Listener l : mListeners) { |
| l.onChannelChanged(previousChannel, mCurrentChannel); |
| } |
| } |
| |
| private void updateChannelData(List<Channel> channels) { |
| mChannels.clear(); |
| mChannels.addAll(channels); |
| |
| mChannelMap.clear(); |
| mChannelIndexMap.clear(); |
| for (int i = 0; i < channels.size(); ++i) { |
| Channel channel = channels.get(i); |
| long channelId = channel.getId(); |
| mChannelMap.put(channelId, channel); |
| mChannelIndexMap.put(channelId, i); |
| } |
| updateBrowsableChannels(); |
| |
| if (mCurrentChannel != null && !mCurrentChannel.isPassthrough()) { |
| Channel prevChannel = mCurrentChannel; |
| setCurrentChannelAndNotify(mChannelMap.get(mCurrentChannel.getId())); |
| if (mCurrentChannel == null) { |
| for (Listener l : mListeners) { |
| l.onCurrentChannelUnavailable(prevChannel); |
| } |
| } |
| } |
| // TODO: Do not call onBrowsableChannelListChanged, when only non-browsable |
| // channels are changed. |
| for (Listener l : mListeners) { |
| l.onBrowsableChannelListChanged(); |
| } |
| } |
| |
| private void updateBrowsableChannels() { |
| mBrowsableChannels.clear(); |
| for (Channel channel : mChannels) { |
| if (channel.isBrowsable()) { |
| mBrowsableChannels.add(channel); |
| } |
| } |
| } |
| } |