TIF: Redefine time shift status and change callback and position APIs

Also added/modified the documentation and addressed feedback from the
previous CL.

Change-Id: I046970cd0d2688886039d37a332bbe11bc5a4b0f
diff --git a/api/current.txt b/api/current.txt
index bf55b7c..d2979fa 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -17539,9 +17539,10 @@
     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 TIME_SHIFT_STATUS_AVAILABLE = 3; // 0x3
+    field public static final int TIME_SHIFT_STATUS_UNAVAILABLE = 2; // 0x2
+    field public static final int TIME_SHIFT_STATUS_UNKNOWN = 0; // 0x0
+    field public static final int TIME_SHIFT_STATUS_UNSUPPORTED = 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
@@ -17576,7 +17577,6 @@
     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>);
@@ -17595,6 +17595,7 @@
     method public abstract boolean onSetSurface(android.view.Surface);
     method public void onSurfaceChanged(int, int, int);
     method public long onTimeShiftGetCurrentPosition();
+    method public long onTimeShiftGetStartPosition();
     method public void onTimeShiftPause();
     method public void onTimeShiftResume();
     method public void onTimeShiftSeekTo(long);
@@ -17647,19 +17648,18 @@
     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 setTimeShiftPositionCallback(android.media.tv.TvView.TimeShiftPositionCallback);
     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 {
diff --git a/api/system-current.txt b/api/system-current.txt
index 1152de5..7e06899 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -18902,9 +18902,10 @@
     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 TIME_SHIFT_STATUS_AVAILABLE = 3; // 0x3
+    field public static final int TIME_SHIFT_STATUS_UNAVAILABLE = 2; // 0x2
+    field public static final int TIME_SHIFT_STATUS_UNKNOWN = 0; // 0x0
+    field public static final int TIME_SHIFT_STATUS_UNSUPPORTED = 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
@@ -18992,7 +18993,6 @@
     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>);
@@ -19014,6 +19014,7 @@
     method public abstract boolean onSetSurface(android.view.Surface);
     method public void onSurfaceChanged(int, int, int);
     method public long onTimeShiftGetCurrentPosition();
+    method public long onTimeShiftGetStartPosition();
     method public void onTimeShiftPause();
     method public void onTimeShiftResume();
     method public void onTimeShiftSeekTo(long);
@@ -19090,7 +19091,6 @@
     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);
@@ -19100,6 +19100,7 @@
     method public void setMain();
     method public void setOnUnhandledInputEventListener(android.media.tv.TvView.OnUnhandledInputEventListener);
     method public void setStreamVolume(float);
+    method public void setTimeShiftPositionCallback(android.media.tv.TvView.TimeShiftPositionCallback);
     method public void setZOrderMediaOverlay(boolean);
     method public void setZOrderOnTop(boolean);
     method public void timeShiftPause();
@@ -19108,7 +19109,6 @@
     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 {
diff --git a/media/java/android/media/tv/ITvInputManager.aidl b/media/java/android/media/tv/ITvInputManager.aidl
index f96469e..b6491d8 100644
--- a/media/java/android/media/tv/ITvInputManager.aidl
+++ b/media/java/android/media/tv/ITvInputManager.aidl
@@ -78,7 +78,7 @@
     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);
+    void timeShiftEnablePositionTracking(in IBinder sessionToken, boolean enable, int userId);
 
     // For TV input hardware binding
     List<TvInputHardwareInfo> getHardwareList();
diff --git a/media/java/android/media/tv/ITvInputSession.aidl b/media/java/android/media/tv/ITvInputSession.aidl
index 306abb8..a054200 100644
--- a/media/java/android/media/tv/ITvInputSession.aidl
+++ b/media/java/android/media/tv/ITvInputSession.aidl
@@ -51,5 +51,5 @@
     void timeShiftResume();
     void timeShiftSeekTo(long timeMs);
     void timeShiftSetPlaybackRate(float rate, int audioMode);
-    void timeShiftTrackCurrentPosition(boolean enabled);
+    void timeShiftEnablePositionTracking(boolean enable);
 }
diff --git a/media/java/android/media/tv/ITvInputSessionWrapper.java b/media/java/android/media/tv/ITvInputSessionWrapper.java
index f22a8fc..a3442e3 100644
--- a/media/java/android/media/tv/ITvInputSessionWrapper.java
+++ b/media/java/android/media/tv/ITvInputSessionWrapper.java
@@ -61,7 +61,7 @@
     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 static final int DO_TIME_SHIFT_ENABLE_POSITION_TRACKING = 18;
 
     private final HandlerCaller mCaller;
 
@@ -174,8 +174,8 @@
                 mTvInputSessionImpl.timeShiftSetPlaybackRate((Float) msg.obj, msg.arg1);
                 break;
             }
