InCallController.java revision 9787e0e80d8960cf8b0ca74c7cdc4c4aac97187a
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.telecom; 18 19import android.Manifest; 20import android.content.ComponentName; 21import android.content.Context; 22import android.content.Intent; 23import android.content.ServiceConnection; 24import android.content.pm.PackageManager; 25import android.content.pm.ResolveInfo; 26import android.content.pm.ServiceInfo; 27import android.content.res.Resources; 28import android.net.Uri; 29import android.os.IBinder; 30import android.os.RemoteException; 31import android.os.UserHandle; 32import android.telecom.AudioState; 33import android.telecom.CallProperties; 34import android.telecom.CallState; 35import android.telecom.InCallService; 36import android.telecom.ParcelableCall; 37import android.telecom.PhoneCapabilities; 38import android.telecom.TelecomManager; 39import android.util.ArrayMap; 40 41// TODO: Needed for move to system service: import com.android.internal.R; 42import com.android.internal.telecom.IInCallService; 43import com.android.internal.util.IndentingPrintWriter; 44 45import com.google.common.collect.ImmutableCollection; 46 47import java.util.ArrayList; 48import java.util.Iterator; 49import java.util.List; 50import java.util.Map; 51import 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 */ 58public 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 onCallCapabilitiesChanged(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(); 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 void onPostDialWait(Call call, String remaining) { 214 if (!mInCallServices.isEmpty()) { 215 Log.i(this, "Calling onPostDialWait, remaining = %s", remaining); 216 for (IInCallService inCallService : mInCallServices.values()) { 217 try { 218 inCallService.setPostDialWait(mCallIdMapper.getCallId(call), remaining); 219 } catch (RemoteException ignored) { 220 } 221 } 222 } 223 } 224 225 @Override 226 public void onIsConferencedChanged(Call call) { 227 Log.d(this, "onIsConferencedChanged %s", call); 228 updateCall(call); 229 } 230 231 void bringToForeground(boolean showDialpad) { 232 if (!mInCallServices.isEmpty()) { 233 for (IInCallService inCallService : mInCallServices.values()) { 234 try { 235 inCallService.bringToForeground(showDialpad); 236 } catch (RemoteException ignored) { 237 } 238 } 239 } else { 240 Log.w(this, "Asking to bring unbound in-call UI to foreground."); 241 } 242 } 243 244 /** 245 * Unbinds an existing bound connection to the in-call app. 246 */ 247 private void unbind() { 248 ThreadUtil.checkOnMainThread(); 249 Iterator<Map.Entry<ComponentName, InCallServiceConnection>> iterator = 250 mServiceConnections.entrySet().iterator(); 251 while (iterator.hasNext()) { 252 Log.i(this, "Unbinding from InCallService %s"); 253 mContext.unbindService(iterator.next().getValue()); 254 iterator.remove(); 255 } 256 mInCallServices.clear(); 257 } 258 259 /** 260 * Binds to the in-call app if not already connected by binding directly to the saved 261 * component name of the {@link IInCallService} implementation. 262 */ 263 private void bind() { 264 ThreadUtil.checkOnMainThread(); 265 if (mInCallServices.isEmpty()) { 266 PackageManager packageManager = mContext.getPackageManager(); 267 Intent serviceIntent = new Intent(InCallService.SERVICE_INTERFACE); 268 269 for (ResolveInfo entry : packageManager.queryIntentServices(serviceIntent, 0)) { 270 ServiceInfo serviceInfo = entry.serviceInfo; 271 if (serviceInfo != null) { 272 boolean hasServiceBindPermission = serviceInfo.permission != null && 273 serviceInfo.permission.equals( 274 Manifest.permission.BIND_INCALL_SERVICE); 275 boolean hasControlInCallPermission = packageManager.checkPermission( 276 Manifest.permission.CONTROL_INCALL_EXPERIENCE, 277 serviceInfo.packageName) == PackageManager.PERMISSION_GRANTED; 278 279 if (!hasServiceBindPermission) { 280 Log.w(this, "InCallService does not have BIND_INCALL_SERVICE permission: " + 281 serviceInfo.packageName); 282 continue; 283 } 284 285 if (!hasControlInCallPermission) { 286 Log.w(this, 287 "InCall UI does not have CONTROL_INCALL_EXPERIENCE permission: " + 288 serviceInfo.packageName); 289 continue; 290 } 291 292 InCallServiceConnection inCallServiceConnection = new InCallServiceConnection(); 293 ComponentName componentName = new ComponentName(serviceInfo.packageName, 294 serviceInfo.name); 295 296 Log.i(this, "Attempting to bind to InCall %s, is dupe? %b ", 297 serviceInfo.packageName, 298 mServiceConnections.containsKey(componentName)); 299 300 if (!mServiceConnections.containsKey(componentName)) { 301 Intent intent = new Intent(InCallService.SERVICE_INTERFACE); 302 intent.setComponent(componentName); 303 304 if (mContext.bindServiceAsUser(intent, inCallServiceConnection, 305 Context.BIND_AUTO_CREATE, UserHandle.CURRENT)) { 306 mServiceConnections.put(componentName, inCallServiceConnection); 307 } 308 } 309 } 310 } 311 } 312 } 313 314 /** 315 * Persists the {@link IInCallService} instance and starts the communication between 316 * this class and in-call app by sending the first update to in-call app. This method is 317 * called after a successful binding connection is established. 318 * 319 * @param componentName The service {@link ComponentName}. 320 * @param service The {@link IInCallService} implementation. 321 */ 322 private void onConnected(ComponentName componentName, IBinder service) { 323 ThreadUtil.checkOnMainThread(); 324 325 Log.i(this, "onConnected to %s", componentName); 326 327 IInCallService inCallService = IInCallService.Stub.asInterface(service); 328 329 try { 330 inCallService.setInCallAdapter(new InCallAdapter(CallsManager.getInstance(), 331 mCallIdMapper)); 332 mInCallServices.put(componentName, inCallService); 333 } catch (RemoteException e) { 334 Log.e(this, e, "Failed to set the in-call adapter."); 335 return; 336 } 337 338 // Upon successful connection, send the state of the world to the service. 339 ImmutableCollection<Call> calls = CallsManager.getInstance().getCalls(); 340 if (!calls.isEmpty()) { 341 Log.i(this, "Adding %s calls to InCallService after onConnected: %s", calls.size(), 342 componentName); 343 for (Call call : calls) { 344 try { 345 // Track the call if we don't already know about it. 346 Log.i(this, "addCall after binding: %s", call); 347 addCall(call); 348 349 inCallService.addCall(toParcelableCall(call, 350 componentName.equals(mInCallComponentName) /* includeVideoProvider */)); 351 } catch (RemoteException ignored) { 352 } 353 } 354 onAudioStateChanged(null, CallsManager.getInstance().getAudioState()); 355 } else { 356 unbind(); 357 } 358 } 359 360 /** 361 * Cleans up an instance of in-call app after the service has been unbound. 362 * 363 * @param disconnectedComponent The {@link ComponentName} of the service which disconnected. 364 */ 365 private void onDisconnected(ComponentName disconnectedComponent) { 366 Log.i(this, "onDisconnected from %s", disconnectedComponent); 367 ThreadUtil.checkOnMainThread(); 368 369 if (mInCallServices.containsKey(disconnectedComponent)) { 370 mInCallServices.remove(disconnectedComponent); 371 } 372 373 if (mServiceConnections.containsKey(disconnectedComponent)) { 374 // One of the services that we were bound to has disconnected. If the default in-call UI 375 // has disconnected, disconnect all calls and un-bind all other InCallService 376 // implementations. 377 if (disconnectedComponent.equals(mInCallComponentName)) { 378 Log.i(this, "In-call UI %s disconnected.", disconnectedComponent); 379 CallsManager.getInstance().disconnectAllCalls(); 380 unbind(); 381 } else { 382 Log.i(this, "In-Call Service %s suddenly disconnected", disconnectedComponent); 383 // Else, if it wasn't the default in-call UI, then one of the other in-call services 384 // disconnected and, well, that's probably their fault. Clear their state and 385 // ignore. 386 InCallServiceConnection serviceConnection = 387 mServiceConnections.get(disconnectedComponent); 388 389 // We still need to call unbind even though it disconnected. 390 mContext.unbindService(serviceConnection); 391 392 mServiceConnections.remove(disconnectedComponent); 393 mInCallServices.remove(disconnectedComponent); 394 } 395 } 396 } 397 398 /** 399 * Informs all {@link InCallService} instances of the updated call information. Changes to the 400 * video provider are only communicated to the default in-call UI. 401 * 402 * @param call The {@link Call}. 403 */ 404 private void updateCall(Call call) { 405 if (!mInCallServices.isEmpty()) { 406 for (Map.Entry<ComponentName, IInCallService> entry : mInCallServices.entrySet()) { 407 ComponentName componentName = entry.getKey(); 408 IInCallService inCallService = entry.getValue(); 409 ParcelableCall parcelableCall = toParcelableCall(call, 410 componentName.equals(mInCallComponentName) /* includeVideoProvider */); 411 412 Log.v(this, "updateCall %s ==> %s", call, parcelableCall); 413 try { 414 inCallService.updateCall(parcelableCall); 415 } catch (RemoteException ignored) { 416 } 417 } 418 } 419 } 420 421 /** 422 * Parcels all information for a {@link Call} into a new {@link ParcelableCall} instance. 423 * 424 * @param call The {@link Call} to parcel. 425 * @param includeVideoProvider When {@code true}, the {@link IVideoProvider} is included in the 426 * parcelled call. When {@code false}, the {@link IVideoProvider} is not included. 427 * @return The {@link ParcelableCall} containing all call information from the {@link Call}. 428 */ 429 private ParcelableCall toParcelableCall(Call call, boolean includeVideoProvider) { 430 String callId = mCallIdMapper.getCallId(call); 431 432 int capabilities = call.getCallCapabilities(); 433 if (CallsManager.getInstance().isAddCallCapable(call)) { 434 capabilities |= PhoneCapabilities.ADD_CALL; 435 } 436 437 // Disable mute and add call for emergency calls. 438 if (call.isEmergencyCall()) { 439 capabilities &= ~PhoneCapabilities.MUTE; 440 capabilities &= ~PhoneCapabilities.ADD_CALL; 441 } 442 443 int properties = call.isConference() ? CallProperties.CONFERENCE : 0; 444 445 int state = call.getState(); 446 if (state == CallState.ABORTED) { 447 state = CallState.DISCONNECTED; 448 } 449 450 if (call.isLocallyDisconnecting() && state != CallState.DISCONNECTED) { 451 state = CallState.DISCONNECTING; 452 } 453 454 String parentCallId = null; 455 Call parentCall = call.getParentCall(); 456 if (parentCall != null) { 457 parentCallId = mCallIdMapper.getCallId(parentCall); 458 } 459 460 long connectTimeMillis = call.getConnectTimeMillis(); 461 List<Call> childCalls = call.getChildCalls(); 462 List<String> childCallIds = new ArrayList<>(); 463 if (!childCalls.isEmpty()) { 464 connectTimeMillis = Long.MAX_VALUE; 465 for (Call child : childCalls) { 466 if (child.getConnectTimeMillis() > 0) { 467 connectTimeMillis = Math.min(child.getConnectTimeMillis(), connectTimeMillis); 468 } 469 childCallIds.add(mCallIdMapper.getCallId(child)); 470 } 471 } 472 473 if (call.isRespondViaSmsCapable()) { 474 capabilities |= PhoneCapabilities.RESPOND_VIA_TEXT; 475 } 476 477 Uri handle = call.getHandlePresentation() == TelecomManager.PRESENTATION_ALLOWED ? 478 call.getHandle() : null; 479 String callerDisplayName = call.getCallerDisplayNamePresentation() == 480 TelecomManager.PRESENTATION_ALLOWED ? call.getCallerDisplayName() : null; 481 482 List<Call> conferenceableCalls = call.getConferenceableCalls(); 483 List<String> conferenceableCallIds = new ArrayList<String>(conferenceableCalls.size()); 484 for (Call otherCall : conferenceableCalls) { 485 String otherId = mCallIdMapper.getCallId(otherCall); 486 if (otherId != null) { 487 conferenceableCallIds.add(otherId); 488 } 489 } 490 491 return new ParcelableCall( 492 callId, 493 state, 494 call.getDisconnectCause(), 495 call.getCannedSmsResponses(), 496 capabilities, 497 properties, 498 connectTimeMillis, 499 handle, 500 call.getHandlePresentation(), 501 callerDisplayName, 502 call.getCallerDisplayNamePresentation(), 503 call.getGatewayInfo(), 504 call.getTargetPhoneAccount(), 505 includeVideoProvider ? call.getVideoProvider() : null, 506 parentCallId, 507 childCallIds, 508 call.getStatusHints(), 509 call.getVideoState(), 510 conferenceableCallIds, 511 call.getExtras()); 512 } 513 514 /** 515 * Adds the call to the list of calls tracked by the {@link InCallController}. 516 * @param call The call to add. 517 */ 518 private void addCall(Call call) { 519 if (mCallIdMapper.getCallId(call) == null) { 520 mCallIdMapper.addCall(call); 521 call.addListener(mCallListener); 522 } 523 } 524 525 /** 526 * Dumps the state of the {@link InCallController}. 527 * 528 * @param pw The {@code IndentingPrintWriter} to write the state to. 529 */ 530 public void dump(IndentingPrintWriter pw) { 531 pw.println("mInCallServices (InCalls registered):"); 532 pw.increaseIndent(); 533 for (ComponentName componentName : mInCallServices.keySet()) { 534 pw.println(componentName); 535 } 536 pw.decreaseIndent(); 537 538 pw.println("mServiceConnections (InCalls bound):"); 539 pw.increaseIndent(); 540 for (ComponentName componentName : mServiceConnections.keySet()) { 541 pw.println(componentName); 542 } 543 pw.decreaseIndent(); 544 } 545} 546