Add CTS Verifier for the time shifting API in TIF

Bug: 21640152
Change-Id: Ibdb1ba37413722de29e0332764fbe750447575d6
diff --git a/apps/CtsVerifier/AndroidManifest.xml b/apps/CtsVerifier/AndroidManifest.xml
index a914e7c..ac9466d 100644
--- a/apps/CtsVerifier/AndroidManifest.xml
+++ b/apps/CtsVerifier/AndroidManifest.xml
@@ -1449,6 +1449,17 @@
                     android:value="android.software.live_tv" />
         </activity>
 
+        <activity android:name=".tv.TimeShiftTestActivity"
+                android:label="@string/tv_time_shift_test">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.cts.intent.category.MANUAL_TEST" />
+            </intent-filter>
+            <meta-data android:name="test_category" android:value="@string/test_category_tv" />
+            <meta-data android:name="test_required_features"
+                    android:value="android.software.live_tv" />
+        </activity>
+
         <activity android:name=".screenpinning.ScreenPinningTestActivity"
             android:label="@string/screen_pinning_test">
             <intent-filter>
@@ -1460,12 +1471,6 @@
                        android:value="android.hardware.type.television:android.software.leanback:android.hardware.type.watch" />
         </activity>
 
-        <activity android:name=".tv.MockTvInputSettingsActivity">
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-            </intent-filter>
-        </activity>
-
         <activity android:name=".tv.MockTvInputSetupActivity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
diff --git a/apps/CtsVerifier/res/values/strings.xml b/apps/CtsVerifier/res/values/strings.xml
index 926a993..ad41891 100644
--- a/apps/CtsVerifier/res/values/strings.xml
+++ b/apps/CtsVerifier/res/values/strings.xml
@@ -1616,6 +1616,41 @@
     The Spanish audio track should be selected.
     </string>
 
+    <string name="tv_time_shift_test">TV app time shift test</string>
+    <string name="tv_time_shift_test_info">
+    This test verifies that the TV app invokes proper time shift APIs in the framwork.
+    </string>
+    <string name="tv_time_shift_test_pause_resume">
+    Press the \"Launch TV app\" button. Verify that the playback control is available.
+    Pause the playback and then resume it.
+    </string>
+    <string name="tv_time_shift_test_verify_resume_after_pause">
+    The playback should be resumed after pause.
+    </string>
+    <string name="tv_time_shift_test_verify_position_tracking">
+    The playback position tracking should be activated.
+    </string>
+    <string name="tv_time_shift_test_speed_rate">
+    Press the \"Launch TV app\" button. Verify that the playback control is available.
+    Rewind the playback and in a few seconds fast-forward it.
+    </string>
+    <string name="tv_time_shift_test_verify_rewind">
+    The playback should be rewinded.
+    </string>
+    <string name="tv_time_shift_test_verify_fast_forward">
+    The playback should be fast-forwarded.
+    </string>
+    <string name="tv_time_shift_test_seek">
+    Press the \"Launch TV app\" button. Verify that the playback control is available.
+    Seek to previous and then seek to next.
+    </string>
+    <string name="tv_time_shift_test_verify_seek_to_previous">
+    The playback position should be moved to the previous position.
+    </string>
+    <string name="tv_time_shift_test_verify_seek_to_next">
+    The playback position should be moved to the next position.
+    </string>
+
     <string name="overlay_view_text">Overlay View Dummy Text</string>
     <string name="fake_rating">Fake</string>
 
diff --git a/apps/CtsVerifier/res/xml/mock_tv_input_service.xml b/apps/CtsVerifier/res/xml/mock_tv_input_service.xml
index 1a2cf86..d9cb867 100644
--- a/apps/CtsVerifier/res/xml/mock_tv_input_service.xml
+++ b/apps/CtsVerifier/res/xml/mock_tv_input_service.xml
@@ -15,5 +15,4 @@
 -->
 
 <tv-input xmlns:android="http://schemas.android.com/apk/res/android"
-    android:setupActivity="com.android.cts.verifier.tv.MockTvInputSetupActivity"
-    android:settingsActivity="com.android.cts.verifier.tv.MockTvInputSettingsActivity" />
+    android:setupActivity="com.android.cts.verifier.tv.MockTvInputSetupActivity" />
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/tv/MockTvInputService.java b/apps/CtsVerifier/src/com/android/cts/verifier/tv/MockTvInputService.java
index 9f4ac2d..f875684 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/tv/MockTvInputService.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/tv/MockTvInputService.java
@@ -16,8 +16,7 @@
 
 package com.android.cts.verifier.tv;
 