-            case DO_TIME_SHIFT_TRACK_CURRENT_POSITION: {
-                mTvInputSessionImpl.timeShiftTrackCurrentPosition((Boolean) msg.obj);
+            case DO_TIME_SHIFT_ENABLE_POSITION_TRACKING: {
+                mTvInputSessionImpl.timeShiftEnablePositionTracking((Boolean) msg.obj);
                 break;
             }
             default: {
@@ -290,9 +290,9 @@
     }
 
     @Override
-    public void timeShiftTrackCurrentPosition(boolean enabled) {
-        mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_TIME_SHIFT_TRACK_CURRENT_POSITION,
-                Boolean.valueOf(enabled)));
+    public void timeShiftEnablePositionTracking(boolean enable) {
+        mCaller.executeOrSendMessage(mCaller.obtainMessageO(
+                DO_TIME_SHIFT_ENABLE_POSITION_TRACKING, Boolean.valueOf(enable)));
     }
 
     private final class TvInputEventReceiver extends InputEventReceiver {
diff --git a/media/java/android/media/tv/TvInputManager.java b/media/java/android/media/tv/TvInputManager.java
index a4d8174..3c67ea0 100644
--- a/media/java/android/media/tv/TvInputManager.java
+++ b/media/java/android/media/tv/TvInputManager.java
@@ -18,6 +18,7 @@
 
 import android.annotation.SystemApi;
 import android.graphics.Rect;
+import android.media.MediaPlayer;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
@@ -72,24 +73,29 @@
     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;
+    private static final int TIME_SHIFT_STATUS_END = 3;
 
     /**
-     * Time shifting is available. In this status, the application can pause/resume the playback,
-     * seek to a specific position, and change the playback rate.
+     * Status prior to calling {@link TvInputService.Session#notifyTimeShiftStatusChanged}.
      */
-    public static final int TIME_SHIFT_STATUS_AVAILABLE = TIME_SHIFT_STATUS_START;
+    public static final int TIME_SHIFT_STATUS_UNKNOWN = TIME_SHIFT_STATUS_START;
 
     /**
-     * Time shifting is not available.
+     * The TV input does not support time shifting.
      */
-    public static final int TIME_SHIFT_STATUS_UNAVAILABLE = 1;
+    public static final int TIME_SHIFT_STATUS_UNSUPPORTED = 1;
 
     /**
-     * An error occurred while handling a time shift request. To recover the status, tune to a
-     * new channel.
+     * Time shifting is currently not available but might work again later.
      */
-    public static final int TIME_SHIFT_STATUS_ERROR = TIME_SHIFT_STATUS_END;
+    public static final int TIME_SHIFT_STATUS_UNAVAILABLE = 2;
+
+    /**
+     * Time shifting is currently available. In this status, the application assumes it can
+     * pause/resume playback, seek to a specified time position and set playback rate and audio
+     * mode.
+     */
+    public static final int TIME_SHIFT_STATUS_AVAILABLE = TIME_SHIFT_STATUS_END;
 
     public static final long TIME_SHIFT_INVALID_TIME = Long.MIN_VALUE;
 
@@ -352,35 +358,41 @@
         }
 
         /**
-         * This is called when the trick play status is changed.
+         * This is called when the time shift status is changed.
          *
          * @param session A {@link TvInputManager.Session} associated with this callback.
-         * @param status The current time shift status:
+         * @param status The current time shift status. Should be one of the followings.
          * <ul>
-         * <li>{@link TvInputManager#TIME_SHIFT_STATUS_AVAILABLE}
+         * <li>{@link TvInputManager#TIME_SHIFT_STATUS_UNSUPPORTED}
          * <li>{@link TvInputManager#TIME_SHIFT_STATUS_UNAVAILABLE}
-         * <li>{@link TvInputManager#TIME_SHIFT_STATUS_ERROR}
+         * <li>{@link TvInputManager#TIME_SHIFT_STATUS_AVAILABLE}
          * </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.
+         * This is called when the start playback position is changed.
+         * <p>
+         * The start playback position of the time shifted program should be adjusted when the TV
+         * input cannot retain the whole recorded program due to some reason (e.g. limitation on
+         * storage space). This is necessary to prevent the application from allowing the user to
+         * seek to a time position that is not reachable.
+         * </p>
          *
          * @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.
+         * @param timeMs The start playback position of the time shifted program, in milliseconds
+         *            since the epoch.
          */
         public void onTimeShiftStartPositionChanged(Session session, long timeMs) {
         }
 
         /**
-         * This is called when the current position is changed.
+         * This is called when the current playback position is changed.
          *
          * @param session A {@link TvInputManager.Session} associated with this callback.
-         * @param timeMs The current position, in milliseconds since the epoch.
+         * @param timeMs The current playback position of the time shifted program, in milliseconds
+         *            since the epoch.
          */
         public void onTimeShiftCurrentPositionChanged(Session session, long timeMs) {
         }
