InCallController.java revision a9173085c5c176d3f632e50e962701bba2473b2a
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 41import com.android.internal.telecom.IInCallService; 42import com.google.common.collect.ImmutableCollection; 43 44import java.util.ArrayList; 45import java.util.Iterator; 46import java.util.List; 47import java.util.Map; 48import java.util.concurrent.ConcurrentHashMap; 49 50/** 51 * Binds to {@link IInCallService} and provides the service to {@link CallsManager} through which it 52 * can send updates to the in-call app. This class is created and owned by CallsManager and retains 53 * a binding to the {@link IInCallService} (implemented by the in-call app). 54 */ 55public final class InCallController extends CallsManagerListenerBase { 56 /** 57 * Used to bind to the in-call app and triggers the start of communication between 58 * this class and in-call app. 59 */ 60 private class InCallServiceConnection implements ServiceConnection { 61 /** {@inheritDoc} */ 62 @Override public void onServiceConnected(ComponentName name, IBinder service) { 63 Log.d(this, "onServiceConnected: %s", name); 64 onConnected(name, service); 65 } 66 67 /** {@inheritDoc} */ 68 @Override public void onServiceDisconnected(ComponentName name) { 69 Log.d(this, "onDisconnected: %s", name); 70 onDisconnected(name); 71 } 72 } 73 74 private final Call.Listener mCallListener = new Call.ListenerBase() { 75 @Override 76 public void onCallCapabilitiesChanged(Call call) { 77 updateCall(call); 78 } 79 80 @Override 81 public void onCannedSmsResponsesLoaded(Call call) { 82 updateCall(call); 83 } 84 85 @Override 86 public void onVideoCallProviderChanged(Call call) { 87 updateCall(call); 88 } 89 90 @Override 91 public void onStatusHintsChanged(Call call) { 92 updateCall(call); 93 } 94 95 @Override 96 public void onHandleChanged(Call call) { 97 updateCall(call); 98 } 99 100 @Override 101 public void onCallerDisplayNameChanged(Call call) { 102 updateCall(call); 103 } 104 105 @Override 106 public void onVideoStateChanged(Call call) { 107 updateCall(call); 108 } 109 110 @Override 111 public void onTargetPhoneAccountChanged(Call call) { 112 updateCall(call); 113 } 114 115 @Override 116 public void onConferenceableCallsChanged(Call call) { 117 updateCall(call); 118 } 119 }; 120 121 /** 122 * Maintains a binding connection to the in-call app(s). 123 * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is 124 * load factor before resizing, 1 means we only expect a single thread to 125 * access the map so make only a single shard 126 */ 127 private final Map<ComponentName, InCallServiceConnection> mServiceConnections = 128 new ConcurrentHashMap<ComponentName, InCallServiceConnection>(8, 0.9f, 1); 129 130 /** The in-call app implementations, see {@link IInCallService}. */ 131 private final Map<ComponentName, IInCallService> mInCallServices = new ArrayMap<>(); 132 133 private final CallIdMapper mCallIdMapper = new CallIdMapper("InCall"); 134 135 /** The {@link ComponentName} of the default InCall UI. */ 136 private final ComponentName mInCallComponentName; 137 138 public InCallController() { 139 Context context = TelecomApp.getInstance(); 140 Resources resources = context.getResources(); 141 142 mInCallComponentName = new ComponentName( 143 resources.getString(R.string.ui_default_package), 144 resources.getString(R.string.incall_default_class)); 145 } 146 147 @Override 148 public void onCallAdded(Call call) { 149 if (mInCallServices.isEmpty()) { 150 bind(); 151 } else { 152 Log.i(this, "onCallAdded: %s", call); 153 // Track the call if we don't already know about it. 154 addCall(call); 155 156 for (Map.Entry<ComponentName, IInCallService> entry : mInCallServices.entrySet()) { 157 ComponentName componentName = entry.getKey(); 158 IInCallService inCallService = entry.getValue(); 159 160 ParcelableCall parcelableCall = toParcelableCall(call, 161 componentName.equals(mInCallComponentName) /* includeVideoProvider */); 162 try { 163 inCallService.addCall(parcelableCall); 164 } catch (RemoteException ignored) { 165 } 166 } 167 } 168 } 169 170 @Override 171 public void onCallRemoved(Call call) { 172 Log.i(this, "onCallRemoved: %s", call); 173 if (CallsManager.getInstance().getCalls().isEmpty()) { 174 // TODO: Wait for all messages to be delivered to the service before unbinding. 175 unbind(); 176 } 177 call.removeListener(mCallListener); 178 mCallIdMapper.removeCall(call); 179 } 180 181 @Override 182 public void onCallStateChanged(Call call, int oldState, int newState) { 183 updateCall(call); 184 } 185 186 @Override 187 public void onConnectionServiceChanged( 188 Call call, 189 ConnectionServiceWrapper oldService, 190 ConnectionServiceWrapper newService) { 191 updateCall(call); 192 } 193 194 @Override 195 public void onAudioStateChanged(AudioState oldAudioState, AudioState newAudioState) { 196 if (!mInCallServices.isEmpty()) { 197 Log.i(this, "Calling onAudioStateChanged, audioState: %s -> %s", oldAudioState, 198 newAudioState); 199 for (IInCallService inCallService : mInCallServices.values()) { 200 try { 201 inCallService.onAudioStateChanged(newAudioState); 202 } catch (RemoteException ignored) { 203 } 204 } 205 } 206 } 207 208 void onPostDialWait(Call call, String remaining) { 209 if (!mInCallServices.isEmpty()) { 210 Log.i(this, "Calling onPostDialWait, remaining = %s", remaining); 211 for (IInCallService inCallService : mInCallServices.values()) { 212 try { 213 inCallService.setPostDialWait(mCallIdMapper.getCallId(call), remaining); 214 } catch (RemoteException ignored) { 215 } 216 } 217 } 218 } 219 220 @Override 221 public void onIsConferencedChanged(Call call) { 222 Log.d(this, "onIsConferencedChanged %s", call); 223 updateCall(call); 224 } 225 226 void bringToForeground(boolean showDialpad) { 227 if (!mInCallServices.isEmpty()) { 228 for (IInCallService inCallService : mInCallServices.values()) { 229 try { 230 inCallService.bringToForeground(showDialpad); 231 } catch (RemoteException ignored) { 232 } 233 } 234 } else { 235 Log.w(this, "Asking to bring unbound in-call UI to foreground."); 236 } 237 } 238 239 /** 240 * Unbinds an existing bound connection to the in-call app. 241 */ 242 private void unbind() { 243 ThreadUtil.checkOnMainThread(); 244 Iterator<Map.Entry<ComponentName, InCallServiceConnection>> iterator = 245 mServiceConnections.entrySet().iterator(); 246 while (iterator.hasNext()) { 247 Log.i(this, "Unbinding from InCallService %s"); 248 TelecomApp.getInstance().unbindService(iterator.next().getValue()); 249 iterator.remove(); 250 } 251 mInCallServices.clear(); 252 } 253 254 /** 255 * Binds to the in-call app if not already connected by binding directly to the saved 256 * component name of the {@link IInCallService} implementation. 257 */ 258 private void bind() { 259 ThreadUtil.checkOnMainThread(); 260 if (mInCallServices.isEmpty()) { 261 mServiceConnections.clear(); 262 Context context = TelecomApp.getInstance(); 263 PackageManager packageManager = TelecomApp.getInstance().getPackageManager(); 264 Intent serviceIntent = new Intent(InCallService.SERVICE_INTERFACE); 265 266 for (ResolveInfo entry : packageManager.queryIntentServices(serviceIntent, 0)) { 267 ServiceInfo serviceInfo = entry.serviceInfo; 268 if (serviceInfo != null) { 269 boolean hasServiceBindPermission = serviceInfo.permission != null && 270 serviceInfo.permission.equals( 271 Manifest.permission.BIND_INCALL_SERVICE); 272 boolean hasControlInCallPermission = packageManager.checkPermission( 273 Manifest.permission.CONTROL_INCALL_EXPERIENCE, 274 serviceInfo.packageName) == PackageManager.PERMISSION_GRANTED; 275 276 if (!hasServiceBindPermission) { 277 Log.w(this, "InCallService does not have BIND_INCALL_SERVICE permission: " + 278 serviceInfo.packageName); 279 continue; 280 } 281 282 if (!hasControlInCallPermission) { 283 Log.w(this, 284 "InCall UI does not have CONTROL_INCALL_EXPERIENCE permission: " + 285 serviceInfo.packageName); 286 continue; 287 } 288 289 Log.i(this, "Attempting to bind to InCall " + serviceInfo.packageName); 290 InCallServiceConnection inCallServiceConnection = new InCallServiceConnection(); 291 ComponentName componentName = new ComponentName(serviceInfo.packageName, 292 serviceInfo.name); 293 294 if (!mServiceConnections.containsKey(componentName)) { 295 Intent intent = new Intent(InCallService.SERVICE_INTERFACE); 296 intent.setComponent(componentName); 297 298 if (context.bindServiceAsUser(intent, inCallServiceConnection, 299 Context.BIND_AUTO_CREATE, UserHandle.CURRENT)) { 300 mServiceConnections.put(componentName, inCallServiceConnection); 301 } 302 } 303 } 304 } 305 } 306 } 307 308 /** 309 * Persists the {@link IInCallService} instance and starts the communication between 310 * this class and in-call app by sending the first update to in-call app. This method is 311 * called after a successful binding connection is established. 312 * 313 * @param componentName The service {@link ComponentName}. 314 * @param service The {@link IInCallService} implementation. 315 */ 316 private void onConnected(ComponentName componentName, IBinder service) { 317 ThreadUtil.checkOnMainThread(); 318 319 Log.i(this, "onConnected to %s", componentName); 320 321 IInCallService inCallService = IInCallService.Stub.asInterface(service); 322 323 try { 324 inCallService.setInCallAdapter(new InCallAdapter(CallsManager.getInstance(), 325 mCallIdMapper)); 326 mInCallServices.put(componentName, inCallService); 327 } catch (RemoteException e) { 328 Log.e(this, e, "Failed to set the in-call adapter."); 329 return; 330 } 331 332 // Upon successful connection, send the state of the world to the service. 333 ImmutableCollection<Call> calls = CallsManager.getInstance().getCalls(); 334 if (!calls.isEmpty()) { 335 Log.i(this, "Adding %s calls to InCallService after onConnected: %s", calls.size(), 336 componentName); 337 for (Call call : calls) { 338 try { 339 // Track the call if we don't already know about it. 340 Log.i(this, "addCall after binding: %s", call); 341 addCall(call); 342 343 inCallService.addCall(toParcelableCall(call, 344 componentName.equals(mInCallComponentName) /* includeVideoProvider */)); 345 } catch (RemoteException ignored) { 346 } 347 } 348 onAudioStateChanged(null, CallsManager.getInstance().getAudioState()); 349 } else { 350 unbind(); 351 } 352 } 353 354 /** 355 * Cleans up an instance of in-call app after the service has been unbound. 356 * 357 * @param disconnectedComponent The {@link ComponentName} of the service which disconnected. 358 */ 359 private void onDisconnected(ComponentName disconnectedComponent) { 360 Log.i(this, "onDisconnected from %s", disconnectedComponent); 361 ThreadUtil.checkOnMainThread(); 362 Context context = TelecomApp.getInstance(); 363 364 if (mInCallServices.containsKey(disconnectedComponent)) { 365 mInCallServices.remove(disconnectedComponent); 366 } 367 368 if (mServiceConnections.containsKey(disconnectedComponent)) { 369 // One of the services that we were bound to has disconnected. If the default in-call UI 370 // has disconnected, disconnect all calls and un-bind all other InCallService 371 // implementations. 372 if (disconnectedComponent.equals(mInCallComponentName)) { 373 Log.i(this, "In-call UI %s disconnected.", disconnectedComponent); 374 CallsManager.getInstance().disconnectAllCalls(); 375 unbind(); 376 } else { 377 Log.i(this, "In-Call Service %s suddenly disconnected", disconnectedComponent); 378 // Else, if it wasn't the default in-call UI, then one of the other in-call services 379 // disconnected and, well, that's probably their fault. Clear their state and 380 // ignore. 381 InCallServiceConnection serviceConnection = 382 mServiceConnections.get(disconnectedComponent); 383 384 // We still need to call unbind even though it disconnected. 385 context.unbindService(serviceConnection); 386 387 mServiceConnections.remove(disconnectedComponent); 388 mInCallServices.remove(disconnectedComponent); 389 } 390 } 391 } 392 393 /** 394 * Informs all {@link InCallService} instances of the updated call information. Changes to the 395 * video provider are only communicated to the default in-call UI. 396 * 397 * @param call The {@link Call}. 398 */ 399 private void updateCall(Call call) { 400 if (!mInCallServices.isEmpty()) { 401 for (Map.Entry<ComponentName, IInCallService> entry : mInCallServices.entrySet()) { 402 ComponentName componentName = entry.getKey(); 403 IInCallService inCallService = entry.getValue(); 404 ParcelableCall parcelableCall = toParcelableCall(call, 405 componentName.equals(mInCallComponentName) /* includeVideoProvider */); 406 407 Log.v(this, "updateCall %s ==> %s", call, parcelableCall); 408 try { 409 inCallService.updateCall(parcelableCall); 410 } catch (RemoteException ignored) { 411 } 412 } 413 } 414 } 415 416 /** 417 * Parcels all information for a {@link Call} into a new {@link ParcelableCall} instance. 418 * 419 * @param call The {@link Call} to parcel. 420 * @param includeVideoProvider When {@code true}, the {@link IVideoProvider} is included in the 421 * parcelled call. When {@code false}, the {@link IVideoProvider} is not included. 422 * @return The {@link ParcelableCall} containing all call information from the {@link Call}. 423 */ 424 private ParcelableCall toParcelableCall(Call call, boolean includeVideoProvider) { 425 String callId = mCallIdMapper.getCallId(call); 426 427 int capabilities = call.getCallCapabilities(); 428 if (CallsManager.getInstance().isAddCallCapable(call)) { 429 capabilities |= PhoneCapabilities.ADD_CALL; 430 } 431 432 // Disable mute and add call for emergency calls. 433 if (call.isEmergencyCall()) { 434 capabilities &= ~PhoneCapabilities.MUTE; 435 capabilities &= ~PhoneCapabilities.ADD_CALL; 436 } 437 438 int properties = call.isConference() ? CallProperties.CONFERENCE : 0; 439 440 int state = call.getState(); 441 if (state == CallState.ABORTED) { 442 state = CallState.DISCONNECTED; 443 } 444 445 String parentCallId = null; 446 Call parentCall = call.getParentCall(); 447 if (parentCall != null) { 448 parentCallId = mCallIdMapper.getCallId(parentCall); 449 } 450 451 long connectTimeMillis = call.getConnectTimeMillis(); 452 List<Call> childCalls = call.getChildCalls(); 453 List<String> childCallIds = new ArrayList<>(); 454 if (!childCalls.isEmpty()) { 455 connectTimeMillis = Long.MAX_VALUE; 456 for (Call child : childCalls) { 457 if (child.getConnectTimeMillis() > 0) { 458 connectTimeMillis = Math.min(child.getConnectTimeMillis(), connectTimeMillis); 459 } 460 childCallIds.add(mCallIdMapper.getCallId(child)); 461 } 462 } 463 464 if (call.isRespondViaSmsCapable()) { 465 capabilities |= PhoneCapabilities.RESPOND_VIA_TEXT; 466 } 467 468 Uri handle = call.getHandlePresentation() == TelecomManager.PRESENTATION_ALLOWED ? 469 call.getHandle() : null; 470 String callerDisplayName = call.getCallerDisplayNamePresentation() == 471 TelecomManager.PRESENTATION_ALLOWED ? call.getCallerDisplayName() : null; 472 473 List<Call> conferenceableCalls = call.getConferenceableCalls(); 474 List<String> conferenceableCallIds = new ArrayList<String>(conferenceableCalls.size()); 475 for (Call otherCall : conferenceableCalls) { 476 String otherId = mCallIdMapper.getCallId(otherCall); 477 if (otherId != null) { 478 conferenceableCallIds.add(otherId); 479 } 480 } 481 482 return new ParcelableCall( 483 callId, 484 state, 485 call.getDisconnectCause(), 486 call.getDisconnectMessage(), 487 call.getCannedSmsResponses(), 488 capabilities, 489 properties, 490 connectTimeMillis, 491 handle, 492 call.getHandlePresentation(), 493 callerDisplayName, 494 call.getCallerDisplayNamePresentation(), 495 call.getGatewayInfo(), 496 call.getTargetPhoneAccount(), 497 includeVideoProvider ? call.getVideoProvider() : null, 498 parentCallId, 499 childCallIds, 500 call.getStatusHints(), 501 call.getVideoState(), 502 conferenceableCallIds, 503 call.getExtras()); 504 } 505 506 /** 507 * Adds the call to the list of calls tracked by the {@link InCallController}. 508 * @param call The call to add. 509 */ 510 private void addCall(Call call) { 511 if (mCallIdMapper.getCallId(call) == null) { 512 mCallIdMapper.addCall(call); 513 call.addListener(mCallListener); 514 } 515 } 516} 517