1 /* 2 * Copyright (C) 2014 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.cts.verifier.tv; 18 19 import android.annotation.SuppressLint; 20 import android.content.BroadcastReceiver; 21 import android.content.ComponentName; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.IntentFilter; 25 import android.graphics.Bitmap; 26 import android.graphics.BitmapFactory; 27 import android.graphics.Canvas; 28 import android.graphics.Color; 29 import android.graphics.Rect; 30 import android.media.PlaybackParams; 31 import android.media.tv.TvContentRating; 32 import android.media.tv.TvContract; 33 import android.media.tv.TvInputManager; 34 import android.media.tv.TvInputService; 35 import android.media.tv.TvTrackInfo; 36 import android.net.Uri; 37 import android.os.Bundle; 38 import android.os.Handler; 39 import android.os.Looper; 40 import android.os.Message; 41 import android.util.Log; 42 import android.view.LayoutInflater; 43 import android.view.Surface; 44 import android.view.View; 45 import android.widget.TextView; 46 47 import com.android.cts.verifier.R; 48 49 import java.util.ArrayList; 50 import java.util.List; 51 52 @SuppressLint("NewApi") 53 public class MockTvInputService extends TvInputService { 54 private static final String TAG = "MockTvInputService"; 55 56 private static final String BROADCAST_ACTION = "action"; 57 private static final String SELECT_TRACK_TYPE = "type"; 58 private static final String SELECT_TRACK_ID = "id"; 59 private static final String CAPTION_ENABLED = "enabled"; 60 private static final String PAUSE_CALLED = "pause_called"; 61 private static final float DISPLAY_RATIO_EPSILON = 0.01f; 62 63 private static final Object sLock = new Object(); 64 private static Callback sTuneCallback = null; 65 private static Callback sOverlayViewCallback = null; 66 private static Callback sBroadcastCallback = null; 67 private static Callback sUnblockContentCallback = null; 68 private static Callback sSelectTrackCallback = null; 69 private static Callback sSetCaptionEnabledCallback = null; 70 // Callbacks for time shift. 71 private static Callback sResumeAfterPauseCallback = null; 72 private static Callback sPositionTrackingCallback = null; 73 private static Callback sRewindCallback = null; 74 private static Callback sFastForwardCallback = null; 75 private static Callback sSeekToPreviousCallback = null; 76 private static Callback sSeekToNextCallback = null; 77 private static Callback sOverlayViewSizeChangedCallback = null; 78 79 private static TvContentRating sRating = null; 80 81 static final TvTrackInfo sEngAudioTrack = 82 new TvTrackInfo.Builder(TvTrackInfo.TYPE_AUDIO, "audio_eng") 83 .setAudioChannelCount(2) 84 .setAudioSampleRate(48000) 85 .setLanguage("eng") 86 .build(); 87 static final TvTrackInfo sSpaAudioTrack = 88 new TvTrackInfo.Builder(TvTrackInfo.TYPE_AUDIO, "audio_spa") 89 .setAudioChannelCount(2) 90 .setAudioSampleRate(48000) 91 .setLanguage("spa") 92 .build(); 93 static final TvTrackInfo sEngSubtitleTrack = 94 new TvTrackInfo.Builder(TvTrackInfo.TYPE_SUBTITLE, "subtitle_eng") 95 .setLanguage("eng") 96 .build(); 97 static final TvTrackInfo sKorSubtitleTrack = 98 new TvTrackInfo.Builder(TvTrackInfo.TYPE_SUBTITLE, "subtitle_kor") 99 .setLanguage("kor") 100 .build(); 101 // These parameters make the display aspect ratio of sDummyVideoTrack be 4:3, 102 // which is one of common standards. 103 static final TvTrackInfo sDummyVideoTrack = 104 new TvTrackInfo.Builder(TvTrackInfo.TYPE_VIDEO, "video_dummy") 105 .setVideoWidth(704) 106 .setVideoHeight(480) 107 .setVideoPixelAspectRatio(0.909f) 108 .setVideoFrameRate(60) 109 .build(); 110 111 private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { 112 @Override 113 public void onReceive(Context context, Intent intent) { 114 synchronized (sLock) { 115 if (sBroadcastCallback != null) { 116 String expectedAction = 117 sBroadcastCallback.getBundle().getString(BROADCAST_ACTION); 118 if (intent.getAction().equals(expectedAction)) { 119 sBroadcastCallback.post(); 120 sBroadcastCallback = null; 121 } 122 } 123 } 124 } 125 }; 126 expectTune(View postTarget, Runnable successCallback)127 static void expectTune(View postTarget, Runnable successCallback) { 128 synchronized (sLock) { 129 sTuneCallback = new Callback(postTarget, successCallback); 130 } 131 } 132 expectBroadcast(View postTarget, String action, Runnable successCallback)133 static void expectBroadcast(View postTarget, String action, Runnable successCallback) { 134 synchronized (sLock) { 135 sBroadcastCallback = new Callback(postTarget, successCallback); 136 sBroadcastCallback.getBundle().putString(BROADCAST_ACTION, action); 137 } 138 } 139 expectUnblockContent(View postTarget, Runnable successCallback)140 static void expectUnblockContent(View postTarget, Runnable successCallback) { 141 synchronized (sLock) { 142 sUnblockContentCallback = new Callback(postTarget, successCallback); 143 } 144 } 145 setBlockRating(TvContentRating rating)146 static void setBlockRating(TvContentRating rating) { 147 synchronized (sLock) { 148 sRating = rating; 149 } 150 } 151 expectOverlayView(View postTarget, Runnable successCallback)152 static void expectOverlayView(View postTarget, Runnable successCallback) { 153 synchronized (sLock) { 154 sOverlayViewCallback = new Callback(postTarget, successCallback); 155 } 156 } 157 expectSelectTrack(int type, String id, View postTarget, Runnable successCallback)158 static void expectSelectTrack(int type, String id, View postTarget, Runnable successCallback) { 159 synchronized (sLock) { 160 sSelectTrackCallback = new Callback(postTarget, successCallback); 161 sSelectTrackCallback.getBundle().putInt(SELECT_TRACK_TYPE, type); 162 sSelectTrackCallback.getBundle().putString(SELECT_TRACK_ID, id); 163 } 164 } 165 expectSetCaptionEnabled(boolean enabled, View postTarget, Runnable successCallback)166 static void expectSetCaptionEnabled(boolean enabled, View postTarget, 167 Runnable successCallback) { 168 synchronized (sLock) { 169 sSetCaptionEnabledCallback = new Callback(postTarget, successCallback); 170 sSetCaptionEnabledCallback.getBundle().putBoolean(CAPTION_ENABLED, enabled); 171 } 172 } 173 expectResumeAfterPause(View postTarget, Runnable successCallback)174 static void expectResumeAfterPause(View postTarget, Runnable successCallback) { 175 synchronized (sLock) { 176 sResumeAfterPauseCallback = new Callback(postTarget, successCallback); 177 } 178 } 179 expectPositionTracking(View postTarget, Runnable successCallback)180 static void expectPositionTracking(View postTarget, Runnable successCallback) { 181 synchronized (sLock) { 182 sPositionTrackingCallback = new Callback(postTarget, successCallback); 183 } 184 } 185 expectRewind(View postTarget, Runnable successCallback)186 static void expectRewind(View postTarget, Runnable successCallback) { 187 synchronized (sLock) { 188 sRewindCallback = new Callback(postTarget, successCallback); 189 } 190 } 191 expectFastForward(View postTarget, Runnable successCallback)192 static void expectFastForward(View postTarget, Runnable successCallback) { 193 synchronized (sLock) { 194 sFastForwardCallback = new Callback(postTarget, successCallback); 195 } 196 } 197 expectSeekToPrevious(View postTarget, Runnable successCallback)198 static void expectSeekToPrevious(View postTarget, Runnable successCallback) { 199 synchronized (sLock) { 200 sSeekToPreviousCallback = new Callback(postTarget, successCallback); 201 } 202 } 203 expectSeekToNext(View postTarget, Runnable successCallback)204 static void expectSeekToNext(View postTarget, Runnable successCallback) { 205 synchronized (sLock) { 206 sSeekToNextCallback = new Callback(postTarget, successCallback); 207 } 208 } 209 expectedVideoAspectRatio(View postTarget, Runnable successCallback)210 static void expectedVideoAspectRatio(View postTarget, Runnable successCallback) { 211 synchronized (sLock) { 212 sOverlayViewSizeChangedCallback = new Callback(postTarget, successCallback); 213 } 214 } 215 getInputId(Context context)216 static String getInputId(Context context) { 217 return TvContract.buildInputId(new ComponentName(context, 218 MockTvInputService.class.getName())); 219 } 220 221 @Override onCreate()222 public void onCreate() { 223 super.onCreate(); 224 IntentFilter intentFilter = new IntentFilter(); 225 intentFilter.addAction(TvInputManager.ACTION_BLOCKED_RATINGS_CHANGED); 226 intentFilter.addAction(TvInputManager.ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED); 227 registerReceiver(mBroadcastReceiver, intentFilter); 228 } 229 230 @Override onDestroy()231 public void onDestroy() { 232 unregisterReceiver(mBroadcastReceiver); 233 super.onDestroy(); 234 } 235 236 @Override onCreateSession(String inputId)237 public Session onCreateSession(String inputId) { 238 Session session = new MockSessionImpl(this); 239 session.setOverlayViewEnabled(true); 240 return session; 241 } 242 243 private static class MockSessionImpl extends Session { 244 private static final int MSG_SEEK = 1000; 245 private static final int SEEK_DELAY_MS = 300; 246 247 private final Context mContext; 248 private Surface mSurface = null; 249 private List<TvTrackInfo> mTracks = new ArrayList<>(); 250 251 private long mRecordStartTimeMs; 252 private long mPausedTimeMs; 253 // The time in milliseconds when the current position is lastly updated. 254 private long mLastCurrentPositionUpdateTimeMs; 255 // The current playback position. 256 private long mCurrentPositionMs; 257 // The current playback speed rate. 258 private float mSpeed; 259 260 private final Handler mHandler = new Handler(Looper.getMainLooper()) { 261 @Override 262 public void handleMessage(Message msg) { 263 if (msg.what == MSG_SEEK) { 264 // Actually, this input doesn't play any videos, it just shows the image. 265 // So we should simulate the playback here by changing the current playback 266 // position periodically in order to test the time shift. 267 // If the playback is paused, the current playback position doesn't need to be 268 // changed. 269 if (mPausedTimeMs == 0) { 270 long currentTimeMs = System.currentTimeMillis(); 271 mCurrentPositionMs += (long) ((currentTimeMs 272 - mLastCurrentPositionUpdateTimeMs) * mSpeed); 273 mCurrentPositionMs = Math.max(mRecordStartTimeMs, 274 Math.min(mCurrentPositionMs, currentTimeMs)); 275 mLastCurrentPositionUpdateTimeMs = currentTimeMs; 276 } 277 sendEmptyMessageDelayed(MSG_SEEK, SEEK_DELAY_MS); 278 } 279 super.handleMessage(msg); 280 } 281 }; 282 MockSessionImpl(Context context)283 private MockSessionImpl(Context context) { 284 super(context); 285 mContext = context; 286 mTracks.add(sEngAudioTrack); 287 mTracks.add(sSpaAudioTrack); 288 mTracks.add(sEngSubtitleTrack); 289 mTracks.add(sKorSubtitleTrack); 290 mTracks.add(sDummyVideoTrack); 291 } 292 293 @Override onRelease()294 public void onRelease() { 295 } 296 draw()297 private void draw() { 298 Surface surface = mSurface; 299 if (surface == null) return; 300 if (!surface.isValid()) return; 301 302 Canvas c = surface.lockCanvas(null); 303 if (c == null) return; 304 try { 305 Bitmap b = BitmapFactory.decodeResource( 306 mContext.getResources(), R.drawable.icon); 307 int srcWidth = b.getWidth(); 308 int srcHeight = b.getHeight(); 309 int dstWidth = c.getWidth(); 310 int dstHeight = c.getHeight(); 311 c.drawColor(Color.BLACK); 312 c.drawBitmap(b, new Rect(0, 0, srcWidth, srcHeight), 313 new Rect(10, 10, dstWidth - 10, dstHeight - 10), null); 314 } finally { 315 surface.unlockCanvasAndPost(c); 316 } 317 } 318 319 @Override onCreateOverlayView()320 public View onCreateOverlayView() { 321 LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( 322 LAYOUT_INFLATER_SERVICE); 323 View view = inflater.inflate(R.layout.tv_overlay, null); 324 TextView textView = (TextView) view.findViewById(R.id.overlay_view_text); 325 textView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { 326 @Override 327 public void onLayoutChange(View v, int left, int top, int right, int bottom, 328 int oldLeft, int oldTop, int oldRight, int oldBottom) { 329 Callback overlayViewCallback = null; 330 synchronized (sLock) { 331 overlayViewCallback = sOverlayViewCallback; 332 sOverlayViewCallback = null; 333 } 334 if (overlayViewCallback != null) { 335 overlayViewCallback.post(); 336 } 337 } 338 }); 339 return view; 340 } 341 342 @Override onSetSurface(Surface surface)343 public boolean onSetSurface(Surface surface) { 344 mSurface = surface; 345 if (surface != null) { 346 draw(); 347 } 348 return true; 349 } 350 351 @Override onSetStreamVolume(float volume)352 public void onSetStreamVolume(float volume) { 353 } 354 355 @Override onTune(Uri channelUri)356 public boolean onTune(Uri channelUri) { 357 synchronized (sLock) { 358 if (sRating != null) { 359 notifyContentBlocked(sRating); 360 } 361 if (sTuneCallback != null) { 362 sTuneCallback.post(); 363 sTuneCallback = null; 364 } 365 if (sRating == null) { 366 notifyContentAllowed(); 367 } 368 } 369 notifyVideoAvailable(); 370 notifyTracksChanged(mTracks); 371 notifyTrackSelected(TvTrackInfo.TYPE_AUDIO, sEngAudioTrack.getId()); 372 notifyTrackSelected(TvTrackInfo.TYPE_SUBTITLE, null); 373 notifyTrackSelected(TvTrackInfo.TYPE_VIDEO, sDummyVideoTrack.getId()); 374 375 notifyTimeShiftStatusChanged(TvInputManager.TIME_SHIFT_STATUS_AVAILABLE); 376 mRecordStartTimeMs = mCurrentPositionMs = mLastCurrentPositionUpdateTimeMs 377 = System.currentTimeMillis(); 378 mPausedTimeMs = 0; 379 mHandler.sendEmptyMessageDelayed(MSG_SEEK, SEEK_DELAY_MS); 380 mSpeed = 1; 381 return true; 382 } 383 384 @Override onSelectTrack(int type, String trackId)385 public boolean onSelectTrack(int type, String trackId) { 386 synchronized (sLock) { 387 if (sSelectTrackCallback != null) { 388 Bundle bundle = sSelectTrackCallback.getBundle(); 389 if (bundle.getInt(SELECT_TRACK_TYPE) == type 390 && bundle.getString(SELECT_TRACK_ID).equals(trackId)) { 391 sSelectTrackCallback.post(); 392 sSelectTrackCallback = null; 393 } 394 } 395 } 396 notifyTrackSelected(type, trackId); 397 return true; 398 } 399 400 @Override onSetCaptionEnabled(boolean enabled)401 public void onSetCaptionEnabled(boolean enabled) { 402 synchronized (sLock) { 403 if (sSetCaptionEnabledCallback != null) { 404 Bundle bundle = sSetCaptionEnabledCallback.getBundle(); 405 if (bundle.getBoolean(CAPTION_ENABLED) == enabled) { 406 sSetCaptionEnabledCallback.post(); 407 sSetCaptionEnabledCallback = null; 408 } 409 } 410 } 411 } 412 413 @Override onUnblockContent(TvContentRating unblockedRating)414 public void onUnblockContent(TvContentRating unblockedRating) { 415 synchronized (sLock) { 416 if (sRating != null && sRating.equals(unblockedRating)) { 417 sUnblockContentCallback.post(); 418 sRating = null; 419 notifyContentAllowed(); 420 } 421 } 422 } 423 424 @Override onTimeShiftGetCurrentPosition()425 public long onTimeShiftGetCurrentPosition() { 426 synchronized (sLock) { 427 if (sPositionTrackingCallback != null) { 428 sPositionTrackingCallback.post(); 429 sPositionTrackingCallback = null; 430 } 431 } 432 Log.d(TAG, "currentPositionMs=" + mCurrentPositionMs); 433 return mCurrentPositionMs; 434 } 435 436 @Override onTimeShiftGetStartPosition()437 public long onTimeShiftGetStartPosition() { 438 return mRecordStartTimeMs; 439 } 440 441 @Override onTimeShiftPause()442 public void onTimeShiftPause() { 443 synchronized (sLock) { 444 if (sResumeAfterPauseCallback != null) { 445 sResumeAfterPauseCallback.mBundle.putBoolean(PAUSE_CALLED, true); 446 } 447 } 448 mCurrentPositionMs = mPausedTimeMs = mLastCurrentPositionUpdateTimeMs 449 = System.currentTimeMillis(); 450 } 451 452 @Override onTimeShiftResume()453 public void onTimeShiftResume() { 454 synchronized (sLock) { 455 if (sResumeAfterPauseCallback != null 456 && sResumeAfterPauseCallback.mBundle.getBoolean(PAUSE_CALLED)) { 457 sResumeAfterPauseCallback.post(); 458 sResumeAfterPauseCallback = null; 459 } 460 } 461 mSpeed = 1; 462 mPausedTimeMs = 0; 463 mLastCurrentPositionUpdateTimeMs = System.currentTimeMillis(); 464 } 465 466 @Override onTimeShiftSeekTo(long timeMs)467 public void onTimeShiftSeekTo(long timeMs) { 468 synchronized (sLock) { 469 if (mCurrentPositionMs > timeMs) { 470 if (sSeekToPreviousCallback != null) { 471 sSeekToPreviousCallback.post(); 472 sSeekToPreviousCallback = null; 473 } 474 } else if (mCurrentPositionMs < timeMs) { 475 if (sSeekToNextCallback != null) { 476 sSeekToNextCallback.post(); 477 sSeekToNextCallback = null; 478 } 479 } 480 } 481 mLastCurrentPositionUpdateTimeMs = System.currentTimeMillis(); 482 mCurrentPositionMs = Math.max(mRecordStartTimeMs, 483 Math.min(timeMs, mLastCurrentPositionUpdateTimeMs)); 484 } 485 486 @Override onTimeShiftSetPlaybackParams(PlaybackParams params)487 public void onTimeShiftSetPlaybackParams(PlaybackParams params) { 488 synchronized(sLock) { 489 if (params != null) { 490 if (params.getSpeed() > 1) { 491 if (sFastForwardCallback != null) { 492 sFastForwardCallback.post(); 493 sFastForwardCallback = null; 494 } 495 } else if (params.getSpeed() < 1) { 496 if (sRewindCallback != null) { 497 sRewindCallback.post(); 498 sRewindCallback = null; 499 } 500 } 501 } 502 } 503 mSpeed = params.getSpeed(); 504 } 505 506 @Override onOverlayViewSizeChanged(int width, int height)507 public void onOverlayViewSizeChanged(int width, int height) { 508 synchronized(sLock) { 509 draw(); 510 if (sOverlayViewSizeChangedCallback != null) { 511 if (sDummyVideoTrack.getVideoHeight() <= 0 512 || sDummyVideoTrack.getVideoWidth() <= 0) { 513 Log.w(TAG, 514 "The width or height of the selected video track is invalid."); 515 } else if (height <= 0 || width <= 0) { 516 Log.w(TAG, "The width or height of the OverlayView is incorrect."); 517 } else if (Math.abs((float)width / height 518 - (float)sDummyVideoTrack.getVideoWidth() 519 * sDummyVideoTrack.getVideoPixelAspectRatio() 520 / sDummyVideoTrack.getVideoHeight()) < DISPLAY_RATIO_EPSILON) { 521 // Verify the video display aspect ratio is correct 522 // and setVideoPixelAspectRatio() works for the view size. 523 sOverlayViewSizeChangedCallback.post(); 524 sOverlayViewSizeChangedCallback = null; 525 } 526 } 527 } 528 } 529 } 530 531 private static class Callback { 532 private final View mPostTarget; 533 private final Runnable mAction; 534 private final Bundle mBundle = new Bundle(); 535 Callback(View postTarget, Runnable action)536 Callback(View postTarget, Runnable action) { 537 mPostTarget = postTarget; 538 mAction = action; 539 } 540 post()541 public void post() { 542 mPostTarget.post(mAction); 543 } 544 getBundle()545 public Bundle getBundle() { 546 return mBundle; 547 } 548 } 549 } 550