@@ -893,7 +905,7 @@
                 }
             }
         } catch (RemoteException e) {
-            Log.e(TAG, "TvInputManager initialization failed: " + e);
+            Log.e(TAG, "TvInputManager initialization failed", e);
         }
     }
 
@@ -1677,12 +1689,13 @@
         }
 
         /**
-         * 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.
+         * Seeks to a specified time position.
+         * <p>
+         * Normally, the position is given within range between the start and the current time,
+         * inclusively.
          *
-         * @param timeMs The target time, in milliseconds since the epoch.
+         * @param timeMs The time position to seek to, in milliseconds since the epoch.
+         * @see TvView.TimeShiftPositionCallback#onTimeShiftStartPositionChanged
          */
         void timeShiftSeekTo(long timeMs) {
             if (mToken == null) {
@@ -1697,10 +1710,10 @@
         }
 
         /**
-         * Sets a playback rate and an audio mode.
+         * Sets playback rate and 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:
+         * @param audioMode Audio playback mode. Must be one of the supported audio modes:
          * <ul>
          * <li> {@link android.media.MediaPlayer#PLAYBACK_RATE_AUDIO_MODE_RESAMPLE}
          * </ul>
@@ -1710,6 +1723,9 @@
                 Log.w(TAG, "The session has been already released");
                 return;
             }
+            if (audioMode != MediaPlayer.PLAYBACK_RATE_AUDIO_MODE_RESAMPLE) {
+                throw new IllegalArgumentException("Unknown audio playback mode " + audioMode);
+            }
             try {
                 mService.timeShiftSetPlaybackRate(mToken, rate, audioMode, mUserId);
             } catch (RemoteException e) {
@@ -1718,15 +1734,17 @@
         }
 
         /**
-         * Returns the current playback position.
+         * Enable/disable position tracking.
+         *
+         * @param enable {@code true} to enable tracking, {@code false} otherwise.
          */
-        void timeShiftTrackCurrentPosition(boolean enabled) {
+        void timeShiftEnablePositionTracking(boolean enable) {
             if (mToken == null) {
                 Log.w(TAG, "The session has been already released");
                 return;
             }
             try {
-                mService.timeShiftTrackCurrentPosition(mToken, enabled, mUserId);
+                mService.timeShiftEnablePositionTracking(mToken, enable, mUserId);
             } catch (RemoteException e) {
                 throw new RuntimeException(e);
             }
diff --git a/media/java/android/media/tv/TvInputService.java b/media/java/android/media/tv/TvInputService.java
index 93abc2b..4e7aaa0 100644
--- a/media/java/android/media/tv/TvInputService.java
+++ b/media/java/android/media/tv/TvInputService.java
@@ -250,10 +250,10 @@
         private boolean mOverlayViewEnabled;
         private IBinder mWindowToken;
         private Rect mOverlayFrame;
+        private long mStartPositionMs;
         private long mCurrentPositionMs;
-        private final TimeShiftCurrentPositionTrackingRunnable
-                mTimeShiftCurrentPositionTrackingRunnable =
-                new TimeShiftCurrentPositionTrackingRunnable();
+        private final TimeShiftPositionTrackingRunnable
+                mTimeShiftPositionTrackingRunnable = new TimeShiftPositionTrackingRunnable();
 
         private final Object mLock = new Object();
         // @GuardedBy("mLock")
@@ -320,7 +320,7 @@
                             mSessionCallback.onSessionEvent(eventType, eventArgs);
                         }
                     } catch (RemoteException e) {
-                        Log.w(TAG, "error in sending event (event=" + eventType + ")");
+                        Log.w(TAG, "error in sending event (event=" + eventType + ")", e);
                     }
                 }
             });
@@ -341,7 +341,7 @@
                             mSessionCallback.onChannelRetuned(channelUri);
                         }
                     } catch (RemoteException e) {
-                        Log.w(TAG, "error in notifyChannelRetuned");
+                        Log.w(TAG, "error in notifyChannelRetuned", e);
                     }
                 }
             });
@@ -380,7 +380,7 @@
                             mSessionCallback.onTracksChanged(tracks);
                         }
                     } catch (RemoteException e) {
-                        Log.w(TAG, "error in notifyTracksChanged");
+                        Log.w(TAG, "error in notifyTracksChanged", e);
                     }
                 }
             });
@@ -410,7 +410,7 @@
                             mSessionCallback.onTrackSelected(type, trackId);
                         }
                     } catch (RemoteException e) {
-                        Log.w(TAG, "error in notifyTrackSelected");
+                        Log.w(TAG, "error in notifyTrackSelected", e);
                     }
                 }
             });
@@ -433,7 +433,7 @@
                             mSessionCallback.onVideoAvailable();
                         }
                     } catch (RemoteException e) {
-                        Log.w(TAG, "error in notifyVideoAvailable");
+                        Log.w(TAG, "error in notifyVideoAvailable", e);
                     }
                 }
             });
