Merge "Add null checks to MissedCallNotifier." into lmp-dev
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index de59c06..a85710e 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -33,6 +33,12 @@
     <uses-permission android:name="android.permission.STOP_APP_SWITCHES" />
     <uses-permission android:name="android.permission.VIBRATE" />
     <uses-permission android:name="android.permission.WRITE_CALL_LOG" />
+    <!-- Protects the ability to register any PhoneAccount with a capability flags of either
+         PhoneAccount#CAPABILITY_CALL_PROVIDER or PhoneAccount#CAPABILITY_SIM_SUBSCRIPTION. -->
+    <permission
+            android:name="com.android.telecomm.permission.REGISTER_PROVIDER_OR_SUBSCRIPTION"
+            android:label="Register CALL_PROVIDER or SIM_SUBSCRIPTION PhoneAccount"
+            android:protectionLevel="signature"/>
 
     <!-- Declare which SDK level this application was built against. This is needed so that IDEs
          can check for incompatible APIs. -->
@@ -130,7 +136,7 @@
              CALL_PRIVILEGED permission or the broadcast will not be processed. High priority of
              1000 is used in all intent filters to prevent anything but the system from processing
              this intent (b/8871505). -->
-        <!-- TODO(santoscordon): Is there really a notion of an emergency SIP number? If not, can
+        <!-- TODO: Is there really a notion of an emergency SIP number? If not, can
              that scheme be removed from this activity? -->
         <activity-alias android:name="EmergencyCallActivity"
                 android:targetActivity="CallActivity"
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index 2bf055c..f59039e 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -38,7 +38,7 @@
     <string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"پیام به <xliff:g id="PHONE_NUMBER">%s</xliff:g> ارسال شد."</string>
     <string name="phone_account_preferences_title" msgid="3932310998135189661">"تنظیمات برگزیده حساب تلفن"</string>
     <string name="default_outgoing_account_title" msgid="8261079649574578970">"حساب خروجی پیش‌فرض"</string>
-    <string name="sim_call_manager_account" msgid="2559930293628077755">"‏حساب برقراری تماس با Wi-Fi"</string>
+    <string name="sim_call_manager_account" msgid="2559930293628077755">"‏حساب برقراری تماس تلفنی با Wi-Fi"</string>
     <string name="account_ask_every_time" msgid="944077828070287407">"هر بار پرسیده شود"</string>
-    <string name="do_not_use_sim_call_manager" msgid="5519252524007323694">"‏از تماس با Wi-Fi استفاده نشود"</string>
+    <string name="do_not_use_sim_call_manager" msgid="5519252524007323694">"‏از تماس تلفنی با Wi-Fi استفاده نشود"</string>
 </resources>
diff --git a/src/com/android/telecomm/Call.java b/src/com/android/telecomm/Call.java
index d6373a1..16dc323 100644
--- a/src/com/android/telecomm/Call.java
+++ b/src/com/android/telecomm/Call.java
@@ -22,10 +22,12 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
+import android.telecomm.CallCapabilities;
 import android.telecomm.CallPropertyPresentation;
 import android.telecomm.CallState;
 import android.telecomm.Connection;
 import android.telecomm.ConnectionRequest;
+import android.telecomm.ConnectionService.VideoCallProvider;
 import android.telecomm.GatewayInfo;
 import android.telecomm.ParcelableConnection;
 import android.telecomm.PhoneAccount;
@@ -44,6 +46,7 @@
 import com.android.telecomm.ContactsAsyncHelper.OnImageLoadCompleteListener;
 import com.google.common.base.Preconditions;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
@@ -85,6 +88,8 @@
         void onStartActivityFromInCall(Call call, PendingIntent intent);
         void onTargetPhoneAccountChanged(Call call);
         void onConnectionManagerPhoneAccountChanged(Call call);
+        void onPhoneAccountChanged(Call call);
+        void onConferenceableCallsChanged(Call call);
     }
 
     abstract static class ListenerBase implements Listener {
@@ -134,6 +139,10 @@
         public void onTargetPhoneAccountChanged(Call call) {}
         @Override
         public void onConnectionManagerPhoneAccountChanged(Call call) {}
+        @Override
+        public void onPhoneAccountChanged(Call call) {}
+        @Override
+        public void onConferenceableCallsChanged(Call call) {}
     }
 
     private static final OnQueryCompleteListener sCallerInfoQueryListener =
@@ -187,6 +196,10 @@
 
     private final Handler mHandler = new Handler();
 
+    private final List<Call> mConferenceableCalls = new ArrayList<>();
+
+    private PhoneAccountHandle mPhoneAccountHandle;
+
     private long mConnectTimeMillis;
 
     /** The state of the call. */
