Add Telecom CTS tests

Move existing Telecom XTS tests into CTS tests.
Update CTS test semantics to reflect that we no longer have certain
system permissions.
Update tests to no longer require public API, and build against the
current SDK.

Bug: 20303674

Change-Id: I45c74b475bf348ac1da7d7c259f761d9da5a05ee
diff --git a/CtsTestCaseList.mk b/CtsTestCaseList.mk
index 4035af7b..956502e 100644
--- a/CtsTestCaseList.mk
+++ b/CtsTestCaseList.mk
@@ -162,6 +162,7 @@
     CtsSecurityTestCases \
     CtsSignatureTestCases \
     CtsSpeechTestCases \
+    CtsTelecomTestCases \
     CtsTelephonyTestCases \
     CtsTextTestCases \
     CtsTextureViewTestCases \
diff --git a/tests/tests/telecom/Android.mk b/tests/tests/telecom/Android.mk
new file mode 100644
index 0000000..51d97f5
--- /dev/null
+++ b/tests/tests/telecom/Android.mk
@@ -0,0 +1,33 @@
+# Copyright (C) 2015 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_PACKAGE_NAME := CtsTelecomTestCases
+
+# Don't include this package in any target.
+LOCAL_MODULE_TAGS := optional
+
+# When built, explicitly put it in the data partition.
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
+
+LOCAL_STATIC_JAVA_LIBRARIES := ctstestrunner
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_CTS_PACKAGE)
diff --git a/tests/tests/telecom/AndroidManifest.xml b/tests/tests/telecom/AndroidManifest.xml
new file mode 100644
index 0000000..91c22f1
--- /dev/null
+++ b/tests/tests/telecom/AndroidManifest.xml
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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"
+    package="com.android.cts.telecom">
+    <uses-sdk android:minSdkVersion="21" />
+    <uses-permission android:name="android.permission.CALL_PHONE" />>
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
+    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+    <application>
+        <uses-library android:name="android.test.runner" />
+
+        <service android:name="android.telecom.cts.MockConnectionService"
+            android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE" >
+            <intent-filter>
+                <action android:name="android.telecom.ConnectionService" />
+            </intent-filter>
+        </service>
+
+        <service android:name="android.telecom.cts.MockInCallService"
+            android:permission="android.permission.BIND_INCALL_SERVICE" >
+            <intent-filter>
+                <action android:name="android.telecom.InCallService"/>
+            </intent-filter>
+        </service>
+
+        <activity android:name="android.telecom.cts.MockDialerActivity">
+            <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>
+
+    <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.cts.telecom"
+                     android:label="CTS tests for android.telecom package">
+        <meta-data android:name="listener"
+            android:value="com.android.cts.runner.CtsTestRunListener" />
+    </instrumentation>
+</manifest>
+
diff --git a/tests/tests/telecom/src/android/telecom/cts/BasicInCallServiceTest.java b/tests/tests/telecom/src/android/telecom/cts/BasicInCallServiceTest.java
new file mode 100644
index 0000000..0b5fe61
--- /dev/null
+++ b/tests/tests/telecom/src/android/telecom/cts/BasicInCallServiceTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telecom.cts;
+
+import static android.telecom.cts.TestUtils.shouldTestTelecom;
+
+import android.telecom.cts.MockInCallService.InCallServiceCallbacks;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.telecom.Call;
+import android.telecom.InCallService;
+import android.test.InstrumentationTestCase;
+import android.text.TextUtils;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Sanity test that adding a new call via the CALL intent works correctly.
+ */
+public class BasicInCallServiceTest extends InstrumentationTestCase {
+
+    private static final Uri TEST_NUMBER = Uri.fromParts("tel", "7", null);
+
+    private Context mContext;
+    private String mPreviousDefaultDialer = null;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mContext = getInstrumentation().getContext();
+        mPreviousDefaultDialer = TestUtils.getDefaultDialer(getInstrumentation());
+        TestUtils.setDefaultDialer(getInstrumentation(), TestUtils.PACKAGE);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        if (!TextUtils.isEmpty(mPreviousDefaultDialer)) {
+            TestUtils.setDefaultDialer(getInstrumentation(), mPreviousDefaultDialer);
+        }
+        super.tearDown();
+    }
+
+    /**
+     * Tests that when sending a CALL intent via the Telecom -> Telephony stack, Telecom
+     * binds to the registered {@link InCallService}s and adds a new call. This test will
+     * actually place a phone call to the number 7. It should still pass even if there is no
+     * SIM card inserted.
+     */
+    public void testTelephonyCall_bindsToInCallServiceAndAddsCall() {
+        if (!shouldTestTelecom(mContext)) {
+            return;
+        }
+
+        final Intent intent = new Intent(Intent.ACTION_CALL, TEST_NUMBER);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+        final InCallServiceCallbacks callbacks = createCallbacks();
+
+        MockInCallService.setCallbacks(callbacks);
+
+        mContext.startActivity(intent);
+
+        try {
+            if (callbacks.lock.tryAcquire(3, TimeUnit.SECONDS)) {
+                return;
+            }
+        } catch (InterruptedException e) {
+        }
+
+        fail("No call added to InCallService.");
+    }
+
+    private MockInCallService.InCallServiceCallbacks createCallbacks() {
+        final InCallServiceCallbacks callbacks = new InCallServiceCallbacks() {
+            @Override
+            public void onCallAdded(Call call, int numCalls) {
+                assertEquals("InCallService should have 1 call after adding call", 1, numCalls);
+                call.disconnect();
+                lock.release();
+            }
+        };
+        return callbacks;
+    }
+}
diff --git a/tests/tests/telecom/src/android/telecom/cts/ConnectionTest.java b/tests/tests/telecom/src/android/telecom/cts/ConnectionTest.java
new file mode 100644
index 0000000..9a1ebc9
--- /dev/null
+++ b/tests/tests/telecom/src/android/telecom/cts/ConnectionTest.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telecom.cts;
+
+import static android.telecom.cts.TestUtils.shouldTestTelecom;
+
+import android.os.Build;
+import android.telecom.Connection;
+import android.telecom.DisconnectCause;
+import android.test.AndroidTestCase;
+
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+public class ConnectionTest extends AndroidTestCase {
+
+    public void testStateCallbacks() {
+        if (!shouldTestTelecom(getContext())) {
+            return;
+        }
+
+        final Semaphore lock = new Semaphore(0);
+        Connection connection = createConnection(lock);
+
+        waitForStateChange(lock);
+        assertEquals(Connection.STATE_NEW, connection.getState());
+
+        connection.setInitializing();
+        waitForStateChange(lock);
+        assertEquals(Connection.STATE_INITIALIZING, connection.getState());
+
+        connection.setInitialized();
+        waitForStateChange(lock);
+        assertEquals(Connection.STATE_NEW, connection.getState());
+
+        connection.setRinging();
+        waitForStateChange(lock);
+        assertEquals(Connection.STATE_RINGING, connection.getState());
+
+        connection.setDialing();
+        waitForStateChange(lock);
+        assertEquals(Connection.STATE_DIALING, connection.getState());
+
+        connection.setActive();
+        waitForStateChange(lock);
+        assertEquals(Connection.STATE_ACTIVE, connection.getState());
+
+        connection.setOnHold();
+        waitForStateChange(lock);
+        assertEquals(Connection.STATE_HOLDING, connection.getState());
+
+        connection.setDisconnected(
+                new DisconnectCause(DisconnectCause.LOCAL, "Test call"));
+        waitForStateChange(lock);
+        assertEquals(Connection.STATE_DISCONNECTED, connection.getState());
+
+        connection.setRinging();
+        waitForStateChange(lock);
+        assertEquals("Connection should not move out of STATE_DISCONNECTED.",
+                Connection.STATE_DISCONNECTED, connection.getState());
+    }
+
+    /**
+     * {@link UnsupportedOperationException} is only thrown in L MR1+.
+     */
+    public void testFailedState() {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
+            return;
+        }
+        Connection connection = Connection.createFailedConnection(
+                new DisconnectCause(DisconnectCause.LOCAL, "Test call"));
+        assertEquals(Connection.STATE_DISCONNECTED, connection.getState());
+
+        try {
+            connection.setRinging();
+        } catch (UnsupportedOperationException e) {
+            return;
+        }
+        fail("Connection should not move out of STATE_DISCONNECTED");
+    }
+
+    /**
+     * {@link UnsupportedOperationException} is only thrown in L MR1+.
+     */
+    public void testCanceledState() {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
+            return;
+        }
+        Connection connection = Connection.createCanceledConnection();
+        assertEquals(Connection.STATE_DISCONNECTED, connection.getState());
+
+        try {
+            connection.setDialing();
+        } catch (UnsupportedOperationException e) {
+            return;
+        }
+        fail("Connection should not move out of STATE_DISCONNECTED");
+    }
+
+    private static Connection createConnection(final Semaphore lock) {
+        BasicConnection connection = new BasicConnection();
+        connection.setLock(lock);
+        return connection;
+    }
+
+    private static void waitForStateChange(Semaphore lock) {
+        try {
+            lock.tryAcquire(1000, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException e) {
+            fail("State transition timed out");
+        }
+    }
+
+    private static final class BasicConnection extends Connection {
+        private Semaphore mLock;
+
+        public void setLock(Semaphore lock) {
+            mLock = lock;
+        }
+
+        @Override
+        public void onStateChanged(int state) {
+            mLock.release();
+        }
+    }
+}
diff --git a/tests/tests/telecom/src/android/telecom/cts/ExtendedInCallServiceTest.java b/tests/tests/telecom/src/android/telecom/cts/ExtendedInCallServiceTest.java
new file mode 100644
index 0000000..1a9d8d7
--- /dev/null
+++ b/tests/tests/telecom/src/android/telecom/cts/ExtendedInCallServiceTest.java
@@ -0,0 +1,439 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telecom.cts;
+
+import static android.telecom.cts.TestUtils.*;
+
+import android.telecom.cts.MockConnectionService.ConnectionServiceCallbacks;
+import android.telecom.cts.MockInCallService.InCallServiceCallbacks;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Color;
+import android.net.Uri;
+import android.telecom.CallAudioState;
+import android.telecom.Call;
+import android.telecom.Connection;
+import android.telecom.ConnectionRequest;
+import android.telecom.ConnectionService;
+import android.telecom.InCallService;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.test.InstrumentationTestCase;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Extended suite of tests that use {@MockConnectionService} and {@MockInCallService} to verify
+ * the functionality of the Telecom service. Requires that the version of GmsCore installed on the
+ * device has the REGISTER_CALL_PROVIDER permission.
+ */
+public class ExtendedInCallServiceTest extends InstrumentationTestCase {
+    public static final PhoneAccountHandle TEST_PHONE_ACCOUNT_HANDLE =
+            new PhoneAccountHandle(new ComponentName(PACKAGE, COMPONENT), ACCOUNT_ID);
+
+    public static final PhoneAccount TEST_PHONE_ACCOUNT = PhoneAccount.builder(
+            TEST_PHONE_ACCOUNT_HANDLE, LABEL)
+            .setAddress(Uri.parse("tel:555-TEST"))
+            .setSubscriptionAddress(Uri.parse("tel:555-TEST"))
+            .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
+            .setHighlightColor(Color.RED)
+            .setShortDescription(LABEL)
+            .setSupportedUriSchemes(Arrays.asList("tel"))
+            .build();
+
+    private Context mContext;
+    private TelecomManager mTelecomManager;
+    private InCallServiceCallbacks mInCallCallbacks;
+    private ConnectionServiceCallbacks mConnectionCallbacks;
+    private String mPreviousDefaultDialer = null;
+
+    private static int sCounter = 0;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mContext = getInstrumentation().getContext();
+        mTelecomManager = (TelecomManager) mContext.getSystemService(Context.TELECOM_SERVICE);
+
+        if (shouldTestTelecom(mContext)) {
+            mTelecomManager.registerPhoneAccount(TEST_PHONE_ACCOUNT);
+            TestUtils.enablePhoneAccount(getInstrumentation(), TEST_PHONE_ACCOUNT_HANDLE);
+            mPreviousDefaultDialer = TestUtils.getDefaultDialer(getInstrumentation());
+            TestUtils.setDefaultDialer(getInstrumentation(), PACKAGE);
+            setupCallbacks();
+            placeAndVerifyCall();
+            verifyConnectionService();
+        }
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        if (shouldTestTelecom(mContext)) {
+            if (mInCallCallbacks != null && mInCallCallbacks.getService() != null) {
+                mInCallCallbacks.getService().disconnectLastCall();
+                assertNumCalls(mInCallCallbacks.getService(), 0);
+            }
+            if (!TextUtils.isEmpty(mPreviousDefaultDialer)) {
+                TestUtils.setDefaultDialer(getInstrumentation(), mPreviousDefaultDialer);
+            }
+            mTelecomManager.unregisterPhoneAccount(TEST_PHONE_ACCOUNT_HANDLE);
+        }
+        super.tearDown();
+    }
+
+    public void testWithMockConnection_AddNewOutgoingCallAndThenDisconnect() {
+        if (!shouldTestTelecom(mContext)) {
+            return;
+        }
+
+        final MockInCallService inCallService = mInCallCallbacks.getService();
+        inCallService.disconnectLastCall();
+
+        assertNumCalls(inCallService, 0);
+    }
+
+    public void testWithMockConnection_MuteAndUnmutePhone() {
+        if (!shouldTestTelecom(mContext)) {
+            return;
+        }
+
+        final MockInCallService inCallService = mInCallCallbacks.getService();
+
+        final Call call = inCallService.getLastCall();
+        final MockConnection connection = mConnectionCallbacks.outgoingConnection;
+
+        assertCallState(call, Call.STATE_ACTIVE);
+
+        assertMuteState(connection, false);
+
+        inCallService.setMuted(true);;
+
+        assertMuteState(connection, true);
+        assertMuteState(inCallService, true);
+
+        inCallService.setMuted(false);
+        assertMuteState(connection, false);
+        assertMuteState(inCallService, false);
+    }
+
+    public void testWithMockConnection_SwitchAudioRoutes() {
+        if (!shouldTestTelecom(mContext)) {
+            return;
+        }
+
+        final MockInCallService inCallService = mInCallCallbacks.getService();
+        final MockConnection connection = mConnectionCallbacks.outgoingConnection;
+
+        final Call call = inCallService.getLastCall();
+        assertCallState(call, Call.STATE_ACTIVE);
+
+        // Only test speaker and earpiece modes because the other modes are dependent on having
+        // a bluetooth headset or wired headset connected.
+
+        inCallService.setAudioRoute(CallAudioState.ROUTE_SPEAKER);
+        assertAudioRoute(connection, CallAudioState.ROUTE_SPEAKER);
+        assertAudioRoute(inCallService, CallAudioState.ROUTE_SPEAKER);
+
+        inCallService.setAudioRoute(CallAudioState.ROUTE_EARPIECE);
+        assertAudioRoute(connection, CallAudioState.ROUTE_EARPIECE);
+        assertAudioRoute(inCallService, CallAudioState.ROUTE_EARPIECE);
+    }
+
+    /**
+     * Tests that DTMF Tones are sent from the {@link InCallService} to the
+     * {@link ConnectionService} in the correct sequence.
+     */
+    public void testWithMockConnection_DtmfTones() {
+        if (!shouldTestTelecom(mContext)) {
+            return;
+        }
+
+        final MockInCallService inCallService = mInCallCallbacks.getService();
+        final MockConnection connection = mConnectionCallbacks.outgoingConnection;
+
+        final Call call = inCallService.getLastCall();
+        assertCallState(call, Call.STATE_ACTIVE);
+
+        assertDtmfString(connection, "");
+
+        call.playDtmfTone('1');
+        assertDtmfString(connection, "1");
+
+        call.playDtmfTone('2');
+        assertDtmfString(connection, "12");
+
+        call.playDtmfTone('3');
+        call.playDtmfTone('4');
+        call.playDtmfTone('5');
+        assertDtmfString(connection, "12345");
+    }
+
+    public void testWithMockConnection_HoldAndUnholdCall() {
+        if (!shouldTestTelecom(mContext)) {
+            return;
+        }
+
+        final MockInCallService inCallService = mInCallCallbacks.getService();
+        final MockConnection connection = mConnectionCallbacks.outgoingConnection;
+
+        final Call call = inCallService.getLastCall();
+
+        assertCallState(call, Call.STATE_ACTIVE);
+
+        call.hold();
+        assertCallState(call, Call.STATE_HOLDING);
+        assertEquals(Connection.STATE_HOLDING, connection.getState());
+
+        call.unhold();
+        assertCallState(call, Call.STATE_ACTIVE);
+        assertEquals(Connection.STATE_ACTIVE, connection.getState());
+    }
+
+    private void sleep(long ms) {
+        try {
+            Thread.sleep(ms);
+        } catch (InterruptedException e) {
+        }
+    }
+
+    private void setupCallbacks() {
+        mInCallCallbacks = new InCallServiceCallbacks() {
+            @Override
+            public void onCallAdded(Call call, int numCalls) {
+                this.lock.release();
+            }
+        };
+
+        MockInCallService.setCallbacks(mInCallCallbacks);
+
+        mConnectionCallbacks = new ConnectionServiceCallbacks() {
+            @Override
+            public void onCreateOutgoingConnection(MockConnection connection,
+                    ConnectionRequest request) {
+                this.lock.release();
+            }
+        };
+
+        MockConnectionService.setCallbacks(mConnectionCallbacks);
+    }
+
+    /**
+     *  Puts Telecom in a state where there is an active call provided by the
+     *  {@link MockConnectionService} which can be tested.
+     */
+    private void placeAndVerifyCall() {
+        placeNewCallWithPhoneAccount();
+
+        try {
+            if (!mInCallCallbacks.lock.tryAcquire(3, TimeUnit.SECONDS)) {
+                fail("No call added to InCallService.");
+            }
+        } catch (InterruptedException e) {
+            Log.i(TAG, "Test interrupted!");
+        }
+
+        assertEquals("InCallService should contain 1 call after adding a call.", 1,
+                mInCallCallbacks.getService().getCallCount());
+        assertTrue("TelecomManager should be in a call", mTelecomManager.isInCall());
+    }
+
+    private void verifyConnectionService() {
+        try {
+            if (!mConnectionCallbacks.lock.tryAcquire(3, TimeUnit.SECONDS)) {
+                fail("No outgoing call connection requested by Telecom");
+            }
+        } catch (InterruptedException e) {
+            Log.i(TAG, "Test interrupted!");
+        }
+
+        assertNotNull("Telecom should bind to and create ConnectionService",
+                mConnectionCallbacks.getService());
+        assertNotNull("Telecom should create outgoing connection for outgoing call",
+                mConnectionCallbacks.outgoingConnection);
+        assertNull("Telecom should not create incoming connection for outgoing call",
+                mConnectionCallbacks.incomingConnection);
+
+        final MockConnection connection = mConnectionCallbacks.outgoingConnection;
+        connection.setDialing();
+        connection.setActive();
+
+        assertEquals(Connection.STATE_ACTIVE, connection.getState());
+    }
+
+    /**
+     * Place a new outgoing call via the {@link MockConnectionService}
+     */
+    private void placeNewCallWithPhoneAccount() {
+        final Intent intent = new Intent(Intent.ACTION_CALL, getTestNumber());
+        intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, TEST_PHONE_ACCOUNT_HANDLE);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        mContext.startActivity(intent);
+    }
+
+    /**
+     * Create a new number each time for a new test. Telecom has special logic to reuse certain
+     * calls if multiple calls to the same number are placed within a short period of time which
+     * can cause certain tests to fail.
+     */
+    private Uri getTestNumber() {
+        return Uri.fromParts("tel", String.valueOf(sCounter++), null);
+    }
+
+    private void assertNumCalls(final MockInCallService inCallService, final int numCalls) {
+        waitUntilConditionIsTrueOrTimeout(new Condition() {
+            @Override
+            public Object expected() {
+                return numCalls;
+            }
+            @Override
+            public Object actual() {
+                return inCallService.getCallCount();
+            }
+        },
+        WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
+        "InCallService should contain " + numCalls + " calls."
+    );
+    }
+
+    private void assertMuteState(final InCallService incallService, final boolean isMuted) {
+        waitUntilConditionIsTrueOrTimeout(
+                new Condition() {
+                    @Override
+                    public Object expected() {
+                        return isMuted;
+                    }
+
+                    @Override
+                    public Object actual() {
+                        return incallService.getCallAudioState().isMuted();
+                    }
+                },
+                WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
+                "Phone's mute state should be: " + isMuted
+        );
+    }
+
+    private void assertMuteState(final MockConnection connection, final boolean isMuted) {
+        waitUntilConditionIsTrueOrTimeout(
+                new Condition() {
+                    @Override
+                    public Object expected() {
+                        return isMuted;
+                    }
+
+                    @Override
+                    public Object actual() {
+                        return connection.getCallAudioState().isMuted();
+                    }
+                },
+                WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
+                "Connection's mute state should be: " + isMuted
+        );
+    }
+
+    private void assertAudioRoute(final InCallService incallService, final int route) {
+        waitUntilConditionIsTrueOrTimeout(
+                new Condition() {
+                    @Override
+                    public Object expected() {
+                        return route;
+                    }
+
+                    @Override
+                    public Object actual() {
+                        return incallService.getCallAudioState().getRoute();
+                    }
+                },
+                WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
+                "Phone's audio route should be: " + route
+        );
+    }
+
+    private void assertAudioRoute(final MockConnection connection, final int route) {
+        waitUntilConditionIsTrueOrTimeout(
+                new Condition() {
+                    @Override
+                    public Object expected() {
+                        return route;
+                    }
+
+                    @Override
+                    public Object actual() {
+                        return connection.getCallAudioState().getRoute();
+                    }
+                },
+                WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
+                "Connection's audio route should be: " + route
+        );
+    }
+
+    private void assertCallState(final Call call, final int state) {
+        waitUntilConditionIsTrueOrTimeout(
+                new Condition() {
+                    @Override
+                    public Object expected() {
+                        return state;
+                    }
+
+                    @Override
+                    public Object actual() {
+                        return call.getState();
+                    }
+                },
+                WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
+                "Call should be in state " + state
+        );
+    }
+
+    private void assertDtmfString(final MockConnection connection, final String dtmfString) {
+        waitUntilConditionIsTrueOrTimeout(new Condition() {
+                @Override
+                public Object expected() {
+                    return dtmfString;
+                }
+
+                @Override
+                public Object actual() {
+                    return connection.getDtmfString();
+                }
+            },
+            WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
+            "DTMF string should be equivalent to entered DTMF characters: " + dtmfString
+        );
+    }
+
+    private void waitUntilConditionIsTrueOrTimeout(Condition condition, long timeout,
+            String description) {
+        final long start = System.currentTimeMillis();
+        while (!condition.expected().equals(condition.actual())
+                && System.currentTimeMillis() - start < timeout) {
+            sleep(50);
+        }
+        assertEquals(description, condition.expected(), condition.actual());
+    }
+
+    private interface Condition {
+        Object expected();
+        Object actual();
+    }
+}
diff --git a/tests/tests/telecom/src/android/telecom/cts/MockConnection.java b/tests/tests/telecom/src/android/telecom/cts/MockConnection.java
new file mode 100644
index 0000000..67a0fe0
--- /dev/null
+++ b/tests/tests/telecom/src/android/telecom/cts/MockConnection.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telecom.cts;
+
+import static android.telecom.CallAudioState.*;
+import android.telecom.CallAudioState;
+import android.telecom.Connection;
+import android.telecom.DisconnectCause;
+import android.util.Log;
+
+/**
+ * {@link Connection} subclass that immediately performs any state changes that are a result of
+ * callbacks sent from Telecom.
+ */
+public class MockConnection extends Connection {
+
+    private CallAudioState mCallAudioState =
+            new CallAudioState(false, CallAudioState.ROUTE_EARPIECE, ROUTE_EARPIECE | ROUTE_SPEAKER);
+    private int mState = STATE_NEW;
+    private String mDtmfString = "";
+
+    @Override
+    public void onAnswer() {
+        setActive();
+    }
+
+    @Override
+    public void onReject() {
+        setDisconnected(new DisconnectCause(DisconnectCause.REJECTED));
+    }
+
+    @Override
+    public void onHold() {
+        setOnHold();
+    }
+
+    @Override
+    public void onUnhold() {
+        setActive();
+    }
+
+    @Override
+    public void onDisconnect() {
+        setDisconnected(new DisconnectCause(DisconnectCause.LOCAL));
+        destroy();
+    }
+
+    @Override
+    public void onAbort() {
+    }
+
+    @Override
+    public void onPlayDtmfTone(char c) {
+        mDtmfString += c;
+    }
+
+    @Override
+    public void onCallAudioStateChanged(CallAudioState state) {
+        mCallAudioState = state;
+    }
+
+    @Override
+    public void onStateChanged(int state) {
+        mState = state;
+    }
+
+    public int getCurrentState()  {
+        return mState;
+    }
+
+    public CallAudioState getCurrentCallAudioState() {
+        return mCallAudioState;
+    }
+
+    public String getDtmfString() {
+        return mDtmfString;
+    }
+}
diff --git a/tests/tests/telecom/src/android/telecom/cts/MockConnectionService.java b/tests/tests/telecom/src/android/telecom/cts/MockConnectionService.java
new file mode 100644
index 0000000..2b54f32
--- /dev/null
+++ b/tests/tests/telecom/src/android/telecom/cts/MockConnectionService.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telecom.cts;
+
+import android.telecom.Connection;
+import android.telecom.ConnectionRequest;
+import android.telecom.ConnectionService;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+
+import java.util.concurrent.Semaphore;
+
+public class MockConnectionService extends ConnectionService {
+    private static ConnectionServiceCallbacks sCallbacks;
+    private static Object sLock = new Object();
+
+    public static abstract class ConnectionServiceCallbacks {
+        private MockConnectionService mService;
+        public MockConnection outgoingConnection;
+        public MockConnection incomingConnection;
+        public Semaphore lock = new Semaphore(0);
+
+        public void onCreateOutgoingConnection(MockConnection connection,
+                ConnectionRequest request) {};
+        public void onCreateIncomingConnection(MockConnection connection,
+                ConnectionRequest request) {};
+
+        final public MockConnectionService getService() {
+            return mService;
+        }
+
+        final public void setService(MockConnectionService service) {
+            mService = service;
+        }
+    }
+
+    @Override
+    public Connection onCreateOutgoingConnection(PhoneAccountHandle connectionManagerPhoneAccount,
+            ConnectionRequest request) {
+        final MockConnection connection = new MockConnection();
+        connection.setAddress(request.getAddress(), TelecomManager.PRESENTATION_ALLOWED);
+
+        final ConnectionServiceCallbacks callbacks = getCallbacks();
+        if (callbacks != null) {
+            callbacks.setService(this);
+            callbacks.outgoingConnection = connection;
+            callbacks.onCreateOutgoingConnection(connection, request);
+        }
+        return connection;
+    }
+
+    @Override
+    public Connection onCreateIncomingConnection(PhoneAccountHandle connectionManagerPhoneAccount,
+            ConnectionRequest request) {
+        final MockConnection connection = new MockConnection();
+        connection.setAddress(request.getAddress(), TelecomManager.PRESENTATION_ALLOWED);
+
+        final ConnectionServiceCallbacks callbacks = getCallbacks();
+        if (callbacks != null) {
+            callbacks.setService(this);
+            callbacks.incomingConnection = connection;
+            callbacks.onCreateIncomingConnection(connection, request);
+        }
+        return connection;
+    }
+
+    public static void setCallbacks(ConnectionServiceCallbacks callbacks) {
+        synchronized (sLock) {
+            sCallbacks = callbacks;
+        }
+    }
+
+    private ConnectionServiceCallbacks getCallbacks() {
+        synchronized (sLock) {
+            if (sCallbacks != null) {
+                sCallbacks.setService(this);
+            }
+            return sCallbacks;
+        }
+    }
+}
diff --git a/tests/tests/telecom/src/android/telecom/cts/MockInCallService.java b/tests/tests/telecom/src/android/telecom/cts/MockInCallService.java
new file mode 100644
index 0000000..cecc603
--- /dev/null
+++ b/tests/tests/telecom/src/android/telecom/cts/MockInCallService.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telecom.cts;
+
+import android.telecom.Call;
+import android.telecom.InCallService;
+
+import java.util.ArrayList;
+import java.util.concurrent.Semaphore;
+
+public class MockInCallService extends InCallService {
+    private ArrayList<Call> mCalls = new ArrayList<>();
+    private static InCallServiceCallbacks sCallbacks;
+
+    private static final Object sLock = new Object();
+
+    public static abstract class InCallServiceCallbacks {
+        private MockInCallService mService;
+        public Semaphore lock = new Semaphore(0);
+
+        public void onCallAdded(Call call, int numCalls) {};
+        public void onCallRemoved(Call call, int numCalls) {};
+        public void onCallStateChanged(Call call, int state) {};
+
+        final public MockInCallService getService() {
+            return mService;
+        }
+
+        final public void setService(MockInCallService service) {
+            mService = service;
+        }
+    }
+
+    private Call.Callback mCallCallback = new Call.Callback() {
+        @Override
+        public void onStateChanged(Call call, int state) {
+            if (getCallbacks() != null) {
+                getCallbacks().onCallStateChanged(call, state);
+            }
+        }
+    };
+
+    @Override
+    public android.os.IBinder onBind(android.content.Intent intent) {
+        if (getCallbacks() != null) {
+            getCallbacks().setService(this);
+        }
+        return super.onBind(intent);
+    }
+
+    @Override
+    public void onCallAdded(Call call) {
+        if (!mCalls.contains(call)) {
+            mCalls.add(call);
+            call.registerCallback(mCallCallback);
+        }
+        if (getCallbacks() != null) {
+            getCallbacks().onCallAdded(call, mCalls.size());
+        }
+    }
+
+    @Override
+    public void onCallRemoved(Call call) {
+        mCalls.remove(call);
+        if (getCallbacks() != null) {
+            getCallbacks().onCallRemoved(call, mCalls.size());
+        }
+    }
+
+    /**
+     * @return the number of calls currently added to the {@code InCallService}.
+     */
+    public int getCallCount() {
+        return mCalls.size();
+    }
+
+    /**
+     * @return the most recently added call that exists inside the {@code InCallService}
+     */
+    public Call getLastCall() {
+        if (mCalls.size() >= 1) {
+            return mCalls.get(mCalls.size() - 1);
+        }
+        return null;
+    }
+
+    public void disconnectLastCall() {
+        final Call call = getLastCall();
+        if (call != null) {
+            call.disconnect();
+        }
+    }
+
+    public static void setCallbacks(InCallServiceCallbacks callbacks) {
+        synchronized (sLock) {
+            sCallbacks = callbacks;
+        }
+    }
+
+    private InCallServiceCallbacks getCallbacks() {
+        synchronized (sLock) {
+            if (sCallbacks != null) {
+                sCallbacks.setService(this);
+            }
+            return sCallbacks;
+        }
+    }
+}
diff --git a/tests/tests/telecom/src/android/telecom/cts/TelecomAvailabilityTest.java b/tests/tests/telecom/src/android/telecom/cts/TelecomAvailabilityTest.java
new file mode 100644
index 0000000..cc0afe4
--- /dev/null
+++ b/tests/tests/telecom/src/android/telecom/cts/TelecomAvailabilityTest.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telecom.cts;
+
+import static android.telecom.cts.TestUtils.shouldTestTelecom;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.test.InstrumentationTestCase;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tests for Telecom service. These tests only run on L+ devices because Telecom was
+ * added in L.
+ */
+public class TelecomAvailabilityTest extends InstrumentationTestCase {
+    private static final String TAG = TelecomAvailabilityTest.class.getSimpleName();
+    private static final String TELECOM_PACKAGE_NAME = "com.android.server.telecom";
+    private static final String TELEPHONY_PACKAGE_NAME = "com.android.phone";
+
+    private PackageManager mPackageManager;
+    private Context mContext;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mContext = getInstrumentation().getContext();
+        mPackageManager = getInstrumentation().getTargetContext().getPackageManager();
+    }
+
+    /**
+     * Test that the Telecom APK is pre-installed and a system app (FLAG_SYSTEM).
+     */
+    public void testTelecomIsPreinstalledAndSystem() {
+        if (!shouldTestTelecom(mContext)) {
+            return;
+        }
+
+        PackageInfo packageInfo = findOnlyTelecomPackageInfo(mPackageManager);
+        ApplicationInfo applicationInfo = packageInfo.applicationInfo;
+        assertTrue("Telecom APK must be FLAG_SYSTEM",
+                (applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0);
+        Log.d(TAG, String.format("Telecom APK is FLAG_SYSTEM %d", applicationInfo.flags));
+    }
+
+    /**
+     * Test that the Telecom APK is registered to handle CALL intents, and that the Telephony APK
+     * is not.
+     */
+    public void testTelecomHandlesCallIntents() {
+        if (!shouldTestTelecom(mContext)) {
+            return;
+        }
+
+        final Intent intent = new Intent(Intent.ACTION_CALL, Uri.fromParts("tel", "1234567", null));
+        final List<ResolveInfo> activities =
+                mPackageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
+
+        boolean telecomMatches = false;
+        boolean telephonyMatches = false;
+        for (ResolveInfo resolveInfo : activities) {
+            if (resolveInfo.activityInfo == null) {
+                continue;
+            }
+            if (!telecomMatches
+                    && TELECOM_PACKAGE_NAME.equals(resolveInfo.activityInfo.packageName)) {
+                telecomMatches = true;
+            } else if (!telephonyMatches
+                    && TELEPHONY_PACKAGE_NAME.equals(resolveInfo.activityInfo.packageName)) {
+                telephonyMatches = true;
+            }
+        }
+
+        assertTrue("Telecom APK must be registered to handle CALL intents", telecomMatches);
+        assertFalse("Telephony APK must NOT be registered to handle CALL intents",
+                telephonyMatches);
+    }
+
+    /**
+     * @return The {@link PackageInfo} of the only app named {@code PACKAGE_NAME}.
+     */
+    private static PackageInfo findOnlyTelecomPackageInfo(PackageManager packageManager) {
+        List<PackageInfo> telecomPackages = findMatchingPackages(packageManager);
+        assertEquals(String.format("There must be only one package named %s", TELECOM_PACKAGE_NAME),
+                1, telecomPackages.size());
+        return telecomPackages.get(0);
+    }
+
+    /**
+     * Finds all packages that have {@code PACKAGE_NAME} name.
+     *
+     * @param pm the android package manager
+     * @return a list of {@link PackageInfo} records
+     */
+    private static List<PackageInfo> findMatchingPackages(PackageManager pm) {
+        List<PackageInfo> packageInfoList = new ArrayList<PackageInfo>();
+        for (PackageInfo info : pm.getInstalledPackages(0)) {
+            if (TELECOM_PACKAGE_NAME.equals(info.packageName)) {
+                packageInfoList.add(info);
+            }
+        }
+        return packageInfoList;
+    }
+}
diff --git a/tests/tests/telecom/src/android/telecom/cts/TestUtils.java b/tests/tests/telecom/src/android/telecom/cts/TestUtils.java
new file mode 100644
index 0000000..8cca04c
--- /dev/null
+++ b/tests/tests/telecom/src/android/telecom/cts/TestUtils.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.telecom.cts;
+
+import android.app.Instrumentation;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.ParcelFileDescriptor;
+import android.telecom.PhoneAccountHandle;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+
+public class TestUtils {
+    static final String TAG = "TelecomXTSTests";
+    static final boolean HAS_TELECOM = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
+    static final long WAIT_FOR_STATE_CHANGE_TIMEOUT_MS = 10000;
+
+    public static final String PACKAGE = "com.android.cts.telecom";
+    public static final String COMPONENT = "android.telecom.cts.MockConnectionService";
+    public static final String ACCOUNT_ID = "xtstest_CALL_PROVIDER_ID";
+
+    public static final String LABEL = "CTS_MockConnectionService";
+
+    private static final String COMMAND_SET_DEFAULT_DIALER = "telecom set-default-dialer ";
+
+    private static final String COMMAND_GET_DEFAULT_DIALER = "telecom get-default-dialer";
+
+    private static final String COMMAND_ENABLE = "telecom set-phone-account-enabled ";
+
+    public static boolean shouldTestTelecom(Context context) {
+        if (!HAS_TELECOM) {
+            return false;
+        }
+        final PackageManager pm = context.getPackageManager();
+        return pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY);
+    }
+
+    public static void setDefaultDialer(Instrumentation instrumentation, String packageName)
+            throws Exception {
+        executeShellCommand(instrumentation, COMMAND_SET_DEFAULT_DIALER + packageName);
+    }
+
+    public static String getDefaultDialer(Instrumentation instrumentation) throws Exception {
+        return executeShellCommand(instrumentation, COMMAND_GET_DEFAULT_DIALER);
+    }
+
+    public static void enablePhoneAccount(Instrumentation instrumentation,
+            PhoneAccountHandle handle) throws Exception {
+        final ComponentName component = handle.getComponentName();
+        executeShellCommand(instrumentation, COMMAND_ENABLE
+                + component.getPackageName() + "/" + component.getClassName() + " "
+                + handle.getId());
+    }
+
+    /**
+     * Executes the given shell command and returns the output in a string. Note that even
+     * if we don't care about the output, we have to read the stream completely to make the
+     * command execute.
+     */
+    public static String executeShellCommand(Instrumentation instrumentation,
+            String command) throws Exception {
+        final ParcelFileDescriptor pfd =
+                instrumentation.getUiAutomation().executeShellCommand(command);
+        BufferedReader br = null;
+        try (InputStream in = new FileInputStream(pfd.getFileDescriptor())) {
+            br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
+            String str = null;
+            StringBuilder out = new StringBuilder();
+            while ((str = br.readLine()) != null) {
+                out.append(str);
+            }
+            return out.toString();
+        } finally {
+            if (br != null) {
+                closeQuietly(br);
+            }
+            closeQuietly(pfd);
+        }
+    }
+
+    private static void closeQuietly(AutoCloseable closeable) {
+        if (closeable != null) {
+            try {
+                closeable.close();
+            } catch (RuntimeException rethrown) {
+                throw rethrown;
+            } catch (Exception ignored) {
+            }
+        }
+    }
+}