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.testinput; 18 19 import android.annotation.TargetApi; 20 import android.content.ComponentName; 21 import android.content.ContentUris; 22 import android.content.ContentValues; 23 import android.content.Context; 24 import android.database.Cursor; 25 import android.graphics.Canvas; 26 import android.graphics.Color; 27 import android.graphics.Paint; 28 import android.media.PlaybackParams; 29 import android.media.tv.TvContract; 30 import android.media.tv.TvContract.Programs; 31 import android.media.tv.TvContract.RecordedPrograms; 32 import android.media.tv.TvInputManager; 33 import android.media.tv.TvInputService; 34 import android.media.tv.TvTrackInfo; 35 import android.net.Uri; 36 import android.os.AsyncTask; 37 import android.os.Build; 38 import android.os.Handler; 39 import android.os.Looper; 40 import android.os.Message; 41 import android.util.Log; 42 import android.view.KeyEvent; 43 import android.view.Surface; 44 45 import com.android.tv.input.TunerHelper; 46 import com.android.tv.testing.ChannelInfo; 47 import com.android.tv.testing.testinput.ChannelState; 48 49 import java.util.Date; 50 import java.util.concurrent.TimeUnit; 51 52 /** 53 * Simple TV input service which provides test channels. 54 */ 55 public class TestTvInputService extends TvInputService { 56 private static final String TAG = "TestTvInputService"; 57 private static final int REFRESH_DELAY_MS = 1000 / 5; 58 private static final boolean DEBUG = false; 59 60 // Consider the command delivering time from Live TV. 61 private static final long MAX_COMMAND_DELAY = TimeUnit.SECONDS.toMillis(3); 62 63 private final TestInputControl mBackend = TestInputControl.getInstance(); 64 65 private TunerHelper mTunerHelper; 66 buildInputId(Context context)67 public static String buildInputId(Context context) { 68 return TvContract.buildInputId(new ComponentName(context, TestTvInputService.class)); 69 } 70 71 @Override onCreate()72 public void onCreate() { 73 super.onCreate(); 74 mBackend.init(this, buildInputId(this)); 75 mTunerHelper = new TunerHelper(getResources().getInteger(R.integer.tuner_count)); 76 } 77 78 @Override onCreateSession(String inputId)79 public Session onCreateSession(String inputId) { 80 Log.v(TAG, "Creating session for " + inputId); 81 // onCreateSession always succeeds because this session can be used to play the recorded 82 // program. 83 return new SimpleSessionImpl(this); 84 } 85 86 @TargetApi(Build.VERSION_CODES.N) 87 @Override onCreateRecordingSession(String inputId)88 public RecordingSession onCreateRecordingSession(String inputId) { 89 Log.v(TAG, "Creating recording session for " + inputId); 90 if (!mTunerHelper.tunerAvailableForRecording()) { 91 return null; 92 } 93 return new SimpleRecordingSessionImpl(this, inputId); 94 } 95 96 /** 97 * Simple session implementation that just display some text. 98 */ 99 private class SimpleSessionImpl extends Session { 100 private static final int MSG_SEEK = 1000; 101 private static final int SEEK_DELAY_MS = 300; 102 103 private final Paint mTextPaint = new Paint(); 104 private final DrawRunnable mDrawRunnable = new DrawRunnable(); 105 private Surface mSurface = null; 106 private Uri mChannelUri = null; 107 private ChannelInfo mChannel = null; 108 private ChannelState mCurrentState = null; 109 private String mCurrentVideoTrackId = null; 110 private String mCurrentAudioTrackId = null; 111 112 private long mRecordStartTimeMs; 113 private long mPausedTimeMs; 114 // The time in milliseconds when the current position is lastly updated. 115 private long mLastCurrentPositionUpdateTimeMs; 116 // The current playback position. 117 private long mCurrentPositionMs; 118 // The current playback speed rate. 119 private float mSpeed; 120 121 private final Handler mHandler = new Handler(Looper.myLooper()) { 122 @Override 123 public void handleMessage(Message msg) { 124 if (msg.what == MSG_SEEK) { 125 // Actually, this input doesn't play any videos, it just shows the image. 126 // So we should simulate the playback here by changing the current playback 127 // position periodically in order to test the time shift. 128 // If the playback is paused, the current playback position doesn't need to be 129 // changed. 130 if (mPausedTimeMs == 0) { 131 long currentTimeMs = System.currentTimeMillis(); 132 mCurrentPositionMs += (long) ((currentTimeMs 133 - mLastCurrentPositionUpdateTimeMs) * mSpeed); 134 mCurrentPositionMs = Math.max(mRecordStartTimeMs, 135 Math.min(mCurrentPositionMs, currentTimeMs)); 136 mLastCurrentPositionUpdateTimeMs = currentTimeMs; 137 } 138 sendEmptyMessageDelayed(MSG_SEEK, SEEK_DELAY_MS); 139 } 140 super.handleMessage(msg); 141 } 142 }; 143 SimpleSessionImpl(Context context)144 SimpleSessionImpl(Context context) { 145 super(context); 146 mTextPaint.setColor(Color.BLACK); 147 mTextPaint.setTextSize(150); 148 mHandler.post(mDrawRunnable); 149 if (DEBUG) { 150 Log.v(TAG, "Created session " + this); 151 } 152 } 153 setAudioTrack(String selectedAudioTrackId)154 private void setAudioTrack(String selectedAudioTrackId) { 155 Log.i(TAG, "Set audio track to " + selectedAudioTrackId); 156 mCurrentAudioTrackId = selectedAudioTrackId; 157 notifyTrackSelected(TvTrackInfo.TYPE_AUDIO, mCurrentAudioTrackId); 158 } 159 setVideoTrack(String selectedVideoTrackId)160 private void setVideoTrack(String selectedVideoTrackId) { 161 Log.i(TAG, "Set video track to " + selectedVideoTrackId); 162 mCurrentVideoTrackId = selectedVideoTrackId; 163 notifyTrackSelected(TvTrackInfo.TYPE_VIDEO, mCurrentVideoTrackId); 164 } 165 166 @Override onRelease()167 public void onRelease() { 168 if (DEBUG) { 169 Log.v(TAG, "Releasing session " + this); 170 } 171 mTunerHelper.stopTune(mChannelUri); 172 mDrawRunnable.cancel(); 173 mHandler.removeCallbacks(mDrawRunnable); 174 mSurface = null; 175 mChannelUri = null; 176 mChannel = null; 177 mCurrentState = null; 178 } 179 180 @Override onSetSurface(Surface surface)181 public boolean onSetSurface(Surface surface) { 182 synchronized (mDrawRunnable) { 183 mSurface = surface; 184 } 185 if (surface != null) { 186 if (DEBUG) { 187 Log.v(TAG, "Surface set"); 188 } 189 } else { 190 if (DEBUG) { 191 Log.v(TAG, "Surface unset"); 192 } 193 } 194 195 return true; 196 } 197 198 @Override onSurfaceChanged(int format, int width, int height)199 public void onSurfaceChanged(int format, int width, int height) { 200 super.onSurfaceChanged(format, width, height); 201 Log.d(TAG, "format=" + format + " width=" + width + " height=" + height); 202 } 203 204 @Override onSetStreamVolume(float volume)205 public void onSetStreamVolume(float volume) { 206 // No-op 207 } 208 209 @Override onTune(Uri channelUri)210 public boolean onTune(Uri channelUri) { 211 Log.i(TAG, "Tune to " + channelUri); 212 mTunerHelper.stopTune(mChannelUri); 213 mChannelUri = channelUri; 214 ChannelInfo info = mBackend.getChannelInfo(channelUri); 215 synchronized (mDrawRunnable) { 216 if (info == null || mChannel == null 217 || mChannel.originalNetworkId != info.originalNetworkId) { 218 mCurrentState = null; 219 } 220 mChannel = info; 221 mCurrentVideoTrackId = null; 222 mCurrentAudioTrackId = null; 223 } 224 if (mChannel == null) { 225 Log.i(TAG, "Channel not found for " + channelUri); 226 notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); 227 } else if (!mTunerHelper.tune(channelUri, false)) { 228 Log.i(TAG, "No available tuner for " + channelUri); 229 notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); 230 } else { 231 Log.i(TAG, "Tuning to " + mChannel); 232 } 233 notifyTimeShiftStatusChanged(TvInputManager.TIME_SHIFT_STATUS_AVAILABLE); 234 mRecordStartTimeMs = mCurrentPositionMs = mLastCurrentPositionUpdateTimeMs 235 = System.currentTimeMillis(); 236 mPausedTimeMs = 0; 237 mHandler.sendEmptyMessageDelayed(MSG_SEEK, SEEK_DELAY_MS); 238 mSpeed = 1; 239 return true; 240 } 241 242 @Override onSetCaptionEnabled(boolean enabled)243 public void onSetCaptionEnabled(boolean enabled) { 244 // No-op 245 } 246 247 @Override onKeyDown(int keyCode, KeyEvent event)248 public boolean onKeyDown(int keyCode, KeyEvent event) { 249 Log.d(TAG, "onKeyDown (keyCode=" + keyCode + ", event=" + event + ")"); 250 return true; 251 } 252 253 @Override onKeyUp(int keyCode, KeyEvent event)254 public boolean onKeyUp(int keyCode, KeyEvent event) { 255 Log.d(TAG, "onKeyUp (keyCode=" + keyCode + ", event=" + event + ")"); 256 return true; 257 } 258 259 @Override onTimeShiftGetCurrentPosition()260 public long onTimeShiftGetCurrentPosition() { 261 Log.d(TAG, "currentPositionMs=" + mCurrentPositionMs); 262 return mCurrentPositionMs; 263 } 264 265 @Override onTimeShiftGetStartPosition()266 public long onTimeShiftGetStartPosition() { 267 return mRecordStartTimeMs; 268 } 269 270 @Override onTimeShiftPause()271 public void onTimeShiftPause() { 272 mCurrentPositionMs = mPausedTimeMs = mLastCurrentPositionUpdateTimeMs 273 = System.currentTimeMillis(); 274 } 275 276 @Override onTimeShiftResume()277 public void onTimeShiftResume() { 278 mSpeed = 1; 279 mPausedTimeMs = 0; 280 mLastCurrentPositionUpdateTimeMs = System.currentTimeMillis(); 281 } 282 283 @Override onTimeShiftSeekTo(long timeMs)284 public void onTimeShiftSeekTo(long timeMs) { 285 mLastCurrentPositionUpdateTimeMs = System.currentTimeMillis(); 286 mCurrentPositionMs = Math.max(mRecordStartTimeMs, 287 Math.min(timeMs, mLastCurrentPositionUpdateTimeMs)); 288 } 289 290 @Override onTimeShiftSetPlaybackParams(PlaybackParams params)291 public void onTimeShiftSetPlaybackParams(PlaybackParams params) { 292 mSpeed = params.getSpeed(); 293 } 294 295 private final class DrawRunnable implements Runnable { 296 private volatile boolean mIsCanceled = false; 297 298 @Override run()299 public void run() { 300 if (mIsCanceled) { 301 return; 302 } 303 if (DEBUG) { 304 Log.v(TAG, "Draw task running"); 305 } 306 boolean updatedState = false; 307 ChannelState oldState; 308 ChannelState newState = null; 309 Surface currentSurface; 310 ChannelInfo currentChannel; 311 312 synchronized (this) { 313 oldState = mCurrentState; 314 currentSurface = mSurface; 315 currentChannel = mChannel; 316 if (currentChannel != null) { 317 newState = mBackend.getChannelState(currentChannel.originalNetworkId); 318 if (oldState == null || newState.getVersion() > oldState.getVersion()) { 319 mCurrentState = newState; 320 updatedState = true; 321 } 322 } else { 323 mCurrentState = null; 324 } 325 326 if (currentSurface != null) { 327 String now = new Date(mCurrentPositionMs).toString(); 328 String name = currentChannel == null ? "Null" : currentChannel.name; 329 try { 330 Canvas c = currentSurface.lockCanvas(null); 331 c.drawColor(0xFF888888); 332 c.drawText(name, 100f, 200f, mTextPaint); 333 c.drawText(now, 100f, 400f, mTextPaint); 334 // Assuming c.drawXXX will never fail. 335 currentSurface.unlockCanvasAndPost(c); 336 } catch (IllegalArgumentException e) { 337 // The surface might have been abandoned. Ignore the exception. 338 } 339 if (DEBUG) { 340 Log.v(TAG, "Post to canvas"); 341 } 342 } else { 343 if (DEBUG) { 344 Log.v(TAG, "No surface"); 345 } 346 } 347 } 348 if (updatedState) { 349 update(oldState, newState, currentChannel); 350 } 351 352 if (!mIsCanceled) { 353 mHandler.postDelayed(this, REFRESH_DELAY_MS); 354 } 355 } 356 update(ChannelState oldState, ChannelState newState, ChannelInfo currentChannel)357 private void update(ChannelState oldState, ChannelState newState, 358 ChannelInfo currentChannel) { 359 Log.i(TAG, "Updating channel " + currentChannel.number + " state to " + newState); 360 notifyTracksChanged(newState.getTrackInfoList()); 361 if (oldState == null || oldState.getTuneStatus() != newState.getTuneStatus()) { 362 if (newState.getTuneStatus() == ChannelState.TUNE_STATUS_VIDEO_AVAILABLE) { 363 notifyVideoAvailable(); 364 //TODO handle parental controls. 365 notifyContentAllowed(); 366 setAudioTrack(newState.getSelectedAudioTrackId()); 367 setVideoTrack(newState.getSelectedVideoTrackId()); 368 } else { 369 notifyVideoUnavailable(newState.getTuneStatus()); 370 } 371 } 372 } 373 cancel()374 public void cancel() { 375 mIsCanceled = true; 376 } 377 } 378 } 379 380 private class SimpleRecordingSessionImpl extends RecordingSession { 381 private final String[] PROGRAM_PROJECTION = { 382 Programs.COLUMN_TITLE, 383 Programs.COLUMN_EPISODE_TITLE, 384 Programs.COLUMN_SHORT_DESCRIPTION, 385 Programs.COLUMN_POSTER_ART_URI, 386 Programs.COLUMN_THUMBNAIL_URI, 387 Programs.COLUMN_CANONICAL_GENRE, 388 Programs.COLUMN_CONTENT_RATING, 389 Programs.COLUMN_START_TIME_UTC_MILLIS, 390 Programs.COLUMN_END_TIME_UTC_MILLIS, 391 Programs.COLUMN_VIDEO_WIDTH, 392 Programs.COLUMN_VIDEO_HEIGHT, 393 Programs.COLUMN_SEASON_DISPLAY_NUMBER, 394 Programs.COLUMN_SEASON_TITLE, 395 Programs.COLUMN_EPISODE_DISPLAY_NUMBER, 396 }; 397 398 private final String mInputId; 399 private long mStartTime; 400 private long mEndTime; 401 private Uri mChannelUri; 402 private Uri mProgramHintUri; 403 SimpleRecordingSessionImpl(Context context, String inputId)404 public SimpleRecordingSessionImpl(Context context, String inputId) { 405 super(context); 406 mInputId = inputId; 407 } 408 409 @Override onTune(Uri uri)410 public void onTune(Uri uri) { 411 Log.i(TAG, "SimpleReccordingSesesionImpl: onTune()"); 412 mTunerHelper.stopRecording(mChannelUri); 413 mChannelUri = uri; 414 ChannelInfo channel = mBackend.getChannelInfo(uri); 415 if (channel == null) { 416 notifyError(TvInputManager.RECORDING_ERROR_UNKNOWN); 417 } else if (!mTunerHelper.tune(uri, true)) { 418 notifyError(TvInputManager.RECORDING_ERROR_RESOURCE_BUSY); 419 } else { 420 notifyTuned(uri); 421 } 422 } 423 424 @Override onStartRecording(Uri programHintUri)425 public void onStartRecording(Uri programHintUri) { 426 Log.i(TAG, "SimpleReccordingSesesionImpl: onStartRecording()"); 427 mStartTime = System.currentTimeMillis(); 428 mProgramHintUri = programHintUri; 429 } 430 431 @Override onStopRecording()432 public void onStopRecording() { 433 Log.i(TAG, "SimpleReccordingSesesionImpl: onStopRecording()"); 434 mEndTime = System.currentTimeMillis(); 435 final long startTime = mStartTime; 436 final long endTime = mEndTime; 437 final Uri programHintUri = mProgramHintUri; 438 final Uri channelUri = mChannelUri; 439 new AsyncTask<Void, Void, Void>() { 440 @Override 441 protected Void doInBackground(Void... arg0) { 442 long time = System.currentTimeMillis(); 443 if (programHintUri != null) { 444 // Retrieves program info from mProgramHintUri 445 try (Cursor c = getContentResolver().query(programHintUri, 446 PROGRAM_PROJECTION, null, null, null)) { 447 if (c != null && c.getCount() > 0) { 448 storeRecordedProgram(c, startTime, endTime); 449 return null; 450 } 451 } catch (Exception e) { 452 Log.w(TAG, "Error querying " + this, e); 453 } 454 } 455 // Retrieves the current program 456 try (Cursor c = getContentResolver().query( 457 TvContract.buildProgramsUriForChannel(channelUri, startTime, 458 endTime - startTime < MAX_COMMAND_DELAY ? startTime : 459 endTime - MAX_COMMAND_DELAY), 460 PROGRAM_PROJECTION, null, null, null)) { 461 if (c != null && c.getCount() == 1) { 462 storeRecordedProgram(c, startTime, endTime); 463 return null; 464 } 465 } catch (Exception e) { 466 Log.w(TAG, "Error querying " + this, e); 467 } 468 storeRecordedProgram(null, startTime, endTime); 469 return null; 470 } 471 472 private void storeRecordedProgram(Cursor c, long startTime, long endTime) { 473 ContentValues values = new ContentValues(); 474 values.put(RecordedPrograms.COLUMN_INPUT_ID, mInputId); 475 values.put(RecordedPrograms.COLUMN_CHANNEL_ID, 476 ContentUris.parseId(channelUri)); 477 values.put(RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, 478 endTime - startTime); 479 if (c != null) { 480 int index = 0; 481 c.moveToNext(); 482 values.put(Programs.COLUMN_TITLE, c.getString(index++)); 483 values.put(Programs.COLUMN_EPISODE_TITLE, c.getString(index++)); 484 values.put(Programs.COLUMN_SHORT_DESCRIPTION, c.getString(index++)); 485 values.put(Programs.COLUMN_POSTER_ART_URI, c.getString(index++)); 486 values.put(Programs.COLUMN_THUMBNAIL_URI, c.getString(index++)); 487 values.put(Programs.COLUMN_CANONICAL_GENRE, c.getString(index++)); 488 values.put(Programs.COLUMN_CONTENT_RATING, c.getString(index++)); 489 values.put(Programs.COLUMN_START_TIME_UTC_MILLIS, c.getLong(index++)); 490 values.put(Programs.COLUMN_END_TIME_UTC_MILLIS, c.getLong(index++)); 491 values.put(Programs.COLUMN_VIDEO_WIDTH, c.getLong(index++)); 492 values.put(Programs.COLUMN_VIDEO_HEIGHT, c.getLong(index++)); 493 values.put(Programs.COLUMN_SEASON_DISPLAY_NUMBER, c.getString(index++)); 494 values.put(Programs.COLUMN_SEASON_TITLE, c.getString(index++)); 495 values.put(Programs.COLUMN_EPISODE_DISPLAY_NUMBER, 496 c.getString(index++)); 497 } else { 498 values.put(RecordedPrograms.COLUMN_TITLE, "No program info"); 499 values.put(RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS, startTime); 500 values.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, endTime); 501 } 502 Uri uri = getContentResolver() 503 .insert(TvContract.RecordedPrograms.CONTENT_URI, values); 504 notifyRecordingStopped(uri); 505 } 506 }.execute(); 507 } 508 509 @Override onRelease()510 public void onRelease() { 511 Log.i(TAG, "SimpleReccordingSesesionImpl: onRelease()"); 512 mTunerHelper.stopRecording(mChannelUri); 513 mChannelUri = null; 514 } 515 } 516 } 517