@@ -312,7 +325,8 @@
     }
 
     /** {@inheritDoc} */
-    @Override public String toString() {
+    @Override
+    public String toString() {
         String component = null;
         if (mConnectionService != null && mConnectionService.getComponentName() != null) {
             component = mConnectionService.getComponentName().flattenToShortString();
@@ -513,8 +527,9 @@
     }
 
     void setCallCapabilities(int callCapabilities) {
+        Log.v(this, "setCallCapabilities: %s", CallCapabilities.toString(callCapabilities));
         if (mCallCapabilities != callCapabilities) {
-            mCallCapabilities = callCapabilities;
+           mCallCapabilities = callCapabilities;
             for (Listener l : mListeners) {
                 l.onCallCapabilitiesChanged(this);
             }
@@ -566,15 +581,15 @@
         if (mDirectToVoicemailQueryPending) {
             if (mCallerInfo != null && mCallerInfo.shouldSendToVoicemail) {
                 Log.i(this, "Directing call to voicemail: %s.", this);
-                // TODO(santoscordon): Once we move State handling from CallsManager to Call, we
+                // TODO: Once we move State handling from CallsManager to Call, we
                 // will not need to set RINGING state prior to calling reject.
                 setState(CallState.RINGING);
                 reject(false, null);
             } else {
-                // TODO(santoscordon): Make this class (not CallsManager) responsible for changing
+                // TODO: Make this class (not CallsManager) responsible for changing
                 // the call state to RINGING.
 
-                // TODO(santoscordon): Replace this with state transition to RINGING.
+                // TODO: Replace this with state transition to RINGING.
                 for (Listener l : mListeners) {
                     l.onSuccessfulIncomingCall(this);
                 }
@@ -597,6 +612,7 @@
     @Override
     public void handleCreateConnectionSuccessful(
             ConnectionRequest request, ParcelableConnection connection) {
+        Log.v(this, "handleCreateConnectionSuccessful %s", connection);
         mCreateConnectionProcessor = null;
         setState(getStateFromConnectionState(connection.getState()));
         setTargetPhoneAccount(connection.getPhoneAccount());
@@ -841,7 +857,7 @@
     }
 
     void splitFromConference() {
-        // TODO(santoscordon): todo
+        // TODO: todo
     }
 
     void swapWithBackgroundCall() {
@@ -869,6 +885,15 @@
         }
     }
 
+    void setConferenceableCalls(List<Call> conferenceableCalls) {
+        mConferenceableCalls.clear();
+        mConferenceableCalls.addAll(conferenceableCalls);
+    }
+
+    List<Call> getConferenceableCalls() {
+        return mConferenceableCalls;
+    }
+
     private void addChildCall(Call call) {
         if (!mChildCalls.contains(call)) {
             mChildCalls.add(call);
diff --git a/src/com/android/telecomm/CallActivity.java b/src/com/android/telecomm/CallActivity.java
index eb65c25..607d994 100644
--- a/src/com/android/telecomm/CallActivity.java
+++ b/src/com/android/telecomm/CallActivity.java
@@ -56,7 +56,7 @@
     protected void onCreate(Bundle bundle) {
         super.onCreate(bundle);
 
-        // TODO(santoscordon): This activity will be displayed until the next screen which could be
+        // TODO: This activity will be displayed until the next screen which could be
         // the in-call UI and error dialog or potentially a call-type selection dialog.
         // Traditionally, this has been a black screen with a spinner. We need to reevaluate if this
         // is still desired and add back if necessary. Currently, the activity is set to NoDisplay
@@ -69,7 +69,7 @@
         Log.d(this, " - intent = %s", intent);
         Log.d(this, " - configuration = %s", configuration);
 
-        // TODO(santoscordon): Figure out if there is something to restore from bundle.
+        // TODO: Figure out if there is something to restore from bundle.
         // See OutgoingCallBroadcaster in services/Telephony for more.
 
         processIntent(intent);
diff --git a/src/com/android/telecomm/CallAudioManager.java b/src/com/android/telecomm/CallAudioManager.java
index 979a51a..e53de84 100644
--- a/src/com/android/telecomm/CallAudioManager.java
+++ b/src/com/android/telecomm/CallAudioManager.java
@@ -74,6 +74,7 @@
         if (CallsManager.getInstance().getCalls().isEmpty()) {
             Log.v(this, "all calls removed, reseting system audio to default state");
             setInitialAudioState(null);
+            mWasSpeakerOn = false;
         }
         updateAudioStreamAndMode();
     }
diff --git a/src/com/android/telecomm/CallIdMapper.java b/src/com/android/telecomm/CallIdMapper.java
index 2366343..ae13860 100644
--- a/src/com/android/telecomm/CallIdMapper.java
+++ b/src/com/android/telecomm/CallIdMapper.java
@@ -85,7 +85,7 @@
     void checkValidCallId(String callId) {
         // Note, no need for thread check, this method is thread safe.
         if (!isValidCallId(callId)) {
-            // TODO(santoscordon): Re-enable this once we stop getting updates to
+            // TODO: Re-enable this once we stop getting updates to
             // ConnectionServiceWrapper for remote connections.
             //throw new IllegalArgumentException(
             //        "Invalid call ID for " + mCallIdPrefix + ": " + callId);
diff --git a/src/com/android/telecomm/CallsManager.java b/src/com/android/telecomm/CallsManager.java
index 5ce9fe8..e75a9a1 100644
--- a/src/com/android/telecomm/CallsManager.java
+++ b/src/com/android/telecomm/CallsManager.java
@@ -42,7 +42,7 @@
  */
 public final class CallsManager extends Call.ListenerBase {
 
-    // TODO(santoscordon): Consider renaming this CallsManagerPlugin.
+    // TODO: Consider renaming this CallsManagerPlugin.
     interface CallsManagerListener {
         void onCallAdded(Call call);
         void onCallRemoved(Call call);
@@ -276,7 +276,7 @@
                 false /* isConference */);
 
         call.setExtras(extras);
-        // TODO(santoscordon): Move this to be a part of addCall()
+        // TODO: Move this to be a part of addCall()
         call.addListener(this);
         call.startCreateConnection();
     }
@@ -293,6 +293,14 @@
     void placeOutgoingCall(Uri handle, GatewayInfo gatewayInfo, PhoneAccountHandle accountHandle,
             boolean speakerphoneOn, int videoState) {
 
+        // We only allow a single outgoing call at any given time. Before placing a call, make sure
+        // there doesn't already exist another outgoing call.
+        Call currentOutgoing = getFirstCallWithState(CallState.NEW, CallState.DIALING);
+        if (currentOutgoing != null) {
+            Log.i(this, "Canceling simultaneous outgoing call.");
+            return;
+        }
+
         TelecommApp app = TelecommApp.getInstance();
         final Uri uriHandle = (gatewayInfo == null) ? handle : gatewayInfo.getGatewayHandle();
 
@@ -324,7 +332,7 @@
         call.setStartWithSpeakerphoneOn(speakerphoneOn);
         call.setVideoState(videoState);
 
-        // TODO(santoscordon): Move this to be a part of addCall()
+        // TODO: Move this to be a part of addCall()
         call.addListener(this);
         addCall(call);
 
@@ -357,9 +365,10 @@
     /**
      * Attempts to start a conference call for the specified call.
      *
-     * @param call The call to conference with.
+     * @param call The call to conference.
+     * @param otherCall The other call to conference with.
      */
-    void conference(Call call) {
+    void conference(Call call, Call otherCall) {
         Call conferenceCall = new Call(
                 mConnectionServiceRepository,
                 null /* handle */,
@@ -392,9 +401,9 @@
                 Log.v(this, "Holding active/dialing call %s before answering incoming call %s.",
                         mForegroundCall, call);
                 mForegroundCall.hold();
-                // TODO(santoscordon): Wait until we get confirmation of the active call being
+                // TODO: Wait until we get confirmation of the active call being
                 // on-hold before answering the new call.
-                // TODO(santoscordon): Import logic from CallManager.acceptCall()
+                // TODO: Import logic from CallManager.acceptCall()
             }
 
             for (CallsManagerListener listener : mListeners) {
@@ -694,7 +703,7 @@
     private void addCall(Call call) {
         mCalls.add(call);
 
-        // TODO(santoscordon): Update mForegroundCall prior to invoking
+        // TODO: Update mForegroundCall prior to invoking
         // onCallAdded for calls which immediately take the foreground (like the first call).
         for (CallsManagerListener listener : mListeners) {
             listener.onCallAdded(call);
@@ -737,9 +746,9 @@
             // Unfortunately, in the telephony world the radio is king. So if the call notifies
             // us that the call is in a particular state, we allow it even if it doesn't make
             // sense (e.g., ACTIVE -> RINGING).
-            // TODO(santoscordon): Consider putting a stop to the above and turning CallState
+            // TODO: Consider putting a stop to the above and turning CallState
             // into a well-defined state machine.
-            // TODO(santoscordon): Define expected state transitions here, and log when an
+            // TODO: Define expected state transitions here, and log when an
             // unexpected transition occurs.
             call.setState(newState);
 
@@ -759,7 +768,7 @@
     private void updateForegroundCall() {
         Call newForegroundCall = null;
         for (Call call : mCalls) {
-            // TODO(santoscordon): Foreground-ness needs to be explicitly set. No call, regardless
+            // TODO: Foreground-ness needs to be explicitly set. No call, regardless
             // of its state will be foreground by default and instead the connection service should
             // be notified when its calls enter and exit foreground state. Foreground will mean that
             // the call should play audio and listen to microphone if it wants.
diff --git a/src/com/android/telecomm/ConnectionServiceWrapper.java b/src/com/android/telecomm/ConnectionServiceWrapper.java
index c791932..4a45902 100644
--- a/src/com/android/telecomm/ConnectionServiceWrapper.java
+++ b/src/com/android/telecomm/ConnectionServiceWrapper.java
@@ -79,7 +79,8 @@
     private static final int MSG_SET_HANDLE = 19;
     private static final int MSG_SET_CALLER_DISPLAY_NAME = 20;
     private static final int MSG_SET_VIDEO_STATE = 21;
-    private static final int MSG_START_ACTIVITY_FROM_IN_CALL = 22;
+    private static final int MSG_SET_CONFERENCEABLE_CONNECTIONS = 22;
+    private static final int MSG_START_ACTIVITY_FROM_IN_CALL = 23;
 
     private final Handler mHandler = new Handler() {
         @Override
@@ -314,6 +315,28 @@
                     }
                     break;
                 }
+                case MSG_SET_CONFERENCEABLE_CONNECTIONS: {
+                    SomeArgs args = (SomeArgs) msg.obj;
+                    try {
+                        call = mCallIdMapper.getCall(args.arg1);
+                        if (call != null ){
+                            @SuppressWarnings("unchecked")
+                            List<String> conferenceableIds = (List<String>) args.arg2;
+                            List<Call> conferenceableCalls =
+                                    new ArrayList<>(conferenceableIds.size());
+                            for (String otherId : (List<String>) args.arg2) {
+                                Call otherCall = mCallIdMapper.getCall(otherId);
+                                if (otherCall != null && otherCall != call) {
+                                    conferenceableCalls.add(otherCall);
+                                }
+                            }
+                            call.setConferenceableCalls(conferenceableCalls);
+                        }
+                    } finally {
+                        args.recycle();
+                    }
+                    break;
+                }
                 case MSG_START_ACTIVITY_FROM_IN_CALL: {
                     SomeArgs args = (SomeArgs) msg.obj;
                     try {
@@ -513,6 +536,17 @@
         }
 
         @Override
+        public void setConferenceableConnections(
+                String callId, List<String> conferenceableCallIds) {
+            logIncoming("setConferenceableConnections %s %s", callId, conferenceableCallIds);
+            mCallIdMapper.checkValidCallId(callId);
+            SomeArgs args = SomeArgs.obtain();
+            args.arg1 = callId;
+            args.arg2 = conferenceableCallIds;
+            mHandler.obtainMessage(MSG_SET_CONFERENCEABLE_CONNECTIONS, args).sendToTarget();
+        }
+
+        @Override
         public void startActivityFromInCall(String callId, PendingIntent intent) {
             logIncoming("startActivityFromInCall %s %s", callId, intent);
             mCallIdMapper.checkValidCallId(callId);
@@ -860,6 +894,7 @@
         // Only give remote connection services to this connection service if it is listed as
         // the connection manager.
         PhoneAccountHandle simCallManager = registrar.getSimCallManager();
+        Log.d(this, "queryRemoteConnectionServices finds simCallManager = %s", simCallManager);
         if (simCallManager == null ||
                 !simCallManager.getComponentName().equals(getComponentName())) {
             noRemoteServices(callback);
diff --git a/src/com/android/telecomm/CreateConnectionProcessor.java b/src/com/android/telecomm/CreateConnectionProcessor.java
index f3449ff..5a186f4 100644
--- a/src/com/android/telecomm/CreateConnectionProcessor.java
+++ b/src/com/android/telecomm/CreateConnectionProcessor.java
@@ -147,6 +147,8 @@
         }
         PhoneAccountHandle simCallManager =
                 TelecommApp.getInstance().getPhoneAccountRegistrar().getSimCallManager();
+
+        Log.d(this, "adjustAttemptsForWifi finds simCallManager = %s", simCallManager);
         if (simCallManager != null &&
                 !Objects.equals(simCallManager, mAttemptRecords.get(0).targetPhoneAccount)) {
             mAttemptRecords.set(
diff --git a/src/com/android/telecomm/InCallAdapter.java b/src/com/android/telecomm/InCallAdapter.java
index 0acb3f5..0e6c9df 100644
--- a/src/com/android/telecomm/InCallAdapter.java
+++ b/src/com/android/telecomm/InCallAdapter.java
@@ -161,14 +161,21 @@
                 case MSG_SET_AUDIO_ROUTE:
                     mCallsManager.setAudioRoute(msg.arg1);
                     break;
-                case MSG_CONFERENCE:
-                    call = mCallIdMapper.getCall(msg.obj);
-                    if (call != null) {
-                        mCallsManager.conference(call);
-                    } else {
-                        Log.w(this, "conference, unknown call id: %s", msg.obj);
+                case MSG_CONFERENCE: {
+                    SomeArgs args = (SomeArgs) msg.obj;
+                    try {
+                        call = mCallIdMapper.getCall(args.arg1);
+                        Call otherCall = mCallIdMapper.getCall(args.arg2);
+                        if (call != null && otherCall != null) {
+                            mCallsManager.conference(call, otherCall);
+                        } else {
+                            Log.w(this, "conference, unknown call id: %s", msg.obj);
+                        }
+                    } finally {
+                        args.recycle();
                     }
                     break;
+                }
                 case MSG_SPLIT_FROM_CONFERENCE:
                     call = mCallIdMapper.getCall(msg.obj);
                     if (call != null) {
@@ -293,8 +300,11 @@
     }
 
     @Override
-    public void conference(String callId) {
-        mHandler.obtainMessage(MSG_CONFERENCE, callId).sendToTarget();
+    public void conference(String callId, String otherCallId) {
+        SomeArgs args = SomeArgs.obtain();
+        args.arg1 = callId;
+        args.arg2 = otherCallId;
+        mHandler.obtainMessage(MSG_CONFERENCE, args).sendToTarget();
     }
 
     @Override
diff --git a/src/com/android/telecomm/InCallController.java b/src/com/android/telecomm/InCallController.java
index e12372d..c94b656 100644
--- a/src/com/android/telecomm/InCallController.java
+++ b/src/com/android/telecomm/InCallController.java
@@ -112,6 +112,11 @@
         public void onTargetPhoneAccountChanged(Call call) {
             updateCall(call);
         }
+
+        @Override
+        public void onConferenceableCallsChanged(Call call) {
+            updateCall(call);
+        }
     };
 
     /** Maintains a binding connection to the in-call app. */
@@ -146,7 +151,7 @@
     @Override
     public void onCallRemoved(Call call) {
         if (CallsManager.getInstance().getCalls().isEmpty()) {
-            // TODO(sail): Wait for all messages to be delivered to the service before unbinding.
+            // TODO: Wait for all messages to be delivered to the service before unbinding.
             unbind();
         }
         call.removeListener(mCallListener);
@@ -238,7 +243,7 @@
                     UserHandle.CURRENT)) {
                 Log.w(this, "Could not connect to the in-call app (%s)", component);
 
-                // TODO(santoscordon): Implement retry or fall-back-to-default logic.
+                // TODO: Implement retry or fall-back-to-default logic.
             }
         }
     }
@@ -336,6 +341,15 @@
         String callerDisplayName = call.getCallerDisplayNamePresentation() ==
                 CallPropertyPresentation.ALLOWED ?  call.getCallerDisplayName() : null;
 
+        List<Call> conferenceableCalls = call.getConferenceableCalls();
+        List<String> conferenceableCallIds = new ArrayList<String>(conferenceableCalls.size());
+        for (Call otherCall : conferenceableCalls) {
+            String otherId = mCallIdMapper.getCallId(otherCall);
+            if (otherId != null) {
+                conferenceableCallIds.add(otherId);
+            }
+        }
+
         return new ParcelableCall(
                 callId,
                 state,
@@ -354,6 +368,7 @@
                 parentCallId,
                 childCallIds,
                 call.getStatusHints(),
-                call.getVideoState());
+                call.getVideoState(),
+                conferenceableCallIds);
     }
 }
diff --git a/src/com/android/telecomm/InCallTonePlayer.java b/src/com/android/telecomm/InCallTonePlayer.java
index 7f08587..5ccdafa 100644
--- a/src/com/android/telecomm/InCallTonePlayer.java
+++ b/src/com/android/telecomm/InCallTonePlayer.java
@@ -193,10 +193,10 @@
                 return;
             }
 
-            // TODO(santoscordon): Certain CDMA tones need to check the ringer-volume state before
+            // TODO: Certain CDMA tones need to check the ringer-volume state before
             // playing. See CallNotifier.InCallTonePlayer.
 
-            // TODO(santoscordon): Some tones play through the end of a call so we need to inform
+            // TODO: Some tones play through the end of a call so we need to inform
             // CallAudioManager that we want focus the same way that Ringer does.
 
             synchronized (this) {
diff --git a/src/com/android/telecomm/MissedCallNotifier.java b/src/com/android/telecomm/MissedCallNotifier.java
index ca2c56e..9e757df 100644
--- a/src/com/android/telecomm/MissedCallNotifier.java
+++ b/src/com/android/telecomm/MissedCallNotifier.java
@@ -39,7 +39,7 @@
 
 /**
  * Creates a notification for calls that the user missed (neither answered nor rejected).
- * TODO(santoscordon): Make TelephonyManager.clearMissedCalls call into this class.
+ * TODO: Make TelephonyManager.clearMissedCalls call into this class.
  * STOPSHIP: Resolve b/13769374 about moving this class to InCall.
  */
 class MissedCallNotifier extends CallsManagerListenerBase {
@@ -186,7 +186,7 @@
         } else if (!TextUtils.isEmpty(handle)) {
             // A handle should always be displayed LTR using {@link BidiFormatter} regardless of the
             // content of the rest of the notification.
-            // TODO(santoscordon): Does this apply to SIP addresses?
+            // TODO: Does this apply to SIP addresses?
             BidiFormatter bidiFormatter = BidiFormatter.getInstance();
             return bidiFormatter.unicodeWrap(handle, TextDirectionHeuristics.LTR);
         } else {
diff --git a/src/com/android/telecomm/TelecommServiceImpl.java b/src/com/android/telecomm/TelecommServiceImpl.java
index 3598422..dc7050f 100644
--- a/src/com/android/telecomm/TelecommServiceImpl.java
+++ b/src/com/android/telecomm/TelecommServiceImpl.java
@@ -44,7 +44,8 @@
  * Implementation of the ITelecomm interface.
  */
 public class TelecommServiceImpl extends ITelecommService.Stub {
-    private static final String TELEPHONY_PACKAGE_NAME = "com.android.phone";
+    private static final String REGISTER_PROVIDER_OR_SUBSCRIPTION =
+            "com.android.telecomm.permission.REGISTER_PROVIDER_OR_SUBSCRIPTION";
 
     /** ${inheritDoc} */
     @Override
@@ -195,7 +196,7 @@
                     account.getAccountHandle().getComponentName().getPackageName());
             if (PhoneAccountRegistrar.has(account, PhoneAccount.CAPABILITY_CALL_PROVIDER) ||
                 PhoneAccountRegistrar.has(account, PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
-                enforceModifyPermissionOrCallingPackage(TELEPHONY_PACKAGE_NAME);
+                enforceRegisterProviderOrSubscriptionPermission();
             }
             mPhoneAccountRegistrar.registerPhoneAccount(account);
         } catch (Exception e) {
@@ -410,8 +411,13 @@
         }
     }
 
+    private void enforceRegisterProviderOrSubscriptionPermission() {
+        TelecommApp.getInstance().enforceCallingOrSelfPermission(
+                REGISTER_PROVIDER_OR_SUBSCRIPTION, null);
+    }
+
     private void enforceModifyPermissionOrCallingPackage(String packageName) {
-        // TODO(santoscordon): Use a new telecomm permission for this instead of reusing modify.
+        // TODO: Use a new telecomm permission for this instead of reusing modify.
         try {
             enforceModifyPermission();
         } catch (SecurityException e) {
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index fecfffb..bbc3292 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -15,10 +15,13 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.telecomm.tests">
+          coreApp="true"
+          package="com.android.telecomm.tests">
 
     <!-- Test connection service outgoing video preview. -->
     <uses-permission android:name="android.permission.CAMERA" />
+    <uses-permission
+            android:name="com.android.telecomm.permission.REGISTER_PROVIDER_OR_SUBSCRIPTION" />
 
     <application android:label="@string/app_name">
         <uses-library android:name="android.test.runner" />
@@ -31,6 +34,12 @@
             </intent-filter>
         </service>
 
+        <service android:name="com.android.telecomm.testapps.TestConnectionManager">
+            <intent-filter>
+                <action android:name="android.telecomm.ConnectionService" />
+            </intent-filter>
+        </service>
+
         <activity android:name="com.android.telecomm.testapps.TestCallActivity"
                 android:label="@string/testCallActivityLabel">
             <intent-filter>
diff --git a/tests/res/drawable-xhdpi/stat_sys_phone_call.png b/tests/res/drawable-xhdpi/stat_sys_phone_call.png
new file mode 100644
index 0000000..1bb4340
--- /dev/null
+++ b/tests/res/drawable-xhdpi/stat_sys_phone_call.png
Binary files differ
diff --git a/tests/src/com/android/telecomm/testapps/CallServiceNotifier.java b/tests/src/com/android/telecomm/testapps/CallServiceNotifier.java
index 0dbb0b1..debfc9b 100644
--- a/tests/src/com/android/telecomm/testapps/CallServiceNotifier.java
+++ b/tests/src/com/android/telecomm/testapps/CallServiceNotifier.java
@@ -16,6 +16,8 @@
 
 package com.android.telecomm.testapps;
 
+import com.android.telecomm.tests.R;
+
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
@@ -88,19 +90,28 @@
      * Registers a phone account with telecomm.
      */
     public void registerPhoneAccount(Context context) {
-        PhoneAccount account = new PhoneAccount(
+        TelecommManager telecommManager =
+                (TelecommManager) context.getSystemService(Context.TELECOMM_SERVICE);
+        telecommManager.registerPhoneAccount(new PhoneAccount(
                 new PhoneAccountHandle(
                         new ComponentName(context, TestConnectionService.class),
                         PHONE_ACCOUNT_ID),
                 Uri.parse("tel:555-TEST"),
                 "555-TEST",
                 PhoneAccount.CAPABILITY_CALL_PROVIDER,
-                0,  // iconResId
+                R.drawable.stat_sys_phone_call,
                 "Dummy Service",
-                "a short description for the dummy service");
-        TelecommManager telecommManager =
-                (TelecommManager) context.getSystemService(Context.TELECOMM_SERVICE);
-        telecommManager.registerPhoneAccount(account);
+                "a short description for the dummy service"));
+        telecommManager.registerPhoneAccount(new PhoneAccount(
+                new PhoneAccountHandle(
+                        new ComponentName(context, TestConnectionManager.class),
+                        PHONE_ACCOUNT_ID),
+                Uri.parse("tel:555-CMGR"),
+                "555-CMGR",
+                PhoneAccount.CAPABILITY_CONNECTION_MANAGER,
+                R.drawable.stat_sys_phone_call,
+                "Dummy Connection Manager",
+                "a short description for the dummy connection manager"));
     }
 
     /**
diff --git a/tests/src/com/android/telecomm/testapps/TestConnectionManager.java b/tests/src/com/android/telecomm/testapps/TestConnectionManager.java
new file mode 100644
index 0000000..6a8433a
--- /dev/null
+++ b/tests/src/com/android/telecomm/testapps/TestConnectionManager.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2013 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.telecomm.testapps;
+
+import android.app.PendingIntent;
+import android.net.Uri;
+import android.telecomm.CallAudioState;
+import android.telecomm.Connection;
+import android.telecomm.ConnectionRequest;
+import android.telecomm.ConnectionService;
+import android.telecomm.PhoneAccountHandle;
+import android.telecomm.RemoteConnection;
+import android.telecomm.StatusHints;
+import android.util.Log;
+
+import java.util.Random;
+
+/**
+ * Service which acts as a fake ConnectionManager if so configured.
+ * TODO(santoscordon): Rename all classes in the directory to Dummy* (e.g., DummyConnectionService).
+ */
+public class TestConnectionManager extends ConnectionService {
+    /**
+     * Random number generator used to generate phone numbers.
+     */
+    private Random mRandom = new Random();
+
+    private final class TestManagedConnection extends Connection {
+        private final RemoteConnection.Listener mProxyListener = new RemoteConnection.Listener() {
+            @Override
+            public void onStateChanged(RemoteConnection connection, int state) {
+                setState(state);
+            }
+
+            @Override
+            public void onDisconnected(RemoteConnection connection, int cause, String message) {
+                setDisconnected(cause, message);
+                destroy();
+            }
+
+            @Override
+            public void onRequestingRingback(RemoteConnection connection, boolean ringback) {
+                setRequestingRingback(ringback);
+            }
+
+            @Override
+            public void onCallCapabilitiesChanged(RemoteConnection connection,
+                    int callCapabilities) {
+                setCallCapabilities(callCapabilities);
+            }
+
+            @Override
+            public void onPostDialWait(RemoteConnection connection, String remainingDigits) {
+                setPostDialWait(remainingDigits);
+            }
+
+            @Override
+            public void onAudioModeIsVoipChanged(RemoteConnection connection, boolean isVoip) {
+                setAudioModeIsVoip(isVoip);
+            }
+
+            @Override
+            public void onStatusHintsChanged(RemoteConnection connection, StatusHints statusHints) {
+                setStatusHints(statusHints);
+            }
+
+            @Override
+            public void onVideoStateChanged(RemoteConnection connection, int videoState) {
+                setVideoState(videoState);
+            }
+
+            @Override
+            public void onHandleChanged(RemoteConnection connection, Uri handle, int presentation) {
+                setHandle(handle, presentation);
+            }
+
+            @Override
+            public void onCallerDisplayNameChanged(
+                    RemoteConnection connection, String callerDisplayName, int presentation) {
+                setCallerDisplayName(callerDisplayName, presentation);
+            }
+
+            @Override
+            public void onStartActivityFromInCall(
+                    RemoteConnection connection, PendingIntent intent) {
+                startActivityFromInCall(intent);
+            }
+
+            @Override
+            public void onDestroyed(RemoteConnection connection) {
+                destroy();
+            }
+        };
+
+        private final RemoteConnection mRemoteConnection;
+        private final boolean mIsIncoming;
+
+        TestManagedConnection(RemoteConnection remoteConnection, boolean isIncoming) {
+            mRemoteConnection = remoteConnection;
+            mIsIncoming = isIncoming;
+            mRemoteConnection.addListener(mProxyListener);
+            setState(mRemoteConnection.getState());
+        }
+
+        @Override
+        public void onAbort() {
+            mRemoteConnection.abort();
+        }
+
+        /** ${inheritDoc} */
+        @Override
+        public void onAnswer(int videoState) {
+            mRemoteConnection.answer(videoState);
+        }
+
+        /** ${inheritDoc} */
+        @Override
+        public void onDisconnect() {
+            mRemoteConnection.disconnect();
+        }
+
+        /** ${inheritDoc} */
+        @Override
+        public void onHold() {
+            mRemoteConnection.hold();
+        }
+
+        /** ${inheritDoc} */
+        @Override
+        public void onReject() {
+            mRemoteConnection.reject();
+        }
+
+        /** ${inheritDoc} */
+        @Override
+        public void onUnhold() {
+            mRemoteConnection.unhold();
+        }
+
+        @Override
+        public void onSetAudioState(CallAudioState state) {
+            mRemoteConnection.setAudioState(state);
+        }
+
+        private void setState(int state) {
+            log("setState: " + state);
+            switch (state) {
+                case State.ACTIVE:
+                    setActive();
+                    break;
+                case State.HOLDING:
+                    setOnHold();
+                    break;
+                case State.DIALING:
+                    setDialing();
+                    break;
+                case State.RINGING:
+                    setRinging();
+                    break;
+            }
+        }
+    }
+
+    private static void log(String msg) {
+        Log.w("telecomtestcs", "[TestConnectionService] " + msg);
+    }
+
+    @Override
+    public Connection onCreateOutgoingConnection(
+            PhoneAccountHandle connectionManagerAccount,
+            final ConnectionRequest request) {
+        return new TestManagedConnection(
+                createRemoteOutgoingConnection(
+                        request.getAccountHandle(),
+                        request),
+                false);
+    }
+
+    @Override
+    public Connection onCreateIncomingConnection(
+            PhoneAccountHandle connectionManagerAccount,
+            final ConnectionRequest request) {
+        return new TestManagedConnection(
+                createRemoteOutgoingConnection(
+                        request.getAccountHandle(),
+                        request),
+                true);
+    }
+}
diff --git a/tests/src/com/android/telecomm/testapps/TestConnectionService.java b/tests/src/com/android/telecomm/testapps/TestConnectionService.java
index 2d1084a..bd9239b 100644
--- a/tests/src/com/android/telecomm/testapps/TestConnectionService.java
+++ b/tests/src/com/android/telecomm/testapps/TestConnectionService.java
@@ -47,7 +47,7 @@
 
 /**
  * Service which provides fake calls to test the ConnectionService interface.
- * TODO(santoscordon): Rename all classes in the directory to Dummy* (e.g., DummyConnectionService).
+ * TODO: Rename all classes in the directory to Dummy* (e.g., DummyConnectionService).
  */
 public class TestConnectionService extends ConnectionService {
     public static final String EXTRA_GATEWAY_PROVIDER_PACKAGE =
@@ -407,7 +407,7 @@
 
         // If the number starts with 555, then we handle it ourselves. If not, then we
         // use a remote connection service.
-        // TODO(santoscordon): Have a special phone number to test the account-picker dialog flow.
+        // TODO: Have a special phone number to test the account-picker dialog flow.
         if (number != null && number.startsWith("555")) {
             // Normally we would use the original request as is, but for testing purposes, we are
             // adding ".." to the end of the number to follow its path more easily through the logs.