Merge "Add BIND_CONNECTION_SERVICE permission. (2/2)" into lmp-dev
diff --git a/src/com/android/telecomm/Call.java b/src/com/android/telecomm/Call.java
index 9d587ab..e2dbb79 100644
--- a/src/com/android/telecomm/Call.java
+++ b/src/com/android/telecomm/Call.java
@@ -53,7 +53,7 @@
 import java.util.Locale;
 import java.util.Objects;
 import java.util.Set;
-import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.ConcurrentHashMap;
 
 /**
  *  Encapsulates all aspects of a given phone call throughout its lifecycle, starting
@@ -248,8 +248,14 @@
     /** Info used by the connection services. */
     private Bundle mExtras = Bundle.EMPTY;
 
-    /** Set of listeners on this call. */
-    private Set<Listener> mListeners = new CopyOnWriteArraySet<>();
+    /** Set of listeners on this call.
+     *
+     * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
+     * load factor before resizing, 1 means we only expect a single thread to
+     * access the map so make only a single shard
+     */
+    private final Set<Listener> mListeners = Collections.newSetFromMap(
+            new ConcurrentHashMap<Listener, Boolean>(8, 0.9f, 1));
 
     private CreateConnectionProcessor mCreateConnectionProcessor;
 
@@ -322,7 +328,9 @@
     }
 
     void removeListener(Listener listener) {
-        mListeners.remove(listener);
+        if (listener != null) {
+            mListeners.remove(listener);
+        }
     }
 
     /** {@inheritDoc} */
@@ -629,6 +637,9 @@
 
         setVideoProvider(connection.getVideoProvider());
         setVideoState(connection.getVideoState());
