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