Merge "Fixing conference managment regression." into lmp-mr1-dev
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 8bf516d..8a85f2c 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -410,6 +410,28 @@
         return mState;
     }
 
+    private boolean shouldContinueProcessingAfterDisconnect() {
+        // Stop processing once the call is active.
+        if (!CreateConnectionTimeout.isCallBeingPlaced(this)) {
+            return false;
+        }
+
+        // Make sure that there are additional connection services to process.
+        if (mCreateConnectionProcessor == null
+            || !mCreateConnectionProcessor.isProcessingComplete()
+            || !mCreateConnectionProcessor.hasMorePhoneAccounts()) {
+            return false;
+        }
+
+        if (mDisconnectCause == null) {
+            return false;
+        }
+
+        // Continue processing if the current attempt failed or timed out.
+        return mDisconnectCause.getCode() == DisconnectCause.ERROR ||
+            mCreateConnectionProcessor.isCallTimedOut();
+    }
+
     /**
      * Sets the call state. Although there exists the notion of appropriate state transitions
      * (see {@link CallState}), in practice those expectations break down when cellular systems
@@ -420,13 +442,8 @@
         if (mState != newState) {
             Log.v(this, "setState %s -> %s", mState, newState);
 
-            if (newState == CallState.DISCONNECTED
-                    && (mState == CallState.DIALING || mState == CallState.CONNECTING)
-                    && mCreateConnectionProcessor != null
-                    && mCreateConnectionProcessor.isProcessingComplete()
-                    && mCreateConnectionProcessor.hasMorePhoneAccounts()
-                    && mDisconnectCause != null
-                    && mDisconnectCause.getCode() == DisconnectCause.ERROR) {
+            if (newState == CallState.DISCONNECTED && shouldContinueProcessingAfterDisconnect()) {
+                Log.w(this, "continuing processing disconnected call with another service");
                 mCreateConnectionProcessor.continueProcessingIfPossible(this, mDisconnectCause);
                 return;
             }
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.
             }
         }
 
diff --git a/src/com/android/server/telecom/CreateConnectionTimeout.java b/src/com/android/server/telecom/CreateConnectionTimeout.java
new file mode 100644
index 0000000..3f308cc
--- /dev/null
+++ b/src/com/android/server/telecom/CreateConnectionTimeout.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2015, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Handler;
+import android.telecom.CallState;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import android.telephony.PhoneStateListener;
+import android.telephony.ServiceState;
+
+import java.util.Collection;
+import java.util.Objects;
+
+/**
+ * Registers a timeout for a call and disconnects the call when the timeout expires.
+ */
+final class CreateConnectionTimeout extends PhoneStateListener implements Runnable {
+    private final Context mContext;
+    private final PhoneAccountRegistrar mPhoneAccountRegistrar;
+    private final ConnectionServiceWrapper mConnectionService;
+    private final Call mCall;
+    private final Handler mHandler = new Handler();
+    private boolean mIsRegistered;
+    private boolean mIsCallTimedOut;
+
+    CreateConnectionTimeout(Context context, PhoneAccountRegistrar phoneAccountRegistrar,
+            ConnectionServiceWrapper service, Call call) {
+        mContext = context;
+        mPhoneAccountRegistrar = phoneAccountRegistrar;
+        mConnectionService = service;
+        mCall = call;
+    }
+
+    boolean isTimeoutNeededForCall(Collection<PhoneAccountHandle> accounts,
+            PhoneAccountHandle currentAccount) {
+        // Non-emergency calls timeout automatically at the radio layer. No need for a timeout here.
+        if (!TelephonyUtil.shouldProcessAsEmergency(mContext, mCall.getHandle())) {
+            return false;
+        }
+
+        // If there's no connection manager to fallback on then there's no point in having a
+        // timeout.
+        PhoneAccountHandle connectionManager = mPhoneAccountRegistrar.getSimCallManager();
+        if (!accounts.contains(connectionManager)) {
+            return false;
+        }
+
+        // No need to add a timeout if the current attempt is over the connection manager.
+        if (Objects.equals(connectionManager, currentAccount)) {
+            return false;
+        }
+
+        // To reduce the number of scenarios where a timeout is needed, only use a timeout if
+        // we're connected to Wi-Fi. This ensures that the fallback connection manager has an
+        // alternate route to place the call. TODO: remove this condition or allow connection
+        // managers to specify transports. See http://b/19199181.
+        if (!isConnectedToWifi()) {
+            return false;
+        }
+
+        Log.d(this, "isTimeoutNeededForCall, returning true");
+        return true;
+    }
+
+    void registerTimeout() {
+        Log.d(this, "registerTimeout");
+        mIsRegistered = true;
+        // First find out the cellular service state. Based on the state we decide whether a timeout
+        // will actually be enforced and if so how long it should be.
+        TelephonyManager telephonyManager =
+            (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
+        telephonyManager.listen(this, PhoneStateListener.LISTEN_SERVICE_STATE);
+        telephonyManager.listen(this, 0);
+    }
+
+    void unregisterTimeout() {
+        Log.d(this, "unregisterTimeout");
+        mIsRegistered = false;
+        mHandler.removeCallbacksAndMessages(null);
+    }
+
+    boolean isCallTimedOut() {
+        return mIsCallTimedOut;
+    }
+
+    @Override
+    public void onServiceStateChanged(ServiceState serviceState) {
+        long timeoutLengthMillis = getTimeoutLengthMillis(serviceState);
+        if (!mIsRegistered) {
+            Log.d(this, "onServiceStateChanged, timeout no longer registered, skipping");
+        } else if (timeoutLengthMillis  <= 0) {
+            Log.d(this, "onServiceStateChanged, timeout set to %d, skipping", timeoutLengthMillis);
+        } else if (serviceState.getState() == ServiceState.STATE_IN_SERVICE) {
+            // If cellular service is available then don't bother with a timeout.
+            Log.d(this, "onServiceStateChanged, cellular service available, skipping");
+        } else {
+            mHandler.postDelayed(this, timeoutLengthMillis);
+        }
+    }
+
+    @Override
+    public void run() {
+        if (mIsRegistered && isCallBeingPlaced(mCall)) {
+            Log.d(this, "run, call timed out, calling disconnect");
+            mIsCallTimedOut = true;
+            mConnectionService.disconnect(mCall);
+        }
+    }
+
+    static boolean isCallBeingPlaced(Call call) {
+        int state = call.getState();
+        return state == CallState.NEW
+            || state == CallState.CONNECTING
+            || state == CallState.DIALING;
+    }
+
+    private long getTimeoutLengthMillis(ServiceState serviceState) {
+        // If the radio is off then use a longer timeout. This gives us more time to power on the
+        // radio.
+        if (serviceState.getState() == ServiceState.STATE_POWER_OFF) {
+            return Timeouts.getEmergencyCallTimeoutRadioOffMillis(
+                    mContext.getContentResolver());
+        } else {
+            return Timeouts.getEmergencyCallTimeoutMillis(mContext.getContentResolver());
+        }
+    }
+
+    private boolean isConnectedToWifi() {
+        ConnectivityManager cm = (ConnectivityManager) mContext.getSystemService(
+            Context.CONNECTIVITY_SERVICE);
+        if (cm != null) {
+          NetworkInfo ni = cm.getActiveNetworkInfo();
+          return ni != null && ni.isConnected() && ni.getType() == ConnectivityManager.TYPE_WIFI;
+        }
+        return false;
+    }
+}
diff --git a/src/com/android/server/telecom/Timeouts.java b/src/com/android/server/telecom/Timeouts.java
index 75b94c2..b5cf39a 100644
--- a/src/com/android/server/telecom/Timeouts.java
+++ b/src/com/android/server/telecom/Timeouts.java
@@ -72,4 +72,21 @@
         return get(contentResolver, "delay_between_dtmf_tones_ms", 300L);
     }
 
+    /**
+     * Returns the amount of time to wait for an emergency call to be placed before routing to
+     * a different call service. A value of 0 or less means no timeout should be used.
+     */
+    public static long getEmergencyCallTimeoutMillis(ContentResolver contentResolver) {
+        return get(contentResolver, "emergency_call_timeout_millis", 25000L /* 25 seconds */);
+    }
+
+    /**
+     * Returns the amount of time to wait for an emergency call to be placed before routing to
+     * a different call service. This timeout is used only when the radio is powered off (for
+     * example in airplane mode). A value of 0 or less means no timeout should be used.
+     */
+    public static long getEmergencyCallTimeoutRadioOffMillis(ContentResolver contentResolver) {
+        return get(contentResolver, "emergency_call_timeout_radio_off_millis",
+                60000L /* 1 minute */);
+    }
 }