+        setRequestingRingback(connection.isRequestingRingback());
+        setAudioModeIsVoip(connection.getAudioModeIsVoip());
+        setStatusHints(connection.getStatusHints());
 
         if (mIsIncoming) {
             // We do not handle incoming calls immediately when they are verified by the connection
@@ -656,14 +667,12 @@
             setDisconnectCause(code, null);
             setState(CallState.DISCONNECTED);
 
-            Listener[] listeners = mListeners.toArray(new Listener[mListeners.size()]);
-            for (int i = 0; i < listeners.length; i++) {
-                listeners[i].onFailedIncomingCall(this);
+            for (Listener listener : mListeners) {
+                listener.onFailedIncomingCall(this);
             }
         } else {
-            Listener[] listeners = mListeners.toArray(new Listener[mListeners.size()]);
-            for (int i = 0; i < listeners.length; i++) {
-                listeners[i].onFailedOutgoingCall(this, code, msg);
+            for (Listener listener : mListeners) {
+                listener.onFailedOutgoingCall(this, code, msg);
             }
             clearConnectionService();
         }
@@ -671,19 +680,18 @@
 
     @Override
     public void handleCreateConnectionCancelled() {
+        Log.v(this, "handleCreateConnectionCancelled");
         mCreateConnectionProcessor = null;
         if (mIsIncoming) {
             clearConnectionService();
             setDisconnectCause(DisconnectCause.OUTGOING_CANCELED, null);
 
-            Listener[] listeners = mListeners.toArray(new Listener[mListeners.size()]);
-            for (int i = 0; i < listeners.length; i++) {
-                listeners[i].onFailedIncomingCall(this);
+        for (Listener listener : mListeners) {
+                listener.onFailedIncomingCall(this);
             }
         } else {
-            Listener[] listeners = mListeners.toArray(new Listener[mListeners.size()]);
-            for (int i = 0; i < listeners.length; i++) {
-                listeners[i].onCancelledOutgoingCall(this);
+        for (Listener listener : mListeners) {
+                listener.onCancelledOutgoingCall(this);
             }
             clearConnectionService();
         }
@@ -706,7 +714,7 @@
      */
     void stopDtmfTone() {
         if (mConnectionService == null) {
-            Log.w(this, "stopDtmfTone() request on a call without a connectino service.");
+            Log.w(this, "stopDtmfTone() request on a call without a connection service.");
         } else {
             Log.i(this, "Send stopDtmfTone to connection service for call %s", this);
             mConnectionService.stopDtmfTone(this);
@@ -717,25 +725,33 @@
      * Attempts to disconnect the call through the connection service.
      */
     void disconnect() {
-        if (mState == CallState.NEW || mState == CallState.PRE_DIAL_WAIT) {
+        if (mState == CallState.NEW || mState == CallState.PRE_DIAL_WAIT ||
+                mState == CallState.CONNECTING) {
             Log.v(this, "Aborting call %s", this);
             abort();
         } else if (mState != CallState.ABORTED && mState != CallState.DISCONNECTED) {
-            Preconditions.checkNotNull(mConnectionService);
-
-            Log.i(this, "Send disconnect to connection service for call: %s", this);
-            // The call isn't officially disconnected until the connection service confirms that the
-            // call was actually disconnected. Only then is the association between call and
-            // connection service severed, see {@link CallsManager#markCallAsDisconnected}.
-            mConnectionService.disconnect(this);
+            if (mConnectionService == null) {
+                Log.e(this, new Exception(), "disconnect() request on a call without a"
+                        + " connection service.");
+            } else {
+                Log.i(this, "Send disconnect to connection service for call: %s", this);
+                // The call isn't officially disconnected until the connection service
+                // confirms that the call was actually disconnected. Only then is the
+                // association between call and connection service severed, see
+                // {@link CallsManager#markCallAsDisconnected}.
+                mConnectionService.disconnect(this);
+            }
         }
     }
 
     void abort() {
         if (mCreateConnectionProcessor != null) {
             mCreateConnectionProcessor.abort();
-        } else if (mState == CallState.PRE_DIAL_WAIT) {
-            handleCreateConnectionFailed(DisconnectCause.LOCAL, null);
+        } else if (mState == CallState.NEW || mState == CallState.PRE_DIAL_WAIT ||
+                mState == CallState.CONNECTING) {
+            handleCreateConnectionCancelled();
+        } else {
+            Log.v(this, "Cannot abort a call which isn't either PRE_DIAL_WAIT or CONNECTING");
         }
     }
 
@@ -1200,6 +1216,8 @@
 
     private int getStateFromConnectionState(int state) {
         switch (state) {
+            case Connection.STATE_INITIALIZING:
+                return CallState.CONNECTING;
             case Connection.STATE_ACTIVE:
                 return CallState.ACTIVE;
             case Connection.STATE_DIALING:
diff --git a/src/com/android/telecomm/CallActivity.java b/src/com/android/telecomm/CallActivity.java
index f6a9b87..06cc9cb 100644
--- a/src/com/android/telecomm/CallActivity.java
+++ b/src/com/android/telecomm/CallActivity.java
@@ -25,7 +25,6 @@
 import android.os.Bundle;
 import android.telecomm.PhoneAccountHandle;
 import android.telecomm.TelecommManager;
-import android.telecomm.TelecommManager;
 import android.telephony.PhoneNumberUtils;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
@@ -124,8 +123,16 @@
         PhoneAccountHandle phoneAccountHandle = intent.getParcelableExtra(
                 TelecommManager.EXTRA_PHONE_ACCOUNT_HANDLE);
 
+        Bundle clientExtras = null;
+        if (intent.hasExtra(TelecommManager.EXTRA_OUTGOING_CALL_EXTRAS)) {
+            clientExtras = intent.getBundleExtra(TelecommManager.EXTRA_OUTGOING_CALL_EXTRAS);
+        }
+        if (clientExtras == null) {
+            clientExtras = Bundle.EMPTY;
+        }
+
         // Send to CallsManager to ensure the InCallUI gets kicked off before the broadcast returns
-        Call call = mCallsManager.startOutgoingCall(handle, phoneAccountHandle);
+        Call call = mCallsManager.startOutgoingCall(handle, phoneAccountHandle, clientExtras);
 
         NewOutgoingCallIntentBroadcaster broadcaster = new NewOutgoingCallIntentBroadcaster(
                 mCallsManager, call, intent, isDefaultDialer());
diff --git a/src/com/android/telecomm/CallsManager.java b/src/com/android/telecomm/CallsManager.java
index fa24144..314020d 100644
--- a/src/com/android/telecomm/CallsManager.java
+++ b/src/com/android/telecomm/CallsManager.java
@@ -27,10 +27,10 @@
 import com.google.common.collect.ImmutableCollection;
 import com.google.common.collect.ImmutableList;
 
-import java.util.HashSet;
+import java.util.Collections;
 import java.util.List;
 import java.util.Set;
-import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.ConcurrentHashMap;
 
 /**
  * Singleton.
@@ -65,8 +65,13 @@
     /**
      * The main call repository. Keeps an instance of all live calls. New incoming and outgoing
      * calls are added to the map and removed when the calls move to the disconnected state.
+    *
+     * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
+     * load factor before resizing, 1 means we only expect a single thread to
+     * access the map so make only a single shard
      */
-    private final Set<Call> mCalls = new CopyOnWriteArraySet<Call>();
+    private final Set<Call> mCalls = Collections.newSetFromMap(
+            new ConcurrentHashMap<Call, Boolean>(8, 0.9f, 1));
 
     private final ConnectionServiceRepository mConnectionServiceRepository =
             new ConnectionServiceRepository();
@@ -74,7 +79,10 @@
     private final InCallController mInCallController = new InCallController();
     private final CallAudioManager mCallAudioManager;
     private final Ringer mRinger;
-    private final Set<CallsManagerListener> mListeners = new HashSet<>();
+    // For this set initial table size to 16 because we add 13 listeners in
+    // the CallsManager constructor.
+    private final Set<CallsManagerListener> mListeners = Collections.newSetFromMap(
+            new ConcurrentHashMap<CallsManagerListener, Boolean>(16, 0.9f, 1));
     private final HeadsetMediaButton mHeadsetMediaButton;
     private final WiredHeadsetManager mWiredHeadsetManager;
     private final TtyManager mTtyManager;
@@ -151,8 +159,7 @@
     @Override
     public void onCancelledOutgoingCall(Call call) {
         Log.v(this, "onCancelledOutgoingCall, call: %s", call);
-        setCallState(call, CallState.ABORTED);
-        removeCall(call);
+        markCallAsDisconnected(call, DisconnectCause.OUTGOING_CANCELED, null);
     }
 
     @Override
@@ -292,8 +299,13 @@
      * NOTE: emergency calls will never pass through this because they call
      * placeOutgoingCall directly.
      *
+     * @param handle Handle to connect the call with.
+     * @param phoneAccountHandle The phone account which contains the component name of the connection
+     *                     service to use for this call.
+     * @param extras The optional extras Bundle passed with the intent used for the outgoing call.
+     *
      */
-    Call startOutgoingCall(Uri handle, PhoneAccountHandle phoneAccountHandle) {
+    Call startOutgoingCall(Uri handle, PhoneAccountHandle phoneAccountHandle, Bundle extras) {
         // We only allow a single outgoing call at any given time. Before placing a call, make sure
         // there doesn't already exist another outgoing call.
         Call call = getFirstCallWithState(CallState.NEW, CallState.DIALING);
@@ -313,6 +325,7 @@
                 phoneAccountHandle,
                 false /* isIncoming */,
                 false /* isConference */);
+        call.setExtras(extras);
         call.setState(CallState.CONNECTING);
 
         if (!isPotentialMMICode(handle)) {
@@ -393,7 +406,7 @@
             call.startCreateConnection();
         } else {
             // This is the state where the user is expected to select an account
-            call.setState(CallState.PRE_DIAL_WAIT);
+            setCallState(call, CallState.PRE_DIAL_WAIT);
         }
     }
 
@@ -636,6 +649,13 @@
     }
 
     /**
+     * Removes an existing disconnected call, and notifies the in-call app.
+     */
+    void markCallAsRemoved(Call call) {
+        removeCall(call);
+    }
+
+    /**
      * Cleans up any calls currently associated with the specified connection service when the
      * service binder disconnects unexpectedly.
      *
@@ -643,7 +663,7 @@
      */
     void handleConnectionServiceDeath(ConnectionServiceWrapper service) {
         if (service != null) {
-            for (Call call : ImmutableList.copyOf(mCalls)) {
+            for (Call call : mCalls) {
                 if (call.getConnectionService() == service) {
                     markCallAsDisconnected(call, DisconnectCause.ERROR_UNSPECIFIED, null);
                 }
diff --git a/src/com/android/telecomm/ConnectionServiceWrapper.java b/src/com/android/telecomm/ConnectionServiceWrapper.java
index 2d5ade2..23ffab7 100644
--- a/src/com/android/telecomm/ConnectionServiceWrapper.java
+++ b/src/com/android/telecomm/ConnectionServiceWrapper.java
@@ -48,6 +48,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 
 /**
  * Wrapper for {@link IConnectionService}s, handles binding to {@link IConnectionService} and keeps
@@ -236,8 +237,18 @@
                     }
                     break;
                 }
-                case MSG_REMOVE_CALL:
+                case MSG_REMOVE_CALL: {
+                    call = mCallIdMapper.getCall(msg.obj);
+                    if (call != null) {
+                        if (call.isActive()) {
+                            mCallsManager.markCallAsDisconnected(
+                                    call, DisconnectCause.NORMAL, null);
+                        } else {
+                            mCallsManager.markCallAsRemoved(call);
+                        }
+                    }
                     break;
+                }
                 case MSG_ON_POST_DIAL_WAIT: {
                     SomeArgs args = (SomeArgs) msg.obj;
                     try {
@@ -475,6 +486,9 @@
         @Override
         public void removeCall(String callId) {
             logIncoming("removeCall %s", callId);
+            if (mCallIdMapper.isValidCallId(callId)) {
+                mHandler.obtainMessage(MSG_REMOVE_CALL, callId).sendToTarget();
+            }
         }
 
         @Override
@@ -602,7 +616,13 @@
 
     private final Adapter mAdapter = new Adapter();
     private final CallsManager mCallsManager = CallsManager.getInstance();
-    private final Set<Call> mPendingConferenceCalls = new HashSet<>();
+    /**
+     * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
+     * load factor before resizing, 1 means we only expect a single thread to
+     * access the map so make only a single shard
+     */
+    private final Set<Call> mPendingConferenceCalls = Collections.newSetFromMap(
+            new ConcurrentHashMap<Call, Boolean>(8, 0.9f, 1));
     private final CallIdMapper mCallIdMapper = new CallIdMapper("ConnectionService");
     private final Map<String, CreateConnectionResponse> mPendingResponses = new HashMap<>();
 
@@ -943,7 +963,8 @@
         }
 
         // Make a list of ConnectionServices that are listed as being associated with SIM accounts
-        final Set<ConnectionServiceWrapper> simServices = new HashSet<>();
+        final Set<ConnectionServiceWrapper> simServices = Collections.newSetFromMap(
+                new ConcurrentHashMap<ConnectionServiceWrapper, Boolean>(8, 0.9f, 1));
         for (PhoneAccountHandle handle : registrar.getOutgoingPhoneAccounts()) {
             PhoneAccount account = registrar.getPhoneAccount(handle);
             if ((account.getCapabilities() & PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION) != 0) {
diff --git a/src/com/android/telecomm/InCallController.java b/src/com/android/telecomm/InCallController.java
index 4f24c27..47f7f2b 100644
--- a/src/com/android/telecomm/InCallController.java
+++ b/src/com/android/telecomm/InCallController.java
@@ -369,6 +369,7 @@
                 childCallIds,
                 call.getStatusHints(),
                 call.getVideoState(),
-                conferenceableCallIds);
+                conferenceableCallIds,
+                call.getExtras());
     }
 }
diff --git a/src/com/android/telecomm/NewOutgoingCallIntentBroadcaster.java b/src/com/android/telecomm/NewOutgoingCallIntentBroadcaster.java
index a3aa740..65c1309 100644
--- a/src/com/android/telecomm/NewOutgoingCallIntentBroadcaster.java
+++ b/src/com/android/telecomm/NewOutgoingCallIntentBroadcaster.java
@@ -100,11 +100,19 @@
             String resultHandle = getResultData();
             Log.v(this, "- got number from resultData: %s", Log.pii(resultHandle));
 
+            boolean endEarly = false;
             if (resultHandle == null) {
                 Log.v(this, "Call cancelled (null number), returning...");
-                return;
+                endEarly = true;
             } else if (PhoneNumberUtils.isPotentialLocalEmergencyNumber(context, resultHandle)) {
                 Log.w(this, "Cannot modify outgoing call to emergency number %s.", resultHandle);
+                endEarly = true;
+            }
+
+            if (endEarly) {
+                if (mCall != null) {
+                    mCall.disconnect();
+                }
                 return;
             }
 
diff --git a/src/com/android/telecomm/PhoneAccountRegistrar.java b/src/com/android/telecomm/PhoneAccountRegistrar.java
index 98dcdf3..71eb95b 100644
--- a/src/com/android/telecomm/PhoneAccountRegistrar.java
+++ b/src/com/android/telecomm/PhoneAccountRegistrar.java
@@ -288,7 +288,9 @@
     }
 
     public void removeListener(Listener l) {
-        mListeners.remove(l);
+        if (l != null) {
+            mListeners.remove(l);
+        }
     }
 
     private void fireAccountsChanged() {
diff --git a/src/com/android/telecomm/RespondViaSmsSettings.java b/src/com/android/telecomm/RespondViaSmsSettings.java
index 1adf45c..231dbaa 100644
--- a/src/com/android/telecomm/RespondViaSmsSettings.java
+++ b/src/com/android/telecomm/RespondViaSmsSettings.java
@@ -152,19 +152,6 @@
      * Finish current Activity and go up to the top level Settings.
      */
     public static void goUpToTopLevelSetting(Activity activity) {
-        Intent intent = new Intent();
-        try {
-            intent.setClassName(
-                    activity.createPackageContext("com.android.phone", 0),
-                    "com.android.phone.CallFeaturesSetting");
-        } catch (PackageManager.NameNotFoundException e) {
-            Log.w(RespondViaSmsSettings.class,
-                    "Exception building package context com.android.phone", e);
-            return;
-        }
-        intent.setAction(Intent.ACTION_MAIN);
-        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
-        activity.startActivity(intent);
         activity.finish();
      }
 }
diff --git a/src/com/android/telecomm/ServiceBinder.java b/src/com/android/telecomm/ServiceBinder.java
index e918593..48442de 100644
--- a/src/com/android/telecomm/ServiceBinder.java
+++ b/src/com/android/telecomm/ServiceBinder.java
@@ -26,10 +26,11 @@
 import com.google.common.base.Preconditions;
 import com.google.common.base.Strings;
 
-import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 
+import java.util.Collections;
 import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 
 /**
  * Abstract class to perform the work of binding and unbinding to the specified service interface.
@@ -153,8 +154,12 @@
 
     /**
      * Set of currently registered listeners.
+     * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
+     * load factor before resizing, 1 means we only expect a single thread to
+     * access the map so make only a single shard
      */
-    private Set<Listener> mListeners = Sets.newHashSet();
+    private final Set<Listener> mListeners = Collections.newSetFromMap(
+            new ConcurrentHashMap<Listener, Boolean>(8, 0.9f, 1));
 
     /**
      * Persists the specified parameters and initializes the new instance.
@@ -231,7 +236,9 @@
     }
 
     final void removeListener(Listener listener) {
-        mListeners.remove(listener);
+        if (listener != null) {
+            mListeners.remove(listener);
+        }
     }
 
     /**
@@ -291,9 +298,7 @@
             setServiceInterface(binder);
 
             if (binder == null) {
-                // Use a copy of the listener list to allow the listeners to unregister themselves
-                // as part of the unbind without causing issues.
-                for (Listener l : ImmutableSet.copyOf(mListeners)) {
+                for (Listener l : mListeners) {
                     l.onUnbind(this);
                 }
             }
diff --git a/src/com/android/telecomm/WiredHeadsetManager.java b/src/com/android/telecomm/WiredHeadsetManager.java
index e59f1a5..aa2bbd1 100644
--- a/src/com/android/telecomm/WiredHeadsetManager.java
+++ b/src/com/android/telecomm/WiredHeadsetManager.java
@@ -22,7 +22,9 @@
 import android.content.IntentFilter;
 import android.media.AudioManager;
 
-import java.util.HashSet;
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 
 /** Listens for and caches headset state. */
 class WiredHeadsetManager {
@@ -45,7 +47,13 @@
 
     private final WiredHeadsetBroadcastReceiver mReceiver;
     private boolean mIsPluggedIn;
-    private final HashSet<Listener> mListeners = new HashSet<>();
+    /**
+     * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
+     * load factor before resizing, 1 means we only expect a single thread to
+     * access the map so make only a single shard
+     */
+    private final Set<Listener> mListeners = Collections.newSetFromMap(
+            new ConcurrentHashMap<Listener, Boolean>(8, 0.9f, 1));
 
     WiredHeadsetManager(Context context) {
         mReceiver = new WiredHeadsetBroadcastReceiver();
@@ -63,7 +71,9 @@
     }
 
     void removeListener(Listener listener) {
-        mListeners.remove(listener);
+        if (listener != null) {
+            mListeners.remove(listener);
+        }
     }
 
     boolean isPluggedIn() {