am 44a3a39c: Merge "TIF: Move watch history logging to TvProvider" into lmp-dev
* commit '44a3a39c39b57fe9988bdbfd3472dfe5818571f5': TIF: Move watch history logging to TvProvider
This commit is contained in:
@@ -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.
|
||||
* <p>
|
||||
* This is a part of the channel URI and matches to {@link BaseColumns#_ID}.
|
||||
* </p><p>
|
||||
@@ -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.
|
||||
* <p>
|
||||
* Type: INTEGER (long)
|
||||
* </p>
|
||||
@@ -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.)
|
||||
* <p>
|
||||
* 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 ','.
|
||||
* </p><p>
|
||||
* Type: TEXT
|
||||
* </p>
|
||||
* @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.)
|
||||
* <p>
|
||||
* 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.
|
||||
* </p><p>
|
||||
* Type: TEXT
|
||||
* </p>
|
||||
*/
|
||||
public static final String COLUMN_INTERNAL_SESSION_TOKEN = "session_token";
|
||||
|
||||
private WatchedPrograms() {}
|
||||
}
|
||||
|
||||
@@ -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<UserState> mUserStates = new SparseArray<UserState>();
|
||||
|
||||
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<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 {
|
||||
@@ -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<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;
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user