@@ -467,7 +467,7 @@
                             mSessionCallback.onVideoUnavailable(reason);
                         }
                     } catch (RemoteException e) {
-                        Log.w(TAG, "error in notifyVideoUnavailable");
+                        Log.w(TAG, "error in notifyVideoUnavailable", e);
                     }
                 }
             });
@@ -508,7 +508,7 @@
                             mSessionCallback.onContentAllowed();
                         }
                     } catch (RemoteException e) {
-                        Log.w(TAG, "error in notifyContentAllowed");
+                        Log.w(TAG, "error in notifyContentAllowed", e);
                     }
                 }
             });
@@ -550,30 +550,36 @@
                             mSessionCallback.onContentBlocked(rating.flattenToString());
                         }
                     } catch (RemoteException e) {
-                        Log.w(TAG, "error in notifyContentBlocked");
+                        Log.w(TAG, "error in notifyContentBlocked", e);
                     }
                 }
             });
         }
 
         /**
-         * Informs the application that the trick play status is changed.
+         * Informs the application that the time shift 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.
+         * Prior to calling this method, the application assumes the status
+         * {@link TvInputManager#TIME_SHIFT_STATUS_UNKNOWN}. Right after the session is created, it
+         * is important to invoke the method with the status
+         * {@link TvInputManager#TIME_SHIFT_STATUS_AVAILABLE} if the implementation does support
+         * time shifting, or {@link TvInputManager#TIME_SHIFT_STATUS_UNSUPPORTED} otherwise. Failure
+         * to notifying the current status change immediately might result in an undesirable
+         * behavior in the application such as hiding the play controls.
+         * </p><p>
+         * If the status {@link TvInputManager#TIME_SHIFT_STATUS_AVAILABLE} is reported, the
+         * application assumes it can pause/resume playback, seek to a specified time position and
+         * set playback rate and audio mode. The implementation should override
+         * {@link #onTimeShiftPause}, {@link #onTimeShiftResume}, {@link #onTimeShiftSeekTo},
+         * {@link #onTimeShiftGetStartPosition}, {@link #onTimeShiftGetCurrentPosition} and
+         * {@link #onTimeShiftSetPlaybackRate}.
          * </p>
          *
-         * @param status The current time shift status:
+         * @param status The current time shift status. Should be one of the followings.
          * <ul>
-         * <li>{@link TvInputManager#TIME_SHIFT_STATUS_AVAILABLE}
+         * <li>{@link TvInputManager#TIME_SHIFT_STATUS_UNSUPPORTED}
          * <li>{@link TvInputManager#TIME_SHIFT_STATUS_UNAVAILABLE}
-         * <li>{@link TvInputManager#TIME_SHIFT_STATUS_ERROR}
+         * <li>{@link TvInputManager#TIME_SHIFT_STATUS_AVAILABLE}
          * </ul>
          */
         public void notifyTimeShiftStatusChanged(final int status) {
@@ -586,23 +592,13 @@
                             mSessionCallback.onTimeShiftStatusChanged(status);
                         }
                     } catch (RemoteException e) {
-                        Log.w(TAG, "error in notifyTimeShiftStatusChanged");
+                        Log.w(TAG, "error in notifyTimeShiftStatusChanged", e);
                     }
                 }
             });
         }
 
-        /**
-         * 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) {
+        private void notifyTimeShiftStartPositionChanged(final long timeMs) {
             executeOrPostRunnable(new Runnable() {
                 @Override
                 public void run() {
@@ -612,17 +608,12 @@
                             mSessionCallback.onTimeShiftStartPositionChanged(timeMs);
                         }
                     } catch (RemoteException e) {
-                        Log.w(TAG, "error in notifyTimeShiftStartPositionChanged");
+                        Log.w(TAG, "error in notifyTimeShiftStartPositionChanged", e);
                     }
                 }
             });
         }
 
-        /**
-         * 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
@@ -633,7 +624,7 @@
                             mSessionCallback.onTimeShiftCurrentPositionChanged(timeMs);
                         }
                     } catch (RemoteException e) {
-                        Log.w(TAG, "error in notifyTimeShiftCurrentPositionChanged");
+                        Log.w(TAG, "error in notifyTimeShiftCurrentPositionChanged", e);
                     }
                 }
             });
@@ -666,7 +657,7 @@
                             mSessionCallback.onLayoutSurface(left, top, right, bottom);
                         }
                     } catch (RemoteException e) {
-                        Log.w(TAG, "error in layoutSurface");
+                        Log.w(TAG, "error in layoutSurface", e);
                     }
                 }
             });
@@ -711,7 +702,7 @@
          * When {@code setSurface(null)} is called, the implementation should stop using the Surface
          * object previously given and release any references to it.
          *
-         * @param surface possibly {@code null} {@link Surface} an application passes to this TV
+         * @param surface possibly {@code null} {@link Surface} the application passes to this TV
          *            input session.
          * @return {@code true} if the surface was set, {@code false} otherwise.
          */
