Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d86eb6f8 authored by Jae Seo's avatar Jae Seo Committed by Android (Google) Code Review
Browse files

Merge "TIF: Move watch history logging to TvProvider" into lmp-dev

parents 8b72930b 7eb75dff
Loading
Loading
Loading
Loading
+23 −8
Original line number Original line Diff line number Diff line
@@ -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;


@@ -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>
@@ -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. */
@@ -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>
@@ -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() {}
    }
    }
+84 −235
Original line number Original line Diff line number Diff line
@@ -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;
@@ -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());


@@ -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());
@@ -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) {
@@ -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;
@@ -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 {
@@ -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;
        }
        }
    }
    }