blob: fe13898028b03720738aed7ea6e02febd1a98d69 [file] [log] [blame]
/*
* 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);
}
}
}
}