-import com.android.cts.verifier.R;
-
+import android.annotation.SuppressLint;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
@@ -28,6 +27,7 @@
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Rect;
+import android.media.PlaybackParams;
 import android.media.tv.TvContentRating;
 import android.media.tv.TvContract;
 import android.media.tv.TvInputManager;
@@ -35,14 +35,21 @@
 import android.media.tv.TvTrackInfo;
 import android.net.Uri;
 import android.os.Bundle;
-import android.view.Surface;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
 import android.view.LayoutInflater;
+import android.view.Surface;
 import android.view.View;
 import android.widget.TextView;
 
+import com.android.cts.verifier.R;
+
 import java.util.ArrayList;
 import java.util.List;
 
+@SuppressLint("NewApi")
 public class MockTvInputService extends TvInputService {
     private static final String TAG = "MockTvInputService";
 
@@ -50,6 +57,7 @@
     private static final String SELECT_TRACK_TYPE = "type";
     private static final String SELECT_TRACK_ID = "id";
     private static final String CAPTION_ENABLED = "enabled";
+    private static final String PAUSE_CALLED = "pause_called";
 
     private static Object sLock = new Object();
     private static Callback sTuneCallback = null;
@@ -58,6 +66,14 @@
     private static Callback sUnblockContentCallback = null;
     private static Callback sSelectTrackCallback = null;
     private static Callback sSetCaptionEnabledCallback = null;
+    // Callbacks for time shift.
+    private static Callback sResumeAfterPauseCallback = null;
+    private static Callback sPositionTrackingCallback = null;
+    private static Callback sRewindCallback = null;
+    private static Callback sFastForwardCallback = null;
+    private static Callback sSeekToPreviousCallback = null;
+    private static Callback sSeekToNextCallback = null;
+
     private static TvContentRating sRating = null;
 
     static final TvTrackInfo sEngAudioTrack =
@@ -144,6 +160,42 @@
         }
     }
 
+    static void expectResumeAfterPause(View postTarget, Runnable successCallback) {
+        synchronized (sLock) {
+            sResumeAfterPauseCallback = new Callback(postTarget, successCallback);
+        }
+    }
+
+    static void expectPositionTracking(View postTarget, Runnable successCallback) {
+        synchronized (sLock) {
+            sPositionTrackingCallback = new Callback(postTarget, successCallback);
+        }
+    }
+
+    static void expectRewind(View postTarget, Runnable successCallback) {
+        synchronized (sLock) {
+            sRewindCallback = new Callback(postTarget, successCallback);
+        }
+    }
+
+    static void expectFastForward(View postTarget, Runnable successCallback) {
+        synchronized (sLock) {
+            sFastForwardCallback = new Callback(postTarget, successCallback);
+        }
+    }
+
+    static void expectSeekToPrevious(View postTarget, Runnable successCallback) {
+        synchronized (sLock) {
+            sSeekToPreviousCallback = new Callback(postTarget, successCallback);
+        }
+    }
+
+    static void expectSeekToNext(View postTarget, Runnable successCallback) {
+        synchronized (sLock) {
+            sSeekToNextCallback = new Callback(postTarget, successCallback);
+        }
+    }
+
     static String getInputId(Context context) {
         return TvContract.buildInputId(new ComponentName(context,
                         MockTvInputService.class.getName()));
@@ -172,10 +224,45 @@
     }
 
     private static class MockSessionImpl extends Session {
+        private static final int MSG_SEEK = 1000;
+        private static final int SEEK_DELAY_MS = 300;
+
         private final Context mContext;
         private Surface mSurface = null;
         private List<TvTrackInfo> mTracks = new ArrayList<>();
 
+        private long mRecordStartTimeMs;
+        private long mPausedTimeMs;
+        // The time in milliseconds when the current position is lastly updated.
+        private long mLastCurrentPositionUpdateTimeMs;
+        // The current playback position.
+        private long mCurrentPositionMs;
+        // The current playback speed rate.
+        private float mSpeed;
+
+        private final Handler mHandler = new Handler(Looper.getMainLooper()) {
+            @Override
+            public void handleMessage(Message msg) {
+                if (msg.what == MSG_SEEK) {
+                    // Actually, this input doesn't play any videos, it just shows the image.
+                    // So we should simulate the playback here by changing the current playback
+                    // position periodically in order to test the time shift.
+                    // If the playback is paused, the current playback position doesn't need to be
+                    // changed.
+                    if (mPausedTimeMs == 0) {
+                        long currentTimeMs = System.currentTimeMillis();
+                        mCurrentPositionMs += (long) ((currentTimeMs
+                                - mLastCurrentPositionUpdateTimeMs) * mSpeed);
+                        mCurrentPositionMs = Math.max(mRecordStartTimeMs,
+                                Math.min(mCurrentPositionMs, currentTimeMs));
+                        mLastCurrentPositionUpdateTimeMs = currentTimeMs;
+                    }
+                    sendEmptyMessageDelayed(MSG_SEEK, SEEK_DELAY_MS);
+                }
+                super.handleMessage(msg);
+            }
+        };
+
         private MockSessionImpl(Context context) {
             super(context);
             mContext = context;
@@ -263,6 +350,12 @@
             notifyTracksChanged(mTracks);
             notifyTrackSelected(TvTrackInfo.TYPE_AUDIO, sEngAudioTrack.getId());
             notifyTrackSelected(TvTrackInfo.TYPE_SUBTITLE, null);
+            notifyTimeShiftStatusChanged(TvInputManager.TIME_SHIFT_STATUS_AVAILABLE);
+            mRecordStartTimeMs = mCurrentPositionMs = mLastCurrentPositionUpdateTimeMs
+                    = System.currentTimeMillis();
+            mPausedTimeMs = 0;
+            mHandler.sendEmptyMessageDelayed(MSG_SEEK, SEEK_DELAY_MS);
+            mSpeed = 1;
             return true;
         }
 
@@ -305,6 +398,88 @@
                 }
             }
         }
