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