Add TvInputManagerTest & TvViewTest

Bug: 16409584
Change-Id: I64bdd35a76713fba212004b2e92e8f8ae92c3680
diff --git a/tests/tests/tv/Android.mk b/tests/tests/tv/Android.mk
index b45129d..5df50ca 100644
--- a/tests/tests/tv/Android.mk
+++ b/tests/tests/tv/Android.mk
@@ -24,7 +24,7 @@
 
 LOCAL_PACKAGE_NAME := CtsTvTestCases
 
-LOCAL_STATIC_JAVA_LIBRARIES := ctstestrunner
+LOCAL_STATIC_JAVA_LIBRARIES := ctsdeviceutil ctstestrunner
 
 LOCAL_SDK_VERSION := current
 
diff --git a/tests/tests/tv/AndroidManifest.xml b/tests/tests/tv/AndroidManifest.xml
index 5fc5aef..33c8e0b 100644
--- a/tests/tests/tv/AndroidManifest.xml
+++ b/tests/tests/tv/AndroidManifest.xml
@@ -17,9 +17,16 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="android.media.tv.cts">
+        package="com.android.cts.tv">
+
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
+
+    <uses-permission android:name="android.permission.READ_EPG_DATA" />
+    <uses-permission android:name="android.permission.WRITE_EPG_DATA" />
 
     <application>
+        <uses-library android:name="android.test.runner" />
+
         <activity android:name="android.media.tv.cts.TvInputSetupActivityStub">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -32,21 +39,43 @@
             </intent-filter>
         </activity>
 
-        <service android:name="android.media.tv.cts.StubTvInputService"
+        <service android:name="android.media.tv.cts.StubTunerTvInputService"
                  android:permission="android.permission.BIND_TV_INPUT"
                  android:label="TV input stub"
                  android:icon="@drawable/robot"
-                 android:process=":tvInputStub">
+                 android:process=":tunerTvInputStub">
             <intent-filter>
                 <action android:name="android.media.tv.TvInputService" />
             </intent-filter>
             <meta-data android:name="android.media.tv.input"
                        android:resource="@xml/stub_tv_input_service" />
         </service>
+
+        <service android:name="android.media.tv.cts.NoMetadataTvInputService"
+                 android:permission="android.permission.BIND_TV_INPUT">
+            <intent-filter>
+                <action android:name="android.media.tv.TvInputService" />
+            </intent-filter>
+        </service>
+
+        <service android:name="android.media.tv.cts.NoPermissionTvInputService">
+            <intent-filter>
+                <action android:name="android.media.tv.TvInputService" />
+            </intent-filter>
+            <meta-data android:name="android.media.tv.input"
+                       android:resource="@xml/stub_tv_input_service" />
+        </service>
+
+        <activity android:name="android.media.tv.cts.TvViewStubActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+            </intent-filter>
+        </activity>
     </application>
 
     <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
-            android:targetPackage="android.media.tv.cts"
+            android:targetPackage="com.android.cts.tv"
             android:label="Tests for the TV APIs.">
         <meta-data android:name="listener"
                 android:value="com.android.cts.runner.CtsTestRunListener" />
