TvInputManagerService.java revision 226a51958d645a8e2be3e40a6b6daaca558b4913
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.tv;
18
19import android.app.ActivityManager;
20import android.content.BroadcastReceiver;
21import android.content.ComponentName;
22import android.content.ContentProviderOperation;
23import android.content.ContentProviderResult;
24import android.content.ContentResolver;
25import android.content.ContentUris;
26import android.content.ContentValues;
27import android.content.Context;
28import android.content.Intent;
29import android.content.IntentFilter;
30import android.content.OperationApplicationException;
31import android.content.ServiceConnection;
32import android.content.pm.PackageManager;
33import android.content.pm.ResolveInfo;
34import android.content.pm.ServiceInfo;
35import android.database.Cursor;
36import android.graphics.Rect;
37import android.media.tv.ITvInputClient;
38import android.media.tv.ITvInputHardware;
39import android.media.tv.ITvInputHardwareCallback;
40import android.media.tv.ITvInputManager;
41import android.media.tv.ITvInputService;
42import android.media.tv.ITvInputServiceCallback;
43import android.media.tv.ITvInputSession;
44import android.media.tv.ITvInputSessionCallback;
45import android.media.tv.TvContract;
46import android.media.tv.TvInputHardwareInfo;
47import android.media.tv.TvInputInfo;
48import android.media.tv.TvInputService;
49import android.net.Uri;
50import android.os.Binder;
51import android.os.Bundle;
52import android.os.Handler;
53import android.os.IBinder;
54import android.os.Looper;
55import android.os.Message;
56import android.os.Process;
57import android.os.RemoteException;
58import android.os.UserHandle;
59import android.util.Slog;
60import android.util.SparseArray;
61import android.view.InputChannel;
62import android.view.Surface;
63
64import com.android.internal.content.PackageMonitor;
65import com.android.internal.os.SomeArgs;
66import com.android.server.IoThread;
67import com.android.server.SystemService;
68
69import org.xmlpull.v1.XmlPullParserException;
70
71import java.io.IOException;
72import java.util.ArrayList;
73import java.util.HashMap;
74import java.util.HashSet;
75import java.util.List;
76import java.util.Map;
77import java.util.Set;
78
79/** This class provides a system service that manages television inputs. */
80public final class TvInputManagerService extends SystemService {
81    // STOPSHIP: Turn debugging off.
82    private static final boolean DEBUG = true;
83    private static final String TAG = "TvInputManagerService";
84
85    private final Context mContext;
86    private final TvInputHardwareManager mTvInputHardwareManager;
87
88    private final ContentResolver mContentResolver;
89
90    // A global lock.
91    private final Object mLock = new Object();
92
93    // ID of the current user.
94    private int mCurrentUserId = UserHandle.USER_OWNER;
95
96    // A map from user id to UserState.
97    private final SparseArray<UserState> mUserStates = new SparseArray<UserState>();
98
99    private final Handler mLogHandler;
100
101    public TvInputManagerService(Context context) {
102        super(context);
103
104        mContext = context;
105        mContentResolver = context.getContentResolver();
106        mLogHandler = new LogHandler(IoThread.get().getLooper());
107
108        mTvInputHardwareManager = new TvInputHardwareManager(context);
109
110        synchronized (mLock) {
111            mUserStates.put(mCurrentUserId, new UserState());
112        }
113    }
114
115    @Override
116    public void onStart() {
117        publishBinderService(Context.TV_INPUT_SERVICE, new BinderService());
118    }
119
120    @Override
121    public void onBootPhase(int phase) {
122        if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
123            registerBroadcastReceivers();
124            synchronized (mLock) {
125                buildTvInputListLocked(mCurrentUserId);
126            }
127        }
128    }
129
130    private void registerBroadcastReceivers() {
131        PackageMonitor monitor = new PackageMonitor() {
132            @Override
133            public void onSomePackagesChanged() {
134                synchronized (mLock) {
135                    buildTvInputListLocked(mCurrentUserId);
136                }
137            }
138
139            @Override
140            public void onPackageRemoved(String packageName, int uid) {
141                synchronized (mLock) {
142                    UserState userState = getUserStateLocked(mCurrentUserId);
143                    if (!userState.packageList.contains(packageName)) {
144                        // Not a TV input package.
145                        return;
146                    }
147                }
148
149                ArrayList<ContentProviderOperation> operations =
150                        new ArrayList<ContentProviderOperation>();
151
152                String selection = TvContract.BaseTvColumns.COLUMN_PACKAGE_NAME + "=?";
153                String[] selectionArgs = { packageName };
154
155                operations.add(ContentProviderOperation.newDelete(TvContract.Channels.CONTENT_URI)
156                        .withSelection(selection, selectionArgs).build());
157                operations.add(ContentProviderOperation.newDelete(TvContract.Programs.CONTENT_URI)
158                        .withSelection(selection, selectionArgs).build());
159                operations.add(ContentProviderOperation
160                        .newDelete(TvContract.WatchedPrograms.CONTENT_URI)
161                        .withSelection(selection, selectionArgs).build());
162
163                ContentProviderResult[] results = null;
164                try {
165                    results = mContentResolver.applyBatch(TvContract.AUTHORITY, operations);
166                } catch (RemoteException | OperationApplicationException e) {
167                    Slog.e(TAG, "error in applyBatch" + e);
168                }
169
170                if (DEBUG) {
171                    Slog.d(TAG, "onPackageRemoved(packageName=" + packageName + ", uid=" + uid
172                            + ")");
173                    Slog.d(TAG, "results=" + results);
174                }
175            }
176        };
177        monitor.register(mContext, null, UserHandle.ALL, true);
178
179        IntentFilter intentFilter = new IntentFilter();
180        intentFilter.addAction(Intent.ACTION_USER_SWITCHED);
181        intentFilter.addAction(Intent.ACTION_USER_REMOVED);
182        mContext.registerReceiverAsUser(new BroadcastReceiver() {
183            @Override
184            public void onReceive(Context context, Intent intent) {
185                String action = intent.getAction();
186                if (Intent.ACTION_USER_SWITCHED.equals(action)) {
187                    switchUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0));
188                } else if (Intent.ACTION_USER_REMOVED.equals(action)) {
189                    removeUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0));
190                }
191            }
192        }, UserHandle.ALL, intentFilter, null, null);
193    }
194
195    private void buildTvInputListLocked(int userId) {
196        UserState userState = getUserStateLocked(userId);
197        userState.inputMap.clear();
198        userState.packageList.clear();
199
200        if (DEBUG) Slog.d(TAG, "buildTvInputList");
201        PackageManager pm = mContext.getPackageManager();
202        List<ResolveInfo> services = pm.queryIntentServices(
203                new Intent(TvInputService.SERVICE_INTERFACE),
204                PackageManager.GET_SERVICES | PackageManager.GET_META_DATA);
205        for (ResolveInfo ri : services) {
206            ServiceInfo si = ri.serviceInfo;
207            if (!android.Manifest.permission.BIND_TV_INPUT.equals(si.permission)) {
208                Slog.w(TAG, "Skipping TV input " + si.name + ": it does not require the permission "
209                        + android.Manifest.permission.BIND_TV_INPUT);
210                continue;
211            }
212            try {
213                TvInputInfo info = TvInputInfo.createTvInputInfo(mContext, ri);
214                if (DEBUG) Slog.d(TAG, "add " + info.getId());
215                userState.inputMap.put(info.getId(), info);
216                userState.packageList.add(si.packageName);
217
218                // Reconnect the service if existing input is updated.
219                updateServiceConnectionLocked(info.getId(), userId);
220            } catch (IOException | XmlPullParserException e) {
221                Slog.e(TAG, "Can't load TV input " + si.name, e);
222            }
223        }
224    }
225
226    private void switchUser(int userId) {
227        synchronized (mLock) {
228            if (mCurrentUserId == userId) {
229                return;
230            }
231            // final int oldUserId = mCurrentUserId;
232            // TODO: Release services and sessions in the old user state, if needed.
233            mCurrentUserId = userId;
234
235            UserState userState = mUserStates.get(userId);
236            if (userState == null) {
237                userState = new UserState();
238            }
239            mUserStates.put(userId, userState);
240            buildTvInputListLocked(userId);
241        }
242    }
243
244    private void removeUser(int userId) {
245        synchronized (mLock) {
246            UserState userState = mUserStates.get(userId);
247            if (userState == null) {
248                return;
249            }
250            // Release created sessions.
251            for (SessionState state : userState.sessionStateMap.values()) {
252                if (state.mSession != null) {
253                    try {
254                        state.mSession.release();
255                    } catch (RemoteException e) {
256                        Slog.e(TAG, "error in release", e);
257                    }
258                }
259            }
260            userState.sessionStateMap.clear();
261
262            // Unregister all callbacks and unbind all services.
263            for (ServiceState serviceState : userState.serviceStateMap.values()) {
264                if (serviceState.mCallback != null) {
265                    try {
266                        serviceState.mService.unregisterCallback(serviceState.mCallback);
267                    } catch (RemoteException e) {
268                        Slog.e(TAG, "error in unregisterCallback", e);
269                    }
270                }
271                serviceState.mClientTokens.clear();
272                mContext.unbindService(serviceState.mConnection);
273            }
274            userState.serviceStateMap.clear();
275
276            userState.clientStateMap.clear();
277
278            mUserStates.remove(userId);
279        }
280    }
281
282    private UserState getUserStateLocked(int userId) {
283        UserState userState = mUserStates.get(userId);
284        if (userState == null) {
285            throw new IllegalStateException("User state not found for user ID " + userId);
286        }
287        return userState;
288    }
289
290    private ServiceState getServiceStateLocked(String inputId, int userId) {
291        UserState userState = getUserStateLocked(userId);
292        ServiceState serviceState = userState.serviceStateMap.get(inputId);
293        if (serviceState == null) {
294            throw new IllegalStateException("Service state not found for " + inputId + " (userId="
295                    + userId + ")");
296        }
297        return serviceState;
298    }
299
300    private SessionState getSessionStateLocked(IBinder sessionToken, int callingUid, int userId) {
301        UserState userState = getUserStateLocked(userId);
302        SessionState sessionState = userState.sessionStateMap.get(sessionToken);
303        if (sessionState == null) {
304            throw new IllegalArgumentException("Session state not found for token " + sessionToken);
305        }
306        // Only the application that requested this session or the system can access it.
307        if (callingUid != Process.SYSTEM_UID && callingUid != sessionState.mCallingUid) {
308            throw new SecurityException("Illegal access to the session with token " + sessionToken
309                    + " from uid " + callingUid);
310        }
311        return sessionState;
312    }
313
314    private ITvInputSession getSessionLocked(IBinder sessionToken, int callingUid, int userId) {
315        SessionState sessionState = getSessionStateLocked(sessionToken, callingUid, userId);
316        ITvInputSession session = sessionState.mSession;
317        if (session == null) {
318            throw new IllegalStateException("Session not yet created for token " + sessionToken);
319        }
320        return session;
321    }
322
323    private int resolveCallingUserId(int callingPid, int callingUid, int requestedUserId,
324            String methodName) {
325        return ActivityManager.handleIncomingUser(callingPid, callingUid, requestedUserId, false,
326                false, methodName, null);
327    }
328
329    private void updateServiceConnectionLocked(String inputId, int userId) {
330        UserState userState = getUserStateLocked(userId);
331        ServiceState serviceState = userState.serviceStateMap.get(inputId);
332        if (serviceState == null) {
333            return;
334        }
335        if (serviceState.mReconnecting) {
336            if (!serviceState.mSessionTokens.isEmpty()) {
337                // wait until all the sessions are removed.
338                return;
339            }
340            serviceState.mReconnecting = false;
341        }
342        boolean isStateEmpty = serviceState.mClientTokens.isEmpty()
343                && serviceState.mSessionTokens.isEmpty();
344        if (serviceState.mService == null && !isStateEmpty && userId == mCurrentUserId) {
345            // This means that the service is not yet connected but its state indicates that we
346            // have pending requests. Then, connect the service.
347            if (serviceState.mBound) {
348                // We have already bound to the service so we don't try to bind again until after we
349                // unbind later on.
350                return;
351            }
352            if (DEBUG) {
353                Slog.d(TAG, "bindServiceAsUser(inputId=" + inputId + ", userId=" + userId
354                        + ")");
355            }
356
357            Intent i = new Intent(TvInputService.SERVICE_INTERFACE).setComponent(
358                    userState.inputMap.get(inputId).getComponent());
359            // Binding service may fail if the service is updating.
360            // In that case, the connection will be revived in buildTvInputListLocked called by
361            // onSomePackagesChanged.
362            serviceState.mBound = mContext.bindServiceAsUser(
363                    i, serviceState.mConnection, Context.BIND_AUTO_CREATE, new UserHandle(userId));
364        } else if (serviceState.mService != null && isStateEmpty) {
365            // This means that the service is already connected but its state indicates that we have
366            // nothing to do with it. Then, disconnect the service.
367            if (DEBUG) {
368                Slog.d(TAG, "unbindService(inputId=" + inputId + ")");
369            }
370            mContext.unbindService(serviceState.mConnection);
371            userState.serviceStateMap.remove(inputId);
372        }
373    }
374
375    private ClientState createClientStateLocked(IBinder clientToken, int userId) {
376        UserState userState = getUserStateLocked(userId);
377        ClientState clientState = new ClientState(clientToken, userId);
378        try {
379            clientToken.linkToDeath(clientState, 0);
380        } catch (RemoteException e) {
381            Slog.e(TAG, "Client is already died.");
382        }
383        userState.clientStateMap.put(clientToken, clientState);
384        return clientState;
385    }
386
387    private void createSessionInternalLocked(ITvInputService service, final IBinder sessionToken,
388            final int userId) {
389        final UserState userState = getUserStateLocked(userId);
390        final SessionState sessionState = userState.sessionStateMap.get(sessionToken);
391        if (DEBUG) {
392            Slog.d(TAG, "createSessionInternalLocked(inputId=" + sessionState.mInputId + ")");
393        }
394
395        final InputChannel[] channels = InputChannel.openInputChannelPair(sessionToken.toString());
396
397        // Set up a callback to send the session token.
398        ITvInputSessionCallback callback = new ITvInputSessionCallback.Stub() {
399            @Override
400            public void onSessionCreated(ITvInputSession session) {
401                if (DEBUG) {
402                    Slog.d(TAG, "onSessionCreated(inputId=" + sessionState.mInputId + ")");
403                }
404                synchronized (mLock) {
405                    sessionState.mSession = session;
406                    if (session == null) {
407                        removeSessionStateLocked(sessionToken, userId);
408                        sendSessionTokenToClientLocked(sessionState.mClient, sessionState.mInputId,
409                                null, null, sessionState.mSeq);
410                    } else {
411                        try {
412                            session.asBinder().linkToDeath(sessionState, 0);
413                        } catch (RemoteException e) {
414                            Slog.e(TAG, "Session is already died.");
415                        }
416
417                        IBinder clientToken = sessionState.mClient.asBinder();
418                        ClientState clientState = userState.clientStateMap.get(clientToken);
419                        if (clientState == null) {
420                            clientState = createClientStateLocked(clientToken, userId);
421                        }
422                        clientState.mSessionTokens.add(sessionState.mSessionToken);
423
424                        sendSessionTokenToClientLocked(sessionState.mClient, sessionState.mInputId,
425                                sessionToken, channels[0], sessionState.mSeq);
426                    }
427                    channels[0].dispose();
428                }
429            }
430
431            @Override
432            public void onVideoStreamChanged(int width, int height, boolean interlaced) {
433                synchronized (mLock) {
434                    if (DEBUG) {
435                        Slog.d(TAG, "onVideoStreamChanged(" + width + ", " + height + ")");
436                    }
437                    if (sessionState.mSession == null || sessionState.mClient == null) {
438                        return;
439                    }
440                    try {
441                        sessionState.mClient.onVideoStreamChanged(width, height, interlaced,
442                                sessionState.mSeq);
443                    } catch (RemoteException e) {
444                        Slog.e(TAG, "error in onVideoStreamChanged");
445                    }
446                }
447            }
448
449            @Override
450            public void onAudioStreamChanged(int channelCount) {
451                synchronized (mLock) {
452                    if (DEBUG) {
453                        Slog.d(TAG, "onAudioStreamChanged(" + channelCount + ")");
454                    }
455                    if (sessionState.mSession == null || sessionState.mClient == null) {
456                        return;
457                    }
458                    try {
459                        sessionState.mClient.onAudioStreamChanged(channelCount, sessionState.mSeq);
460                    } catch (RemoteException e) {
461                        Slog.e(TAG, "error in onAudioStreamChanged");
462                    }
463                }
464            }
465
466            @Override
467            public void onClosedCaptionStreamChanged(boolean hasClosedCaption) {
468                synchronized (mLock) {
469                    if (DEBUG) {
470                        Slog.d(TAG, "onClosedCaptionStreamChanged(" + hasClosedCaption + ")");
471                    }
472                    if (sessionState.mSession == null || sessionState.mClient == null) {
473                        return;
474                    }
475                    try {
476                        sessionState.mClient.onClosedCaptionStreamChanged(hasClosedCaption,
477                                sessionState.mSeq);
478                    } catch (RemoteException e) {
479                        Slog.e(TAG, "error in onClosedCaptionStreamChanged");
480                    }
481                }
482            }
483
484            @Override
485            public void onSessionEvent(String eventType, Bundle eventArgs) {
486                synchronized (mLock) {
487                    if (DEBUG) {
488                        Slog.d(TAG, "onEvent(what=" + eventType + ", data=" + eventArgs + ")");
489                    }
490                    if (sessionState.mSession == null || sessionState.mClient == null) {
491                        return;
492                    }
493                    try {
494                        sessionState.mClient.onSessionEvent(eventType, eventArgs,
495                                sessionState.mSeq);
496                    } catch (RemoteException e) {
497                        Slog.e(TAG, "error in onSessionEvent");
498                    }
499                }
500            }
501        };
502
503        // Create a session. When failed, send a null token immediately.
504        try {
505            service.createSession(channels[1], callback);
506        } catch (RemoteException e) {
507            Slog.e(TAG, "error in createSession", e);
508            removeSessionStateLocked(sessionToken, userId);
509            sendSessionTokenToClientLocked(sessionState.mClient, sessionState.mInputId, null, null,
510                    sessionState.mSeq);
511        }
512        channels[1].dispose();
513    }
514
515    private void sendSessionTokenToClientLocked(ITvInputClient client, String inputId,
516            IBinder sessionToken, InputChannel channel, int seq) {
517        try {
518            client.onSessionCreated(inputId, sessionToken, channel, seq);
519        } catch (RemoteException exception) {
520            Slog.e(TAG, "error in onSessionCreated", exception);
521        }
522    }
523
524    private void releaseSessionLocked(IBinder sessionToken, int callingUid, int userId) {
525        SessionState sessionState = getSessionStateLocked(sessionToken, callingUid, userId);
526        if (sessionState.mSession != null) {
527            try {
528                sessionState.mSession.release();
529            } catch (RemoteException e) {
530                Slog.w(TAG, "session is already disapeared", e);
531            }
532            sessionState.mSession = null;
533        }
534        removeSessionStateLocked(sessionToken, userId);
535    }
536
537    private void removeSessionStateLocked(IBinder sessionToken, int userId) {
538        // Remove the session state from the global session state map of the current user.
539        UserState userState = getUserStateLocked(userId);
540        SessionState sessionState = userState.sessionStateMap.remove(sessionToken);
541
542        // Close the open log entry, if any.
543        if (sessionState.mLogUri != null) {
544            SomeArgs args = SomeArgs.obtain();
545            args.arg1 = sessionState.mLogUri;
546            args.arg2 = System.currentTimeMillis();
547            mLogHandler.obtainMessage(LogHandler.MSG_CLOSE_ENTRY, args).sendToTarget();
548        }
549
550        // Also remove the session token from the session token list of the current client and
551        // service.
552        ClientState clientState = userState.clientStateMap.get(sessionState.mClient.asBinder());
553        if (clientState != null) {
554            clientState.mSessionTokens.remove(sessionToken);
555            if (clientState.isEmpty()) {
556                userState.clientStateMap.remove(sessionState.mClient.asBinder());
557            }
558        }
559
560        ServiceState serviceState = userState.serviceStateMap.get(sessionState.mInputId);
561        if (serviceState != null) {
562            serviceState.mSessionTokens.remove(sessionToken);
563        }
564        updateServiceConnectionLocked(sessionState.mInputId, userId);
565    }
566
567    private void unregisterCallbackInternalLocked(IBinder clientToken, String inputId,
568            int userId) {
569        UserState userState = getUserStateLocked(userId);
570        ClientState clientState = userState.clientStateMap.get(clientToken);
571        if (clientState != null) {
572            clientState.mInputIds.remove(inputId);
573            if (clientState.isEmpty()) {
574                userState.clientStateMap.remove(clientToken);
575            }
576        }
577
578        ServiceState serviceState = userState.serviceStateMap.get(inputId);
579        if (serviceState == null) {
580            return;
581        }
582
583        // Remove this client from the client list and unregister the callback.
584        serviceState.mClientTokens.remove(clientToken);
585        if (!serviceState.mClientTokens.isEmpty()) {
586            // We have other clients who want to keep the callback. Do this later.
587            return;
588        }
589        if (serviceState.mService == null || serviceState.mCallback == null) {
590            return;
591        }
592        try {
593            serviceState.mService.unregisterCallback(serviceState.mCallback);
594        } catch (RemoteException e) {
595            Slog.e(TAG, "error in unregisterCallback", e);
596        } finally {
597            serviceState.mCallback = null;
598            updateServiceConnectionLocked(inputId, userId);
599        }
600    }
601
602    private void broadcastServiceAvailabilityChangedLocked(ServiceState serviceState) {
603        for (IBinder clientToken : serviceState.mClientTokens) {
604            try {
605                ITvInputClient.Stub.asInterface(clientToken).onAvailabilityChanged(
606                        serviceState.mTvInputInfo.getId(), serviceState.mAvailable);
607            } catch (RemoteException e) {
608                Slog.e(TAG, "error in onAvailabilityChanged", e);
609            }
610        }
611    }
612
613    private final class BinderService extends ITvInputManager.Stub {
614        @Override
615        public List<TvInputInfo> getTvInputList(int userId) {
616            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
617                    Binder.getCallingUid(), userId, "getTvInputList");
618            final long identity = Binder.clearCallingIdentity();
619            try {
620                synchronized (mLock) {
621                    UserState userState = getUserStateLocked(resolvedUserId);
622                    return new ArrayList<TvInputInfo>(userState.inputMap.values());
623                }
624            } finally {
625                Binder.restoreCallingIdentity(identity);
626            }
627        }
628
629        @Override
630        public boolean getAvailability(final ITvInputClient client, final String inputId,
631                int userId) {
632            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
633                    Binder.getCallingUid(), userId, "getAvailability");
634            final long identity = Binder.clearCallingIdentity();
635            try {
636                synchronized (mLock) {
637                    UserState userState = getUserStateLocked(resolvedUserId);
638                    ServiceState serviceState = userState.serviceStateMap.get(inputId);
639                    if (serviceState != null) {
640                        // We already know the status of this input service. Return the cached
641                        // status.
642                        return serviceState.mAvailable;
643                    }
644                }
645            } finally {
646                Binder.restoreCallingIdentity(identity);
647            }
648            // STOPSHIP: Redesign the API around the availability change. For now, the service
649            // will be always available.
650            return true;
651        }
652
653        @Override
654        public void registerCallback(final ITvInputClient client, final String inputId,
655                int userId) {
656            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
657                    Binder.getCallingUid(), userId, "registerCallback");
658            final long identity = Binder.clearCallingIdentity();
659            try {
660                synchronized (mLock) {
661                    // Create a new service callback and add it to the callback map of the current
662                    // service.
663                    UserState userState = getUserStateLocked(resolvedUserId);
664                    ServiceState serviceState = userState.serviceStateMap.get(inputId);
665                    if (serviceState == null) {
666                        serviceState = new ServiceState(
667                                userState.inputMap.get(inputId), resolvedUserId);
668                        userState.serviceStateMap.put(inputId, serviceState);
669                    }
670                    IBinder clientToken = client.asBinder();
671                    if (!serviceState.mClientTokens.contains(clientToken)) {
672                        serviceState.mClientTokens.add(clientToken);
673                    }
674
675                    ClientState clientState = userState.clientStateMap.get(clientToken);
676                    if (clientState == null) {
677                        clientState = createClientStateLocked(clientToken, resolvedUserId);
678                    }
679                    if (!clientState.mInputIds.contains(inputId)) {
680                        clientState.mInputIds.add(inputId);
681                    }
682
683                    if (serviceState.mService != null) {
684                        if (serviceState.mCallback != null) {
685                            // We already handled.
686                            return;
687                        }
688                        serviceState.mCallback = new ServiceCallback(resolvedUserId);
689                        try {
690                            serviceState.mService.registerCallback(serviceState.mCallback);
691                        } catch (RemoteException e) {
692                            Slog.e(TAG, "error in registerCallback", e);
693                        }
694                    } else {
695                        updateServiceConnectionLocked(inputId, resolvedUserId);
696                    }
697                }
698            } finally {
699                Binder.restoreCallingIdentity(identity);
700            }
701        }
702
703        @Override
704        public void unregisterCallback(ITvInputClient client, String inputId, int userId) {
705            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
706                    Binder.getCallingUid(), userId, "unregisterCallback");
707            final long identity = Binder.clearCallingIdentity();
708            try {
709                synchronized (mLock) {
710                    unregisterCallbackInternalLocked(client.asBinder(), inputId, resolvedUserId);
711                }
712            } finally {
713                Binder.restoreCallingIdentity(identity);
714            }
715        }
716
717        @Override
718        public void createSession(final ITvInputClient client, final String inputId,
719                int seq, int userId) {
720            final int callingUid = Binder.getCallingUid();
721            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
722                    userId, "createSession");
723            final long identity = Binder.clearCallingIdentity();
724            try {
725                synchronized (mLock) {
726                    UserState userState = getUserStateLocked(resolvedUserId);
727                    ServiceState serviceState = userState.serviceStateMap.get(inputId);
728                    if (serviceState == null) {
729                        serviceState = new ServiceState(
730                                userState.inputMap.get(inputId), resolvedUserId);
731                        userState.serviceStateMap.put(inputId, serviceState);
732                    }
733                    // Send a null token immediately while reconnecting.
734                    if (serviceState.mReconnecting == true) {
735                        sendSessionTokenToClientLocked(client, inputId, null, null, seq);
736                        return;
737                    }
738
739                    // Create a new session token and a session state.
740                    IBinder sessionToken = new Binder();
741                    SessionState sessionState = new SessionState(sessionToken, inputId, client,
742                            seq, callingUid, resolvedUserId);
743
744                    // Add them to the global session state map of the current user.
745                    userState.sessionStateMap.put(sessionToken, sessionState);
746
747                    // Also, add them to the session state map of the current service.
748                    serviceState.mSessionTokens.add(sessionToken);
749
750                    if (serviceState.mService != null) {
751                        createSessionInternalLocked(serviceState.mService, sessionToken,
752                                resolvedUserId);
753                    } else {
754                        updateServiceConnectionLocked(inputId, resolvedUserId);
755                    }
756                }
757            } finally {
758                Binder.restoreCallingIdentity(identity);
759            }
760        }
761
762        @Override
763        public void releaseSession(IBinder sessionToken, int userId) {
764            final int callingUid = Binder.getCallingUid();
765            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
766                    userId, "releaseSession");
767            final long identity = Binder.clearCallingIdentity();
768            try {
769                synchronized (mLock) {
770                    releaseSessionLocked(sessionToken, callingUid, resolvedUserId);
771                }
772            } finally {
773                Binder.restoreCallingIdentity(identity);
774            }
775        }
776
777        @Override
778        public void setSurface(IBinder sessionToken, Surface surface, int userId) {
779            final int callingUid = Binder.getCallingUid();
780            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
781                    userId, "setSurface");
782            final long identity = Binder.clearCallingIdentity();
783            try {
784                synchronized (mLock) {
785                    try {
786                        getSessionLocked(sessionToken, callingUid, resolvedUserId).setSurface(
787                                surface);
788                    } catch (RemoteException e) {
789                        Slog.e(TAG, "error in setSurface", e);
790                    }
791                }
792            } finally {
793                if (surface != null) {
794                    // surface is not used in TvInputManagerService.
795                    surface.release();
796                }
797                Binder.restoreCallingIdentity(identity);
798            }
799        }
800
801        @Override
802        public void setVolume(IBinder sessionToken, float volume, int userId) {
803            final int callingUid = Binder.getCallingUid();
804            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
805                    userId, "setVolume");
806            final long identity = Binder.clearCallingIdentity();
807            try {
808                synchronized (mLock) {
809                    try {
810                        getSessionLocked(sessionToken, callingUid, resolvedUserId).setVolume(
811                                volume);
812                    } catch (RemoteException e) {
813                        Slog.e(TAG, "error in setVolume", e);
814                    }
815                }
816            } finally {
817                Binder.restoreCallingIdentity(identity);
818            }
819        }
820
821        @Override
822        public void tune(IBinder sessionToken, final Uri channelUri, int userId) {
823            final int callingUid = Binder.getCallingUid();
824            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
825                    userId, "tune");
826            final long identity = Binder.clearCallingIdentity();
827            try {
828                synchronized (mLock) {
829                    try {
830                        getSessionLocked(sessionToken, callingUid, resolvedUserId).tune(channelUri);
831
832                        long currentTime = System.currentTimeMillis();
833                        long channelId = ContentUris.parseId(channelUri);
834
835                        // Close the open log entry first, if any.
836                        UserState userState = getUserStateLocked(resolvedUserId);
837                        SessionState sessionState = userState.sessionStateMap.get(sessionToken);
838                        if (sessionState.mLogUri != null) {
839                            SomeArgs args = SomeArgs.obtain();
840                            args.arg1 = sessionState.mLogUri;
841                            args.arg2 = currentTime;
842                            mLogHandler.obtainMessage(LogHandler.MSG_CLOSE_ENTRY, args)
843                                    .sendToTarget();
844                        }
845
846                        // Create a log entry and fill it later.
847                        String packageName = userState.inputMap.get(sessionState.mInputId)
848                                .getServiceInfo().packageName;
849                        ContentValues values = new ContentValues();
850                        values.put(TvContract.WatchedPrograms.COLUMN_PACKAGE_NAME, packageName);
851                        values.put(TvContract.WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS,
852                                currentTime);
853                        values.put(TvContract.WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS, 0);
854                        values.put(TvContract.WatchedPrograms.COLUMN_CHANNEL_ID, channelId);
855
856                        sessionState.mLogUri = mContentResolver.insert(
857                                TvContract.WatchedPrograms.CONTENT_URI, values);
858                        SomeArgs args = SomeArgs.obtain();
859                        args.arg1 = sessionState.mLogUri;
860                        args.arg2 = ContentUris.parseId(channelUri);
861                        args.arg3 = currentTime;
862                        mLogHandler.obtainMessage(LogHandler.MSG_OPEN_ENTRY, args).sendToTarget();
863                    } catch (RemoteException e) {
864                        Slog.e(TAG, "error in tune", e);
865                        return;
866                    }
867                }
868            } finally {
869                Binder.restoreCallingIdentity(identity);
870            }
871        }
872
873        @Override
874        public void createOverlayView(IBinder sessionToken, IBinder windowToken, Rect frame,
875                int userId) {
876            final int callingUid = Binder.getCallingUid();
877            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
878                    userId, "createOverlayView");
879            final long identity = Binder.clearCallingIdentity();
880            try {
881                synchronized (mLock) {
882                    try {
883                        getSessionLocked(sessionToken, callingUid, resolvedUserId)
884                                .createOverlayView(windowToken, frame);
885                    } catch (RemoteException e) {
886                        Slog.e(TAG, "error in createOverlayView", e);
887                    }
888                }
889            } finally {
890                Binder.restoreCallingIdentity(identity);
891            }
892        }
893
894        @Override
895        public void relayoutOverlayView(IBinder sessionToken, Rect frame, int userId) {
896            final int callingUid = Binder.getCallingUid();
897            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
898                    userId, "relayoutOverlayView");
899            final long identity = Binder.clearCallingIdentity();
900            try {
901                synchronized (mLock) {
902                    try {
903                        getSessionLocked(sessionToken, callingUid, resolvedUserId)
904                                .relayoutOverlayView(frame);
905                    } catch (RemoteException e) {
906                        Slog.e(TAG, "error in relayoutOverlayView", e);
907                    }
908                }
909            } finally {
910                Binder.restoreCallingIdentity(identity);
911            }
912        }
913
914        @Override
915        public void removeOverlayView(IBinder sessionToken, int userId) {
916            final int callingUid = Binder.getCallingUid();
917            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
918                    userId, "removeOverlayView");
919            final long identity = Binder.clearCallingIdentity();
920            try {
921                synchronized (mLock) {
922                    try {
923                        getSessionLocked(sessionToken, callingUid, resolvedUserId)
924                                .removeOverlayView();
925                    } catch (RemoteException e) {
926                        Slog.e(TAG, "error in removeOverlayView", e);
927                    }
928                }
929            } finally {
930                Binder.restoreCallingIdentity(identity);
931            }
932        }
933
934        @Override
935        public List<TvInputHardwareInfo> getHardwareList() throws RemoteException {
936            if (mContext.checkCallingPermission(
937                    android.Manifest.permission.TV_INPUT_HARDWARE)
938                    != PackageManager.PERMISSION_GRANTED) {
939                return null;
940            }
941
942            final long identity = Binder.clearCallingIdentity();
943            try {
944                return mTvInputHardwareManager.getHardwareList();
945            } finally {
946                Binder.restoreCallingIdentity(identity);
947            }
948        }
949
950        @Override
951        public ITvInputHardware acquireTvInputHardware(int deviceId,
952                ITvInputHardwareCallback callback, int userId) throws RemoteException {
953            if (mContext.checkCallingPermission(
954                    android.Manifest.permission.TV_INPUT_HARDWARE)
955                    != PackageManager.PERMISSION_GRANTED) {
956                return null;
957            }
958
959            final long identity = Binder.clearCallingIdentity();
960            final int callingUid = Binder.getCallingUid();
961            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
962                    userId, "acquireTvInputHardware");
963            try {
964                return mTvInputHardwareManager.acquireHardware(
965                        deviceId, callback, callingUid, resolvedUserId);
966            } finally {
967                Binder.restoreCallingIdentity(identity);
968            }
969        }
970
971        @Override
972        public void releaseTvInputHardware(int deviceId, ITvInputHardware hardware, int userId)
973                throws RemoteException {
974            if (mContext.checkCallingPermission(
975                    android.Manifest.permission.TV_INPUT_HARDWARE)
976                    != PackageManager.PERMISSION_GRANTED) {
977                return;
978            }
979
980            final long identity = Binder.clearCallingIdentity();
981            final int callingUid = Binder.getCallingUid();
982            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
983                    userId, "releaseTvInputHardware");
984            try {
985                mTvInputHardwareManager.releaseHardware(
986                        deviceId, hardware, callingUid, resolvedUserId);
987            } finally {
988                Binder.restoreCallingIdentity(identity);
989            }
990        }
991    }
992
993    private static final class UserState {
994        // A mapping from the TV input id to its TvInputInfo.
995        private final Map<String, TvInputInfo> inputMap = new HashMap<String,TvInputInfo>();
996
997        // A list of all TV input packages.
998        private final Set<String> packageList = new HashSet<String>();
999
1000        // A mapping from the token of a client to its state.
1001        private final Map<IBinder, ClientState> clientStateMap =
1002                new HashMap<IBinder, ClientState>();
1003
1004        // A mapping from the name of a TV input service to its state.
1005        private final Map<String, ServiceState> serviceStateMap =
1006                new HashMap<String, ServiceState>();
1007
1008        // A mapping from the token of a TV input session to its state.
1009        private final Map<IBinder, SessionState> sessionStateMap =
1010                new HashMap<IBinder, SessionState>();
1011    }
1012
1013    private final class ClientState implements IBinder.DeathRecipient {
1014        private final List<String> mInputIds = new ArrayList<String>();
1015        private final List<IBinder> mSessionTokens = new ArrayList<IBinder>();
1016
1017        private IBinder mClientToken;
1018        private final int mUserId;
1019
1020        ClientState(IBinder clientToken, int userId) {
1021            mClientToken = clientToken;
1022            mUserId = userId;
1023        }
1024
1025        public boolean isEmpty() {
1026            return mInputIds.isEmpty() && mSessionTokens.isEmpty();
1027        }
1028
1029        @Override
1030        public void binderDied() {
1031            synchronized (mLock) {
1032                UserState userState = getUserStateLocked(mUserId);
1033                // DO NOT remove the client state of clientStateMap in this method. It will be
1034                // removed in releaseSessionLocked() or unregisterCallbackInternalLocked().
1035                ClientState clientState = userState.clientStateMap.get(mClientToken);
1036                if (clientState != null) {
1037                    while (clientState.mSessionTokens.size() > 0) {
1038                        releaseSessionLocked(
1039                                clientState.mSessionTokens.get(0), Process.SYSTEM_UID, mUserId);
1040                    }
1041                    while (clientState.mInputIds.size() > 0) {
1042                        unregisterCallbackInternalLocked(
1043                                mClientToken, clientState.mInputIds.get(0), mUserId);
1044                    }
1045                }
1046                mClientToken = null;
1047            }
1048        }
1049    }
1050
1051    private final class ServiceState {
1052        private final List<IBinder> mClientTokens = new ArrayList<IBinder>();
1053        private final List<IBinder> mSessionTokens = new ArrayList<IBinder>();
1054        private final ServiceConnection mConnection;
1055        private final TvInputInfo mTvInputInfo;
1056
1057        private ITvInputService mService;
1058        private ServiceCallback mCallback;
1059        private boolean mBound;
1060        private boolean mAvailable;
1061        private boolean mReconnecting;
1062
1063        private ServiceState(TvInputInfo inputInfo, int userId) {
1064            mTvInputInfo = inputInfo;
1065            mConnection = new InputServiceConnection(inputInfo, userId);
1066        }
1067    }
1068
1069    private final class SessionState implements IBinder.DeathRecipient {
1070        private final String mInputId;
1071        private final ITvInputClient mClient;
1072        private final int mSeq;
1073        private final int mCallingUid;
1074        private final int mUserId;
1075        private final IBinder mSessionToken;
1076        private ITvInputSession mSession;
1077        private Uri mLogUri;
1078
1079        private SessionState(IBinder sessionToken, String inputId, ITvInputClient client, int seq,
1080                int callingUid, int userId) {
1081            mSessionToken = sessionToken;
1082            mInputId = inputId;
1083            mClient = client;
1084            mSeq = seq;
1085            mCallingUid = callingUid;
1086            mUserId = userId;
1087        }
1088
1089        @Override
1090        public void binderDied() {
1091            synchronized (mLock) {
1092                mSession = null;
1093                if (mClient != null) {
1094                    try {
1095                        mClient.onSessionReleased(mSeq);
1096                    } catch(RemoteException e) {
1097                        Slog.e(TAG, "error in onSessionReleased", e);
1098                    }
1099                }
1100                removeSessionStateLocked(mSessionToken, mUserId);
1101            }
1102        }
1103    }
1104
1105    private final class InputServiceConnection implements ServiceConnection {
1106        private final TvInputInfo mTvInputInfo;
1107        private final int mUserId;
1108
1109        private InputServiceConnection(TvInputInfo inputInfo, int userId) {
1110            mUserId = userId;
1111            mTvInputInfo = inputInfo;
1112        }
1113
1114        @Override
1115        public void onServiceConnected(ComponentName name, IBinder service) {
1116            if (DEBUG) {
1117                Slog.d(TAG, "onServiceConnected(inputId=" + mTvInputInfo.getId() + ")");
1118            }
1119            synchronized (mLock) {
1120                ServiceState serviceState = getServiceStateLocked(mTvInputInfo.getId(), mUserId);
1121                serviceState.mService = ITvInputService.Stub.asInterface(service);
1122
1123                // Register a callback, if we need to.
1124                if (!serviceState.mClientTokens.isEmpty() && serviceState.mCallback == null) {
1125                    serviceState.mCallback = new ServiceCallback(mUserId);
1126                    try {
1127                        serviceState.mService.registerCallback(serviceState.mCallback);
1128                    } catch (RemoteException e) {
1129                        Slog.e(TAG, "error in registerCallback", e);
1130                    }
1131                }
1132
1133                // And create sessions, if any.
1134                for (IBinder sessionToken : serviceState.mSessionTokens) {
1135                    createSessionInternalLocked(serviceState.mService, sessionToken, mUserId);
1136                }
1137            }
1138        }
1139
1140        @Override
1141        public void onServiceDisconnected(ComponentName name) {
1142            if (DEBUG) {
1143                Slog.d(TAG, "onServiceDisconnected(inputId=" + mTvInputInfo.getId() + ")");
1144            }
1145            if (!mTvInputInfo.getComponent().equals(name)) {
1146                throw new IllegalArgumentException("Mismatched ComponentName: "
1147                        + mTvInputInfo.getComponent() + " (expected), " + name + " (actual).");
1148            }
1149            synchronized (mLock) {
1150                UserState userState = getUserStateLocked(mUserId);
1151                ServiceState serviceState = userState.serviceStateMap.get(mTvInputInfo.getId());
1152                if (serviceState != null) {
1153                    serviceState.mReconnecting = true;
1154                    serviceState.mBound = false;
1155                    serviceState.mService = null;
1156                    serviceState.mCallback = null;
1157
1158                    // Send null tokens for not finishing create session events.
1159                    for (IBinder sessionToken : serviceState.mSessionTokens) {
1160                        SessionState sessionState = userState.sessionStateMap.get(sessionToken);
1161                        if (sessionState.mSession == null) {
1162                            removeSessionStateLocked(sessionToken, sessionState.mUserId);
1163                            sendSessionTokenToClientLocked(sessionState.mClient,
1164                                    sessionState.mInputId, null, null, sessionState.mSeq);
1165                        }
1166                    }
1167
1168                    if (serviceState.mAvailable) {
1169                        serviceState.mAvailable = false;
1170                        broadcastServiceAvailabilityChangedLocked(serviceState);
1171                    }
1172                    updateServiceConnectionLocked(mTvInputInfo.getId(), mUserId);
1173                }
1174            }
1175        }
1176    }
1177
1178    private final class ServiceCallback extends ITvInputServiceCallback.Stub {
1179        private final int mUserId;
1180
1181        ServiceCallback(int userId) {
1182            mUserId = userId;
1183        }
1184
1185        @Override
1186        public void onAvailabilityChanged(String inputId, boolean isAvailable) {
1187            if (DEBUG) {
1188                Slog.d(TAG, "onAvailabilityChanged(inputId=" + inputId + ", isAvailable="
1189                        + isAvailable + ")");
1190            }
1191            synchronized (mLock) {
1192                ServiceState serviceState = getServiceStateLocked(inputId, mUserId);
1193                if (serviceState.mAvailable != isAvailable) {
1194                    serviceState.mAvailable = isAvailable;
1195                    broadcastServiceAvailabilityChangedLocked(serviceState);
1196                }
1197            }
1198        }
1199    }
1200
1201    private final class LogHandler extends Handler {
1202        private static final int MSG_OPEN_ENTRY = 1;
1203        private static final int MSG_UPDATE_ENTRY = 2;
1204        private static final int MSG_CLOSE_ENTRY = 3;
1205
1206        public LogHandler(Looper looper) {
1207            super(looper);
1208        }
1209
1210        @Override
1211        public void handleMessage(Message msg) {
1212            switch (msg.what) {
1213                case MSG_OPEN_ENTRY: {
1214                    SomeArgs args = (SomeArgs) msg.obj;
1215                    Uri uri = (Uri) args.arg1;
1216                    long channelId = (long) args.arg2;
1217                    long time = (long) args.arg3;
1218                    onOpenEntry(uri, channelId, time);
1219                    args.recycle();
1220                    return;
1221                }
1222                case MSG_UPDATE_ENTRY: {
1223                    SomeArgs args = (SomeArgs) msg.obj;
1224                    Uri uri = (Uri) args.arg1;
1225                    long channelId = (long) args.arg2;
1226                    long time = (long) args.arg3;
1227                    onUpdateEntry(uri, channelId, time);
1228                    args.recycle();
1229                    return;
1230                }
1231                case MSG_CLOSE_ENTRY: {
1232                    SomeArgs args = (SomeArgs) msg.obj;
1233                    Uri uri = (Uri) args.arg1;
1234                    long time = (long) args.arg2;
1235                    onCloseEntry(uri, time);
1236                    args.recycle();
1237                    return;
1238                }
1239                default: {
1240                    Slog.w(TAG, "Unhandled message code: " + msg.what);
1241                    return;
1242                }
1243            }
1244        }
1245
1246        private void onOpenEntry(Uri uri, long channelId, long watchStarttime) {
1247            String[] projection = {
1248                    TvContract.Programs.COLUMN_TITLE,
1249                    TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS,
1250                    TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS,
1251                    TvContract.Programs.COLUMN_SHORT_DESCRIPTION
1252            };
1253            String selection = TvContract.Programs.COLUMN_CHANNEL_ID + "=? AND "
1254                    + TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS + "<=? AND "
1255                    + TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS + ">?";
1256            String[] selectionArgs = {
1257                    String.valueOf(channelId),
1258                    String.valueOf(watchStarttime),
1259                    String.valueOf(watchStarttime)
1260            };
1261            String sortOrder = TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS + " ASC";
1262            Cursor cursor = null;
1263            try {
1264                cursor = mContentResolver.query(TvContract.Programs.CONTENT_URI, projection,
1265                        selection, selectionArgs, sortOrder);
1266                if (cursor != null && cursor.moveToNext()) {
1267                    ContentValues values = new ContentValues();
1268                    values.put(TvContract.WatchedPrograms.COLUMN_TITLE, cursor.getString(0));
1269                    values.put(TvContract.WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS,
1270                            cursor.getLong(1));
1271                    long endTime = cursor.getLong(2);
1272                    values.put(TvContract.WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS, endTime);
1273                    values.put(TvContract.WatchedPrograms.COLUMN_DESCRIPTION, cursor.getString(3));
1274                    mContentResolver.update(uri, values, null, null);
1275
1276                    // Schedule an update when the current program ends.
1277                    SomeArgs args = SomeArgs.obtain();
1278                    args.arg1 = uri;
1279                    args.arg2 = channelId;
1280                    args.arg3 = endTime;
1281                    Message msg = obtainMessage(LogHandler.MSG_UPDATE_ENTRY, args);
1282                    sendMessageDelayed(msg, endTime - System.currentTimeMillis());
1283                }
1284            } finally {
1285                if (cursor != null) {
1286                    cursor.close();
1287                }
1288            }
1289        }
1290
1291        private void onUpdateEntry(Uri uri, long channelId, long time) {
1292            String[] projection = {
1293                    TvContract.WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS,
1294                    TvContract.WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS,
1295                    TvContract.WatchedPrograms.COLUMN_TITLE,
1296                    TvContract.WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS,
1297                    TvContract.WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS,
1298                    TvContract.WatchedPrograms.COLUMN_DESCRIPTION
1299            };
1300            Cursor cursor = null;
1301            try {
1302                cursor = mContentResolver.query(uri, projection, null, null, null);
1303                if (cursor != null && cursor.moveToNext()) {
1304                    long watchStartTime = cursor.getLong(0);
1305                    long watchEndTime = cursor.getLong(1);
1306                    String title = cursor.getString(2);
1307                    long startTime = cursor.getLong(3);
1308                    long endTime = cursor.getLong(4);
1309                    String description = cursor.getString(5);
1310
1311                    // Do nothing if the current log entry is already closed.
1312                    if (watchEndTime > 0) {
1313                        return;
1314                    }
1315
1316                    // The current program has just ended. Create a (complete) log entry off the
1317                    // current entry.
1318                    ContentValues values = new ContentValues();
1319                    values.put(TvContract.WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS,
1320                            watchStartTime);
1321                    values.put(TvContract.WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS, time);
1322                    values.put(TvContract.WatchedPrograms.COLUMN_CHANNEL_ID, channelId);
1323                    values.put(TvContract.WatchedPrograms.COLUMN_TITLE, title);
1324                    values.put(TvContract.WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS, startTime);
1325                    values.put(TvContract.WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS, endTime);
1326                    values.put(TvContract.WatchedPrograms.COLUMN_DESCRIPTION, description);
1327                    mContentResolver.insert(TvContract.WatchedPrograms.CONTENT_URI, values);
1328                }
1329            } finally {
1330                if (cursor != null) {
1331                    cursor.close();
1332                }
1333            }
1334            // Re-open the current log entry with the next program information.
1335            onOpenEntry(uri, channelId, time);
1336        }
1337
1338        private void onCloseEntry(Uri uri, long watchEndTime) {
1339            ContentValues values = new ContentValues();
1340            values.put(TvContract.WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS, watchEndTime);
1341            mContentResolver.update(uri, values, null, null);
1342        }
1343    }
1344}
1345