Merge "Add a new field is_multi into Satellite atoms" into main
diff --git a/src/com/android/services/telephony/TelephonyConnectionService.java b/src/com/android/services/telephony/TelephonyConnectionService.java
index 457e8f8..4cb0575 100644
--- a/src/com/android/services/telephony/TelephonyConnectionService.java
+++ b/src/com/android/services/telephony/TelephonyConnectionService.java
@@ -153,6 +153,10 @@
// Timeout to wait for the termination of incoming call before continue with the emergency call.
private static final int DEFAULT_REJECT_INCOMING_CALL_TIMEOUT_MS = 10 * 1000; // 10 seconds.
+ // Timeout to wait for ending active call on other domain before continuing with
+ // the emergency call.
+ private static final int DEFAULT_DISCONNECT_CALL_ON_OTHER_DOMAIN_TIMEOUT_MS = 2 * 1000;
+
// If configured, reject attempts to dial numbers matching this pattern.
private static final Pattern CDMA_ACTIVATION_CODE_REGEX_PATTERN =
Pattern.compile("\\*228[0-9]{0,2}");
@@ -1366,7 +1370,11 @@
}
return resultConnection;
} else {
- if (mTelephonyManagerProxy.isConcurrentCallsPossible()) {
+ // If call sequencing is enabled, Telecom will take care of holding calls across
+ // subscriptions if needed before delegating the connection creation over to
+ // Telephony.
+ if (mTelephonyManagerProxy.isConcurrentCallsPossible()
+ && !mTelecomFlags.enableCallSequencing()) {
Conferenceable c = maybeHoldCallsOnOtherSubs(request.getAccountHandle());
if (c != null) {
delayDialForOtherSubHold(phone, c, (success) -> {
@@ -2851,11 +2859,142 @@
+ "reject incoming, dialing canceled");
return;
}
- placeEmergencyConnectionOnSelectedDomain(request, resultConnection, phone);
+ // Hang up the active calls if the domain of currently active call is different
+ // from the domain selected by domain selector.
+ if (Flags.hangupActiveCallBasedOnEmergencyCallDomain()) {
+ CompletableFuture<Void> disconnectCall = maybeDisconnectCallsOnOtherDomain(
+ phone, resultConnection, result,
+ getAllConnections(), getAllConferences(), (ret) -> {
+ if (!ret) {
+ Log.i(this, "createEmergencyConnection: "
+ + "disconnecting call on other domain failed");
+ }
+ });
+
+ CompletableFuture<Void> unused = disconnectCall.thenRun(() -> {
+ if (resultConnection.getState() == Connection.STATE_DISCONNECTED) {
+ Log.i(this, "createEmergencyConnection: "
+ + "disconnect call on other domain, dialing canceled");
+ return;
+ }
+ placeEmergencyConnectionOnSelectedDomain(request, resultConnection, phone);
+ });
+ } else {
+ placeEmergencyConnectionOnSelectedDomain(request, resultConnection, phone);
+ }
});
}, mDomainSelectionMainExecutor);
}
+ /**
+ * Disconnect the active calls on the other domain for an emergency call.
+ * For example,
+ * - Active IMS normal call and CS emergency call
+ * - Active CS normal call and IMS emergency call
+ *
+ * @param phone The Phone to be used for an emergency call.
+ * @param emergencyConnection The connection created for an emergency call.
+ * @param emergencyDomain The selected domain for an emergency call.
+ * @param connections All individual connections, including conference participants.
+ * @param conferences All conferences.
+ * @param completeConsumer The consumer to call once the call hangup has been completed.
+ * {@code true} if the operation commpletes successfully, or
+ * {@code false} if the operation timed out/failed.
+ */
+ @VisibleForTesting
+ public static CompletableFuture<Void> maybeDisconnectCallsOnOtherDomain(Phone phone,
+ Connection emergencyConnection,
+ @NetworkRegistrationInfo.Domain int emergencyDomain,
+ @NonNull Collection<Connection> connections,
+ @NonNull Collection<Conference> conferences,
+ Consumer<Boolean> completeConsumer) {
+ List<Connection> activeConnections = connections.stream()
+ .filter(c -> {
+ return !c.equals(emergencyConnection)
+ && isConnectionOnOtherDomain(c, phone, emergencyDomain);
+ }).toList();
+ List<Conference> activeConferences = conferences.stream()
+ .filter(c -> {
+ Connection pc = c.getPrimaryConnection();
+ return isConnectionOnOtherDomain(pc, phone, emergencyDomain);
+ }).toList();
+
+ if (activeConnections.isEmpty() && activeConferences.isEmpty()) {
+ // There are no active calls.
+ completeConsumer.accept(true);
+ return CompletableFuture.completedFuture(null);
+ }
+
+ Log.i(LOG_TAG, "maybeDisconnectCallsOnOtherDomain: "
+ + "connections=" + activeConnections.size()
+ + ", conferences=" + activeConferences.size());
+
+ try {
+ CompletableFuture<Boolean> future = null;
+
+ for (Connection c : activeConnections) {
+ TelephonyConnection tc = (TelephonyConnection) c;
+ if (tc.getState() != Connection.STATE_DISCONNECTED) {
+ if (future == null) {
+ future = new CompletableFuture<>();
+ tc.getOriginalConnection().addListener(new OnDisconnectListener(future));
+ }
+ tc.hangup(android.telephony.DisconnectCause.OUTGOING_EMERGENCY_CALL_PLACED);
+ }
+ }
+
+ for (Conference c : activeConferences) {
+ if (c.getState() != Connection.STATE_DISCONNECTED) {
+ c.onDisconnect();
+ }
+ }
+
+ if (future != null) {
+ // A timeout that will complete the future to not block the outgoing call
+ // indefinitely.
+ CompletableFuture<Boolean> timeout = new CompletableFuture<>();
+ phone.getContext().getMainThreadHandler().postDelayed(
+ () -> timeout.complete(false),
+ DEFAULT_DISCONNECT_CALL_ON_OTHER_DOMAIN_TIMEOUT_MS);
+ // Ensure that the Consumer is completed on the main thread.
+ return future.acceptEitherAsync(timeout, completeConsumer,
+ phone.getContext().getMainExecutor()).exceptionally((ex) -> {
+ Log.w(LOG_TAG, "maybeDisconnectCallsOnOtherDomain: exceptionally="
+ + ex);
+ return null;
+ });
+ } else {
+ completeConsumer.accept(true);
+ return CompletableFuture.completedFuture(null);
+ }
+ } catch (Exception e) {
+ Log.w(LOG_TAG, "maybeDisconnectCallsOnOtherDomain: exception=" + e.getMessage());
+ completeConsumer.accept(false);
+ return CompletableFuture.completedFuture(null);
+ }
+ }
+
+ private static boolean isConnectionOnOtherDomain(Connection c, Phone phone,
+ @NetworkRegistrationInfo.Domain int domain) {
+ if (c instanceof TelephonyConnection) {
+ TelephonyConnection tc = (TelephonyConnection) c;
+ Phone callPhone = tc.getPhone();
+ int callDomain = NetworkRegistrationInfo.DOMAIN_UNKNOWN;
+
+ if (callPhone != null && callPhone.getSubId() == phone.getSubId()) {
+ if (tc.isGsmCdmaConnection()) {
+ callDomain = NetworkRegistrationInfo.DOMAIN_CS;
+ } else if (tc.isImsConnection()) {
+ callDomain = NetworkRegistrationInfo.DOMAIN_PS;
+ }
+ }
+
+ return callDomain != NetworkRegistrationInfo.DOMAIN_UNKNOWN
+ && callDomain != domain;
+ }
+ return false;
+ }
+
private void dialCsEmergencyCall(final Phone phone,
final TelephonyConnection resultConnection, final ConnectionRequest request) {
Log.d(this, "dialCsEmergencyCall");
diff --git a/testapps/TestSatelliteApp/AndroidManifest.xml b/testapps/TestSatelliteApp/AndroidManifest.xml
index 1825df2..de455f2 100644
--- a/testapps/TestSatelliteApp/AndroidManifest.xml
+++ b/testapps/TestSatelliteApp/AndroidManifest.xml
@@ -17,7 +17,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.phone.testapps.satellitetestapp">
- <application android:label="SatelliteTestApp">
+ <application
+ android:networkSecurityConfig="@xml/network_security_config"
+ android:label="SatelliteTestApp">
<activity
android:name=".SatelliteTestApp"
android:exported="true"
@@ -55,10 +57,17 @@
<action android:name="android.telephony.satellite.SatelliteService" />
</intent-filter>
</service>
+
+ <meta-data
+ android:name="android.telephony.PROPERTY_SATELLITE_DATA_OPTIMIZED"
+ android:value="true"/>
</application>
<uses-permission android:name="android.permission.SATELLITE_COMMUNICATION" />
<uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE" />
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.BIND_SATELLITE_SERVICE" />
+ <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.WRITE_SETTINGS" />
+ <uses-permission android:name="android.permission.INTERNET" />
</manifest>
diff --git a/testapps/TestSatelliteApp/res/layout/activity_SatelliteTestApp.xml b/testapps/TestSatelliteApp/res/layout/activity_SatelliteTestApp.xml
index 26b45e3..43cce9b 100644
--- a/testapps/TestSatelliteApp/res/layout/activity_SatelliteTestApp.xml
+++ b/testapps/TestSatelliteApp/res/layout/activity_SatelliteTestApp.xml
@@ -81,5 +81,12 @@
android:paddingStart="4dp"
android:paddingEnd="4dp"
android:text="@string/TestSatelliteWrapper"/>
+ <Button
+ android:id="@+id/TestSatelliteConstrainConnection"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingStart="4dp"
+ android:paddingEnd="4dp"
+ android:text="@string/TestSatelliteConstrainConnection"/>
</LinearLayout>
</ScrollView>
diff --git a/testapps/TestSatelliteApp/res/values/donottranslate_strings.xml b/testapps/TestSatelliteApp/res/values/donottranslate_strings.xml
index 5c3a72d..f48c022 100644
--- a/testapps/TestSatelliteApp/res/values/donottranslate_strings.xml
+++ b/testapps/TestSatelliteApp/res/values/donottranslate_strings.xml
@@ -62,6 +62,8 @@
<string name="sendMessage">sendMessage</string>
<string name="receiveMessage">receiveMessage</string>
+ <string name="TestSatelliteConstrainConnection">Test Satellite Constrain Connection</string>
+
<string name="TestSatelliteWrapper">Test Satellite Wrapper</string>
<string name="requestNtnSignalStrength">requestNtnSignalStrength</string>
<string name="registerForNtnSignalStrengthChanged">registerForNtnSignalStrengthChanged</string>
diff --git a/testapps/TestSatelliteApp/res/xml/network_security_config.xml b/testapps/TestSatelliteApp/res/xml/network_security_config.xml
new file mode 100644
index 0000000..463e65a
--- /dev/null
+++ b/testapps/TestSatelliteApp/res/xml/network_security_config.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+ <domain-config cleartextTrafficPermitted="true">
+ <domain includeSubdomains="true">www.google.com</domain>
+ </domain-config>
+</network-security-config>
\ No newline at end of file
diff --git a/testapps/TestSatelliteApp/src/com/android/phone/testapps/satellitetestapp/PingTask.java b/testapps/TestSatelliteApp/src/com/android/phone/testapps/satellitetestapp/PingTask.java
new file mode 100644
index 0000000..fe86c21
--- /dev/null
+++ b/testapps/TestSatelliteApp/src/com/android/phone/testapps/satellitetestapp/PingTask.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.phone.testapps.satellitetestapp;
+
+import android.net.Network;
+import android.os.AsyncTask;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Scanner;
+
+class PingTask extends AsyncTask<Network, Integer, Integer> {
+ protected Integer doInBackground(Network... network) {
+ ping(network[0]);
+ return 0;
+ }
+ String ping(Network network) {
+ URL url = null;
+ try {
+ url = new URL("http://www.google.com");
+ } catch (Exception e) {
+ Log.d("SatelliteDataConstrained", "exception: " + e);
+ }
+ if (url != null) {
+ try {
+ Log.d("SatelliteDataConstrained", "ping " + url);
+ String result = httpGet(network, url);
+ Log.d("SatelliteDataConstrained", "Ping Success");
+ return result;
+ } catch (Exception e) {
+ Log.d("SatelliteDataConstrained", "exception: " + e);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Performs a HTTP GET to the specified URL on the specified Network, and returns
+ * the response body decoded as UTF-8.
+ */
+ private static String httpGet(Network network, URL httpUrl) throws IOException {
+ HttpURLConnection connection = (HttpURLConnection) network.openConnection(httpUrl);
+ try {
+ InputStream inputStream = connection.getInputStream();
+ Log.d("httpGet", "httpUrl + " + httpUrl);
+ Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
+ return scanner.hasNext() ? scanner.next() : "";
+ } finally {
+ connection.disconnect();
+ }
+ }
+}
diff --git a/testapps/TestSatelliteApp/src/com/android/phone/testapps/satellitetestapp/SatelliteTestApp.java b/testapps/TestSatelliteApp/src/com/android/phone/testapps/satellitetestapp/SatelliteTestApp.java
index cb56e87..911e179 100644
--- a/testapps/TestSatelliteApp/src/com/android/phone/testapps/satellitetestapp/SatelliteTestApp.java
+++ b/testapps/TestSatelliteApp/src/com/android/phone/testapps/satellitetestapp/SatelliteTestApp.java
@@ -23,15 +23,24 @@
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
import android.os.Bundle;
+import android.os.Looper;
import android.os.IBinder;
import android.telephony.satellite.stub.SatelliteDatagram;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
+import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
/**
* SatelliteTestApp main activity to navigate to other APIs related to satellite.
@@ -41,14 +50,23 @@
private static final String TAG = "SatelliteTestApp";
public static TestSatelliteService sSatelliteService;
private final Object mSendDatagramLock = new Object();
-
+ Network mNetwork = null;
+ Context mContext;
+ ConnectivityManager mConnectivityManager;
+ NetworkCallback mSatelliteConstrainNetworkCallback;
+ private final ExecutorService executor = Executors.newSingleThreadExecutor();
private TestSatelliteServiceConnection mSatelliteServiceConn;
private List<SatelliteDatagram> mSentSatelliteDatagrams = new ArrayList<>();
private static final int REQUEST_CODE_SEND_SMS = 1;
+ private final int NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED = 37;
+ private boolean isNetworkRequested = false;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ mContext = getApplicationContext();
+
+ mConnectivityManager = getSystemService(ConnectivityManager.class);
if (mSatelliteServiceConn == null) {
mSatelliteServiceConn = new TestSatelliteServiceConnection();
@@ -106,6 +124,21 @@
startActivity(intent);
}
});
+
+ findViewById(R.id.TestSatelliteConstrainConnection).setOnClickListener(view -> {
+ executor.execute(() -> {
+ Log.e(TAG, "onClick");
+ mSatelliteConstrainNetworkCallback = new NetworkCallback() {
+ @Override
+ public void onAvailable(final Network network) {
+ makeSatelliteDataConstrainedPing(network);
+ }
+ };
+ if(isNetworkRequested == false) {
+ requestingNetwork();
+ }
+ });
+ });
}
@Override
@@ -117,6 +150,61 @@
}
}
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if(isNetworkRequested == true) {
+ releasingNetwork();
+ }
+ }
+
+ private void requestingNetwork() {
+ Log.e(TAG, "Requesting Network");
+ isNetworkRequested = true;
+ NetworkRequest request = new NetworkRequest.Builder()
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
+ .removeCapability(NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED)
+ .addTransportType(NetworkCapabilities.TRANSPORT_SATELLITE)
+ .build();
+
+ // Requesting for Network
+ mConnectivityManager.requestNetwork(request, mSatelliteConstrainNetworkCallback);
+ Log.e(TAG, "onClick + " + request);
+ }
+
+
+ private void makeSatelliteDataConstrainedPing(final Network network) {
+ Log.e(TAG, "onAvailable + " + network);
+ mNetwork = network;
+
+ try {
+ PingTask pingTask = new PingTask();
+ Log.d(TAG, "Connecting Satellite for ping");
+ String pingResult = pingTask.ping(mNetwork);
+ if(pingResult != null) {
+ Toast.makeText(mContext, "Ping Passed!", Toast.LENGTH_SHORT).show();
+ } else {
+ Toast.makeText(mContext, "Ping Failed!", Toast.LENGTH_SHORT).show();
+ }
+ } catch (Exception e) {
+ Log.d(TAG, "Exception at ping: " + e);
+ } finally {
+ // Releasing the callback in the background thread
+ releasingNetwork();
+ }
+ }
+
+ private void releasingNetwork() {
+ Log.e(TAG, "Realsing Network");
+ try {
+ mConnectivityManager
+ .unregisterNetworkCallback(mSatelliteConstrainNetworkCallback);
+ } catch (Exception e) {
+ Log.d("SatelliteDataConstrined", "Exception: " + e);
+ }
+ isNetworkRequested = false;
+ }
private final ILocalSatelliteListener mSatelliteListener =
new ILocalSatelliteListener.Stub() {
diff --git a/tests/src/com/android/TelephonyTestBase.java b/tests/src/com/android/TelephonyTestBase.java
index 5e7cdda..d8c3727 100644
--- a/tests/src/com/android/TelephonyTestBase.java
+++ b/tests/src/com/android/TelephonyTestBase.java
@@ -24,7 +24,9 @@
import android.content.ContextWrapper;
import android.content.res.Resources;
import android.os.Handler;
+import android.os.HandlerThread;
import android.os.Looper;
+import android.os.TestLooperManager;
import android.util.Log;
import androidx.test.InstrumentationRegistry;
@@ -70,6 +72,10 @@
@Mock protected DataNetworkController mDataNetworkController;
@Mock private MetricsCollector mMetricsCollector;
+ private HandlerThread mTestHandlerThread;
+ protected Looper mTestLooper;
+ protected TestLooperManager mLooperManager;
+
private final HashMap<InstanceKey, Object> mOldInstances = new HashMap<>();
private final LinkedList<InstanceKey> mInstanceKeys = new LinkedList<>();
@@ -118,9 +124,47 @@
public void tearDown() throws Exception {
// Ensure there are no static references to handlers after test completes.
PhoneConfigurationManager.unregisterAllMultiSimConfigChangeRegistrants();
+ cleanupTestLooper();
restoreInstances();
}
+ protected void setupTestLooper() {
+ mTestHandlerThread = new HandlerThread("TestHandlerThread");
+ mTestHandlerThread.start();
+ mTestLooper = mTestHandlerThread.getLooper();
+ mLooperManager = new TestLooperManager(mTestLooper);
+ }
+
+ private void cleanupTestLooper() {
+ mTestLooper = null;
+ if (mLooperManager != null) {
+ mLooperManager.release();
+ mLooperManager = null;
+ }
+ if (mTestHandlerThread != null) {
+ mTestHandlerThread.quit();
+ try {
+ mTestHandlerThread.join();
+ } catch (InterruptedException ex) {
+ Log.w("TelephonyTestBase", "HandlerThread join interrupted", ex);
+ }
+ mTestHandlerThread = null;
+ }
+ }
+
+ protected void processOneMessage() {
+ var msg = mLooperManager.next();
+ mLooperManager.execute(msg);
+ mLooperManager.recycle(msg);
+ }
+
+ protected void processAllMessages() {
+ for (var msg = mLooperManager.poll(); msg != null && msg.getTarget() != null;) {
+ mLooperManager.execute(msg);
+ mLooperManager.recycle(msg);
+ }
+ }
+
protected final boolean waitForExecutorAction(Executor executor, long timeoutMillis) {
final CountDownLatch lock = new CountDownLatch(1);
executor.execute(() -> {
diff --git a/tests/src/com/android/phone/CarrierConfigLoaderTest.java b/tests/src/com/android/phone/CarrierConfigLoaderTest.java
index 324bcbc..5b306e6 100644
--- a/tests/src/com/android/phone/CarrierConfigLoaderTest.java
+++ b/tests/src/com/android/phone/CarrierConfigLoaderTest.java
@@ -52,7 +52,6 @@
import android.telephony.TelephonyManager;
import android.telephony.TelephonyRegistryManager;
import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
import androidx.test.InstrumentationRegistry;
@@ -82,7 +81,6 @@
* Unit Test for CarrierConfigLoader.
*/
@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
public class CarrierConfigLoaderTest extends TelephonyTestBase {
@Rule
public TestRule compatChangeRule = new PlatformCompatChangeRule();
@@ -109,7 +107,6 @@
private TelephonyManager mTelephonyManager;
private CarrierConfigLoader mCarrierConfigLoader;
private Handler mHandler;
- private TestableLooper mTestableLooper;
// The AIDL stub will use PermissionEnforcer to check permission from the caller.
private FakePermissionEnforcer mFakePermissionEnforcer = new FakePermissionEnforcer();
@@ -117,6 +114,7 @@
@Before
public void setUp() throws Exception {
super.setUp();
+ setupTestLooper();
doReturn(true).when(mPackageManager).hasSystemFeature(
eq(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION));
doReturn(Context.PERMISSION_ENFORCER_SERVICE).when(mContext).getSystemServiceName(
@@ -152,8 +150,7 @@
when(mContext.getSystemService(TelephonyRegistryManager.class)).thenReturn(
mTelephonyRegistryManager);
- mTestableLooper = TestableLooper.get(this);
- mCarrierConfigLoader = new CarrierConfigLoader(mContext, mTestableLooper.getLooper(),
+ mCarrierConfigLoader = new CarrierConfigLoader(mContext, mTestLooper,
mFeatureFlags);
mHandler = mCarrierConfigLoader.getHandler();
@@ -213,7 +210,10 @@
mCarrierConfigLoader.saveNoSimConfigToXml(PLATFORM_CARRIER_CONFIG_PACKAGE, config);
mCarrierConfigLoader.updateConfigForPhoneId(DEFAULT_PHONE_ID,
IccCardConstants.INTENT_VALUE_ICC_ABSENT);
- mTestableLooper.processAllMessages();
+ processOneMessage();
+ processOneMessage();
+ processOneMessage();
+ processOneMessage();
assertThat(mCarrierConfigLoader.getConfigFromDefaultApp(DEFAULT_PHONE_ID)).isNull();
assertThat(mCarrierConfigLoader.getConfigFromCarrierApp(DEFAULT_PHONE_ID)).isNull();
@@ -252,7 +252,7 @@
DEFAULT_PHONE_ID, carrierId, config);
mCarrierConfigLoader.updateConfigForPhoneId(DEFAULT_PHONE_ID,
IccCardConstants.INTENT_VALUE_ICC_LOADED);
- mTestableLooper.processAllMessages();
+ processAllMessages();
assertThat(mCarrierConfigLoader.getConfigFromDefaultApp(DEFAULT_PHONE_ID).getInt(
CARRIER_CONFIG_EXAMPLE_KEY)).isEqualTo(CARRIER_CONFIG_EXAMPLE_VALUE);
@@ -294,7 +294,8 @@
mCarrierConfigLoader.overrideConfig(DEFAULT_SUB_ID, null /*overrides*/,
false/*persistent*/);
- mTestableLooper.processAllMessages();
+ processOneMessage();
+ processOneMessage();
assertThat(mCarrierConfigLoader.getOverrideConfig(DEFAULT_PHONE_ID).isEmpty()).isTrue();
verify(mSubscriptionManagerService).updateSubscriptionByCarrierConfig(
@@ -316,7 +317,8 @@
PersistableBundle config = getTestConfig();
mCarrierConfigLoader.overrideConfig(DEFAULT_SUB_ID, config /*overrides*/,
false/*persistent*/);
- mTestableLooper.processAllMessages();
+ processOneMessage();
+ processOneMessage();
assertThat(mCarrierConfigLoader.getOverrideConfig(DEFAULT_PHONE_ID).getInt(
CARRIER_CONFIG_EXAMPLE_KEY)).isEqualTo(CARRIER_CONFIG_EXAMPLE_VALUE);
@@ -480,10 +482,10 @@
any(Intent.class), any(ServiceConnection.class), anyInt());
doNothing().when(mContext).sendBroadcastAsUser(any(Intent.class), any(UserHandle.class));
mHandler.sendMessage(mHandler.obtainMessage(17 /* EVENT_MULTI_SIM_CONFIG_CHANGED */));
- mTestableLooper.processAllMessages();
+ processAllMessages();
mCarrierConfigLoader.updateConfigForPhoneId(1, IccCardConstants.INTENT_VALUE_ICC_ABSENT);
- mTestableLooper.processAllMessages();
+ processAllMessages();
}
@Test
@@ -503,18 +505,20 @@
Mockito.clearInvocations(mTelephonyRegistryManager);
Mockito.clearInvocations(mContext);
mHandler.sendMessage(mHandler.obtainMessage(13 /* EVENT_SYSTEM_UNLOCKED */));
- mTestableLooper.processAllMessages();
+ processOneMessage();
mHandler.sendMessage(mHandler.obtainMessage(5 /* EVENT_FETCH_DEFAULT_DONE */));
- mTestableLooper.processAllMessages();
+ processOneMessage();
+ processOneMessage();
mHandler.sendMessage(mHandler.obtainMessage(6 /* EVENT_FETCH_CARRIER_DONE */));
- mTestableLooper.processAllMessages();
+ processOneMessage();
+ processOneMessage();
ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
verify(mSubscriptionManagerService).updateSubscriptionByCarrierConfig(eq(0), anyString(),
any(PersistableBundle.class), runnableCaptor.capture());
runnableCaptor.getValue().run();
- mTestableLooper.processAllMessages();
+ processAllMessages();
// Broadcast should be sent for backwards compatibility.
verify(mContext).sendBroadcastAsUser(any(Intent.class), any(UserHandle.class));
diff --git a/tests/src/com/android/phone/ImsStateCallbackControllerTest.java b/tests/src/com/android/phone/ImsStateCallbackControllerTest.java
index 5521ac0..0c91af7 100644
--- a/tests/src/com/android/phone/ImsStateCallbackControllerTest.java
+++ b/tests/src/com/android/phone/ImsStateCallbackControllerTest.java
@@ -985,7 +985,9 @@
when(mSubscriptionManager.getActiveSubscriptionIdList()).thenReturn(subIds);
}
- private void processAllMessages() {
+ // Override - not using mTestLooper from the base class
+ @Override
+ protected void processAllMessages() {
while (!mLooper.getLooper().getQueue().isIdle()) {
mLooper.processAllMessages();
}
diff --git a/tests/src/com/android/phone/PhoneInterfaceManagerTest.java b/tests/src/com/android/phone/PhoneInterfaceManagerTest.java
index 88b84a7..266481e 100644
--- a/tests/src/com/android/phone/PhoneInterfaceManagerTest.java
+++ b/tests/src/com/android/phone/PhoneInterfaceManagerTest.java
@@ -59,6 +59,7 @@
import com.android.internal.telephony.Phone;
import com.android.internal.telephony.RILConstants;
import com.android.internal.telephony.flags.FeatureFlags;
+import com.android.internal.telephony.satellite.SatelliteController;
import com.android.internal.telephony.subscription.SubscriptionManagerService;
import com.android.phone.satellite.accesscontrol.SatelliteAccessController;
@@ -118,6 +119,9 @@
replaceInstance(SatelliteAccessController.class, "sInstance", null,
Mockito.mock(SatelliteAccessController.class));
+ replaceInstance(SatelliteController.class, "sInstance", null,
+ Mockito.mock(SatelliteController.class));
+
mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(
InstrumentationRegistry.getInstrumentation().getTargetContext());
doReturn(mSharedPreferences).when(mPhoneGlobals)
diff --git a/tests/src/com/android/phone/slice/SlicePurchaseControllerTest.java b/tests/src/com/android/phone/slice/SlicePurchaseControllerTest.java
index 5637c3a..a792780 100644
--- a/tests/src/com/android/phone/slice/SlicePurchaseControllerTest.java
+++ b/tests/src/com/android/phone/slice/SlicePurchaseControllerTest.java
@@ -21,6 +21,7 @@
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyBoolean;
import static org.mockito.Mockito.anyInt;
@@ -40,6 +41,7 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
import android.os.AsyncResult;
import android.os.Handler;
import android.os.HandlerThread;
@@ -56,6 +58,7 @@
import android.telephony.data.UrspRule;
import android.testing.TestableLooper;
+import androidx.test.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.android.TelephonyTestBase;
@@ -335,6 +338,12 @@
mSlicePurchaseController.purchasePremiumCapability(
TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY, mHandler.obtainMessage());
mTestableLooper.processAllMessages();
+ if (isAutomotive()) {
+ // TODO(b/401032628): this test is flaky here
+ assumeTrue(
+ TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_NOT_AVAILABLE
+ != mResult);
+ }
assertEquals(
TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA_SUBSCRIPTION,
mResult);
@@ -350,6 +359,11 @@
mResult);
}
+ private boolean isAutomotive() {
+ return InstrumentationRegistry.getTargetContext().getPackageManager()
+ .hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
+ }
+
@Test
public void testPurchasePremiumCapabilityResultNetworkNotAvailable() {
doReturn((int) TelephonyManager.NETWORK_TYPE_BITMASK_NR).when(mPhone)
diff --git a/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java b/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java
index e27bea5..2605114 100644
--- a/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java
+++ b/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java
@@ -109,6 +109,8 @@
import com.android.internal.telephony.flags.Flags;
import com.android.internal.telephony.gsm.SuppServiceNotification;
import com.android.internal.telephony.imsphone.ImsPhone;
+import com.android.internal.telephony.imsphone.ImsPhoneCall;
+import com.android.internal.telephony.imsphone.ImsPhoneConnection;
import com.android.internal.telephony.satellite.SatelliteController;
import com.android.internal.telephony.satellite.SatelliteSOSMessageRecommender;
import com.android.internal.telephony.subscription.SubscriptionInfoInternal;
@@ -197,9 +199,15 @@
public void onHold() {
wasHeld = true;
}
+
+ @Override
+ void setOriginalConnection(com.android.internal.telephony.Connection connection) {
+ mOriginalConnection = connection;
+ }
}
public static class SimpleConference extends Conference {
+ public boolean wasDisconnected = false;
public boolean wasUnheld = false;
public SimpleConference(PhoneAccountHandle phoneAccountHandle) {
@@ -207,6 +215,11 @@
}
@Override
+ public void onDisconnect() {
+ wasDisconnected = true;
+ }
+
+ @Override
public void onUnhold() {
wasUnheld = true;
}
@@ -340,6 +353,7 @@
mBinderStub = (IConnectionService.Stub) mTestConnectionService.onBind(null);
mSetFlagsRule.enableFlags(Flags.FLAG_DO_NOT_OVERRIDE_PRECISE_LABEL);
mSetFlagsRule.enableFlags(Flags.FLAG_CALL_EXTRA_FOR_NON_HOLD_SUPPORTED_CARRIERS);
+ mSetFlagsRule.disableFlags(Flags.FLAG_HANGUP_ACTIVE_CALL_BASED_ON_EMERGENCY_CALL_DOMAIN);
}
@After
@@ -3739,6 +3753,234 @@
}
@Test
+ public void testDomainSelectionAddCsEmergencyCallWhenImsCallActive() throws Exception {
+ mSetFlagsRule.enableFlags(Flags.FLAG_HANGUP_ACTIVE_CALL_BASED_ON_EMERGENCY_CALL_DOMAIN);
+
+ setupForCallTest();
+ doReturn(1).when(mPhone0).getSubId();
+ doReturn(1).when(mImsPhone).getSubId();
+ ImsPhoneCall imsPhoneCall = Mockito.mock(ImsPhoneCall.class);
+ ImsPhoneConnection imsPhoneConnection = Mockito.mock(ImsPhoneConnection.class);
+ when(imsPhoneCall.getPhone()).thenReturn(mImsPhone);
+ when(imsPhoneConnection.getCall()).thenReturn(imsPhoneCall);
+ when(imsPhoneConnection.getPhoneType()).thenReturn(PhoneConstants.PHONE_TYPE_IMS);
+
+ // PROPERTY_IS_EXTERNAL_CALL: to avoid extra processing that is not related to this test.
+ SimpleTelephonyConnection tc1 = createTestConnection(PHONE_ACCOUNT_HANDLE_1,
+ android.telecom.Connection.PROPERTY_IS_EXTERNAL_CALL, false);
+ // IMS connection is set.
+ tc1.setOriginalConnection(imsPhoneConnection);
+ mTestConnectionService.addExistingConnection(PHONE_ACCOUNT_HANDLE_1, tc1);
+
+ assertEquals(1, mTestConnectionService.getAllConnections().size());
+ TelephonyConnection connection1 = (TelephonyConnection)
+ mTestConnectionService.getAllConnections().toArray()[0];
+ assertEquals(tc1, connection1);
+
+ // Add a CS emergency call.
+ String telecomCallId2 = "TC2";
+ int selectedDomain = DOMAIN_CS;
+ setupForDialForDomainSelection(mPhone0, selectedDomain, true);
+ getTestContext().getCarrierConfig(0 /*subId*/).putBoolean(
+ CarrierConfigManager.KEY_ALLOW_HOLD_CALL_DURING_EMERGENCY_BOOL, true);
+
+ mTestConnectionService.onCreateOutgoingConnection(PHONE_ACCOUNT_HANDLE_1,
+ createConnectionRequest(PHONE_ACCOUNT_HANDLE_1,
+ TEST_EMERGENCY_NUMBER, telecomCallId2));
+
+ // Hang up the active IMS call due to CS emergency call.
+ ArgumentCaptor<Connection.Listener> listenerCaptor =
+ ArgumentCaptor.forClass(Connection.Listener.class);
+ verify(imsPhoneConnection).addListener(listenerCaptor.capture());
+ assertTrue(tc1.wasDisconnected);
+
+ // Call disconnection completed.
+ Connection.Listener listener = listenerCaptor.getValue();
+ assertNotNull(listener);
+ listener.onDisconnect(0);
+
+ // Continue to proceed the outgoing emergency call after active call is disconnected.
+ ArgumentCaptor<android.telecom.Connection> connectionCaptor =
+ ArgumentCaptor.forClass(android.telecom.Connection.class);
+ verify(mDomainSelectionResolver)
+ .getDomainSelectionConnection(eq(mPhone0), eq(SELECTOR_TYPE_CALLING), eq(true));
+ verify(mEmergencyStateTracker)
+ .startEmergencyCall(eq(mPhone0), connectionCaptor.capture(), eq(false));
+ verify(mSatelliteSOSMessageRecommender, times(2))
+ .onEmergencyCallStarted(any(), anyBoolean());
+ verify(mEmergencyCallDomainSelectionConnection).createEmergencyConnection(any(), any());
+ verify(mPhone0).dial(anyString(), any(), any());
+
+ android.telecom.Connection tc = connectionCaptor.getValue();
+ assertNotNull(tc);
+ assertEquals(telecomCallId2, tc.getTelecomCallId());
+ assertEquals(mTestConnectionService.getEmergencyConnection(), tc);
+ }
+
+ @Test
+ public void testDomainSelectionAddImsEmergencyCallWhenCsCallActive() throws Exception {
+ mSetFlagsRule.enableFlags(Flags.FLAG_HANGUP_ACTIVE_CALL_BASED_ON_EMERGENCY_CALL_DOMAIN);
+
+ setupForCallTest();
+
+ // PROPERTY_IS_EXTERNAL_CALL: to avoid extra processing that is not related to this test.
+ SimpleTelephonyConnection tc1 = createTestConnection(PHONE_ACCOUNT_HANDLE_1,
+ android.telecom.Connection.PROPERTY_IS_EXTERNAL_CALL, false);
+ // CS connection is set.
+ tc1.setOriginalConnection(mInternalConnection);
+ mTestConnectionService.addExistingConnection(PHONE_ACCOUNT_HANDLE_1, tc1);
+
+ assertEquals(1, mTestConnectionService.getAllConnections().size());
+ TelephonyConnection connection1 = (TelephonyConnection)
+ mTestConnectionService.getAllConnections().toArray()[0];
+ assertEquals(tc1, connection1);
+
+ // Add an IMS emergency call.
+ String telecomCallId2 = "TC2";
+ int selectedDomain = DOMAIN_PS;
+ setupForDialForDomainSelection(mPhone0, selectedDomain, true);
+ getTestContext().getCarrierConfig(0 /*subId*/).putBoolean(
+ CarrierConfigManager.KEY_ALLOW_HOLD_CALL_DURING_EMERGENCY_BOOL, true);
+
+ mTestConnectionService.onCreateOutgoingConnection(PHONE_ACCOUNT_HANDLE_1,
+ createConnectionRequest(PHONE_ACCOUNT_HANDLE_1,
+ TEST_EMERGENCY_NUMBER, telecomCallId2));
+
+ // Hang up the active CS call due to IMS emergency call.
+ ArgumentCaptor<Connection.Listener> listenerCaptor =
+ ArgumentCaptor.forClass(Connection.Listener.class);
+ verify(mInternalConnection).addListener(listenerCaptor.capture());
+ assertTrue(tc1.wasDisconnected);
+
+ // Call disconnection completed.
+ Connection.Listener listener = listenerCaptor.getValue();
+ assertNotNull(listener);
+ listener.onDisconnect(0);
+
+ // Continue to proceed the outgoing emergency call after active call is disconnected.
+ ArgumentCaptor<android.telecom.Connection> connectionCaptor =
+ ArgumentCaptor.forClass(android.telecom.Connection.class);
+ verify(mDomainSelectionResolver)
+ .getDomainSelectionConnection(eq(mPhone0), eq(SELECTOR_TYPE_CALLING), eq(true));
+ verify(mEmergencyStateTracker)
+ .startEmergencyCall(eq(mPhone0), connectionCaptor.capture(), eq(false));
+ verify(mSatelliteSOSMessageRecommender, times(2))
+ .onEmergencyCallStarted(any(), anyBoolean());
+ verify(mEmergencyCallDomainSelectionConnection).createEmergencyConnection(any(), any());
+ verify(mPhone0).dial(anyString(), any(), any());
+
+ android.telecom.Connection tc = connectionCaptor.getValue();
+ assertNotNull(tc);
+ assertEquals(telecomCallId2, tc.getTelecomCallId());
+ assertEquals(mTestConnectionService.getEmergencyConnection(), tc);
+ }
+
+ @Test
+ @SmallTest
+ public void testDomainSelectionMaybeDisconnectCallsOnOtherDomainWhenNoActiveCalls() {
+ SimpleTelephonyConnection ec = createTestConnection(PHONE_ACCOUNT_HANDLE_1, 0, true);
+ Consumer<Boolean> consumer = (result) -> {
+ if (!result) {
+ fail("Unexpected result=" + result);
+ }
+ };
+ CompletableFuture<Void> unused =
+ TelephonyConnectionService.maybeDisconnectCallsOnOtherDomain(mPhone0,
+ ec, DOMAIN_PS, Collections.emptyList(), Collections.emptyList(), consumer);
+
+ assertTrue(unused.isDone());
+ }
+
+ @Test
+ @SmallTest
+ public void testDomainSelectionMaybeDisconnectCallsOnOtherDomainWhenConferenceOnly() {
+ setupForCallTest();
+ ArrayList<android.telecom.Conference> conferences = new ArrayList<>();
+ SimpleTelephonyConnection tc1 = createTestConnection(PHONE_ACCOUNT_HANDLE_1, 0, false);
+ SimpleConference conference = createTestConference(PHONE_ACCOUNT_HANDLE_1, 0);
+ tc1.setOriginalConnection(mInternalConnection);
+ conference.addConnection(tc1);
+ conferences.add(conference);
+
+ SimpleTelephonyConnection ec = createTestConnection(PHONE_ACCOUNT_HANDLE_1, 0, true);
+ Consumer<Boolean> consumer = (result) -> {
+ if (!result) {
+ fail("Unexpected result=" + result);
+ }
+ };
+ CompletableFuture<Void> unused =
+ TelephonyConnectionService.maybeDisconnectCallsOnOtherDomain(
+ mPhone0, ec, DOMAIN_PS, Collections.emptyList(), conferences, consumer);
+
+ assertTrue(unused.isDone());
+ assertTrue(conference.wasDisconnected);
+ }
+
+ @Test
+ @SmallTest
+ public void testDomainSelectionMaybeDisconnectCallsOnOtherDomainWhenActiveCall() {
+ setupForCallTest();
+ ArrayList<android.telecom.Connection> connections = new ArrayList<>();
+ ArrayList<android.telecom.Conference> conferences = new ArrayList<>();
+ SimpleTelephonyConnection tc1 = createTestConnection(PHONE_ACCOUNT_HANDLE_1, 0, false);
+ SimpleConference conference = createTestConference(PHONE_ACCOUNT_HANDLE_1, 0);
+ tc1.setOriginalConnection(mInternalConnection);
+ connections.add(tc1);
+ conference.addConnection(tc1);
+ conferences.add(conference);
+
+ SimpleTelephonyConnection ec = createTestConnection(PHONE_ACCOUNT_HANDLE_1, 0, true);
+ Consumer<Boolean> consumer = (result) -> {
+ if (!result) {
+ fail("Unexpected result=" + result);
+ }
+ };
+ CompletableFuture<Void> unused =
+ TelephonyConnectionService.maybeDisconnectCallsOnOtherDomain(
+ mPhone0, ec, DOMAIN_PS, connections, conferences, consumer);
+
+ assertFalse(unused.isDone());
+ assertTrue(tc1.wasDisconnected);
+ assertTrue(conference.wasDisconnected);
+
+ ArgumentCaptor<Connection.Listener> listenerCaptor =
+ ArgumentCaptor.forClass(Connection.Listener.class);
+ verify(mInternalConnection).addListener(listenerCaptor.capture());
+
+ // Call disconnection completed.
+ Connection.Listener listener = listenerCaptor.getValue();
+ assertNotNull(listener);
+ listener.onDisconnect(0);
+
+ assertTrue(unused.isDone());
+ }
+
+ @Test
+ @SmallTest
+ public void testDomainSelectionMaybeDisconnectCallsOnOtherDomainWhenExceptionOccurs() {
+ setupForCallTest();
+ ArrayList<android.telecom.Connection> connections = new ArrayList<>();
+ SimpleTelephonyConnection tc1 = createTestConnection(PHONE_ACCOUNT_HANDLE_1, 0, false);
+ tc1.setOriginalConnection(mInternalConnection);
+ connections.add(tc1);
+ doThrow(new NullPointerException("Intended: Connection is null"))
+ .when(mInternalConnection).addListener(any());
+
+ SimpleTelephonyConnection ec = createTestConnection(PHONE_ACCOUNT_HANDLE_1, 0, true);
+ Consumer<Boolean> consumer = (result) -> {
+ if (result) {
+ fail("Unexpected result=" + result);
+ }
+ };
+ CompletableFuture<Void> unused =
+ TelephonyConnectionService.maybeDisconnectCallsOnOtherDomain(
+ mPhone0, ec, DOMAIN_PS, connections, Collections.emptyList(), consumer);
+
+ assertTrue(unused.isDone());
+ assertFalse(tc1.wasDisconnected);
+ }
+
+ @Test
public void testDomainSelectionWithMmiCode() {
//UT domain selection should not be handled by new domain selector.
doNothing().when(mContext).startActivityAsUser(any(), any());