@@ -730,10 +721,10 @@
         }
 
         /**
-         * Called when a size of an overlay view is changed by an application. Even when the overlay
-         * view is disabled by {@link #setOverlayViewEnabled}, this is called. The size is same as
-         * the size of {@link Surface} in general. Once {@link #layoutSurface} is called, the sizes
-         * of {@link Surface} and the overlay view can be different.
+         * Called when a size of an overlay view is changed by the application. Even when the
+         * overlay view is disabled by {@link #setOverlayViewEnabled}, this is called. The size is
+         * same as the size of {@link Surface} in general. Once {@link #layoutSurface} is called,
+         * the sizes of {@link Surface} and the overlay view can be different.
          *
          * @param width The width of the overlay view.
          * @param height The height of the overlay view.
@@ -836,7 +827,7 @@
         }
 
         /**
-         * Called when an application requests to create an overlay view. Each session
+         * Called when the application requests to create an overlay view. Each session
          * implementation can override this method and return its own view.
          *
          * @return a view attached to the overlay window
@@ -846,66 +837,94 @@
         }
 
         /**
-         * Called when an application requests to pause the playback.
+         * Called when the application requests to pause playback.
          *
-         * @see #onTimeShiftResume()
-         * @see #onTimeShiftSeekTo(long)
-         * @see #onTimeShiftSetPlaybackRate(float, int)
-         * @see #onTimeShiftGetCurrentPosition()
+         * @see #onTimeShiftResume
+         * @see #onTimeShiftSeekTo
+         * @see #onTimeShiftSetPlaybackRate
+         * @see #onTimeShiftGetStartPosition
+         * @see #onTimeShiftGetCurrentPosition
          */
         public void onTimeShiftPause() {
         }
 
         /**
-         * Called when an application requests to resume the playback.
+         * Called when the application requests to resume playback.
          *
-         * @see #onTimeShiftPause()
-         * @see #onTimeShiftSeekTo(long)
-         * @see #onTimeShiftSetPlaybackRate(float, int)
-         * @see #onTimeShiftGetCurrentPosition()
+         * @see #onTimeShiftPause
+         * @see #onTimeShiftSeekTo
+         * @see #onTimeShiftSetPlaybackRate
+         * @see #onTimeShiftGetStartPosition
+         * @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.
+         * Called when the application requests to seek to a specified time position. Normally, the
+         * position is given within range between the start and the current time, inclusively. The
+         * implementation is expected to seek to the nearest time position if the given position is
+         * not in the range.
          *
-         * @param timeMs The target time, in milliseconds since the epoch
-         * @see #onTimeShiftResume()
-         * @see #onTimeShiftPause()
-         * @see #onTimeShiftSetPlaybackRate(float, int)
-         * @see #onTimeShiftGetCurrentPosition()
+         * @param timeMs The time position to seek to, in milliseconds since the epoch.
+         * @see #onTimeShiftResume
+         * @see #onTimeShiftPause
+         * @see #onTimeShiftSetPlaybackRate
+         * @see #onTimeShiftGetStartPosition
+         * @see #onTimeShiftGetCurrentPosition
          */
         public void onTimeShiftSeekTo(long timeMs) {
         }
 
         /**
-         * Called when an application sets a playback rate and an audio mode.
+         * Called when the application sets playback rate and 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:
+         * @param audioMode 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()
+         * @see #onTimeShiftResume
+         * @see #onTimeShiftPause
+         * @see #onTimeShiftSeekTo
+         * @see #onTimeShiftGetStartPosition
+         * @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.
+         * Returns the start playback position for time shifting, in milliseconds since the epoch.
+         * Returns {@link TvInputManager#TIME_SHIFT_INVALID_TIME} if the position is unknown at the
+         * moment.
+         * <p>
+         * The start playback position of the time shifted program should be adjusted when the
+         * implementation cannot retain the whole recorded program due to some reason (e.g.
+         * limitation on storage space). It is the earliest possible time position that the user can
+         * seek to, thus failure to notifying its change immediately might result in bad experience
+         * where the application allows the user to seek to an invalid time position.
+         * </p>
          *
-         * @see #onTimeShiftResume()
-         * @see #onTimeShiftPause()
-         * @see #onTimeShiftSeekTo(long)
-         * @see #onTimeShiftSetPlaybackRate(float, int)
+         * @see #onTimeShiftResume
+         * @see #onTimeShiftPause
+         * @see #onTimeShiftSeekTo
+         * @see #onTimeShiftSetPlaybackRate
+         * @see #onTimeShiftGetCurrentPosition
+         */
+        public long onTimeShiftGetStartPosition() {
+            return TvInputManager.TIME_SHIFT_INVALID_TIME;
+        }
+
+        /**
+         * Returns the current playback position for time shifting, in milliseconds since the epoch.
+         * Returns {@link TvInputManager#TIME_SHIFT_INVALID_TIME} if the position is unknown at the
+         * moment.
+         *
+         * @see #onTimeShiftResume
+         * @see #onTimeShiftPause
+         * @see #onTimeShiftSeekTo
+         * @see #onTimeShiftSetPlaybackRate
+         * @see #onTimeShiftGetStartPosition
          */
         public long onTimeShiftGetCurrentPosition() {
             return TvInputManager.TIME_SHIFT_INVALID_TIME;
@@ -1043,7 +1062,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);
+            mHandler.removeCallbacks(mTimeShiftPositionTrackingRunnable);
         }
 
         /**
@@ -1125,7 +1144,7 @@
          * Creates an overlay view. This calls {@link #onCreateOverlayView} to get a view to attach
          * to the overlay window.
          *
-         * @param windowToken A window token of an application.
+         * @param windowToken A window token of the application.
          * @param frame A position of the overlay view.
          */
         void createOverlayView(IBinder windowToken, Rect frame) {
@@ -1245,13 +1264,16 @@
         }
 
         /**
-         * Turns on/off the current position tracking.
+         * Enable/disable position tracking.
+         *
+         * @param enable {@code true} to enable tracking, {@code false} otherwise.
          */
