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