Merge "Publish new Telecomm API for Connection Services"
diff --git a/api/current.txt b/api/current.txt
index 128c4d2..1fd5d63 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -26738,6 +26738,83 @@
     enum_constant public static final android.telecomm.CallState RINGING;
   }
 
+  public abstract class Connection {
+    ctor protected Connection();
+    method public final android.telecomm.CallAudioState getCallAudioState();
+    method public final android.net.Uri getHandle();
+    method protected void onAbort();
+    method protected void onAnswer();
+    method protected void onDisconnect();
+    method protected void onHold();
+    method protected void onPlayDtmfTone(char);
+    method protected void onReject();
+    method protected void onSetAudioState(android.telecomm.CallAudioState);
+    method protected void onSetSignal(android.os.Bundle);
+    method protected void onStopDtmfTone();
+    method protected void onUnhold();
+    method protected void setActive();
+    method public void setAudioState(android.telecomm.CallAudioState);
+    method protected void setDialing();
+    method protected void setDisconnected(int, java.lang.String);
+    method protected void setHandle(android.net.Uri);
+    method protected void setOnHold();
+    method protected void setRinging();
+    method public static java.lang.String stateToString(int);
+  }
+
+  public static abstract interface Connection.Listener {
+    method public abstract void onAudioStateChanged(android.telecomm.Connection, android.telecomm.CallAudioState);
+    method public abstract void onDestroyed(android.telecomm.Connection);
+    method public abstract void onDisconnected(android.telecomm.Connection, int, java.lang.String);
+    method public abstract void onHandleChanged(android.telecomm.Connection, android.net.Uri);
+    method public abstract void onSignalChanged(android.telecomm.Connection, android.os.Bundle);
+    method public abstract void onStateChanged(android.telecomm.Connection, int);
+  }
+
+  public static class Connection.ListenerBase implements android.telecomm.Connection.Listener {
+    ctor public Connection.ListenerBase();
+    method public void onAudioStateChanged(android.telecomm.Connection, android.telecomm.CallAudioState);
+    method public void onDestroyed(android.telecomm.Connection);
+    method public void onDisconnected(android.telecomm.Connection, int, java.lang.String);
+    method public void onHandleChanged(android.telecomm.Connection, android.net.Uri);
+    method public void onSignalChanged(android.telecomm.Connection, android.os.Bundle);
+    method public void onStateChanged(android.telecomm.Connection, int);
+  }
+
+  public final class Connection.State {
+    field public static final int ACTIVE = 3; // 0x3
+    field public static final int DIALING = 2; // 0x2
+    field public static final int DISCONNECTED = 5; // 0x5
+    field public static final int HOLDING = 4; // 0x4
+    field public static final int NEW = 0; // 0x0
+    field public static final int RINGING = 1; // 0x1
+  }
+
+  public final class ConnectionRequest {
+    ctor public ConnectionRequest(android.net.Uri, android.os.Bundle);
+    method public android.os.Bundle getExtras();
+    method public android.net.Uri getHandle();
+  }
+
+  public abstract class ConnectionService extends android.telecomm.CallService {
+    ctor public ConnectionService();
+    method public final void abort(java.lang.String);
+    method public final void answer(java.lang.String);
+    method public final void call(android.telecomm.CallInfo);
+    method public final void disconnect(java.lang.String);
+    method public final void hold(java.lang.String);
+    method public final void isCompatibleWith(android.telecomm.CallInfo);
+    method public final void onAudioStateChanged(java.lang.String, android.telecomm.CallAudioState);
+    method public void onCreateConnections(android.telecomm.ConnectionRequest, android.telecomm.Response<android.telecomm.ConnectionRequest, android.telecomm.Connection>);
+    method public void onCreateIncomingConnection(android.telecomm.ConnectionRequest, android.telecomm.Response<android.telecomm.ConnectionRequest, android.telecomm.Connection>);
+    method public void onFindSubscriptions(android.net.Uri, android.telecomm.Response<android.net.Uri, android.telecomm.Subscription>);
+    method public final void playDtmfTone(java.lang.String, char);
+    method public final void reject(java.lang.String);
+    method public final void setIncomingCallId(java.lang.String, android.os.Bundle);
+    method public final void stopDtmfTone(java.lang.String);
+    method public final void unhold(java.lang.String);
+  }
+
   public class GatewayInfo implements android.os.Parcelable {
     method public int describeContents();
     method public android.net.Uri getGatewayHandle();
@@ -26789,6 +26866,18 @@
     method protected abstract void updateCall(android.telecomm.InCallCall);
   }
 