diff --git a/tests/tests/tv/res/layout/tvview_layout.xml b/tests/tests/tv/res/layout/tvview_layout.xml
new file mode 100644
index 0000000..a15d8ff
--- /dev/null
+++ b/tests/tests/tv/res/layout/tvview_layout.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2014 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <android.media.tv.TvView
+        android:id="@+id/tvview"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"/>
+</LinearLayout>
diff --git a/tests/tests/tv/src/android/media/tv/cts/NoMetadataTvInputService.java b/tests/tests/tv/src/android/media/tv/cts/NoMetadataTvInputService.java
new file mode 100644
index 0000000..2c5b6e0
--- /dev/null
+++ b/tests/tests/tv/src/android/media/tv/cts/NoMetadataTvInputService.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.tv.cts;
+
+public class NoMetadataTvInputService extends StubTvInputService {
+}
diff --git a/tests/tests/tv/src/android/media/tv/cts/NoPermissionTvInputService.java b/tests/tests/tv/src/android/media/tv/cts/NoPermissionTvInputService.java
new file mode 100644
index 0000000..1909e4b
--- /dev/null
+++ b/tests/tests/tv/src/android/media/tv/cts/NoPermissionTvInputService.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.tv.cts;
+
+public class NoPermissionTvInputService extends StubTvInputService {
+}
diff --git a/tests/tests/tv/src/android/media/tv/cts/StubTunerTvInputService.java b/tests/tests/tv/src/android/media/tv/cts/StubTunerTvInputService.java
new file mode 100644
index 0000000..22abbf5
--- /dev/null
+++ b/tests/tests/tv/src/android/media/tv/cts/StubTunerTvInputService.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.tv.cts;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.media.tv.TvContract;
+import android.media.tv.TvInputInfo;
+import android.media.tv.TvInputManager;
+import android.media.tv.TvInputService;
+import android.media.tv.TvTrackInfo;
+import android.net.Uri;
+import android.view.Surface;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class StubTunerTvInputService extends TvInputService {
+    public static void insertChannels(ContentResolver resolver, TvInputInfo info) {
+        if (!info.getServiceInfo().name.equals(StubTunerTvInputService.class.getName())) {
+            throw new IllegalArgumentException("info mismatch");
+        }
+        ContentValues redValues = new ContentValues();
+        redValues.put(TvContract.Channels.COLUMN_INPUT_ID, info.getId());
+        redValues.put(TvContract.Channels.COLUMN_DISPLAY_NUMBER, "0");
+        redValues.put(TvContract.Channels.COLUMN_DISPLAY_NAME, "Red");
+        redValues.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA, new byte[] { 0 });
+        ContentValues greenValues = new ContentValues();
+        greenValues.put(TvContract.Channels.COLUMN_INPUT_ID, info.getId());
+        greenValues.put(TvContract.Channels.COLUMN_DISPLAY_NUMBER, "1");
+        greenValues.put(TvContract.Channels.COLUMN_DISPLAY_NAME, "Green");
+        greenValues.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA, new byte[] { 1 });
+        ContentValues blueValues = new ContentValues();
+        blueValues.put(TvContract.Channels.COLUMN_INPUT_ID, info.getId());
+        blueValues.put(TvContract.Channels.COLUMN_DISPLAY_NUMBER, "2");
+        blueValues.put(TvContract.Channels.COLUMN_DISPLAY_NAME, "Blue");
+        blueValues.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA, new byte[] { 2 });
+        resolver.bulkInsert(TvContract.Channels.CONTENT_URI,
+                new ContentValues[] { redValues, greenValues, blueValues });
+    }
+
+    public static void deleteChannels(ContentResolver resolver, TvInputInfo info) {
+        if (!info.getServiceInfo().name.equals(StubTunerTvInputService.class.getName())) {
+            throw new IllegalArgumentException("info mismatch");
+        }
+        resolver.delete(TvContract.buildChannelsUriForInput(info.getId()), null, null);
+    }
+
+    @Override
+    public Session onCreateSession(String inputId) {
+        return new StubSessionImpl(this);
+    }
+
+    private static class StubSessionImpl extends Session {
+        private static final int[] COLORS = { Color.RED, Color.GREEN, Color.BLUE };
+        private Surface mSurface;
+        private Object mLock = new Object();
+        private int mCurrentIndex = -1;
+        private Context mContext;
+        private final List<TvTrackInfo> mTrackList = new ArrayList<>();
+        private final TvTrackInfo mVideoTrack1;
+        private final TvTrackInfo mVideoTrack2;
+        private final TvTrackInfo mAudioTrack1;
+        private final TvTrackInfo mAudioTrack2;
+        private final TvTrackInfo mSubtitleTrack1;
+        private final TvTrackInfo mSubtitleTrack2;
+
+        StubSessionImpl(Context context) {
+            super(context);
+            mContext = context;
+            mVideoTrack1 = new TvTrackInfo.Builder(TvTrackInfo.TYPE_VIDEO, "video-HD")
+                    .setVideoHeight(1920).setVideoHeight(1080).build();
+            mVideoTrack2 = new TvTrackInfo.Builder(TvTrackInfo.TYPE_VIDEO, "video-SD")
+                    .setVideoHeight(640).setVideoHeight(360).build();
+            mAudioTrack1 = new TvTrackInfo.Builder(TvTrackInfo.TYPE_AUDIO, "audio-stereo-eng")
+                    .setLanguage("eng").setAudioChannelCount(2).setAudioSampleRate(48000).build();
+            mAudioTrack2 = new TvTrackInfo.Builder(TvTrackInfo.TYPE_AUDIO, "audio-mono-esp")
+                    .setLanguage("esp").setAudioChannelCount(1).setAudioSampleRate(48000).build();
+            mSubtitleTrack1 = new TvTrackInfo.Builder(TvTrackInfo.TYPE_SUBTITLE, "subtitle-eng")
+                    .setLanguage("eng").build();
+            mSubtitleTrack2 = new TvTrackInfo.Builder(TvTrackInfo.TYPE_SUBTITLE, "subtitle-esp")
+                    .setLanguage("esp").build();
+            mTrackList.add(mVideoTrack1);
+            mTrackList.add(mVideoTrack2);
+            mTrackList.add(mAudioTrack1);
+            mTrackList.add(mAudioTrack2);
+            mTrackList.add(mSubtitleTrack1);
+            mTrackList.add(mSubtitleTrack2);
+        }
+
+        @Override
+        public void onRelease() {
+        }
+
+        private void updateSurfaceLocked() {
+            if (mCurrentIndex >= 0 && mSurface != null) {
+                Canvas c = mSurface.lockCanvas(null);
+                c.drawColor(COLORS[mCurrentIndex]);
+                mSurface.unlockCanvasAndPost(c);
+            }
+        }
+
+        @Override
+        public boolean onSetSurface(Surface surface) {
+            synchronized (mLock) {
+                mSurface = surface;
+                updateSurfaceLocked();
+                return true;
+            }
+        }
+
+        @Override
+        public void onSetStreamVolume(float volume) {
+        }
+
+        @Override
+        public boolean onTune(Uri channelUri) {
+            synchronized (mLock) {
+                notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING);
+                String[] projection = { TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA };
+                Cursor cursor = mContext.getContentResolver().query(
+                        channelUri, projection, null, null, null);
+                try {
+                    if (cursor != null && cursor.moveToNext()) {
+                        mCurrentIndex = cursor.getBlob(0)[0];
+                    } else {
+                        mCurrentIndex = -1;
+                    }
+                } finally {
+                    if (cursor != null) {
+                        cursor.close();
+                    }
+                }
+                updateSurfaceLocked();
+                // Notify tracks
+                if (mCurrentIndex == 0) {
+                    notifyTracksChanged(mTrackList);
+                    notifyTrackSelected(TvTrackInfo.TYPE_VIDEO, mVideoTrack1.getId());
+                    notifyTrackSelected(TvTrackInfo.TYPE_AUDIO, mAudioTrack1.getId());
+                    notifyTrackSelected(TvTrackInfo.TYPE_SUBTITLE, null);
+                }
+                notifyVideoAvailable();
+                return true;
+            }
+        }
+
+        @Override
+        public boolean onSelectTrack(int type, String trackId) {
+            notifyTrackSelected(type, trackId);
+            return true;
+        }
+
+        @Override
+        public void onSetCaptionEnabled(boolean enabled) {
+        }
+    }
+}
diff --git a/tests/tests/tv/src/android/media/tv/cts/TvInputInfoTest.java b/tests/tests/tv/src/android/media/tv/cts/TvInputInfoTest.java
index 02802a7..41011ec 100644
--- a/tests/tests/tv/src/android/media/tv/cts/TvInputInfoTest.java
+++ b/tests/tests/tv/src/android/media/tv/cts/TvInputInfoTest.java
@@ -38,7 +38,7 @@
                 (TvInputManager) mContext.getSystemService(Context.TV_INPUT_SERVICE);
         for (TvInputInfo info : manager.getTvInputList()) {
             if (info.getServiceInfo().name.equals(
-                    StubTvInputService.class.getName())) {
+                    StubTunerTvInputService.class.getName())) {
                 mStubInfo = info;
                 break;
             }
