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 android.media.tv.cts; 18 19 import android.app.Activity; 20 import android.app.Instrumentation; 21 import android.content.Context; 22 import android.database.Cursor; 23 import android.media.tv.TvContract; 24 import android.media.tv.TvInputInfo; 25 import android.media.tv.TvInputManager; 26 import android.media.tv.TvTrackInfo; 27 import android.media.tv.TvView; 28 import android.media.tv.TvView.TvInputCallback; 29 import android.net.Uri; 30 import android.os.Bundle; 31 import android.test.ActivityInstrumentationTestCase2; 32 import android.test.UiThreadTest; 33 import android.tv.cts.R; 34 import android.util.ArrayMap; 35 import android.util.SparseIntArray; 36 import android.view.InputEvent; 37 import android.view.KeyEvent; 38 39 import androidx.test.InstrumentationRegistry; 40 41 import com.android.compatibility.common.util.PollingCheck; 42 43 import org.junit.Ignore; 44 45 import java.util.ArrayList; 46 import java.util.Collections; 47 import java.util.List; 48 import java.util.Map; 49 50 /** 51 * Test {@link android.media.tv.TvView}. 52 */ 53 public class TvViewTest extends ActivityInstrumentationTestCase2<TvViewStubActivity> { 54 /** The maximum time to wait for an operation. */ 55 private static final long TIME_OUT_MS = 15000L; 56 57 private static final String PERMISSION_ACCESS_WATCHED_PROGRAMS = 58 "com.android.providers.tv.permission.ACCESS_WATCHED_PROGRAMS"; 59 private static final String PERMISSION_WRITE_EPG_DATA = 60 "com.android.providers.tv.permission.WRITE_EPG_DATA"; 61 62 private TvView mTvView; 63 private Activity mActivity; 64 private Instrumentation mInstrumentation; 65 private TvInputManager mManager; 66 private TvInputInfo mStubInfo; 67 private TvInputInfo mFaultyStubInfo; 68 private final MockCallback mCallback = new MockCallback(); 69 70 public static class MockCallback extends TvInputCallback { 71 private final Map<String, Boolean> mVideoAvailableMap = new ArrayMap<>(); 72 private final Map<String, SparseIntArray> mSelectedTrackGenerationMap = new ArrayMap<>(); 73 private final Map<String, Integer> mTracksGenerationMap = new ArrayMap<>(); 74 private final Object mLock = new Object(); 75 private volatile int mConnectionFailedCount; 76 private volatile int mDisconnectedCount; 77 isVideoAvailable(String inputId)78 public boolean isVideoAvailable(String inputId) { 79 synchronized (mLock) { 80 Boolean available = mVideoAvailableMap.get(inputId); 81 return available == null ? false : available.booleanValue(); 82 } 83 } 84 getSelectedTrackGeneration(String inputId, int type)85 public int getSelectedTrackGeneration(String inputId, int type) { 86 synchronized (mLock) { 87 SparseIntArray selectedTrackGenerationMap = 88 mSelectedTrackGenerationMap.get(inputId); 89 if (selectedTrackGenerationMap == null) { 90 return 0; 91 } 92 return selectedTrackGenerationMap.get(type, 0); 93 } 94 } 95 resetCount()96 public void resetCount() { 97 mConnectionFailedCount = 0; 98 mDisconnectedCount = 0; 99 } 100 getConnectionFailedCount()101 public int getConnectionFailedCount() { 102 return mConnectionFailedCount; 103 } 104 getDisconnectedCount()105 public int getDisconnectedCount() { 106 return mDisconnectedCount; 107 } 108 109 @Override onConnectionFailed(String inputId)110 public void onConnectionFailed(String inputId) { 111 mConnectionFailedCount++; 112 } 113 114 @Override onDisconnected(String inputId)115 public void onDisconnected(String inputId) { 116 mDisconnectedCount++; 117 } 118 119 @Override onVideoAvailable(String inputId)120 public void onVideoAvailable(String inputId) { 121 synchronized (mLock) { 122 mVideoAvailableMap.put(inputId, true); 123 } 124 } 125 126 @Override onVideoUnavailable(String inputId, int reason)127 public void onVideoUnavailable(String inputId, int reason) { 128 synchronized (mLock) { 129 mVideoAvailableMap.put(inputId, false); 130 } 131 } 132 133 @Override onTrackSelected(String inputId, int type, String trackId)134 public void onTrackSelected(String inputId, int type, String trackId) { 135 synchronized (mLock) { 136 SparseIntArray selectedTrackGenerationMap = 137 mSelectedTrackGenerationMap.get(inputId); 138 if (selectedTrackGenerationMap == null) { 139 selectedTrackGenerationMap = new SparseIntArray(); 140 mSelectedTrackGenerationMap.put(inputId, selectedTrackGenerationMap); 141 } 142 int currentGeneration = selectedTrackGenerationMap.get(type, 0); 143 selectedTrackGenerationMap.put(type, currentGeneration + 1); 144 } 145 } 146 147 @Override onTracksChanged(String inputId, List<TvTrackInfo> trackList)148 public void onTracksChanged(String inputId, List<TvTrackInfo> trackList) { 149 synchronized (mLock) { 150 Integer tracksGeneration = mTracksGenerationMap.get(inputId); 151 mTracksGenerationMap.put(inputId, 152 tracksGeneration == null ? 1 : (tracksGeneration + 1)); 153 } 154 } 155 } 156 157 /** 158 * Instantiates a new TV view test. 159 */ TvViewTest()160 public TvViewTest() { 161 super(TvViewStubActivity.class); 162 } 163 164 /** 165 * Find the TV view specified by id. 166 * 167 * @param id the id 168 * @return the TV view 169 */ findTvViewById(int id)170 private TvView findTvViewById(int id) { 171 return (TvView) mActivity.findViewById(id); 172 } 173 174 @Override setUp()175 protected void setUp() throws Exception { 176 super.setUp(); 177 mActivity = getActivity(); 178 if (!Utils.hasTvInputFramework(mActivity)) { 179 return; 180 } 181 182 InstrumentationRegistry 183 .getInstrumentation() 184 .getUiAutomation() 185 .adoptShellPermissionIdentity( 186 PERMISSION_ACCESS_WATCHED_PROGRAMS, PERMISSION_WRITE_EPG_DATA); 187 188 mInstrumentation = getInstrumentation(); 189 mTvView = findTvViewById(R.id.tvview); 190 mManager = (TvInputManager) mActivity.getSystemService(Context.TV_INPUT_SERVICE); 191 for (TvInputInfo info : mManager.getTvInputList()) { 192 if (info.getServiceInfo().name.equals(StubTunerTvInputService.class.getName())) { 193 mStubInfo = info; 194 } 195 if (info.getServiceInfo().name.equals(FaultyTvInputService.class.getName())) { 196 mFaultyStubInfo = info; 197 } 198 if (mStubInfo != null && mFaultyStubInfo != null) { 199 break; 200 } 201 } 202 assertNotNull(mStubInfo); 203 mTvView.setCallback(mCallback); 204 } 205 206 @Override tearDown()207 protected void tearDown() throws Exception { 208 if (!Utils.hasTvInputFramework(getActivity())) { 209 super.tearDown(); 210 return; 211 } 212 StubTunerTvInputService.deleteChannels(mActivity.getContentResolver(), mStubInfo); 213 StubTunerTvInputService.clearTracks(); 214 try { 215 runTestOnUiThread(new Runnable() { 216 @Override 217 public void run() { 218 mTvView.reset(); 219 } 220 }); 221 } catch (Throwable t) { 222 throw new RuntimeException(t); 223 } 224 mInstrumentation.waitForIdleSync(); 225 226 InstrumentationRegistry.getInstrumentation().getUiAutomation() 227 .dropShellPermissionIdentity(); 228 super.tearDown(); 229 } 230 231 @UiThreadTest testConstructor()232 public void testConstructor() throws Exception { 233 if (!Utils.hasTvInputFramework(getActivity())) { 234 return; 235 } 236 new TvView(mActivity); 237 238 new TvView(mActivity, null); 239 240 new TvView(mActivity, null, 0); 241 } 242 tryTuneAllChannels(Bundle params, Runnable runOnEachChannel)243 private void tryTuneAllChannels(Bundle params, Runnable runOnEachChannel) throws Throwable { 244 StubTunerTvInputService.insertChannels(mActivity.getContentResolver(), mStubInfo); 245 246 Uri uri = TvContract.buildChannelsUriForInput(mStubInfo.getId()); 247 String[] projection = { TvContract.Channels._ID }; 248 try (Cursor cursor = mActivity.getContentResolver().query( 249 uri, projection, null, null, null)) { 250 while (cursor != null && cursor.moveToNext()) { 251 long channelId = cursor.getLong(0); 252 Uri channelUri = TvContract.buildChannelUri(channelId); 253 if (params != null) { 254 mTvView.tune(mStubInfo.getId(), channelUri, params); 255 } else { 256 mTvView.tune(mStubInfo.getId(), channelUri); 257 } 258 mInstrumentation.waitForIdleSync(); 259 new PollingCheck(TIME_OUT_MS) { 260 @Override 261 protected boolean check() { 262 return mCallback.isVideoAvailable(mStubInfo.getId()); 263 } 264 }.run(); 265 266 if (runOnEachChannel != null) { 267 runOnEachChannel.run(); 268 } 269 } 270 } 271 } 272 testSimpleTune()273 public void testSimpleTune() throws Throwable { 274 if (!Utils.hasTvInputFramework(getActivity())) { 275 return; 276 } 277 tryTuneAllChannels(null, null); 278 } 279 testSimpleTuneWithBundle()280 public void testSimpleTuneWithBundle() throws Throwable { 281 if (!Utils.hasTvInputFramework(getActivity())) { 282 return; 283 } 284 Bundle params = new Bundle(); 285 params.putString("android.media.tv.cts.TvViewTest.inputId", mStubInfo.getId()); 286 tryTuneAllChannels(params, null); 287 } 288 selectTrackAndVerify(final int type, final TvTrackInfo track, List<TvTrackInfo> tracks)289 private void selectTrackAndVerify(final int type, final TvTrackInfo track, 290 List<TvTrackInfo> tracks) { 291 String selectedTrackId = mTvView.getSelectedTrack(type); 292 final int previousGeneration = mCallback.getSelectedTrackGeneration( 293 mStubInfo.getId(), type); 294 mTvView.selectTrack(type, track == null ? null : track.getId()); 295 296 if ((track == null && selectedTrackId != null) 297 || (track != null && !track.getId().equals(selectedTrackId))) { 298 // Check generation change only if we're actually changing track. 299 new PollingCheck(TIME_OUT_MS) { 300 @Override 301 protected boolean check() { 302 return mCallback.getSelectedTrackGeneration( 303 mStubInfo.getId(), type) > previousGeneration; 304 } 305 }.run(); 306 } 307 308 selectedTrackId = mTvView.getSelectedTrack(type); 309 assertEquals(selectedTrackId, track == null ? null : track.getId()); 310 if (selectedTrackId != null) { 311 TvTrackInfo selectedTrack = null; 312 for (TvTrackInfo item : tracks) { 313 if (item.getId().equals(selectedTrackId)) { 314 selectedTrack = item; 315 break; 316 } 317 } 318 assertNotNull(selectedTrack); 319 assertEquals(track.getType(), selectedTrack.getType()); 320 assertEquals(track.getExtra(), selectedTrack.getExtra()); 321 switch (track.getType()) { 322 case TvTrackInfo.TYPE_VIDEO: 323 assertEquals(track.getVideoHeight(), selectedTrack.getVideoHeight()); 324 assertEquals(track.getVideoWidth(), selectedTrack.getVideoWidth()); 325 assertEquals(track.getVideoPixelAspectRatio(), 326 selectedTrack.getVideoPixelAspectRatio(), 0.001f); 327 break; 328 case TvTrackInfo.TYPE_AUDIO: 329 assertEquals(track.getAudioChannelCount(), 330 selectedTrack.getAudioChannelCount()); 331 assertEquals(track.getAudioSampleRate(), selectedTrack.getAudioSampleRate()); 332 assertEquals(track.getLanguage(), selectedTrack.getLanguage()); 333 break; 334 case TvTrackInfo.TYPE_SUBTITLE: 335 assertEquals(track.getLanguage(), selectedTrack.getLanguage()); 336 assertEquals(track.getDescription(), selectedTrack.getDescription()); 337 break; 338 default: 339 fail("Unrecognized type: " + track.getType()); 340 } 341 } 342 } 343 testTrackChange()344 public void testTrackChange() throws Throwable { 345 if (!Utils.hasTvInputFramework(getActivity())) { 346 return; 347 } 348 TvTrackInfo videoTrack1 = new TvTrackInfo.Builder(TvTrackInfo.TYPE_VIDEO, "video-HD") 349 .setVideoHeight(1920).setVideoWidth(1080).build(); 350 TvTrackInfo videoTrack2 = new TvTrackInfo.Builder(TvTrackInfo.TYPE_VIDEO, "video-SD") 351 .setVideoHeight(640).setVideoWidth(360).setVideoPixelAspectRatio(1.09f).build(); 352 TvTrackInfo audioTrack1 = 353 new TvTrackInfo.Builder(TvTrackInfo.TYPE_AUDIO, "audio-stereo-eng") 354 .setLanguage("eng").setAudioChannelCount(2).setAudioSampleRate(48000).build(); 355 TvTrackInfo audioTrack2 = new TvTrackInfo.Builder(TvTrackInfo.TYPE_AUDIO, "audio-mono-esp") 356 .setLanguage("esp").setAudioChannelCount(1).setAudioSampleRate(48000).build(); 357 TvTrackInfo subtitleTrack1 = 358 new TvTrackInfo.Builder(TvTrackInfo.TYPE_SUBTITLE, "subtitle-eng") 359 .setLanguage("eng").build(); 360 TvTrackInfo subtitleTrack2 = 361 new TvTrackInfo.Builder(TvTrackInfo.TYPE_SUBTITLE, "subtitle-esp") 362 .setLanguage("esp").build(); 363 TvTrackInfo subtitleTrack3 = 364 new TvTrackInfo.Builder(TvTrackInfo.TYPE_SUBTITLE, "subtitle-eng2") 365 .setLanguage("eng").setDescription("audio commentary").build(); 366 367 StubTunerTvInputService.injectTrack(videoTrack1, videoTrack2, audioTrack1, audioTrack2, 368 subtitleTrack1, subtitleTrack2); 369 370 final List<TvTrackInfo> tracks = new ArrayList<TvTrackInfo>(); 371 Collections.addAll(tracks, videoTrack1, videoTrack2, audioTrack1, audioTrack2, 372 subtitleTrack1, subtitleTrack2, subtitleTrack3); 373 tryTuneAllChannels(null, new Runnable() { 374 @Override 375 public void run() { 376 new PollingCheck(TIME_OUT_MS) { 377 @Override 378 protected boolean check() { 379 return mTvView.getTracks(TvTrackInfo.TYPE_AUDIO) != null; 380 } 381 }.run(); 382 final int[] types = { TvTrackInfo.TYPE_AUDIO, TvTrackInfo.TYPE_VIDEO, 383 TvTrackInfo.TYPE_SUBTITLE }; 384 for (int type : types) { 385 for (TvTrackInfo track : mTvView.getTracks(type)) { 386 selectTrackAndVerify(type, track, tracks); 387 } 388 selectTrackAndVerify(TvTrackInfo.TYPE_SUBTITLE, null, tracks); 389 } 390 } 391 }); 392 } 393 verifyKeyEvent(final KeyEvent keyEvent, final InputEvent[] unhandledEvent)394 private void verifyKeyEvent(final KeyEvent keyEvent, final InputEvent[] unhandledEvent) { 395 unhandledEvent[0] = null; 396 mInstrumentation.sendKeySync(keyEvent); 397 mInstrumentation.waitForIdleSync(); 398 new PollingCheck(TIME_OUT_MS) { 399 @Override 400 protected boolean check() { 401 return unhandledEvent[0] != null; 402 } 403 }.run(); 404 assertTrue(unhandledEvent[0] instanceof KeyEvent); 405 KeyEvent unhandled = (KeyEvent) unhandledEvent[0]; 406 assertEquals(unhandled.getAction(), keyEvent.getAction()); 407 assertEquals(unhandled.getKeyCode(), keyEvent.getKeyCode()); 408 } 409 testOnUnhandledInputEventListener()410 public void testOnUnhandledInputEventListener() throws Throwable { 411 if (!Utils.hasTvInputFramework(getActivity())) { 412 return; 413 } 414 final InputEvent[] unhandledEvent = { null }; 415 mTvView.setOnUnhandledInputEventListener(new TvView.OnUnhandledInputEventListener() { 416 @Override 417 public boolean onUnhandledInputEvent(InputEvent event) { 418 unhandledEvent[0] = event; 419 return true; 420 } 421 }); 422 423 StubTunerTvInputService.insertChannels(mActivity.getContentResolver(), mStubInfo); 424 425 Uri uri = TvContract.buildChannelsUriForInput(mStubInfo.getId()); 426 String[] projection = { TvContract.Channels._ID }; 427 try (Cursor cursor = mActivity.getContentResolver().query( 428 uri, projection, null, null, null)) { 429 assertNotNull(cursor); 430 assertTrue(cursor.moveToNext()); 431 long channelId = cursor.getLong(0); 432 Uri channelUri = TvContract.buildChannelUri(channelId); 433 mTvView.tune(mStubInfo.getId(), channelUri); 434 mInstrumentation.waitForIdleSync(); 435 new PollingCheck(TIME_OUT_MS) { 436 @Override 437 protected boolean check() { 438 return mCallback.isVideoAvailable(mStubInfo.getId()); 439 } 440 }.run(); 441 } 442 runTestOnUiThread(new Runnable() { 443 @Override 444 public void run() { 445 mTvView.setFocusable(true); 446 mTvView.requestFocus(); 447 } 448 }); 449 mInstrumentation.waitForIdleSync(); 450 assertTrue(mTvView.isFocused()); 451 452 verifyKeyEvent( 453 new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_0), unhandledEvent); 454 verifyKeyEvent( 455 new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_0), unhandledEvent); 456 } 457 testConnectionFailed()458 public void testConnectionFailed() throws Throwable { 459 if (!Utils.hasTvInputFramework(getActivity())) { 460 return; 461 } 462 mCallback.resetCount(); 463 mTvView.tune("invalid_input_id", TvContract.Channels.CONTENT_URI); 464 mInstrumentation.waitForIdleSync(); 465 new PollingCheck(TIME_OUT_MS) { 466 @Override 467 protected boolean check() { 468 return mCallback.getConnectionFailedCount() > 0; 469 } 470 }.run(); 471 } 472 473 @Ignore("b/216866512") ignoredTestDisconnected()474 public void ignoredTestDisconnected() throws Throwable { 475 if (!Utils.hasTvInputFramework(getActivity())) { 476 return; 477 } 478 mCallback.resetCount(); 479 Uri fakeChannelUri = TvContract.buildChannelUri(0); 480 mTvView.tune(mFaultyStubInfo.getId(), fakeChannelUri); 481 new PollingCheck(TIME_OUT_MS) { 482 @Override 483 protected boolean check() { 484 return mCallback.getDisconnectedCount() > 0; 485 } 486 }.run(); 487 } 488 testSetZOrderMediaOverlay()489 public void testSetZOrderMediaOverlay() throws Exception { 490 if (!Utils.hasTvInputFramework(getActivity())) { 491 return; 492 } 493 // Verifying the z-order from app is not possible. Here we just check if calling APIs does 494 // not lead to any break. 495 mTvView.setZOrderMediaOverlay(true); 496 mInstrumentation.waitForIdleSync(); 497 mTvView.setZOrderMediaOverlay(false); 498 mInstrumentation.waitForIdleSync(); 499 } 500 testSetZOrderOnTop()501 public void testSetZOrderOnTop() throws Exception { 502 if (!Utils.hasTvInputFramework(getActivity())) { 503 return; 504 } 505 // Verifying the z-order from app is not possible. Here we just check if calling APIs does 506 // not lead to any break. 507 mTvView.setZOrderOnTop(true); 508 mInstrumentation.waitForIdleSync(); 509 mTvView.setZOrderOnTop(false); 510 mInstrumentation.waitForIdleSync(); 511 } 512 testGetAudioPresentations()513 public void testGetAudioPresentations() throws Exception { 514 if (!Utils.hasTvInputFramework(getActivity())) { 515 return; 516 } 517 518 StubTunerTvInputService.insertChannels(mActivity.getContentResolver(), mStubInfo); 519 520 Uri uri = TvContract.buildChannelsUriForInput(mStubInfo.getId()); 521 String[] projection = {TvContract.Channels._ID}; 522 try (Cursor cursor = 523 mActivity.getContentResolver().query(uri, projection, null, null, null)) { 524 assertNotNull(cursor); 525 assertTrue(cursor.moveToNext()); 526 long channelId = cursor.getLong(0); 527 Uri channelUri = TvContract.buildChannelUri(channelId); 528 mTvView.tune(mStubInfo.getId(), channelUri); 529 mInstrumentation.waitForIdleSync(); 530 new PollingCheck(TIME_OUT_MS) { 531 @Override 532 protected boolean check() { 533 return !mTvView.getAudioPresentations().isEmpty(); 534 } 535 }.run(); 536 } 537 assertTrue(mTvView.getAudioPresentations().size() == 1); 538 assertTrue(mTvView.getAudioPresentations().get(0).getPresentationId() 539 == StubTunerTvInputService.TEST_AUDIO_PRESENTATION.getPresentationId()); 540 assertTrue(mTvView.getAudioPresentations().get(0).getProgramId() 541 == StubTunerTvInputService.TEST_AUDIO_PRESENTATION.getProgramId()); 542 } 543 544 @UiThreadTest testUnhandledInputEvent()545 public void testUnhandledInputEvent() throws Exception { 546 if (!Utils.hasTvInputFramework(getActivity())) { 547 return; 548 } 549 boolean result = mTvView.dispatchUnhandledInputEvent(null); 550 assertFalse(result); 551 result = new InputEventHandlingTvView(mActivity).dispatchUnhandledInputEvent(null); 552 assertTrue(result); 553 } 554 555 private static class InputEventHandlingTvView extends TvView { InputEventHandlingTvView(Context context)556 public InputEventHandlingTvView(Context context) { 557 super(context); 558 } 559 560 @Override onUnhandledInputEvent(InputEvent event)561 public boolean onUnhandledInputEvent(InputEvent event) { 562 return true; 563 } 564 } 565 } 566