Home | History | Annotate | Download | only in phone
      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