-        void timeShiftTrackCurrentPosition(boolean enabled) {
-            if (enabled) {
-                mHandler.post(mTimeShiftCurrentPositionTrackingRunnable);
+        void timeShiftEnablePositionTracking(boolean enable) {
+            if (enable) {
+                mHandler.post(mTimeShiftPositionTrackingRunnable);
             } else {
-                mHandler.removeCallbacks(mTimeShiftCurrentPositionTrackingRunnable);
+                mHandler.removeCallbacks(mTimeShiftPositionTrackingRunnable);
+                mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME;
                 mCurrentPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME;
             }
         }
@@ -1283,7 +1305,7 @@
                 }
                 isNavigationKey = isNavigationKey(keyEvent.getKeyCode());
                 // When media keys and KEYCODE_MEDIA_AUDIO_TRACK are dispatched to ViewRootImpl,
-                // ViewRootImpl always consumes the keys. In this case, an application loses
+                // ViewRootImpl always consumes the keys. In this case, the application loses
                 // a chance to handle media keys. Therefore, media keys are not dispatched to
                 // ViewRootImpl.
                 skipDispatchToOverlayView = KeyEvent.isMediaKey(keyEvent.getKeyCode())
@@ -1352,16 +1374,21 @@
             }
         }
 
-        private final class TimeShiftCurrentPositionTrackingRunnable implements Runnable {
+        private final class TimeShiftPositionTrackingRunnable implements Runnable {
             @Override
             public void run() {
-                long pos = onTimeShiftGetCurrentPosition();
-                if (mCurrentPositionMs != pos) {
-                    mCurrentPositionMs = pos;
-                    notifyTimeShiftCurrentPositionChanged(pos);
+                long startPositionMs = onTimeShiftGetStartPosition();
+                if (mStartPositionMs != startPositionMs) {
+                    mStartPositionMs = startPositionMs;
+                    notifyTimeShiftStartPositionChanged(startPositionMs);
                 }
-                mHandler.removeCallbacks(mTimeShiftCurrentPositionTrackingRunnable);
-                mHandler.postDelayed(mTimeShiftCurrentPositionTrackingRunnable,
+                long currentPositionMs = onTimeShiftGetCurrentPosition();
+                if (mCurrentPositionMs != currentPositionMs) {
+                    mCurrentPositionMs = currentPositionMs;
+                    notifyTimeShiftCurrentPositionChanged(currentPositionMs);
+                }
+                mHandler.removeCallbacks(mTimeShiftPositionTrackingRunnable);
+                mHandler.postDelayed(mTimeShiftPositionTrackingRunnable,
                         POSITION_UPDATE_INTERVAL_MS);
             }
         }
@@ -1538,7 +1565,7 @@
                 try {
                     mCallbacks.getBroadcastItem(i).addHardwareTvInput(deviceId, inputInfo);
                 } catch (RemoteException e) {
-                    Log.e(TAG, "Error while broadcasting.", e);
+                    Log.e(TAG, "error in broadcastAddHardwareTvInput", e);
                 }
             }
             mCallbacks.finishBroadcast();
@@ -1550,7 +1577,7 @@
                 try {
                     mCallbacks.getBroadcastItem(i).addHdmiTvInput(id, inputInfo);
                 } catch (RemoteException e) {
-                    Log.e(TAG, "Error while broadcasting.", e);
+                    Log.e(TAG, "error in broadcastAddHdmiTvInput", e);
                 }
             }
             mCallbacks.finishBroadcast();
@@ -1562,7 +1589,7 @@
                 try {
                     mCallbacks.getBroadcastItem(i).removeTvInput(inputId);
                 } catch (RemoteException e) {
-                    Log.e(TAG, "Error while broadcasting.", e);
+                    Log.e(TAG, "error in broadcastRemoveTvInput", e);
                 }
             }
             mCallbacks.finishBroadcast();
