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