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