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