+
+        @Override
+        public long onTimeShiftGetCurrentPosition() {
+            synchronized (sLock) {
+                if (sPositionTrackingCallback != null) {
+                    sPositionTrackingCallback.post();
+                    sPositionTrackingCallback = null;
+                }
+            }
+            Log.d(TAG, "currentPositionMs=" + mCurrentPositionMs);
+            return mCurrentPositionMs;
+        }
+
+        @Override
+        public long onTimeShiftGetStartPosition() {
+            return mRecordStartTimeMs;
+        }
+
+        @Override
+        public void onTimeShiftPause() {
+            synchronized (sLock) {
+                if (sResumeAfterPauseCallback != null) {
+                    sResumeAfterPauseCallback.mBundle.putBoolean(PAUSE_CALLED, true);
+                }
+            }
+            mCurrentPositionMs = mPausedTimeMs = mLastCurrentPositionUpdateTimeMs
+                    = System.currentTimeMillis();
+        }
+
+        @Override
+        public void onTimeShiftResume() {
+            synchronized (sLock) {
+                if (sResumeAfterPauseCallback != null
+                        && sResumeAfterPauseCallback.mBundle.getBoolean(PAUSE_CALLED)) {
+                    sResumeAfterPauseCallback.post();
+                    sResumeAfterPauseCallback = null;
+                }
+            }
+            mSpeed = 1;
+            mPausedTimeMs = 0;
+            mLastCurrentPositionUpdateTimeMs = System.currentTimeMillis();
+        }
+
+        @Override
+        public void onTimeShiftSeekTo(long timeMs) {
+            synchronized (sLock) {
+                if (mCurrentPositionMs > timeMs) {
+                    if (sSeekToPreviousCallback != null) {
+                        sSeekToPreviousCallback.post();
+                        sSeekToPreviousCallback = null;
+                    }
+                } else if (mCurrentPositionMs < timeMs) {
+                    if (sSeekToNextCallback != null) {
+                        sSeekToNextCallback.post();
+                        sSeekToNextCallback = null;
+                    }
+                }
+            }
+            mLastCurrentPositionUpdateTimeMs = System.currentTimeMillis();
+            mCurrentPositionMs = Math.max(mRecordStartTimeMs,
+                    Math.min(timeMs, mLastCurrentPositionUpdateTimeMs));
+        }
+
+        @Override
+        public void onTimeShiftSetPlaybackParams(PlaybackParams params) {
+            synchronized(sLock) {
+                if (params != null) {
+                    if (params.getSpeed() > 1) {
+                        if (sFastForwardCallback != null) {
+                            sFastForwardCallback.post();
+                            sFastForwardCallback = null;
+                        }
+                    } else if (params.getSpeed() < 1) {
+                        if (sRewindCallback != null) {
+                            sRewindCallback.post();
+                            sRewindCallback = null;
+                        }
+                    }
+                }
+            }
+            mSpeed = params.getSpeed();
+        }
     }
 
     private static class Callback {
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/tv/MockTvInputSettingsActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/tv/MockTvInputSettingsActivity.java
deleted file mode 100644
index 4231db7..0000000
--- a/apps/CtsVerifier/src/com/android/cts/verifier/tv/MockTvInputSettingsActivity.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * 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 com.android.cts.verifier.tv;
-
-import android.preference.PreferenceActivity;
-
-public class MockTvInputSettingsActivity extends PreferenceActivity {
-
-}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/tv/TimeShiftTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/tv/TimeShiftTestActivity.java
new file mode 100644
index 0000000..5e4036c
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/tv/TimeShiftTestActivity.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2015 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 com.android.cts.verifier.tv;
+
+import android.content.Intent;
+import android.database.Cursor;
+import android.media.tv.TvContract;
+import android.view.View;
+import android.widget.Toast;
+
+import com.android.cts.verifier.R;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests for verifying TV app behavior on time shift.
+ */
+public class TimeShiftTestActivity extends TvAppVerifierActivity
+        implements View.OnClickListener {
+    private static final long TIMEOUT_MS = TimeUnit.MINUTES.toMillis(5);
+    private static final boolean NOT_PASSED = false;
+    private static final boolean PASSED = true;
+
+    private View mPauseResumeItem;
+    private View mVerifyResumeAfterPauseItem;
+    private View mVerifyPositionTrackingItem;
+
+    private View mSetPlaybackParamsItem;
+    private View mVerifyRewindItem;
+    private View mVerifyFastForwardItem;
+
+    private View mSeekToItem;
+    private View mVerifySeekToPreviousItem;
+    private View mVerifySeekToNextItem;
+
+    private Intent mTvAppIntent = null;
+
+    @Override
+    public void onClick(View v) {
+        final View postTarget = getPostTarget();
+
+        if (containsButton(mPauseResumeItem, v)) {
+            mVerifyResumeAfterPauseItem.setTag(NOT_PASSED);
+            mVerifyPositionTrackingItem.setTag(NOT_PASSED);
+
+            final Runnable failCallback = new Runnable() {
+                @Override
+                public void run() {
+                    setPassState(mVerifyResumeAfterPauseItem, false);
+                    setPassState(mVerifyPositionTrackingItem, false);
+                }
+            };
+            postTarget.postDelayed(failCallback, TIMEOUT_MS);
+            MockTvInputService.expectResumeAfterPause(postTarget, new Runnable() {
+                @Override
+                public void run() {
+                    postTarget.removeCallbacks(failCallback);
+                    setPassState(mPauseResumeItem, true);
+                    setPassState(mVerifyResumeAfterPauseItem, true);
+                    mVerifyResumeAfterPauseItem.setTag(PASSED);
+                    if (isPassedState(mVerifyResumeAfterPauseItem)
+                            && isPassedState(mVerifyPositionTrackingItem)) {
+                        setButtonEnabled(mSetPlaybackParamsItem, true);
+                    }
+                }
+            });
+            MockTvInputService.expectPositionTracking(postTarget, new Runnable() {
+                @Override
+                public void run() {
+                    postTarget.removeCallbacks(failCallback);
+                    setPassState(mPauseResumeItem, true);
+                    setPassState(mVerifyPositionTrackingItem, true);
+                    mVerifyPositionTrackingItem.setTag(PASSED);
+                    if (isPassedState(mVerifyResumeAfterPauseItem)
+                            && isPassedState(mVerifyPositionTrackingItem)) {
+                        setButtonEnabled(mSetPlaybackParamsItem, true);
+                    }
+                }
+            });
+        } else if (containsButton(mSetPlaybackParamsItem, v)) {
+            mVerifyRewindItem.setTag(NOT_PASSED);
+            mVerifyFastForwardItem.setTag(NOT_PASSED);
+
+            final Runnable failCallback = new Runnable() {
+                @Override
+                public void run() {
+                    setPassState(mVerifyRewindItem, false);
+                    setPassState(mVerifyFastForwardItem, false);
+                }
+            };
+            postTarget.postDelayed(failCallback, TIMEOUT_MS);
+            MockTvInputService.expectRewind(postTarget, new Runnable() {
+                @Override
+                public void run() {
+                    postTarget.removeCallbacks(failCallback);
+                    setPassState(mSetPlaybackParamsItem, true);
+                    setPassState(mVerifyRewindItem, true);
+                    mVerifyRewindItem.setTag(PASSED);
+                    if (isPassedState(mVerifyRewindItem) && isPassedState(mVerifyFastForwardItem)) {
+                        setButtonEnabled(mSeekToItem, true);
+                    }
+                }
+            });
+            MockTvInputService.expectFastForward(postTarget, new Runnable() {
+                @Override
+                public void run() {
+                    postTarget.removeCallbacks(failCallback);
+                    setPassState(mSetPlaybackParamsItem, true);
+                    setPassState(mVerifyFastForwardItem, true);
+                    mVerifyFastForwardItem.setTag(PASSED);
+                    if (isPassedState(mVerifyRewindItem) && isPassedState(mVerifyFastForwardItem)) {
+                        setButtonEnabled(mSeekToItem, true);
+                    }
+                }
+            });
+        } else if (containsButton(mSeekToItem, v)) {
+            mVerifySeekToPreviousItem.setTag(NOT_PASSED);
+            mVerifySeekToNextItem.setTag(NOT_PASSED);
+
+            final Runnable failCallback = new Runnable() {
+                @Override
+                public void run() {
+                    setPassState(mVerifySeekToPreviousItem, false);
+                    setPassState(mVerifySeekToNextItem, false);
+                }
+            };
+            postTarget.postDelayed(failCallback, TIMEOUT_MS);
+            MockTvInputService.expectSeekToPrevious(postTarget, new Runnable() {
+                @Override
+                public void run() {
+                    postTarget.removeCallbacks(failCallback);
+                    setPassState(mSeekToItem, true);
+                    setPassState(mVerifySeekToPreviousItem, true);
+                    mVerifySeekToPreviousItem.setTag(PASSED);
+                    if (isPassedState(mVerifySeekToPreviousItem)
+                            && isPassedState(mVerifySeekToNextItem)) {
+                        getPassButton().setEnabled(true);
+                    }
+                }
+            });
+            MockTvInputService.expectSeekToNext(postTarget, new Runnable() {
+                @Override
+                public void run() {
+                    postTarget.removeCallbacks(failCallback);
+                    setPassState(mSeekToItem, true);
+                    setPassState(mVerifySeekToNextItem, true);
+                    mVerifySeekToNextItem.setTag(PASSED);
+                    if (isPassedState(mVerifySeekToPreviousItem)
+                            && isPassedState(mVerifySeekToNextItem)) {
+                        getPassButton().setEnabled(true);
+                    }
+                }
+            });
+        }
+        if (mTvAppIntent == null) {
+            String[] projection = { TvContract.Channels._ID };
+            try (Cursor cursor = getContentResolver().query(
+                    TvContract.buildChannelsUriForInput(MockTvInputService.getInputId(this)),
+                    projection, null, null, null)) {
+                if (cursor != null && cursor.moveToNext()) {
+                    mTvAppIntent = new Intent(Intent.ACTION_VIEW,
+                            TvContract.buildChannelUri(cursor.getLong(0)));
+                }
+            }
+            if (mTvAppIntent == null) {
+                Toast.makeText(this, R.string.tv_channel_not_found, Toast.LENGTH_SHORT).show();
+                return;
+            }
+        }
+        startActivity(mTvAppIntent);
+    }
+
+    @Override
+    protected void createTestItems() {
+        mPauseResumeItem = createUserItem(
+                R.string.tv_time_shift_test_pause_resume,
+                R.string.tv_launch_tv_app, this);
+        setButtonEnabled(mPauseResumeItem, true);
+        mVerifyResumeAfterPauseItem = createAutoItem(
+                R.string.tv_time_shift_test_verify_resume_after_pause);
+        mVerifyPositionTrackingItem = createAutoItem(
+                R.string.tv_time_shift_test_verify_position_tracking);
+        mSetPlaybackParamsItem = createUserItem(
+                R.string.tv_time_shift_test_speed_rate,
+                R.string.tv_launch_tv_app, this);
+        mVerifyRewindItem = createAutoItem(
+                R.string.tv_time_shift_test_verify_rewind);
+        mVerifyFastForwardItem = createAutoItem(
+                R.string.tv_time_shift_test_verify_fast_forward);
+        mSeekToItem = createUserItem(
+                R.string.tv_time_shift_test_seek,
+                R.string.tv_launch_tv_app, this);
+        mVerifySeekToPreviousItem = createAutoItem(
+                R.string.tv_time_shift_test_verify_seek_to_previous);
+        mVerifySeekToNextItem = createAutoItem(
+                R.string.tv_time_shift_test_verify_seek_to_next);
+    }
+
+    @Override
+    protected void setInfoResources() {
+        setInfoResources(R.string.tv_time_shift_test,
+                R.string.tv_time_shift_test_info, -1);
+    }
+
+    private boolean isPassedState(View view) {
+        return ((Boolean) view.getTag()) == PASSED;
+    }
+}