Merge "Support for enhanced call/connection extras." into nyc-dev
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 3402cc4..4e3c79d 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -74,6 +74,11 @@
     public static final int CALL_DIRECTION_INCOMING = 2;
     public static final int CALL_DIRECTION_UNKNOWN = 3;
 
+    /** Identifies extras changes which originated from a connection service. */
+    public static final int SOURCE_CONNECTION_SERVICE = 1;
+    /** Identifies extras changes which originated from an incall service. */
+    public static final int SOURCE_INCALL_SERVICE = 2;
+
     /**
      * Listener for events on the call.
      */
@@ -95,7 +100,8 @@
         void onCallerInfoChanged(Call call);
         void onIsVoipAudioModeChanged(Call call);
         void onStatusHintsChanged(Call call);
-        void onExtrasChanged(Call call);
+        void onExtrasChanged(Call c, int source, Bundle extras);
+        void onExtrasRemoved(Call c, int source, List<String> keys);
         void onHandleChanged(Call call);
         void onCallerDisplayNameChanged(Call call);
         void onVideoStateChanged(Call call);
@@ -144,7 +150,9 @@
         @Override
         public void onStatusHintsChanged(Call call) {}
         @Override
-        public void onExtrasChanged(Call call) {}
+        public void onExtrasChanged(Call c, int source, Bundle extras) {}
+        @Override
+        public void onExtrasRemoved(Call c, int source, List<String> keys) {}
         @Override
         public void onHandleChanged(Call call) {}
         @Override
@@ -1069,7 +1077,7 @@
         setRingbackRequested(connection.isRingbackRequested());
         setIsVoipAudioMode(connection.getIsVoipAudioMode());
         setStatusHints(connection.getStatusHints());
-        setExtras(connection.getExtras());
+        putExtras(SOURCE_CONNECTION_SERVICE, connection.getExtras());
 
         mConferenceableCalls.clear();
         for (String id : connection.getConferenceableConnectionIds()) {
@@ -1327,10 +1335,64 @@
         return mExtras;
     }
 
-    void setExtras(Bundle extras) {
-        mExtras = extras;
+    /**
+     * Adds extras to the extras bundle associated with this {@link Call}.
+     *
+     * Note: this method needs to know the source of the extras change (see
+     * {@link #SOURCE_CONNECTION_SERVICE}, {@link #SOURCE_INCALL_SERVICE}).  Extras changes which
+     * originate from a connection service will only be notified to incall services.  Likewise,
+     * changes originating from the incall services will only notify the connection service of the
+     * change.
+     *
+     * @param source The source of the extras addition.
+     * @param extras The extras.
+     */
+    void putExtras(int source, Bundle extras) {
+        if (extras == null) {
+            return;
+        }
+        if (mExtras == null) {
+            mExtras = new Bundle();
+        }
+        mExtras.putAll(extras);
+
         for (Listener l : mListeners) {
-            l.onExtrasChanged(this);
+            l.onExtrasChanged(this, source, extras);
+        }
+
+        // If the change originated from an InCallService, notify the connection service.
+        if (source == SOURCE_INCALL_SERVICE) {
+            mConnectionService.onExtrasChanged(this, mExtras);
+        }
+    }
+
+    /**
+     * Removes extras from the extras bundle associated with this {@link Call}.
+     *
+     * Note: this method needs to know the source of the extras change (see
+     * {@link #SOURCE_CONNECTION_SERVICE}, {@link #SOURCE_INCALL_SERVICE}).  Extras changes which
+     * originate from a connection service will only be notified to incall services.  Likewise,
+     * changes originating from the incall services will only notify the connection service of the
+     * change.
+     *
+     * @param source The source of the extras removal.
+     * @param keys The extra keys to remove.
+     */
+    void removeExtras(int source, List<String> keys) {
+        if (mExtras == null) {
+            return;
+        }
+        for (String key : keys) {
+            mExtras.remove(key);
+        }
+
+        for (Listener l : mListeners) {
+            l.onExtrasRemoved(this, source, keys);
+        }
+
+        // If the change originated from an InCallService, notify the connection service.
+        if (source == SOURCE_INCALL_SERVICE) {
+            mConnectionService.onExtrasChanged(this, mExtras);
         }
     }
 
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 73d5556..16e8f92 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -1144,7 +1144,14 @@
     }
 
     @Override
