TIF: Add time shift APIs.

Change-Id: I4faecd99d9e565c7228fc4b9da99e2adf1e67f19
diff --git a/api/current.txt b/api/current.txt
index 202139c..dc2bb40 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -17465,6 +17465,10 @@
     field public static final int INPUT_STATE_CONNECTED_STANDBY = 1; // 0x1
     field public static final int INPUT_STATE_DISCONNECTED = 2; // 0x2
     field public static final java.lang.String META_DATA_CONTENT_RATING_SYSTEMS = "android.media.tv.metadata.CONTENT_RATING_SYSTEMS";
+    field public static final long TIME_SHIFT_INVALID_TIME = -9223372036854775808L; // 0x8000000000000000L
+    field public static final int TIME_SHIFT_STATUS_AVAILABLE = 0; // 0x0
+    field public static final int TIME_SHIFT_STATUS_ERROR = 2; // 0x2
+    field public static final int TIME_SHIFT_STATUS_UNAVAILABLE = 1; // 0x1
     field public static final int VIDEO_UNAVAILABLE_REASON_BUFFERING = 3; // 0x3
     field public static final int VIDEO_UNAVAILABLE_REASON_TUNING = 1; // 0x1
     field public static final int VIDEO_UNAVAILABLE_REASON_UNKNOWN = 0; // 0x0
@@ -17499,6 +17503,8 @@
     method public void notifyChannelRetuned(android.net.Uri);
     method public void notifyContentAllowed();
     method public void notifyContentBlocked(android.media.tv.TvContentRating);
+    method public void notifyTimeShiftStartPositionChanged(long);
+    method public void notifyTimeShiftStatusChanged(int);
     method public void notifyTrackSelected(int, java.lang.String);
     method public void notifyTracksChanged(java.util.List<android.media.tv.TvTrackInfo>);
     method public void notifyVideoAvailable();
@@ -17515,6 +17521,11 @@
     method public abstract void onSetStreamVolume(float);
     method public abstract boolean onSetSurface(android.view.Surface);
     method public void onSurfaceChanged(int, int, int);
+    method public long onTimeShiftGetCurrentPosition();
+    method public void onTimeShiftPause();
+    method public void onTimeShiftResume();
+    method public void onTimeShiftSeekTo(long);
+    method public void onTimeShiftSetPlaybackRate(float, int);
     method public boolean onTouchEvent(android.view.MotionEvent);
     method public boolean onTrackballEvent(android.view.MotionEvent);
     method public abstract boolean onTune(android.net.Uri);
@@ -17563,19 +17574,31 @@
     method public java.util.List<android.media.tv.TvTrackInfo> getTracks(int);
     method protected void onLayout(boolean, int, int, int, int);
     method public boolean onUnhandledInputEvent(android.view.InputEvent);
+    method public void registerTimeShiftPositionCallback(android.media.tv.TvView.TimeShiftPositionCallback);
     method public void reset();
     method public void selectTrack(int, java.lang.String);
     method public void setCallback(android.media.tv.TvView.TvInputCallback);
     method public void setCaptionEnabled(boolean);
     method public void setOnUnhandledInputEventListener(android.media.tv.TvView.OnUnhandledInputEventListener);
     method public void setStreamVolume(float);
+    method public void timeShiftPause();
+    method public void timeShiftResume();
+    method public void timeShiftSeekTo(long);
+    method public void timeShiftSetPlaybackRate(float, int);
     method public void tune(java.lang.String, android.net.Uri);
+    method public void unregisterTimeShiftPositionCallback(android.media.tv.TvView.TimeShiftPositionCallback);
   }
 
   public static abstract interface TvView.OnUnhandledInputEventListener {
     method public abstract boolean onUnhandledInputEvent(android.view.InputEvent);
   }
 
+  public static abstract class TvView.TimeShiftPositionCallback {
+    ctor public TvView.TimeShiftPositionCallback();
+    method public void onTimeShiftCurrentPositionChanged(java.lang.String, long);
+    method public void onTimeShiftStartPositionChanged(java.lang.String, long);
+  }
+
   public static abstract class TvView.TvInputCallback {
     ctor public TvView.TvInputCallback();
     method public void onChannelRetuned(java.lang.String, android.net.Uri);
@@ -17583,6 +17606,7 @@
     method public void onContentAllowed(java.lang.String);
     method public void onContentBlocked(java.lang.String, android.media.tv.TvContentRating);
     method public void onDisconnected(java.lang.String);
+    method public void onTimeShiftStatusChanged(java.lang.String, int);
     method public void onTrackSelected(java.lang.String, int, java.lang.String);
     method public void onTracksChanged(java.lang.String, java.util.List<android.media.tv.TvTrackInfo>);
     method public void onVideoAvailable(java.lang.String);
diff --git a/api/system-current.txt b/api/system-current.txt
index 8d8e0c3..11d705f 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -18825,6 +18825,10 @@
     field public static final int INPUT_STATE_CONNECTED_STANDBY = 1; // 0x1
     field public static final int INPUT_STATE_DISCONNECTED = 2; // 0x2
     field public static final java.lang.String META_DATA_CONTENT_RATING_SYSTEMS = "android.media.tv.metadata.CONTENT_RATING_SYSTEMS";
+    field public static final long TIME_SHIFT_INVALID_TIME = -9223372036854775808L; // 0x8000000000000000L
+    field public static final int TIME_SHIFT_STATUS_AVAILABLE = 0; // 0x0
+    field public static final int TIME_SHIFT_STATUS_ERROR = 2; // 0x2
+    field public static final int TIME_SHIFT_STATUS_UNAVAILABLE = 1; // 0x1
     field public static final int VIDEO_UNAVAILABLE_REASON_BUFFERING = 3; // 0x3
     field public static final int VIDEO_UNAVAILABLE_REASON_TUNING = 1; // 0x1
     field public static final int VIDEO_UNAVAILABLE_REASON_UNKNOWN = 0; // 0x0
@@ -18867,6 +18871,9 @@
     method public void onSessionCreated(android.media.tv.TvInputManager.Session);
     method public void onSessionEvent(android.media.tv.TvInputManager.Session, java.lang.String, android.os.Bundle);
     method public void onSessionReleased(android.media.tv.TvInputManager.Session);
+    method public void onTimeShiftCurrentPositionChanged(android.media.tv.TvInputManager.Session, long);
+    method public void onTimeShiftStartPositionChanged(android.media.tv.TvInputManager.Session, long);
+    method public void onTimeShiftStatusChanged(android.media.tv.TvInputManager.Session, int);
     method public void onTrackSelected(android.media.tv.TvInputManager.Session, int, java.lang.String);
     method public void onTracksChanged(android.media.tv.TvInputManager.Session, java.util.List<android.media.tv.TvTrackInfo>);
     method public void onVideoAvailable(android.media.tv.TvInputManager.Session);
@@ -18909,6 +18916,8 @@
     method public void notifyContentAllowed();
     method public void notifyContentBlocked(android.media.tv.TvContentRating);
     method public void notifySessionEvent(java.lang.String, android.os.Bundle);
+    method public void notifyTimeShiftStartPositionChanged(long);
+    method public void notifyTimeShiftStatusChanged(int);
     method public void notifyTrackSelected(int, java.lang.String);
     method public void notifyTracksChanged(java.util.List<android.media.tv.TvTrackInfo>);
     method public void notifyVideoAvailable();
@@ -18928,6 +18937,11 @@
     method public abstract void onSetStreamVolume(float);
     method public abstract boolean onSetSurface(android.view.Surface);
     method public void onSurfaceChanged(int, int, int);
+    method public long onTimeShiftGetCurrentPosition();
+    method public void onTimeShiftPause();
+    method public void onTimeShiftResume();
+    method public void onTimeShiftSeekTo(long);
+    method public void onTimeShiftSetPlaybackRate(float, int);
     method public boolean onTouchEvent(android.view.MotionEvent);
     method public boolean onTrackballEvent(android.view.MotionEvent);
     method public abstract boolean onTune(android.net.Uri);
@@ -19000,6 +19014,7 @@
     method public java.util.List<android.media.tv.TvTrackInfo> getTracks(int);
     method protected void onLayout(boolean, int, int, int, int);
     method public boolean onUnhandledInputEvent(android.view.InputEvent);
+    method public void registerTimeShiftPositionCallback(android.media.tv.TvView.TimeShiftPositionCallback);
     method public void requestUnblockContent(android.media.tv.TvContentRating);
     method public void reset();
     method public void selectTrack(int, java.lang.String);
@@ -19011,14 +19026,25 @@
     method public void setStreamVolume(float);
     method public void setZOrderMediaOverlay(boolean);
     method public void setZOrderOnTop(boolean);
+    method public void timeShiftPause();
+    method public void timeShiftResume();
+    method public void timeShiftSeekTo(long);
+    method public void timeShiftSetPlaybackRate(float, int);
     method public void tune(java.lang.String, android.net.Uri);
     method public void tune(java.lang.String, android.net.Uri, android.os.Bundle);
+    method public void unregisterTimeShiftPositionCallback(android.media.tv.TvView.TimeShiftPositionCallback);
   }
 
   public static abstract interface TvView.OnUnhandledInputEventListener {
     method public abstract boolean onUnhandledInputEvent(android.view.InputEvent);
   }
 
