TvInputManagerService.java revision 5c80ad2077f3e755413ea47a35f51e9d25dbb083
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            return false;
635        }
636
637        @Override
638        public void registerCallback(final ITvInputClient client, final String inputId,
639                int userId) {
640            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
641                    Binder.getCallingUid(), userId, "registerCallback");
642            final long identity = Binder.clearCallingIdentity();
643            try {
644                synchronized (mLock) {
645                    // Create a new service callback and add it to the callback map of the current
646                    // service.
647                    UserState userState = getUserStateLocked(resolvedUserId);
648                    ServiceState serviceState = userState.serviceStateMap.get(inputId);
649                    if (serviceState == null) {
650                        serviceState = new ServiceState(
651                                userState.inputMap.get(inputId), resolvedUserId);
652                        userState.serviceStateMap.put(inputId, serviceState);
653                    }
654                    IBinder clientToken = client.asBinder();
655                    if (!serviceState.mClientTokens.contains(clientToken)) {
656                        serviceState.mClientTokens.add(clientToken);
657                    }
658
659                    ClientState clientState = userState.clientStateMap.get(clientToken);
660                    if (clientState == null) {
661                        clientState = createClientStateLocked(clientToken, resolvedUserId);
662                    }
663                    if (!clientState.mInputIds.contains(inputId)) {
664                        clientState.mInputIds.add(inputId);
665                    }
666
667                    if (serviceState.mService != null) {
668                        if (serviceState.mCallback != null) {
669                            // We already handled.
670                            return;
671                        }
672                        serviceState.mCallback = new ServiceCallback(resolvedUserId);
673                        try {
674                            serviceState.mService.registerCallback(serviceState.mCallback);
675                        } catch (RemoteException e) {
676                            Slog.e(TAG, "error in registerCallback", e);
677                        }
678                    } else {
679                        updateServiceConnectionLocked(inputId, resolvedUserId);
680                    }
681                }
682            } finally {
683                Binder.restoreCallingIdentity(identity);
684            }
685        }
686
687        @Override
688        public void unregisterCallback(ITvInputClient client, String inputId, int userId) {
689            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
690                    Binder.getCallingUid(), userId, "unregisterCallback");
691            final long identity = Binder.clearCallingIdentity();
692            try {
693                synchronized (mLock) {
694                    unregisterCallbackInternalLocked(client.asBinder(), inputId, resolvedUserId);
695                }
696            } finally {
697                Binder.restoreCallingIdentity(identity);
698            }
699        }
700
701        @Override
702        public void createSession(final ITvInputClient client, final String inputId,
703                int seq, int userId) {
704            final int callingUid = Binder.getCallingUid();
705            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
706                    userId, "createSession");
707            final long identity = Binder.clearCallingIdentity();
708            try {
709                synchronized (mLock) {
710                    UserState userState = getUserStateLocked(resolvedUserId);
711                    ServiceState serviceState = userState.serviceStateMap.get(inputId);
712                    if (serviceState == null) {
713                        serviceState = new ServiceState(
714                                userState.inputMap.get(inputId), resolvedUserId);
715                        userState.serviceStateMap.put(inputId, serviceState);
716                    }
717                    // Send a null token immediately while reconnecting.
718                    if (serviceState.mReconnecting == true) {
719                        sendSessionTokenToClientLocked(client, inputId, null, null, seq);
720                        return;
721                    }
722
723                    // Create a new session token and a session state.
724                    IBinder sessionToken = new Binder();
725                    SessionState sessionState = new SessionState(sessionToken, inputId, client,
726                            seq, callingUid, resolvedUserId);
727
728                    // Add them to the global session state map of the current user.
729                    userState.sessionStateMap.put(sessionToken, sessionState);
730
731                    // Also, add them to the session state map of the current service.
732                    serviceState.mSessionTokens.add(sessionToken);
733
734                    if (serviceState.mService != null) {
735                        createSessionInternalLocked(serviceState.mService, sessionToken,
736                                resolvedUserId);
737                    } else {
738                        updateServiceConnectionLocked(inputId, resolvedUserId);
739                    }
740                }
741            } finally {
742                Binder.restoreCallingIdentity(identity);
743            }
744        }
745
746        @Override
747        public void releaseSession(IBinder sessionToken, int userId) {
748            final int callingUid = Binder.getCallingUid();
749            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
750                    userId, "releaseSession");
751            final long identity = Binder.clearCallingIdentity();
752            try {
753                synchronized (mLock) {
754                    releaseSessionLocked(sessionToken, callingUid, resolvedUserId);
755                }
756            } finally {
757                Binder.restoreCallingIdentity(identity);
758            }
759        }
760
761        @Override
762        public void setSurface(IBinder sessionToken, Surface surface, int userId) {
763            final int callingUid = Binder.getCallingUid();
764            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
765                    userId, "setSurface");
766            final long identity = Binder.clearCallingIdentity();
767            try {
768                synchronized (mLock) {
769                    try {
770                        getSessionLocked(sessionToken, callingUid, resolvedUserId).setSurface(
771                                surface);
772                    } catch (RemoteException e) {
773                        Slog.e(TAG, "error in setSurface", e);
774                    }
775                }
776            } finally {
777                if (surface != null) {
778                    // surface is not used in TvInputManagerService.
779                    surface.release();
780                }
781                Binder.restoreCallingIdentity(identity);
782            }
783        }
784
785        @Override
786        public void setVolume(IBinder sessionToken, float volume, int userId) {
787            final int callingUid = Binder.getCallingUid();
788            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
789                    userId, "setVolume");
790            final long identity = Binder.clearCallingIdentity();
791            try {
792                synchronized (mLock) {
793                    try {
794                        getSessionLocked(sessionToken, callingUid, resolvedUserId).setVolume(
795                                volume);
796                    } catch (RemoteException e) {
797                        Slog.e(TAG, "error in setVolume", e);
798                    }
799                }
800            } finally {
801                Binder.restoreCallingIdentity(identity);
802            }
803        }
804
805        @Override
806        public void tune(IBinder sessionToken, final Uri channelUri, int userId) {
807            final int callingUid = Binder.getCallingUid();
808            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
809                    userId, "tune");
810            final long identity = Binder.clearCallingIdentity();
811            try {
812                synchronized (mLock) {
813                    try {
814                        getSessionLocked(sessionToken, callingUid, resolvedUserId).tune(channelUri);
815
816                        long currentTime = System.currentTimeMillis();
817                        long channelId = ContentUris.parseId(channelUri);
818
819                        // Close the open log entry first, if any.
820                        UserState userState = getUserStateLocked(resolvedUserId);
821                        SessionState sessionState = userState.sessionStateMap.get(sessionToken);
822                        if (sessionState.mLogUri != null) {
823                            SomeArgs args = SomeArgs.obtain();
824                            args.arg1 = sessionState.mLogUri;
825                            args.arg2 = currentTime;
826                            mLogHandler.obtainMessage(LogHandler.MSG_CLOSE_ENTRY, args)
827                                    .sendToTarget();
828                        }
829
830                        // Create a log entry and fill it later.
831                        String packageName = userState.inputMap.get(sessionState.mInputId)
832                                .getServiceInfo().packageName;
833                        ContentValues values = new ContentValues();
834                        values.put(TvContract.WatchedPrograms.COLUMN_PACKAGE_NAME, packageName);
835                        values.put(TvContract.WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS,
836                                currentTime);
837                        values.put(TvContract.WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS, 0);
838                        values.put(TvContract.WatchedPrograms.COLUMN_CHANNEL_ID, channelId);
839
840                        sessionState.mLogUri = mContentResolver.insert(
841                                TvContract.WatchedPrograms.CONTENT_URI, values);
842                        SomeArgs args = SomeArgs.obtain();
843                        args.arg1 = sessionState.mLogUri;
844                        args.arg2 = ContentUris.parseId(channelUri);
845                        args.arg3 = currentTime;
846                        mLogHandler.obtainMessage(LogHandler.MSG_OPEN_ENTRY, args).sendToTarget();
847                    } catch (RemoteException e) {
848                        Slog.e(TAG, "error in tune", e);
849                        return;
850                    }
851                }
852            } finally {
853                Binder.restoreCallingIdentity(identity);
854            }
855        }
856
857        @Override
858        public void createOverlayView(IBinder sessionToken, IBinder windowToken, Rect frame,
859                int userId) {
860            final int callingUid = Binder.getCallingUid();
861            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
862                    userId, "createOverlayView");
863            final long identity = Binder.clearCallingIdentity();
864            try {
865                synchronized (mLock) {
866                    try {
867                        getSessionLocked(sessionToken, callingUid, resolvedUserId)
868                                .createOverlayView(windowToken, frame);
869                    } catch (RemoteException e) {
870                        Slog.e(TAG, "error in createOverlayView", e);
871                    }
872                }
873            } finally {
874                Binder.restoreCallingIdentity(identity);
875            }
876        }
877
878        @Override
879        public void relayoutOverlayView(IBinder sessionToken, Rect frame, int userId) {
880            final int callingUid = Binder.getCallingUid();
881            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
882                    userId, "relayoutOverlayView");
883            final long identity = Binder.clearCallingIdentity();
884            try {
885                synchronized (mLock) {
886                    try {
887                        getSessionLocked(sessionToken, callingUid, resolvedUserId)
888                                .relayoutOverlayView(frame);
889                    } catch (RemoteException e) {
890                        Slog.e(TAG, "error in relayoutOverlayView", e);
891                    }
892                }
893            } finally {
894                Binder.restoreCallingIdentity(identity);
895            }
896        }
897
898        @Override
899        public void removeOverlayView(IBinder sessionToken, int userId) {
900            final int callingUid = Binder.getCallingUid();
901            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
902                    userId, "removeOverlayView");
903            final long identity = Binder.clearCallingIdentity();
904            try {
905                synchronized (mLock) {
906                    try {
907                        getSessionLocked(sessionToken, callingUid, resolvedUserId)
908                                .removeOverlayView();
909                    } catch (RemoteException e) {
910                        Slog.e(TAG, "error in removeOverlayView", e);
911                    }
912                }
913            } finally {
914                Binder.restoreCallingIdentity(identity);
915            }
916        }
917
918        @Override
919        public List<TvInputHardwareInfo> getHardwareList() throws RemoteException {
920            if (mContext.checkCallingPermission(
921                    android.Manifest.permission.TV_INPUT_HARDWARE)
922                    != PackageManager.PERMISSION_GRANTED) {
923                return null;
924            }
925
926            final long identity = Binder.clearCallingIdentity();
927            try {
928                return mTvInputHardwareManager.getHardwareList();
929            } finally {
930                Binder.restoreCallingIdentity(identity);
931            }
932        }
933
934        @Override
935        public ITvInputHardware acquireTvInputHardware(int deviceId,
936                ITvInputHardwareCallback callback, int userId) throws RemoteException {
937            if (mContext.checkCallingPermission(
938                    android.Manifest.permission.TV_INPUT_HARDWARE)
939                    != PackageManager.PERMISSION_GRANTED) {
940                return null;
941            }
942
943            final long identity = Binder.clearCallingIdentity();
944            final int callingUid = Binder.getCallingUid();
945            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
946                    userId, "acquireTvInputHardware");
947            try {
948                return mTvInputHardwareManager.acquireHardware(
949                        deviceId, callback, callingUid, resolvedUserId);
950            } finally {
951                Binder.restoreCallingIdentity(identity);
952            }
953        }
954
955        @Override
956        public void releaseTvInputHardware(int deviceId, ITvInputHardware hardware, int userId)
957                throws RemoteException {
958            if (mContext.checkCallingPermission(
959                    android.Manifest.permission.TV_INPUT_HARDWARE)
960                    != PackageManager.PERMISSION_GRANTED) {
961                return;
962            }
963
964            final long identity = Binder.clearCallingIdentity();
965            final int callingUid = Binder.getCallingUid();
966            final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
967                    userId, "releaseTvInputHardware");
968            try {
969                mTvInputHardwareManager.releaseHardware(
970                        deviceId, hardware, callingUid, resolvedUserId);
971            } finally {
972                Binder.restoreCallingIdentity(identity);
973            }
974        }
975    }
976
977    private static final class UserState {
978        // A mapping from the TV input id to its TvInputInfo.
979        private final Map<String, TvInputInfo> inputMap = new HashMap<String,TvInputInfo>();
980
981        // A list of all TV input packages.
982        private final Set<String> packageList = new HashSet<String>();
983
984        // A mapping from the token of a client to its state.
985        private final Map<IBinder, ClientState> clientStateMap =
986                new HashMap<IBinder, ClientState>();
987
988        // A mapping from the name of a TV input service to its state.
989        private final Map<String, ServiceState> serviceStateMap =
990                new HashMap<String, ServiceState>();
991
992        // A mapping from the token of a TV input session to its state.
993        private final Map<IBinder, SessionState> sessionStateMap =
994                new HashMap<IBinder, SessionState>();
995    }
996
997    private final class ClientState implements IBinder.DeathRecipient {
998        private final List<String> mInputIds = new ArrayList<String>();
999        private final List<IBinder> mSessionTokens = new ArrayList<IBinder>();
1000
1001        private IBinder mClientToken;
1002        private final int mUserId;
1003
1004        ClientState(IBinder clientToken, int userId) {
1005            mClientToken = clientToken;
1006            mUserId = userId;
1007        }
1008
1009        public boolean isEmpty() {
1010            return mInputIds.isEmpty() && mSessionTokens.isEmpty();
1011        }
1012
1013        @Override
1014        public void binderDied() {
1015            synchronized (mLock) {
1016                UserState userState = getUserStateLocked(mUserId);
1017                // DO NOT remove the client state of clientStateMap in this method. It will be
1018                // removed in releaseSessionLocked() or unregisterCallbackInternalLocked().
1019                ClientState clientState = userState.clientStateMap.get(mClientToken);
1020                if (clientState != null) {
1021                    while (clientState.mSessionTokens.size() > 0) {
1022                        releaseSessionLocked(
1023                                clientState.mSessionTokens.get(0), Process.SYSTEM_UID, mUserId);
1024                    }
1025                    while (clientState.mInputIds.size() > 0) {
1026                        unregisterCallbackInternalLocked(
1027                                mClientToken, clientState.mInputIds.get(0), mUserId);
1028                    }
1029                }
1030                mClientToken = null;
1031            }
1032        }
1033    }
1034
1035    private final class ServiceState {
1036        private final List<IBinder> mClientTokens = new ArrayList<IBinder>();
1037        private final List<IBinder> mSessionTokens = new ArrayList<IBinder>();
1038        private final ServiceConnection mConnection;
1039        private final TvInputInfo mTvInputInfo;
1040
1041        private ITvInputService mService;
1042        private ServiceCallback mCallback;
1043        private boolean mBound;
1044        private boolean mAvailable;
1045        private boolean mReconnecting;
1046
1047        private ServiceState(TvInputInfo inputInfo, int userId) {
1048            mTvInputInfo = inputInfo;
1049            mConnection = new InputServiceConnection(inputInfo, userId);
1050        }
1051    }
1052
1053    private final class SessionState implements IBinder.DeathRecipient {
1054        private final String mInputId;
1055        private final ITvInputClient mClient;
1056        private final int mSeq;
1057        private final int mCallingUid;
1058        private final int mUserId;
1059        private final IBinder mSessionToken;
1060        private ITvInputSession mSession;
1061        private Uri mLogUri;
1062
1063        private SessionState(IBinder sessionToken, String inputId, ITvInputClient client, int seq,
1064                int callingUid, int userId) {
1065            mSessionToken = sessionToken;
1066            mInputId = inputId;
1067            mClient = client;
1068            mSeq = seq;
1069            mCallingUid = callingUid;
1070            mUserId = userId;
1071        }
1072
1073        @Override
1074        public void binderDied() {
1075            synchronized (mLock) {
1076                mSession = null;
1077                if (mClient != null) {
1078                    try {
1079                        mClient.onSessionReleased(mSeq);
1080                    } catch(RemoteException e) {
1081                        Slog.e(TAG, "error in onSessionReleased", e);
1082                    }
1083                }
1084                removeSessionStateLocked(mSessionToken, mUserId);
1085            }
1086        }
1087    }
1088
1089    private final class InputServiceConnection implements ServiceConnection {
1090        private final TvInputInfo mTvInputInfo;
1091        private final int mUserId;
1092
1093        private InputServiceConnection(TvInputInfo inputInfo, int userId) {
1094            mUserId = userId;
1095            mTvInputInfo = inputInfo;
1096        }
1097
1098        @Override
1099        public void onServiceConnected(ComponentName name, IBinder service) {
1100            if (DEBUG) {
1101                Slog.d(TAG, "onServiceConnected(inputId=" + mTvInputInfo.getId() + ")");
1102            }
1103            synchronized (mLock) {
1104                ServiceState serviceState = getServiceStateLocked(mTvInputInfo.getId(), mUserId);
1105                serviceState.mService = ITvInputService.Stub.asInterface(service);
1106
1107                // Register a callback, if we need to.
1108                if (!serviceState.mClientTokens.isEmpty() && serviceState.mCallback == null) {
1109                    serviceState.mCallback = new ServiceCallback(mUserId);
1110                    try {
1111                        serviceState.mService.registerCallback(serviceState.mCallback);
1112                    } catch (RemoteException e) {
1113                        Slog.e(TAG, "error in registerCallback", e);
1114                    }
1115                }
1116
1117                // And create sessions, if any.
1118                for (IBinder sessionToken : serviceState.mSessionTokens) {
1119                    createSessionInternalLocked(serviceState.mService, sessionToken, mUserId);
1120                }
1121            }
1122        }
1123
1124        @Override
1125        public void onServiceDisconnected(ComponentName name) {
1126            if (DEBUG) {
1127                Slog.d(TAG, "onServiceDisconnected(inputId=" + mTvInputInfo.getId() + ")");
1128            }
1129            if (!mTvInputInfo.getComponent().equals(name)) {
1130                throw new IllegalArgumentException("Mismatched ComponentName: "
1131                        + mTvInputInfo.getComponent() + " (expected), " + name + " (actual).");
1132            }
1133            synchronized (mLock) {
1134                UserState userState = getUserStateLocked(mUserId);
1135                ServiceState serviceState = userState.serviceStateMap.get(mTvInputInfo.getId());
1136                if (serviceState != null) {
1137                    serviceState.mReconnecting = true;
1138                    serviceState.mBound = false;
1139                    serviceState.mService = null;
1140                    serviceState.mCallback = null;
1141
1142                    // Send null tokens for not finishing create session events.
1143                    for (IBinder sessionToken : serviceState.mSessionTokens) {
1144                        SessionState sessionState = userState.sessionStateMap.get(sessionToken);
1145                        if (sessionState.mSession == null) {
1146                            removeSessionStateLocked(sessionToken, sessionState.mUserId);
1147                            sendSessionTokenToClientLocked(sessionState.mClient,
1148                                    sessionState.mInputId, null, null, sessionState.mSeq);
1149                        }
1150                    }
1151
1152                    if (serviceState.mAvailable) {
1153                        serviceState.mAvailable = false;
1154                        broadcastServiceAvailabilityChangedLocked(serviceState);
1155                    }
1156                    updateServiceConnectionLocked(mTvInputInfo.getId(), mUserId);
1157                }
1158            }
1159        }
1160    }
1161
1162    private final class ServiceCallback extends ITvInputServiceCallback.Stub {
1163        private final int mUserId;
1164
1165        ServiceCallback(int userId) {
1166            mUserId = userId;
1167        }
1168
1169        @Override
1170        public void onAvailabilityChanged(String inputId, boolean isAvailable) {
1171            if (DEBUG) {
1172                Slog.d(TAG, "onAvailabilityChanged(inputId=" + inputId + ", isAvailable="
1173                        + isAvailable + ")");
1174            }
1175            synchronized (mLock) {
1176                ServiceState serviceState = getServiceStateLocked(inputId, mUserId);
1177                if (serviceState.mAvailable != isAvailable) {
1178                    serviceState.mAvailable = isAvailable;
1179                    broadcastServiceAvailabilityChangedLocked(serviceState);
1180                }
1181            }
1182        }
1183    }
1184
1185    private final class LogHandler extends Handler {
1186        private static final int MSG_OPEN_ENTRY = 1;
1187        private static final int MSG_UPDATE_ENTRY = 2;
1188        private static final int MSG_CLOSE_ENTRY = 3;
1189
1190        public LogHandler(Looper looper) {
1191            super(looper);
1192        }
1193
1194        @Override
1195        public void handleMessage(Message msg) {
1196            switch (msg.what) {
1197                case MSG_OPEN_ENTRY: {
1198                    SomeArgs args = (SomeArgs) msg.obj;
1199                    Uri uri = (Uri) args.arg1;
1200                    long channelId = (long) args.arg2;
1201                    long time = (long) args.arg3;
1202                    onOpenEntry(uri, channelId, time);
1203                    args.recycle();
1204                    return;
1205                }
1206                case MSG_UPDATE_ENTRY: {
1207                    SomeArgs args = (SomeArgs) msg.obj;
1208                    Uri uri = (Uri) args.arg1;
1209                    long channelId = (long) args.arg2;
1210                    long time = (long) args.arg3;
1211                    onUpdateEntry(uri, channelId, time);
1212                    args.recycle();
1213                    return;
1214                }
1215                case MSG_CLOSE_ENTRY: {
1216                    SomeArgs args = (SomeArgs) msg.obj;
1217                    Uri uri = (Uri) args.arg1;
1218                    long time = (long) args.arg2;
1219                    onCloseEntry(uri, time);
1220                    args.recycle();
1221                    return;
1222                }
1223                default: {
1224                    Slog.w(TAG, "Unhandled message code: " + msg.what);
1225                    return;
1226                }
1227            }
1228        }
1229
1230        private void onOpenEntry(Uri uri, long channelId, long watchStarttime) {
1231            String[] projection = {
1232                    TvContract.Programs.COLUMN_TITLE,
1233                    TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS,
1234                    TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS,
1235                    TvContract.Programs.COLUMN_SHORT_DESCRIPTION
1236            };
1237            String selection = TvContract.Programs.COLUMN_CHANNEL_ID + "=? AND "
1238                    + TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS + "<=? AND "
1239                    + TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS + ">?";
1240            String[] selectionArgs = {
1241                    String.valueOf(channelId),
1242                    String.valueOf(watchStarttime),
1243                    String.valueOf(watchStarttime)
1244            };
1245            String sortOrder = TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS + " ASC";
1246            Cursor cursor = null;
1247            try {
1248                cursor = mContentResolver.query(TvContract.Programs.CONTENT_URI, projection,
1249                        selection, selectionArgs, sortOrder);
1250                if (cursor != null && cursor.moveToNext()) {
1251                    ContentValues values = new ContentValues();
1252                    values.put(TvContract.WatchedPrograms.COLUMN_TITLE, cursor.getString(0));
1253                    values.put(TvContract.WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS,
1254                            cursor.getLong(1));
1255                    long endTime = cursor.getLong(2);
1256                    values.put(TvContract.WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS, endTime);
1257                    values.put(TvContract.WatchedPrograms.COLUMN_DESCRIPTION, cursor.getString(3));
1258                    mContentResolver.update(uri, values, null, null);
1259
1260                    // Schedule an update when the current program ends.
1261                    SomeArgs args = SomeArgs.obtain();
1262                    args.arg1 = uri;
1263                    args.arg2 = channelId;
1264                    args.arg3 = endTime;
1265                    Message msg = obtainMessage(LogHandler.MSG_UPDATE_ENTRY, args);
1266                    sendMessageDelayed(msg, endTime - System.currentTimeMillis());
1267                }
1268            } finally {
1269                if (cursor != null) {
1270                    cursor.close();
1271                }
1272            }
1273        }
1274
1275        private void onUpdateEntry(Uri uri, long channelId, long time) {
1276            String[] projection = {
1277                    TvContract.WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS,
1278                    TvContract.WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS,
1279                    TvContract.WatchedPrograms.COLUMN_TITLE,
1280                    TvContract.WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS,
1281                    TvContract.WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS,
1282                    TvContract.WatchedPrograms.COLUMN_DESCRIPTION
1283            };
1284            Cursor cursor = null;
1285            try {
1286                cursor = mContentResolver.query(uri, projection, null, null, null);
1287                if (cursor != null && cursor.moveToNext()) {
1288                    long watchStartTime = cursor.getLong(0);
1289                    long watchEndTime = cursor.getLong(1);
1290                    String title = cursor.getString(2);
1291                    long startTime = cursor.getLong(3);
1292                    long endTime = cursor.getLong(4);
1293                    String description = cursor.getString(5);
1294
1295                    // Do nothing if the current log entry is already closed.
1296                    if (watchEndTime > 0) {
1297                        return;
1298                    }
1299
1300                    // The current program has just ended. Create a (complete) log entry off the
1301                    // current entry.
1302                    ContentValues values = new ContentValues();
1303                    values.put(TvContract.WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS,
1304                            watchStartTime);
1305                    values.put(TvContract.WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS, time);
1306                    values.put(TvContract.WatchedPrograms.COLUMN_CHANNEL_ID, channelId);
1307                    values.put(TvContract.WatchedPrograms.COLUMN_TITLE, title);
1308                    values.put(TvContract.WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS, startTime);
1309                    values.put(TvContract.WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS, endTime);
1310                    values.put(TvContract.WatchedPrograms.COLUMN_DESCRIPTION, description);
1311                    mContentResolver.insert(TvContract.WatchedPrograms.CONTENT_URI, values);
1312                }
1313            } finally {
1314                if (cursor != null) {
1315                    cursor.close();
1316                }
1317            }
1318            // Re-open the current log entry with the next program information.
1319            onOpenEntry(uri, channelId, time);
1320        }
1321
1322        private void onCloseEntry(Uri uri, long watchEndTime) {
1323            ContentValues values = new ContentValues();
1324            values.put(TvContract.WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS, watchEndTime);
1325            mContentResolver.update(uri, values, null, null);
1326        }
1327    }
1328}
1329