Home | History | Annotate | Download | only in tv
      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