@@ -49,9 +49,8 @@
     public void testGetIntentForSettingsActivity() throws Exception {
         Intent intent = mStubInfo.getIntentForSettingsActivity();
 
-        assertEquals(intent.getComponent(), new ComponentName(
-                TvInputSettingsActivityStub.class.getPackage().getName(),
-                TvInputSettingsActivityStub.class.getName()));
+        assertEquals(intent.getComponent(), new ComponentName(mContext,
+                TvInputSettingsActivityStub.class));
         String inputId = intent.getStringExtra(TvInputInfo.EXTRA_INPUT_ID);
         assertEquals(mStubInfo.getId(), inputId);
     }
@@ -59,9 +58,8 @@
     public void testGetIntentForSetupActivity() throws Exception {
         Intent intent = mStubInfo.getIntentForSetupActivity();
 
-        assertEquals(intent.getComponent(), new ComponentName(
-                TvInputSetupActivityStub.class.getPackage().getName(),
-                TvInputSetupActivityStub.class.getName()));
+        assertEquals(intent.getComponent(), new ComponentName(mContext,
+                TvInputSetupActivityStub.class));
         String inputId = intent.getStringExtra(TvInputInfo.EXTRA_INPUT_ID);
         assertEquals(mStubInfo.getId(), inputId);
     }
