1 /* 2 * Copyright (C) 2013 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.phone; 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.os.Handler; 28 import android.os.IBinder; 29 import android.os.Message; 30 import android.os.PowerManager; 31 import android.os.RemoteException; 32 import android.os.SystemClock; 33 import android.os.SystemProperties; 34 import android.text.TextUtils; 35 import android.util.Log; 36 37 import com.android.internal.telephony.Connection; 38 import com.android.internal.telephony.Connection.PostDialState; 39 import com.android.phone.AudioRouter.AudioModeListener; 40 import com.android.phone.NotificationMgr.StatusBarHelper; 41 import com.android.services.telephony.common.AudioMode; 42 import com.android.services.telephony.common.Call; 43 import com.android.services.telephony.common.ICallHandlerService; 44 import com.google.common.collect.Lists; 45 46 import java.util.List; 47 48 /** 49 * This class is responsible for passing through call state changes to the CallHandlerService. 50 */ 51 public class CallHandlerServiceProxy extends Handler 52 implements CallModeler.Listener, AudioModeListener { 53 54 private static final String TAG = CallHandlerServiceProxy.class.getSimpleName(); 55 private static final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt( 56 "ro.debuggable", 0) == 1); 57 58 public static final int RETRY_DELAY_MILLIS = 2000; 59 public static final int RETRY_DELAY_LONG_MILLIS = 30 * 1000; // 30 seconds 60 private static final int BIND_RETRY_MSG = 1; 61 private static final int BIND_TIME_OUT = 2; 62 private static final int MAX_SHORT_DELAY_RETRY_COUNT = 5; 63 64 private AudioRouter mAudioRouter; 65 private CallCommandService mCallCommandService; 66 private CallModeler mCallModeler; 67 private Context mContext; 68 private boolean mFullUpdateOnConnect; 69 70 private ICallHandlerService mCallHandlerServiceGuarded; // Guarded by mServiceAndQueueLock 71 // Single queue to guarantee ordering 72 private List<QueueParams> mQueue; // Guarded by mServiceAndQueueLock 73 74 private final Object mServiceAndQueueLock = new Object(); 75 private int mBindRetryCount = 0; 76 77 @Override 78 public void handleMessage(Message msg) { 79 super.handleMessage(msg); 80 81 switch (msg.what) { 82 case BIND_RETRY_MSG: 83 // Remove any pending messages since we're already performing the action. 84 // If the call to setupServiceConnection() fails, it will queue up another retry. 85 removeMessages(BIND_RETRY_MSG); 86 handleConnectRetry(); 87 break; 88 case BIND_TIME_OUT: 89 // Remove any pending messages since we're already performing the action. 90 // If the call to setupServiceConnection() fails, it will queue up another retry. 91 removeMessages(BIND_TIME_OUT); 92 synchronized (mServiceAndQueueLock) { 93 if(mCallHandlerServiceGuarded == null) { 94 Log.w(TAG, "Binding time out. InCallUI did not respond in time."); 95 try { 96 mContext.unbindService(mConnection); 97 } catch(Exception e) { 98 Log.w(TAG, "unbindservice exception", e); 99 } 100 mConnection = null; 101 handleConnectRetry(); 102 } 103 } 104 break; 105 } 106 } 107 108 public CallHandlerServiceProxy(Context context, CallModeler callModeler, 109 CallCommandService callCommandService, AudioRouter audioRouter) { 110 if (DBG) { 111 Log.d(TAG, "init CallHandlerServiceProxy"); 112 } 113 mContext = context; 114 mCallCommandService = callCommandService; 115 mCallModeler = callModeler; 116 mAudioRouter = audioRouter; 117 118 mAudioRouter.addAudioModeListener(this); 119 mCallModeler.addListener(this); 120 } 121 122 @Override 123 public void onDisconnect(Call call) { 124 // Wake up in case the screen was off. 125 wakeUpScreen(); 126 synchronized (mServiceAndQueueLock) { 127 if (mCallHandlerServiceGuarded == null) { 128 if (DBG) { 129 Log.d(TAG, "CallHandlerService not connected. Enqueue disconnect"); 130 } 131 enqueueDisconnect(call); 132 setupServiceConnection(); 133 return; 134 } 135 } 136 processDisconnect(call); 137 } 138 139 private void wakeUpScreen() { 140 Log.d(TAG, "wakeUpScreen()"); 141 final PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); 142 pm.wakeUp(SystemClock.uptimeMillis()); 143 } 144 145 private void processDisconnect(Call call) { 146 try { 147 if (DBG) { 148 Log.d(TAG, "onDisconnect: " + call); 149 } 150 synchronized (mServiceAndQueueLock) { 151 if (mCallHandlerServiceGuarded != null) { 152 mCallHandlerServiceGuarded.onDisconnect(call); 153 } 154 } 155 if (!mCallModeler.hasLiveCall()) { 156 unbind(); 157 } 158 } catch (Exception e) { 159 Log.e(TAG, "Remote exception handling onDisconnect ", e); 160 } 161 } 162 163 @Override 164 public void onIncoming(Call call) { 165 // for new incoming calls, reset the retry count. 166 resetConnectRetryCount(); 167 168 synchronized (mServiceAndQueueLock) { 169 if (mCallHandlerServiceGuarded == null) { 170 if (DBG) { 171 Log.d(TAG, "CallHandlerService not connected. Enqueue incoming."); 172 } 173 enqueueIncoming(call); 174 setupServiceConnection(); 175 return; 176 } 177 } 178 processIncoming(call); 179 } 180 181 private void processIncoming(Call call) { 182 if (DBG) { 183 Log.d(TAG, "onIncoming: " + call); 184 } 185 try { 186 // TODO: check RespondViaSmsManager.allowRespondViaSmsForCall() 187 // must refactor call method to accept proper call object. 188 synchronized (mServiceAndQueueLock) { 189 if (mCallHandlerServiceGuarded != null) { 190 mCallHandlerServiceGuarded.onIncoming(call, 191 RejectWithTextMessageManager.loadCannedResponses()); 192 } 193 } 194 } catch (Exception e) { 195 Log.e(TAG, "Remote exception handling onUpdate", e); 196 } 197 } 198 199 @Override 200 public void onUpdate(List<Call> calls) { 201 synchronized (mServiceAndQueueLock) { 202 if (mCallHandlerServiceGuarded == null) { 203 if (DBG) { 204 Log.d(TAG, "CallHandlerService not connected. Enqueue update."); 205 } 206 enqueueUpdate(calls); 207 setupServiceConnection(); 208 return; 209 } 210 } 211 processUpdate(calls); 212 } 213 214 private void processUpdate(List<Call> calls) { 215 if (DBG) { 216 Log.d(TAG, "onUpdate: " + calls.toString()); 217 } 218 try { 219 synchronized (mServiceAndQueueLock) { 220 if (mCallHandlerServiceGuarded != null) { 221 mCallHandlerServiceGuarded.onUpdate(calls); 222 } 223 } 224 if (!mCallModeler.hasLiveCall()) { 225 // TODO: unbinding happens in both onUpdate and onDisconnect because the ordering 226 // is not deterministic. Unbinding in both ensures that the service is unbound. 227 // But it also makes this in-efficient because we are unbinding twice, which leads 228 // to the CallHandlerService performing onCreate() and onDestroy() twice for each 229 // disconnect. 230 unbind(); 231 } 232 } catch (Exception e) { 233 Log.e(TAG, "Remote exception handling onUpdate", e); 234 } 235 } 236 237 238 @Override 239 public void onPostDialAction(Connection.PostDialState state, int callId, String remainingChars, 240 char currentChar) { 241 if (state != PostDialState.WAIT) return; 242 try { 243 synchronized (mServiceAndQueueLock) { 244 if (mCallHandlerServiceGuarded == null) { 245 if (DBG) { 246 Log.d(TAG, "CallHandlerService not conneccted. Skipping " 247 + "onPostDialWait()."); 248 } 249 return; 250 } 251 } 252 253 mCallHandlerServiceGuarded.onPostDialWait(callId, remainingChars); 254 } catch (Exception e) { 255 Log.e(TAG, "Remote exception handling onUpdate", e); 256 } 257 } 258 259 @Override 260 public void onAudioModeChange(int newMode, boolean muted) { 261 try { 262 synchronized (mServiceAndQueueLock) { 263 if (mCallHandlerServiceGuarded == null) { 264 if (DBG) { 265 Log.d(TAG, "CallHandlerService not conneccted. Skipping " 266 + "onAudioModeChange()."); 267 } 268 return; 269 } 270 } 271 272 // Just do a simple log for now. 273 Log.i(TAG, "Updating with new audio mode: " + AudioMode.toString(newMode) + 274 " with mute " + muted); 275 276 mCallHandlerServiceGuarded.onAudioModeChange(newMode, muted); 277 } catch (Exception e) { 278 Log.e(TAG, "Remote exception handling onAudioModeChange", e); 279 } 280 } 281 282 @Override 283 public void onSupportedAudioModeChange(int modeMask) { 284 try { 285 synchronized (mServiceAndQueueLock) { 286 if (mCallHandlerServiceGuarded == null) { 287 if (DBG) { 288 Log.d(TAG, "CallHandlerService not conneccted. Skipping" 289 + "onSupportedAudioModeChange()."); 290 } 291 return; 292 } 293 } 294 295 if (DBG) { 296 Log.d(TAG, "onSupportAudioModeChange: " + AudioMode.toString(modeMask)); 297 } 298 299 mCallHandlerServiceGuarded.onSupportedAudioModeChange(modeMask); 300 } catch (Exception e) { 301 Log.e(TAG, "Remote exception handling onAudioModeChange", e); 302 } 303 304 } 305 306 private ServiceConnection mConnection = null; 307 308 private class InCallServiceConnection implements ServiceConnection { 309 @Override public void onServiceConnected (ComponentName className, IBinder service){ 310 if (DBG) { 311 Log.d(TAG, "Service Connected"); 312 } 313 onCallHandlerServiceConnected(ICallHandlerService.Stub.asInterface(service)); 314 removeMessages(BIND_TIME_OUT); 315 if (DBG) { 316 Log.d(TAG, "Service Connected. Cancel timer"); 317 } 318 resetConnectRetryCount(); 319 } 320 321 @Override public void onServiceDisconnected (ComponentName className){ 322 Log.i(TAG, "Disconnected from UI service."); 323 synchronized (mServiceAndQueueLock) { 324 // Technically, unbindService is un-necessary since the framework will schedule and 325 // restart the crashed service. But there is a exponential backoff for the restart. 326 // Unbind explicitly and setup again to avoid the backoff since it's important to 327 // always have an in call ui. 328 unbind(); 329 330 reconnectOnRemainingCalls(); 331 } 332 } 333 } 334 335 public void bringToForeground(boolean showDialpad) { 336 // only support this call if the service is already connected. 337 synchronized (mServiceAndQueueLock) { 338 if (mCallHandlerServiceGuarded != null && mCallModeler.hasLiveCall()) { 339 try { 340 if (DBG) Log.d(TAG, "bringToForeground: " + showDialpad); 341 mCallHandlerServiceGuarded.bringToForeground(showDialpad); 342 } catch (RemoteException e) { 343 Log.e(TAG, "Exception handling bringToForeground", e); 344 } 345 } 346 } 347 } 348 349 private static Intent getInCallServiceIntent(Context context) { 350 final Intent serviceIntent = new Intent(ICallHandlerService.class.getName()); 351 final ComponentName component = new ComponentName(context.getResources().getString( 352 R.string.ui_default_package), context.getResources().getString( 353 R.string.incall_default_class)); 354 serviceIntent.setComponent(component); 355 return serviceIntent; 356 } 357 358 /** 359 * Sets up the connection with ICallHandlerService 360 */ 361 private void setupServiceConnection() { 362 if (!PhoneGlobals.sVoiceCapable) { 363 return; 364 } 365 366 final Intent serviceIntent = getInCallServiceIntent(mContext); 367 if (DBG) { 368 Log.d(TAG, "binding to service " + serviceIntent); 369 } 370 371 synchronized (mServiceAndQueueLock) { 372 if (mConnection == null) { 373 mConnection = new InCallServiceConnection(); 374 375 boolean failedConnection = false; 376 final PackageManager packageManger = mContext.getPackageManager(); 377 final List<ResolveInfo> services = packageManger.queryIntentServices(serviceIntent, 378 0); 379 380 ServiceInfo serviceInfo = null; 381 382 for (int i = 0; i < services.size(); i++) { 383 final ResolveInfo info = services.get(i); 384 if (info.serviceInfo != null) { 385 if (Manifest.permission.BIND_CALL_SERVICE.equals( 386 info.serviceInfo.permission)) { 387 serviceInfo = info.serviceInfo; 388 break; 389 } 390 } 391 } 392 393 if (serviceInfo == null) { 394 // Service not found, retry again after some delay 395 // This can happen if the service is being installed by the package manager. 396 // Between deletes and installs, bindService could get a silent service not 397 // found error. 398 Log.w(TAG, "Default call handler service not found."); 399 failedConnection = true; 400 } else { 401 402 serviceIntent.setComponent(new ComponentName(serviceInfo.packageName, 403 serviceInfo.name)); 404 if (DBG) { 405 Log.d(TAG, "binding to service " + serviceIntent); 406 } 407 if (!mContext.bindService(serviceIntent, mConnection, 408 Context.BIND_AUTO_CREATE)) { 409 // This happens when the in-call package is in the middle of being installed 410 Log.w(TAG, "Could not bind to default call handler service: " + 411 serviceIntent.getComponent()); 412 failedConnection = true; 413 } 414 } 415 416 if (failedConnection) { 417 mConnection = null; 418 enqueueConnectRetry(BIND_RETRY_MSG); 419 } else { 420 enqueueConnectRetry(BIND_TIME_OUT); 421 } 422 } else { 423 Log.d(TAG, "Service connection to in call service already started."); 424 } 425 } 426 } 427 428 private void resetConnectRetryCount() { 429 mBindRetryCount = 0; 430 } 431 432 private void incrementRetryCount() { 433 // Reset to the short delay retry count to avoid overflow 434 if (Integer.MAX_VALUE == mBindRetryCount) { 435 mBindRetryCount = MAX_SHORT_DELAY_RETRY_COUNT; 436 } 437 438 mBindRetryCount++; 439 } 440 441 private void handleConnectRetry() { 442 // Something else triggered the connection, cancel. 443 if (mConnection != null) { 444 Log.i(TAG, "Retry: already connected."); 445 return; 446 } 447 448 if (mCallModeler.hasLiveCall()) { 449 // Update the count when we are actually trying the retry instead of when the 450 // retry is queued up. 451 incrementRetryCount(); 452 453 Log.i(TAG, "Retrying connection: " + mBindRetryCount); 454 setupServiceConnection(); 455 } else { 456 Log.i(TAG, "Canceling connection retry since there are no calls."); 457 // We are not currently connected and there is no call so lets not bother 458 // with the retry. Also, empty the queue of pending messages to send 459 // to the UI. 460 synchronized (mServiceAndQueueLock) { 461 if (mQueue != null) { 462 mQueue.clear(); 463 } 464 } 465 466 // Since we have no calls, reset retry count. 467 resetConnectRetryCount(); 468 } 469 } 470 471 /** 472 * Called after the connection failed and a retry is needed. 473 * Queues up a retry to happen with a delay. 474 */ 475 private void enqueueConnectRetry(int msg) { 476 final boolean isLongDelay = (mBindRetryCount > MAX_SHORT_DELAY_RETRY_COUNT); 477 final int delay = isLongDelay ? RETRY_DELAY_LONG_MILLIS : RETRY_DELAY_MILLIS; 478 479 Log.w(TAG, "InCallUI Connection failed. Enqueuing delayed retry for " + delay + " ms." + 480 " retries(" + mBindRetryCount + ")"); 481 482 sendEmptyMessageDelayed(msg, delay); 483 } 484 485 private void unbind() { 486 synchronized (mServiceAndQueueLock) { 487 // On unbind, reenable the notification shade and navigation bar just in case the 488 // in-call UI crashed on an incoming call. 489 final StatusBarHelper statusBarHelper = PhoneGlobals.getInstance().notificationMgr. 490 statusBarHelper; 491 statusBarHelper.enableSystemBarNavigation(true); 492 statusBarHelper.enableExpandedView(true); 493 if (mCallHandlerServiceGuarded != null) { 494 Log.d(TAG, "Unbinding service."); 495 mCallHandlerServiceGuarded = null; 496 mContext.unbindService(mConnection); 497 } 498 mConnection = null; 499 } 500 } 501 502 /** 503 * Called when the in-call UI service is connected. Send command interface to in-call. 504 */ 505 private void onCallHandlerServiceConnected(ICallHandlerService callHandlerService) { 506 507 synchronized (mServiceAndQueueLock) { 508 mCallHandlerServiceGuarded = callHandlerService; 509 510 // Before we send any updates, we need to set up the initial service calls. 511 makeInitialServiceCalls(); 512 513 processQueue(); 514 515 if (mFullUpdateOnConnect) { 516 mFullUpdateOnConnect = false; 517 onUpdate(mCallModeler.getFullList()); 518 } 519 } 520 } 521 522 /** 523 * Checks to see if there are any live calls left, and if so, try reconnecting the UI. 524 */ 525 private void reconnectOnRemainingCalls() { 526 if (mCallModeler.hasLiveCall()) { 527 mFullUpdateOnConnect = true; 528 setupServiceConnection(); 529 } 530 } 531 532 /** 533 * Makes initial service calls to set up callcommandservice and audio modes. 534 */ 535 private void makeInitialServiceCalls() { 536 try { 537 mCallHandlerServiceGuarded.startCallService(mCallCommandService); 538 539 onSupportedAudioModeChange(mAudioRouter.getSupportedAudioModes()); 540 onAudioModeChange(mAudioRouter.getAudioMode(), mAudioRouter.getMute()); 541 } catch (RemoteException e) { 542 Log.e(TAG, "Remote exception calling CallHandlerService::setCallCommandService", e); 543 } 544 } 545 546 private List<QueueParams> getQueue() { 547 if (mQueue == null) { 548 mQueue = Lists.newArrayList(); 549 } 550 return mQueue; 551 } 552 553 private void enqueueDisconnect(Call call) { 554 getQueue().add(new QueueParams(QueueParams.METHOD_DISCONNECT, new Call(call))); 555 } 556 557 private void enqueueIncoming(Call call) { 558 getQueue().add(new QueueParams(QueueParams.METHOD_INCOMING, new Call(call))); 559 } 560 561 private void enqueueUpdate(List<Call> calls) { 562 final List<Call> copy = Lists.newArrayList(); 563 for (Call call : calls) { 564 copy.add(new Call(call)); 565 } 566 getQueue().add(new QueueParams(QueueParams.METHOD_UPDATE, copy)); 567 } 568 569 private void processQueue() { 570 synchronized (mServiceAndQueueLock) { 571 if (mQueue != null) { 572 for (QueueParams params : mQueue) { 573 switch (params.mMethod) { 574 case QueueParams.METHOD_INCOMING: 575 processIncoming((Call) params.mArg); 576 break; 577 case QueueParams.METHOD_UPDATE: 578 processUpdate((List<Call>) params.mArg); 579 break; 580 case QueueParams.METHOD_DISCONNECT: 581 processDisconnect((Call) params.mArg); 582 break; 583 default: 584 throw new IllegalArgumentException("Method type " + params.mMethod + 585 " not recognized."); 586 } 587 } 588 mQueue.clear(); 589 mQueue = null; 590 } 591 } 592 } 593 594 /** 595 * Holds method parameters. 596 */ 597 private static class QueueParams { 598 private static final int METHOD_INCOMING = 1; 599 private static final int METHOD_UPDATE = 2; 600 private static final int METHOD_DISCONNECT = 3; 601 602 private final int mMethod; 603 private final Object mArg; 604 605 private QueueParams(int method, Object arg) { 606 mMethod = method; 607 this.mArg = arg; 608 } 609 } 610 } 611