-    public void onExtrasChanged(Call call) {
+    public void onExtrasChanged(Call c, int source, Bundle extras) {
+        if (source != Call.SOURCE_CONNECTION_SERVICE) {
+            return;
+        }
+        handleCallTechnologyChange(c);
+    }
+
+    private void handleCallTechnologyChange(Call call) {
         if (call.getExtras() != null
                 && call.getExtras().containsKey(TelecomManager.EXTRA_CALL_TECHNOLOGY_TYPE)) {
 
@@ -1461,7 +1468,7 @@
         call.setVideoState(parcelableConference.getVideoState());
         call.setVideoProvider(parcelableConference.getVideoProvider());
         call.setStatusHints(parcelableConference.getStatusHints());
-        call.setExtras(parcelableConference.getExtras());
+        call.putExtras(Call.SOURCE_CONNECTION_SERVICE, parcelableConference.getExtras());
 
         // TODO: Move this to be a part of addCall()
         call.addListener(this);
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
index 5543f9b..69c7573 100644
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -487,16 +487,33 @@
         }
 
         @Override
-        public void setExtras(String callId, Bundle extras) {
-            Log.startSession("CSW.sE");
+        public void putExtras(String callId, Bundle extras) {
+            Log.startSession("CSW.pE");
             long token = Binder.clearCallingIdentity();
             try {
                 synchronized (mLock) {
                     Bundle.setDefusable(extras, true);
-                    logIncoming("setExtras %s %s", callId, extras);
                     Call call = mCallIdMapper.getCall(callId);
                     if (call != null) {
-                        call.setExtras(extras);
+                        call.putExtras(Call.SOURCE_CONNECTION_SERVICE, extras);
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(token);
+                Log.endSession();
+            }
+        }
+
+        @Override
+        public void removeExtras(String callId, List<String> keys) {
+            Log.startSession("CSW.rE");
+            long token = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    logIncoming("removeExtra %s %s", callId, keys);
+                    Call call = mCallIdMapper.getCall(callId);
+                    if (call != null) {
+                        call.removeExtras(Call.SOURCE_CONNECTION_SERVICE, keys);
                     }
                 }
             } finally {
@@ -962,6 +979,17 @@
         }
     }
 
+    void onExtrasChanged(Call call, Bundle extras) {
+        final String callId = mCallIdMapper.getCallId(call);
+        if (callId != null && isServiceValid("onExtrasChanged")) {
+            try {
+                logOutgoing("onExtrasChanged %s %s", callId, extras);
+                mServiceInterface.onExtrasChanged(callId, extras);
+            } catch (RemoteException ignored) {
+            }
+        }
+    }
+
     /** {@inheritDoc} */
     @Override
     protected void setServiceInterface(IBinder binder) {
diff --git a/src/com/android/server/telecom/InCallAdapter.java b/src/com/android/server/telecom/InCallAdapter.java
index 4dd06d2..31d268e 100644
--- a/src/com/android/server/telecom/InCallAdapter.java
+++ b/src/com/android/server/telecom/InCallAdapter.java
@@ -22,6 +22,8 @@
 
 import com.android.internal.telecom.IInCallAdapter;
 
+import java.util.List;
+
 /**
  * Receives call commands and updates from in-call app and passes them through to CallsManager.
  * {@link InCallController} creates an instance of this class and passes it to the in-call app after
@@ -415,6 +417,50 @@
     }
 
     @Override
+    public void putExtras(String callId, Bundle extras) {
+        try {
+            Log.startSession("ICA.pE", mOwnerComponentName);
+            long token = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    Call call = mCallIdMapper.getCall(callId);
+                    if (call != null) {
+                        call.putExtras(Call.SOURCE_INCALL_SERVICE, extras);
+                    } else {
+                        Log.w(this, "putExtras, unknown call id: %s", callId);
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        } finally {
+            Log.endSession();
+        }
+    }
+
+    @Override
+    public void removeExtras(String callId, List<String> keys) {
+        try {
+            Log.startSession("ICA.rE", mOwnerComponentName);
+            long token = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    Call call = mCallIdMapper.getCall(callId);
+                    if (call != null) {
+                        call.removeExtras(Call.SOURCE_INCALL_SERVICE, keys);
+                    } else {
+                        Log.w(this, "removeExtra, unknown call id: %s", callId);
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        } finally {
+            Log.endSession();
+        }
+    }
+
+    @Override
     public void turnOnProximitySensor() {
         try {
             Log.startSession("ICA.tOnPS", mOwnerComponentName);
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index 93589ed..4ee6c20 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -33,6 +33,7 @@
 import android.os.Trace;
 import android.os.UserHandle;
 import android.telecom.CallAudioState;
+import android.telecom.ConnectionService;
 import android.telecom.DefaultDialerManager;
 import android.telecom.InCallService;
 import android.telecom.ParcelableCall;
@@ -104,8 +105,44 @@
             updateCall(call);
         }
 
+        /**
+         * Listens for changes to extras reported by a Telecom {@link Call}.
+         *
+         * Extras changes can originate from a {@link ConnectionService} or an {@link InCallService}
+         * so we will only trigger an update of the call information if the source of the extras
+         * change was a {@link ConnectionService}.
+         *
+         * @param call The call.
+         * @param source The source of the extras change ({@link Call#SOURCE_CONNECTION_SERVICE} or
+         *               {@link Call#SOURCE_INCALL_SERVICE}).
+         * @param extras The extras.
+         */
         @Override
-        public void onExtrasChanged(Call call) {
+        public void onExtrasChanged(Call call, int source, Bundle extras) {
+            // Do not inform InCallServices of changes which originated there.
+            if (source == Call.SOURCE_INCALL_SERVICE) {
+                return;
+            }
+            updateCall(call);
+        }
+
+        /**
+         * Listens for changes to extras reported by a Telecom {@link Call}.
+         *
+         * Extras changes can originate from a {@link ConnectionService} or an {@link InCallService}
+         * so we will only trigger an update of the call information if the source of the extras
+         * change was a {@link ConnectionService}.
+         *  @param call The call.
+         * @param source The source of the extras change ({@link Call#SOURCE_CONNECTION_SERVICE} or
+         *               {@link Call#SOURCE_INCALL_SERVICE}).
+         * @param keys The extra key removed
+         */
+        @Override
+        public void onExtrasRemoved(Call call, int source, List<String> keys) {
+            // Do not inform InCallServices of changes which originated there.
+            if (source == Call.SOURCE_INCALL_SERVICE) {
+                return;
+            }
             updateCall(call);
         }
 
diff --git a/testapps/src/com/android/server/telecom/testapps/TestConnectionService.java b/testapps/src/com/android/server/telecom/testapps/TestConnectionService.java
index c819838..b30e372 100644
--- a/testapps/src/com/android/server/telecom/testapps/TestConnectionService.java
+++ b/testapps/src/com/android/server/telecom/testapps/TestConnectionService.java
@@ -374,7 +374,7 @@
                 connectionExtras.putString(Connection.EXTRA_CALL_SUBJECT,
                         "This is a test of call subject lines.");
             }
-            connection.setExtras(connectionExtras);
+            connection.putExtras(connectionExtras);
 
             setAddress(connection, address);
 
diff --git a/tests/src/com/android/server/telecom/tests/BasicCallTests.java b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
index ac080ae..6279c5e 100644
--- a/tests/src/com/android/server/telecom/tests/BasicCallTests.java
+++ b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
@@ -585,33 +585,6 @@
         assertTrue(updatedConference.getChildCallIds().contains(callId3.mCallId));
     }
 
-    private ParcelableCall makeConferenceCall() throws Exception {
-        IdPair callId1 = startAndMakeActiveOutgoingCall("650-555-1212",
-                mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
-
-        IdPair callId2 = startAndMakeActiveOutgoingCall("650-555-1213",
-                mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
-
-        IInCallAdapter inCallAdapter = mInCallServiceFixtureX.getInCallAdapter();
-        inCallAdapter.conference(callId1.mCallId, callId2.mCallId);
-        // Wait for wacky non-deterministic behavior
-        Thread.sleep(200);
-        ParcelableCall call1 = mInCallServiceFixtureX.getCall(callId1.mCallId);
-        ParcelableCall call2 = mInCallServiceFixtureX.getCall(callId2.mCallId);
-        // Check that the two calls end up with a parent in the end
-        assertNotNull(call1.getParentCallId());
-        assertNotNull(call2.getParentCallId());
-        assertEquals(call1.getParentCallId(), call2.getParentCallId());
-
-        // Check to make sure that the parent call made it to the in-call service
-        String parentCallId = call1.getParentCallId();
-        ParcelableCall conferenceCall = mInCallServiceFixtureX.getCall(parentCallId);
-        assertEquals(2, conferenceCall.getChildCallIds().size());
-        assertTrue(conferenceCall.getChildCallIds().contains(callId1.mCallId));
-        assertTrue(conferenceCall.getChildCallIds().contains(callId2.mCallId));
-        return conferenceCall;
-    }
-
     /**
      * Tests the {@link Call#pullExternalCall()} API.  Verifies that if a call is not an external
      * call, no pull call request is made to the connection service.
diff --git a/tests/src/com/android/server/telecom/tests/CallExtrasTest.java b/tests/src/com/android/server/telecom/tests/CallExtrasTest.java
new file mode 100644
index 0000000..e9cd733
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/CallExtrasTest.java
@@ -0,0 +1,433 @@
+/*
+ * Copyright (C) 2016 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.tests;
+
+import android.os.Bundle;
+import android.telecom.Call;
+import android.telecom.Conference;
+import android.telecom.Connection;
+import android.telecom.InCallService;
+import android.telecom.ParcelableCall;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.test.suitebuilder.annotation.MediumTest;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * Tests the {@link Connection} and {@link Call} extras functionality.
+ */
+public class CallExtrasTest extends TelecomSystemTest {
+
+    public final static String EXTRA_KEY_STR = "STRINGKEY";
+    public final static String EXTRA_KEY_STR2 = "BLAHTEST";
+    public final static String EXTRA_KEY_INT = "INTKEY";
+    public final static String EXTRA_KEY_BOOL = "BOOLKEY";
+    public final static String EXTRA_VALUE_STR = "socks";
+    public final static String EXTRA_VALUE2_STR = "mozzarella";
+    public final static int EXTRA_VALUE_INT = 1234;
+
+    /**
+     * Tests setting extras on the connection side and ensuring they are propagated through to
+     * the InCallService.
+     *
+     * @throws Exception
+     */
+    @MediumTest
+    public void testCsPutExtras() throws Exception {
+        // Get a call up and running.
+        IdPair ids = startAndMakeActiveIncomingCall("650-555-1212",
+                mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+
+        // Communicate extras from the ConnectionService to the InCallService.
+        Bundle extras = new Bundle();
+        extras.putString(EXTRA_KEY_STR, EXTRA_VALUE_STR);
+        extras.putInt(EXTRA_KEY_INT, EXTRA_VALUE_INT);
+
+        Connection connection = mConnectionServiceFixtureA.mLatestConnection;
+        connection.putExtras(extras);
+        mInCallServiceFixtureX.waitForUpdate();
+        assertTrue(mInCallServiceFixtureX.getCall(ids.mCallId).getExtras().containsKey(
+                EXTRA_KEY_STR));
+        assertTrue(mInCallServiceFixtureX.getCall(ids.mCallId).getExtras().containsKey(
+                EXTRA_KEY_INT));
+    }
+
+    /**
+     * Tests setting extras on the connection side and ensuring they are propagated through to
+     * the InCallService.
+     *
+     * @throws Exception
+     */
+    @MediumTest
+    public void testCsPutBooleanExtra() throws Exception {
+        // Get a call up and running.
+        IdPair ids = startAndMakeActiveIncomingCall("650-555-1212",
+                mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+
+        Connection connection = mConnectionServiceFixtureA.mLatestConnection;
+        connection.putExtra(EXTRA_KEY_BOOL, true);
+        mInCallServiceFixtureX.waitForUpdate();
+        assertTrue(mInCallServiceFixtureX.getCall(ids.mCallId).getExtras()
+                .containsKey(EXTRA_KEY_BOOL));
+        assertTrue(
+                mInCallServiceFixtureX.getCall(ids.mCallId).getExtras().getBoolean(EXTRA_KEY_BOOL));
+    }
+
+    /**
+     * Tests setting extras on the connection side and ensuring they are propagated through to
+     * the InCallService.
+     *
+     * @throws Exception
+     */
+    @MediumTest
+    public void testCsPutIntExtra() throws Exception {
+        // Get a call up and running.
+        IdPair ids = startAndMakeActiveIncomingCall("650-555-1212",
+                mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+
+        Connection connection = mConnectionServiceFixtureA.mLatestConnection;
+        connection.putExtra(EXTRA_KEY_INT, EXTRA_VALUE_INT);
+        mInCallServiceFixtureX.waitForUpdate();
+        assertTrue(
+                mInCallServiceFixtureX.getCall(ids.mCallId).getExtras().containsKey(EXTRA_KEY_INT));
+        assertEquals(EXTRA_VALUE_INT,
+                mInCallServiceFixtureX.getCall(ids.mCallId).getExtras().getInt(EXTRA_KEY_INT));
+    }
+
+    /**
+     * Tests setting extras on the connection side and ensuring they are propagated through to
+     * the InCallService.
+     *
+     * @throws Exception
+     */
+    @MediumTest
+    public void testCsPutStringExtra() throws Exception {
+        // Get a call up and running.
+        IdPair ids = startAndMakeActiveIncomingCall("650-555-1212",
+                mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+
+        Connection connection = mConnectionServiceFixtureA.mLatestConnection;
+        connection.putExtra(EXTRA_KEY_STR, EXTRA_VALUE_STR);
+
+        mInCallServiceFixtureX.waitForUpdate();
+        assertTrue(
+                mInCallServiceFixtureX.getCall(ids.mCallId).getExtras().containsKey(EXTRA_KEY_STR));
+        assertEquals(EXTRA_VALUE_STR,
+                mInCallServiceFixtureX.getCall(ids.mCallId).getExtras().getString(EXTRA_KEY_STR));
+    }
+
+    /**
+     * Tests remove extras on the connection side and ensuring the removal is reflected in the
+     * InCallService.
+     *
+     * @throws Exception
+     */
+    @MediumTest
+    public void testCsRemoveExtra() throws Exception {
+        // Get a call up and running."STRING"
+        IdPair ids = startAndMakeActiveIncomingCall("650-555-1212",
+                mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+
+        // Add something.
+        Connection connection = mConnectionServiceFixtureA.mLatestConnection;
+        connection.putExtra(EXTRA_KEY_STR2, EXTRA_VALUE_STR);
+        connection.putExtra(EXTRA_KEY_STR, EXTRA_VALUE_STR);
+        mInCallServiceFixtureX.waitForUpdate();
+        assertTrue(
+                mInCallServiceFixtureX.getCall(ids.mCallId).getExtras().containsKey(EXTRA_KEY_STR));
+        assertEquals(EXTRA_VALUE_STR,
+                mInCallServiceFixtureX.getCall(ids.mCallId).getExtras().getString(EXTRA_KEY_STR));
+
+        // Take it away.
+        connection.removeExtras(new ArrayList<String>(Arrays.asList(EXTRA_KEY_STR)));
+        mInCallServiceFixtureX.waitForUpdate();
+        assertFalse(
+                mInCallServiceFixtureX.getCall(ids.mCallId).getExtras().containsKey(EXTRA_KEY_STR));
+        assertTrue(mInCallServiceFixtureX.getCall(ids.mCallId).getExtras()
+                .containsKey(EXTRA_KEY_STR2));
+    }
+
+    /**
+     * Tests putting a new value for an existing extras key.
+     *
+     * @throws Exception
+     */
+    @MediumTest
+    public void testCsUpdateExisting() throws Exception {
+        // Get a call up and running.
+        IdPair ids = startAndMakeActiveIncomingCall("650-555-1212",
+                mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+
+        Connection connection = mConnectionServiceFixtureA.mLatestConnection;
+
+        // Communicate extras from the ConnectionService to the InCallService.
+        Bundle extras = new Bundle();
+        extras.putString(EXTRA_KEY_STR, EXTRA_VALUE_STR);
+        extras.putInt(EXTRA_KEY_INT, EXTRA_VALUE_INT);
+        connection.putExtras(extras);
+        mInCallServiceFixtureX.waitForUpdate();
+        assertTrue(
+                mInCallServiceFixtureX.getCall(ids.mCallId).getExtras().containsKey(EXTRA_KEY_STR));
+        assertTrue(
+                mInCallServiceFixtureX.getCall(ids.mCallId).getExtras().containsKey(EXTRA_KEY_INT));
+
+        connection.putExtra(EXTRA_KEY_STR, EXTRA_VALUE2_STR);
+        mInCallServiceFixtureX.waitForUpdate();
+        assertEquals(EXTRA_VALUE2_STR,
+                mInCallServiceFixtureX.getCall(ids.mCallId).getExtras().getString(EXTRA_KEY_STR));
+        assertTrue(
+                mInCallServiceFixtureX.getCall(ids.mCallId).getExtras().containsKey(EXTRA_KEY_INT));
+    }
+
+    /**
+     * Tests ability of the deprecated setExtras method to detect changes to the extras bundle
+     * and merge these changes into the telecom extras.  The old setExtras worked by just replacing
+     * the entire extras bundle, so we need to ensure that we can properly handle cases where an
+     * API user has added or removed items from the extras, but still gracefully merge this into the
+     * extras maintained for the connection.
+     *
+     * @throws Exception
+     */
+    @MediumTest
+    public void testCsSetExtras() throws Exception {
+        // Get a call up and running.
+        IdPair ids = startAndMakeActiveIncomingCall("650-555-1212",
+                mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+
+        Connection connection = mConnectionServiceFixtureA.mLatestConnection;
+
+        // Set the initial bundle.
+        Bundle extras = new Bundle();
+        extras.putString(EXTRA_KEY_STR, EXTRA_VALUE_STR);
+        extras.putInt(EXTRA_KEY_INT, EXTRA_VALUE_INT);
+        connection.setExtras(extras);
+        mInCallServiceFixtureX.waitForUpdate();
+        assertTrue(
+                mInCallServiceFixtureX.getCall(ids.mCallId).getExtras().containsKey(EXTRA_KEY_STR));
+        assertTrue(
+                mInCallServiceFixtureX.getCall(ids.mCallId).getExtras().containsKey(EXTRA_KEY_INT));
+
+        // Modify the initial bundle to add a value and remove another.
+        extras.putString(EXTRA_KEY_STR2, EXTRA_VALUE2_STR);
+        extras.remove(EXTRA_KEY_STR);
+        connection.setExtras(extras);
+        mInCallServiceFixtureX.waitForUpdate();
+        assertFalse(
+                mInCallServiceFixtureX.getCall(ids.mCallId).getExtras().containsKey(EXTRA_KEY_STR));
+        assertTrue(
+                mInCallServiceFixtureX.getCall(ids.mCallId).getExtras().containsKey(EXTRA_KEY_INT));
+        assertTrue(
+                mInCallServiceFixtureX.getCall(ids.mCallId).getExtras()
+                        .containsKey(EXTRA_KEY_STR2));
+    }
+
+    /**
+     * Tests that additions to the extras via an {@link InCallService} are propagated back down to
+     * the {@link Connection}.
+     *
+     * @throws Exception
+     */
+    @MediumTest
+    public void testICSPutExtras() throws Exception {
+        Bundle extras = new Bundle();
+        extras.putString(EXTRA_KEY_STR, EXTRA_VALUE_STR);
+        extras.putInt(EXTRA_KEY_INT, EXTRA_VALUE_INT);
+
+        // Get a call up and running.
+        IdPair ids = startAndMakeActiveIncomingCall("650-555-1212",
+                mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+        mInCallServiceFixtureX.mInCallAdapter.putExtras(ids.mCallId, extras);
+        mConnectionServiceFixtureA.waitForExtras();
+
+        Connection connection = mConnectionServiceFixtureA.mLatestConnection;
+        assertNotNull(connection);
+        Bundle connectionExtras = connection.getExtras();
+        assertTrue(connectionExtras.containsKey(EXTRA_KEY_STR));
+        assertEquals(EXTRA_VALUE_STR, extras.getString(EXTRA_KEY_STR));
+        assertTrue(connectionExtras.containsKey(EXTRA_KEY_INT));
+        assertEquals(EXTRA_VALUE_INT, extras.getInt(EXTRA_KEY_INT));
+    }
+
+    /**
+     * A bi-directional test of the extras.  Tests setting extras from both the ConnectionService
+     * and InCall side and ensuring the bundles are merged appropriately.
+     *
+     * @throws Exception
+     */
+    @LargeTest
+    public void testExtrasBidirectional() throws Exception {
+        // Get a call up and running.
+        IdPair ids = startAndMakeActiveIncomingCall("650-555-1212",
+                mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+
+        Connection connection = mConnectionServiceFixtureA.mLatestConnection;
+
+        // Set the initial bundle.
+        Bundle someExtras = new Bundle();
+        someExtras.putString(EXTRA_KEY_STR, EXTRA_VALUE_STR);
+        someExtras.putInt(EXTRA_KEY_INT, EXTRA_VALUE_INT);
+        connection.setExtras(someExtras);
+        mInCallServiceFixtureX.waitForUpdate();
+        assertTrue(
+                mInCallServiceFixtureX.getCall(ids.mCallId).getExtras().containsKey(EXTRA_KEY_STR));
+        assertTrue(
+                mInCallServiceFixtureX.getCall(ids.mCallId).getExtras().containsKey(EXTRA_KEY_INT));
+
+        // From the InCall side, add another key
+        Bundle someMoreExtras = new Bundle();
+        someMoreExtras.putBoolean(EXTRA_KEY_BOOL, true);
+        mInCallServiceFixtureX.mInCallAdapter.putExtras(ids.mCallId, someMoreExtras);
+        mConnectionServiceFixtureA.waitForExtras();
+        Bundle connectionExtras = connection.getExtras();
+        assertTrue(connectionExtras.containsKey(EXTRA_KEY_STR));
+        assertTrue(connectionExtras.containsKey(EXTRA_KEY_INT));
+        assertTrue(connectionExtras.containsKey(EXTRA_KEY_BOOL));
+
+        // Modify the initial bundle to add a value and remove another.
+        someExtras.putString(EXTRA_KEY_STR2, EXTRA_VALUE2_STR);
+        someExtras.remove(EXTRA_KEY_STR);
+        connection.setExtras(someExtras);
+        mInCallServiceFixtureX.waitForUpdate();
+        assertFalse(
+                mInCallServiceFixtureX.getCall(ids.mCallId).getExtras().containsKey(EXTRA_KEY_STR));
+        assertTrue(
+                mInCallServiceFixtureX.getCall(ids.mCallId).getExtras().containsKey(EXTRA_KEY_INT));
+        assertTrue(
+                mInCallServiceFixtureX.getCall(ids.mCallId).getExtras()
+                        .containsKey(EXTRA_KEY_STR2));
+        assertTrue(
+                mInCallServiceFixtureX.getCall(ids.mCallId).getExtras()
+                        .containsKey(EXTRA_KEY_BOOL));
+    }
+
+    /**
+     * Similar to {@link #testCsSetExtras()}, tests to ensure the existing setExtras functionality
+     * is maintained.
+     *
+     * @throws Exception
+     */
+    @LargeTest
+    public void testConferenceSetExtras() throws Exception {
+        ParcelableCall call = makeConferenceCall();
+        String conferenceId = call.getId();
+
+        Conference conference = mConnectionServiceFixtureA.mLatestConference;
+        assertNotNull(conference);
+
+        Bundle someExtras = new Bundle();
+        someExtras.putString(EXTRA_KEY_STR, EXTRA_VALUE_STR);
+        someExtras.putInt(EXTRA_KEY_INT, EXTRA_VALUE_INT);
+        conference.setExtras(someExtras);
+
+        mInCallServiceFixtureX.waitForUpdate();
+        assertTrue(
+                mInCallServiceFixtureX.getCall(conferenceId).getExtras()
+                        .containsKey(EXTRA_KEY_STR));
+        assertTrue(
+                mInCallServiceFixtureX.getCall(conferenceId).getExtras()
+                        .containsKey(EXTRA_KEY_INT));
+
+        someExtras.putString(EXTRA_KEY_STR2, EXTRA_VALUE2_STR);
+        someExtras.remove(EXTRA_KEY_INT);
+        conference.setExtras(someExtras);
+
+        mInCallServiceFixtureX.waitForUpdate();
+        assertTrue(
+                mInCallServiceFixtureX.getCall(conferenceId).getExtras()
+                        .containsKey(EXTRA_KEY_STR));
+        assertFalse(
+                mInCallServiceFixtureX.getCall(conferenceId).getExtras()
+                        .containsKey(EXTRA_KEY_INT));
+        assertTrue(
+                mInCallServiceFixtureX.getCall(conferenceId).getExtras()
+                        .containsKey(EXTRA_KEY_STR2));
+    }
+
+    /**
+     * Tests putExtras for conferences.
+     *
+     * @throws Exception
+     */
+    @LargeTest
+    public void testConferenceExtraOperations() throws Exception {
+        ParcelableCall call = makeConferenceCall();
+        String conferenceId = call.getId();
+        Conference conference = mConnectionServiceFixtureA.mLatestConference;
+        assertNotNull(conference);
+
+        conference.putExtra(EXTRA_KEY_STR, EXTRA_VALUE_STR);
+        conference.putExtra(EXTRA_KEY_INT, EXTRA_VALUE_INT);
+        conference.putExtra(EXTRA_KEY_BOOL, true);
+        mInCallServiceFixtureX.waitForUpdate();
+
+        assertTrue(mInCallServiceFixtureX.getCall(conferenceId).getExtras()
+                .containsKey(EXTRA_KEY_STR));
+        assertEquals(EXTRA_VALUE_STR,
+                mInCallServiceFixtureX.getCall(conferenceId).getExtras().get(EXTRA_KEY_STR));
+        assertTrue(
+                mInCallServiceFixtureX.getCall(conferenceId).getExtras()
+                        .containsKey(EXTRA_KEY_INT));
+        assertEquals(EXTRA_VALUE_INT,
+                mInCallServiceFixtureX.getCall(conferenceId).getExtras().get(EXTRA_KEY_INT));
+        assertEquals(true,
+                mInCallServiceFixtureX.getCall(conferenceId).getExtras().get(EXTRA_KEY_BOOL));
+
+        conference.removeExtras(new ArrayList<String>(Arrays.asList(EXTRA_KEY_STR)));
+        mInCallServiceFixtureX.waitForUpdate();
+        assertFalse(mInCallServiceFixtureX.getCall(conferenceId).getExtras()
+                .containsKey(EXTRA_KEY_STR));
+    }
+
+    /**
+     * Tests communication of extras from an InCallService to a Conference.
+     *
+     * @throws Exception
+     */
+    @LargeTest
+    public void testConferenceICS() throws Exception {
+        ParcelableCall call = makeConferenceCall();
+        String conferenceId = call.getId();
+        Conference conference = mConnectionServiceFixtureA.mLatestConference;
+
+        Bundle someExtras = new Bundle();
+        someExtras.putString(EXTRA_KEY_STR, EXTRA_VALUE_STR);
+        mInCallServiceFixtureX.mInCallAdapter.putExtras(conferenceId, someExtras);
+        mConnectionServiceFixtureA.waitForExtras();
+
+        Bundle conferenceExtras = conference.getExtras();
+        assertTrue(conferenceExtras.containsKey(EXTRA_KEY_STR));
+
+        Bundle someMoreExtras = new Bundle();
+        someMoreExtras.putString(EXTRA_KEY_STR2, EXTRA_VALUE_STR);
+        someMoreExtras.putInt(EXTRA_KEY_INT, EXTRA_VALUE_INT);
+        someMoreExtras.putBoolean(EXTRA_KEY_BOOL, true);
+        mInCallServiceFixtureX.mInCallAdapter.putExtras(conferenceId, someMoreExtras);
+        mConnectionServiceFixtureA.waitForExtras();
+        conferenceExtras = conference.getExtras();
+        assertTrue(conferenceExtras.containsKey(EXTRA_KEY_STR));
+        assertTrue(conferenceExtras.containsKey(EXTRA_KEY_STR2));
+        assertTrue(conferenceExtras.containsKey(EXTRA_KEY_INT));
+        assertTrue(conferenceExtras.containsKey(EXTRA_KEY_BOOL));
+
+        mInCallServiceFixtureX.mInCallAdapter.removeExtras(conferenceId,
+                new ArrayList<String>(Arrays.asList(EXTRA_KEY_STR)));
+        mConnectionServiceFixtureA.waitForExtras();
+        conferenceExtras = conference.getExtras();
+        assertFalse(conferenceExtras.containsKey(EXTRA_KEY_STR));
+    }
+}
diff --git a/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java b/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java
index 27f169d..8d78f3d 100644
--- a/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java
+++ b/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java
@@ -48,11 +48,14 @@
 
 import java.lang.Override;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Controls a test {@link IConnectionService} as would be provided by a source of connectivity
@@ -61,6 +64,7 @@
 public class ConnectionServiceFixture implements TestFixture<IConnectionService> {
     static int INVALID_VIDEO_STATE = -1;
     static int CAPABILITIES_NOT_SPECIFIED = 0;
+    public CountDownLatch mExtrasLock = new CountDownLatch(1);
 
     /**
      * Implementation of ConnectionService that performs no-ops for tasks normally meant for
@@ -73,7 +77,8 @@
         @Override
         public Connection onCreateUnknownConnection(
                 PhoneAccountHandle connectionManagerPhoneAccount, ConnectionRequest request) {
-            return new FakeConnection(request.getVideoState(), request.getAddress());
+            mLatestConnection = new FakeConnection(request.getVideoState(), request.getAddress());
+            return mLatestConnection;
         }
 
         @Override
@@ -82,6 +87,7 @@
             FakeConnection fakeConnection =  new FakeConnection(
                     mVideoState == INVALID_VIDEO_STATE ? request.getVideoState() : mVideoState,
                     request.getAddress());
+            mLatestConnection = fakeConnection;
             if (mCapabilities != CAPABILITIES_NOT_SPECIFIED) {
                 fakeConnection.setConnectionCapabilities(mCapabilities);
             }
@@ -92,7 +98,8 @@
         @Override
         public Connection onCreateOutgoingConnection(
                 PhoneAccountHandle connectionManagerPhoneAccount, ConnectionRequest request) {
-            return new FakeConnection(request.getVideoState(), request.getAddress());
+            mLatestConnection = new FakeConnection(request.getVideoState(), request.getAddress());
+            return mLatestConnection;
         }
 
         @Override
@@ -103,6 +110,7 @@
             Conference fakeConference = new FakeConference();
             fakeConference.addConnection(cxn1);
             fakeConference.addConnection(cxn2);
+            mLatestConference = fakeConference;
             addConference(fakeConference);
         }
     }
@@ -119,6 +127,11 @@
             setActive();
             setAddress(address, TelecomManager.PRESENTATION_ALLOWED);
         }
+
+        @Override
+        public void onExtrasChanged(Bundle extras) {
+            mExtrasLock.countDown();
+        }
     }
 
     public class FakeConference extends Conference {
@@ -136,6 +149,12 @@
             // Do nothing besides inform the connection that it was merged into this conference.
             connection.setConference(this);
         }
+
+        @Override
+        public void onExtrasChanged(Bundle extras) {
+            Log.w(this, "FakeConference onExtrasChanged");
+            mExtrasLock.countDown();
+        }
     }
 
     public class FakeConnectionService extends IConnectionService.Stub {
@@ -248,6 +267,10 @@
         public void sendCallEvent(String callId, String event, Bundle extras) throws RemoteException
         {}
 
+        public void onExtrasChanged(String callId, Bundle extras) throws RemoteException {
+            mConnectionServiceDelegateAdapter.onExtrasChanged(callId, extras);
+        }
+
         @Override
         public IBinder asBinder() {
             return this;
@@ -304,6 +327,8 @@
     }
 
     public String mLatestConnectionId;
+    public Connection mLatestConnection;
+    public Conference mLatestConference;
     public final Set<IConnectionServiceAdapter> mConnectionServiceAdapters = new HashSet<>();
     public final Map<String, ConnectionInfo> mConnectionById = new HashMap<>();
     public final Map<String, ConferenceInfo> mConferenceById = new HashMap<>();
@@ -491,6 +516,18 @@
         }
     }
 
+    /**
+     * Waits until the {@link Connection#onExtrasChanged(Bundle)} API has been called on a
+     * {@link Connection} or {@link Conference}.
+     */
+    public void waitForExtras() {
+        try {
+            mExtrasLock.await(TelecomSystemTest.TEST_TIMEOUT, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException ie) {
+        }
+        mExtrasLock = new CountDownLatch(1);
+    }
+
     private ParcelableConference parcelable(ConferenceInfo c) {
         return new ParcelableConference(
                 c.phoneAccount,
diff --git a/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java b/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java
index eb007a3..bf12411 100644
--- a/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java
+++ b/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java
@@ -31,6 +31,8 @@
 
 import java.util.HashMap;
 import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Controls a test {@link IInCallService} as would be provided by an InCall UI on a system.
@@ -47,6 +49,7 @@
     public boolean mShowDialpad;
     public boolean mCanAddCall;
     public boolean mSilenceRinger;
+    public CountDownLatch mLock = new CountDownLatch(1);
 
     public class FakeInCallService extends IInCallService.Stub {
         @Override
@@ -76,6 +79,7 @@
             }
             mCallById.put(call.getId(), call);
             mLatestCallId = call.getId();
+            mLock.countDown();
         }
 
         @Override
@@ -144,4 +148,13 @@
     public IInCallAdapter getInCallAdapter() {
         return mInCallAdapter;
     }
+
+    public void waitForUpdate() {
+        try {
+            mLock.await(5000, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException ie) {
+            return;
+        }
+        mLock = new CountDownLatch(1);
+    }
 }
diff --git a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
index 12d82b3..eeacd84 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
@@ -249,6 +249,33 @@
         super.tearDown();
     }
 
+    protected ParcelableCall makeConferenceCall() throws Exception {
+        IdPair callId1 = startAndMakeActiveOutgoingCall("650-555-1212",
+                mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+
+        IdPair callId2 = startAndMakeActiveOutgoingCall("650-555-1213",
+                mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+
+        IInCallAdapter inCallAdapter = mInCallServiceFixtureX.getInCallAdapter();
+        inCallAdapter.conference(callId1.mCallId, callId2.mCallId);
+        // Wait for wacky non-deterministic behavior
+        Thread.sleep(200);
+        ParcelableCall call1 = mInCallServiceFixtureX.getCall(callId1.mCallId);
+        ParcelableCall call2 = mInCallServiceFixtureX.getCall(callId2.mCallId);
+        // Check that the two calls end up with a parent in the end
+        assertNotNull(call1.getParentCallId());
+        assertNotNull(call2.getParentCallId());
+        assertEquals(call1.getParentCallId(), call2.getParentCallId());
+
+        // Check to make sure that the parent call made it to the in-call service
+        String parentCallId = call1.getParentCallId();
+        ParcelableCall conferenceCall = mInCallServiceFixtureX.getCall(parentCallId);
+        assertEquals(2, conferenceCall.getChildCallIds().size());
+        assertTrue(conferenceCall.getChildCallIds().contains(callId1.mCallId));
+        assertTrue(conferenceCall.getChildCallIds().contains(callId2.mCallId));
+        return conferenceCall;
+    }
+
     private void setupTelecomSystem() throws Exception {
         // Use actual implementations instead of mocking the interface out.
         HeadsetMediaButtonFactory headsetMediaButtonFactory =