+  public static abstract class TvView.TimeShiftPositionCallback {
+    ctor public TvView.TimeShiftPositionCallback();
+    method public void onTimeShiftCurrentPositionChanged(java.lang.String, long);
+    method public void onTimeShiftStartPositionChanged(java.lang.String, long);
+  }
+
   public static abstract class TvView.TvInputCallback {
     ctor public TvView.TvInputCallback();
     method public void onChannelRetuned(java.lang.String, android.net.Uri);
@@ -19027,6 +19053,7 @@
     method public void onContentBlocked(java.lang.String, android.media.tv.TvContentRating);
     method public void onDisconnected(java.lang.String);
     method public void onEvent(java.lang.String, java.lang.String, android.os.Bundle);
+    method public void onTimeShiftStatusChanged(java.lang.String, int);
     method public void onTrackSelected(java.lang.String, int, java.lang.String);
     method public void onTracksChanged(java.lang.String, java.util.List<android.media.tv.TvTrackInfo>);
     method public void onVideoAvailable(java.lang.String);
diff --git a/media/java/android/media/tv/ITvInputClient.aidl b/media/java/android/media/tv/ITvInputClient.aidl
index 7a023d6..86c0e5d 100644
--- a/media/java/android/media/tv/ITvInputClient.aidl
+++ b/media/java/android/media/tv/ITvInputClient.aidl
@@ -40,4 +40,7 @@
     void onContentAllowed(int seq);
     void onContentBlocked(in String rating, int seq);
     void onLayoutSurface(int left, int top, int right, int bottom, int seq);
+    void onTimeShiftStatusChanged(int status, int seq);
+    void onTimeShiftStartPositionChanged(long timeMs, int seq);
+    void onTimeShiftCurrentPositionChanged(long timeMs, int seq);
 }
diff --git a/media/java/android/media/tv/ITvInputManager.aidl b/media/java/android/media/tv/ITvInputManager.aidl
index 21549c9..f96469e 100644
--- a/media/java/android/media/tv/ITvInputManager.aidl
+++ b/media/java/android/media/tv/ITvInputManager.aidl
@@ -74,6 +74,12 @@
 
     void requestUnblockContent(in IBinder sessionToken, in String unblockedRating, int userId);
 
+    void timeShiftPause(in IBinder sessionToken, int userId);
+    void timeShiftResume(in IBinder sessionToken, int userId);
+    void timeShiftSeekTo(in IBinder sessionToken, long timeMs, int userId);
+    void timeShiftSetPlaybackRate(in IBinder sessionToken, float rate, int audioMode, int userId);
+    void timeShiftTrackCurrentPosition(in IBinder sessionToken, boolean enabled, int userId);
+
     // For TV input hardware binding
     List<TvInputHardwareInfo> getHardwareList();
     ITvInputHardware acquireTvInputHardware(int deviceId, in ITvInputHardwareCallback callback,
diff --git a/media/java/android/media/tv/ITvInputSession.aidl b/media/java/android/media/tv/ITvInputSession.aidl
index 1aad2fa..306abb8 100644
--- a/media/java/android/media/tv/ITvInputSession.aidl
+++ b/media/java/android/media/tv/ITvInputSession.aidl
@@ -46,4 +46,10 @@
     void removeOverlayView();
 
     void requestUnblockContent(in String unblockedRating);
+
+    void timeShiftPause();
+    void timeShiftResume();
+    void timeShiftSeekTo(long timeMs);
+    void timeShiftSetPlaybackRate(float rate, int audioMode);
+    void timeShiftTrackCurrentPosition(boolean enabled);
 }
diff --git a/media/java/android/media/tv/ITvInputSessionCallback.aidl b/media/java/android/media/tv/ITvInputSessionCallback.aidl
index 063d10d..e936810 100644
--- a/media/java/android/media/tv/ITvInputSessionCallback.aidl
+++ b/media/java/android/media/tv/ITvInputSessionCallback.aidl
@@ -37,4 +37,7 @@
     void onContentAllowed();
     void onContentBlocked(in String rating);
     void onLayoutSurface(int left, int top, int right, int bottom);
