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 17 package com.android.server.telecom; 18 19 import android.Manifest; 20 import android.content.ComponentName; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.ServiceConnection; 24 import android.content.pm.PackageManager; 25 import android.content.pm.ResolveInfo; 26 import android.content.pm.ServiceInfo; 27 import android.content.res.Resources; 28 import android.net.Uri; 29 import android.os.IBinder; 30 import android.os.RemoteException; 31 import android.os.Trace; 32 import android.os.UserHandle; 33 import android.telecom.AudioState; 34 import android.telecom.CallProperties; 35 import android.telecom.CallState; 36 import android.telecom.Connection; 37 import android.telecom.InCallService; 38 import android.telecom.ParcelableCall; 39 import android.telecom.TelecomManager; 40 import android.util.ArrayMap; 41 42 // TODO: Needed for move to system service: import com.android.internal.R; 43 import com.android.internal.telecom.IInCallService; 44 import com.android.internal.util.IndentingPrintWriter; 45 46 import java.util.ArrayList; 47 import java.util.Collection; 48 import java.util.Iterator; 49 import java.util.List; 50 import java.util.Map; 51 import java.util.concurrent.ConcurrentHashMap; 52 53 /** 54 * Binds to {@link IInCallService} and provides the service to {@link CallsManager} through which it 55 * can send updates to the in-call app. This class is created and owned by CallsManager and retains 56 * a binding to the {@link IInCallService} (implemented by the in-call app). 57 */ 58 public final class InCallController extends CallsManagerListenerBase { 59 /** 60 * Used to bind to the in-call app and triggers the start of communication between 61 * this class and in-call app. 62 */ 63 private class InCallServiceConnection implements ServiceConnection { 64 /** {@inheritDoc} */ 65 @Override public void onServiceConnected(ComponentName name, IBinder service) { 66 Log.d(this, "onServiceConnected: %s", name); 67 onConnected(name, service); 68 } 69 70 /** {@inheritDoc} */ 71 @Override public void onServiceDisconnected(ComponentName name) { 72 Log.d(this, "onDisconnected: %s", name); 73 onDisconnected(name); 74 } 75 } 76 77 private final Call.Listener mCallListener = new Call.ListenerBase() { 78 @Override 79 public void onConnectionCapabilitiesChanged(Call call) { 80 updateCall(call); 81 } 82 83 @Override 84 public void onCannedSmsResponsesLoaded(Call call) { 85 updateCall(call); 86 } 87 88 @Override 89 public void onVideoCallProviderChanged(Call call) { 90 updateCall(call); 91 } 92 93 @Override 94 public void onStatusHintsChanged(Call call) { 95 updateCall(call); 96 } 97 98 @Override 99 public void onHandleChanged(Call call) { 100 updateCall(call); 101 } 102 103 @Override 104 public void onCallerDisplayNameChanged(Call call) { 105 updateCall(call); 106 } 107 108 @Override 109 public void onVideoStateChanged(Call call) { 110 updateCall(call); 111 } 112 113 @Override 114 public void onTargetPhoneAccountChanged(Call call) { 115 updateCall(call); 116 } 117 118 @Override 119 public void onConferenceableCallsChanged(Call call) { 120 updateCall(call); 121 } 122 }; 123 124 /** 125 * Maintains a binding connection to the in-call app(s). 126 * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is 127 * load factor before resizing, 1 means we only expect a single thread to 128 * access the map so make only a single shard 129 */ 130 private final Map<ComponentName, InCallServiceConnection> mServiceConnections = 131 new ConcurrentHashMap<ComponentName, InCallServiceConnection>(8, 0.9f, 1); 132 133 /** The in-call app implementations, see {@link IInCallService}. */ 134 private final Map<ComponentName, IInCallService> mInCallServices = new ArrayMap<>(); 135 136 private final CallIdMapper mCallIdMapper = new CallIdMapper("InCall"); 137 138 /** The {@link ComponentName} of the default InCall UI. */ 139 private final ComponentName mInCallComponentName; 140 141 private final Context mContext; 142 143 public InCallController(Context context) { 144 mContext = context; 145 Resources resources = mContext.getResources(); 146 147 mInCallComponentName = new ComponentName( 148 resources.getString(R.string.ui_default_package), 149 resources.getString(R.string.incall_default_class)); 150 } 151 152 @Override 153 public void onCallAdded(Call call) { 154 if (mInCallServices.isEmpty()) { 155 bind(call); 156 } else { 157 Log.i(this, "onCallAdded: %s", call); 158 // Track the call if we don't already know about it. 159 addCall(call); 160 161 for (Map.Entry<ComponentName, IInCallService> entry : mInCallServices.entrySet()) { 162 ComponentName componentName = entry.getKey(); 163 IInCallService inCallService = entry.getValue(); 164 165 ParcelableCall parcelableCall = toParcelableCall(call, 166 componentName.equals(mInCallComponentName) /* includeVideoProvider */); 167 try { 168 inCallService.addCall(parcelableCall); 169 } catch (RemoteException ignored) { 170 } 171 } 172 } 173 } 174 175 @Override 176 public void onCallRemoved(Call call) { 177 Log.i(this, "onCallRemoved: %s", call); 178 if (CallsManager.getInstance().getCalls().isEmpty()) { 179 // TODO: Wait for all messages to be delivered to the service before unbinding. 180 unbind(); 181 } 182 call.removeListener(mCallListener); 183 mCallIdMapper.removeCall(call); 184 } 185 186 @Override 187 public void onCallStateChanged(Call call, int oldState, int newState) { 188 updateCall(call); 189 } 190 191 @Override 192 public void onConnectionServiceChanged( 193 Call call, 194 ConnectionServiceWrapper oldService, 195 ConnectionServiceWrapper newService) { 196 updateCall(call); 197 } 198 199 @Override 200 public void onAudioStateChanged(AudioState oldAudioState, AudioState newAudioState) { 201 if (!mInCallServices.isEmpty()) { 202 Log.i(this, "Calling onAudioStateChanged, audioState: %s -> %s", oldAudioState, 203 newAudioState); 204 for (IInCallService inCallService : mInCallServices.values()) { 205 try { 206 inCallService.onAudioStateChanged(newAudioState); 207 } catch (RemoteException ignored) { 208 } 209 } 210 } 211 } 212 213 @Override 214 public void onCanAddCallChanged(boolean canAddCall) { 215 if (!mInCallServices.isEmpty()) { 216 Log.i(this, "onCanAddCallChanged : %b", canAddCall); 217 for (IInCallService inCallService : mInCallServices.values()) { 218 try { 219 inCallService.onCanAddCallChanged(canAddCall); 220 } catch (RemoteException ignored) { 221 } 222 } 223 } 224 } 225 226 void onPostDialWait(Call call, String remaining) { 227 if (!mInCallServices.isEmpty()) { 228 Log.i(this, "Calling onPostDialWait, remaining = %s", remaining); 229 for (IInCallService inCallService : mInCallServices.values()) { 230 try { 231 inCallService.setPostDialWait(mCallIdMapper.getCallId(call), remaining); 232 } catch (RemoteException ignored) { 233 } 234 } 235 } 236 } 237 238 @Override 239 public void onIsConferencedChanged(Call call) { 240 Log.d(this, "onIsConferencedChanged %s", call); 241 updateCall(call); 242 } 243 244 void bringToForeground(boolean showDialpad) { 245 if (!mInCallServices.isEmpty()) { 246 for (IInCallService inCallService : mInCallServices.values()) { 247 try { 248 inCallService.bringToForeground(showDialpad); 249 } catch (RemoteException ignored) { 250 } 251 } 252 } else { 253 Log.w(this, "Asking to bring unbound in-call UI to foreground."); 254 } 255 } 256 257 /** 258 * Unbinds an existing bound connection to the in-call app. 259 */ 260 private void unbind() { 261 ThreadUtil.checkOnMainThread(); 262 Iterator<Map.Entry<ComponentName, InCallServiceConnection>> iterator = 263 mServiceConnections.entrySet().iterator(); 264 while (iterator.hasNext()) { 265 Log.i(this, "Unbinding from InCallService %s"); 266 mContext.unbindService(iterator.next().getValue()); 267 iterator.remove(); 268 } 269 mInCallServices.clear(); 270 } 271 272 /** 273 * Binds to the in-call app if not already connected by binding directly to the saved 274 * component name of the {@link IInCallService} implementation. 275 * 276 * @param call The newly added call that triggered the binding to the in-call services. 277 */ 278 private void bind(Call call) { 279 ThreadUtil.checkOnMainThread(); 280 if (mInCallServices.isEmpty()) { 281 PackageManager packageManager = mContext.getPackageManager(); 282 Intent serviceIntent = new Intent(InCallService.SERVICE_INTERFACE); 283 284 for (ResolveInfo entry : packageManager.queryIntentServices(serviceIntent, 0)) { 285 ServiceInfo serviceInfo = entry.serviceInfo; 286 if (serviceInfo != null) { 287 boolean hasServiceBindPermission = serviceInfo.permission != null && 288 serviceInfo.permission.equals( 289 Manifest.permission.BIND_INCALL_SERVICE); 290 boolean hasControlInCallPermission = packageManager.checkPermission( 291 Manifest.permission.CONTROL_INCALL_EXPERIENCE, 292 serviceInfo.packageName) == PackageManager.PERMISSION_GRANTED; 293 294 if (!hasServiceBindPermission) { 295 Log.w(this, "InCallService does not have BIND_INCALL_SERVICE permission: " + 296 serviceInfo.packageName); 297 continue; 298 } 299 300 if (!hasControlInCallPermission) { 301 Log.w(this, 302 "InCall UI does not have CONTROL_INCALL_EXPERIENCE permission: " + 303 serviceInfo.packageName); 304 continue; 305 } 306 307 InCallServiceConnection inCallServiceConnection = new InCallServiceConnection(); 308 ComponentName componentName = new ComponentName(serviceInfo.packageName, 309 serviceInfo.name); 310 311 Log.i(this, "Attempting to bind to InCall %s, is dupe? %b ", 312 serviceInfo.packageName, 313 mServiceConnections.containsKey(componentName)); 314 315 if (!mServiceConnections.containsKey(componentName)) { 316 Intent intent = new Intent(InCallService.SERVICE_INTERFACE); 317 intent.setComponent(componentName); 318 319 final int bindFlags; 320 if (mInCallComponentName.equals(componentName)) { 321 bindFlags = Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT; 322 if (!call.isIncoming()) { 323 intent.putExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, 324 call.getExtras()); 325 intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, 326 call.getTargetPhoneAccount()); 327 } 328 } else { 329 bindFlags = Context.BIND_AUTO_CREATE; 330 } 331 332 if (mContext.bindServiceAsUser(intent, inCallServiceConnection, bindFlags, 333 UserHandle.CURRENT)) { 334 mServiceConnections.put(componentName, inCallServiceConnection); 335 } 336 } 337 } 338 } 339 } 340 } 341 342 /** 343 * Persists the {@link IInCallService} instance and starts the communication between 344 * this class and in-call app by sending the first update to in-call app. This method is 345 * called after a successful binding connection is established. 346 * 347 * @param componentName The service {@link ComponentName}. 348 * @param service The {@link IInCallService} implementation. 349 */ 350 private void onConnected(ComponentName componentName, IBinder service) { 351 ThreadUtil.checkOnMainThread(); 352 Trace.beginSection("onConnected: " + componentName); 353 Log.i(this, "onConnected to %s", componentName); 354 355 IInCallService inCallService = IInCallService.Stub.asInterface(service); 356 357 try { 358 inCallService.setInCallAdapter(new InCallAdapter(CallsManager.getInstance(), 359 mCallIdMapper)); 360 mInCallServices.put(componentName, inCallService); 361 } catch (RemoteException e) { 362 Log.e(this, e, "Failed to set the in-call adapter."); 363 Trace.endSection(); 364 return; 365 } 366 367 // Upon successful connection, send the state of the world to the service. 368 Collection<Call> calls = CallsManager.getInstance().getCalls(); 369 if (!calls.isEmpty()) { 370 Log.i(this, "Adding %s calls to InCallService after onConnected: %s", calls.size(), 371 componentName); 372 for (Call call : calls) { 373 try { 374 // Track the call if we don't already know about it. 375 Log.i(this, "addCall after binding: %s", call); 376 addCall(call); 377 378 inCallService.addCall(toParcelableCall(call, 379 componentName.equals(mInCallComponentName) /* includeVideoProvider */)); 380 } catch (RemoteException ignored) { 381 } 382 } 383 onAudioStateChanged(null, CallsManager.getInstance().getAudioState()); 384 onCanAddCallChanged(CallsManager.getInstance().canAddCall()); 385 } else { 386 unbind(); 387 } 388 Trace.endSection(); 389 } 390 391 /** 392 * Cleans up an instance of in-call app after the service has been unbound. 393 * 394 * @param disconnectedComponent The {@link ComponentName} of the service which disconnected. 395 */ 396 private void onDisconnected(ComponentName disconnectedComponent) { 397 Log.i(this, "onDisconnected from %s", disconnectedComponent); 398 ThreadUtil.checkOnMainThread(); 399 400 if (mInCallServices.containsKey(disconnectedComponent)) { 401 mInCallServices.remove(disconnectedComponent); 402 } 403 404 if (mServiceConnections.containsKey(disconnectedComponent)) { 405 // One of the services that we were bound to has disconnected. If the default in-call UI 406 // has disconnected, disconnect all calls and un-bind all other InCallService 407 // implementations. 408 if (disconnectedComponent.equals(mInCallComponentName)) { 409 Log.i(this, "In-call UI %s disconnected.", disconnectedComponent); 410 CallsManager.getInstance().disconnectAllCalls(); 411 unbind(); 412 } else { 413 Log.i(this, "In-Call Service %s suddenly disconnected", disconnectedComponent); 414 // Else, if it wasn't the default in-call UI, then one of the other in-call services 415 // disconnected and, well, that's probably their fault. Clear their state and 416 // ignore. 417 InCallServiceConnection serviceConnection = 418 mServiceConnections.get(disconnectedComponent); 419 420 // We still need to call unbind even though it disconnected. 421 mContext.unbindService(serviceConnection); 422 423 mServiceConnections.remove(disconnectedComponent); 424 mInCallServices.remove(disconnectedComponent); 425 } 426 } 427 } 428 429 /** 430 * Informs all {@link InCallService} instances of the updated call information. Changes to the 431 * video provider are only communicated to the default in-call UI. 432 * 433 * @param call The {@link Call}. 434 */ 435 private void updateCall(Call call) { 436 if (!mInCallServices.isEmpty()) { 437 for (Map.Entry<ComponentName, IInCallService> entry : mInCallServices.entrySet()) { 438 ComponentName componentName = entry.getKey(); 439 IInCallService inCallService = entry.getValue(); 440 ParcelableCall parcelableCall = toParcelableCall(call, 441 componentName.equals(mInCallComponentName) /* includeVideoProvider */); 442 Log.v(this, "updateCall %s ==> %s", call, parcelableCall); 443 try { 444 inCallService.updateCall(parcelableCall); 445 } catch (RemoteException ignored) { 446 } 447 } 448 } 449 } 450 451 /** 452 * Parcels all information for a {@link Call} into a new {@link ParcelableCall} instance. 453 * 454 * @param call The {@link Call} to parcel. 455 * @param includeVideoProvider When {@code true}, the {@link IVideoProvider} is included in the 456 * parceled call. When {@code false}, the {@link IVideoProvider} is not included. 457 * @return The {@link ParcelableCall} containing all call information from the {@link Call}. 458 */ 459 private ParcelableCall toParcelableCall(Call call, boolean includeVideoProvider) { 460 String callId = mCallIdMapper.getCallId(call); 461 462 int state = call.getState(); 463 int capabilities = convertConnectionToCallCapabilities(call.getConnectionCapabilities()); 464 465 // If this is a single-SIM device, the "default SIM" will always be the only SIM. 466 boolean isDefaultSmsAccount = 467 CallsManager.getInstance().getPhoneAccountRegistrar().isUserSelectedSmsPhoneAccount( 468 call.getTargetPhoneAccount()); 469 if (call.isRespondViaSmsCapable() && isDefaultSmsAccount) { 470 capabilities |= android.telecom.Call.Details.CAPABILITY_RESPOND_VIA_TEXT; 471 } 472 473 if (call.isEmergencyCall()) { 474 capabilities = removeCapability( 475 capabilities, android.telecom.Call.Details.CAPABILITY_MUTE); 476 } 477 478 if (state == CallState.DIALING) { 479 capabilities = removeCapability( 480 capabilities, android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL); 481 capabilities = removeCapability( 482 capabilities, android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE); 483 } 484 485 if (state == CallState.ABORTED) { 486 state = CallState.DISCONNECTED; 487 } 488 489 if (call.isLocallyDisconnecting() && state != CallState.DISCONNECTED) { 490 state = CallState.DISCONNECTING; 491 } 492 493 String parentCallId = null; 494 Call parentCall = call.getParentCall(); 495 if (parentCall != null) { 496 parentCallId = mCallIdMapper.getCallId(parentCall); 497 } 498 499 long connectTimeMillis = call.getConnectTimeMillis(); 500 List<Call> childCalls = call.getChildCalls(); 501 List<String> childCallIds = new ArrayList<>(); 502 if (!childCalls.isEmpty()) { 503 long childConnectTimeMillis = Long.MAX_VALUE; 504 for (Call child : childCalls) { 505 if (child.getConnectTimeMillis() > 0) { 506 childConnectTimeMillis = Math.min(child.getConnectTimeMillis(), 507 childConnectTimeMillis); 508 } 509 childCallIds.add(mCallIdMapper.getCallId(child)); 510 } 511 512 if (childConnectTimeMillis != Long.MAX_VALUE) { 513 connectTimeMillis = childConnectTimeMillis; 514 } 515 } 516 517 Uri handle = call.getHandlePresentation() == TelecomManager.PRESENTATION_ALLOWED ? 518 call.getHandle() : null; 519 String callerDisplayName = call.getCallerDisplayNamePresentation() == 520 TelecomManager.PRESENTATION_ALLOWED ? call.getCallerDisplayName() : null; 521 522 List<Call> conferenceableCalls = call.getConferenceableCalls(); 523 List<String> conferenceableCallIds = new ArrayList<String>(conferenceableCalls.size()); 524 for (Call otherCall : conferenceableCalls) { 525 String otherId = mCallIdMapper.getCallId(otherCall); 526 if (otherId != null) { 527 conferenceableCallIds.add(otherId); 528 } 529 } 530 531 int properties = call.isConference() ? CallProperties.CONFERENCE : 0; 532 return new ParcelableCall( 533 callId, 534 state, 535 call.getDisconnectCause(), 536 call.getCannedSmsResponses(), 537 capabilities, 538 properties, 539 connectTimeMillis, 540 handle, 541 call.getHandlePresentation(), 542 callerDisplayName, 543 call.getCallerDisplayNamePresentation(), 544 call.getGatewayInfo(), 545 call.getTargetPhoneAccount(), 546 includeVideoProvider ? call.getVideoProvider() : null, 547 parentCallId, 548 childCallIds, 549 call.getStatusHints(), 550 call.getVideoState(), 551 conferenceableCallIds, 552 call.getExtras()); 553 } 554 555 private static final int[] CONNECTION_TO_CALL_CAPABILITY = new int[] { 556 Connection.CAPABILITY_HOLD, 557 android.telecom.Call.Details.CAPABILITY_HOLD, 558 559 Connection.CAPABILITY_SUPPORT_HOLD, 560 android.telecom.Call.Details.CAPABILITY_SUPPORT_HOLD, 561 562 Connection.CAPABILITY_MERGE_CONFERENCE, 563 android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE, 564 565 Connection.CAPABILITY_SWAP_CONFERENCE, 566 android.telecom.Call.Details.CAPABILITY_SWAP_CONFERENCE, 567 568 Connection.CAPABILITY_UNUSED, 569 android.telecom.Call.Details.CAPABILITY_UNUSED, 570 571 Connection.CAPABILITY_RESPOND_VIA_TEXT, 572 android.telecom.Call.Details.CAPABILITY_RESPOND_VIA_TEXT, 573 574 Connection.CAPABILITY_MUTE, 575 android.telecom.Call.Details.CAPABILITY_MUTE, 576 577 Connection.CAPABILITY_MANAGE_CONFERENCE, 578 android.telecom.Call.Details.CAPABILITY_MANAGE_CONFERENCE, 579 580 Connection.CAPABILITY_SUPPORTS_VT_LOCAL, 581 android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL, 582 583 Connection.CAPABILITY_SUPPORTS_VT_REMOTE, 584 android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE, 585 586 Connection.CAPABILITY_HIGH_DEF_AUDIO, 587 android.telecom.Call.Details.CAPABILITY_HIGH_DEF_AUDIO, 588 589 Connection.CAPABILITY_VoWIFI, 590 android.telecom.Call.Details.CAPABILITY_VoWIFI, 591 592 Connection.CAPABILITY_SEPARATE_FROM_CONFERENCE, 593 android.telecom.Call.Details.CAPABILITY_SEPARATE_FROM_CONFERENCE, 594 595 Connection.CAPABILITY_DISCONNECT_FROM_CONFERENCE, 596 android.telecom.Call.Details.CAPABILITY_DISCONNECT_FROM_CONFERENCE, 597 598 Connection.CAPABILITY_GENERIC_CONFERENCE, 599 android.telecom.Call.Details.CAPABILITY_GENERIC_CONFERENCE 600 }; 601 602 private static int convertConnectionToCallCapabilities(int connectionCapabilities) { 603 int callCapabilities = 0; 604 for (int i = 0; i < CONNECTION_TO_CALL_CAPABILITY.length; i += 2) { 605 if ((CONNECTION_TO_CALL_CAPABILITY[i] & connectionCapabilities) != 0) { 606 callCapabilities |= CONNECTION_TO_CALL_CAPABILITY[i + 1]; 607 } 608 } 609 return callCapabilities; 610 } 611 612 /** 613 * Adds the call to the list of calls tracked by the {@link InCallController}. 614 * @param call The call to add. 615 */ 616 private void addCall(Call call) { 617 if (mCallIdMapper.getCallId(call) == null) { 618 mCallIdMapper.addCall(call); 619 call.addListener(mCallListener); 620 } 621 } 622 623 /** 624 * Removes the specified capability from the set of capabilities bits and returns the new set. 625 */ 626 private static int removeCapability(int capabilities, int capability) { 627 return capabilities & ~capability; 628 } 629 630 /** 631 * Dumps the state of the {@link InCallController}. 632 * 633 * @param pw The {@code IndentingPrintWriter} to write the state to. 634 */ 635 public void dump(IndentingPrintWriter pw) { 636 pw.println("mInCallServices (InCalls registered):"); 637 pw.increaseIndent(); 638 for (ComponentName componentName : mInCallServices.keySet()) { 639 pw.println(componentName); 640 } 641 pw.decreaseIndent(); 642 643 pw.println("mServiceConnections (InCalls bound):"); 644 pw.increaseIndent(); 645 for (ComponentName componentName : mServiceConnections.keySet()) { 646 pw.println(componentName); 647 } 648 pw.decreaseIndent(); 649 } 650 } 651