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 onCreate()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 onDestroy()98 public void onDestroy() { 99 super.onDestroy(); 100 unregisterReceiver(mParentalControlsBroadcastReceiver); 101 mHandlerThread.quit(); 102 mHandlerThread = null; 103 mDbHandler = null; 104 } 105 106 @Override onCreateSession(String inputId)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 RichTvInputSessionImpl(Context context, String inputId)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 handleMessage(Message msg)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 onRelease()216 public void onRelease() { 217 if (mDbHandler != null) { 218 mDbHandler.removeCallbacks(mPlayCurrentProgramRunnable); 219 } 220 releasePlayer(); 221 mSessions.remove(this); 222 } 223 224 @Override onCreateOverlayView()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 onSetSurface(Surface surface)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 onSetStreamVolume(float volume)251 public void onSetStreamVolume(float volume) { 252 if (mPlayer != null) { 253 mPlayer.setVolume(volume); 254 } 255 mVolume = volume; 256 } 257 playProgram(PlaybackInfo info)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 onTune(Uri channelUri)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 onSetCaptionEnabled(boolean enabled)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 onSelectTrack(int type, String trackId)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 onUnblockContent(TvContentRating rating)336 public void onUnblockContent(TvContentRating rating) { 337 if (rating != null) { 338 unblockContent(rating); 339 } 340 } 341 releasePlayer()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 checkContentBlockNeeded()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 unblockContent(TvContentRating rating)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 getCaptionFontSize()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 PlayCurrentProgramRunnable(Uri channelUri)400 public PlayCurrentProgramRunnable(Uri channelUri) { 401 mChannelUri = channelUri; 402 } 403 404 @Override run()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 ChannelInfo(String number, String name, String logoUrl, int originalNetworkId, int transportStreamId, int serviceId, int videoWidth, int videoHeight, List<ProgramInfo> programs)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 ProgramInfo(String title, String posterArtUri, String description, long durationSec, TvContentRating[] contentRatings, String videoUrl, int videoType, int resourceId)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 PlaybackInfo(long startTimeMs, long endTimeMs, String videoUrl, int videoType, TvContentRating[] contentRatings)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 TvInput(String displayName, String name, String description, String logoThumbUrl, String logoBackgroundUrl)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