diff --git a/tests/tests/tv/src/android/media/tv/cts/TvInputManagerTest.java b/tests/tests/tv/src/android/media/tv/cts/TvInputManagerTest.java
new file mode 100644
index 0000000..775ddbd
--- /dev/null
+++ b/tests/tests/tv/src/android/media/tv/cts/TvInputManagerTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.tv.cts;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.test.AndroidTestCase;
+import android.media.tv.TvInputInfo;
+import android.media.tv.TvInputManager;
+import android.media.tv.TvInputService;
+
+import java.util.List;
+
+/**
+ * Test for {@link android.media.tv.TvInputManager}.
+ */
+public class TvInputManagerTest extends AndroidTestCase {
+    private static final String[] VALID_TV_INPUT_SERVICES = {
+        StubTunerTvInputService.class.getName()
+    };
+    private static final String[] INVALID_TV_INPUT_SERVICES = {
+        NoMetadataTvInputService.class.getName(), NoPermissionTvInputService.class.getName()
+    };
+
+    private String mStubId;
+    private TvInputManager mManager;
+
+    private static TvInputInfo getInfoForClassName(List<TvInputInfo> list, String name) {
+        for (TvInputInfo info : list) {
+            if (info.getServiceInfo().name.equals(name)) {
+                return info;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public void setUp() throws Exception {
+        mManager = (TvInputManager) mContext.getSystemService(Context.TV_INPUT_SERVICE);
+        mStubId = getInfoForClassName(
+                mManager.getTvInputList(), StubTunerTvInputService.class.getName()).getId();
+    }
+
+    public void testGetInputState() throws Exception {
+        assertEquals(mManager.getInputState(mStubId), TvInputManager.INPUT_STATE_CONNECTED);
+    }
+
+    public void testGetTvInputInfo() throws Exception {
+        assertEquals(mManager.getTvInputInfo(mStubId), getInfoForClassName(
+                mManager.getTvInputList(), StubTunerTvInputService.class.getName()));
+    }
+
+    public void testGetTvInputList() throws Exception {
+        List<TvInputInfo> list = mManager.getTvInputList();
+        for (String name : VALID_TV_INPUT_SERVICES) {
+            assertNotNull("getTvInputList() doesn't contain valid input: " + name,
+                    getInfoForClassName(list, name));
+        }
+        for (String name : INVALID_TV_INPUT_SERVICES) {
+            assertNull("getTvInputList() contains invalind input: " + name,
+                    getInfoForClassName(list, name));
+        }
+    }
+}
diff --git a/tests/tests/tv/src/android/media/tv/cts/TvViewStubActivity.java b/tests/tests/tv/src/android/media/tv/cts/TvViewStubActivity.java
new file mode 100644
index 0000000..5035e14
--- /dev/null
+++ b/tests/tests/tv/src/android/media/tv/cts/TvViewStubActivity.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.tv.cts;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+import com.android.cts.tv.R;
+
+public class TvViewStubActivity extends Activity {
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.tvview_layout);
+    }
+}
diff --git a/tests/tests/tv/src/android/media/tv/cts/TvViewTest.java b/tests/tests/tv/src/android/media/tv/cts/TvViewTest.java
new file mode 100644
index 0000000..c4e8228
--- /dev/null
+++ b/tests/tests/tv/src/android/media/tv/cts/TvViewTest.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.tv.cts;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.content.Context;
+import android.cts.util.PollingCheck;
+import android.database.Cursor;
+import android.test.ActivityInstrumentationTestCase2;
+import android.media.tv.TvContentRating;
+import android.media.tv.TvContract;
+import android.media.tv.TvInputInfo;
+import android.media.tv.TvInputManager;
+import android.media.tv.TvTrackInfo;
+import android.media.tv.TvView;
+import android.net.Uri;
+import android.util.ArrayMap;
+import android.util.SparseIntArray;
+
+import com.android.cts.tv.R;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Test {@link TvView}.
+ */
+public class TvViewTest extends ActivityInstrumentationTestCase2<TvViewStubActivity> {
+    /** The maximum time to wait for an operation. */
+    private static final long TIME_OUT = 15000L;
+
+    private TvView mTvView;
+    private Activity mActivity;
+    private Instrumentation mInstrumentation;
+    private TvInputManager mManager;
+    private TvInputInfo mStubInfo;
+    private final MockListener mListener = new MockListener();
+
+    private static class MockListener extends TvView.TvInputListener {
+        private final Map<String, Boolean> mVideoAvailableMap = new ArrayMap<>();
+        private final Map<String, SparseIntArray> mSelectedTrackGenerationMap = new ArrayMap<>();
+        private final Map<String, Integer> mTracksGenerationMap = new ArrayMap<>();
+        private Object mLock = new Object();
+
+        public boolean isVideoAvailable(String inputId) {
+            synchronized (mLock) {
+                Boolean available = mVideoAvailableMap.get(inputId);
+                return available == null ? true : available.booleanValue();
+            }
+        }
+
+        public int getSelectedTrackGeneration(String inputId, int type) {
+            synchronized (mLock) {
+                SparseIntArray selectedTrackGenerationMap =
+                        mSelectedTrackGenerationMap.get(inputId);
+                if (selectedTrackGenerationMap == null) {
+                    return 0;
+                }
+                return selectedTrackGenerationMap.get(type, 0);
+            }
+        }
+
+        public int getTrackGeneration(String inputId) {
+            synchronized (mLock) {
+                Integer tracksGeneration = mTracksGenerationMap.get(inputId);
+                return tracksGeneration == null ? 0 : tracksGeneration.intValue();
+            }
+        }
+
+        @Override
+        public void onVideoAvailable(String inputId) {
+            synchronized (mLock) {
+                mVideoAvailableMap.put(inputId, true);
+            }
+        }
+
+        @Override
+        public void onVideoUnavailable(String inputId, int reason) {
+            synchronized (mLock) {
+                mVideoAvailableMap.put(inputId, false);
+            }
+        }
+
+        @Override
+        public void onTrackSelected(String inputId, int type, String trackId) {
+            synchronized (mLock) {
+                SparseIntArray selectedTrackGenerationMap =
+                        mSelectedTrackGenerationMap.get(inputId);
+                if (selectedTrackGenerationMap == null) {
+                    selectedTrackGenerationMap = new SparseIntArray();
+                    mSelectedTrackGenerationMap.put(inputId, selectedTrackGenerationMap);
+                }
+                int currentGeneration = selectedTrackGenerationMap.get(type, 0);
+                selectedTrackGenerationMap.put(type, currentGeneration + 1);
+            }
+        }
+
+        @Override
+        public void onTracksChanged(String inputId, List<TvTrackInfo> trackList) {
+            synchronized (mLock) {
+                Integer tracksGeneration = mTracksGenerationMap.get(inputId);
+                mTracksGenerationMap.put(inputId,
+                        tracksGeneration == null ? 1 : (tracksGeneration + 1));
+            }
+        }
+    }
+
+    /**
+     * Instantiates a new TV view test.
+     */
+    public TvViewTest() {
+        super(TvViewStubActivity.class);
+    }
+
+    /**
+     * Find the TV view specified by id.
+     *
+     * @param id the id
+     * @return the TV view
+     */
+    private TvView findTvViewById(int id) {
+        return (TvView) mActivity.findViewById(id);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mActivity = getActivity();
+        mInstrumentation = getInstrumentation();
+        mTvView = findTvViewById(R.id.tvview);
+        mManager = (TvInputManager) mActivity.getSystemService(Context.TV_INPUT_SERVICE);
+        for (TvInputInfo info : mManager.getTvInputList()) {
+            if (info.getServiceInfo().name.equals(StubTunerTvInputService.class.getName())) {
+                mStubInfo = info;
+                break;
+            }
+        }
+        assertNotNull(mStubInfo);
+        mTvView.setTvInputListener(mListener);
+    }
+
+    public void testConstructor() throws Exception {
+        new TvView(mActivity);
+
+        new TvView(mActivity, null);
+
+        new TvView(mActivity, null, 0);
+    }
+
+    private void tryTuneAllChannels(Runnable runOnEachChannel) throws Throwable {
+        StubTunerTvInputService.insertChannels(mActivity.getContentResolver(), mStubInfo);
+
+        Uri uri = TvContract.buildChannelsUriForInput(mStubInfo.getId());
+        String[] projection = { TvContract.Channels._ID };
+        Cursor cursor = mActivity.getContentResolver().query(
+                uri, projection, null, null, null);
+        try {
+            while (cursor != null && cursor.moveToNext()) {
+                long channelId = cursor.getLong(0);
+                Uri channelUri = TvContract.buildChannelUri(channelId);
+                mTvView.tune(mStubInfo.getId(), channelUri);
+                mInstrumentation.waitForIdleSync();
+                new PollingCheck(TIME_OUT) {
+                    @Override
+                    protected boolean check() {
+                        return mListener.isVideoAvailable(mStubInfo.getId());
+                    }
+                }.run();
+
+                if (runOnEachChannel != null) {
+                    runOnEachChannel.run();
+                }
+            }
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+        runTestOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTvView.reset();
+            }
+        });
+        mInstrumentation.waitForIdleSync();
+
+        StubTunerTvInputService.deleteChannels(mActivity.getContentResolver(), mStubInfo);
+    }
+
+    public void testSimpleTune() throws Throwable {
+        tryTuneAllChannels(null);
+    }
+
+    private void selectTrackAndVerify(final int type, final TvTrackInfo track) {
+        final int previousGeneration = mListener.getSelectedTrackGeneration(
+                mStubInfo.getId(), type);
+        mTvView.selectTrack(type, track == null ? null : track.getId());
+        new PollingCheck(TIME_OUT) {
+            @Override
+            protected boolean check() {
+                return mListener.getSelectedTrackGeneration(
+                        mStubInfo.getId(), type) > previousGeneration;
+            }
+        }.run();
+        assertEquals(mTvView.getSelectedTrack(type), track == null ? null : track.getId());
+    }
+
+    public void testTrackChange() throws Throwable {
+        tryTuneAllChannels(new Runnable() {
+            @Override
+            public void run() {
+                new PollingCheck(TIME_OUT) {
+                    @Override
+                    protected boolean check() {
+                        return mTvView.getTracks(TvTrackInfo.TYPE_AUDIO) != null;
+                    }
+                }.run();
+                final int[] types = { TvTrackInfo.TYPE_AUDIO, TvTrackInfo.TYPE_VIDEO,
+                    TvTrackInfo.TYPE_SUBTITLE };
+                for (int type : types) {
+                    final int typeF = type;
+                    for (TvTrackInfo track : mTvView.getTracks(type)) {
+                        selectTrackAndVerify(type, track);
+                    }
+                    selectTrackAndVerify(TvTrackInfo.TYPE_SUBTITLE, null);
+                }
+            }
+        });
+    }
+}