+    void onTimeShiftStatusChanged(int status);
+    void onTimeShiftStartPositionChanged(long timeMs);
+    void onTimeShiftCurrentPositionChanged(long timeMs);
 }
diff --git a/media/java/android/media/tv/ITvInputSessionWrapper.java b/media/java/android/media/tv/ITvInputSessionWrapper.java
index 94c9690..f22a8fc 100644
--- a/media/java/android/media/tv/ITvInputSessionWrapper.java
+++ b/media/java/android/media/tv/ITvInputSessionWrapper.java
@@ -57,6 +57,11 @@
     private static final int DO_RELAYOUT_OVERLAY_VIEW = 11;
     private static final int DO_REMOVE_OVERLAY_VIEW = 12;
     private static final int DO_REQUEST_UNBLOCK_CONTENT = 13;
+    private static final int DO_TIME_SHIFT_PAUSE = 14;
+    private static final int DO_TIME_SHIFT_RESUME = 15;
+    private static final int DO_TIME_SHIFT_SEEK_TO = 16;
+    private static final int DO_TIME_SHIFT_SET_PLAYBACK_RATE = 17;
+    private static final int DO_TIME_SHIFT_TRACK_CURRENT_POSITION = 18;
 
     private final HandlerCaller mCaller;
 
@@ -153,6 +158,26 @@
                 mTvInputSessionImpl.unblockContent((String) msg.obj);
                 break;
             }
+            case DO_TIME_SHIFT_PAUSE: {
+                mTvInputSessionImpl.timeShiftPause();
+                break;
+            }
+            case DO_TIME_SHIFT_RESUME: {
+                mTvInputSessionImpl.timeShiftResume();
+                break;
+            }
+            case DO_TIME_SHIFT_SEEK_TO: {
+                mTvInputSessionImpl.timeShiftSeekTo((Long) msg.obj);
+                break;
+            }
+            case DO_TIME_SHIFT_SET_PLAYBACK_RATE: {
+                mTvInputSessionImpl.timeShiftSetPlaybackRate((Float) msg.obj, msg.arg1);
+                break;
+            }
+            case DO_TIME_SHIFT_TRACK_CURRENT_POSITION: {
+                mTvInputSessionImpl.timeShiftTrackCurrentPosition((Boolean) msg.obj);
+                break;
+            }
             default: {
                 Log.w(TAG, "Unhandled message code: " + msg.what);
                 break;
@@ -242,6 +267,34 @@
                 DO_REQUEST_UNBLOCK_CONTENT, unblockedRating));
     }
 
+    @Override
+    public void timeShiftPause() {
+        mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_TIME_SHIFT_PAUSE));
+    }
+
+    @Override
+    public void timeShiftResume() {
+        mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_TIME_SHIFT_RESUME));
+    }
+
+    @Override
+    public void timeShiftSeekTo(long timeMs) {
+        mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_TIME_SHIFT_SEEK_TO,
+                Long.valueOf(timeMs)));
+    }
+
+    @Override
+    public void timeShiftSetPlaybackRate(float rate, int audioMode) {
+        mCaller.executeOrSendMessage(mCaller.obtainMessageIO(DO_TIME_SHIFT_SET_PLAYBACK_RATE,
+                audioMode, Float.valueOf(rate)));
+    }
+
+    @Override
+    public void timeShiftTrackCurrentPosition(boolean enabled) {
+        mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_TIME_SHIFT_TRACK_CURRENT_POSITION,
+                Boolean.valueOf(enabled)));
+    }
+
     private final class TvInputEventReceiver extends InputEventReceiver {
         public TvInputEventReceiver(InputChannel inputChannel, Looper looper) {
             super(inputChannel, looper);
diff --git a/media/java/android/media/tv/TvInputManager.java b/media/java/android/media/tv/TvInputManager.java
index f55299e..a4d8174 100644
--- a/media/java/android/media/tv/TvInputManager.java
+++ b/media/java/android/media/tv/TvInputManager.java
@@ -71,6 +71,28 @@
      */
     public static final int VIDEO_UNAVAILABLE_REASON_BUFFERING = VIDEO_UNAVAILABLE_REASON_END;
 
+    private static final int TIME_SHIFT_STATUS_START = 0;
+    private static final int TIME_SHIFT_STATUS_END = 2;
+
+    /**
+     * Time shifting is available. In this status, the application can pause/resume the playback,
+     * seek to a specific position, and change the playback rate.
+     */
+    public static final int TIME_SHIFT_STATUS_AVAILABLE = TIME_SHIFT_STATUS_START;
+
+    /**
+     * Time shifting is not available.
+     */
+    public static final int TIME_SHIFT_STATUS_UNAVAILABLE = 1;
+
+    /**
+     * An error occurred while handling a time shift request. To recover the status, tune to a
+     * new channel.
+     */
+    public static final int TIME_SHIFT_STATUS_ERROR = TIME_SHIFT_STATUS_END;
+
+    public static final long TIME_SHIFT_INVALID_TIME = Long.MIN_VALUE;
+
     /**
      * The TV input is in unknown state.
      * <p>
@@ -271,7 +293,7 @@
         /**
          * This is called when the video is not available, so the TV input stops the playback.
          *
-         * @param session A {@link TvInputManager.Session} associated with this callback
+         * @param session A {@link TvInputManager.Session} associated with this callback.
          * @param reason The reason why the TV input stopped the playback:
          * <ul>
          * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_UNKNOWN}
@@ -287,7 +309,7 @@
          * This is called when the current program content turns out to be allowed to watch since
          * its content rating is not blocked by parental controls.
          *
-         * @param session A {@link TvInputManager.Session} associated with this callback
+         * @param session A {@link TvInputManager.Session} associated with this callback.
          */
         public void onContentAllowed(Session session) {
         }
@@ -296,7 +318,7 @@
          * This is called when the current program content turns out to be not allowed to watch
          * since its content rating is blocked by parental controls.
          *
-         * @param session A {@link TvInputManager.Session} associated with this callback
+         * @param session A {@link TvInputManager.Session} associated with this callback.
          * @param rating The content ration of the blocked program.
          */
         public void onContentBlocked(Session session, TvContentRating rating) {
@@ -306,7 +328,7 @@
          * This is called when {@link TvInputService.Session#layoutSurface} is called to change the
          * layout of surface.
          *
-         * @param session A {@link TvInputManager.Session} associated with this callback
+         * @param session A {@link TvInputManager.Session} associated with this callback.
          * @param left Left position.
          * @param top Top position.
          * @param right Right position.
@@ -328,6 +350,40 @@
         @SystemApi
         public void onSessionEvent(Session session, String eventType, Bundle eventArgs) {
         }
+
+        /**
+         * This is called when the trick play status is changed.
+         *
+         * @param session A {@link TvInputManager.Session} associated with this callback.
+         * @param status The current time shift status:
+         * <ul>
+         * <li>{@link TvInputManager#TIME_SHIFT_STATUS_AVAILABLE}
+         * <li>{@link TvInputManager#TIME_SHIFT_STATUS_UNAVAILABLE}
+         * <li>{@link TvInputManager#TIME_SHIFT_STATUS_ERROR}
+         * </ul>
+         */
+        public void onTimeShiftStatusChanged(Session session, int status) {
+        }
+
+        /**
+         * This is called when the time shift start position is changed. The application may seek to
+         * a position in the range from the start position and the current time, inclusive.
+         *
+         * @param session A {@link TvInputManager.Session} associated with this callback.
+         * @param timeMs The start of the possible time shift range, in milliseconds since the
+         *         epoch.
+         */
+        public void onTimeShiftStartPositionChanged(Session session, long timeMs) {
+        }
+
+        /**
+         * This is called when the current position is changed.
+         *
+         * @param session A {@link TvInputManager.Session} associated with this callback.
+         * @param timeMs The current position, in milliseconds since the epoch.
+         */
+        public void onTimeShiftCurrentPositionChanged(Session session, long timeMs) {
+        }
     }
 
     private static final class SessionCallbackRecord {
@@ -450,6 +506,33 @@
                 }
             });
         }