@@ -1583,7 +1610,7 @@
                             // Failed to create a session.
                             cb.onSessionCreated(null, null);
                         } catch (RemoteException e) {
-                            Log.e(TAG, "error in onSessionCreated");
+                            Log.e(TAG, "error in onSessionCreated", e);
                         }
                         return;
                     }
@@ -1604,7 +1631,7 @@
                             try {
                                 cb.onSessionCreated(null, null);
                             } catch (RemoteException e) {
-                                Log.e(TAG, "error in onSessionCreated");
+                                Log.e(TAG, "error in onSessionCreated", e);
                             }
                             return;
                         }
@@ -1635,7 +1662,7 @@
                     try {
                         cb.onSessionCreated(stub, hardwareSessionToken);
                     } catch (RemoteException e) {
-                        Log.e(TAG, "error in onSessionCreated");
+                        Log.e(TAG, "error in onSessionCreated", e);
                     }
                     if (sessionImpl != null) {
                         sessionImpl.initialize(cb);
diff --git a/media/java/android/media/tv/TvView.java b/media/java/android/media/tv/TvView.java
index 115d094..42c2cd7 100644
--- a/media/java/android/media/tv/TvView.java
+++ b/media/java/android/media/tv/TvView.java
@@ -42,7 +42,6 @@
 import android.view.ViewRootImpl;
 
 import java.lang.ref.WeakReference;
-import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -104,7 +103,7 @@
     private int mSurfaceViewRight;
     private int mSurfaceViewTop;
     private int mSurfaceViewBottom;
-    private List<TimeShiftPositionCallback> mTimeShiftPositionCallbacks = new ArrayList<>();
+    private TimeShiftPositionCallback mTimeShiftPositionCallback;
 
     private final SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() {
         @Override
@@ -175,8 +174,8 @@
     /**
      * Sets the callback to be invoked when an event is dispatched to this TvView.
      *
-     * @param callback The callback to receive events. A value of {@code null} removes any existing
-     *            callbacks.
+     * @param callback The callback to receive events. A value of {@code null} removes the existing
+     *            callback.
      */
     public void setCallback(TvInputCallback callback) {
         mCallback = callback;
@@ -422,7 +421,7 @@
     }
 
     /**
-     * Pauses the playback. Call {@link #timeShiftResume()} to restart the playback.
+     * Pauses playback. No-op if it is already paused. Call {@link #timeShiftResume} to resume.
      */
     public void timeShiftPause() {
         if (mSession != null) {
@@ -431,7 +430,7 @@
     }
 
     /**
-     * Resumes the playback. No-op if it is already playing the channel.
+     * Resumes playback. No-op if it is already resumed. Call {@link #timeShiftPause} to pause.
      */
     public void timeShiftResume() {
         if (mSession != null) {
@@ -440,11 +439,11 @@
     }
 
     /**
-     * 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.
+     * Seeks to a specified time position. {@code timeMs} must be equal to or greater than the start
+     * position returned by {@link TimeShiftPositionCallback#onTimeShiftStartPositionChanged} and
+     * equal to or less than the current time.
      *
-     * @param timeMs The target time, in milliseconds since the epoch.
+     * @param timeMs The time position to seek to, in milliseconds since the epoch.
      */
     public void timeShiftSeekTo(long timeMs) {
         if (mSession != null) {
@@ -453,10 +452,10 @@
     }
 
     /**
-     * Sets a playback rate and an audio mode.
+     * Sets playback rate and 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:
+     * @param audioMode Audio playback mode. Must be one of the supported audio modes:
      * <ul>
      * <li> {@link android.media.MediaPlayer#PLAYBACK_RATE_AUDIO_MODE_RESAMPLE}
      * </ul>
@@ -468,36 +467,21 @@
     }
 
     /**
-     * Registers a {@link TvView.TimeShiftPositionCallback}.
+     * Sets the callback to be invoked when the time shift position is changed.
      *
-     * @param callback A callback used to monitor the time shift range and current position.
+     * @param callback The callback to receive time shift position changes. A value of {@code null}
+     *            removes the existing callback.
      */
-    public void registerTimeShiftPositionCallback(TimeShiftPositionCallback callback) {
-        if (callback == null) {
-            throw new IllegalArgumentException("callback can not be null.");
-        }
-        mTimeShiftPositionCallbacks.add(callback);
-        ensureCurrentPositionTracking();
+    public void setTimeShiftPositionCallback(TimeShiftPositionCallback callback) {
+        mTimeShiftPositionCallback = callback;
+        ensurePositionTracking();
     }
 
-    /**
-     * 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() {
+    private void ensurePositionTracking() {
         if (mSession == null) {
             return;
         }
-        mSession.timeShiftTrackCurrentPosition(!mTimeShiftPositionCallbacks.isEmpty());
+        mSession.timeShiftEnablePositionTracking(mTimeShiftPositionCallback != null);
     }
 
     /**
@@ -810,17 +794,22 @@
     }
 
     /**
-     * Callback used to receive the information on the possible range for time shifting and currrent
-     * position.
+     * Callback used to receive time shift position changes.
      */
     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.
+         * This is called when the start playback position is changed.
+         * <p>
+         * The start playback position of the time shifted program can be adjusted by the TV input
+         * when it cannot retain the whole recorded program due to some reason (e.g. limitation on
+         * storage space). The application should not allow the user to seek to a position earlier
+         * than the start position.
+         * </p>
          *
          * @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.
+         * @param timeMs The start playback position of the time shifted program, in milliseconds
+         *            since the epoch.
          */
         public void onTimeShiftStartPositionChanged(String inputId, long timeMs) {
         }
@@ -829,7 +818,8 @@
          * 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.
+         * @param timeMs The current playback position of the time shifted program, in milliseconds
+         *            since the epoch.
          */
         public void onTimeShiftCurrentPositionChanged(String inputId, long timeMs) {
         }
@@ -958,11 +948,11 @@
          * 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:
+         * @param status The current time shift status. Should be one of the followings.
          * <ul>
-         * <li>{@link TvInputManager#TIME_SHIFT_STATUS_AVAILABLE}
+         * <li>{@link TvInputManager#TIME_SHIFT_STATUS_UNSUPPORTED}
          * <li>{@link TvInputManager#TIME_SHIFT_STATUS_UNAVAILABLE}
-         * <li>{@link TvInputManager#TIME_SHIFT_STATUS_ERROR}
+         * <li>{@link TvInputManager#TIME_SHIFT_STATUS_AVAILABLE}
          * </ul>
          */
         public void onTimeShiftStatusChanged(String inputId, int status) {
@@ -1040,7 +1030,7 @@
                     mAppPrivateCommandAction = null;
                     mAppPrivateCommandData = null;
                 }
-                ensureCurrentPositionTracking();
+                ensurePositionTracking();
             } else {
                 mSessionCallback = null;
                 if (mCallback != null) {
@@ -1234,8 +1224,8 @@
                 Log.w(TAG, "onTimeShiftStartPositionChanged - session not created");
                 return;
             }
-            for (TimeShiftPositionCallback callback : mTimeShiftPositionCallbacks) {
-                callback.onTimeShiftStartPositionChanged(mInputId, timeMs);
+            if (mTimeShiftPositionCallback != null) {
+                mTimeShiftPositionCallback.onTimeShiftStartPositionChanged(mInputId, timeMs);
             }
         }
 
@@ -1248,8 +1238,8 @@
                 Log.w(TAG, "onTimeShiftCurrentPositionChanged - session not created");
                 return;
             }
-            for (TimeShiftPositionCallback callback : mTimeShiftPositionCallbacks) {
-                callback.onTimeShiftCurrentPositionChanged(mInputId, timeMs);
+            if (mTimeShiftPositionCallback != null) {
+                mTimeShiftPositionCallback.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 152370a..9b4b522 100644
--- a/services/core/java/com/android/server/tv/TvInputManagerService.java
+++ b/services/core/java/com/android/server/tv/TvInputManagerService.java
@@ -1422,19 +1422,19 @@
         }
 
         @Override
-        public void timeShiftTrackCurrentPosition(IBinder sessionToken, boolean enabled,
+        public void timeShiftEnablePositionTracking(IBinder sessionToken, boolean enable,
                 int userId) {
             final int callingUid = Binder.getCallingUid();
             final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
-                    userId, "timeShiftTrackCurrentPosition");
+                    userId, "timeShiftEnablePositionTracking");
             final long identity = Binder.clearCallingIdentity();
             try {
                 synchronized (mLock) {
                     try {
                         getSessionLocked(sessionToken, callingUid, resolvedUserId)
-                                .timeShiftTrackCurrentPosition(enabled);
+                                .timeShiftEnablePositionTracking(enable);
                     } catch (RemoteException | SessionNotFoundException e) {
-                        Slog.e(TAG, "error in timeShiftTrackCurrentPosition", e);
+                        Slog.e(TAG, "error in timeShiftEnablePositionTracking", e);
                     }
                 }
             } finally {