Split unit tests and interactive test apps apart

This CL splits the automated unit tests (*.tests package) and the
interactive test connection services (*.testapps package).

Apart from code hygiene and flexibility moving forward, this makes
Android manifest and build files, and the relevant dependencies,
simpler and easier to follow.

Change-Id: Id8c7763ae65f437fdfdabe8b0a4f3561adadbcb3
diff --git a/testapps/Android.mk b/testapps/Android.mk
new file mode 100644
index 0000000..9c11d34
--- /dev/null
+++ b/testapps/Android.mk
@@ -0,0 +1,31 @@
+#
+# 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.
+#
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+        android-ex-camera2 \
+        guava
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := TelecomTestApps
+LOCAL_CERTIFICATE := platform
+
+LOCAL_MODULE_TAGS := tests
+
+include $(BUILD_PACKAGE)
diff --git a/testapps/AndroidManifest.xml b/testapps/AndroidManifest.xml
new file mode 100644
index 0000000..747d377
--- /dev/null
+++ b/testapps/AndroidManifest.xml
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          coreApp="true"
+          package="com.android.server.telecom.testapps">
+
+    <uses-permission android:name="android.permission.CAMERA" />
+    <uses-permission android:name="android.permission.CONTROL_INCALL_EXPERIENCE" />
+    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+    <uses-permission android:name="android.permission.REGISTER_CALL_PROVIDER" />
+    <uses-permission android:name="android.permission.REGISTER_CONNECTION_MANAGER" />
+    <uses-permission android:name="android.permission.REGISTER_SIM_SUBSCRIPTION" />
+
+    <application android:label="@string/app_name">
+        <uses-library android:name="android.test.runner" />
+
+        <!-- Miscellaneous telecom app-related test activities. -->
+
+        <service android:name="com.android.server.telecom.testapps.TestConnectionService"
+                 android:permission="android.permission.BIND_CONNECTION_SERVICE" >
+            <intent-filter>
+                <action android:name="android.telecom.ConnectionService" />
+            </intent-filter>
+        </service>
+
+        <service android:name="com.android.server.telecom.testapps.TestConnectionManager"
+                 android:permission="android.permission.BIND_CONNECTION_SERVICE" >
+            <intent-filter>
+                <action android:name="android.telecom.ConnectionService" />
+            </intent-filter>
+        </service>
+
+        <service android:name="com.android.server.telecom.testapps.TestInCallServiceImpl"
+                 android:process="com.android.server.telecom.testapps.TestInCallService"
+                 android:permission="android.permission.BIND_INCALL_SERVICE" >
+            <intent-filter>
+                <action android:name="android.telecom.InCallService"/>
+            </intent-filter>
+        </service>
+
+        <activity android:name="com.android.server.telecom.testapps.TestCallActivity"
+                  android:label="@string/testCallActivityLabel">
+            <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>
+            <intent-filter>
+                <action android:name="android.telecom.testapps.ACTION_START_INCOMING_CALL" />
+                <action android:name="android.telecom.testapps.ACTION_NEW_UNKNOWN_CALL" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:scheme="tel" />
+                <data android:scheme="sip" />
+            </intent-filter>
+        </activity>
+
+        <receiver android:name="com.android.server.telecom.testapps.CallNotificationReceiver"
+                  android:exported="false">
+            <intent-filter>
+                <action android:name="com.android.server.telecom.testapps.ACTION_CALL_SERVICE_EXIT" />
+            </intent-filter>
+        </receiver>
+
+        <activity android:name="com.android.server.telecom.testapps.TestDialerActivity"
+                  android:label="@string/testDialerActivityLabel"
+                  android:process="com.android.server.telecom.testapps.TestInCallService">
+            <intent-filter>
+                <action android:name="android.intent.action.DIAL" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:mimeType="vnd.android.cursor.item/phone" />
+                <data android:mimeType="vnd.android.cursor.item/person" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.DIAL" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:scheme="voicemail" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.DIAL" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <action android:name="android.intent.action.DIAL" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:scheme="tel" />
+            </intent-filter>
+            <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>
+    </application>
+</manifest>
diff --git a/testapps/res/drawable-xhdpi/stat_sys_phone_call.png b/testapps/res/drawable-xhdpi/stat_sys_phone_call.png
new file mode 100644
index 0000000..1bb4340
--- /dev/null
+++ b/testapps/res/drawable-xhdpi/stat_sys_phone_call.png
Binary files differ
diff --git a/testapps/res/layout/testdialer_main.xml b/testapps/res/layout/testdialer_main.xml
new file mode 100644
index 0000000..a5453fc
--- /dev/null
+++ b/testapps/res/layout/testdialer_main.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical" >
+    <EditText
+        android:id="@+id/number"
+        android:inputType="number"
+        android:layout_width="200dp"
+        android:layout_height="wrap_content" />
+    <Button
+        android:id="@+id/place_call_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/placeCallButton" />
+    <Button
+        android:id="@+id/set_default_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/defaultDialerButton" />
+</LinearLayout>
diff --git a/testapps/res/raw/beep_boop.ogg b/testapps/res/raw/beep_boop.ogg
new file mode 100644
index 0000000..83148f0
--- /dev/null
+++ b/testapps/res/raw/beep_boop.ogg
Binary files differ
diff --git a/testapps/res/raw/outgoing_video.mp4 b/testapps/res/raw/outgoing_video.mp4
new file mode 100644
index 0000000..3e4f1cb
--- /dev/null
+++ b/testapps/res/raw/outgoing_video.mp4
Binary files differ
diff --git a/testapps/res/raw/test_pattern.mp4 b/testapps/res/raw/test_pattern.mp4
new file mode 100644
index 0000000..401066f
--- /dev/null
+++ b/testapps/res/raw/test_pattern.mp4
Binary files differ
diff --git a/testapps/res/raw/test_video.mp4 b/testapps/res/raw/test_video.mp4
new file mode 100644
index 0000000..1a454b3
--- /dev/null
+++ b/testapps/res/raw/test_video.mp4
Binary files differ
diff --git a/testapps/res/values/donottranslate_strings.xml b/testapps/res/values/donottranslate_strings.xml
new file mode 100644
index 0000000..91d8628
--- /dev/null
+++ b/testapps/res/values/donottranslate_strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<resources>
+    <!-- Application label -->
+    <string name="app_name">TelecommTests</string>
+
+    <!-- String for the TestCallActivity -->
+    <string name="testCallActivityLabel">Test Connection Service App</string>
+
+    <!-- String for the TestDialerActivity -->
+    <string name="testDialerActivityLabel">Test Dialer</string>
+
+    <!-- String for button in TestDialerActivity that reassigns the default Dialer -->
+    <string name="defaultDialerButton">Default dialer request</string>
+
+    <!-- String for button in TestDialerActivity that places a test call -->
+    <string name="placeCallButton">Place call</string>
+</resources>
diff --git a/testapps/src/com/android/server/telecom/testapps/CallNotificationReceiver.java b/testapps/src/com/android/server/telecom/testapps/CallNotificationReceiver.java
new file mode 100644
index 0000000..a835bf1
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/CallNotificationReceiver.java
@@ -0,0 +1,108 @@
+/*
+ * 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.server.telecom.testapps;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.telecom.CallState;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.util.Log;
+
+/**
+ * This class receives the notification callback intents used to update call states for
+ * {@link TestConnectionService}.
+ */
+public class CallNotificationReceiver extends BroadcastReceiver {
+
+    static final String TAG = CallNotificationReceiver.class.getSimpleName();
+    /**
+     * Exit intent action is sent when the user clicks the "exit" action of the
+     * TestConnectionService notification. Used to cancel (remove) the notification.
+     */
+    static final String ACTION_CALL_SERVICE_EXIT =
+            "com.android.server.telecom.testapps.ACTION_CALL_SERVICE_EXIT";
+    static final String ACTION_REGISTER_PHONE_ACCOUNT =
+            "com.android.server.telecom.testapps.ACTION_REGISTER_PHONE_ACCOUNT";
+    static final String ACTION_SHOW_ALL_PHONE_ACCOUNTS =
+            "com.android.server.telecom.testapps.ACTION_SHOW_ALL_PHONE_ACCOUNTS";
+    static final String ACTION_VIDEO_CALL =
+            "com.android.server.telecom.testapps.ACTION_VIDEO_CALL";
+    static final String ACTION_AUDIO_CALL =
+            "com.android.server.telecom.testapps.ACTION_AUDIO_CALL";
+
+    /** {@inheritDoc} */
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        String action = intent.getAction();
+        if (ACTION_CALL_SERVICE_EXIT.equals(action)) {
+            CallServiceNotifier.getInstance().cancelNotifications(context);
+        } else if (ACTION_REGISTER_PHONE_ACCOUNT.equals(action)) {
+            CallServiceNotifier.getInstance().registerPhoneAccount(context);
+        } else if (ACTION_SHOW_ALL_PHONE_ACCOUNTS.equals(action)) {
+            CallServiceNotifier.getInstance().showAllPhoneAccounts(context);
+        } else if (ACTION_VIDEO_CALL.equals(action)) {
+            sendIncomingCallIntent(context, null, true);
+        } else if (ACTION_AUDIO_CALL.equals(action)) {
+            sendIncomingCallIntent(context, null, false);
+        }
+    }
+
+    /**
+     * Creates and sends the intent to add an incoming call through Telecom.
+     *
+     * @param context The current context.
+     * @param isVideoCall {@code True} if this is a video call.
+     */
+    public static void sendIncomingCallIntent(Context context, Uri handle, boolean isVideoCall) {
+        PhoneAccountHandle phoneAccount = new PhoneAccountHandle(
+                new ComponentName(context, TestConnectionService.class),
+                CallServiceNotifier.SIM_SUBSCRIPTION_ID);
+
+        // For the purposes of testing, indicate whether the incoming call is a video call by
+        // stashing an indicator in the EXTRA_INCOMING_CALL_EXTRAS.
+        Bundle extras = new Bundle();
+        extras.putBoolean(TestConnectionService.EXTRA_IS_VIDEO_CALL, isVideoCall);
+        if (handle != null) {
+            extras.putParcelable(TestConnectionService.EXTRA_HANDLE, handle);
+        }
+
+        TelecomManager.from(context).addNewIncomingCall(phoneAccount, extras);
+    }
+
+    public static void addNewUnknownCall(Context context, Uri handle, Bundle extras) {
+        Log.i(TAG, "Adding new unknown call with handle " + handle);
+        PhoneAccountHandle phoneAccount = new PhoneAccountHandle(
+                new ComponentName(context, TestConnectionService.class),
+                CallServiceNotifier.SIM_SUBSCRIPTION_ID);
+
+        if (extras == null) {
+            extras = new Bundle();
+        }
+
+        if (handle != null) {
+            extras.putParcelable(TelecomManager.EXTRA_UNKNOWN_CALL_HANDLE, handle);
+            extras.putParcelable(TestConnectionService.EXTRA_HANDLE, handle);
+        }
+
+        TelecomManager.from(context).addNewUnknownCall(phoneAccount, extras);
+    }
+}
diff --git a/testapps/src/com/android/server/telecom/testapps/CallServiceNotifier.java b/testapps/src/com/android/server/telecom/testapps/CallServiceNotifier.java
new file mode 100644
index 0000000..d40f92d
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/CallServiceNotifier.java
@@ -0,0 +1,298 @@
+/*
+ * 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.server.telecom.testapps;
+
+import com.android.server.telecom.testapps.R;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Color;
+import android.net.Uri;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.util.Log;
+import android.widget.Toast;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Class used to create, update and cancel the notification used to display and update call state
+ * for {@link TestConnectionService}.
+ */
+public class CallServiceNotifier {
+    private static final CallServiceNotifier INSTANCE = new CallServiceNotifier();
+
+    static final String CALL_PROVIDER_ID = "testapps_TestConnectionService_CALL_PROVIDER_ID";
+    static final String SIM_SUBSCRIPTION_ID = "testapps_TestConnectionService_SIM_SUBSCRIPTION_ID";
+    static final String CONNECTION_MANAGER_ID =
+            "testapps_TestConnectionService_CONNECTION_MANAGER_ID";
+
+    /**
+     * Static notification IDs.
+     */
+    private static final int CALL_NOTIFICATION_ID = 1;
+    private static final int PHONE_ACCOUNT_NOTIFICATION_ID = 2;
+
+    /**
+     * Whether the added call should be started as a video call. Referenced by
+     * {@link TestConnectionService} to know whether to provide a call video provider.
+     */
+    public static boolean mStartVideoCall;
+
+    /**
+     * Singleton accessor.
+     */
+    public static CallServiceNotifier getInstance() {
+        return INSTANCE;
+    }
+
+    /**
+     * Creates a CallService & initializes notification manager.
+     */
+    private CallServiceNotifier() {
+    }
+
+    /**
+     * Updates the notification in the notification pane.
+     */
+    public void updateNotification(Context context) {
+        log("adding the notification ------------");
+        getNotificationManager(context).notify(CALL_NOTIFICATION_ID, getMainNotification(context));
+        getNotificationManager(context).notify(
+                PHONE_ACCOUNT_NOTIFICATION_ID, getPhoneAccountNotification(context));
+    }
+
+    /**
+     * Cancels the notification.
+     */
+    public void cancelNotifications(Context context) {
+        log("canceling notification");
+        getNotificationManager(context).cancel(CALL_NOTIFICATION_ID);
+        getNotificationManager(context).cancel(PHONE_ACCOUNT_NOTIFICATION_ID);
+    }
+
+    /**
+     * Registers a phone account with telecom.
+     */
+    public void registerPhoneAccount(Context context) {
+        TelecomManager telecomManager =
+                (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
+
+        telecomManager.clearAccounts();
+
+        telecomManager.registerPhoneAccount(PhoneAccount.builder(
+                new PhoneAccountHandle(
+                        new ComponentName(context, TestConnectionService.class),
+                        CALL_PROVIDER_ID),
+                "TelecomTestApp Call Provider")
+                .setAddress(Uri.parse("tel:555-TEST"))
+                .setSubscriptionAddress(Uri.parse("tel:555-TEST"))
+                .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER |
+                        PhoneAccount.CAPABILITY_VIDEO_CALLING)
+                .setIcon(context, R.drawable.stat_sys_phone_call, Color.RED)
+                .setHighlightColor(Color.RED)
+                .setShortDescription("a short description for the call provider")
+                .setSupportedUriSchemes(Arrays.asList("tel"))
+                .build());
+
+        telecomManager.registerPhoneAccount(PhoneAccount.builder(
+                new PhoneAccountHandle(
+                        new ComponentName(context, TestConnectionService.class),
+                        SIM_SUBSCRIPTION_ID),
+                "TelecomTestApp SIM Subscription")
+                .setAddress(Uri.parse("tel:555-TSIM"))
+                .setSubscriptionAddress(Uri.parse("tel:555-TSIM"))
+                .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER |
+                        PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION |
+                        PhoneAccount.CAPABILITY_VIDEO_CALLING)
+                .setIcon(context, R.drawable.stat_sys_phone_call, Color.GREEN)
+                .setHighlightColor(Color.GREEN)
+                .setShortDescription("a short description for the sim subscription")
+                .build());
+
+        telecomManager.registerPhoneAccount(PhoneAccount.builder(
+                        new PhoneAccountHandle(
+                                new ComponentName(context, TestConnectionManager.class),
+                                CONNECTION_MANAGER_ID),
+                        "TelecomTestApp CONNECTION MANAGER")
+                .setAddress(Uri.parse("tel:555-CMGR"))
+                .setSubscriptionAddress(Uri.parse("tel:555-CMGR"))
+                .setCapabilities(PhoneAccount.CAPABILITY_CONNECTION_MANAGER)
+                .setIcon(context, R.drawable.stat_sys_phone_call, Color.BLUE)
+                .setShortDescription("a short description for the connection manager")
+                .build());
+    }
+
+    /**
+     * Displays all phone accounts registered with telecom.
+     */
+    public void showAllPhoneAccounts(Context context) {
+        TelecomManager telecomManager =
+                (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
+        List<PhoneAccountHandle> accounts = telecomManager.getCallCapablePhoneAccounts();
+
+        Toast.makeText(context, accounts.toString(), Toast.LENGTH_LONG).show();
+    }
+
+    /**
+     * Returns the system's notification manager needed to add/remove notifications.
+     */
+    private NotificationManager getNotificationManager(Context context) {
+        return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+    }
+
+    /**
+     * Creates a notification object for using the telecom APIs.
+     */
+    private Notification getPhoneAccountNotification(Context context) {
+        final Notification.Builder builder = new Notification.Builder(context);
+        // Both notifications have buttons and only the first one with buttons will show its
+        // buttons.  Since the phone accounts notification is always first, setting false ensures
+        // it can be dismissed to use the other notification.
+        builder.setOngoing(false);
+        builder.setPriority(Notification.PRIORITY_HIGH);
+
+        final PendingIntent intent = createShowAllPhoneAccountsIntent(context);
+        builder.setContentIntent(intent);
+
+        builder.setSmallIcon(android.R.drawable.stat_sys_phone_call);
+        // TODO: Consider moving this into a strings.xml
+        builder.setContentText("Test phone accounts via telecom APIs.");
+        builder.setContentTitle("Test Phone Accounts");
+
+        addRegisterPhoneAccountAction(builder, context);
+        addShowAllPhoneAccountsAction(builder, context);
+
+        return builder.build();
+    }
+
+    /**
+     * Creates a notification object out of the current calls state.
+     */
+    private Notification getMainNotification(Context context) {
+        final Notification.Builder builder = new Notification.Builder(context);
+        builder.setOngoing(true);
+        builder.setPriority(Notification.PRIORITY_HIGH);
+        builder.setSmallIcon(android.R.drawable.stat_sys_phone_call);
+        builder.setContentText("Test calls via CallService API");
+        builder.setContentTitle("Test Connection Service");
+
+        addAddVideoCallAction(builder, context);
+        addAddCallAction(builder, context);
+        addExitAction(builder, context);
+
+        return builder.build();
+    }
+
+    /**
+     * Creates the intent to remove the notification.
+     */
+    private PendingIntent createExitIntent(Context context) {
+        final Intent intent = new Intent(CallNotificationReceiver.ACTION_CALL_SERVICE_EXIT, null,
+                context, CallNotificationReceiver.class);
+
+        return PendingIntent.getBroadcast(context, 0, intent, 0);
+    }
+
+    /**
+     * Creates the intent to register a phone account.
+     */
+    private PendingIntent createRegisterPhoneAccountIntent(Context context) {
+        final Intent intent = new Intent(CallNotificationReceiver.ACTION_REGISTER_PHONE_ACCOUNT,
+                null, context, CallNotificationReceiver.class);
+        return PendingIntent.getBroadcast(context, 0, intent, 0);
+    }
+
+    /**
+     * Creates the intent to show all phone accounts.
+     */
+    private PendingIntent createShowAllPhoneAccountsIntent(Context context) {
+        final Intent intent = new Intent(CallNotificationReceiver.ACTION_SHOW_ALL_PHONE_ACCOUNTS,
+                null, context, CallNotificationReceiver.class);
+        return PendingIntent.getBroadcast(context, 0, intent, 0);
+    }
+
+    /**
+     * Creates the intent to start an incoming video call
+     */
+    private PendingIntent createIncomingVideoCall(Context context) {
+        final Intent intent = new Intent(CallNotificationReceiver.ACTION_VIDEO_CALL,
+                null, context, CallNotificationReceiver.class);
+        return PendingIntent.getBroadcast(context, 0, intent, 0);
+    }
+
+    /**
+     * Creates the intent to start an incoming audio call
+     */
+    private PendingIntent createIncomingAudioCall(Context context) {
+        final Intent intent = new Intent(CallNotificationReceiver.ACTION_AUDIO_CALL,
+                null, context, CallNotificationReceiver.class);
+        return PendingIntent.getBroadcast(context, 0, intent, 0);
+    }
+
+    /**
+     * Adds an action to the Notification Builder for adding an incoming call through Telecom.
+     * @param builder The Notification Builder.
+     */
+    private void addAddCallAction(Notification.Builder builder, Context context) {
+        builder.addAction(0, "Add Call", createIncomingAudioCall(context));
+    }
+
+    /**
+     * Adds an action to the Notification Builder to add an incoming video call through Telecom.
+     */
+    private void addAddVideoCallAction(Notification.Builder builder, Context context) {
+        builder.addAction(0, "Add Video", createIncomingVideoCall(context));
+    }
+
+    /**
+     * Adds an action to remove the notification.
+     */
+    private void addExitAction(Notification.Builder builder, Context context) {
+        builder.addAction(0, "Exit", createExitIntent(context));
+    }
+
+    /**
+     * Adds an action to show all registered phone accounts on a device.
+     */
+    private void addShowAllPhoneAccountsAction(Notification.Builder builder, Context context) {
+        builder.addAction(0, "Show Accts", createShowAllPhoneAccountsIntent(context));
+    }
+
+    /**
+     * Adds an action to register a new phone account.
+     */
+    private void addRegisterPhoneAccountAction(Notification.Builder builder, Context context) {
+        builder.addAction(0, "Reg.Acct.", createRegisterPhoneAccountIntent(context));
+    }
+
+    public boolean shouldStartVideoCall() {
+        return mStartVideoCall;
+    }
+
+    private static void log(String msg) {
+        Log.w("testcallservice", "[CallServiceNotifier] " + msg);
+    }
+}
diff --git a/testapps/src/com/android/server/telecom/testapps/CameraThread.java b/testapps/src/com/android/server/telecom/testapps/CameraThread.java
new file mode 100644
index 0000000..fbcd6b2
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/CameraThread.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.telecom.testapps;
+
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import java.lang.AutoCloseable;
+import java.lang.Exception;
+import java.lang.Override;
+import java.lang.String;
+import java.lang.Thread;
+import java.lang.Throwable;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Camera thread class used for handling camera callbacks.
+ */
+public class CameraThread implements AutoCloseable {
+    private static final String TAG = "CameraThread";
+    private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
+
+    // Timeout for initializing looper and opening camera in Milliseconds.
+    private static final long WAIT_FOR_COMMAND_TO_COMPLETE = 5000;
+    private Looper mLooper = null;
+    private Handler mHandler = null;
+
+    /**
+     * Create and start a looper thread, return the Handler
+     */
+    public synchronized Handler start() throws Exception {
+        final ConditionVariable startDone = new ConditionVariable();
+        if (mHandler != null) {
+            Log.w(TAG, "Looper thread already started");
+            return mHandler;
+        }
+
+        new Thread() {
+            @Override
+            public void run() {
+                if (VERBOSE) Log.v(TAG, "start loopRun");
+                Looper.prepare();
+                // Save the looper so that we can terminate this thread
+                // after we are done with it.
+                mLooper = Looper.myLooper();
+                mHandler = new Handler();
+                startDone.open();
+                Looper.loop();
+                if (VERBOSE) Log.v(TAG, "createLooperThread: finished");
+            }
+        }.start();
+
+        if (VERBOSE) Log.v(TAG, "start waiting for looper");
+        if (!startDone.block(WAIT_FOR_COMMAND_TO_COMPLETE)) {
+            throw new TimeoutException("createLooperThread: start timeout");
+        }
+        return mHandler;
+    }
+
+    /**
+     * Terminate the looper thread
+     */
+    public synchronized void close() throws Exception {
+        if (mLooper == null || mHandler == null) {
+            Log.w(TAG, "Looper thread doesn't start yet");
+            return;
+        }
+
+        if (VERBOSE) Log.v(TAG, "Terminate looper thread");
+        mLooper.quit();
+        mLooper.getThread().join();
+        mLooper = null;
+        mHandler = null;
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            close();
+        } finally {
+            super.finalize();
+        }
+    }
+}
diff --git a/testapps/src/com/android/server/telecom/testapps/TestCallActivity.java b/testapps/src/com/android/server/telecom/testapps/TestCallActivity.java
new file mode 100644
index 0000000..6f4ae20
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/TestCallActivity.java
@@ -0,0 +1,60 @@
+/*
+ * 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.server.telecom.testapps;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+
+/**
+ * This activity exists in order to add an icon to the launcher. This activity has no UI of its own
+ * and instead starts the notification for {@link TestConnectionService} via
+ * {@link CallServiceNotifier}. After triggering a notification update, this activity immediately
+ * finishes.
+ *
+ * To directly trigger a new incoming call, use the following adb command:
+ *
+ * adb shell am start -a android.telecom.testapps.ACTION_START_INCOMING_CALL -d "tel:123456789"
+ */
+public class TestCallActivity extends Activity {
+
+    public static final String ACTION_NEW_INCOMING_CALL =
+            "android.telecom.testapps.ACTION_START_INCOMING_CALL";
+
+    /*
+     * Action to exercise TelecomManager.addNewUnknownCall().
+     */
+    public static final String ACTION_NEW_UNKNOWN_CALL =
+            "android.telecom.testapps.ACTION_NEW_UNKNOWN_CALL";
+
+    @Override
+    protected void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        final Intent intent = getIntent();
+        final String action = intent != null ? intent.getAction() : null;
+        final Uri data = intent != null ? intent.getData() : null;
+        if (ACTION_NEW_INCOMING_CALL.equals(action) && data != null) {
+            CallNotificationReceiver.sendIncomingCallIntent(this, data, false);
+        } else if (ACTION_NEW_UNKNOWN_CALL.equals(action) && data != null) {
+            CallNotificationReceiver.addNewUnknownCall(this, data, intent.getExtras());
+        } else {
+            CallServiceNotifier.getInstance().updateNotification(this);
+        }
+        finish();
+    }
+}
diff --git a/testapps/src/com/android/server/telecom/testapps/TestConnectionManager.java b/testapps/src/com/android/server/telecom/testapps/TestConnectionManager.java
new file mode 100644
index 0000000..a27be39
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/TestConnectionManager.java
@@ -0,0 +1,336 @@
+/*
+ * 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.server.telecom.testapps;
+
+import android.net.Uri;
+import android.telecom.AudioState;
+import android.telecom.Conference;
+import android.telecom.Connection;
+import android.telecom.ConnectionRequest;
+import android.telecom.ConnectionService;
+import android.telecom.DisconnectCause;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.RemoteConference;
+import android.telecom.RemoteConnection;
+import android.telecom.StatusHints;
+import android.telecom.VideoProfile;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 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 {
+    public final class TestManagedConnection extends Connection {
+        private final RemoteConnection.Callback mRemoteCallback = new RemoteConnection.Callback() {
+            @Override
+            public void onStateChanged(RemoteConnection connection, int state) {
+                setState(state);
+            }
+
+            @Override
+            public void onDisconnected(
+                    RemoteConnection connection, DisconnectCause disconnectCause) {
+                setDisconnected(disconnectCause);
+                destroy();
+            }
+
+            @Override
+            public void onRingbackRequested(RemoteConnection connection, boolean ringback) {
+                setRingbackRequested(ringback);
+            }
+
+            @Override
+            public void onConnectionCapabilitiesChanged(RemoteConnection connection,
+                    int connectionCapabilities) {
+                setConnectionCapabilities(connectionCapabilities);
+            }
+
+            @Override
+            public void onPostDialWait(RemoteConnection connection, String remainingDigits) {
+                setPostDialWait(remainingDigits);
+            }
+
+            @Override
+            public void onVoipAudioChanged(RemoteConnection connection, boolean isVoip) {
+                setAudioModeIsVoip(isVoip);
+            }
+
+            @Override
+            public void onStatusHintsChanged(RemoteConnection connection, StatusHints statusHints) {
+                setStatusHints(statusHints);
+            }
+
+            @Override
+            public void onVideoStateChanged(RemoteConnection connection, int videoState) {
+                if (videoState == VideoProfile.VideoState.BIDIRECTIONAL) {
+                    setVideoProvider(new TestManagedVideoProvider(connection.getVideoProvider()));
+                }
+                setVideoState(videoState);
+            }
+
+            @Override
+            public void onAddressChanged(
+                    RemoteConnection connection, Uri address, int presentation) {
+                setAddress(address, presentation);
+            }
+
+            @Override
+            public void onCallerDisplayNameChanged(
+                    RemoteConnection connection, String callerDisplayName, int presentation) {
+                setCallerDisplayName(callerDisplayName, presentation);
+            }
+
+            @Override
+            public void onDestroyed(RemoteConnection connection) {
+                destroy();
+                mManagedConnectionByRemote.remove(mRemote);
+            }
+
+            @Override
+            public void onConferenceableConnectionsChanged(
+                    RemoteConnection connect,
+                    List<RemoteConnection> conferenceable) {
+                List<Connection> c = new ArrayList<>();
+                for (RemoteConnection remote : conferenceable) {
+                    if (mManagedConnectionByRemote.containsKey(remote)) {
+                        c.add(mManagedConnectionByRemote.get(remote));
+                    }
+                }
+                setConferenceableConnections(c);
+            }
+        };
+
+        private final RemoteConnection mRemote;
+        private final boolean mIsIncoming;
+
+        TestManagedConnection(RemoteConnection remote, boolean isIncoming) {
+            mRemote = remote;
+            mIsIncoming = isIncoming;
+            mRemote.registerCallback(mRemoteCallback);
+            setState(mRemote.getState());
+            setVideoState(mRemote.getVideoState());
+        }
+
+        @Override
+        public void onAbort() {
+            mRemote.abort();
+        }
+
+        /** ${inheritDoc} */
+        @Override
+        public void onAnswer(int videoState) {
+            mRemote.answer(videoState);
+        }
+
+        /** ${inheritDoc} */
+        @Override
+        public void onDisconnect() {
+            mRemote.disconnect();
+        }
+
+        @Override
+        public void onPlayDtmfTone(char c) {
+            mRemote.playDtmfTone(c);
+        }
+
+        /** ${inheritDoc} */
+        @Override
+        public void onHold() {
+            mRemote.hold();
+        }
+
+        /** ${inheritDoc} */
+        @Override
+        public void onReject() {
+            mRemote.reject();
+        }
+
+        /** ${inheritDoc} */
+        @Override
+        public void onUnhold() {
+            mRemote.unhold();
+        }
+
+        @Override
+        public void onAudioStateChanged(AudioState state) {
+            mRemote.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;
+            }
+        }
+    }
+
+    public final class TestManagedConference extends Conference {
+        private final RemoteConference.Callback mRemoteCallback = new RemoteConference.Callback() {
+            @Override
+            public void onStateChanged(RemoteConference conference, int oldState, int newState) {
+                switch (newState) {
+                    case Connection.STATE_DISCONNECTED:
+                        // See onDisconnected below
+                        break;
+                    case Connection.STATE_HOLDING:
+                        setOnHold();
+                        break;
+                    case Connection.STATE_ACTIVE:
+                        setActive();
+                        break;
+                    default:
+                        log("unrecognized state for Conference: " + newState);
+                        break;
+                }
+            }
+
+            @Override
+            public void onDisconnected(RemoteConference conference,
+                    DisconnectCause disconnectCause) {
+                setDisconnected(disconnectCause);
+            }
+
+            @Override
+            public void onConnectionAdded(
+                    RemoteConference conference,
+                    RemoteConnection connection) {
+                TestManagedConnection c = mManagedConnectionByRemote.get(connection);
+                if (c == null) {
+                    log("onConnectionAdded cannot find remote connection: " + connection);
+                } else {
+                    addConnection(c);
+                }
+            }
+
+            @Override
+            public void onConnectionRemoved(
+                    RemoteConference conference,
+                    RemoteConnection connection) {
+                TestManagedConnection c = mManagedConnectionByRemote.get(connection);
+                if (c == null) {
+                    log("onConnectionRemoved cannot find remote connection: " + connection);
+                } else {
+                    removeConnection(c);
+                }
+            }
+
+            @Override
+            public void onConnectionCapabilitiesChanged(RemoteConference conference,
+                    int connectionCapabilities) {
+                setConnectionCapabilities(connectionCapabilities);
+            }
+
+            @Override
+            public void onDestroyed(RemoteConference conference) {
+                destroy();
+                mRemote.unregisterCallback(mRemoteCallback);
+                mManagedConferenceByRemote.remove(mRemote);
+            }
+
+        };
+
+        @Override
+        public void onPlayDtmfTone(char c) {
+            mRemote.playDtmfTone(c);
+        };
+
+        @Override
+        public void onStopDtmfTone() {
+            mRemote.stopDtmfTone();
+        };
+
+        private final RemoteConference mRemote;
+
+        public TestManagedConference(RemoteConference remote) {
+            super(null);
+            mRemote = remote;
+            remote.registerCallback(mRemoteCallback);
+            setActive();
+            for (RemoteConnection r : remote.getConnections()) {
+                TestManagedConnection c = mManagedConnectionByRemote.get(r);
+                if (c != null) {
+                    addConnection(c);
+                }
+            }
+        }
+    }
+
+    static void log(String msg) {
+        Log.w("telecomtestcs", "[TestConnectionManager] " + msg);
+    }
+
+    private final Map<RemoteConference, TestManagedConference> mManagedConferenceByRemote
+            = new HashMap<>();
+    private final Map<RemoteConnection, TestManagedConnection> mManagedConnectionByRemote
+            = new HashMap<>();
+
+    @Override
+    public Connection onCreateOutgoingConnection(
+            PhoneAccountHandle connectionManagerAccount,
+            final ConnectionRequest request) {
+        return makeConnection(request, false);
+    }
+
+    @Override
+    public Connection onCreateIncomingConnection(
+            PhoneAccountHandle connectionManagerAccount,
+            final ConnectionRequest request) {
+        return makeConnection(request, true);
+    }
+
+    @Override
+    public void onConference(Connection a, Connection b) {
+        conferenceRemoteConnections(
+                ((TestManagedConnection) a).mRemote,
+                ((TestManagedConnection) b).mRemote);
+    }
+
+    @Override
+    public void onRemoteConferenceAdded(RemoteConference remoteConference) {
+        addConference(new TestManagedConference(remoteConference));
+    }
+
+    Map<RemoteConnection, TestManagedConnection> getManagedConnectionByRemote() {
+        return mManagedConnectionByRemote;
+    }
+
+    private Connection makeConnection(ConnectionRequest request, boolean incoming) {
+        RemoteConnection remote = incoming
+                ? createRemoteIncomingConnection(request.getAccountHandle(), request)
+                : createRemoteOutgoingConnection(request.getAccountHandle(), request);
+        TestManagedConnection local = new TestManagedConnection(remote, false);
+        mManagedConnectionByRemote.put(remote, local);
+        return local;
+    }
+}
diff --git a/testapps/src/com/android/server/telecom/testapps/TestConnectionService.java b/testapps/src/com/android/server/telecom/testapps/TestConnectionService.java
new file mode 100644
index 0000000..d71ef9d
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/TestConnectionService.java
@@ -0,0 +1,440 @@
+/*
+ * 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.server.telecom.testapps;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.telecom.AudioState;
+import android.telecom.Conference;
+import android.telecom.Connection;
+import android.telecom.DisconnectCause;
+import android.telecom.PhoneAccount;
+import android.telecom.ConnectionRequest;
+import android.telecom.ConnectionService;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
+import android.util.Log;
+
+import com.android.server.telecom.testapps.R;
+
+import java.lang.String;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * Service which provides fake calls to test the ConnectionService interface.
+ * TODO: Rename all classes in the directory to Dummy* (e.g., DummyConnectionService).
+ */
+public class TestConnectionService extends ConnectionService {
+    /**
+     * Intent extra used to pass along whether a call is video or audio based on the user's choice
+     * in the notification.
+     */
+    public static final String EXTRA_IS_VIDEO_CALL = "extra_is_video_call";
+
+    public static final String EXTRA_HANDLE = "extra_handle";
+
+    /**
+     * Random number generator used to generate phone numbers.
+     */
+    private Random mRandom = new Random();
+
+    private final class TestConference extends Conference {
+
+        private final Connection.Listener mConnectionListener = new Connection.Listener() {
+            @Override
+            public void onDestroyed(Connection c) {
+                removeConnection(c);
+                if (getConnections().size() == 0) {
+                    setDisconnected(new DisconnectCause(DisconnectCause.REMOTE));
+                    destroy();
+                }
+            }
+        };
+
+        public TestConference(Connection a, Connection b) {
+            super(null);
+            setConnectionCapabilities(
+                    Connection.CAPABILITY_SUPPORT_HOLD |
+                    Connection.CAPABILITY_HOLD |
+                    Connection.CAPABILITY_MUTE |
+                    Connection.CAPABILITY_MANAGE_CONFERENCE);
+            addConnection(a);
+            addConnection(b);
+
+            a.addConnectionListener(mConnectionListener);
+            b.addConnectionListener(mConnectionListener);
+
+            a.setConference(this);
+            b.setConference(this);
+
+            setActive();
+        }
+
+        @Override
+        public void onDisconnect() {
+            for (Connection c : getConnections()) {
+                c.setDisconnected(new DisconnectCause(DisconnectCause.REMOTE));
+                c.destroy();
+            }
+        }
+
+        @Override
+        public void onSeparate(Connection connection) {
+            if (getConnections().contains(connection)) {
+                connection.setConference(null);
+                removeConnection(connection);
+                connection.removeConnectionListener(mConnectionListener);
+            }
+        }
+
+        @Override
+        public void onHold() {
+            for (Connection c : getConnections()) {
+                c.setOnHold();
+            }
+            setOnHold();
+        }
+
+        @Override
+        public void onUnhold() {
+            for (Connection c : getConnections()) {
+                c.setActive();
+            }
+            setActive();
+        }
+    }
+
+    private final class TestConnection extends Connection {
+        private final boolean mIsIncoming;
+
+        /** Used to cleanup camera and media when done with connection. */
+        private TestVideoProvider mTestVideoCallProvider;
+
+        TestConnection(boolean isIncoming) {
+            mIsIncoming = isIncoming;
+            // Assume all calls are video capable.
+            int capabilities = getConnectionCapabilities();
+            capabilities |= CAPABILITY_SUPPORTS_VT_LOCAL;
+            capabilities |= CAPABILITY_MUTE;
+            capabilities |= CAPABILITY_SUPPORT_HOLD;
+            capabilities |= CAPABILITY_HOLD;
+            setConnectionCapabilities(capabilities);
+        }
+
+        void startOutgoing() {
+            setDialing();
+            mHandler.postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    setActive();
+                    activateCall(TestConnection.this);
+                }
+            }, 4000);
+        }
+
+        /** ${inheritDoc} */
+        @Override
+        public void onAbort() {
+            destroyCall(this);
+            destroy();
+        }
+
+        /** ${inheritDoc} */
+        @Override
+        public void onAnswer(int videoState) {
+            setVideoState(videoState);
+            activateCall(this);
+            setActive();
+            updateConferenceable();
+        }
+
+        /** ${inheritDoc} */
+        @Override
+        public void onPlayDtmfTone(char c) {
+            if (c == '1') {
+                setDialing();
+            }
+        }
+
+        /** ${inheritDoc} */
+        @Override
+        public void onStopDtmfTone() { }
+
+        /** ${inheritDoc} */
+        @Override
+        public void onDisconnect() {
+            setDisconnected(new DisconnectCause(DisconnectCause.REMOTE));
+            destroyCall(this);
+            destroy();
+        }
+
+        /** ${inheritDoc} */
+        @Override
+        public void onHold() {
+            setOnHold();
+        }
+
+        /** ${inheritDoc} */
+        @Override
+        public void onReject() {
+            setDisconnected(new DisconnectCause(DisconnectCause.REJECTED));
+            destroyCall(this);
+            destroy();
+        }
+
+        /** ${inheritDoc} */
+        @Override
+        public void onUnhold() {
+            setActive();
+        }
+
+        @Override
+        public void onAudioStateChanged(AudioState state) { }
+
+        public void setTestVideoCallProvider(TestVideoProvider testVideoCallProvider) {
+            mTestVideoCallProvider = testVideoCallProvider;
+        }
+
+        /**
+         * Stops playback of test videos.
+         */
+        private void stopAndCleanupMedia() {
+            if (mTestVideoCallProvider != null) {
+                mTestVideoCallProvider.stopAndCleanupMedia();
+                mTestVideoCallProvider.stopCamera();
+            }
+        }
+    }
+
+    private final List<TestConnection> mCalls = new ArrayList<>();
+    private final Handler mHandler = new Handler();
+
+    /** Used to play an audio tone during a call. */
+    private MediaPlayer mMediaPlayer;
+
+    @Override
+    public boolean onUnbind(Intent intent) {
+        log("onUnbind");
+        mMediaPlayer = null;
+        return super.onUnbind(intent);
+    }
+
+    @Override
+    public void onConference(Connection a, Connection b) {
+        addConference(new TestConference(a, b));
+    }
+
+    @Override
+    public Connection onCreateOutgoingConnection(
+            PhoneAccountHandle connectionManagerAccount,
+            final ConnectionRequest originalRequest) {
+
+        final Uri handle = originalRequest.getAddress();
+        String number = originalRequest.getAddress().getSchemeSpecificPart();
+        log("call, number: " + number);
+
+        // Crash on 555-DEAD to test call service crashing.
+        if ("5550340".equals(number)) {
+            throw new RuntimeException("Goodbye, cruel world.");
+        }
+
+        Bundle extras = originalRequest.getExtras();
+        String gatewayPackage = extras.getString(TelecomManager.GATEWAY_PROVIDER_PACKAGE);
+        Uri originalHandle = extras.getParcelable(TelecomManager.GATEWAY_ORIGINAL_ADDRESS);
+
+        log("gateway package [" + gatewayPackage + "], original handle [" +
+                originalHandle + "]");
+
+        final TestConnection connection = new TestConnection(false /* isIncoming */);
+        connection.setAddress(handle, TelecomManager.PRESENTATION_ALLOWED);
+
+        // If the number starts with 555, then we handle it ourselves. If not, then we
+        // use a remote connection service.
+        // 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.
+            final ConnectionRequest request = new ConnectionRequest(
+                    originalRequest.getAccountHandle(),
+                    Uri.fromParts(handle.getScheme(),
+                    handle.getSchemeSpecificPart() + "..", ""),
+                    originalRequest.getExtras(),
+                    originalRequest.getVideoState());
+
+            addCall(connection);
+            connection.startOutgoing();
+
+            for (Connection c : getAllConnections()) {
+                c.setOnHold();
+            }
+        } else {
+            log("Not a test number");
+        }
+        return connection;
+    }
+
+    @Override
+    public Connection onCreateIncomingConnection(
+            PhoneAccountHandle connectionManagerAccount,
+            final ConnectionRequest request) {
+        PhoneAccountHandle accountHandle = request.getAccountHandle();
+        ComponentName componentName = new ComponentName(this, TestConnectionService.class);
+
+        if (accountHandle != null && componentName.equals(accountHandle.getComponentName())) {
+            final TestConnection connection = new TestConnection(true);
+            // Get the stashed intent extra that determines if this is a video call or audio call.
+            Bundle extras = request.getExtras();
+            boolean isVideoCall = extras.getBoolean(EXTRA_IS_VIDEO_CALL);
+            Uri providedHandle = extras.getParcelable(EXTRA_HANDLE);
+
+            // Use dummy number for testing incoming calls.
+            Uri address = providedHandle == null ?
+                    Uri.fromParts(PhoneAccount.SCHEME_TEL, getDummyNumber(isVideoCall), null)
+                    : providedHandle;
+            if (isVideoCall) {
+                TestVideoProvider testVideoCallProvider =
+                        new TestVideoProvider(getApplicationContext());
+                connection.setVideoProvider(testVideoCallProvider);
+
+                // Keep reference to original so we can clean up the media players later.
+                connection.setTestVideoCallProvider(testVideoCallProvider);
+            }
+
+            int videoState = isVideoCall ?
+                    VideoProfile.VideoState.BIDIRECTIONAL :
+                    VideoProfile.VideoState.AUDIO_ONLY;
+            connection.setVideoState(videoState);
+            connection.setAddress(address, TelecomManager.PRESENTATION_ALLOWED);
+
+            addCall(connection);
+
+            ConnectionRequest newRequest = new ConnectionRequest(
+                    request.getAccountHandle(),
+                    address,
+                    request.getExtras(),
+                    videoState);
+            connection.setVideoState(videoState);
+            return connection;
+        } else {
+            return Connection.createFailedConnection(new DisconnectCause(DisconnectCause.ERROR,
+                    "Invalid inputs: " + accountHandle + " " + componentName));
+        }
+    }
+
+    @Override
+    public Connection onCreateUnknownConnection(PhoneAccountHandle connectionManagerPhoneAccount,
+            final ConnectionRequest request) {
+        PhoneAccountHandle accountHandle = request.getAccountHandle();
+        ComponentName componentName = new ComponentName(this, TestConnectionService.class);
+        if (accountHandle != null && componentName.equals(accountHandle.getComponentName())) {
+            final TestConnection connection = new TestConnection(false);
+            final Bundle extras = request.getExtras();
+            final Uri providedHandle = extras.getParcelable(EXTRA_HANDLE);
+
+            Uri handle = providedHandle == null ?
+                    Uri.fromParts(PhoneAccount.SCHEME_TEL, getDummyNumber(false), null)
+                    : providedHandle;
+
+            connection.setAddress(handle,  TelecomManager.PRESENTATION_ALLOWED);
+            connection.setDialing();
+
+            addCall(connection);
+            return connection;
+        } else {
+            return Connection.createFailedConnection(new DisconnectCause(DisconnectCause.ERROR,
+                    "Invalid inputs: " + accountHandle + " " + componentName));
+        }
+    }
+
+    private void activateCall(TestConnection connection) {
+        if (mMediaPlayer == null) {
+            mMediaPlayer = createMediaPlayer();
+        }
+        if (!mMediaPlayer.isPlaying()) {
+            mMediaPlayer.start();
+        }
+    }
+
+    private void destroyCall(TestConnection connection) {
+        mCalls.remove(connection);
+
+        // Ensure any playing media and camera resources are released.
+        connection.stopAndCleanupMedia();
+
+        // Stops audio if there are no more calls.
+        if (mCalls.isEmpty() && mMediaPlayer != null && mMediaPlayer.isPlaying()) {
+            mMediaPlayer.stop();
+            mMediaPlayer.release();
+            mMediaPlayer = createMediaPlayer();
+        }
+
+        updateConferenceable();
+    }
+
+    private void addCall(TestConnection connection) {
+        mCalls.add(connection);
+        updateConferenceable();
+    }
+
+    private void updateConferenceable() {
+        List<Connection> freeConnections = new ArrayList<>();
+        freeConnections.addAll(mCalls);
+        for (int i = 0; i < freeConnections.size(); i++) {
+            if (freeConnections.get(i).getConference() != null) {
+                freeConnections.remove(i);
+            }
+        }
+        for (int i = 0; i < freeConnections.size(); i++) {
+            Connection c = freeConnections.remove(i);
+            c.setConferenceableConnections(freeConnections);
+            freeConnections.add(i, c);
+        }
+    }
+
+    private MediaPlayer createMediaPlayer() {
+        // Prepare the media player to play a tone when there is a call.
+        MediaPlayer mediaPlayer = MediaPlayer.create(getApplicationContext(), R.raw.beep_boop);
+        mediaPlayer.setLooping(true);
+        return mediaPlayer;
+    }
+
+    private static void log(String msg) {
+        Log.w("telecomtestcs", "[TestConnectionService] " + msg);
+    }
+
+    /**
+     * Generates a random phone number of format 555YXXX.  Where Y will be {@code 1} if the
+     * phone number is for a video call and {@code 0} for an audio call.  XXX is a randomly
+     * generated phone number.
+     *
+     * @param isVideo {@code True} if the call is a video call.
+     * @return The phone number.
+     */
+    private String getDummyNumber(boolean isVideo) {
+        int videoDigit = isVideo ? 1 : 0;
+        int number = mRandom.nextInt(999);
+        return String.format("555%s%03d", videoDigit, number);
+    }
+}
+
diff --git a/testapps/src/com/android/server/telecom/testapps/TestDialerActivity.java b/testapps/src/com/android/server/telecom/testapps/TestDialerActivity.java
new file mode 100644
index 0000000..71c375a
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/TestDialerActivity.java
@@ -0,0 +1,56 @@
+package com.android.server.telecom.testapps;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.EditText;
+
+import com.android.server.telecom.testapps.R;
+
+public class TestDialerActivity extends Activity {
+    private EditText mNumberView;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.testdialer_main);
+        findViewById(R.id.set_default_button).setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                setDefault();
+            }
+        });
+        findViewById(R.id.place_call_button).setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                placeCall();
+            }
+        });
+
+        mNumberView = (EditText) findViewById(R.id.number);
+        updateEditTextWithNumber();
+    }
+
+    @Override
+    protected void onNewIntent(Intent intent) {
+        super.onNewIntent(intent);
+        updateEditTextWithNumber();
+    }
+
+    private void updateEditTextWithNumber() {
+        Intent intent = getIntent();
+        if (intent != null) {
+            mNumberView.setText(intent.getDataString());
+        }
+    }
+
+    private void setDefault() {
+        // TODO: Send a request to become the default dialer application
+    }
+
+    private void placeCall() {
+        // TODO: Place a call with the number entered in the number field
+    }
+}
diff --git a/testapps/src/com/android/server/telecom/testapps/TestInCallServiceImpl.java b/testapps/src/com/android/server/telecom/testapps/TestInCallServiceImpl.java
new file mode 100644
index 0000000..1d7e805
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/TestInCallServiceImpl.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.telecom.testapps;
+
+import android.telecom.InCallService;
+import android.telecom.Phone;
+import android.util.Log;
+
+import java.lang.Override;
+import java.lang.String;
+
+/**
+ * Test In-Call service implementation.  Logs incoming events.  Mainly used to test binding to
+ * multiple {@link InCallService} implementations.
+ */
+public class TestInCallServiceImpl extends InCallService {
+    private static final String TAG = "TestInCallServiceImpl";
+
+    private Phone mPhone;
+
+    private Phone.Listener mPhoneListener = new Phone.Listener() {
+        @Override
+        public void onCallAdded(Phone phone, android.telecom.Call call) {
+            Log.i(TAG, "onCallAdded: "+call.toString());
+        }
+        @Override
+        public void onCallRemoved(Phone phone, android.telecom.Call call) {
+            Log.i(TAG, "onCallRemoved: "+call.toString());
+        }
+    };
+
+    @Override
+    public void onPhoneCreated(Phone phone) {
+        Log.i(TAG, "onPhoneCreated");
+        mPhone = phone;
+        mPhone.addListener(mPhoneListener);
+
+    }
+
+    @Override
+    public void onPhoneDestroyed(Phone phone) {
+        Log.i(TAG, "onPhoneDestroyed");
+        mPhone.removeListener(mPhoneListener);
+        mPhone = null;
+    }
+}
diff --git a/testapps/src/com/android/server/telecom/testapps/TestManagedVideoProvider.java b/testapps/src/com/android/server/telecom/testapps/TestManagedVideoProvider.java
new file mode 100644
index 0000000..649d0c0
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/TestManagedVideoProvider.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.testapps;
+
+import android.telecom.CameraCapabilities;
+import android.telecom.Connection;
+import android.telecom.RemoteConnection;
+import android.telecom.VideoProfile;
+import android.view.Surface;
+
+public class TestManagedVideoProvider extends Connection.VideoProvider {
+
+    private final RemoteConnection.VideoProvider.Listener mRemoteListener =
+            new RemoteConnection.VideoProvider.Listener() {
+        @Override
+        public void onReceiveSessionModifyRequest(RemoteConnection.VideoProvider rvp,
+                VideoProfile videoProfile) {
+            super.onReceiveSessionModifyRequest(rvp, videoProfile);
+        }
+
+        @Override
+        public void onReceiveSessionModifyResponse(RemoteConnection.VideoProvider rvp,
+        int status,
+        VideoProfile requestedProfile, VideoProfile responseProfile) {
+            super.onReceiveSessionModifyResponse(rvp, status, requestedProfile,
+                    responseProfile);
+        }
+
+        @Override
+        public void onHandleCallSessionEvent(RemoteConnection.VideoProvider rvp, int event) {
+            super.onHandleCallSessionEvent(rvp, event);
+        }
+
+        @Override
+        public void onPeerDimensionsChanged(RemoteConnection.VideoProvider rvp, int width,
+        int height) {
+            super.onPeerDimensionsChanged(rvp, width, height);
+        }
+
+        @Override
+        public void onCallDataUsageChanged(RemoteConnection.VideoProvider rvp, int dataUsage) {
+            super.onCallDataUsageChanged(rvp, dataUsage);
+        }
+
+        @Override
+        public void onCameraCapabilitiesChanged(RemoteConnection.VideoProvider rvp,
+                CameraCapabilities cameraCapabilities) {
+            super.onCameraCapabilitiesChanged(rvp, cameraCapabilities);
+        }
+    };
+
+    private final RemoteConnection.VideoProvider mRemoteVideoProvider;
+
+    public TestManagedVideoProvider(RemoteConnection.VideoProvider remoteVideoProvider) {
+        mRemoteVideoProvider = remoteVideoProvider;
+        mRemoteVideoProvider.addListener(mRemoteListener);
+    }
+
+    @Override
+    public void onSetCamera(String cameraId) {
+        mRemoteVideoProvider.setCamera(cameraId);
+    }
+
+    @Override
+    public void onSetPreviewSurface(Surface surface) {
+        mRemoteVideoProvider.setPreviewSurface(surface);
+    }
+
+    @Override
+    public void onSetDisplaySurface(Surface surface) {
+        mRemoteVideoProvider.setDisplaySurface(surface);
+    }
+
+    @Override
+    public void onSetDeviceOrientation(int rotation) {
+        mRemoteVideoProvider.setDeviceOrientation(rotation);
+    }
+
+    @Override
+    public void onSetZoom(float value) {
+        mRemoteVideoProvider.setZoom(value);
+    }
+
+    @Override
+    public void onSendSessionModifyRequest(VideoProfile requestProfile) {
+        mRemoteVideoProvider.sendSessionModifyRequest(requestProfile);
+    }
+
+    @Override
+    public void onSendSessionModifyResponse(VideoProfile responseProfile) {
+        mRemoteVideoProvider.sendSessionModifyResponse(responseProfile);
+    }
+
+    @Override
+    public void onRequestCameraCapabilities() {
+        mRemoteVideoProvider.requestCameraCapabilities();
+    }
+
+    @Override
+    public void onRequestConnectionDataUsage() {
+        mRemoteVideoProvider.requestCallDataUsage();
+    }
+
+    @Override
+    public void onSetPauseImage(String uri) {
+        mRemoteVideoProvider.setPauseImage(uri);
+    }
+}
diff --git a/testapps/src/com/android/server/telecom/testapps/TestVideoProvider.java b/testapps/src/com/android/server/telecom/testapps/TestVideoProvider.java
new file mode 100644
index 0000000..d372e46
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/TestVideoProvider.java
@@ -0,0 +1,334 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.telecom.testapps;
+
+import com.android.ex.camera2.blocking.BlockingCameraManager;
+import com.android.ex.camera2.blocking.BlockingCameraManager.BlockingOpenException;
+import com.android.ex.camera2.blocking.BlockingSessionCallback;
+import com.android.server.telecom.testapps.R;
+
+import android.content.Context;
+import android.graphics.SurfaceTexture;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraManager;
+import android.hardware.camera2.CaptureFailure;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.TotalCaptureResult;
+import android.hardware.camera2.params.StreamConfigurationMap;
+import android.media.MediaPlayer;
+import android.os.Handler;
+import android.telecom.CameraCapabilities;
+import android.telecom.Connection;
+import android.telecom.VideoProfile;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Size;
+import android.view.Surface;
+
+import java.lang.IllegalArgumentException;
+import java.lang.String;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * Implements the VideoCallProvider.
+ */
+public class TestVideoProvider extends Connection.VideoProvider {
+    private CameraCapabilities mCameraCapabilities;
+    private Random random;
+    private Surface mDisplaySurface;
+    private Surface mPreviewSurface;
+    private Context mContext;
+    /** Used to play incoming video during a call. */
+    private MediaPlayer mIncomingMediaPlayer;
+
+    private CameraManager mCameraManager;
+    private CameraDevice mCameraDevice;
+    private CameraCaptureSession mCameraSession;
+    private CameraThread mLooperThread;
+
+    private String mCameraId;
+
+    private static final long SESSION_TIMEOUT_MS = 2000;
+
+    public TestVideoProvider(Context context) {
+        mContext = context;
+        random = new Random();
+        mCameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
+    }
+
+    @Override
+    public void onSetCamera(String cameraId) {
+        log("Set camera to " + cameraId);
+        mCameraId = cameraId;
+
+        stopCamera();
+        // Get the capabilities of the camera
+        setCameraCapabilities(mCameraId);
+    }
+
+    @Override
+    public void onSetPreviewSurface(Surface surface) {
+        log("Set preview surface " + (surface == null ? "unset" : "set"));
+        if (mPreviewSurface != null) {
+            stopCamera();
+        }
+
+        mPreviewSurface = surface;
+
+        if (!TextUtils.isEmpty(mCameraId) && mPreviewSurface != null) {
+            startCamera(mCameraId);
+        }
+    }
+
+    @Override
+    public void onSetDisplaySurface(Surface surface) {
+        log("Set display surface " + (surface == null ? "unset" : "set"));
+        mDisplaySurface = surface;
+
+        if (mDisplaySurface != null) {
+            if (mIncomingMediaPlayer == null) {
+                // For a Rick-Rolling good time use R.raw.test_video
+                mIncomingMediaPlayer = createMediaPlayer(mDisplaySurface, R.raw.test_pattern);
+            }
+            mIncomingMediaPlayer.setSurface(mDisplaySurface);
+            if (!mIncomingMediaPlayer.isPlaying()) {
+                mIncomingMediaPlayer.start();
+            }
+        } else {
+            if (mIncomingMediaPlayer != null) {
+                mIncomingMediaPlayer.stop();
+                mIncomingMediaPlayer.setSurface(null);
+            }
+        }
+    }
+
+    @Override
+    public void onSetDeviceOrientation(int rotation) {
+        log("Set device orientation " + rotation);
+    }
+
+    /**
+     * Sets the zoom value, creating a new CallCameraCapabalities object. If the zoom value is
+     * non-positive, assume that zoom is not supported.
+     */
+    @Override
+    public void onSetZoom(float value) {
+        log("Set zoom to " + value);
+    }
+
+    /**
+     * "Sends" a request with a video call profile. Assumes that this response succeeds and sends
+     * the response back via the CallVideoClient.
+     */
+    @Override
+    public void onSendSessionModifyRequest(VideoProfile requestProfile) {
+        log("Sent session modify request");
+
+        VideoProfile responseProfile = new VideoProfile(
+                requestProfile.getVideoState(), requestProfile.getQuality());
+        receiveSessionModifyResponse(
+                SESSION_MODIFY_REQUEST_SUCCESS,
+                requestProfile,
+                responseProfile);
+    }
+
+    @Override
+    public void onSendSessionModifyResponse(VideoProfile responseProfile) {
+
+    }
+
+    /**
+     * Returns a CallCameraCapabilities object without supporting zoom.
+     */
+    @Override
+    public void onRequestCameraCapabilities() {
+        log("Requested camera capabilities");
+        changeCameraCapabilities(mCameraCapabilities);
+    }
+
+    /**
+     * Randomly reports data usage of value ranging from 10MB to 60MB.
+     */
+    @Override
+    public void onRequestConnectionDataUsage() {
+        log("Requested connection data usage");
+        int dataUsageKb = (10 *1024) + random.nextInt(50 * 1024);
+        changeCallDataUsage(dataUsageKb);
+    }
+
+    /**
+     * We do not have a need to set a paused image.
+     */
+    @Override
+    public void onSetPauseImage(String uri) {
+        // Not implemented.
+    }
+
+    /**
+     * Stop and cleanup the media players used for test video playback.
+     */
+    public void stopAndCleanupMedia() {
+        if (mIncomingMediaPlayer != null) {
+            mIncomingMediaPlayer.setSurface(null);
+            mIncomingMediaPlayer.stop();
+            mIncomingMediaPlayer.release();
+            mIncomingMediaPlayer = null;
+        }
+    }
+
+    private static void log(String msg) {
+        Log.w("TestCallVideoProvider", "[TestCallServiceProvider] " + msg);
+    }
+
+    /**
+     * Creates a media player to play a video resource on a surface.
+     * @param surface The surface.
+     * @param videoResource The video resource.
+     * @return The {@code MediaPlayer}.
+     */
+    private MediaPlayer createMediaPlayer(Surface surface, int videoResource) {
+        MediaPlayer mediaPlayer = MediaPlayer.create(mContext.getApplicationContext(),
+                videoResource);
+        mediaPlayer.setSurface(surface);
+        mediaPlayer.setLooping(true);
+        return mediaPlayer;
+    }
+
+    /**
+     * Starts displaying the camera image on the preview surface.
+     *
+     * @param cameraId
+     */
+    private void startCamera(String cameraId) {
+        stopCamera();
+
+        if (mPreviewSurface == null) {
+            return;
+        }
+
+        // Configure a looper thread.
+        mLooperThread = new CameraThread();
+        Handler mHandler;
+        try {
+            mHandler = mLooperThread.start();
+        } catch (Exception e) {
+            log("Exception: " + e);
+            return;
+        }
+
+        // Get the camera device.
+        try {
+            BlockingCameraManager blockingCameraManager = new BlockingCameraManager(mCameraManager);
+            mCameraDevice = blockingCameraManager.openCamera(cameraId, null /* listener */,
+                    mHandler);
+        } catch (CameraAccessException e) {
+            log("CameraAccessException: " + e);
+            return;
+        } catch (BlockingOpenException be) {
+            log("BlockingOpenException: " + be);
+            return;
+        }
+
+        // Create a capture session to get the preview and display it on the surface.
+        List<Surface> surfaces = new ArrayList<Surface>();
+        surfaces.add(mPreviewSurface);
+        CaptureRequest.Builder mCaptureRequest = null;
+        try {
+            BlockingSessionCallback blkSession = new BlockingSessionCallback();
+            mCameraDevice.createCaptureSession(surfaces, blkSession, mHandler);
+            mCaptureRequest = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
+            mCaptureRequest.addTarget(mPreviewSurface);
+            mCameraSession = blkSession.waitAndGetSession(SESSION_TIMEOUT_MS);
+        } catch (CameraAccessException e) {
+            log("CameraAccessException: " + e);
+            return;
+        }
+
+        // Keep repeating
+        try {
+            mCameraSession.setRepeatingRequest(mCaptureRequest.build(), new CameraCaptureCallback(),
+                    mHandler);
+        } catch (CameraAccessException e) {
+            log("CameraAccessException: " + e);
+            return;
+        }
+    }
+
+    /**
+     * Stops the camera and looper thread.
+     */
+    public void stopCamera() {
+        try {
+            if (mCameraDevice != null) {
+                mCameraDevice.close();
+                mCameraDevice = null;
+            }
+            if (mLooperThread != null) {
+                mLooperThread.close();
+                mLooperThread = null;
+            }
+        } catch (Exception e) {
+           log("stopCamera Exception: "+e.toString());
+        }
+    }
+
+    /**
+     * Required listener for camera capture events.
+     */
+    private class CameraCaptureCallback extends CameraCaptureSession.CaptureCallback {
+        @Override
+        public void onCaptureCompleted(CameraCaptureSession camera, CaptureRequest request,
+                TotalCaptureResult result) {
+        }
+
+        @Override
+        public void onCaptureFailed(CameraCaptureSession camera, CaptureRequest request,
+                CaptureFailure failure) {
+        }
+    }
+
+    /**
+     * Uses the camera manager to retrieve the camera capabilities for the chosen camera.
+     *
+     * @param cameraId The camera ID to get the capabilities for.
+     */
+    private void setCameraCapabilities(String cameraId) {
+        CameraManager cameraManager = (CameraManager) mContext.getSystemService(
+                Context.CAMERA_SERVICE);
+
+        CameraCharacteristics c = null;
+        try {
+            c = cameraManager.getCameraCharacteristics(cameraId);
+        } catch (IllegalArgumentException | CameraAccessException e) {
+            // Ignoring camera problems.
+        }
+        if (c != null) {
+            // Get the video size for the camera
+            StreamConfigurationMap map = c.get(
+                    CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
+            Size previewSize = map.getOutputSizes(SurfaceTexture.class)[0];
+
+            mCameraCapabilities = new CameraCapabilities(true, 1.0f, previewSize.getWidth(),
+                    previewSize.getHeight());
+        }
+    }
+}