Add timeout for emergency calls

Currently connection managers are only able to route
emergency calls if the call can't be placed by the
normal phone account first.

Unfortunately this strategy only works for "fake" emergency
calls (for example when you set a fake number using
ril.ecclist).

When actually dialing "911" the call doesn't disconnect
even if there's no service. This means that the connection
manager never gets a chance to handle the call.

To fix this issue we're adding a timeout for emergency calls
if a connection manager is present. If after 25 seconds
the call is not yet active then we'll disconnect the call
and route it through the connection manager. The timeout
length is 60 seconds if the device is in airplane mode.
Both timeout values can be modified using config values.

To limit the impact of this change we've also added the
following extra condition:
  - only use a connection manager if we have wifi
    connectivity but no cellular connectivity

BUG: 19020123

Change-Id: Id2d740006cbf850db1cffb0d9f2aaca702ef1366
diff --git a/src/com/android/server/telecom/CreateConnectionProcessor.java b/src/com/android/server/telecom/CreateConnectionProcessor.java
index 91091ec..eec1427 100644
--- a/src/com/android/server/telecom/CreateConnectionProcessor.java
+++ b/src/com/android/server/telecom/CreateConnectionProcessor.java
@@ -17,17 +17,24 @@
 package com.android.server.telecom;
 
 import android.content.Context;
+import android.telecom.CallState;
 import android.telecom.DisconnectCause;
 import android.telecom.ParcelableConnection;
 import android.telecom.Phone;
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import android.telephony.PhoneStateListener;
+import android.telephony.ServiceState;
 
 // TODO: Needed for move to system service: import com.android.internal.R;
 
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Set;
 import java.util.Objects;
 
 /**
@@ -90,6 +97,7 @@
     private final PhoneAccountRegistrar mPhoneAccountRegistrar;
     private final Context mContext;
     private boolean mShouldUseConnectionManager = true;
+    private CreateConnectionTimeout mTimeout;
 
     CreateConnectionProcessor(
             Call call, ConnectionServiceRepository repository, CreateConnectionResponse response,
@@ -105,8 +113,13 @@
         return mResponse == null;
     }
 
+    boolean isCallTimedOut() {
+        return mTimeout != null && mTimeout.isCallTimedOut();
+    }
+
     void process() {
         Log.v(this, "process");
+        clearTimeout();
         mAttemptRecords = new ArrayList<>();
         if (mCall.getTargetPhoneAccount() != null) {
             mAttemptRecords.add(new CallAttemptRecord(
@@ -137,6 +150,7 @@
         // more services.
         CreateConnectionResponse response = mResponse;
         mResponse = null;
+        clearTimeout();
 
         ConnectionServiceWrapper service = mCall.getConnectionService();
         if (service != null) {
@@ -189,12 +203,15 @@
                 mCall.setConnectionManagerPhoneAccount(attempt.connectionManagerPhoneAccount);
                 mCall.setTargetPhoneAccount(attempt.targetPhoneAccount);
                 mCall.setConnectionService(service);
+                setTimeoutIfNeeded(service, attempt);
+
                 Log.i(this, "Attempting to call from %s", service.getComponentName());
                 service.createConnection(mCall, new Response(service));
             }
         } else {
             Log.v(this, "attemptNextPhoneAccount, no more accounts, failing");
             if (mResponse != null) {
+                clearTimeout();
                 mResponse.handleCreateConnectionFailure(mLastErrorDisconnectCause != null ?
                         mLastErrorDisconnectCause : new DisconnectCause(DisconnectCause.ERROR));
                 mResponse = null;
@@ -203,6 +220,25 @@
         }
     }
 
+    private void setTimeoutIfNeeded(ConnectionServiceWrapper service, CallAttemptRecord attempt) {
+        clearTimeout();
+
+        CreateConnectionTimeout timeout = new CreateConnectionTimeout(
+                mContext, mPhoneAccountRegistrar, service, mCall);
+        if (timeout.isTimeoutNeededForCall(getConnectionServices(mAttemptRecords),
+                attempt.connectionManagerPhoneAccount)) {
+            mTimeout = timeout;
+            timeout.registerTimeout();
+        }
+    }
+
+    private void clearTimeout() {
+        if (mTimeout != null) {
+            mTimeout.unregisterTimeout();
+            mTimeout = null;
+        }
+    }
+
     private boolean shouldSetConnectionManager() {
         if (!mShouldUseConnectionManager) {
             return false;
@@ -287,7 +323,7 @@
 
             // Next, add the connection manager account as a backup if it can place emergency calls.
             PhoneAccountHandle callManagerHandle = mPhoneAccountRegistrar.getSimCallManager();
-            if (callManagerHandle != null) {
+            if (mShouldUseConnectionManager && callManagerHandle != null) {
                 PhoneAccount callManager = mPhoneAccountRegistrar
                         .getPhoneAccount(callManagerHandle);
                 if (callManager.hasCapabilities(PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS)) {
@@ -306,6 +342,16 @@
         }
     }
 
+    /** Returns all connection services used by the call attempt records. */
+    private static Collection<PhoneAccountHandle> getConnectionServices(
+            List<CallAttemptRecord> records) {
+        HashSet<PhoneAccountHandle> result = new HashSet<>();
+        for (CallAttemptRecord record : records) {
+            result.add(record.connectionManagerPhoneAccount);
+        }
+        return result;
+    }
+
     private class Response implements CreateConnectionResponse {
         private final ConnectionServiceWrapper mService;
 
@@ -326,6 +372,8 @@
                 // in hearing about any more attempts
                 mResponse.handleCreateConnectionSuccess(idMapper, connection);
                 mResponse = null;
+                // If there's a timeout running then don't clear it. The timeout can be triggered
+                // after the call has successfully been created but before it has become active.
             }
         }