Loading media/java/android/media/tv/TvContract.java +23 −8 Original line number Original line Diff line number Diff line Loading @@ -21,6 +21,7 @@ import android.content.ComponentName; import android.content.ContentResolver; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentUris; import android.net.Uri; import android.net.Uri; import android.os.IBinder; import android.provider.BaseColumns; import android.provider.BaseColumns; import android.util.ArraySet; import android.util.ArraySet; Loading Loading @@ -795,7 +796,7 @@ public final class TvContract { public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/program"; 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. * <p> * <p> * This is a part of the channel URI and matches to {@link BaseColumns#_ID}. * This is a part of the channel URI and matches to {@link BaseColumns#_ID}. * </p><p> * </p><p> Loading Loading @@ -1109,6 +1110,7 @@ public final class TvContract { * to this table. * to this table. * @hide * @hide */ */ @SystemApi public static final class WatchedPrograms implements BaseTvColumns { public static final class WatchedPrograms implements BaseTvColumns { /** The content:// style URI for this table. */ /** The content:// style URI for this table. */ Loading Loading @@ -1141,7 +1143,7 @@ public final class TvContract { public static final String COLUMN_WATCH_END_TIME_UTC_MILLIS = "watch_end_time_utc_millis"; 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. * <p> * <p> * Type: INTEGER (long) * Type: INTEGER (long) * </p> * </p> Loading Loading @@ -1181,17 +1183,30 @@ public final class TvContract { public static final String COLUMN_DESCRIPTION = "description"; 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.) * <p> * 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 ','. * </p><p> * Type: TEXT * </p> */ public static final String COLUMN_INTERNAL_TUNE_PARAMS = "tune_params"; /** * The session token of this TV program. (Used internally.) * <p> * <p> * This column contains an encoded string which is comma-separated key-value pairs. * This contains a String representation of {@link IBinder} for * (Ex. "[key1]=[value1], [key2]=[value2]"). COLUMN_TUNE_PARAMS will use '%' as an escape * {@link TvInputService.Session} that provides the current TV program. It is used * character for the characters of '%', '=', and ','. * internally to distinguish watched programs entries from different TV input sessions. * </p><p> * </p><p> * Type: TEXT * Type: TEXT * </p> * </p> * @see TvInputManager.Session.tune(Uri, Bundle) */ */ public static final String COLUMN_TUNE_PARAMS = "tune_params"; public static final String COLUMN_INTERNAL_SESSION_TOKEN = "session_token"; private WatchedPrograms() {} private WatchedPrograms() {} } } Loading services/core/java/com/android/server/tv/TvInputManagerService.java +84 −235 Original line number Original line Diff line number Diff line Loading @@ -35,7 +35,6 @@ import android.content.ServiceConnection; import android.content.pm.PackageManager; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.pm.ServiceInfo; import android.database.Cursor; import android.graphics.Rect; import android.graphics.Rect; import android.hardware.hdmi.HdmiDeviceInfo; import android.hardware.hdmi.HdmiDeviceInfo; import android.media.tv.ITvInputClient; import android.media.tv.ITvInputClient; Loading Loading @@ -108,14 +107,14 @@ public final class TvInputManagerService extends SystemService { // A map from user id to UserState. // A map from user id to UserState. private final SparseArray<UserState> mUserStates = new SparseArray<UserState>(); private final SparseArray<UserState> mUserStates = new SparseArray<UserState>(); private final Handler mLogHandler; private final WatchLogHandler mWatchLogHandler; public TvInputManagerService(Context context) { public TvInputManagerService(Context context) { super(context); super(context); mContext = context; mContext = context; mContentResolver = context.getContentResolver(); mContentResolver = context.getContentResolver(); mLogHandler = new LogHandler(IoThread.get().getLooper()); mWatchLogHandler = new WatchLogHandler(IoThread.get().getLooper()); mTvInputHardwareManager = new TvInputHardwareManager(context, new HardwareListener()); mTvInputHardwareManager = new TvInputHardwareManager(context, new HardwareListener()); Loading Loading @@ -717,14 +716,6 @@ public final class TvInputManagerService extends SystemService { return; 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 // Also remove the session token from the session token list of the current client and // service. // service. ClientState clientState = userState.clientStateMap.get(sessionState.mClient.asBinder()); ClientState clientState = userState.clientStateMap.get(sessionState.mClient.asBinder()); Loading @@ -743,6 +734,12 @@ public final class TvInputManagerService extends SystemService { } } } } updateServiceConnectionLocked(sessionState.mInfo.getComponent(), userId); 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) { private void notifyInputAddedLocked(UserState userState, String inputId) { Loading Loading @@ -1231,41 +1228,19 @@ public final class TvInputManagerService extends SystemService { // Do not log the watch history for passthrough inputs. // Do not log the watch history for passthrough inputs. return; return; } } long currentTime = System.currentTimeMillis(); long channelId = ContentUris.parseId(channelUri); // Close the open log entry first, if any. UserState userState = getUserStateLocked(resolvedUserId); UserState userState = getUserStateLocked(resolvedUserId); SessionState sessionState = userState.sessionStateMap.get(sessionToken); 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( // Log the start of watch. TvContract.WatchedPrograms.CONTENT_URI, values); SomeArgs args = SomeArgs.obtain(); SomeArgs args = SomeArgs.obtain(); args.arg1 = sessionState.mLogUri; args.arg1 = sessionState.mInfo.getComponent().getPackageName(); args.arg2 = ContentUris.parseId(channelUri); args.arg2 = System.currentTimeMillis(); args.arg3 = currentTime; args.arg3 = ContentUris.parseId(channelUri); args.arg4 = sessionState; args.arg4 = params; mLogHandler.obtainMessage(LogHandler.MSG_OPEN_ENTRY, args).sendToTarget(); args.arg5 = sessionToken; mWatchLogHandler.obtainMessage(WatchLogHandler.MSG_LOG_WATCH_START, args) .sendToTarget(); } catch (RemoteException e) { } catch (RemoteException e) { Slog.e(TAG, "error in tune", e); Slog.e(TAG, "error in tune", e); return; return; Loading Loading @@ -1691,39 +1666,6 @@ public final class TvInputManagerService extends SystemService { } } } } } } private String encodeTuneParams(Bundle tuneParams) { StringBuilder builder = new StringBuilder(); Set<String> keySet = tuneParams.keySet(); Iterator<String> 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 { private static final class TvInputState { Loading Loading @@ -2062,195 +2004,102 @@ public final class TvInputManagerService extends SystemService { } } } } private final class LogHandler extends Handler { private final class WatchLogHandler extends Handler { private static final int MSG_OPEN_ENTRY = 1; // There are only two kinds of watch events that can happen on the system: private static final int MSG_UPDATE_ENTRY = 2; // 1. The current TV input session is tuned to a new channel. private static final int MSG_CLOSE_ENTRY = 3; // 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); super(looper); } } @Override @Override public void handleMessage(Message msg) { public void handleMessage(Message msg) { switch (msg.what) { switch (msg.what) { case MSG_OPEN_ENTRY: { case MSG_LOG_WATCH_START: { SomeArgs args = (SomeArgs) msg.obj; SomeArgs args = (SomeArgs) msg.obj; Uri uri = (Uri) args.arg1; String packageName = (String) args.arg1; long channelId = (long) args.arg2; long watchStartTime = (long) args.arg2; long time = (long) args.arg3; long channelId = (long) args.arg3; SessionState sessionState = (SessionState) args.arg4; Bundle tuneParams = (Bundle) args.arg4; onOpenEntry(uri, channelId, time, sessionState); IBinder sessionToken = (IBinder) args.arg5; args.recycle(); return; 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)); } } case MSG_UPDATE_ENTRY: { values.put(TvContract.WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN, SomeArgs args = (SomeArgs) msg.obj; sessionToken.toString()); Uri uri = (Uri) args.arg1; long channelId = (long) args.arg2; mContentResolver.insert(TvContract.WatchedPrograms.CONTENT_URI, values); long time = (long) args.arg3; SessionState sessionState = (SessionState) args.arg4; onUpdateEntry(uri, channelId, time, sessionState); args.recycle(); args.recycle(); return; return; } } case MSG_CLOSE_ENTRY: { case MSG_LOG_WATCH_END: { SomeArgs args = (SomeArgs) msg.obj; SomeArgs args = (SomeArgs) msg.obj; Uri uri = (Uri) args.arg1; IBinder sessionToken = (IBinder) args.arg1; long time = (long) args.arg2; long watchEndTime = (long) args.arg2; onCloseEntry(uri, time); args.recycle(); return; } default: { Slog.w(TAG, "Unhandled message code: " + msg.what); return; } } } 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; } 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(); ContentValues values = new ContentValues(); values.put(TvContract.WatchedPrograms.COLUMN_TITLE, cursor.getString(0)); values.put(TvContract.WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS, values.put(TvContract.WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS, watchEndTime); cursor.getLong(1)); values.put(TvContract.WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN, long endTime = cursor.getLong(2); sessionToken.toString()); 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(); } } } private void onUpdateEntry(Uri uri, long channelId, long time, SessionState sessionState) { mContentResolver.insert(TvContract.WatchedPrograms.CONTENT_URI, values); String[] projection = { args.recycle(); 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; return; } } default: { // Update the watch end time for the current log entry. Slog.w(TAG, "Unhandled message code: " + msg.what); 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; 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. private String encodeTuneParams(Bundle tuneParams) { onOpenEntry(newUri, channelId, time, sessionState); StringBuilder builder = new StringBuilder(); } Set<String> keySet = tuneParams.keySet(); } finally { Iterator<String> it = keySet.iterator(); if (cursor != null) { while (it.hasNext()) { cursor.close(); 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 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) { private String replaceEscapeCharacters(String src) { String[] projection = { TvContract.Channels.COLUMN_SEARCHABLE }; final char ESCAPE_CHARACTER = '%'; String selection = TvContract.Channels._ID + "=?"; final String ENCODING_TARGET_CHARACTERS = "%=,"; String[] selectionArgs = { String.valueOf(channelId) }; StringBuilder builder = new StringBuilder(); Cursor cursor = null; for (char ch : src.toCharArray()) { try { if (ENCODING_TARGET_CHARACTERS.indexOf(ch) >= 0) { cursor = mContentResolver.query(TvContract.Channels.CONTENT_URI, projection, builder.append(ESCAPE_CHARACTER); selection, selectionArgs, null); if (cursor != null && cursor.moveToNext()) { return cursor.getLong(0) == 1 ? true : false; } } finally { if (cursor != null) { cursor.close(); } } builder.append(ch); } } // Unless explicitly specified non-searchable, by default the channel is searchable. return builder.toString(); return true; } } } } Loading Loading
media/java/android/media/tv/TvContract.java +23 −8 Original line number Original line Diff line number Diff line Loading @@ -21,6 +21,7 @@ import android.content.ComponentName; import android.content.ContentResolver; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentUris; import android.net.Uri; import android.net.Uri; import android.os.IBinder; import android.provider.BaseColumns; import android.provider.BaseColumns; import android.util.ArraySet; import android.util.ArraySet; Loading Loading @@ -795,7 +796,7 @@ public final class TvContract { public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/program"; 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. * <p> * <p> * This is a part of the channel URI and matches to {@link BaseColumns#_ID}. * This is a part of the channel URI and matches to {@link BaseColumns#_ID}. * </p><p> * </p><p> Loading Loading @@ -1109,6 +1110,7 @@ public final class TvContract { * to this table. * to this table. * @hide * @hide */ */ @SystemApi public static final class WatchedPrograms implements BaseTvColumns { public static final class WatchedPrograms implements BaseTvColumns { /** The content:// style URI for this table. */ /** The content:// style URI for this table. */ Loading Loading @@ -1141,7 +1143,7 @@ public final class TvContract { public static final String COLUMN_WATCH_END_TIME_UTC_MILLIS = "watch_end_time_utc_millis"; 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. * <p> * <p> * Type: INTEGER (long) * Type: INTEGER (long) * </p> * </p> Loading Loading @@ -1181,17 +1183,30 @@ public final class TvContract { public static final String COLUMN_DESCRIPTION = "description"; 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.) * <p> * 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 ','. * </p><p> * Type: TEXT * </p> */ public static final String COLUMN_INTERNAL_TUNE_PARAMS = "tune_params"; /** * The session token of this TV program. (Used internally.) * <p> * <p> * This column contains an encoded string which is comma-separated key-value pairs. * This contains a String representation of {@link IBinder} for * (Ex. "[key1]=[value1], [key2]=[value2]"). COLUMN_TUNE_PARAMS will use '%' as an escape * {@link TvInputService.Session} that provides the current TV program. It is used * character for the characters of '%', '=', and ','. * internally to distinguish watched programs entries from different TV input sessions. * </p><p> * </p><p> * Type: TEXT * Type: TEXT * </p> * </p> * @see TvInputManager.Session.tune(Uri, Bundle) */ */ public static final String COLUMN_TUNE_PARAMS = "tune_params"; public static final String COLUMN_INTERNAL_SESSION_TOKEN = "session_token"; private WatchedPrograms() {} private WatchedPrograms() {} } } Loading
services/core/java/com/android/server/tv/TvInputManagerService.java +84 −235 Original line number Original line Diff line number Diff line Loading @@ -35,7 +35,6 @@ import android.content.ServiceConnection; import android.content.pm.PackageManager; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.pm.ServiceInfo; import android.database.Cursor; import android.graphics.Rect; import android.graphics.Rect; import android.hardware.hdmi.HdmiDeviceInfo; import android.hardware.hdmi.HdmiDeviceInfo; import android.media.tv.ITvInputClient; import android.media.tv.ITvInputClient; Loading Loading @@ -108,14 +107,14 @@ public final class TvInputManagerService extends SystemService { // A map from user id to UserState. // A map from user id to UserState. private final SparseArray<UserState> mUserStates = new SparseArray<UserState>(); private final SparseArray<UserState> mUserStates = new SparseArray<UserState>(); private final Handler mLogHandler; private final WatchLogHandler mWatchLogHandler; public TvInputManagerService(Context context) { public TvInputManagerService(Context context) { super(context); super(context); mContext = context; mContext = context; mContentResolver = context.getContentResolver(); mContentResolver = context.getContentResolver(); mLogHandler = new LogHandler(IoThread.get().getLooper()); mWatchLogHandler = new WatchLogHandler(IoThread.get().getLooper()); mTvInputHardwareManager = new TvInputHardwareManager(context, new HardwareListener()); mTvInputHardwareManager = new TvInputHardwareManager(context, new HardwareListener()); Loading Loading @@ -717,14 +716,6 @@ public final class TvInputManagerService extends SystemService { return; 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 // Also remove the session token from the session token list of the current client and // service. // service. ClientState clientState = userState.clientStateMap.get(sessionState.mClient.asBinder()); ClientState clientState = userState.clientStateMap.get(sessionState.mClient.asBinder()); Loading @@ -743,6 +734,12 @@ public final class TvInputManagerService extends SystemService { } } } } updateServiceConnectionLocked(sessionState.mInfo.getComponent(), userId); 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) { private void notifyInputAddedLocked(UserState userState, String inputId) { Loading Loading @@ -1231,41 +1228,19 @@ public final class TvInputManagerService extends SystemService { // Do not log the watch history for passthrough inputs. // Do not log the watch history for passthrough inputs. return; return; } } long currentTime = System.currentTimeMillis(); long channelId = ContentUris.parseId(channelUri); // Close the open log entry first, if any. UserState userState = getUserStateLocked(resolvedUserId); UserState userState = getUserStateLocked(resolvedUserId); SessionState sessionState = userState.sessionStateMap.get(sessionToken); 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( // Log the start of watch. TvContract.WatchedPrograms.CONTENT_URI, values); SomeArgs args = SomeArgs.obtain(); SomeArgs args = SomeArgs.obtain(); args.arg1 = sessionState.mLogUri; args.arg1 = sessionState.mInfo.getComponent().getPackageName(); args.arg2 = ContentUris.parseId(channelUri); args.arg2 = System.currentTimeMillis(); args.arg3 = currentTime; args.arg3 = ContentUris.parseId(channelUri); args.arg4 = sessionState; args.arg4 = params; mLogHandler.obtainMessage(LogHandler.MSG_OPEN_ENTRY, args).sendToTarget(); args.arg5 = sessionToken; mWatchLogHandler.obtainMessage(WatchLogHandler.MSG_LOG_WATCH_START, args) .sendToTarget(); } catch (RemoteException e) { } catch (RemoteException e) { Slog.e(TAG, "error in tune", e); Slog.e(TAG, "error in tune", e); return; return; Loading Loading @@ -1691,39 +1666,6 @@ public final class TvInputManagerService extends SystemService { } } } } } } private String encodeTuneParams(Bundle tuneParams) { StringBuilder builder = new StringBuilder(); Set<String> keySet = tuneParams.keySet(); Iterator<String> 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 { private static final class TvInputState { Loading Loading @@ -2062,195 +2004,102 @@ public final class TvInputManagerService extends SystemService { } } } } private final class LogHandler extends Handler { private final class WatchLogHandler extends Handler { private static final int MSG_OPEN_ENTRY = 1; // There are only two kinds of watch events that can happen on the system: private static final int MSG_UPDATE_ENTRY = 2; // 1. The current TV input session is tuned to a new channel. private static final int MSG_CLOSE_ENTRY = 3; // 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); super(looper); } } @Override @Override public void handleMessage(Message msg) { public void handleMessage(Message msg) { switch (msg.what) { switch (msg.what) { case MSG_OPEN_ENTRY: { case MSG_LOG_WATCH_START: { SomeArgs args = (SomeArgs) msg.obj; SomeArgs args = (SomeArgs) msg.obj; Uri uri = (Uri) args.arg1; String packageName = (String) args.arg1; long channelId = (long) args.arg2; long watchStartTime = (long) args.arg2; long time = (long) args.arg3; long channelId = (long) args.arg3; SessionState sessionState = (SessionState) args.arg4; Bundle tuneParams = (Bundle) args.arg4; onOpenEntry(uri, channelId, time, sessionState); IBinder sessionToken = (IBinder) args.arg5; args.recycle(); return; 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)); } } case MSG_UPDATE_ENTRY: { values.put(TvContract.WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN, SomeArgs args = (SomeArgs) msg.obj; sessionToken.toString()); Uri uri = (Uri) args.arg1; long channelId = (long) args.arg2; mContentResolver.insert(TvContract.WatchedPrograms.CONTENT_URI, values); long time = (long) args.arg3; SessionState sessionState = (SessionState) args.arg4; onUpdateEntry(uri, channelId, time, sessionState); args.recycle(); args.recycle(); return; return; } } case MSG_CLOSE_ENTRY: { case MSG_LOG_WATCH_END: { SomeArgs args = (SomeArgs) msg.obj; SomeArgs args = (SomeArgs) msg.obj; Uri uri = (Uri) args.arg1; IBinder sessionToken = (IBinder) args.arg1; long time = (long) args.arg2; long watchEndTime = (long) args.arg2; onCloseEntry(uri, time); args.recycle(); return; } default: { Slog.w(TAG, "Unhandled message code: " + msg.what); return; } } } 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; } 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(); ContentValues values = new ContentValues(); values.put(TvContract.WatchedPrograms.COLUMN_TITLE, cursor.getString(0)); values.put(TvContract.WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS, values.put(TvContract.WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS, watchEndTime); cursor.getLong(1)); values.put(TvContract.WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN, long endTime = cursor.getLong(2); sessionToken.toString()); 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(); } } } private void onUpdateEntry(Uri uri, long channelId, long time, SessionState sessionState) { mContentResolver.insert(TvContract.WatchedPrograms.CONTENT_URI, values); String[] projection = { args.recycle(); 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; return; } } default: { // Update the watch end time for the current log entry. Slog.w(TAG, "Unhandled message code: " + msg.what); 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; 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. private String encodeTuneParams(Bundle tuneParams) { onOpenEntry(newUri, channelId, time, sessionState); StringBuilder builder = new StringBuilder(); } Set<String> keySet = tuneParams.keySet(); } finally { Iterator<String> it = keySet.iterator(); if (cursor != null) { while (it.hasNext()) { cursor.close(); 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 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) { private String replaceEscapeCharacters(String src) { String[] projection = { TvContract.Channels.COLUMN_SEARCHABLE }; final char ESCAPE_CHARACTER = '%'; String selection = TvContract.Channels._ID + "=?"; final String ENCODING_TARGET_CHARACTERS = "%=,"; String[] selectionArgs = { String.valueOf(channelId) }; StringBuilder builder = new StringBuilder(); Cursor cursor = null; for (char ch : src.toCharArray()) { try { if (ENCODING_TARGET_CHARACTERS.indexOf(ch) >= 0) { cursor = mContentResolver.query(TvContract.Channels.CONTENT_URI, projection, builder.append(ESCAPE_CHARACTER); selection, selectionArgs, null); if (cursor != null && cursor.moveToNext()) { return cursor.getLong(0) == 1 ? true : false; } } finally { if (cursor != null) { cursor.close(); } } builder.append(ch); } } // Unless explicitly specified non-searchable, by default the channel is searchable. return builder.toString(); return true; } } } } Loading