Merge "Add Telecom CTS tests" into mnc-dev
diff --git a/CtsTestCaseList.mk b/CtsTestCaseList.mk
index f2ba287..8386124 100644
--- a/CtsTestCaseList.mk
+++ b/CtsTestCaseList.mk
@@ -163,6 +163,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) {
+            }
+        }
+    }
+}