Home | History | Annotate | Download | only in rich
      1 /*
      2  * Copyright 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.example.android.sampletvinput.rich;
     18 
     19 import android.content.BroadcastReceiver;
     20 import android.content.Context;
     21 import android.content.Intent;
     22 import android.content.IntentFilter;
     23 import android.graphics.Point;
     24 import android.media.tv.TvContentRating;
     25 import android.media.tv.TvInputManager;
     26 import android.media.tv.TvInputService;
     27 import android.media.tv.TvTrackInfo;
     28 import android.net.Uri;
     29 import android.os.Handler;
     30 import android.os.HandlerThread;
     31 import android.os.Message;
     32 import android.text.TextUtils;
     33 import android.util.Log;
     34 import android.view.Display;
     35 import android.view.LayoutInflater;
     36 import android.view.Surface;
     37 import android.view.View;
     38 import android.view.WindowManager;
     39 import android.view.accessibility.CaptioningManager;
     40 
     41 import com.example.android.sampletvinput.R;
     42 import com.example.android.sampletvinput.TvContractUtils;
     43 import com.example.android.sampletvinput.player.TvInputPlayer;
     44 import com.example.android.sampletvinput.syncadapter.SyncUtils;
     45 import com.google.android.exoplayer.ExoPlaybackException;
     46 import com.google.android.exoplayer.ExoPlayer;
     47 import com.google.android.exoplayer.text.CaptionStyleCompat;
     48 import com.google.android.exoplayer.text.SubtitleView;
     49 
     50 import java.util.ArrayList;
     51 import java.util.Collections;
     52 import java.util.HashSet;
     53 import java.util.List;
     54 import java.util.Set;
     55 
     56 /**
     57  * TvInputService which provides a full implementation of EPG, subtitles, multi-audio,
     58  * parental controls, and overlay view.
     59  */
     60 public class RichTvInputService extends TvInputService {
     61     private static final String TAG = "RichTvInputService";
     62 
     63     private HandlerThread mHandlerThread;
     64     private Handler mDbHandler;
     65 
     66     private List<RichTvInputSessionImpl> mSessions;
     67     private CaptioningManager mCaptioningManager;
     68 
     69     private final BroadcastReceiver mParentalControlsBroadcastReceiver = new BroadcastReceiver() {
     70         @Override
     71         public void onReceive(Context context, Intent intent) {
     72             if (mSessions != null) {
     73                 for (RichTvInputSessionImpl session : mSessions) {
     74                     session.checkContentBlockNeeded();
     75                 }
     76             }
     77         }
     78     };
     79 
     80     @Override
     81     public void onCreate() {
     82         super.onCreate();
     83         mHandlerThread = new HandlerThread(getClass().getSimpleName());
     84         mHandlerThread.start();
     85         mDbHandler = new Handler(mHandlerThread.getLooper());
     86         mCaptioningManager = (CaptioningManager) getSystemService(Context.CAPTIONING_SERVICE);
     87 
     88         setTheme(android.R.style.Theme_Holo_Light_NoActionBar);
     89 
     90         mSessions = new ArrayList<RichTvInputSessionImpl>();
     91         IntentFilter intentFilter = new IntentFilter();
     92         intentFilter.addAction(TvInputManager.ACTION_BLOCKED_RATINGS_CHANGED);
     93         intentFilter.addAction(TvInputManager.ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED);
     94         registerReceiver(mParentalControlsBroadcastReceiver, intentFilter);
     95     }
     96 
     97     @Override
     98     public void onDestroy() {
     99         super.onDestroy();
    100         unregisterReceiver(mParentalControlsBroadcastReceiver);
    101         mHandlerThread.quit();
    102         mHandlerThread = null;
    103         mDbHandler = null;
    104     }
    105 
    106     @Override
    107     public final Session onCreateSession(String inputId) {
    108         RichTvInputSessionImpl session = new RichTvInputSessionImpl(this, inputId);
    109         session.setOverlayViewEnabled(true);
    110         mSessions.add(session);
    111         return session;
    112     }
    113 
    114     class RichTvInputSessionImpl extends TvInputService.Session implements Handler.Callback {
    115         private static final int MSG_PLAY_PROGRAM = 1000;
    116         private static final float CAPTION_LINE_HEIGHT_RATIO = 0.0533f;
    117 
    118         private final Context mContext;
    119         private final String mInputId;
    120         private TvInputManager mTvInputManager;
    121         protected TvInputPlayer mPlayer;
    122         private Surface mSurface;
    123         private float mVolume;
    124         private boolean mCaptionEnabled;
    125         private PlaybackInfo mCurrentPlaybackInfo;
    126         private TvContentRating mLastBlockedRating;
    127         private TvContentRating mCurrentContentRating;
    128         private String mSelectedSubtitleTrackId;
    129         private SubtitleView mSubtitleView;
    130         private boolean mEpgSyncRequested;
    131         private final Set<TvContentRating> mUnblockedRatingSet = new HashSet<>();
    132         private Handler mHandler;
    133 
    134         private final TvInputPlayer.Callback mPlayerCallback = new TvInputPlayer.Callback() {
    135             private boolean mFirstFrameDrawn;
    136             @Override
    137             public void onPrepared() {
    138                 mFirstFrameDrawn = false;
    139                 List<TvTrackInfo> tracks = new ArrayList<>();
    140                 Collections.addAll(tracks, mPlayer.getTracks(TvTrackInfo.TYPE_AUDIO));
    141                 Collections.addAll(tracks, mPlayer.getTracks(TvTrackInfo.TYPE_VIDEO));
    142                 Collections.addAll(tracks, mPlayer.getTracks(TvTrackInfo.TYPE_SUBTITLE));
    143 
    144                 notifyTracksChanged(tracks);
    145                 notifyTrackSelected(TvTrackInfo.TYPE_AUDIO, mPlayer.getSelectedTrack(
    146                         TvTrackInfo.TYPE_AUDIO));
    147                 notifyTrackSelected(TvTrackInfo.TYPE_VIDEO, mPlayer.getSelectedTrack(
    148                         TvTrackInfo.TYPE_VIDEO));
    149                 notifyTrackSelected(TvTrackInfo.TYPE_SUBTITLE, mPlayer.getSelectedTrack(
    150                         TvTrackInfo.TYPE_SUBTITLE));
    151             }
    152 
    153             @Override
    154             public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
    155                 if (playWhenReady == true && playbackState == ExoPlayer.STATE_BUFFERING) {
    156                     if (mFirstFrameDrawn) {
    157                         notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING);
    158                     }
    159                 } else if (playWhenReady == true && playbackState == ExoPlayer.STATE_READY) {
    160                     notifyVideoAvailable();
    161                 }
    162             }
    163 
    164             @Override
    165             public void onPlayWhenReadyCommitted() {
    166                 // Do nothing.
    167             }
    168 
    169             @Override
    170             public void onPlayerError(ExoPlaybackException e) {
    171                 // Do nothing.
    172             }
    173 
    174             @Override
    175             public void onDrawnToSurface(Surface surface) {
    176                 mFirstFrameDrawn = true;
    177                 notifyVideoAvailable();
    178             }
    179 
    180             @Override
    181             public void onText(String text) {
    182                 if (mSubtitleView != null) {
    183                     if (TextUtils.isEmpty(text)) {
    184                         mSubtitleView.setVisibility(View.INVISIBLE);
    185                     } else {
    186                         mSubtitleView.setVisibility(View.VISIBLE);
    187                         mSubtitleView.setText(text);
    188                     }
    189                 }
    190             }
    191         };
    192 
    193         private PlayCurrentProgramRunnable mPlayCurrentProgramRunnable;
    194 
    195         protected RichTvInputSessionImpl(Context context, String inputId) {
    196             super(context);
    197 
    198             mContext = context;
    199             mInputId = inputId;
    200             mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE);
    201             mLastBlockedRating = null;
    202             mCaptionEnabled = mCaptioningManager.isEnabled();
    203             mHandler = new Handler(this);
    204         }
    205 
    206         @Override
    207         public boolean handleMessage(Message msg) {
    208             if (msg.what == MSG_PLAY_PROGRAM) {
    209                 playProgram((PlaybackInfo) msg.obj);
    210                 return true;
    211             }
    212             return false;
    213         }
    214 
    215         @Override
    216         public void onRelease() {
    217             if (mDbHandler != null) {
    218                 mDbHandler.removeCallbacks(mPlayCurrentProgramRunnable);
    219             }
    220             releasePlayer();
    221             mSessions.remove(this);
    222         }
    223 
    224         @Override
    225         public View onCreateOverlayView() {
    226             LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
    227             View view = inflater.inflate(R.layout.overlayview, null);
    228             mSubtitleView = (SubtitleView) view.findViewById(R.id.subtitles);
    229 
    230             // Configure the subtitle view.
    231             CaptionStyleCompat captionStyle;
    232             float captionTextSize = getCaptionFontSize();
    233             captionStyle = CaptionStyleCompat.createFromCaptionStyle(
    234                     mCaptioningManager.getUserStyle());
    235             captionTextSize *= mCaptioningManager.getFontScale();
    236             mSubtitleView.setStyle(captionStyle);
    237             mSubtitleView.setTextSize(captionTextSize);
    238             return view;
    239         }
    240 
    241         @Override
    242         public boolean onSetSurface(Surface surface) {
    243             if (mPlayer != null) {
    244                 mPlayer.setSurface(surface);
    245             }
    246             mSurface = surface;
    247             return true;
    248         }
    249 
    250         @Override
    251         public void onSetStreamVolume(float volume) {
    252             if (mPlayer != null) {
    253                 mPlayer.setVolume(volume);
    254             }
    255             mVolume = volume;
    256         }
    257 
    258         private boolean playProgram(PlaybackInfo info) {
    259             releasePlayer();
    260 
    261             mCurrentPlaybackInfo = info;
    262             mCurrentContentRating = info.contentRatings.length > 0 ?
    263                     info.contentRatings[0] : null;
    264             mPlayer = new TvInputPlayer();
    265             mPlayer.addCallback(mPlayerCallback);
    266             mPlayer.prepare(RichTvInputService.this, Uri.parse(info.videoUrl), info.videoType);
    267             mPlayer.setSurface(mSurface);
    268             mPlayer.setVolume(mVolume);
    269 
    270             long nowMs = System.currentTimeMillis();
    271             if (info.videoType != TvInputPlayer.SOURCE_TYPE_HTTP_PROGRESSIVE) {
    272                 // If source type is HTTTP progressive, just play from the beginning.
    273                 // TODO: Seeking on http progressive source takes too long.
    274                 //       Enhance ExoPlayer/MediaExtractor and remove the condition above.
    275                 int seekPosMs = (int) (nowMs - info.startTimeMs);
    276                 if (seekPosMs > 0) {
    277                     mPlayer.seekTo(seekPosMs);
    278                 }
    279             }
    280             mPlayer.setPlayWhenReady(true);
    281 
    282             checkContentBlockNeeded();
    283             mDbHandler.postDelayed(mPlayCurrentProgramRunnable, info.endTimeMs - nowMs + 1000);
    284             return true;
    285         }
    286 
    287         @Override
    288         public boolean onTune(Uri channelUri) {
    289             if (mSubtitleView != null) {
    290                 mSubtitleView.setVisibility(View.INVISIBLE);
    291             }
    292             notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING);
    293             mUnblockedRatingSet.clear();
    294 
    295             mDbHandler.removeCallbacks(mPlayCurrentProgramRunnable);
    296             mPlayCurrentProgramRunnable = new PlayCurrentProgramRunnable(channelUri);
    297             mDbHandler.post(mPlayCurrentProgramRunnable);
    298             return true;
    299         }
    300 
    301         @Override
    302         public void onSetCaptionEnabled(boolean enabled) {
    303             mCaptionEnabled = enabled;
    304             if (mPlayer != null) {
    305                 if (enabled) {
    306                     if (mSelectedSubtitleTrackId != null && mPlayer != null) {
    307                         mPlayer.selectTrack(TvTrackInfo.TYPE_SUBTITLE, mSelectedSubtitleTrackId);
    308                     }
    309                 } else {
    310                     mPlayer.selectTrack(TvTrackInfo.TYPE_SUBTITLE, null);
    311                 }
    312             }
    313         }
    314 
    315         @Override
    316         public boolean onSelectTrack(int type, String trackId) {
    317             if (mPlayer != null) {
    318                 if (type == TvTrackInfo.TYPE_SUBTITLE) {
    319                     if (!mCaptionEnabled && trackId != null) {
    320                         return false;
    321                     }
    322                     mSelectedSubtitleTrackId = trackId;
    323                     if (trackId == null) {
    324                         mSubtitleView.setVisibility(View.INVISIBLE);
    325                     }
    326                 }
    327                 if (mPlayer.selectTrack(type, trackId)) {
    328                     notifyTrackSelected(type, trackId);
    329                     return true;
    330                 }
    331             }
    332             return false;
    333         }
    334 
    335         @Override
    336         public void onUnblockContent(TvContentRating rating) {
    337             if (rating != null) {
    338                 unblockContent(rating);
    339             }
    340         }
    341 
    342         private void releasePlayer() {
    343             if (mPlayer != null) {
    344                 mPlayer.removeCallback(mPlayerCallback);
    345                 mPlayer.setSurface(null);
    346                 mPlayer.stop();
    347                 mPlayer.release();
    348                 mPlayer = null;
    349             }
    350         }
    351 
    352         private void checkContentBlockNeeded() {
    353             if (mCurrentContentRating == null || !mTvInputManager.isParentalControlsEnabled()
    354                     || !mTvInputManager.isRatingBlocked(mCurrentContentRating)
    355                     || mUnblockedRatingSet.contains(mCurrentContentRating)) {
    356                 // Content rating is changed so we don't need to block anymore.
    357                 // Unblock content here explicitly to resume playback.
    358                 unblockContent(null);
    359                 return;
    360             }
    361 
    362             mLastBlockedRating = mCurrentContentRating;
    363             if (mPlayer != null) {
    364                 // Children restricted content might be blocked by TV app as well,
    365                 // but TIS should do its best not to show any single frame of blocked content.
    366                 releasePlayer();
    367             }
    368 
    369             notifyContentBlocked(mCurrentContentRating);
    370         }
    371 
    372         private void unblockContent(TvContentRating rating) {
    373             // TIS should unblock content only if unblock request is legitimate.
    374             if (rating == null || mLastBlockedRating == null
    375                     || (mLastBlockedRating != null && rating.equals(mLastBlockedRating))) {
    376                 mLastBlockedRating = null;
    377                 if (rating != null) {
    378                     mUnblockedRatingSet.add(rating);
    379                 }
    380                 if (mPlayer == null && mCurrentPlaybackInfo != null) {
    381                     playProgram(mCurrentPlaybackInfo);
    382                 }
    383                 notifyContentAllowed();
    384             }
    385         }
    386 
    387         private float getCaptionFontSize() {
    388             Display display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE))
    389                     .getDefaultDisplay();
    390             Point displaySize = new Point();
    391             display.getSize(displaySize);
    392             return Math.max(getResources().getDimension(R.dimen.subtitle_minimum_font_size),
    393                     CAPTION_LINE_HEIGHT_RATIO * Math.min(displaySize.x, displaySize.y));
    394         }
    395 
    396         private class PlayCurrentProgramRunnable implements Runnable {
    397             private static final int RETRY_DELAY_MS = 2000;
    398             private final Uri mChannelUri;
    399 
    400             public PlayCurrentProgramRunnable(Uri channelUri) {
    401                 mChannelUri = channelUri;
    402             }
    403 
    404             @Override
    405             public void run() {
    406                 long nowMs = System.currentTimeMillis();
    407                 List<PlaybackInfo> programs = TvContractUtils.getProgramPlaybackInfo(
    408                         mContext.getContentResolver(), mChannelUri, nowMs, nowMs + 1, 1);
    409                 if (!programs.isEmpty()) {
    410                     mHandler.removeMessages(MSG_PLAY_PROGRAM);
    411                     mHandler.obtainMessage(MSG_PLAY_PROGRAM, programs.get(0)).sendToTarget();
    412                 } else {
    413                     Log.w(TAG, "Failed to get program info for " + mChannelUri + ". Retry in " +
    414                             RETRY_DELAY_MS + "ms.");
    415                     mDbHandler.postDelayed(mPlayCurrentProgramRunnable, RETRY_DELAY_MS);
    416                     if (!mEpgSyncRequested) {
    417                         SyncUtils.requestSync(mInputId);
    418                         mEpgSyncRequested = true;
    419                     }
    420                 }
    421             }
    422         }
    423     }
    424 
    425     public static final class ChannelInfo {
    426         public final String number;
    427         public final String name;
    428         public final String logoUrl;
    429         public final int originalNetworkId;
    430         public final int transportStreamId;
    431         public final int serviceId;
    432         public final int videoWidth;
    433         public final int videoHeight;
    434         public final List<ProgramInfo> programs;
    435 
    436         public ChannelInfo(String number, String name, String logoUrl, int originalNetworkId,
    437                            int transportStreamId, int serviceId, int videoWidth, int videoHeight,
    438                            List<ProgramInfo> programs) {
    439             this.number = number;
    440             this.name = name;
    441             this.logoUrl = logoUrl;
    442             this.originalNetworkId = originalNetworkId;
    443             this.transportStreamId = transportStreamId;
    444             this.serviceId = serviceId;
    445             this.videoWidth = videoWidth;
    446             this.videoHeight = videoHeight;
    447             this.programs = programs;
    448         }
    449     }
    450 
    451     public static final class ProgramInfo {
    452         public final String title;
    453         public final String posterArtUri;
    454         public final String description;
    455         public final long durationSec;
    456         public final String videoUrl;
    457         public final int videoType;
    458         public final int resourceId;
    459         public final TvContentRating[] contentRatings;
    460 
    461         public ProgramInfo(String title, String posterArtUri, String description, long durationSec,
    462                            TvContentRating[] contentRatings, String videoUrl, int videoType, int resourceId) {
    463             this.title = title;
    464             this.posterArtUri = posterArtUri;
    465             this.description = description;
    466             this.durationSec = durationSec;
    467             this.contentRatings = contentRatings;
    468             this.videoUrl = videoUrl;
    469             this.videoType = videoType;
    470             this.resourceId = resourceId;
    471         }
    472     }
    473 
    474     public static final class PlaybackInfo {
    475         public final long startTimeMs;
    476         public final long endTimeMs;
    477         public final String videoUrl;
    478         public final int videoType;
    479         public final TvContentRating[] contentRatings;
    480 
    481         public PlaybackInfo(long startTimeMs, long endTimeMs, String videoUrl, int videoType,
    482                             TvContentRating[] contentRatings) {
    483             this.startTimeMs = startTimeMs;
    484             this.endTimeMs = endTimeMs;
    485             this.contentRatings = contentRatings;
    486             this.videoUrl = videoUrl;
    487             this.videoType = videoType;
    488         }
    489     }
    490 
    491     public static final class TvInput {
    492         public final String displayName;
    493         public final String name;
    494         public final String description;
    495         public final String logoThumbUrl;
    496         public final String logoBackgroundUrl;
    497 
    498         public TvInput(String displayName,
    499                        String name,
    500                        String description,
    501                        String logoThumbUrl,
    502                        String logoBackgroundUrl) {
    503             this.displayName = displayName;
    504             this.name = name;
    505             this.description = description;
    506             this.logoThumbUrl = logoThumbUrl;
    507             this.logoBackgroundUrl = logoBackgroundUrl;
    508         }
    509     }
    510 }
    511