+  public abstract interface Response {
+    method public abstract void onError(IN, java.lang.String);
+    method public abstract void onResult(IN, OUT...);
+  }
+
+  public class Subscription implements android.os.Parcelable {
+    ctor public Subscription();
+    method public int describeContents();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator CREATOR;
+  }
+
   public final class TelecommConstants {
     ctor public TelecommConstants();
     field public static final java.lang.String ACTION_CALL_SERVICE;
diff --git a/telecomm/java/android/telecomm/Connection.java b/telecomm/java/android/telecomm/Connection.java
new file mode 100644
index 0000000..6b7463c
--- /dev/null
+++ b/telecomm/java/android/telecomm/Connection.java
@@ -0,0 +1,412 @@
+/*
+ * Copyright (C) 2014 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 android.telecomm;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Represents a connection to a remote endpoint that carries voice traffic.
+ */
+public abstract class Connection {
+
+    private static String TAG = Connection.class.getSimpleName();
+
+    public interface Listener {
+        void onStateChanged(Connection c, int state);
+        void onAudioStateChanged(Connection c, CallAudioState state);
+        void onHandleChanged(Connection c, Uri newHandle);
+        void onSignalChanged(Connection c, Bundle details);
+        void onDisconnected(Connection c, int cause, String message);
+        void onDestroyed(Connection c);
+    }
+
+    public static class ListenerBase implements Listener {
+        /** {@inheritDoc} */
+        @Override
+        public void onStateChanged(Connection c, int state) {}
+
+        /** {@inheritDoc} */
+         @Override
+        public void onAudioStateChanged(Connection c, CallAudioState state) {}
+
+        /** {@inheritDoc} */
+        @Override
+        public void onHandleChanged(Connection c, Uri newHandle) {}
+
+        /** {@inheritDoc} */
+        @Override
+        public void onSignalChanged(Connection c, Bundle details) {}
+
+        /** {@inheritDoc} */
+        @Override
+        public void onDisconnected(Connection c, int cause, String message) {}
+
+        /** {@inheritDoc} */
+        @Override
+        public void onDestroyed(Connection c) {}
+    }
+
+    public final class State {
+        private State() {}
+
+        public static final int NEW = 0;
+        public static final int RINGING = 1;
+        public static final int DIALING = 2;
+        public static final int ACTIVE = 3;
+        public static final int HOLDING = 4;
+        public static final int DISCONNECTED = 5;
+    }
+
+    private final Set<Listener> mListeners = new HashSet<>();
+    private int mState = State.NEW;
+    private CallAudioState mCallAudioState;
+    private Uri mHandle;
+
+    /**
+     * Create a new Connection.
+     */
+    protected Connection() {}
+
+    /**
+     * @return The handle (e.g., phone number) to which this Connection
+     *         is currently communicating.
+     */
+    public final Uri getHandle() {
+        return mHandle;
+    }
+
+    /**
+     * @return The state of this Connection.
+     *
+     * @hide
+     */
+    public final int getState() {
+        return mState;
+    }
+
+    /**
+     * @return The audio state of the call, describing how its audio is currently
+     *         being routed by the system. This is {@code null} if this Connection
+     *         does not directly know about its audio state.
+     */
+    public final CallAudioState getCallAudioState() {
+        return mCallAudioState;
+    }
+
+    /**
+     * Assign a listener to be notified of state changes.
+     *
+     * @param l A listener.
+     * @return This Connection.
+     *
+     * @hide
+     */
+    public final Connection addConnectionListener(Listener l) {
+        mListeners.add(l);
+        return this;
+    }
+
+    /**
+     * Remove a previously assigned listener that was being notified of state changes.
+     *
+     * @param l A Listener.
+     * @return This Connection.
+     *
+     * @hide
+     */
+    public final Connection removeConnectionListener(Listener l) {
+        mListeners.remove(l);
+        return this;
+    }
+
+    /**
+     * Play a DTMF tone in this Connection.
+     *
+     * @param c A DTMF character.
+     *
+     * @hide
+     */
+    public final void playDtmfTone(char c) {
+        Log.d(TAG, "playDtmfTone " + c);
+        onPlayDtmfTone(c);
+    }
+
+    /**
+     * Stop any DTMF tones which may be playing in this Connection.
+     *
+     * @hide
+     */
+    public final void stopDtmfTone() {
+        Log.d(TAG, "stopDtmfTone");
+        onStopDtmfTone();
+    }
+
+    /**
+     * Disconnect this Connection. If and when the Connection can comply with
+     * this request, it will transition to the {@link State#DISCONNECTED}
+     * state and notify its listeners.
+     *
+     * @hide
+     */
+    public final void disconnect() {
+        Log.d(TAG, "disconnect");
+        onDisconnect();
+    }
+
+    /**
+     * Abort this Connection. The Connection will immediately transition to
+     * the {@link State#DISCONNECTED} state, and send no notifications of this
+     * or any other future events.
+     *
+     * @hide
+     */
+    public final void abort() {
+        Log.d(TAG, "abort");
+        onAbort();
+    }
+
+    /**
+     * Place this Connection on hold. If and when the Connection can comply with
+     * this request, it will transition to the {@link State#HOLDING}
+     * state and notify its listeners.
+     *
+     * @hide
+     */
+    public final void hold() {
+        Log.d(TAG, "hold");
+        onHold();
+    }
+
+    /**
+     * Un-hold this Connection. If and when the Connection can comply with
+     * this request, it will transition to the {@link State#ACTIVE}
+     * state and notify its listeners.
+     *
+     * @hide
+     */
+    public final void unhold() {
+        Log.d(TAG, "unhold");
+        onUnhold();
+    }
+
+    /**
+     * Accept a {@link State#RINGING} Connection. If and when the Connection
+     * can comply with this request, it will transition to the {@link State#ACTIVE}
+     * state and notify its listeners.
+     *
+     * @hide
+     */
+    public final void answer() {
+        Log.d(TAG, "answer");
+        if (mState == State.RINGING) {
+            onAnswer();
+        }
+    }
+
+    /**
+     * Reject a {@link State#RINGING} Connection. If and when the Connection
+     * can comply with this request, it will transition to the {@link State#ACTIVE}
+     * state and notify its listeners.
+     *
+     * @hide
+     */
+    public final void reject() {
+        Log.d(TAG, "reject");
+        if (mState == State.RINGING) {
+            onReject();
+        }
+    }
+
+    /**
+     * Inform this Connection that the state of its audio output has been changed externally.
+     *
+     * @param state The new audio state.
+     */
+    public void setAudioState(CallAudioState state) {
+        Log.d(TAG, "setAudioState " + state);
+        onSetAudioState(state);
+    }
+
+    /**
+     * @param state An integer value from {@link State}.
+     * @return A string representation of the value.
+     */
+    public static String stateToString(int state) {
+        switch (state) {
+            case State.NEW:
+                return "NEW";
+            case State.RINGING:
+                return "RINGING";
+            case State.DIALING:
+                return "DIALING";
+            case State.ACTIVE:
+                return "ACTIVE";
+            case State.HOLDING:
+                return "HOLDING";
+            case State.DISCONNECTED:
+                return "DISCONNECTED";
+            default:
+                Log.wtf(TAG, "Unknown state " + state);
+                return "UNKNOWN";
+        }
+    }
+
+    /**
+     * Sets the value of the {@link #getHandle()} property and notifies listeners.
+     *
+     * @param handle The new handle.
+     */
+    protected void setHandle(Uri handle) {
+        Log.d(TAG, "setHandle " + handle);
+        // TODO: Enforce super called
+        mHandle = handle;
+        for (Listener l : mListeners) {
+            l.onHandleChanged(this, handle);
+        }
+    }
+
+    /**
+     * Sets state to active (e.g., an ongoing call where two or more parties can actively
+     * communicate).
+     */
+    protected void setActive() {
+        setState(State.ACTIVE);
+    }
+
+    /**
+     * Sets state to ringing (e.g., an inbound ringing call).
+     */
+    protected void setRinging() {
+        setState(State.RINGING);
+    }
+
+    /**
+     * Sets state to dialing (e.g., dialing an outbound call).
+     */
+    protected void setDialing() {
+        setState(State.DIALING);
+    }
+
+    /**
+     * Sets state to be on hold.
+     */
+    protected void setOnHold() {
+        setState(State.HOLDING);
+    }
+
+    /**
+     * Sets state to disconnected. This will first notify listeners with an
+     * {@link Listener#onStateChanged(Connection, int)} event, then will fire an
+     * {@link Listener#onDisconnected(Connection, int, String)} event with additional
+     * details.
+     *
+     * @param cause The reason for the disconnection, any of
+     *         {@link android.telephony.DisconnectCause}.
+     * @param message Optional call-service-provided message about the disconnect.
+     */
+    protected void setDisconnected(int cause, String message) {
+        setState(State.DISCONNECTED);
+        Log.d(TAG, "Disconnected with cause " + cause + " message " + message);
+        for (Listener l : mListeners) {
+            l.onDisconnected(this, cause, message);
+        }
+    }
+
+    /**
+     * Notifies this Connection and listeners that the {@link #getCallAudioState()} property
+     * has a new value.
+     *
+     * @param state The new call audio state.
+     */
+    protected void onSetAudioState(CallAudioState state) {
+        // TODO: Enforce super called
+        this.mCallAudioState = state;
+        for (Listener l : mListeners) {
+            l.onAudioStateChanged(this, state);
+        }
+    }
+
+    /**
+     * Notifies this Connection and listeners of a change in the current signal levels
+     * for the underlying data transport.
+     *
+     * @param details A {@link android.os.Bundle} containing details of the current level.
+     */
+    protected void onSetSignal(Bundle details) {
+        // TODO: Enforce super called
+        for (Listener l : mListeners) {
+            l.onSignalChanged(this, details);
+        }
+    }
+
+    /**
+     * Notifies this Connection of a request to play a DTMF tone.
+     *
+     * @param c A DTMF character.
+     */
+    protected void onPlayDtmfTone(char c) {}
+
+    /**
+     * Notifies this Connection of a request to stop any currently playing DTMF tones.
+     */
+    protected void onStopDtmfTone() {}
+
+    /**
+     * Notifies this Connection of a request to disconnect.
+     */
+    protected void onDisconnect() {}
+
+    /**
+     * Notifies this Connection of a request to abort.
+     */
+    protected void onAbort() {}
+
+    /**
+     * Notifies this Connection of a request to hold.
+     */
+    protected void onHold() {}
+
+    /**
+     * Notifies this Connection of a request to exit a hold state.
+     */
+    protected void onUnhold() {}
+
+    /**
+     * Notifies this Connection, which is in {@link State#RINGING}, of
+     * a request to accept.
+     */
+    protected void onAnswer() {}
+
+    /**
+     * Notifies this Connection, which is in {@link State#RINGING}, of
+     * a request to reject.
+     */
+    protected void onReject() {}
+
+    private void setState(int state) {
+        Log.d(TAG, "setState: " + stateToString(state));
+        this.mState = state;
+        for (Listener l : mListeners) {
+            l.onStateChanged(this, state);
+        }
+    }
+}
diff --git a/telecomm/java/android/telecomm/ConnectionRequest.java b/telecomm/java/android/telecomm/ConnectionRequest.java
new file mode 100644
index 0000000..c1f1871
--- /dev/null
+++ b/telecomm/java/android/telecomm/ConnectionRequest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2014 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 android.telecomm;
+
+import android.os.Bundle;
+import android.net.Uri;
+
+/**
+ * Simple data container encapsulating a request to some entity to
+ * create a new {@link Connection}.
+ */
+public final class ConnectionRequest {
+
+    // TODO: Token to limit recursive invocations
+    // TODO: Consider upgrading "mHandle" to ordered list of handles, indicating a set of phone
+    //         numbers that would satisfy the client's needs, in order of preference
+    private final Uri mHandle;
+    private final Bundle mExtras;
+
+    public ConnectionRequest(Uri handle, Bundle extras) {
+        mHandle = handle; mExtras = extras;
+    }
+
+    /**
+     * The handle (e.g., phone number) to which the {@link Connection} is to connect.
+     */
+    public Uri getHandle() { return mHandle; }
+
+    /**
+     * Application-specific extra data. Used for passing back information from an incoming
+     * call {@code Intent}, and for any proprietary extensions arranged between a client
+     * and servant {@code ConnectionService} which agree on a vocabulary for such data.
+     */
+    public Bundle getExtras() { return mExtras; }
+
+    public String toString() {
+        return String.format("PhoneConnectionRequest %s %s",
+                mHandle == null
+                        ? Uri.EMPTY
+                        : ConnectionService.toLogSafePhoneNumber(mHandle.toString()),
+                mExtras == null ? "" : mExtras);
+    }
+}
diff --git a/telecomm/java/android/telecomm/ConnectionService.java b/telecomm/java/android/telecomm/ConnectionService.java
new file mode 100644
index 0000000..aba4579
--- /dev/null
+++ b/telecomm/java/android/telecomm/ConnectionService.java
@@ -0,0 +1,345 @@
+/*
+ * Copyright (C) 2014 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 android.telecomm;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A {@link android.app.Service} that provides telephone connections to
+ * processes running on an Android device.
+ */
+public abstract class ConnectionService extends CallService {
+    private static final String TAG = ConnectionService.class.getSimpleName();
+
+    // STOPSHIP: Debug Logging should be conditional on a debug flag or use a set of
+    // logging functions that make it automaticaly so.
+
+    // Flag controlling whether PII is emitted into the logs
+    private static final boolean PII_DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    private static final Connection NULL_CONNECTION = new Connection() {};
+
+    // Mappings from Connections to IDs as understood by the current CallService implementation
+    private final Map<String, Connection> mConnectionById = new HashMap<>();
+    private final Map<Connection, String> mIdByConnection = new HashMap<>();
+
+    private final Connection.Listener mConnectionListener = new Connection.Listener() {
+        @Override
+        public void onStateChanged(Connection c, int state) {
+            String id = mIdByConnection.get(c);
+            Log.d(TAG, "Adapter set state " + id + " " + Connection.stateToString(state));
+            switch (state) {
+                case Connection.State.ACTIVE:
+                    getAdapter().setActive(id);
+                    break;
+                case Connection.State.DIALING:
+                    getAdapter().setDialing(id);
+                    break;
+                case Connection.State.DISCONNECTED:
+                    // Handled in onDisconnected()
+                    break;
+                case Connection.State.HOLDING:
+                    getAdapter().setOnHold(id);
+                    break;
+                case Connection.State.NEW:
+                    // Nothing to tell Telecomm
+                    break;
+                case Connection.State.RINGING:
+                    getAdapter().setRinging(id);
+                    break;
+            }
+        }
+
+        @Override
+        public void onDisconnected(Connection c, int cause, String message) {
+            String id = mIdByConnection.get(c);
+            Log.d(TAG, "Adapter set disconnected " + cause + " " + message);
+            getAdapter().setDisconnected(id, cause, message);
+        }
+
+        @Override
+        public void onHandleChanged(Connection c, Uri newHandle) {
+            // TODO: Unsupported yet
+        }
+
+        @Override
+        public void onAudioStateChanged(Connection c, CallAudioState state) {
+            // TODO: Unsupported yet
+        }
+
+        @Override
+        public void onSignalChanged(Connection c, Bundle details) {
+            // TODO: Unsupported yet
+        }
+
+        @Override
+        public void onDestroyed(Connection c) {
+            removeConnection(c);
+        }
+    };
+
+    @Override
+    public final void isCompatibleWith(final CallInfo callInfo) {
+        Log.d(TAG, "isCompatibleWith " + callInfo);
+        onFindSubscriptions(
+                callInfo.getHandle(),
+                new Response<Uri, Subscription>() {
+                    @Override
+                    public void onResult(Uri handle, Subscription... result) {
+                        boolean isCompatible = result.length > 0;
+                        Log.d(TAG, "adapter setIsCompatibleWith "
+                                + callInfo.getId() + " " + isCompatible);
+                        getAdapter().setIsCompatibleWith(callInfo.getId(), isCompatible);
+                    }
+
+                    @Override
+                    public void onError(Uri handle, String reason) {
+                        Log.wtf(TAG, "Error in onFindSubscriptions " + callInfo.getHandle()
+                                + " error: " + reason);
+                        getAdapter().setIsCompatibleWith(callInfo.getId(), false);
+                    }
+                }
+        );
+    }
+
+    @Override
+    public final void call(final CallInfo callInfo) {
+        Log.d(TAG, "call " + callInfo);
+        onCreateConnections(
+                new ConnectionRequest(
+                        callInfo.getHandle(),
+                        callInfo.getExtras()),
+                new Response<ConnectionRequest, Connection>() {
+                    @Override
+                    public void onResult(ConnectionRequest request, Connection... result) {
+                        if (result.length != 1) {
+                            Log.d(TAG, "adapter handleFailedOutgoingCall " + callInfo);
+                            getAdapter().handleFailedOutgoingCall(
+                                    callInfo.getId(),
+                                    "Created " + result.length + " Connections, expected 1");
+                            for (Connection c : result) {
+                                c.abort();
+                            }
+                        } else {
+                            addConnection(callInfo.getId(), result[0]);
+                            Log.d(TAG, "adapter handleSuccessfulOutgoingCall "
+                                    + callInfo.getId());
+                            getAdapter().handleSuccessfulOutgoingCall(callInfo.getId());
+                        }
+                    }
+
+                    @Override
+                    public void onError(ConnectionRequest request, String reason) {
+                        getAdapter().handleFailedOutgoingCall(callInfo.getId(), reason);
+                    }
+                }
+        );
+    }
+
+    @Override
+    public final void abort(String callId) {
+        Log.d(TAG, "abort " + callId);
+        findConnectionForAction(callId, "abort").abort();
+    }
+
+    @Override
+    public final void setIncomingCallId(final String callId, Bundle extras) {
+        Log.d(TAG, "setIncomingCallId " + callId + " " + extras);
+        onCreateIncomingConnection(
+                new ConnectionRequest(
+                        null,  // TODO: Can we obtain this from "extras"?
+                        extras),
+                new Response<ConnectionRequest, Connection>() {
+                    @Override
+                    public void onResult(ConnectionRequest request, Connection... result) {
+                        if (result.length != 1) {
+                            Log.d(TAG, "adapter handleFailedOutgoingCall " + callId);
+                            getAdapter().handleFailedOutgoingCall(
+                                    callId,
+                                    "Created " + result.length + " Connections, expected 1");
+                            for (Connection c : result) {
+                                c.abort();
+                            }
+                        } else {
+                            addConnection(callId, result[0]);
+                            Log.d(TAG, "adapter notifyIncomingCall " + callId);
+                            // TODO: Uri.EMPTY is because CallInfo crashes when Parceled with a
+                            // null URI ... need to fix that at its cause!
+                            getAdapter().notifyIncomingCall(new CallInfo(
+                                    callId,
+                                    connectionStateToCallState(result[0].getState()),
+                                    request.getHandle() /* result[0].getHandle() == null
+                                            ? Uri.EMPTY : result[0].getHandle() */));
+                        }
+                    }
+
+                    @Override
+                    public void onError(ConnectionRequest request, String reason) {
+                        Log.d(TAG, "adapter failed setIncomingCallId " + request + " " + reason);
+                    }
+                }
+        );
+    }
+
+    @Override
+    public final void answer(String callId) {
+        Log.d(TAG, "answer " + callId);
+        findConnectionForAction(callId, "answer").answer();
+    }
+
+    @Override
+    public final void reject(String callId) {
+        Log.d(TAG, "reject " + callId);
+        findConnectionForAction(callId, "reject").reject();
+    }
+
+    @Override
+    public final void disconnect(String callId) {
+        Log.d(TAG, "disconnect " + callId);
+        findConnectionForAction(callId, "disconnect").disconnect();
+    }
+
+    @Override
+    public final void hold(String callId) {
+        Log.d(TAG, "hold " + callId);
+        findConnectionForAction(callId, "hold").hold();
+    }
+
+    @Override
+    public final void unhold(String callId) {
+        Log.d(TAG, "unhold " + callId);
+        findConnectionForAction(callId, "unhold").unhold();
+    }
+
+    @Override
+    public final void playDtmfTone(String callId, char digit) {
+        Log.d(TAG, "playDtmfTone " + callId + " " + Character.toString(digit));
+        findConnectionForAction(callId, "playDtmfTone").playDtmfTone(digit);
+    }
+
+    @Override
+    public final void stopDtmfTone(String callId) {
+        Log.d(TAG, "stopDtmfTone " + callId);
+        findConnectionForAction(callId, "stopDtmfTone").stopDtmfTone();
+    }
+
+    @Override
+    public final void onAudioStateChanged(String callId, CallAudioState audioState) {
+        Log.d(TAG, "onAudioStateChanged " + callId + " " + audioState);
+        findConnectionForAction(callId, "onAudioStateChanged").setAudioState(audioState);
+    }
+
+    /**
+     * Find a set of Subscriptions matching a given handle (e.g. phone number).
+     *
+     * @param handle A handle (e.g. phone number) with which to connect.
+     * @param callback A callback for providing the result.
+     */
+    public void onFindSubscriptions(
+            Uri handle,
+            Response<Uri, Subscription> callback) {}
+
+    /**
+     * Create a Connection given a request.
+     *
+     * @param request Data encapsulating details of the desired Connection.
+     * @param callback A callback for providing the result.
+     */
+    public void onCreateConnections(
+            ConnectionRequest request,
+            Response<ConnectionRequest, Connection> callback) {}
+
+    /**
+     * Create a Connection to match an incoming connection notification.
+     *
+     * @param request Data encapsulating details of the desired Connection.
+     * @param callback A callback for providing the result.
+     */
+    public void onCreateIncomingConnection(
+            ConnectionRequest request,
+            Response<ConnectionRequest, Connection> callback) {}
+
+    static String toLogSafePhoneNumber(String number) {
+        // For unknown number, log empty string.
+        if (number == null) {
+            return "";
+        }
+
+        if (PII_DEBUG) {
+            // When PII_DEBUG is true we emit PII.
+            return number;
+        }
+
+        // Do exactly same thing as Uri#toSafeString() does, which will enable us to compare
+        // sanitized phone numbers.
+        StringBuilder builder = new StringBuilder();
+        for (int i = 0; i < number.length(); i++) {
+            char c = number.charAt(i);
+            if (c == '-' || c == '@' || c == '.') {
+                builder.append(c);
+            } else {
+                builder.append('x');
+            }
+        }
+        return builder.toString();
+    }
+
+    private CallState connectionStateToCallState(int connectionState) {
+        switch (connectionState) {
+            case Connection.State.NEW:
+                return CallState.NEW;
+            case Connection.State.RINGING:
+                return CallState.RINGING;
+            case Connection.State.DIALING:
+                return CallState.DIALING;
+            case Connection.State.ACTIVE:
+                return CallState.ACTIVE;
+            case Connection.State.HOLDING:
+                return CallState.ON_HOLD;
+            case Connection.State.DISCONNECTED:
+                return CallState.DISCONNECTED;
+            default:
+                Log.wtf(TAG, "Unknown Connection.State " + connectionState);
+                return CallState.NEW;
+        }
+    }
+
+    private void addConnection(String callId, Connection connection) {
+        mConnectionById.put(callId, connection);
+        mIdByConnection.put(connection, callId);
+        connection.addConnectionListener(mConnectionListener);
+    }
+
+    private void removeConnection(Connection connection) {
+        connection.removeConnectionListener(mConnectionListener);
+        mConnectionById.remove(mIdByConnection.get(connection));
+        mIdByConnection.remove(connection);
+    }
+
+    private Connection findConnectionForAction(String callId, String action) {
+        if (mConnectionById.containsKey(callId)) {
+            return mConnectionById.get(callId);
+        }
+        Log.wtf(TAG, action + " - Cannot find Connection \"" + callId + "\"");
+        return NULL_CONNECTION;
+    }
+}
\ No newline at end of file
diff --git a/telecomm/java/android/telecomm/Response.java b/telecomm/java/android/telecomm/Response.java
new file mode 100644
index 0000000..14f8340
--- /dev/null
+++ b/telecomm/java/android/telecomm/Response.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2014 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 android.telecomm;
+
+/**
+ * Used to inform a client of asynchronously returned results.
+ */
+public interface Response<IN, OUT> {
+
+    /**
+     * Provide a set of results.
+     *
+     * @param request The original request.
+     * @param result The results.
+     */
+    void onResult(IN request, OUT... result);
+
+    /**
+     * Indicates the inability to provide results.
+     *
+     * @param request The original request.
+     * @param reason The reason for the failure.
+     */
+    void onError(IN request, String reason);
+}
diff --git a/telecomm/java/android/telecomm/Subscription.java b/telecomm/java/android/telecomm/Subscription.java
new file mode 100644
index 0000000..f187f4d
--- /dev/null
+++ b/telecomm/java/android/telecomm/Subscription.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2014 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 android.telecomm;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Represents a distinct subscription, line of service or call placement method that
+ * a {@link ConnectionService} can use to place phone calls.
+ */
+public class Subscription implements Parcelable {
+
+    public Subscription() {}
+
+    public int describeContents() {
+        return 0;
+    }
+
+    public void writeToParcel(Parcel out, int flags) {}
+
+    public static final Parcelable.Creator<Subscription> CREATOR
+            = new Parcelable.Creator<Subscription>() {
+        public Subscription createFromParcel(Parcel in) {
+            return new Subscription(in);
+        }
+
+        public Subscription[] newArray(int size) {
+            return new Subscription[size];
+        }
+    };
+
+    private Subscription(Parcel in) {}
+}