Self Managed CS implementation.

Implementation for self-managed connection service APIs.

Add ability for self-managed CS to:
- register a phone account and have it be auto-enabled.
- add ability for self-managed calls, ensuring only self-manage calls
can be added.

Add implementations for new isXCallPermitted APIs.
Filter self-managed calls from InCallServices.
Ensure self managed connection creation doesn't use emergency call CS.
Add ability to set audio route from self-managed connection
Overhaul some of the CallsManager getCallsWithState methods to support
other use cases for self-managed calls (also use steams/filters.. Ooo aah)

Test: Manual
Bug: 34159263
Change-Id: I3131fd48ee5c5aa36c0e88992fa51879af07d495
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index de4931f..fb358aa 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -51,6 +51,7 @@
 import android.telephony.PhoneNumberUtils;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
+import android.util.ArraySet;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.telephony.AsyncEmergencyContactNotifier;
@@ -68,6 +69,7 @@
 import com.android.server.telecom.components.ErrorDialogActivity;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -79,6 +81,9 @@
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
 
 /**
  * Singleton.
@@ -122,6 +127,7 @@
     private static final int MAXIMUM_DIALING_CALLS = 1;
     private static final int MAXIMUM_OUTGOING_CALLS = 1;
     private static final int MAXIMUM_TOP_LEVEL_CALLS = 2;
+    private static final int MAXIMUM_SELF_MANAGED_CALLS = 10;
 
     private static final int[] OUTGOING_CALL_STATES =
             {CallState.CONNECTING, CallState.SELECT_PHONE_ACCOUNT, CallState.DIALING,
@@ -131,6 +137,11 @@
             {CallState.CONNECTING, CallState.SELECT_PHONE_ACCOUNT, CallState.DIALING,
                     CallState.PULLING, CallState.ACTIVE};
 
+    private static final int[] ANY_CALL_STATE =
+            {CallState.NEW, CallState.CONNECTING, CallState.SELECT_PHONE_ACCOUNT, CallState.DIALING,
+                    CallState.RINGING, CallState.ACTIVE, CallState.ON_HOLD, CallState.DISCONNECTED,
+                    CallState.ABORTED, CallState.DISCONNECTING, CallState.PULLING};
+
     public static final String TELECOM_CALL_ID_PREFIX = "TC@";
 
     // Maps call technologies in PhoneConstants to those in Analytics.
@@ -391,7 +402,7 @@
         }
 
         if (result.shouldAllowCall) {
-            if (hasMaximumRingingCalls()) {
+            if (hasMaximumManagedRingingCalls(incomingCall)) {
                 if (shouldSilenceInsteadOfReject(incomingCall)) {
                     incomingCall.silence();
                 } else {
@@ -399,7 +410,7 @@
                             "Exceeds maximum number of ringing calls.");
                     rejectCallAndLog(incomingCall);
                 }
-            } else if (hasMaximumDialingCalls()) {
+            } else if (hasMaximumManagedDialingCalls(incomingCall)) {
                 Log.i(this, "onCallFilteringCompleted: Call rejected! Exceeds maximum number of " +
                         "dialing calls.");
                 rejectCallAndLog(incomingCall);
@@ -758,7 +769,12 @@
         setIntentExtrasAndStartTime(call, extras);
         // TODO: Move this to be a part of addCall()
         call.addListener(this);
-        call.startCreateConnection(mPhoneAccountRegistrar);
+
+        if (call.isSelfManaged() && !isIncomingCallPermitted(call, call.getTargetPhoneAccount())) {
+            notifyCreateConnectionFailed(phoneAccountHandle, call);
+        } else {
+            call.startCreateConnection(mPhoneAccountRegistrar);
+        }
     }
 
     void addNewUnknownCall(PhoneAccountHandle phoneAccountHandle, Bundle extras) {
@@ -824,7 +840,11 @@
     }
 
     /**
-     * Kicks off the first steps to creating an outgoing call so that InCallUI can launch.
+     * Kicks off the first steps to creating an outgoing call.
+     *
+     * For managed connections, this is the first step to launching the Incall UI.
+     * For self-managed connections, we don't expect the Incall UI to launch, but this is still a
+     * first step in getting the self-managed ConnectionService to create the connection.
      *
      * @param handle Handle to connect the call with.
      * @param phoneAccountHandle The phone account which contains the component name of the
@@ -904,45 +924,53 @@
             call.setVideoState(videoState);
         }
 
-        List<PhoneAccountHandle> accounts = constructPossiblePhoneAccounts(handle, initiatingUser);
-        Log.v(this, "startOutgoingCall found accounts = " + accounts);
+        PhoneAccount targetPhoneAccount = mPhoneAccountRegistrar.getPhoneAccount(
+                phoneAccountHandle, initiatingUser);
+        boolean isSelfManaged = targetPhoneAccount != null && targetPhoneAccount.isSelfManaged();
 
-        // Only dial with the requested phoneAccount if it is still valid. Otherwise treat this call
-        // as if a phoneAccount was not specified (does the default behavior instead).
-        // Note: We will not attempt to dial with a requested phoneAccount if it is disabled.
-        if (phoneAccountHandle != null) {
-            if (!accounts.contains(phoneAccountHandle)) {
-                phoneAccountHandle = null;
-            }
-        }
+        List<PhoneAccountHandle> accounts;
+        if (!isSelfManaged) {
+            accounts = constructPossiblePhoneAccounts(handle, initiatingUser);
+            Log.v(this, "startOutgoingCall found accounts = " + accounts);
 
-        if (phoneAccountHandle == null && accounts.size() > 0) {
-            // No preset account, check if default exists that supports the URI scheme for the
-            // handle and verify it can be used.
-            if(accounts.size() > 1) {
-                PhoneAccountHandle defaultPhoneAccountHandle =
-                        mPhoneAccountRegistrar.getOutgoingPhoneAccountForScheme(handle.getScheme(),
-                                initiatingUser);
-                if (defaultPhoneAccountHandle != null &&
-                        accounts.contains(defaultPhoneAccountHandle)) {
-                    phoneAccountHandle = defaultPhoneAccountHandle;
+            // Only dial with the requested phoneAccount if it is still valid. Otherwise treat this
+            // call as if a phoneAccount was not specified (does the default behavior instead).
+            // Note: We will not attempt to dial with a requested phoneAccount if it is disabled.
+            if (phoneAccountHandle != null) {
+                if (!accounts.contains(phoneAccountHandle)) {
+                    phoneAccountHandle = null;
                 }
-            } else {
-                // Use the only PhoneAccount that is available
-                phoneAccountHandle = accounts.get(0);
             }
 
+            if (phoneAccountHandle == null && accounts.size() > 0) {
+                // No preset account, check if default exists that supports the URI scheme for the
+                // handle and verify it can be used.
+                if (accounts.size() > 1) {
+                    PhoneAccountHandle defaultPhoneAccountHandle =
+                            mPhoneAccountRegistrar.getOutgoingPhoneAccountForScheme(
+                                    handle.getScheme(), initiatingUser);
+                    if (defaultPhoneAccountHandle != null &&
+                            accounts.contains(defaultPhoneAccountHandle)) {
+                        phoneAccountHandle = defaultPhoneAccountHandle;
+                    }
+                } else {
+                    // Use the only PhoneAccount that is available
+                    phoneAccountHandle = accounts.get(0);
+                }
+            }
+        } else {
+            accounts = Collections.EMPTY_LIST;
         }
 
         call.setTargetPhoneAccount(phoneAccountHandle);
 
-        boolean isPotentialInCallMMICode = isPotentialInCallMMICode(handle);
+        boolean isPotentialInCallMMICode = isPotentialInCallMMICode(handle) && !isSelfManaged;
 
         // Do not support any more live calls.  Our options are to move a call to hold, disconnect
         // a call, or cancel this call altogether. If a call is being reused, then it has already
         // passed the makeRoomForOutgoingCall check once and will fail the second time due to the
         // call transitioning into the CONNECTING state.
-        if (!isPotentialInCallMMICode && (!isReusedCall &&
+        if (!isSelfManaged && !isPotentialInCallMMICode && (!isReusedCall &&
                 !makeRoomForOutgoingCall(call, call.isEmergencyCall()))) {
             // just cancel at this point.
             Log.i(this, "No remaining room for outgoing call: %s", call);
@@ -955,7 +983,7 @@
         }
 
         boolean needsAccountSelection = phoneAccountHandle == null && accounts.size() > 1 &&
-                !call.isEmergencyCall();
+                !call.isEmergencyCall() && !isSelfManaged;
 
         if (needsAccountSelection) {
             // This is the state where the user is expected to select an account
@@ -1042,7 +1070,13 @@
         if (call.getTargetPhoneAccount() != null || call.isEmergencyCall()) {
             // If the account has been set, proceed to place the outgoing call.
             // Otherwise the connection will be initiated when the account is set by the user.
-            call.startCreateConnection(mPhoneAccountRegistrar);
+            if (call.isSelfManaged() && !isOutgoingCallPermitted(call,
+                    call.getTargetPhoneAccount())) {
+
+                notifyCreateConnectionFailed(call.getTargetPhoneAccount(), call);
+            } else {
+                call.startCreateConnection(mPhoneAccountRegistrar);
+            }
         } else if (mPhoneAccountRegistrar.getCallCapablePhoneAccounts(
                 requireCallCapableAccountByHandle ? call.getHandle().getScheme() : null, false,
                 call.getInitiatingUser()).isEmpty()) {
@@ -1948,42 +1982,92 @@
         return false;
     }
 
-    private int getNumCallsWithState(int... states) {
-        int count = 0;
-        for (int state : states) {
-            for (Call call : mCalls) {
-                if (call.getParentCall() == null && call.getState() == state &&
-                        !call.isExternalCall()) {
+    @VisibleForTesting
+    public int getNumCallsWithState(final boolean isSelfManaged, Call excludeCall,
+                                     PhoneAccountHandle phoneAccountHandle, int... states) {
 
-                    count++;
-                }
-            }
+        Set<Integer> desiredStates = IntStream.of(states).boxed().collect(Collectors.toSet());
+
+        Stream<Call> callsStream = mCalls.stream()
+                .filter(call -> desiredStates.contains(call.getState()) &&
+                        call.getParentCall() == null && !call.isExternalCall() &&
+                        call.isSelfManaged() == isSelfManaged);
+
+        // If a call to exclude was specifeid, filter it out.
+        if (excludeCall != null) {
+            callsStream = callsStream.filter(call -> call != excludeCall);
         }
-        return count;
+
+        // If a phone account handle was specified, only consider calls for that phone account.
+        if (phoneAccountHandle != null) {
+            callsStream = callsStream.filter(
+                    call -> phoneAccountHandle.equals(call.getTargetPhoneAccount()));
+        }
+
+        return (int) callsStream.count();
     }
 
-    private boolean hasMaximumLiveCalls() {
-        return MAXIMUM_LIVE_CALLS <= getNumCallsWithState(LIVE_CALL_STATES);
+    private boolean hasMaximumManagedLiveCalls(Call exceptCall) {
+        return MAXIMUM_LIVE_CALLS <= getNumCallsWithState(false /* isSelfManaged */,
+                exceptCall, null /* phoneAccountHandle */, LIVE_CALL_STATES);
     }
 
