diff --git a/media/java/android/media/tv/TvContract.java b/media/java/android/media/tv/TvContract.java
index 97a6b8357d475..e975676c811ec 100644
--- a/media/java/android/media/tv/TvContract.java
+++ b/media/java/android/media/tv/TvContract.java
@@ -21,6 +21,7 @@ import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.net.Uri;
+import android.os.IBinder;
import android.provider.BaseColumns;
import android.util.ArraySet;
@@ -795,7 +796,7 @@ public final class TvContract {
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/program";
/**
- * The ID of the TV channel that contains this TV program.
+ * The ID of the TV channel that provides this TV program.
*
* This is a part of the channel URI and matches to {@link BaseColumns#_ID}.
*
@@ -1109,6 +1110,7 @@ public final class TvContract {
* to this table.
* @hide
*/
+ @SystemApi
public static final class WatchedPrograms implements BaseTvColumns {
/** The content:// style URI for this table. */
@@ -1141,7 +1143,7 @@ public final class TvContract {
public static final String COLUMN_WATCH_END_TIME_UTC_MILLIS = "watch_end_time_utc_millis";
/**
- * The channel ID that contains this TV program.
+ * The ID of the TV channel that provides this TV program.
*
* Type: INTEGER (long)
*
@@ -1181,17 +1183,30 @@ public final class TvContract {
public static final String COLUMN_DESCRIPTION = "description";
/**
- * Extra parameters of the tune operation.
+ * Extra parameters given to {@link TvInputService.Session#tune(Uri, android.os.Bundle)
+ * TvInputService.Session.tune(Uri, android.os.Bundle)} when tuning to the channel that
+ * provides this TV program. (Used internally.)
*
- * This column contains an encoded string which is comma-separated key-value pairs.
- * (Ex. "[key1]=[value1], [key2]=[value2]"). COLUMN_TUNE_PARAMS will use '%' as an escape
- * character for the characters of '%', '=', and ','.
+ * This column contains an encoded string that represents comma-separated key-value pairs of
+ * the tune parameters. (Ex. "[key1]=[value1], [key2]=[value2]"). '%' is used as an escape
+ * character for '%', '=', and ','.
*
* Type: TEXT
*
- * @see TvInputManager.Session.tune(Uri, Bundle)
*/
- public static final String COLUMN_TUNE_PARAMS = "tune_params";
+ public static final String COLUMN_INTERNAL_TUNE_PARAMS = "tune_params";
+
+ /**
+ * The session token of this TV program. (Used internally.)
+ *
+ * This contains a String representation of {@link IBinder} for
+ * {@link TvInputService.Session} that provides the current TV program. It is used
+ * internally to distinguish watched programs entries from different TV input sessions.
+ *
+ * Type: TEXT
+ *
+ */
+ public static final String COLUMN_INTERNAL_SESSION_TOKEN = "session_token";
private WatchedPrograms() {}
}
diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java
index 9c2749810ff57..dfa7e2adc5ac7 100644
--- a/services/core/java/com/android/server/tv/TvInputManagerService.java
+++ b/services/core/java/com/android/server/tv/TvInputManagerService.java
@@ -35,7 +35,6 @@ import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
-import android.database.Cursor;
import android.graphics.Rect;
import android.hardware.hdmi.HdmiDeviceInfo;
import android.media.tv.ITvInputClient;
@@ -108,14 +107,14 @@ public final class TvInputManagerService extends SystemService {
// A map from user id to UserState.
private final SparseArray mUserStates = new SparseArray();
- private final Handler mLogHandler;
+ private final WatchLogHandler mWatchLogHandler;
public TvInputManagerService(Context context) {
super(context);
mContext = context;
mContentResolver = context.getContentResolver();
- mLogHandler = new LogHandler(IoThread.get().getLooper());
+ mWatchLogHandler = new WatchLogHandler(IoThread.get().getLooper());
mTvInputHardwareManager = new TvInputHardwareManager(context, new HardwareListener());
@@ -717,14 +716,6 @@ public final class TvInputManagerService extends SystemService {
return;
}
- // Close the open log entry, if any.
- if (sessionState.mLogUri != null) {
- SomeArgs args = SomeArgs.obtain();
- args.arg1 = sessionState.mLogUri;
- args.arg2 = System.currentTimeMillis();
- mLogHandler.obtainMessage(LogHandler.MSG_CLOSE_ENTRY, args).sendToTarget();
- }
-
// Also remove the session token from the session token list of the current client and
// service.
ClientState clientState = userState.clientStateMap.get(sessionState.mClient.asBinder());
@@ -743,6 +734,12 @@ public final class TvInputManagerService extends SystemService {
}
}
updateServiceConnectionLocked(sessionState.mInfo.getComponent(), userId);
+
+ // Log the end of watch.
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = sessionToken;
+ args.arg2 = System.currentTimeMillis();
+ mWatchLogHandler.obtainMessage(WatchLogHandler.MSG_LOG_WATCH_END, args).sendToTarget();
}
private void notifyInputAddedLocked(UserState userState, String inputId) {
@@ -1231,41 +1228,19 @@ public final class TvInputManagerService extends SystemService {
// Do not log the watch history for passthrough inputs.
return;
}
- long currentTime = System.currentTimeMillis();
- long channelId = ContentUris.parseId(channelUri);
- // Close the open log entry first, if any.
UserState userState = getUserStateLocked(resolvedUserId);
SessionState sessionState = userState.sessionStateMap.get(sessionToken);
- if (sessionState.mLogUri != null) {
- SomeArgs args = SomeArgs.obtain();
- args.arg1 = sessionState.mLogUri;
- args.arg2 = currentTime;
- mLogHandler.obtainMessage(LogHandler.MSG_CLOSE_ENTRY, args)
- .sendToTarget();
- }
- // Create a log entry and fill it later.
- String packageName = sessionState.mInfo.getServiceInfo().packageName;
- ContentValues values = new ContentValues();
- values.put(TvContract.WatchedPrograms.COLUMN_PACKAGE_NAME, packageName);
- values.put(TvContract.WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS,
- currentTime);
- values.put(TvContract.WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS, 0);
- values.put(TvContract.WatchedPrograms.COLUMN_CHANNEL_ID, channelId);
- if (params != null) {
- values.put(TvContract.WatchedPrograms.COLUMN_TUNE_PARAMS,
- encodeTuneParams(params));
- }
-
- sessionState.mLogUri = mContentResolver.insert(
- TvContract.WatchedPrograms.CONTENT_URI, values);
+ // Log the start of watch.
SomeArgs args = SomeArgs.obtain();
- args.arg1 = sessionState.mLogUri;
- args.arg2 = ContentUris.parseId(channelUri);
- args.arg3 = currentTime;
- args.arg4 = sessionState;
- mLogHandler.obtainMessage(LogHandler.MSG_OPEN_ENTRY, args).sendToTarget();
+ args.arg1 = sessionState.mInfo.getComponent().getPackageName();
+ args.arg2 = System.currentTimeMillis();
+ args.arg3 = ContentUris.parseId(channelUri);
+ args.arg4 = params;
+ args.arg5 = sessionToken;
+ mWatchLogHandler.obtainMessage(WatchLogHandler.MSG_LOG_WATCH_START, args)
+ .sendToTarget();
} catch (RemoteException e) {
Slog.e(TAG, "error in tune", e);
return;
@@ -1691,39 +1666,6 @@ public final class TvInputManagerService extends SystemService {
}
}
}
-
- private String encodeTuneParams(Bundle tuneParams) {
- StringBuilder builder = new StringBuilder();
- Set keySet = tuneParams.keySet();
- Iterator it = keySet.iterator();
- while (it.hasNext()) {
- String key = it.next();
- Object value = tuneParams.get(key);
- if (value == null) {
- continue;
- }
- builder.append(replaceEscapeCharacters(key));
- builder.append("=");
- builder.append(replaceEscapeCharacters(value.toString()));
- if (it.hasNext()) {
- builder.append(", ");
- }
- }
- return builder.toString();
- }
-
- private String replaceEscapeCharacters(String src) {
- final char ESCAPE_CHARACTER = '%';
- final String ENCODING_TARGET_CHARACTERS = "%=,";
- StringBuilder builder = new StringBuilder();
- for (char ch : src.toCharArray()) {
- if (ENCODING_TARGET_CHARACTERS.indexOf(ch) >= 0) {
- builder.append(ESCAPE_CHARACTER);
- }
- builder.append(ch);
- }
- return builder.toString();
- }
}
private static final class TvInputState {
@@ -2062,43 +2004,61 @@ public final class TvInputManagerService extends SystemService {
}
}
- private final class LogHandler extends Handler {
- private static final int MSG_OPEN_ENTRY = 1;
- private static final int MSG_UPDATE_ENTRY = 2;
- private static final int MSG_CLOSE_ENTRY = 3;
+ private final class WatchLogHandler extends Handler {
+ // There are only two kinds of watch events that can happen on the system:
+ // 1. The current TV input session is tuned to a new channel.
+ // 2. The session is released for some reason.
+ // The former indicates the end of the previous log entry, if any, followed by the start of
+ // a new entry. The latter indicates the end of the most recent entry for the given session.
+ // Here the system supplies the database the smallest set of information only that is
+ // sufficient to consolidate the log entries while minimizing database operations in the
+ // system service.
+ private static final int MSG_LOG_WATCH_START = 1;
+ private static final int MSG_LOG_WATCH_END = 2;
- public LogHandler(Looper looper) {
+ public WatchLogHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
- case MSG_OPEN_ENTRY: {
+ case MSG_LOG_WATCH_START: {
SomeArgs args = (SomeArgs) msg.obj;
- Uri uri = (Uri) args.arg1;
- long channelId = (long) args.arg2;
- long time = (long) args.arg3;
- SessionState sessionState = (SessionState) args.arg4;
- onOpenEntry(uri, channelId, time, sessionState);
+ String packageName = (String) args.arg1;
+ long watchStartTime = (long) args.arg2;
+ long channelId = (long) args.arg3;
+ Bundle tuneParams = (Bundle) args.arg4;
+ IBinder sessionToken = (IBinder) args.arg5;
+
+ ContentValues values = new ContentValues();
+ values.put(TvContract.WatchedPrograms.COLUMN_PACKAGE_NAME, packageName);
+ values.put(TvContract.WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS,
+ watchStartTime);
+ values.put(TvContract.WatchedPrograms.COLUMN_CHANNEL_ID, channelId);
+ if (tuneParams != null) {
+ values.put(TvContract.WatchedPrograms.COLUMN_INTERNAL_TUNE_PARAMS,
+ encodeTuneParams(tuneParams));
+ }
+ values.put(TvContract.WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN,
+ sessionToken.toString());
+
+ mContentResolver.insert(TvContract.WatchedPrograms.CONTENT_URI, values);
args.recycle();
return;
}
- case MSG_UPDATE_ENTRY: {
+ case MSG_LOG_WATCH_END: {
SomeArgs args = (SomeArgs) msg.obj;
- Uri uri = (Uri) args.arg1;
- long channelId = (long) args.arg2;
- long time = (long) args.arg3;
- SessionState sessionState = (SessionState) args.arg4;
- onUpdateEntry(uri, channelId, time, sessionState);
- args.recycle();
- return;
- }
- case MSG_CLOSE_ENTRY: {
- SomeArgs args = (SomeArgs) msg.obj;
- Uri uri = (Uri) args.arg1;
- long time = (long) args.arg2;
- onCloseEntry(uri, time);
+ IBinder sessionToken = (IBinder) args.arg1;
+ long watchEndTime = (long) args.arg2;
+
+ ContentValues values = new ContentValues();
+ values.put(TvContract.WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS,
+ watchEndTime);
+ values.put(TvContract.WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN,
+ sessionToken.toString());
+
+ mContentResolver.insert(TvContract.WatchedPrograms.CONTENT_URI, values);
args.recycle();
return;
}
@@ -2109,148 +2069,37 @@ public final class TvInputManagerService extends SystemService {
}
}
- private void onOpenEntry(Uri logUri, long channelId, long watchStarttime,
- SessionState sessionState) {
- if (!isChannelSearchable(channelId)) {
- // Do not log anything about non-searchable channels.
- synchronized (mLock) {
- sessionState.mLogUri = null;
+ private String encodeTuneParams(Bundle tuneParams) {
+ StringBuilder builder = new StringBuilder();
+ Set keySet = tuneParams.keySet();
+ Iterator it = keySet.iterator();
+ while (it.hasNext()) {
+ String key = it.next();
+ Object value = tuneParams.get(key);
+ if (value == null) {
+ continue;
}
- mContentResolver.delete(logUri, null, null);
- return;
- }
-
- String[] projection = {
- TvContract.Programs.COLUMN_TITLE,
- TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS,
- TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS,
- TvContract.Programs.COLUMN_SHORT_DESCRIPTION
- };
- String selection = TvContract.Programs.COLUMN_CHANNEL_ID + "=? AND "
- + TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS + "<=? AND "
- + TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS + ">?";
- String[] selectionArgs = {
- String.valueOf(channelId),
- String.valueOf(watchStarttime),
- String.valueOf(watchStarttime)
- };
- String sortOrder = TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS + " ASC";
- Cursor cursor = null;
- try {
- cursor = mContentResolver.query(TvContract.Programs.CONTENT_URI, projection,
- selection, selectionArgs, sortOrder);
- if (cursor != null && cursor.moveToNext()) {
- ContentValues values = new ContentValues();
- values.put(TvContract.WatchedPrograms.COLUMN_TITLE, cursor.getString(0));
- values.put(TvContract.WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS,
- cursor.getLong(1));
- long endTime = cursor.getLong(2);
- values.put(TvContract.WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS, endTime);
- values.put(TvContract.WatchedPrograms.COLUMN_DESCRIPTION, cursor.getString(3));
- mContentResolver.update(logUri, values, null, null);
-
- // Schedule an update when the current program ends.
- SomeArgs args = SomeArgs.obtain();
- args.arg1 = logUri;
- args.arg2 = channelId;
- args.arg3 = endTime;
- args.arg4 = sessionState;
- Message msg = obtainMessage(LogHandler.MSG_UPDATE_ENTRY, args);
- sendMessageDelayed(msg, endTime - System.currentTimeMillis());
- }
- } finally {
- if (cursor != null) {
- cursor.close();
+ builder.append(replaceEscapeCharacters(key));
+ builder.append("=");
+ builder.append(replaceEscapeCharacters(value.toString()));
+ if (it.hasNext()) {
+ builder.append(", ");
}
}
+ return builder.toString();
}
- private void onUpdateEntry(Uri uri, long channelId, long time, SessionState sessionState) {
- String[] projection = {
- TvContract.WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS
- };
- Cursor cursor = null;
- try {
- cursor = mContentResolver.query(uri, projection, null, null, null);
- if (cursor != null && cursor.moveToNext()) {
- long watchEndTime = cursor.getLong(0);
- // Do nothing if the current log entry is already closed.
- if (watchEndTime > 0) {
- return;
- }
-
- // Update the watch end time for the current log entry.
- ContentValues values = new ContentValues();
- values.put(TvContract.WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS, time);
- int c = mContentResolver.update(uri, values, null, null);
- } else {
- // The record has been deleted.
- synchronized (mLock) {
- if (!uri.equals(sessionState.mLogUri)) {
- // If the deleted record is not for the current channel, do not re-open
- // a log entry for the next program.
- return;
- }
- }
- }
- if (cursor != null) {
- cursor.close();
- cursor = null;
- }
-
- // The current program has just ended. Create a new log entry for the next program.
- uri = ContentUris.withAppendedId(TvContract.Channels.CONTENT_URI, channelId);
- projection = new String[] {
- TvContract.Channels.COLUMN_PACKAGE_NAME
- };
- cursor = mContentResolver.query(uri, projection, null, null, null);
- if (cursor != null && cursor.moveToNext()) {
- ContentValues values = new ContentValues();
- values.put(TvContract.WatchedPrograms.COLUMN_PACKAGE_NAME, cursor.getString(0));
- values.put(TvContract.WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS, time);
- values.put(TvContract.WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS, 0);
- values.put(TvContract.WatchedPrograms.COLUMN_CHANNEL_ID, channelId);
- Uri newUri = mContentResolver.insert(TvContract.WatchedPrograms.CONTENT_URI,
- values);
-
- synchronized (mLock) {
- sessionState.mLogUri = newUri;
- }
-
- // Re-open the current log entry with the next program information.
- onOpenEntry(newUri, channelId, time, sessionState);
- }
- } finally {
- if (cursor != null) {
- cursor.close();
+ private String replaceEscapeCharacters(String src) {
+ final char ESCAPE_CHARACTER = '%';
+ final String ENCODING_TARGET_CHARACTERS = "%=,";
+ StringBuilder builder = new StringBuilder();
+ for (char ch : src.toCharArray()) {
+ if (ENCODING_TARGET_CHARACTERS.indexOf(ch) >= 0) {
+ builder.append(ESCAPE_CHARACTER);
}
+ builder.append(ch);
}
- }
-
- private void onCloseEntry(Uri uri, long watchEndTime) {
- ContentValues values = new ContentValues();
- values.put(TvContract.WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS, watchEndTime);
- mContentResolver.update(uri, values, null, null);
- }
-
- private boolean isChannelSearchable(long channelId) {
- String[] projection = { TvContract.Channels.COLUMN_SEARCHABLE };
- String selection = TvContract.Channels._ID + "=?";
- String[] selectionArgs = { String.valueOf(channelId) };
- Cursor cursor = null;
- try {
- cursor = mContentResolver.query(TvContract.Channels.CONTENT_URI, projection,
- selection, selectionArgs, null);
- if (cursor != null && cursor.moveToNext()) {
- return cursor.getLong(0) == 1 ? true : false;
- }
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
- // Unless explicitly specified non-searchable, by default the channel is searchable.
- return true;
+ return builder.toString();
}
}