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