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) {
+ }
+ }
+ }
+}