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