-    private boolean hasMaximumHoldingCalls() {
-        return MAXIMUM_HOLD_CALLS <= getNumCallsWithState(CallState.ON_HOLD);
+    private boolean hasMaximumSelfManagedCalls(Call exceptCall,
+                                                   PhoneAccountHandle phoneAccountHandle) {
+        return MAXIMUM_SELF_MANAGED_CALLS <= getNumCallsWithState(true /* isSelfManaged */,
+                exceptCall, phoneAccountHandle, ANY_CALL_STATE);
     }
 
-    private boolean hasMaximumRingingCalls() {
-        return MAXIMUM_RINGING_CALLS <= getNumCallsWithState(CallState.RINGING);
+    private boolean hasMaximumManagedHoldingCalls(Call exceptCall) {
+        return MAXIMUM_HOLD_CALLS <= getNumCallsWithState(false /* isSelfManaged */, exceptCall,
+                null /* phoneAccountHandle */, CallState.ON_HOLD);
     }
 
-    private boolean hasMaximumOutgoingCalls() {
-        return MAXIMUM_OUTGOING_CALLS <= getNumCallsWithState(OUTGOING_CALL_STATES);
+    private boolean hasMaximumManagedRingingCalls(Call exceptCall) {
+        return MAXIMUM_RINGING_CALLS <= getNumCallsWithState(false /* isSelfManaged */, exceptCall,
+                null /* phoneAccountHandle */, CallState.RINGING);
     }
 
-    private boolean hasMaximumDialingCalls() {
-        return MAXIMUM_DIALING_CALLS <= getNumCallsWithState(CallState.DIALING, CallState.PULLING);
+    private boolean hasMaximumSelfManagedRingingCalls(Call exceptCall,
+                                                      PhoneAccountHandle phoneAccountHandle) {
+        return MAXIMUM_RINGING_CALLS <= getNumCallsWithState(true /* isSelfManaged */, exceptCall,
+                phoneAccountHandle, CallState.RINGING);
+    }
+
+    private boolean hasMaximumManagedOutgoingCalls(Call exceptCall) {
+        return MAXIMUM_OUTGOING_CALLS <= getNumCallsWithState(false /* isSelfManaged */, exceptCall,
+                null /* phoneAccountHandle */, OUTGOING_CALL_STATES);
+    }
+
+    private boolean hasMaximumManagedDialingCalls(Call exceptCall) {
+        return MAXIMUM_DIALING_CALLS <= getNumCallsWithState(false /* isSelfManaged */, exceptCall,
+                null /* phoneAccountHandle */, CallState.DIALING, CallState.PULLING);
+    }
+
+    /**
+     * Given a {@link PhoneAccountHandle} determines if there are calls owned by any other
+     * {@link PhoneAccountHandle}.
+     * @param phoneAccountHandle The {@link PhoneAccountHandle} to check.
+     * @return {@code true} if there are other calls, {@code false} otherwise.
+     */
+    public boolean hasCallsForOtherPhoneAccount(PhoneAccountHandle phoneAccountHandle) {
+        return mCalls.stream().filter(call ->
+                !phoneAccountHandle.equals(call.getTargetPhoneAccount()) &&
+                call.getParentCall() == null &&
+                !call.isExternalCall()).count() > 0;
+    }
+
+    /**
+     * Given a {@link PhoneAccountHandle} determines if there are and managed calls.
+     * @return {@code true} if there are managed calls, {@code false} otherwise.
+     */
+    public boolean hasManagedCalls() {
+        return mCalls.stream().filter(call -> !call.isSelfManaged() &&
+                !call.isExternalCall()).count() > 0;
     }
 
     private boolean makeRoomForOutgoingCall(Call call, boolean isEmergency) {
-        if (hasMaximumLiveCalls()) {
+        if (hasMaximumManagedLiveCalls(call)) {
             // NOTE: If the amount of live calls changes beyond 1, this logic will probably
             // have to change.
             Call liveCall = getFirstCallWithState(LIVE_CALL_STATES);
@@ -1997,7 +2081,7 @@
                 return true;
             }
 
-            if (hasMaximumOutgoingCalls()) {
+            if (hasMaximumManagedOutgoingCalls(call)) {
                 Call outgoingCall = getFirstCallWithState(OUTGOING_CALL_STATES);
                 if (isEmergency && !outgoingCall.isEmergencyCall()) {
                     // Disconnect the current outgoing call if it's not an emergency call. If the
@@ -2019,7 +2103,7 @@
                 return false;
             }
 
-            if (hasMaximumHoldingCalls()) {
+            if (hasMaximumManagedHoldingCalls(call)) {
                 // There is no more room for any more calls, unless it's an emergency.
                 if (isEmergency) {
                     // Kill the current active call, this is easier then trying to disconnect a
@@ -2231,6 +2315,61 @@
                 new MissedCallNotifier.CallInfoFactory());
     }
 
+    public boolean isIncomingCallPermitted(PhoneAccountHandle phoneAccountHandle) {
+        return isIncomingCallPermitted(null /* excludeCall */, phoneAccountHandle);
+    }
+
+    public boolean isIncomingCallPermitted(Call excludeCall,
+                                           PhoneAccountHandle phoneAccountHandle) {
+        if (phoneAccountHandle == null) {
+            return false;
+        }
+        PhoneAccount phoneAccount =
+                mPhoneAccountRegistrar.getPhoneAccountUnchecked(phoneAccountHandle);
+        if (phoneAccount == null) {
+            return false;
+        }
+
+        if (!phoneAccount.isSelfManaged()) {
+            return !hasMaximumManagedRingingCalls(excludeCall) &&
+                    !hasMaximumManagedHoldingCalls(excludeCall);
+        } else {
+            return !hasEmergencyCall() &&
+                    !hasMaximumSelfManagedRingingCalls(excludeCall, phoneAccountHandle) &&
+                    !hasMaximumSelfManagedCalls(excludeCall, phoneAccountHandle) &&
+                    !hasManagedCalls();
+        }
+    }
+
+    public boolean isOutgoingCallPermitted(PhoneAccountHandle phoneAccountHandle) {
+        return isOutgoingCallPermitted(null /* excludeCall */, phoneAccountHandle);
+    }
+
+    public boolean isOutgoingCallPermitted(Call excludeCall,
+                                           PhoneAccountHandle phoneAccountHandle) {
+        if (phoneAccountHandle == null) {
+            return false;
+        }
+        PhoneAccount phoneAccount =
+                mPhoneAccountRegistrar.getPhoneAccountUnchecked(phoneAccountHandle);
+        if (phoneAccount == null) {
+            return false;
+        }
+
+        if (!phoneAccount.isSelfManaged()) {
+            return !hasMaximumManagedOutgoingCalls(excludeCall) &&
+                    !hasMaximumManagedDialingCalls(excludeCall) &&
+                    !hasMaximumManagedLiveCalls(excludeCall) &&
+                    !hasMaximumManagedHoldingCalls(excludeCall);
+        } else {
+            // Only permit outgoing calls if there is no ongoing emergency calls and all other calls
+            // are associated with the current PhoneAccountHandle.
+            return !hasEmergencyCall() &&
+                    !hasMaximumSelfManagedCalls(excludeCall, phoneAccountHandle) &&
+                    !hasCallsForOtherPhoneAccount(phoneAccountHandle);
+        }
+    }
+
     /**
      * Dumps the state of the {@link CallsManager}.
      *
@@ -2314,4 +2453,26 @@
 
       call.setIntentExtras(extras);
     }
+
+    /**
+     * Notifies the {@link android.telecom.ConnectionService} associated with a
+     * {@link PhoneAccountHandle} that the attempt to create a new connection has failed.
+     *
+     * @param phoneAccountHandle The {@link PhoneAccountHandle}.
+     * @param call The {@link Call} which could not be added.
+     */
+    private void notifyCreateConnectionFailed(PhoneAccountHandle phoneAccountHandle, Call call) {
+        if (phoneAccountHandle == null) {
+            return;
+        }
+        ConnectionServiceWrapper service = mConnectionServiceRepository.getService(
+                phoneAccountHandle.getComponentName(), phoneAccountHandle.getUserHandle());
+        if (service == null) {
+            Log.i(this, "Found no connection service.");
+            return;
+        } else {
+            call.setConnectionService(service);
+            service.createConnectionFailed(call);
+        }
+    }
 }
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
index 27af7f4..edb3ee9 100644
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -871,6 +871,13 @@
 
                 Log.addEvent(call, LogUtils.Events.START_CONNECTION, Log.piiHandle(call.getHandle()));
                 try {
+                    // For self-managed incoming calls, if there is another ongoing call Telecom is
+                    // responsible for showing a UI to ask the user if they'd like to answer this
+                    // new incoming call.
+                    boolean shouldShowIncomingCallUI = call.isSelfManaged() &&
+                            !mCallsManager.hasCallsForOtherPhoneAccount(
+                                    call.getTargetPhoneAccount());
+
                     mServiceInterface.createConnection(
                             call.getConnectionManagerPhoneAccount(),
                             callId,
@@ -880,11 +887,11 @@
                                     extras,
                                     call.getVideoState(),
                                     callId,
-                                    false), // TODO(3pcalls): pass in true/false based on whether ux
-                                            // should show.
+                                    shouldShowIncomingCallUI),
                             call.shouldAttachToExistingConnection(),
                             call.isUnknown(),
                             Log.getExternalSession());
+
                 } catch (RemoteException e) {
                     Log.e(this, e, "Failure to createConnection -- %s", getComponentName());
                     mPendingResponses.remove(callId).handleCreateConnectionFailure(
@@ -902,6 +909,50 @@
         mBinder.bind(callback, call);
     }
 
+    /**
+     * Notifies the {@link ConnectionService} associated with a {@link Call} that the request to
+     * create a connection has been denied or failed.
+     * @param call The call.
+     */
+    void createConnectionFailed(final Call call) {
+        Log.d(this, "createConnectionFailed(%s) via %s.", call, getComponentName());
+        BindCallback callback = new BindCallback() {
+            @Override
+            public void onSuccess() {
+                final String callId = mCallIdMapper.getCallId(call);
+                // If still bound, tell the connection service create connection has failed.
+                if (callId != null && isServiceValid("createConnectionFailed")) {
+                    Log.addEvent(call, LogUtils.Events.CREATE_CONNECTION_FAILED,
+                            Log.piiHandle(call.getHandle()));
+                    try {
+                        logOutgoing("createConnectionFailed %s", callId);
+                        mServiceInterface.createConnectionFailed(callId,
+                                new ConnectionRequest(
+                                        call.getTargetPhoneAccount(),
+                                        call.getHandle(),
+                                        call.getIntentExtras(),
+                                        call.getVideoState(),
+                                        callId,
+                                        false),
+                                call.isIncoming(),
+                                Log.getExternalSession());
+                        call.setDisconnectCause(new DisconnectCause(DisconnectCause.CANCELED));
+                        call.disconnect();
+                    } catch (RemoteException e) {
+                    }
+                }
+            }
+
+            @Override
+            public void onFailure() {
+                // Binding failed.  Oh no.
+                Log.w(this, "onFailure - could not bind to CS for call %s", call.getId());
+            }
+        };
+
+        mBinder.bind(callback, call);
+    }
+
     /** @see IConnectionService#abort(String, Session.Info)  */
     void abort(Call call) {
         // Clear out any pending outgoing call data
diff --git a/src/com/android/server/telecom/CreateConnectionProcessor.java b/src/com/android/server/telecom/CreateConnectionProcessor.java
index 2b88848..629e949 100644
--- a/src/com/android/server/telecom/CreateConnectionProcessor.java
+++ b/src/com/android/server/telecom/CreateConnectionProcessor.java
@@ -132,8 +132,10 @@
             mAttemptRecords.add(new CallAttemptRecord(
                     mCall.getTargetPhoneAccount(), mCall.getTargetPhoneAccount()));
         }
-        adjustAttemptsForConnectionManager();
-        adjustAttemptsForEmergency(mCall.getTargetPhoneAccount());
+        if (!mCall.isSelfManaged()) {
+            adjustAttemptsForConnectionManager();
+            adjustAttemptsForEmergency(mCall.getTargetPhoneAccount());
+        }
         mAttemptRecordIterator = mAttemptRecords.iterator();
         attemptNextPhoneAccount();
     }
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index 8e01d04..891734b 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -654,6 +654,11 @@
 
     @Override
     public void onCallAdded(Call call) {
+        if (call.isSelfManaged()) {
+            Log.i(this, "onCallAdded: skipped self-managed %s", call);
+            return;
+        }
+
         if (!isBoundToServices()) {
             bindToServices(call);
         } else {
@@ -688,6 +693,11 @@
 
     @Override
     public void onCallRemoved(Call call) {
+        if (call.isSelfManaged()) {
+            Log.i(this, "onCallRemoved: skipped self-managed %s", call);
+            return;
+        }
+
         Log.i(this, "onCallRemoved: %s", call);
         if (mCallsManager.getCalls().isEmpty()) {
             /** Let's add a 2 second delay before we send unbind to the services to hopefully
@@ -711,6 +721,11 @@
 
     @Override
     public void onExternalCallChanged(Call call, boolean isExternalCall) {
+        if (call.isSelfManaged()) {
+            Log.i(this, "onExternalCallChanged: skipped self-managed %s", call);
+            return;
+        }
+
         Log.i(this, "onExternalCallChanged: %s -> %b", call, isExternalCall);
 
         List<ComponentName> componentsUpdated = new ArrayList<>();
@@ -772,6 +787,11 @@
 
     @Override
     public void onCallStateChanged(Call call, int oldState, int newState) {
+        if (call.isSelfManaged()) {
+            Log.i(this, "onExternalCallChanged: skipped self-managed %s", call);
+            return;
+        }
+
         updateCall(call);
     }
 
@@ -780,6 +800,11 @@
             Call call,
             ConnectionServiceWrapper oldService,
             ConnectionServiceWrapper newService) {
+        if (call.isSelfManaged()) {
+            Log.i(this, "onConnectionServiceChanged: skipped self-managed %s", call);
+            return;
+        }
+
         updateCall(call);
     }
 
@@ -825,6 +850,11 @@
 
     @Override
     public void onIsConferencedChanged(Call call) {
+        if (call.isSelfManaged()) {
+            Log.i(this, "onIsConferencedChanged: skipped self-managed %s", call);
+            return;
+        }
+
         Log.d(this, "onIsConferencedChanged %s", call);
         updateCall(call);
     }
@@ -1119,7 +1149,8 @@
         int numCallsSent = 0;
         for (Call call : calls) {
             try {
-                if (call.isExternalCall() && !info.isExternalCallsSupported()) {
+                if (call.isSelfManaged() ||
+                        (call.isExternalCall() && !info.isExternalCallsSupported())) {
                     continue;
                 }
 
diff --git a/src/com/android/server/telecom/LogUtils.java b/src/com/android/server/telecom/LogUtils.java
index 69ba0ff..121abcf 100644
--- a/src/com/android/server/telecom/LogUtils.java
+++ b/src/com/android/server/telecom/LogUtils.java
@@ -80,6 +80,7 @@
         public static final String START_CALL_WAITING_TONE = "START_CALL_WAITING_TONE";
         public static final String STOP_CALL_WAITING_TONE = "STOP_CALL_WAITING_TONE";
         public static final String START_CONNECTION = "START_CONNECTION";
+        public static final String CREATE_CONNECTION_FAILED = "CREATE_CONNECTION_FAILED";
         public static final String BIND_CS = "BIND_CS";
         public static final String CS_BOUND = "CS_BOUND";
         public static final String CONFERENCE_WITH = "CONF_WITH";
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index fc1eca6..6d5af5b 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -24,6 +24,7 @@
 import static android.Manifest.permission.REGISTER_SIM_SUBSCRIPTION;
 import static android.Manifest.permission.WRITE_SECURE_SETTINGS;
 
+import android.Manifest;
 import android.app.ActivityManager;
 import android.app.AppOpsManager;
 import android.content.ComponentName;
@@ -952,6 +953,13 @@
                             enforceUserHandleMatchesCaller(phoneAccountHandle);
                             enforcePhoneAccountIsRegisteredEnabled(phoneAccountHandle,
                                     Binder.getCallingUserHandle());
+                            if (isSelfManagedConnectionService(phoneAccountHandle)) {
+                                // Self-managed phone account, ensure it has MANAGE_OWN_CALLS.
+                                mContext.enforceCallingOrSelfPermission(
+                                        android.Manifest.permission.MANAGE_OWN_CALLS,
+                                        "Self-managed phone accounts must have MANAGE_OWN_CALLS " +
+                                                "permission.");
+                            }
                         }
                         long token = Binder.clearCallingIdentity();
                         try {
@@ -1030,7 +1038,18 @@
             try {
                 Log.startSession("TSI.pC");
                 enforceCallingPackage(callingPackage);
-                if (!canCallPhone(callingPackage, "placeCall")) {
+
+                PhoneAccountHandle phoneAccountHandle = null;
+                if (extras != null) {
+                    phoneAccountHandle = extras.getParcelable(
+                            TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE);
+                }
+                boolean isSelfManaged = phoneAccountHandle != null &&
+                        isSelfManagedConnectionService(phoneAccountHandle);
+                if (isSelfManaged) {
+                    mContext.enforceCallingOrSelfPermission(Manifest.permission.MANAGE_OWN_CALLS,
+                            "Self-managed ConnectionServices require MANAGE_OWN_CALLS permission.");
+                } else if (!canCallPhone(callingPackage, "placeCall")) {
                     throw new SecurityException("Package " + callingPackage
                             + " is not allowed to place phone calls");
                 }
@@ -1059,7 +1078,8 @@
                         }
                         mUserCallIntentProcessorFactory.create(mContext, userHandle)
                                 .processIntent(
-                                        intent, callingPackage, hasCallAppOp && hasCallPermission);
+                                        intent, callingPackage, isSelfManaged ||
+                                                (hasCallAppOp && hasCallPermission));
                     } finally {
                         Binder.restoreCallingIdentity(token);
                     }
@@ -1194,7 +1214,7 @@
                 synchronized (mLock) {
                     long token = Binder.clearCallingIdentity();
                     try {
-                        // TODO(3pcalls) - Implement body.
+                        return mCallsManager.isIncomingCallPermitted(phoneAccountHandle);
                     } finally {
                         Binder.restoreCallingIdentity(token);
                     }
@@ -1202,7 +1222,6 @@
             } finally {
                 Log.endSession();
             }
-            return true;
         }
 
         /**
@@ -1216,7 +1235,7 @@
                 synchronized (mLock) {
                     long token = Binder.clearCallingIdentity();
                     try {
-                        // TODO(3pcalls) - Implement body.
+                        return mCallsManager.isOutgoingCallPermitted(phoneAccountHandle);
                     } finally {
                         Binder.restoreCallingIdentity(token);
                     }
@@ -1224,8 +1243,6 @@
             } finally {
                 Log.endSession();
             }
-
-            return true;
         }
     };
 
@@ -1454,6 +1471,15 @@
         }
     }
 
+    private boolean isSelfManagedConnectionService(PhoneAccountHandle phoneAccountHandle) {
+        if (phoneAccountHandle != null) {
+                PhoneAccount phoneAccount = mPhoneAccountRegistrar.getPhoneAccountUnchecked(
+                        phoneAccountHandle);
+                return phoneAccount.isSelfManaged();
+        }
+        return false;
+    }
+
     private boolean canCallPhone(String callingPackage, String message) {
         // The system/default dialer can always read phone state - so that emergency calls will
         // still work.
diff --git a/testapps/AndroidManifest.xml b/testapps/AndroidManifest.xml
index c1eb258..6c7202b 100644
--- a/testapps/AndroidManifest.xml
+++ b/testapps/AndroidManifest.xml
@@ -25,6 +25,7 @@
     <uses-permission android:name="android.permission.CAMERA" />
     <uses-permission android:name="android.permission.CALL_PHONE" />
     <uses-permission android:name="android.permission.CONTROL_INCALL_EXPERIENCE" />
+    <uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
     <uses-permission android:name="android.permission.READ_CALL_LOG" />
     <uses-permission android:name="android.permission.READ_PHONE_STATE" />
     <uses-permission android:name="android.permission.REGISTER_CALL_PROVIDER" />
@@ -155,6 +156,24 @@
                 <category android:name="android.intent.category.DEFAULT" />
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
+          </activity>
+
+        <activity android:name="com.android.server.telecom.testapps.SelfManagedCallingActivity"
+                  android:label="@string/selfManagedCallingActivityLabel"
+                  android:process="com.android.server.telecom.testapps.SelfMangingCallingApp">
+          <intent-filter>
+              <action android:name="android.intent.action.MAIN" />
+              <category android:name="android.intent.category.DEFAULT" />
+              <category android:name="android.intent.category.LAUNCHER" />
+          </intent-filter>
         </activity>
+
+        <service android:name="com.android.server.telecom.testapps.SelfManagedConnectionService"
+                 android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
+                 android:process="com.android.server.telecom.testapps.SelfMangingCallingApp">
+          <intent-filter>
+              <action android:name="android.telecom.ConnectionService" />
+          </intent-filter>
+        </service>
     </application>
 </manifest>
diff --git a/testapps/res/layout/self_managed_call_list_item.xml b/testapps/res/layout/self_managed_call_list_item.xml
new file mode 100644
index 0000000..f3be4ad
--- /dev/null
+++ b/testapps/res/layout/self_managed_call_list_item.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 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
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+    <TextView
+        android:id="@+id/phoneNumber"
+        android:layout_gravity="left"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="25dp"
+        android:text="TextView" />
+    <TextView
+        android:id="@+id/callState"
+        android:layout_gravity="left"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="25dp"
+        android:text="TextView" />
+    <LinearLayout
+        android:orientation="horizontal"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content">
+        <Button
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Active"
+            android:id="@+id/setActiveButton" />
+        <Button
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Hold"
+            android:id="@+id/setHeldButton" />
+        <Button
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Disconnect"
+            android:id="@+id/disconnectButton" />
+    </LinearLayout>
+    <LinearLayout
+        android:orientation="horizontal"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content">
+        <Button
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Speaker"
+            android:id="@+id/speakerButton" />
+        <Button
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Earpiece"
+            android:id="@+id/earpieceButton" />
+    </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/testapps/res/layout/self_managed_sample_main.xml b/testapps/res/layout/self_managed_sample_main.xml
new file mode 100644
index 0000000..f239997
--- /dev/null
+++ b/testapps/res/layout/self_managed_sample_main.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 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
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent">
+    <CheckBox
+        android:id="@+id/checkIfPermittedBeforeCalling"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/checkIfPermittedBeforeCallingButton" />
+    <TableLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content">
+        <TableRow android:layout_span="2">
+            <RadioGroup>
+                <RadioButton
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="Acct 1"
+                    android:id="@+id/useAcct1Button"/>
+                <RadioButton
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="Acct 2"
+                    android:id="@+id/useAcct2Button"/>
+            </RadioGroup>
+        </TableRow>
+        <TableRow android:layout_span="2">
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="Place call:" />
+        </TableRow>
+        <TableRow>
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="Number:" />
+            <EditText
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:id="@+id/phoneNumber" />
+        </TableRow>
+        <TableRow>
+            <Button
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="Outgoing Call"
+                android:id="@+id/placeOutgoingCallButton" />
+            <Button
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="Incoming Call"
+                android:id="@+id/placeIncomingCallButton" />
+        </TableRow>
+        <TableRow android:layout_span="2">
+            <ListView
+                android:id="@+id/callList"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:listSelector="@null"
+                android:divider="@null" />
+        </TableRow>
+    </TableLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/testapps/res/raw/sample_audio.ogg b/testapps/res/raw/sample_audio.ogg
new file mode 100644
index 0000000..0129b46
--- /dev/null
+++ b/testapps/res/raw/sample_audio.ogg
Binary files differ
diff --git a/testapps/res/raw/sample_audio2.ogg b/testapps/res/raw/sample_audio2.ogg
new file mode 100644
index 0000000..a0b39b4
--- /dev/null
+++ b/testapps/res/raw/sample_audio2.ogg
Binary files differ
diff --git a/testapps/res/values/donottranslate_strings.xml b/testapps/res/values/donottranslate_strings.xml
index 83d1ef3..74a3fd4 100644
--- a/testapps/res/values/donottranslate_strings.xml
+++ b/testapps/res/values/donottranslate_strings.xml
@@ -49,4 +49,17 @@
     <string name="UssdUiAppLabel">Test Ussd UI</string>
 
     <string name="placeUssdButton">Send USSD</string>
+
+    <!-- String for button in SelfManagedCallingActivity. -->
+    <string name="checkIfPermittedBeforeCallingButton">Check if calls permitted before calling</string>
+
+    <string name="selfManagedCallingActivityLabel">Self-Managed Sample</string>
+
+    <string name="outgoingCallNotPermitted">Outgoing call not permitted.</string>
+
+    <string name="outgoingCallNotPermittedCS">Outgoing call not permitted (CS Reported).</string>
+
+    <string name="incomingCallNotPermitted">Incoming call not permitted.</string>
+
+    <string name="incomingCallNotPermittedCS">Incoming call not permitted (CS Reported).</string>
 </resources>
diff --git a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java
new file mode 100644
index 0000000..e0d7442
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2017 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.testapps;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.net.Uri;
+import android.telecom.ConnectionRequest;
+import android.telecom.Log;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.util.ArrayMap;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Manages the list of {@link SelfManagedConnection} active in the sample third-party calling app.
+ */
+public class SelfManagedCallList {
+    public abstract static class Listener {
+        public void onCreateIncomingConnectionFailed(ConnectionRequest request) {};
+        public void onCreateOutgoingConnectionFailed(ConnectionRequest request) {};
+        public void onConnectionListChanged() {};
+    }
+
+    public static String SELF_MANAGED_ACCOUNT_1 = "1";
+    public static String SELF_MANAGED_ACCOUNT_2 = "2";
+
+    private static SelfManagedCallList sInstance;
+    private static ComponentName COMPONENT_NAME = new ComponentName(
+            SelfManagedCallList.class.getPackage().getName(),
+            SelfManagedConnectionService.class.getName());
+    private static Uri SELF_MANAGED_ADDRESS_1 = Uri.fromParts(PhoneAccount.SCHEME_TEL, "555-1212",
+            "");
+    private static Uri SELF_MANAGED_ADDRESS_2 = Uri.fromParts(PhoneAccount.SCHEME_SIP,
+            "me@test.org", "");
+    private static Map<String, PhoneAccountHandle> mPhoneAccounts = new ArrayMap();
+
+    public static SelfManagedCallList getInstance() {
+        if (sInstance == null) {
+            sInstance = new SelfManagedCallList();
+        }
+        return sInstance;
+    }
+
+    private Listener mListener;
+
+    private List<SelfManagedConnection> mConnections = new ArrayList<>();
+
+    public void setListener(Listener listener) {
+        mListener = listener;
+    }
+
+    public void registerPhoneAccounts(Context context) {
+        registerPhoneAccount(context, SELF_MANAGED_ACCOUNT_1, SELF_MANAGED_ADDRESS_1);
+        registerPhoneAccount(context, SELF_MANAGED_ACCOUNT_2, SELF_MANAGED_ADDRESS_2);
+    }
+
+    public void registerPhoneAccount(Context context, String id, Uri address) {
+        PhoneAccountHandle handle = new PhoneAccountHandle(COMPONENT_NAME, id);
+        mPhoneAccounts.put(id, handle);
+        PhoneAccount.Builder builder = PhoneAccount.builder(handle, id)
+                .addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
+                .addSupportedUriScheme(PhoneAccount.SCHEME_SIP)
+                .setAddress(address)
+                .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED |
+                        PhoneAccount.CAPABILITY_VIDEO_CALLING |
+                        PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING);
+
+        TelecomManager.from(context).registerPhoneAccount(builder.build());
+    }
+
+    public PhoneAccountHandle getPhoneAccountHandle(String id) {
+        return mPhoneAccounts.get(id);
+    }
+
+    public void notifyCreateIncomingConnectionFailed(ConnectionRequest request) {
+        if (mListener != null) {
+            mListener.onCreateIncomingConnectionFailed(request);
+        }
+    }
+
+    public void notifyCreateOutgoingConnectionFailed(ConnectionRequest request) {
+        if (mListener != null) {
+            mListener.onCreateOutgoingConnectionFailed(request);
+        }
+    }
+
+    public void addConnection(SelfManagedConnection connection) {
+        Log.i(this, "addConnection %s", connection);
+        mConnections.add(connection);
+        if (mListener != null) {
+            Log.i(this, "addConnection calling onConnectionListChanged %s", connection);
+            mListener.onConnectionListChanged();
+        }
+    }
+
+    public void removeConnection(SelfManagedConnection connection) {
+        Log.i(this, "removeConnection %s", connection);
+        mConnections.remove(connection);
+        if (mListener != null) {
+            Log.i(this, "removeConnection calling onConnectionListChanged %s", connection);
+            mListener.onConnectionListChanged();
+        }
+    }
+
+    public List<SelfManagedConnection> getConnections() {
+        return mConnections;
+    }
+
+    public void notifyCallModified() {
+        if (mListener != null) {
+            mListener.onConnectionListChanged();
+        }
+    }
+}
diff --git a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallListAdapter.java b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallListAdapter.java
new file mode 100644
index 0000000..29d3c5f
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallListAdapter.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2017 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.testapps;
+
+import android.telecom.CallAudioState;
+import android.telecom.DisconnectCause;
+import android.telecom.PhoneAccountHandle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.TextView;
+
+import java.util.List;
+
+public class SelfManagedCallListAdapter extends BaseAdapter {
+
+    private static final String TAG = "SelfMgCallListAd";
+    /**
+     * Listener used to handle tap of the "disconnect" button for a connection.
+     */
+    private View.OnClickListener mDisconnectListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            View parent = (View) v.getParent().getParent();
+            SelfManagedConnection connection = (SelfManagedConnection) parent.getTag();
+            connection.setConnectionDisconnected(DisconnectCause.LOCAL);
+            SelfManagedCallList.getInstance().removeConnection(connection);
+        }
+    };
+
+    /**
+     * Listener used to handle tap of the "active" button for a connection.
+     */
+    private View.OnClickListener mActiveListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            View parent = (View) v.getParent().getParent();
+            SelfManagedConnection connection = (SelfManagedConnection) parent.getTag();
+            connection.setConnectionActive();
+            notifyDataSetChanged();
+        }
+    };
+
+    /**
+     * Listener used to handle tap of the "held" button for a connection.
+     */
+    private View.OnClickListener mHeldListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            View parent = (View) v.getParent().getParent();
+            SelfManagedConnection connection = (SelfManagedConnection) parent.getTag();
+            connection.setConnectionHeld();
+            notifyDataSetChanged();
+        }
+    };
+
+    private View.OnClickListener mSpeakerListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            View parent = (View) v.getParent().getParent();
+            SelfManagedConnection connection = (SelfManagedConnection) parent.getTag();
+            connection.setAudioRoute(CallAudioState.ROUTE_SPEAKER);
+            notifyDataSetChanged();
+        }
+    };
+
+    private View.OnClickListener mEarpieceListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            View parent = (View) v.getParent().getParent();
+            SelfManagedConnection connection = (SelfManagedConnection) parent.getTag();
+            connection.setAudioRoute(CallAudioState.ROUTE_EARPIECE);
+            notifyDataSetChanged();
+        }
+    };
+
+    private final LayoutInflater mLayoutInflater;
+
+    private List<SelfManagedConnection> mConnections;
+
+    public SelfManagedCallListAdapter(LayoutInflater layoutInflater,
+                                      List<SelfManagedConnection> connections) {
+
+        mLayoutInflater = layoutInflater;
+        mConnections = connections;
+    }
+
+    @Override
+    public int getCount() {
+        return mConnections.size();
+    }
+
+    @Override
+    public Object getItem(int position) {
+        return mConnections.get(position);
+    }
+
+    @Override
+    public long getItemId(int position) {
+        return position;
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        View result = convertView == null
+                ? mLayoutInflater.inflate(R.layout.self_managed_call_list_item, parent, false)
+                : convertView;
+        SelfManagedConnection connection = mConnections.get(position);
+        PhoneAccountHandle phoneAccountHandle = connection.getExtras().getParcelable(
+                SelfManagedConnection.EXTRA_PHONE_ACCOUNT_HANDLE);
+        CallAudioState audioState = connection.getCallAudioState();
+        String audioRoute = "?";
+        if (audioState != null) {
+            switch (audioState.getRoute()) {
+                case CallAudioState.ROUTE_BLUETOOTH:
+                    audioRoute = "BT";
+                    break;
+                case CallAudioState.ROUTE_SPEAKER:
+                    audioRoute = "\uD83D\uDD0A";
+                    break;
+                case CallAudioState.ROUTE_EARPIECE:
+                    audioRoute = "\uD83D\uDC42";
+                    break;
+                case CallAudioState.ROUTE_WIRED_HEADSET:
+                    audioRoute = "\uD83D\uDD0C";
+                    break;
+                default:
+                    audioRoute = "?";
+                    break;
+            }
+        }
+        String callType;
+        if (connection.isIncomingCall()) {
+            if (connection.isIncomingCallUiShowing()) {
+                callType = "Incoming - Show Incoming UX";
+            } else {
+                callType = "Incoming - DO NOT SHOW Incoming UX";
+            }
+        } else {
+            callType = "Outgoing";
+        }
+        setInfoForRow(result, phoneAccountHandle.getId(), connection.getAddress().toString(),
+                android.telecom.Connection.stateToString(connection.getState()), audioRoute,
+                callType);
+        result.setTag(connection);
+        return result;
+    }
+
+    public void updateConnections() {
+        Log.i(TAG, "updateConnections "+ mConnections.size());
+
+        notifyDataSetChanged();
+    }
+
+    private void setInfoForRow(View view, String accountName, String number,
+                               String status, String audioRoute, String callType) {
+
+        TextView numberTextView = (TextView) view.findViewById(R.id.phoneNumber);
+        TextView statusTextView = (TextView) view.findViewById(R.id.callState);
+        View activeButton = view.findViewById(R.id.setActiveButton);
+        activeButton.setOnClickListener(mActiveListener);
+        View disconnectButton = view.findViewById(R.id.disconnectButton);
+        disconnectButton.setOnClickListener(mDisconnectListener);
+        View setHeldButton = view.findViewById(R.id.setHeldButton);
+        setHeldButton.setOnClickListener(mHeldListener);
+        View speakerButton = view.findViewById(R.id.speakerButton);
+        speakerButton.setOnClickListener(mSpeakerListener);
+        View earpieceButton = view.findViewById(R.id.earpieceButton);
+        earpieceButton.setOnClickListener(mEarpieceListener);
+        numberTextView.setText(accountName + " - " + number + " (" + audioRoute + ")");
+        statusTextView.setText(callType + " - Status: " + status);
+    }
+}
diff --git a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java
new file mode 100644
index 0000000..8b7eae0
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2017 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.testapps;
+
+import android.app.Activity;
+import android.net.Uri;
+import android.os.Bundle;
+import android.telecom.ConnectionRequest;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.RadioButton;
+import android.widget.Toast;
+
+import com.android.server.telecom.testapps.R;
+
+import java.util.Objects;
+
+/**
+ * Provides a sample third-party calling app UX which implements the self managed connection service
+ * APIs.
+ */
+public class SelfManagedCallingActivity extends Activity {
+    private static final String TAG = "SelfMgCallActivity";
+    private SelfManagedCallList mCallList = SelfManagedCallList.getInstance();
+    private CheckBox mCheckIfPermittedBeforeCalling;
+    private Button mPlaceOutgoingCallButton;
+    private Button mPlaceIncomingCallButton;
+    private RadioButton mUseAcct1Button;
+    private RadioButton mUseAcct2Button;
+    private EditText mNumber;
+    private ListView mListView;
+    private SelfManagedCallListAdapter mListAdapter;
+
+    private SelfManagedCallList.Listener mCallListListener = new SelfManagedCallList.Listener() {
+        @Override
+        public void onCreateIncomingConnectionFailed(ConnectionRequest request) {
+            Log.i(TAG, "onCreateIncomingConnectionFailed " + request);
+            Toast.makeText(SelfManagedCallingActivity.this,
+                    R.string.incomingCallNotPermittedCS , Toast.LENGTH_SHORT).show();
+        };
+
+        @Override
+        public void onCreateOutgoingConnectionFailed(ConnectionRequest request) {
+            Log.i(TAG, "onCreateOutgoingConnectionFailed " + request);
+            Toast.makeText(SelfManagedCallingActivity.this,
+                    R.string.outgoingCallNotPermittedCS , Toast.LENGTH_SHORT).show();
+        };
+
+        @Override
+        public void onConnectionListChanged() {
+            Log.i(TAG, "onConnectionListChanged");
+            mListAdapter.updateConnections();
+        };
+    };
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.self_managed_sample_main);
+        mCheckIfPermittedBeforeCalling = (CheckBox) findViewById(
+                R.id.checkIfPermittedBeforeCalling);
+        mPlaceOutgoingCallButton = (Button) findViewById(R.id.placeOutgoingCallButton);
+        mPlaceOutgoingCallButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                placeOutgoingCall();
+            }
+        });
+        mPlaceIncomingCallButton = (Button) findViewById(R.id.placeIncomingCallButton);
+        mPlaceIncomingCallButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                placeIncomingCall();
+            }
+        });
+        mUseAcct1Button = (RadioButton) findViewById(R.id.useAcct1Button);
+        mUseAcct2Button = (RadioButton) findViewById(R.id.useAcct2Button);
+        mNumber = (EditText) findViewById(R.id.phoneNumber);
+        mListView = (ListView) findViewById(R.id.callList);
+        mCallList.setListener(mCallListListener);
+        mCallList.registerPhoneAccounts(this);
+        mListAdapter = new SelfManagedCallListAdapter(getLayoutInflater(),
+                mCallList.getConnections());
+        mListView.setAdapter(mListAdapter);
+        Log.i(TAG, "onCreate - mCallList id " + Objects.hashCode(mCallList));
+    }
+
+    private PhoneAccountHandle getSelectedPhoneAccountHandle() {
+        if (mUseAcct1Button.isChecked()) {
+            return mCallList.getPhoneAccountHandle(SelfManagedCallList.SELF_MANAGED_ACCOUNT_1);
+        } else if (mUseAcct2Button.isChecked()) {
+            return mCallList.getPhoneAccountHandle(SelfManagedCallList.SELF_MANAGED_ACCOUNT_2);
+        }
+        return null;
+    }
+
+    private void placeOutgoingCall() {
+        TelecomManager tm = TelecomManager.from(this);
+        PhoneAccountHandle phoneAccountHandle = getSelectedPhoneAccountHandle();
+
+        if (mCheckIfPermittedBeforeCalling.isChecked()) {
+            if (!tm.isOutgoingCallPermitted(phoneAccountHandle)) {
+                Toast.makeText(this, R.string.outgoingCallNotPermitted , Toast.LENGTH_SHORT).show();
+                return;
+            }
+        }
+
+        Bundle extras = new Bundle();
+        extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
+                getSelectedPhoneAccountHandle());
+        tm.placeCall(Uri.parse(mNumber.getText().toString()), extras);
+    }
+
+    private void placeIncomingCall() {
+        TelecomManager tm = TelecomManager.from(this);
+        PhoneAccountHandle phoneAccountHandle = getSelectedPhoneAccountHandle();
+
+        if (mCheckIfPermittedBeforeCalling.isChecked()) {
+            if (!tm.isIncomingCallPermitted(phoneAccountHandle)) {
+                Toast.makeText(this, R.string.incomingCallNotPermitted , Toast.LENGTH_SHORT).show();
+                return;
+            }
+        }
+
+        Bundle extras = new Bundle();
+        extras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS,
+                Uri.parse(mNumber.getText().toString()));
+        tm.addNewIncomingCall(getSelectedPhoneAccountHandle(), extras);
+    }
+}
\ No newline at end of file
diff --git a/testapps/src/com/android/server/telecom/testapps/SelfManagedConnection.java b/testapps/src/com/android/server/telecom/testapps/SelfManagedConnection.java
new file mode 100644
index 0000000..5fd8eba
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/SelfManagedConnection.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2017 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.testapps;
+
+import android.content.Context;
+import android.media.MediaPlayer;
+import android.telecom.CallAudioState;
+import android.telecom.Connection;
+import android.telecom.ConnectionService;
+import android.telecom.DisconnectCause;
+
+/**
+ * Sample self-managed {@link Connection} for a self-managed {@link ConnectionService}.
+ * <p>
+ * See {@link android.telecom} for more information on self-managed {@link ConnectionService}s.
+ */
+public class SelfManagedConnection extends Connection {
+    public static final String EXTRA_PHONE_ACCOUNT_HANDLE =
+            "com.android.server.telecom.testapps.extra.PHONE_ACCOUNT_HANDLE";
+
+    private final SelfManagedCallList mCallList;
+    private final MediaPlayer mMediaPlayer;
+    private final boolean mIsIncomingCall;
+    private boolean mIsIncomingCallUiShowing;
+
+    SelfManagedConnection(SelfManagedCallList callList, Context context, boolean isIncoming) {
+        mCallList = callList;
+        mMediaPlayer = createMediaPlayer(context);
+        mIsIncomingCall = isIncoming;
+    }
+
+    /**
+     * Handles updates to the audio state of the connection.
+     * @param state The new connection audio state.
+     */
+    @Override
+    public void onCallAudioStateChanged(CallAudioState state) {
+        mCallList.notifyCallModified();
+    }
+
+    public void setConnectionActive() {
+        mMediaPlayer.start();
+        setActive();
+    }
+
+    public void setConnectionHeld() {
+        mMediaPlayer.pause();
+        setOnHold();
+    }
+
+    public void setConnectionDisconnected(int cause) {
+        mMediaPlayer.stop();
+        setDisconnected(new DisconnectCause(cause));
+        destroy();
+    }
+
+    public void setIsIncomingCallUiShowing(boolean showing) {
+        mIsIncomingCallUiShowing = showing;
+    }
+
+    public boolean isIncomingCallUiShowing() {
+        return mIsIncomingCallUiShowing;
+    }
+
+    public boolean isIncomingCall() {
+        return mIsIncomingCall;
+    }
+
+    private MediaPlayer createMediaPlayer(Context context) {
+        int audioToPlay = (Math.random() > 0.5f) ? R.raw.sample_audio : R.raw.sample_audio2;
+        MediaPlayer mediaPlayer = MediaPlayer.create(context, audioToPlay);
+        mediaPlayer.setLooping(true);
+        return mediaPlayer;
+    }
+}
diff --git a/testapps/src/com/android/server/telecom/testapps/SelfManagedConnectionService.java b/testapps/src/com/android/server/telecom/testapps/SelfManagedConnectionService.java
new file mode 100644
index 0000000..31bbcd6
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/SelfManagedConnectionService.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2017 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.testapps;
+
+import android.os.Bundle;
+import android.telecom.Connection;
+import android.telecom.ConnectionRequest;
+import android.telecom.ConnectionService;
+import android.telecom.Log;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+
+import java.util.Objects;
+
+/**
+ * Sample implementation of the self-managed {@link ConnectionService} API.
+ * <p>
+ * See {@link android.telecom} for more information on self-managed {@link ConnectionService}s.
+ */
+public class SelfManagedConnectionService extends ConnectionService {
+    private final SelfManagedCallList mCallList = SelfManagedCallList.getInstance();
+
+    @Override
+    public Connection onCreateOutgoingConnection(
+            PhoneAccountHandle connectionManagerAccount,
+            final ConnectionRequest request) {
+
+        return createSelfManagedConnection(request, false);
+    }
+
+    @Override
+    public Connection onCreateIncomingConnection(PhoneAccountHandle connectionManagerPhoneAccount,
+            ConnectionRequest request) {
+        return createSelfManagedConnection(request, true);
+    }
+
+    @Override
+    public void onCreateIncomingConnectionFailed(ConnectionRequest request) {
+        mCallList.notifyCreateIncomingConnectionFailed(request);
+    }
+
+    @Override
+    public void onCreateOutgoingConnectionFailed(ConnectionRequest request) {
+        mCallList.notifyCreateOutgoingConnectionFailed(request);
+    }
+
+
+    private Connection createSelfManagedConnection(ConnectionRequest request, boolean isIncoming) {
+        SelfManagedConnection connection = new SelfManagedConnection(mCallList,
+                getApplicationContext(), isIncoming);
+        connection.setConnectionProperties(Connection.PROPERTY_SELF_MANAGED);
+        connection.setAddress(request.getAddress(), TelecomManager.PRESENTATION_ALLOWED);
+        connection.setExtras(request.getExtras());
+        if (isIncoming) {
+            connection.setIsIncomingCallUiShowing(request.shouldShowIncomingCallUi());
+        }
+
+        // Track the phone account handle which created this connection so we can distinguish them
+        // in the sample call list later.
+        Bundle moreExtras = new Bundle();
+        moreExtras.putParcelable(SelfManagedConnection.EXTRA_PHONE_ACCOUNT_HANDLE,
+                request.getAccountHandle());
+        connection.putExtras(moreExtras);
+        connection.setVideoState(request.getVideoState());
+        Log.i(this, "createSelfManagedConnection %s", connection);
+        mCallList.addConnection(connection);
+        return connection;
+    }
+}
diff --git a/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java b/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java
index 59c9468..9907ca7 100644
--- a/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java
+++ b/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java
@@ -216,7 +216,7 @@
         public void createConnection(PhoneAccountHandle connectionManagerPhoneAccount,
                 String id, ConnectionRequest request, boolean isIncoming, boolean isUnknown,
                 Session.Info info) throws RemoteException {
-            Log.i(ConnectionServiceFixture.this, "xoxox createConnection --> " + id);
+            Log.i(ConnectionServiceFixture.this, "createConnection --> " + id);
 
             if (mConnectionById.containsKey(id)) {
                 throw new RuntimeException("Connection already exists: " + id);
@@ -239,6 +239,18 @@
         }
 
         @Override
+        public void createConnectionFailed(String callId, ConnectionRequest request,
+                boolean isIncoming, Session.Info sessionInfo) throws RemoteException {
+            Log.i(ConnectionServiceFixture.this, "createConnectionFailed --> " + callId);
+
+            if (mConnectionById.containsKey(callId)) {
+                throw new RuntimeException("Connection already exists: " + callId);
+            }
+
+            // TODO(3p-calls): Implement this.
+        }
+
+        @Override
         public void abort(String callId, Session.Info info) throws RemoteException { }
 
         @Override