+
+        void postTimeShiftStatusChanged(final int status) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mSessionCallback.onTimeShiftStatusChanged(mSession, status);
+                }
+            });
+        }
+
+        void postTimeShiftStartPositionChanged(final long timeMs) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mSessionCallback.onTimeShiftStartPositionChanged(mSession, timeMs);
+                }
+            });
+        }
+
+        void postTimeShiftCurrentPositionChanged(final long timeMs) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mSessionCallback.onTimeShiftCurrentPositionChanged(mSession, timeMs);
+                }
+            });
+        }
     }
 
     /**
@@ -718,6 +801,42 @@
                     record.postSessionEvent(eventType, eventArgs);
                 }
             }
+
+            @Override
+            public void onTimeShiftStatusChanged(int status, int seq) {
+                synchronized (mSessionCallbackRecordMap) {
+                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+                    if (record == null) {
+                        Log.e(TAG, "Callback not found for seq " + seq);
+                        return;
+                    }
+                    record.postTimeShiftStatusChanged(status);
+                }
+            }
+
+            @Override
+            public void onTimeShiftStartPositionChanged(long timeMs, int seq) {
+                synchronized (mSessionCallbackRecordMap) {
+                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+                    if (record == null) {
+                        Log.e(TAG, "Callback not found for seq " + seq);
+                        return;
+                    }
+                    record.postTimeShiftStartPositionChanged(timeMs);
+                }
+            }
+
+            @Override
+            public void onTimeShiftCurrentPositionChanged(long timeMs, int seq) {
+                synchronized (mSessionCallbackRecordMap) {
+                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+                    if (record == null) {
+                        Log.e(TAG, "Callback not found for seq " + seq);
+                        return;
+                    }
+                    record.postTimeShiftCurrentPositionChanged(timeMs);
+                }
+            }
         };
         mManagerCallback = new ITvInputManagerCallback.Stub() {
             @Override
@@ -1171,22 +1290,22 @@
         private TvInputEventSender mSender;
         private InputChannel mChannel;
 
-        private final Object mTrackLock = new Object();
-        // @GuardedBy("mTrackLock")
+        private final Object mMetadataLock = new Object();
+        // @GuardedBy("mMetadataLock")
         private final List<TvTrackInfo> mAudioTracks = new ArrayList<TvTrackInfo>();
-        // @GuardedBy("mTrackLock")
+        // @GuardedBy("mMetadataLock")
         private final List<TvTrackInfo> mVideoTracks = new ArrayList<TvTrackInfo>();
-        // @GuardedBy("mTrackLock")
+        // @GuardedBy("mMetadataLock")
         private final List<TvTrackInfo> mSubtitleTracks = new ArrayList<TvTrackInfo>();
-        // @GuardedBy("mTrackLock")
+        // @GuardedBy("mMetadataLock")
         private String mSelectedAudioTrackId;
-        // @GuardedBy("mTrackLock")
+        // @GuardedBy("mMetadataLock")
         private String mSelectedVideoTrackId;
-        // @GuardedBy("mTrackLock")
+        // @GuardedBy("mMetadataLock")
         private String mSelectedSubtitleTrackId;
-        // @GuardedBy("mTrackLock")
+        // @GuardedBy("mMetadataLock")
         private int mVideoWidth;
-        // @GuardedBy("mTrackLock")
+        // @GuardedBy("mMetadataLock")
         private int mVideoHeight;
 
         private Session(IBinder token, InputChannel channel, ITvInputManager service, int userId,
@@ -1322,7 +1441,7 @@
                 Log.w(TAG, "The session has been already released");
                 return;
             }
-            synchronized (mTrackLock) {
+            synchronized (mMetadataLock) {
                 mAudioTracks.clear();
                 mVideoTracks.clear();
                 mSubtitleTracks.clear();
@@ -1367,7 +1486,7 @@
          * @see #getTracks
          */
         public void selectTrack(int type, String trackId) {
-            synchronized (mTrackLock) {
+            synchronized (mMetadataLock) {
                 if (type == TvTrackInfo.TYPE_AUDIO) {
                     if (trackId != null && !containsTrack(mAudioTracks, trackId)) {
                         Log.w(TAG, "Invalid audio trackId: " + trackId);
@@ -1416,7 +1535,7 @@
          * @return the list of tracks for the given type.
          */
         public List<TvTrackInfo> getTracks(int type) {
-            synchronized (mTrackLock) {
+            synchronized (mMetadataLock) {
                 if (type == TvTrackInfo.TYPE_AUDIO) {
                     if (mAudioTracks == null) {
                         return null;
@@ -1445,7 +1564,7 @@
          * @see #selectTrack
          */
         public String getSelectedTrack(int type) {
-            synchronized (mTrackLock) {
+            synchronized (mMetadataLock) {
                 if (type == TvTrackInfo.TYPE_AUDIO) {
                     return mSelectedAudioTrackId;
                 } else if (type == TvTrackInfo.TYPE_VIDEO) {
@@ -1462,7 +1581,7 @@
          * there is an update.
          */
         boolean updateTracks(List<TvTrackInfo> tracks) {
-            synchronized (mTrackLock) {
+            synchronized (mMetadataLock) {
                 mAudioTracks.clear();
                 mVideoTracks.clear();
                 mSubtitleTracks.clear();
@@ -1485,7 +1604,7 @@
          * Returns true if there is an update.
          */
         boolean updateTrackSelection(int type, String trackId) {
-            synchronized (mTrackLock) {
+            synchronized (mMetadataLock) {
                 if (type == TvTrackInfo.TYPE_AUDIO && trackId != mSelectedAudioTrackId) {
                     mSelectedAudioTrackId = trackId;
                     return true;
@@ -1509,7 +1628,7 @@
          * track.
          */
         TvTrackInfo getVideoTrackToNotify() {
-            synchronized (mTrackLock) {
+            synchronized (mMetadataLock) {
                 if (!mVideoTracks.isEmpty() && mSelectedVideoTrackId != null) {
                     for (TvTrackInfo track : mVideoTracks) {
                         if (track.getId().equals(mSelectedVideoTrackId)) {
@@ -1528,6 +1647,92 @@
         }
 
         /**
+         * Pauses the playback. Call {@link #timeShiftResume()} to restart the playback.
+         */
+        void timeShiftPause() {
+            if (mToken == null) {
+                Log.w(TAG, "The session has been already released");
+                return;
+            }
+            try {
+                mService.timeShiftPause(mToken, mUserId);
+            } catch (RemoteException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        /**
+         * Resumes the playback. No-op if it is already playing the channel.
+         */
+        void timeShiftResume() {
+            if (mToken == null) {
+                Log.w(TAG, "The session has been already released");
+                return;
+            }
+            try {
+                mService.timeShiftResume(mToken, mUserId);
+            } catch (RemoteException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        /**
+         * Seeks to the specific time position. The position should be in the range from the start
+         * time from the start time,
+         * {@link TvInputCallback#onTimeShiftStartPositionChanged(String, long)}, to the current
+         * time, inclusive.
+         *
+         * @param timeMs The target time, in milliseconds since the epoch.
+         */
+        void timeShiftSeekTo(long timeMs) {
+            if (mToken == null) {
+                Log.w(TAG, "The session has been already released");
+                return;
+            }
+            try {
+                mService.timeShiftSeekTo(mToken, timeMs, mUserId);
+            } catch (RemoteException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        /**
+         * Sets a playback rate and an audio mode.
+         *
+         * @param rate The ratio between desired playback rate and normal one.
+         * @param audioMode The audio playback mode. Must be one of the supported audio modes:
+         * <ul>
+         * <li> {@link android.media.MediaPlayer#PLAYBACK_RATE_AUDIO_MODE_RESAMPLE}
+         * </ul>
+         */
+        void timeShiftSetPlaybackRate(float rate, int audioMode) {
+            if (mToken == null) {
+                Log.w(TAG, "The session has been already released");
+                return;
+            }
+            try {
+                mService.timeShiftSetPlaybackRate(mToken, rate, audioMode, mUserId);
+            } catch (RemoteException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        /**
+         * Returns the current playback position.
+         */
+        void timeShiftTrackCurrentPosition(boolean enabled) {
+            if (mToken == null) {
+                Log.w(TAG, "The session has been already released");
+                return;
+            }
+            try {
+                mService.timeShiftTrackCurrentPosition(mToken, enabled, mUserId);
+            } catch (RemoteException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        /**
          * Calls {@link TvInputService.Session#appPrivateCommand(String, Bundle)
          * TvInputService.Session.appPrivateCommand()} on the current TvView.
          *
diff --git a/media/java/android/media/tv/TvInputService.java b/media/java/android/media/tv/TvInputService.java
index 8ed383a..93abc2b 100644
--- a/media/java/android/media/tv/TvInputService.java
+++ b/media/java/android/media/tv/TvInputService.java
@@ -235,7 +235,9 @@
      * Base class for derived classes to implement to provide a TV input session.
      */
     public abstract static class Session implements KeyEvent.Callback {
-        private static final int DETACH_OVERLAY_VIEW_TIMEOUT = 5000;
+        private static final int DETACH_OVERLAY_VIEW_TIMEOUT_MS = 5000;
+        private static final int POSITION_UPDATE_INTERVAL_MS = 1000;
+
         private final KeyEvent.DispatcherState mDispatcherState = new KeyEvent.DispatcherState();
         private final WindowManager mWindowManager;
         final Handler mHandler;
@@ -248,6 +250,10 @@
         private boolean mOverlayViewEnabled;
         private IBinder mWindowToken;
         private Rect mOverlayFrame;
+        private long mCurrentPositionMs;
+        private final TimeShiftCurrentPositionTrackingRunnable
+                mTimeShiftCurrentPositionTrackingRunnable =
+                new TimeShiftCurrentPositionTrackingRunnable();
 
         private final Object mLock = new Object();
         // @GuardedBy("mLock")
@@ -264,6 +270,7 @@
             mContext = context;
             mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
             mHandler = new Handler(context.getMainLooper());
+            mCurrentPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME;
         }
 
         /**
@@ -550,6 +557,89 @@
         }
 
         /**
+         * Informs the application that the trick play status is changed.
+         * <p>
+         * The application assumes that time shift is not available by default. So, the
+         * implementation should call this method with
+         * {@link TvInputManager#TIME_SHIFT_STATUS_AVAILABLE} on tune request, if the time shift is
+         * available in the given channel.
+         * Note that sending {@link TvInputManager#TIME_SHIFT_STATUS_AVAILABLE} means the session
+         * implemented {@link #onTimeShiftPause}, {@link #onTimeShiftResume},
+         * {@link #onTimeShiftSeekTo}, {@link #onTimeShiftGetCurrentPosition}, and
+         * {@link #onTimeShiftSetPlaybackRate}, and these are working at the moment.
+         * </p>
+         *
+         * @param status The current time shift status:
+         * <ul>
+         * <li>{@link TvInputManager#TIME_SHIFT_STATUS_AVAILABLE}
+         * <li>{@link TvInputManager#TIME_SHIFT_STATUS_UNAVAILABLE}
+         * <li>{@link TvInputManager#TIME_SHIFT_STATUS_ERROR}
+         * </ul>
+         */
+        public void notifyTimeShiftStatusChanged(final int status) {
+            executeOrPostRunnable(new Runnable() {
+                @Override
+                public void run() {
+                    try {
+                        if (DEBUG) Log.d(TAG, "notifyTimeShiftStatusChanged");
+                        if (mSessionCallback != null) {
+                            mSessionCallback.onTimeShiftStatusChanged(status);
+                        }
+                    } catch (RemoteException e) {
+                        Log.w(TAG, "error in notifyTimeShiftStatusChanged");
+                    }
+                }
+            });
+        }
+
+        /**
+         * Informs the application that the time shift start position is changed.
+         * <p>
+         * The application may seek to a position in the range from the start position and the
+         * current time, inclusive. So, the implementation should call this whenever the range is
+         * updated.
+         * </p>
+         *
+         * @param timeMs the start of possible time shift range, in milliseconds since the epoch.
+         */
+        public void notifyTimeShiftStartPositionChanged(final long timeMs) {
+            executeOrPostRunnable(new Runnable() {
+                @Override
+                public void run() {
+                    try {
+                        if (DEBUG) Log.d(TAG, "notifyTimeShiftStartPositionChanged");
+                        if (mSessionCallback != null) {
+                            mSessionCallback.onTimeShiftStartPositionChanged(timeMs);
+                        }
+                    } catch (RemoteException e) {
+                        Log.w(TAG, "error in notifyTimeShiftStartPositionChanged");
+                    }
+                }
+            });
+        }
+
+        /**
+         * Informs the application that the current playback position is changed.
+         *
+         * @param timeMs The current position, in milliseconds since the epoch.
+         */
+        private void notifyTimeShiftCurrentPositionChanged(final long timeMs) {
+            executeOrPostRunnable(new Runnable() {
+                @Override
+                public void run() {
+                    try {
+                        if (DEBUG) Log.d(TAG, "notifyTimeShiftCurrentPositionChanged");
+                        if (mSessionCallback != null) {
+                            mSessionCallback.onTimeShiftCurrentPositionChanged(timeMs);
+                        }
+                    } catch (RemoteException e) {
+                        Log.w(TAG, "error in notifyTimeShiftCurrentPositionChanged");
+                    }
+                }
+            });
+        }
+
+        /**
          * Assigns a position of the {@link Surface} passed by {@link #onSetSurface}. The position
          * is relative to an overlay view.
          *
@@ -756,6 +846,72 @@
         }
 
         /**
+         * Called when an application requests to pause the playback.
+         *
+         * @see #onTimeShiftResume()
+         * @see #onTimeShiftSeekTo(long)
+         * @see #onTimeShiftSetPlaybackRate(float, int)
+         * @see #onTimeShiftGetCurrentPosition()
+         */
+        public void onTimeShiftPause() {
+        }
+
+        /**
+         * Called when an application requests to resume the playback.
+         *
+         * @see #onTimeShiftPause()
+         * @see #onTimeShiftSeekTo(long)
+         * @see #onTimeShiftSetPlaybackRate(float, int)
+         * @see #onTimeShiftGetCurrentPosition()
+         */
+        public void onTimeShiftResume() {
+        }
+
+        /**
+         * Called when an application requests to seek to a specific position. The {@code timeMs} is
+         * expected to be in a range from the start time,
+         * {@link #notifyTimeShiftStartPositionChanged(long)}, to the current time, inclusive. If it
+         * is not, the implementation should seek to the nearest time position in the range.
+         *
+         * @param timeMs The target time, in milliseconds since the epoch
+         * @see #onTimeShiftResume()
+         * @see #onTimeShiftPause()
+         * @see #onTimeShiftSetPlaybackRate(float, int)
+         * @see #onTimeShiftGetCurrentPosition()
+         */
+        public void onTimeShiftSeekTo(long timeMs) {
+        }
+
+        /**
+         * Called when an application sets a playback rate and an audio mode.
+         *
+         * @param rate The ratio between desired playback rate and normal one.
+         * @param audioMode The audio playback mode. Must be one of the supported audio modes:
+         * <ul>
+         * <li> {@link android.media.MediaPlayer#PLAYBACK_RATE_AUDIO_MODE_RESAMPLE}
+         * </ul>
+         * @see #onTimeShiftResume()
+         * @see #onTimeShiftPause()
+         * @see #onTimeShiftSeekTo(long)
+         * @see #onTimeShiftGetCurrentPosition()
+         */
+        public void onTimeShiftSetPlaybackRate(float rate, int audioMode) {
+        }
+
+        /**
+         * Returns the current playback position in milliseconds since the epoch.
+         * {@link TvInputManager#TIME_SHIFT_INVALID_TIME} if position is unknown at this moment.
+         *
+         * @see #onTimeShiftResume()
+         * @see #onTimeShiftPause()
+         * @see #onTimeShiftSeekTo(long)
+         * @see #onTimeShiftSetPlaybackRate(float, int)
+         */
+        public long onTimeShiftGetCurrentPosition() {
+            return TvInputManager.TIME_SHIFT_INVALID_TIME;
+        }
+
+        /**
          * Default implementation of {@link android.view.KeyEvent.Callback#onKeyDown(int, KeyEvent)
          * KeyEvent.Callback.onKeyDown()}: always returns false (doesn't handle the event).
          * <p>
@@ -887,6 +1043,7 @@
             // Removes the overlay view lastly so that any hanging on the main thread can be handled
             // in {@link #scheduleOverlayViewCleanup}.
             removeOverlayView(true);
+            mHandler.removeCallbacks(mTimeShiftCurrentPositionTrackingRunnable);
         }
 
         /**
@@ -930,6 +1087,7 @@
          * Calls {@link #onTune}.
          */
         void tune(Uri channelUri, Bundle params) {
+            mCurrentPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME;
             onTune(channelUri, params);
             // TODO: Handle failure.
         }
@@ -1059,6 +1217,46 @@
         }
 
         /**
+         * Calls {@link #onTimeShiftPause}.
+         */
+        void timeShiftPause() {
+            onTimeShiftPause();
+        }
+
+        /**
+         * Calls {@link #onTimeShiftResume}.
+         */
+        void timeShiftResume() {
+            onTimeShiftResume();
+        }
+
+        /**
+         * Calls {@link #onTimeShiftSeekTo}.
+         */
+        void timeShiftSeekTo(long timeMs) {
+            onTimeShiftSeekTo(timeMs);
+        }
+
+        /**
+         * Calls {@link #onTimeShiftSetPlaybackRate}.
+         */
+        void timeShiftSetPlaybackRate(float rate, int audioMode) {
+            onTimeShiftSetPlaybackRate(rate, audioMode);
+        }
+
+        /**
+         * Turns on/off the current position tracking.
+         */
+        void timeShiftTrackCurrentPosition(boolean enabled) {
+            if (enabled) {
+                mHandler.post(mTimeShiftCurrentPositionTrackingRunnable);
+            } else {
+                mHandler.removeCallbacks(mTimeShiftCurrentPositionTrackingRunnable);
+                mCurrentPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME;
+            }
+        }
+
+        /**
          * Schedules a task which checks whether the overlay view is detached and kills the process
          * if it is not. Note that this method is expected to be called in a non-main thread.
          */
@@ -1154,12 +1352,26 @@
             }
         }
 
+        private final class TimeShiftCurrentPositionTrackingRunnable implements Runnable {
+            @Override
+            public void run() {
+                long pos = onTimeShiftGetCurrentPosition();
+                if (mCurrentPositionMs != pos) {
+                    mCurrentPositionMs = pos;
+                    notifyTimeShiftCurrentPositionChanged(pos);
+                }
+                mHandler.removeCallbacks(mTimeShiftCurrentPositionTrackingRunnable);
+                mHandler.postDelayed(mTimeShiftCurrentPositionTrackingRunnable,
+                        POSITION_UPDATE_INTERVAL_MS);
+            }
+        }
+
         private final class OverlayViewCleanUpTask extends AsyncTask<View, Void, Void> {
             @Override
             protected Void doInBackground(View... views) {
                 View overlayViewParent = views[0];
                 try {
-                    Thread.sleep(DETACH_OVERLAY_VIEW_TIMEOUT);
+                    Thread.sleep(DETACH_OVERLAY_VIEW_TIMEOUT_MS);
                 } catch (InterruptedException e) {
                     return null;
                 }
diff --git a/media/java/android/media/tv/TvView.java b/media/java/android/media/tv/TvView.java
index 6fc1b82..115d094 100644
--- a/media/java/android/media/tv/TvView.java
+++ b/media/java/android/media/tv/TvView.java
@@ -42,6 +42,7 @@
 import android.view.ViewRootImpl;
 
 import java.lang.ref.WeakReference;
+import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -103,6 +104,7 @@
     private int mSurfaceViewRight;
     private int mSurfaceViewTop;
     private int mSurfaceViewBottom;
+    private List<TimeShiftPositionCallback> mTimeShiftPositionCallbacks = new ArrayList<>();
 
     private final SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() {
         @Override
@@ -420,6 +422,85 @@
     }
 
     /**
+     * Pauses the playback. Call {@link #timeShiftResume()} to restart the playback.
+     */
+    public void timeShiftPause() {
+        if (mSession != null) {
+            mSession.timeShiftPause();
+        }
+    }
+
+    /**
+     * Resumes the playback. No-op if it is already playing the channel.
+     */
+    public void timeShiftResume() {
+        if (mSession != null) {
+            mSession.timeShiftResume();
+        }
+    }
+
+    /**
+     * Seeks to the specific time position. The position should be in the range from the start time
+     * from the start time, {@link TimeShiftPositionCallback#onTimeShiftStartPositionChanged},
+     * to the current time, inclusive.
+     *
+     * @param timeMs The target time, in milliseconds since the epoch.
+     */
+    public void timeShiftSeekTo(long timeMs) {
+        if (mSession != null) {
+            mSession.timeShiftSeekTo(timeMs);
+        }
+    }
+
+    /**
+     * Sets a playback rate and an audio mode.
+     *
+     * @param rate The ratio between desired playback rate and normal one.
+     * @param audioMode The audio playback mode. Must be one of the supported audio modes:
+     * <ul>
+     * <li> {@link android.media.MediaPlayer#PLAYBACK_RATE_AUDIO_MODE_RESAMPLE}
+     * </ul>
+     */
+    public void timeShiftSetPlaybackRate(float rate, int audioMode) {
+        if (mSession != null) {
+            mSession.timeShiftSetPlaybackRate(rate, audioMode);
+        }
+    }
+
+    /**
+     * Registers a {@link TvView.TimeShiftPositionCallback}.
+     *
+     * @param callback A callback used to monitor the time shift range and current position.
+     */
+    public void registerTimeShiftPositionCallback(TimeShiftPositionCallback callback) {
+        if (callback == null) {
+            throw new IllegalArgumentException("callback can not be null.");
+        }
+        mTimeShiftPositionCallbacks.add(callback);
+        ensureCurrentPositionTracking();
+    }
+
+    /**
+     * Unregisters the existing {@link TvView.TimeShiftPositionCallback}.
+     *
+     * @param callback The existing callback to remove.
+     */
+    public void unregisterTimeShiftPositionCallback(TimeShiftPositionCallback callback) {
+        if (callback == null) {
+            throw new IllegalArgumentException("callback can not be null.");
+        }
+        mTimeShiftPositionCallbacks.remove(callback);
+        ensureCurrentPositionTracking();
+    }
+
+    private void ensureCurrentPositionTracking() {
+        if (mSession == null) {
+            return;
+        }
+        mSession.timeShiftTrackCurrentPosition(!mTimeShiftPositionCallbacks.isEmpty());
+    }
+
+    /**
      * Calls {@link TvInputService.Session#appPrivateCommand(String, Bundle)
      * TvInputService.Session.appPrivateCommand()} on the current TvView.
      *
@@ -729,6 +810,32 @@
     }
 
     /**
+     * Callback used to receive the information on the possible range for time shifting and currrent
+     * position.
+     */
+    public abstract static class TimeShiftPositionCallback {
+        /**
+         * This is called when the time shift start position is changed. The application may seek to
+         * a position in the range from the start position and the current time, inclusive.
+         *
+         * @param inputId The ID of the TV input bound to this view.
+         * @param timeMs the start of the possible time shift range, in milliseconds since the
+         *         epoch.
+         */
+        public void onTimeShiftStartPositionChanged(String inputId, long timeMs) {
+        }
+
+        /**
+         * This is called when the current playback position is changed.
+         *
+         * @param inputId The ID of the TV input bound to this view.
+         * @param timeMs The current position, in milliseconds since the epoch.
+         */
+        public void onTimeShiftCurrentPositionChanged(String inputId, long timeMs) {
+        }
+    }
+
+    /**
      * Callback used to receive various status updates on the {@link TvView}.
      */
     public abstract static class TvInputCallback {
@@ -838,6 +945,7 @@
         /**
          * This is invoked when a custom event from the bound TV input is sent to this view.
          *
+         * @param inputId The ID of the TV input bound to this view.
          * @param eventType The type of the event.
          * @param eventArgs Optional arguments of the event.
          * @hide
@@ -845,6 +953,20 @@
         @SystemApi
         public void onEvent(String inputId, String eventType, Bundle eventArgs) {
         }
+
+        /**
+         * This is called when the time shift status is changed.
+         *
+         * @param inputId The ID of the TV input bound to this view.
+         * @param status The current time shift status:
+         * <ul>
+         * <li>{@link TvInputManager#TIME_SHIFT_STATUS_AVAILABLE}
+         * <li>{@link TvInputManager#TIME_SHIFT_STATUS_UNAVAILABLE}
+         * <li>{@link TvInputManager#TIME_SHIFT_STATUS_ERROR}
+         * </ul>
+         */
+        public void onTimeShiftStatusChanged(String inputId, int status) {
+        }
     }
 
     /**
@@ -918,6 +1040,7 @@
                     mAppPrivateCommandAction = null;
                     mAppPrivateCommandData = null;
                 }
+                ensureCurrentPositionTracking();
             } else {
                 mSessionCallback = null;
                 if (mCallback != null) {
@@ -1087,5 +1210,47 @@
                 mCallback.onEvent(mInputId, eventType, eventArgs);
             }
         }
+
+        @Override
+        public void onTimeShiftStatusChanged(Session session, int status) {
+            if (DEBUG) {
+                Log.d(TAG, "onTimeShiftStatusChanged()");
+            }
+            if (this != mSessionCallback) {
+                Log.w(TAG, "onTimeShiftStatusChanged - session not created");
+                return;
+            }
+            if (mCallback != null) {
+                mCallback.onTimeShiftStatusChanged(mInputId, status);
+            }
+        }
+
+        @Override
+        public void onTimeShiftStartPositionChanged(Session session, long timeMs) {
+            if (DEBUG) {
+                Log.d(TAG, "onTimeShiftStartPositionChanged()");
+            }
+            if (this != mSessionCallback) {
+                Log.w(TAG, "onTimeShiftStartPositionChanged - session not created");
+                return;
+            }
+            for (TimeShiftPositionCallback callback : mTimeShiftPositionCallbacks) {
+                callback.onTimeShiftStartPositionChanged(mInputId, timeMs);
+            }
+        }
+
+        @Override
+        public void onTimeShiftCurrentPositionChanged(Session session, long timeMs) {
+            if (DEBUG) {
+                Log.d(TAG, "onTimeShiftCurrentPositionChanged()");
+            }
+            if (this != mSessionCallback) {
+                Log.w(TAG, "onTimeShiftCurrentPositionChanged - session not created");
+                return;
+            }
+            for (TimeShiftPositionCallback callback : mTimeShiftPositionCallbacks) {
+                callback.onTimeShiftCurrentPositionChanged(mInputId, timeMs);
+            }
+        }
     }
 }
diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java
index 5375bfc..152370a 100644
--- a/services/core/java/com/android/server/tv/TvInputManagerService.java
+++ b/services/core/java/com/android/server/tv/TvInputManagerService.java
@@ -1341,6 +1341,108 @@
         }
 
         @Override
+        public void timeShiftPause(IBinder sessionToken, int userId) {
+            final int callingUid = Binder.getCallingUid();
+            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
+                    userId, "timeShiftPause");
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    try {
+                        getSessionLocked(sessionToken, callingUid, resolvedUserId)
+                                .timeShiftPause();
+                    } catch (RemoteException | SessionNotFoundException e) {
+                        Slog.e(TAG, "error in timeShiftPause", e);
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
+        public void timeShiftResume(IBinder sessionToken, int userId) {
+            final int callingUid = Binder.getCallingUid();
+            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
+                    userId, "timeShiftResume");
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    try {
+                        getSessionLocked(sessionToken, callingUid, resolvedUserId)
+                                .timeShiftResume();
+                    } catch (RemoteException | SessionNotFoundException e) {
+                        Slog.e(TAG, "error in timeShiftResume", e);
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
+        public void timeShiftSeekTo(IBinder sessionToken, long timeMs, int userId) {
+            final int callingUid = Binder.getCallingUid();
+            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
+                    userId, "timeShiftSeekTo");
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    try {
+                        getSessionLocked(sessionToken, callingUid, resolvedUserId)
+                                .timeShiftSeekTo(timeMs);
+                    } catch (RemoteException | SessionNotFoundException e) {
+                        Slog.e(TAG, "error in timeShiftSeekTo", e);
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
+        public void timeShiftSetPlaybackRate(IBinder sessionToken, float rate, int audioMode,
+                int userId) {
+            final int callingUid = Binder.getCallingUid();
+            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
+                    userId, "timeShiftSetPlaybackRate");
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    try {
+                        getSessionLocked(sessionToken, callingUid, resolvedUserId)
+                                .timeShiftSetPlaybackRate(rate, audioMode);
+                    } catch (RemoteException | SessionNotFoundException e) {
+                        Slog.e(TAG, "error in timeShiftSetPlaybackRate", e);
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
+        public void timeShiftTrackCurrentPosition(IBinder sessionToken, boolean enabled,
+                int userId) {
+            final int callingUid = Binder.getCallingUid();
+            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
+                    userId, "timeShiftTrackCurrentPosition");
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    try {
+                        getSessionLocked(sessionToken, callingUid, resolvedUserId)
+                                .timeShiftTrackCurrentPosition(enabled);
+                    } catch (RemoteException | SessionNotFoundException e) {
+                        Slog.e(TAG, "error in timeShiftTrackCurrentPosition", e);
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
         public List<TvInputHardwareInfo> getHardwareList() throws RemoteException {
             if (mContext.checkCallingPermission(android.Manifest.permission.TV_INPUT_HARDWARE)
                     != PackageManager.PERMISSION_GRANTED) {
@@ -2144,6 +2246,58 @@
                 }
             }
         }
+
+        @Override
+        public void onTimeShiftStatusChanged(int status) {
+            synchronized (mLock) {
+                if (DEBUG) {
+                    Slog.d(TAG, "onTimeShiftStatusChanged()");
+                }
+                if (mSessionState.session == null || mSessionState.client == null) {
+                    return;
+                }
+                try {
+                    mSessionState.client.onTimeShiftStatusChanged(status, mSessionState.seq);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "error in onTimeShiftStatusChanged", e);
+                }
+            }
+        }
+
+        @Override
+        public void onTimeShiftStartPositionChanged(long timeMs) {
+            synchronized (mLock) {
+                if (DEBUG) {
+                    Slog.d(TAG, "onTimeShiftStartPositionChanged()");
+                }
+                if (mSessionState.session == null || mSessionState.client == null) {
+                    return;
+                }
+                try {
+                    mSessionState.client.onTimeShiftStartPositionChanged(timeMs, mSessionState.seq);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "error in onTimeShiftStartPositionChanged", e);
+                }
+            }
+        }
+
+        @Override
+        public void onTimeShiftCurrentPositionChanged(long timeMs) {
+            synchronized (mLock) {
+                if (DEBUG) {
+                    Slog.d(TAG, "onTimeShiftCurrentPositionChanged()");
+                }
+                if (mSessionState.session == null || mSessionState.client == null) {
+                    return;
+                }
+                try {
+                    mSessionState.client.onTimeShiftCurrentPositionChanged(timeMs,
+                            mSessionState.seq);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "error in onTimeShiftCurrentPositionChanged", e);
+                }
+            }
+        }
     }
 
     private static final class WatchLogHandler extends Handler {