Loading src/java/com/android/internal/telephony/emergency/EmergencyStateTracker.java +167 −14 Original line number Diff line number Diff line Loading @@ -39,14 +39,18 @@ import android.os.UserHandle; import android.preference.PreferenceManager; import android.provider.Settings; import android.sysprop.TelephonyProperties; import android.telephony.AccessNetworkConstants; import android.telephony.Annotation.DisconnectCauses; import android.telephony.CarrierConfigManager; import android.telephony.DisconnectCause; import android.telephony.EmergencyRegResult; import android.telephony.NetworkRegistrationInfo; import android.telephony.PreciseDataConnectionState; import android.telephony.ServiceState; import android.telephony.SubscriptionManager; import android.telephony.TelephonyCallback; import android.telephony.TelephonyManager; import android.telephony.data.ApnSetting; import android.util.ArraySet; import com.android.internal.annotations.VisibleForTesting; Loading @@ -65,6 +69,7 @@ import java.util.Arrays; import java.util.Objects; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.function.Consumer; /** Loading @@ -83,6 +88,7 @@ public class EmergencyStateTracker { private static final boolean DEFAULT_EMERGENCY_CALLBACK_MODE_SUPPORTED = true; /** Default Emergency Callback Mode exit timeout value. */ private static final long DEFAULT_ECM_EXIT_TIMEOUT_MS = 300000; private static final int DEFAULT_EPDN_DISCONNECTION_TIMEOUT_MS = 500; /** The emergency types used when setting the emergency mode on modem. */ @Retention(RetentionPolicy.SOURCE) Loading Loading @@ -120,6 +126,9 @@ public class EmergencyStateTracker { private final Runnable mExitEcmRunnable = this::exitEmergencyCallbackMode; // Tracks emergency calls by callId that have reached {@link Call.State#ACTIVE}. private final Set<String> mActiveEmergencyCalls = new ArraySet<>(); private Phone mPhoneToExit; private int mPdnDisconnectionTimeoutMs = DEFAULT_EPDN_DISCONNECTION_TIMEOUT_MS; private final Object mLock = new Object(); private Phone mPhone; // Tracks ongoing emergency callId to handle a second emergency call private String mOngoingCallId; Loading Loading @@ -165,6 +174,29 @@ public class EmergencyStateTracker { } }; /** * TelephonyCallback used to monitor whether ePDN on cellular network is disconnected or not. */ private final class PreciseDataConnectionStateListener extends TelephonyCallback implements TelephonyCallback.PreciseDataConnectionStateListener { @Override public void onPreciseDataConnectionStateChanged( @NonNull PreciseDataConnectionState dataConnectionState) { ApnSetting apnSetting = dataConnectionState.getApnSetting(); if ((apnSetting == null) || ((apnSetting.getApnTypeBitmask() | ApnSetting.TYPE_EMERGENCY) == 0) || (dataConnectionState.getTransportType() != AccessNetworkConstants.TRANSPORT_TYPE_WWAN)) { return; } int state = dataConnectionState.getState(); Rlog.d(TAG, "onPreciseDataConnectionStateChanged ePDN state=" + state); if (state == TelephonyManager.DATA_DISCONNECTED) exitEmergencyModeIfDelayed(); } } private PreciseDataConnectionStateListener mDataConnectionStateListener; /** PhoneFactory Dependencies for testing. */ @VisibleForTesting public interface PhoneFactoryProxy { Loading @@ -188,6 +220,8 @@ public class EmergencyStateTracker { @VisibleForTesting public interface TelephonyManagerProxy { int getPhoneCount(); void registerTelephonyCallback(int subId, Executor executor, TelephonyCallback callback); void unregisterTelephonyCallback(TelephonyCallback callback); } private final TelephonyManagerProxy mTelephonyManagerProxy; Loading @@ -195,7 +229,6 @@ public class EmergencyStateTracker { private static class TelephonyManagerProxyImpl implements TelephonyManagerProxy { private final TelephonyManager mTelephonyManager; TelephonyManagerProxyImpl(Context context) { mTelephonyManager = new TelephonyManager(context); } Loading @@ -204,6 +237,18 @@ public class EmergencyStateTracker { public int getPhoneCount() { return mTelephonyManager.getActiveModemCount(); } @Override public void registerTelephonyCallback(int subId, Executor executor, TelephonyCallback callback) { TelephonyManager tm = mTelephonyManager.createForSubscriptionId(subId); tm.registerTelephonyCallback(executor, callback); } @Override public void unregisterTelephonyCallback(TelephonyCallback callback) { mTelephonyManager.unregisterTelephonyCallback(callback); } } /** Loading @@ -215,11 +260,15 @@ public class EmergencyStateTracker { } @VisibleForTesting public static final int MSG_SET_EMERGENCY_MODE_DONE = 1; public static final int MSG_SET_EMERGENCY_MODE = 1; @VisibleForTesting public static final int MSG_EXIT_EMERGENCY_MODE = 2; @VisibleForTesting public static final int MSG_EXIT_EMERGENCY_MODE_DONE = 2; public static final int MSG_SET_EMERGENCY_MODE_DONE = 3; @VisibleForTesting public static final int MSG_SET_EMERGENCY_CALLBACK_MODE_DONE = 3; public static final int MSG_EXIT_EMERGENCY_MODE_DONE = 4; @VisibleForTesting public static final int MSG_SET_EMERGENCY_CALLBACK_MODE_DONE = 5; private class MyHandler extends Handler { Loading Loading @@ -263,7 +312,7 @@ public class EmergencyStateTracker { if (mIsEmergencyCallStartedDuringEmergencySms) { Phone phone = mPhone; mPhone = null; exitEmergencyMode(mSmsPhone, emergencyType); exitEmergencyMode(mSmsPhone, emergencyType, false); // Restore call phone for further use. mPhone = phone; Loading Loading @@ -322,6 +371,27 @@ public class EmergencyStateTracker { } break; } case MSG_EXIT_EMERGENCY_MODE: { Rlog.v(TAG, "MSG_EXIT_EMERGENCY_MODE"); exitEmergencyModeIfDelayed(); break; } case MSG_SET_EMERGENCY_MODE: { AsyncResult ar = (AsyncResult) msg.obj; Integer emergencyType = (Integer) ar.userObj; Rlog.v(TAG, "MSG_SET_EMERGENCY_MODE for " + emergencyTypeToString(emergencyType) + ", " + mEmergencyMode); // Should be reached here only when starting a new emergency service // while exiting emergency callback mode on the other slot. if (mEmergencyMode != MODE_EMERGENCY_WWAN) return; final Phone phone = (mPhone != null) ? mPhone : mSmsPhone; if (phone != null) { mWasEmergencyModeSetOnModem = true; phone.setEmergencyMode(MODE_EMERGENCY_WWAN, mHandler.obtainMessage(MSG_SET_EMERGENCY_MODE_DONE, emergencyType)); } break; } default: break; } Loading Loading @@ -459,7 +529,7 @@ public class EmergencyStateTracker { // exit the emergency mode when receiving the result of setting the emergency mode and // the emergency mode for this call will be restarted after the exit complete. if (isInEmergencyMode() && !isEmergencyModeInProgress()) { exitEmergencyMode(mSmsPhone, EMERGENCY_TYPE_SMS); exitEmergencyMode(mSmsPhone, EMERGENCY_TYPE_SMS, false); } mPhone = phone; Loading Loading @@ -511,7 +581,7 @@ public class EmergencyStateTracker { MSG_SET_EMERGENCY_CALLBACK_MODE_DONE); } } else { exitEmergencyMode(mPhone, EMERGENCY_TYPE_CALL); exitEmergencyMode(mPhone, EMERGENCY_TYPE_CALL, false); clearEmergencyCallInfo(); } } Loading Loading @@ -576,9 +646,28 @@ public class EmergencyStateTracker { return; } synchronized (mLock) { unregisterForDataConnectionStateChanges(); if (mPhoneToExit != null) { if (emergencyType != EMERGENCY_TYPE_CALL) { setIsInEmergencyCall(false); } mOnEcmExitCompleteRunnable = null; if (mPhoneToExit != phone) { // Exit emergency mode on the other phone first, // then set emergency mode on the given phone. mPhoneToExit.exitEmergencyMode( mHandler.obtainMessage(MSG_SET_EMERGENCY_MODE, Integer.valueOf(emergencyType))); mPhoneToExit = null; return; } mPhoneToExit = null; } mWasEmergencyModeSetOnModem = true; phone.setEmergencyMode(mode, m); } } private void completeEmergencyMode(@EmergencyType int emergencyType) { completeEmergencyMode(emergencyType, DisconnectCause.NOT_DISCONNECTED); Loading Loading @@ -650,8 +739,10 @@ public class EmergencyStateTracker { * * @param phone the {@code Phone} to exit the emergency mode. * @param emergencyType the emergency type to identify an emergency call or SMS. * @param waitForPdnDisconnect indicates whether it shall wait for the disconnection of ePDN. */ private void exitEmergencyMode(Phone phone, @EmergencyType int emergencyType) { private void exitEmergencyMode(Phone phone, @EmergencyType int emergencyType, boolean waitForPdnDisconnect) { Rlog.i(TAG, "exitEmergencyMode for " + emergencyTypeToString(emergencyType)); if (emergencyType == EMERGENCY_TYPE_CALL) { Loading Loading @@ -687,9 +778,24 @@ public class EmergencyStateTracker { return; } synchronized (mLock) { mWasEmergencyModeSetOnModem = false; if (waitForPdnDisconnect) { registerForDataConnectionStateChanges(phone); mPhoneToExit = phone; if (mPdnDisconnectionTimeoutMs > 0) { // To avoid waiting for the disconnection indefinitely. mHandler.sendEmptyMessageDelayed(MSG_EXIT_EMERGENCY_MODE, mPdnDisconnectionTimeoutMs); } return; } else { unregisterForDataConnectionStateChanges(); mPhoneToExit = null; } phone.exitEmergencyMode(m); } } /** Returns last {@link EmergencyRegResult} as set by {@code setEmergencyMode()}. */ public EmergencyRegResult getEmergencyRegResult() { Loading Loading @@ -887,7 +993,9 @@ public class EmergencyStateTracker { gsmCdmaPhone.notifyEmergencyCallRegistrants(false); // Exit emergency mode on modem. exitEmergencyMode(gsmCdmaPhone, EMERGENCY_TYPE_CALL); // b/299866883: Wait for the disconnection of ePDN before calling exitEmergencyMode. exitEmergencyMode(gsmCdmaPhone, EMERGENCY_TYPE_CALL, mEmergencyCallDomain == NetworkRegistrationInfo.DOMAIN_PS); } mEmergencyCallDomain = NetworkRegistrationInfo.DOMAIN_UNKNOWN; Loading Loading @@ -1026,7 +1134,7 @@ public class EmergencyStateTracker { MSG_SET_EMERGENCY_CALLBACK_MODE_DONE); } } else { exitEmergencyMode(mSmsPhone, EMERGENCY_TYPE_SMS); exitEmergencyMode(mSmsPhone, EMERGENCY_TYPE_SMS, false); } clearEmergencySmsInfo(); Loading Loading @@ -1334,4 +1442,49 @@ public class EmergencyStateTracker { Rlog.i(TAG, "updateNoSimEcbmSupported preference updated slotIndex=" + slotIndex + ", supported=" + carrierConfig); } /** For test purpose only */ @VisibleForTesting public void setPdnDisconnectionTimeoutMs(int timeout) { mPdnDisconnectionTimeoutMs = timeout; } private void exitEmergencyModeIfDelayed() { synchronized (mLock) { if (mPhoneToExit != null) { unregisterForDataConnectionStateChanges(); mPhoneToExit.exitEmergencyMode( mHandler.obtainMessage(MSG_EXIT_EMERGENCY_MODE_DONE, Integer.valueOf(EMERGENCY_TYPE_CALL))); mPhoneToExit = null; } } } /** * Registers for changes to data connection state. */ private void registerForDataConnectionStateChanges(Phone phone) { if ((mDataConnectionStateListener != null) || (phone == null)) { return; } Rlog.i(TAG, "registerForDataConnectionStateChanges"); mDataConnectionStateListener = new PreciseDataConnectionStateListener(); mTelephonyManagerProxy.registerTelephonyCallback(phone.getSubId(), mHandler::post, mDataConnectionStateListener); } /** * Unregisters for changes to data connection state. */ private void unregisterForDataConnectionStateChanges() { if (mDataConnectionStateListener == null) { return; } Rlog.i(TAG, "unregisterForDataConnectionStateChanges"); mTelephonyManagerProxy.unregisterTelephonyCallback(mDataConnectionStateListener); mDataConnectionStateListener = null; } } tests/telephonytests/src/com/android/internal/telephony/emergency/EmergencyStateTrackerTest.java +211 −0 Original line number Diff line number Diff line Loading @@ -57,6 +57,7 @@ import android.telephony.DisconnectCause; import android.telephony.EmergencyRegResult; import android.telephony.ServiceState; import android.telephony.SubscriptionManager; import android.telephony.TelephonyCallback; import android.telephony.TelephonyManager; import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; Loading Loading @@ -597,6 +598,216 @@ public class EmergencyStateTrackerTest extends TelephonyTest { assertFalse(emergencyStateTracker.isInEmergencyMode()); } /** * Test that once endCall() for IMS call is called and we enter ECM, then we exit ECM * after the specified timeout. */ @Test @SmallTest public void endCall_entersEcm_thenExitsEcmAfterTimeoutImsCall() throws Exception { // Setup EmergencyStateTracker EmergencyStateTracker emergencyStateTracker = setupEmergencyStateTracker( /* isSuplDdsSwitchRequiredForEmergencyCall= */ true); // Create test Phone Phone testPhone = setupTestPhoneForEmergencyCall(/* isRoaming= */ true, /* isRadioOn= */ true); setUpAsyncResultForSetEmergencyMode(testPhone, E_REG_RESULT); setUpAsyncResultForExitEmergencyMode(testPhone); CompletableFuture<Integer> unused = emergencyStateTracker.startEmergencyCall(testPhone, TEST_CALL_ID, false); // Set call to ACTIVE emergencyStateTracker.onEmergencyCallStateChanged(Call.State.ACTIVE, TEST_CALL_ID); emergencyStateTracker.onEmergencyCallDomainUpdated( PhoneConstants.PHONE_TYPE_IMS, TEST_CALL_ID); // Set ecm as supported setEcmSupportedConfig(testPhone, /* ecmSupported= */ true); processAllMessages(); emergencyStateTracker.endCall(TEST_CALL_ID); assertTrue(emergencyStateTracker.isInEcm()); Context mockContext = mock(Context.class); replaceInstance(EmergencyStateTracker.class, "mContext", emergencyStateTracker, mockContext); processAllFutureMessages(); ArgumentCaptor<TelephonyCallback> callbackCaptor = ArgumentCaptor.forClass(TelephonyCallback.class); verify(mTelephonyManagerProxy).registerTelephonyCallback(eq(testPhone.getSubId()), any(), callbackCaptor.capture()); TelephonyCallback callback = callbackCaptor.getValue(); assertNotNull(callback); // Verify exitEmergencyMode() is called after timeout verify(testPhone).exitEmergencyMode(any(Message.class)); assertFalse(emergencyStateTracker.isInEmergencyMode()); assertFalse(emergencyStateTracker.isInEcm()); verify(mTelephonyManagerProxy).unregisterTelephonyCallback(eq(callback)); } /** * Test that startEmergencyCall() is called right after exiting ECM on the same slot. */ @Test @SmallTest public void exitEcm_thenDialEmergencyCallOnTheSameSlotRightAfter() throws Exception { // Setup EmergencyStateTracker EmergencyStateTracker emergencyStateTracker = setupEmergencyStateTracker( /* isSuplDdsSwitchRequiredForEmergencyCall= */ true); emergencyStateTracker.setPdnDisconnectionTimeoutMs(0); // Create test Phone Phone testPhone = setupTestPhoneForEmergencyCall(/* isRoaming= */ true, /* isRadioOn= */ true); setUpAsyncResultForSetEmergencyMode(testPhone, E_REG_RESULT); setUpAsyncResultForExitEmergencyMode(testPhone); verify(testPhone, times(0)).setEmergencyMode(anyInt(), any(Message.class)); verify(testPhone, times(0)).exitEmergencyMode(any(Message.class)); CompletableFuture<Integer> unused = emergencyStateTracker.startEmergencyCall(testPhone, TEST_CALL_ID, false); processAllMessages(); verify(testPhone, times(1)).setEmergencyMode(eq(MODE_EMERGENCY_WWAN), any(Message.class)); verify(testPhone, times(0)).exitEmergencyMode(any(Message.class)); // Set call to ACTIVE emergencyStateTracker.onEmergencyCallStateChanged(Call.State.ACTIVE, TEST_CALL_ID); emergencyStateTracker.onEmergencyCallDomainUpdated( PhoneConstants.PHONE_TYPE_IMS, TEST_CALL_ID); // Set ecm as supported setEcmSupportedConfig(testPhone, /* ecmSupported= */ true); processAllMessages(); emergencyStateTracker.endCall(TEST_CALL_ID); assertTrue(emergencyStateTracker.isInEcm()); Context mockContext = mock(Context.class); replaceInstance(EmergencyStateTracker.class, "mContext", emergencyStateTracker, mockContext); processAllFutureMessages(); ArgumentCaptor<TelephonyCallback> callbackCaptor = ArgumentCaptor.forClass(TelephonyCallback.class); verify(mTelephonyManagerProxy).registerTelephonyCallback(eq(testPhone.getSubId()), any(), callbackCaptor.capture()); TelephonyCallback callback = callbackCaptor.getValue(); assertNotNull(callback); assertFalse(emergencyStateTracker.isInEmergencyMode()); assertFalse(emergencyStateTracker.isInEcm()); verify(mTelephonyManagerProxy, times(0)).unregisterTelephonyCallback(eq(callback)); verify(testPhone, times(1)).setEmergencyMode(eq(MODE_EMERGENCY_WWAN), any(Message.class)); verify(testPhone, times(1)).setEmergencyMode(eq(MODE_EMERGENCY_CALLBACK), any(Message.class)); verify(testPhone, times(0)).exitEmergencyMode(any(Message.class)); replaceInstance(EmergencyStateTracker.class, "mContext", emergencyStateTracker, mContext); // dial on the same slot unused = emergencyStateTracker.startEmergencyCall(testPhone, TEST_CALL_ID, false); processAllMessages(); assertTrue(emergencyStateTracker.isInEmergencyMode()); assertFalse(emergencyStateTracker.isInEcm()); verify(mTelephonyManagerProxy, times(1)).unregisterTelephonyCallback(eq(callback)); verify(testPhone, times(2)).setEmergencyMode(eq(MODE_EMERGENCY_WWAN), any(Message.class)); verify(testPhone, times(1)).setEmergencyMode(eq(MODE_EMERGENCY_CALLBACK), any(Message.class)); verify(testPhone, times(0)).exitEmergencyMode(any(Message.class)); } /** * Test that startEmergencyCall() is called right after exiting ECM on the other slot. */ @Test @SmallTest public void exitEcm_thenDialEmergencyCallOnTheOtherSlotRightAfter() throws Exception { // Setup EmergencyStateTracker EmergencyStateTracker emergencyStateTracker = setupEmergencyStateTracker( /* isSuplDdsSwitchRequiredForEmergencyCall= */ true); emergencyStateTracker.setPdnDisconnectionTimeoutMs(0); // Create test Phone Phone testPhone = setupTestPhoneForEmergencyCall(/* isRoaming= */ true, /* isRadioOn= */ true); setUpAsyncResultForSetEmergencyMode(testPhone, E_REG_RESULT); setUpAsyncResultForExitEmergencyMode(testPhone); verify(testPhone, times(0)).setEmergencyMode(anyInt(), any(Message.class)); verify(testPhone, times(0)).exitEmergencyMode(any(Message.class)); CompletableFuture<Integer> unused = emergencyStateTracker.startEmergencyCall(testPhone, TEST_CALL_ID, false); processAllMessages(); verify(testPhone, times(1)).setEmergencyMode(eq(MODE_EMERGENCY_WWAN), any(Message.class)); verify(testPhone, times(0)).exitEmergencyMode(any(Message.class)); // Set call to ACTIVE emergencyStateTracker.onEmergencyCallStateChanged(Call.State.ACTIVE, TEST_CALL_ID); emergencyStateTracker.onEmergencyCallDomainUpdated( PhoneConstants.PHONE_TYPE_IMS, TEST_CALL_ID); // Set ecm as supported setEcmSupportedConfig(testPhone, /* ecmSupported= */ true); processAllMessages(); emergencyStateTracker.endCall(TEST_CALL_ID); assertTrue(emergencyStateTracker.isInEcm()); Context mockContext = mock(Context.class); replaceInstance(EmergencyStateTracker.class, "mContext", emergencyStateTracker, mockContext); processAllFutureMessages(); ArgumentCaptor<TelephonyCallback> callbackCaptor = ArgumentCaptor.forClass(TelephonyCallback.class); verify(mTelephonyManagerProxy).registerTelephonyCallback(eq(testPhone.getSubId()), any(), callbackCaptor.capture()); TelephonyCallback callback = callbackCaptor.getValue(); assertNotNull(callback); assertFalse(emergencyStateTracker.isInEmergencyMode()); assertFalse(emergencyStateTracker.isInEcm()); verify(mTelephonyManagerProxy, times(0)).unregisterTelephonyCallback(eq(callback)); verify(testPhone, times(1)).setEmergencyMode(eq(MODE_EMERGENCY_WWAN), any(Message.class)); verify(testPhone, times(1)).setEmergencyMode(eq(MODE_EMERGENCY_CALLBACK), any(Message.class)); verify(testPhone, times(0)).exitEmergencyMode(any(Message.class)); Phone phone1 = getPhone(1); verify(phone1, times(0)).setEmergencyMode(anyInt(), any(Message.class)); verify(phone1, times(0)).exitEmergencyMode(any(Message.class)); replaceInstance(EmergencyStateTracker.class, "mContext", emergencyStateTracker, mContext); // dial on the other slot unused = emergencyStateTracker.startEmergencyCall(phone1, TEST_CALL_ID, false); processAllMessages(); assertTrue(emergencyStateTracker.isInEmergencyMode()); assertFalse(emergencyStateTracker.isInEcm()); verify(mTelephonyManagerProxy, times(1)).unregisterTelephonyCallback(eq(callback)); verify(testPhone, times(1)).setEmergencyMode(eq(MODE_EMERGENCY_WWAN), any(Message.class)); verify(testPhone, times(1)).setEmergencyMode(eq(MODE_EMERGENCY_CALLBACK), any(Message.class)); verify(testPhone, times(1)).exitEmergencyMode(any(Message.class)); verify(phone1, times(1)).setEmergencyMode(eq(MODE_EMERGENCY_WWAN), any(Message.class)); verify(phone1, times(0)).exitEmergencyMode(any(Message.class)); } /** * Test that after exitEmergencyCallbackMode() is called, the correct intents are sent and * emergency mode is exited on the modem. Loading Loading
src/java/com/android/internal/telephony/emergency/EmergencyStateTracker.java +167 −14 Original line number Diff line number Diff line Loading @@ -39,14 +39,18 @@ import android.os.UserHandle; import android.preference.PreferenceManager; import android.provider.Settings; import android.sysprop.TelephonyProperties; import android.telephony.AccessNetworkConstants; import android.telephony.Annotation.DisconnectCauses; import android.telephony.CarrierConfigManager; import android.telephony.DisconnectCause; import android.telephony.EmergencyRegResult; import android.telephony.NetworkRegistrationInfo; import android.telephony.PreciseDataConnectionState; import android.telephony.ServiceState; import android.telephony.SubscriptionManager; import android.telephony.TelephonyCallback; import android.telephony.TelephonyManager; import android.telephony.data.ApnSetting; import android.util.ArraySet; import com.android.internal.annotations.VisibleForTesting; Loading @@ -65,6 +69,7 @@ import java.util.Arrays; import java.util.Objects; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.function.Consumer; /** Loading @@ -83,6 +88,7 @@ public class EmergencyStateTracker { private static final boolean DEFAULT_EMERGENCY_CALLBACK_MODE_SUPPORTED = true; /** Default Emergency Callback Mode exit timeout value. */ private static final long DEFAULT_ECM_EXIT_TIMEOUT_MS = 300000; private static final int DEFAULT_EPDN_DISCONNECTION_TIMEOUT_MS = 500; /** The emergency types used when setting the emergency mode on modem. */ @Retention(RetentionPolicy.SOURCE) Loading Loading @@ -120,6 +126,9 @@ public class EmergencyStateTracker { private final Runnable mExitEcmRunnable = this::exitEmergencyCallbackMode; // Tracks emergency calls by callId that have reached {@link Call.State#ACTIVE}. private final Set<String> mActiveEmergencyCalls = new ArraySet<>(); private Phone mPhoneToExit; private int mPdnDisconnectionTimeoutMs = DEFAULT_EPDN_DISCONNECTION_TIMEOUT_MS; private final Object mLock = new Object(); private Phone mPhone; // Tracks ongoing emergency callId to handle a second emergency call private String mOngoingCallId; Loading Loading @@ -165,6 +174,29 @@ public class EmergencyStateTracker { } }; /** * TelephonyCallback used to monitor whether ePDN on cellular network is disconnected or not. */ private final class PreciseDataConnectionStateListener extends TelephonyCallback implements TelephonyCallback.PreciseDataConnectionStateListener { @Override public void onPreciseDataConnectionStateChanged( @NonNull PreciseDataConnectionState dataConnectionState) { ApnSetting apnSetting = dataConnectionState.getApnSetting(); if ((apnSetting == null) || ((apnSetting.getApnTypeBitmask() | ApnSetting.TYPE_EMERGENCY) == 0) || (dataConnectionState.getTransportType() != AccessNetworkConstants.TRANSPORT_TYPE_WWAN)) { return; } int state = dataConnectionState.getState(); Rlog.d(TAG, "onPreciseDataConnectionStateChanged ePDN state=" + state); if (state == TelephonyManager.DATA_DISCONNECTED) exitEmergencyModeIfDelayed(); } } private PreciseDataConnectionStateListener mDataConnectionStateListener; /** PhoneFactory Dependencies for testing. */ @VisibleForTesting public interface PhoneFactoryProxy { Loading @@ -188,6 +220,8 @@ public class EmergencyStateTracker { @VisibleForTesting public interface TelephonyManagerProxy { int getPhoneCount(); void registerTelephonyCallback(int subId, Executor executor, TelephonyCallback callback); void unregisterTelephonyCallback(TelephonyCallback callback); } private final TelephonyManagerProxy mTelephonyManagerProxy; Loading @@ -195,7 +229,6 @@ public class EmergencyStateTracker { private static class TelephonyManagerProxyImpl implements TelephonyManagerProxy { private final TelephonyManager mTelephonyManager; TelephonyManagerProxyImpl(Context context) { mTelephonyManager = new TelephonyManager(context); } Loading @@ -204,6 +237,18 @@ public class EmergencyStateTracker { public int getPhoneCount() { return mTelephonyManager.getActiveModemCount(); } @Override public void registerTelephonyCallback(int subId, Executor executor, TelephonyCallback callback) { TelephonyManager tm = mTelephonyManager.createForSubscriptionId(subId); tm.registerTelephonyCallback(executor, callback); } @Override public void unregisterTelephonyCallback(TelephonyCallback callback) { mTelephonyManager.unregisterTelephonyCallback(callback); } } /** Loading @@ -215,11 +260,15 @@ public class EmergencyStateTracker { } @VisibleForTesting public static final int MSG_SET_EMERGENCY_MODE_DONE = 1; public static final int MSG_SET_EMERGENCY_MODE = 1; @VisibleForTesting public static final int MSG_EXIT_EMERGENCY_MODE = 2; @VisibleForTesting public static final int MSG_EXIT_EMERGENCY_MODE_DONE = 2; public static final int MSG_SET_EMERGENCY_MODE_DONE = 3; @VisibleForTesting public static final int MSG_SET_EMERGENCY_CALLBACK_MODE_DONE = 3; public static final int MSG_EXIT_EMERGENCY_MODE_DONE = 4; @VisibleForTesting public static final int MSG_SET_EMERGENCY_CALLBACK_MODE_DONE = 5; private class MyHandler extends Handler { Loading Loading @@ -263,7 +312,7 @@ public class EmergencyStateTracker { if (mIsEmergencyCallStartedDuringEmergencySms) { Phone phone = mPhone; mPhone = null; exitEmergencyMode(mSmsPhone, emergencyType); exitEmergencyMode(mSmsPhone, emergencyType, false); // Restore call phone for further use. mPhone = phone; Loading Loading @@ -322,6 +371,27 @@ public class EmergencyStateTracker { } break; } case MSG_EXIT_EMERGENCY_MODE: { Rlog.v(TAG, "MSG_EXIT_EMERGENCY_MODE"); exitEmergencyModeIfDelayed(); break; } case MSG_SET_EMERGENCY_MODE: { AsyncResult ar = (AsyncResult) msg.obj; Integer emergencyType = (Integer) ar.userObj; Rlog.v(TAG, "MSG_SET_EMERGENCY_MODE for " + emergencyTypeToString(emergencyType) + ", " + mEmergencyMode); // Should be reached here only when starting a new emergency service // while exiting emergency callback mode on the other slot. if (mEmergencyMode != MODE_EMERGENCY_WWAN) return; final Phone phone = (mPhone != null) ? mPhone : mSmsPhone; if (phone != null) { mWasEmergencyModeSetOnModem = true; phone.setEmergencyMode(MODE_EMERGENCY_WWAN, mHandler.obtainMessage(MSG_SET_EMERGENCY_MODE_DONE, emergencyType)); } break; } default: break; } Loading Loading @@ -459,7 +529,7 @@ public class EmergencyStateTracker { // exit the emergency mode when receiving the result of setting the emergency mode and // the emergency mode for this call will be restarted after the exit complete. if (isInEmergencyMode() && !isEmergencyModeInProgress()) { exitEmergencyMode(mSmsPhone, EMERGENCY_TYPE_SMS); exitEmergencyMode(mSmsPhone, EMERGENCY_TYPE_SMS, false); } mPhone = phone; Loading Loading @@ -511,7 +581,7 @@ public class EmergencyStateTracker { MSG_SET_EMERGENCY_CALLBACK_MODE_DONE); } } else { exitEmergencyMode(mPhone, EMERGENCY_TYPE_CALL); exitEmergencyMode(mPhone, EMERGENCY_TYPE_CALL, false); clearEmergencyCallInfo(); } } Loading Loading @@ -576,9 +646,28 @@ public class EmergencyStateTracker { return; } synchronized (mLock) { unregisterForDataConnectionStateChanges(); if (mPhoneToExit != null) { if (emergencyType != EMERGENCY_TYPE_CALL) { setIsInEmergencyCall(false); } mOnEcmExitCompleteRunnable = null; if (mPhoneToExit != phone) { // Exit emergency mode on the other phone first, // then set emergency mode on the given phone. mPhoneToExit.exitEmergencyMode( mHandler.obtainMessage(MSG_SET_EMERGENCY_MODE, Integer.valueOf(emergencyType))); mPhoneToExit = null; return; } mPhoneToExit = null; } mWasEmergencyModeSetOnModem = true; phone.setEmergencyMode(mode, m); } } private void completeEmergencyMode(@EmergencyType int emergencyType) { completeEmergencyMode(emergencyType, DisconnectCause.NOT_DISCONNECTED); Loading Loading @@ -650,8 +739,10 @@ public class EmergencyStateTracker { * * @param phone the {@code Phone} to exit the emergency mode. * @param emergencyType the emergency type to identify an emergency call or SMS. * @param waitForPdnDisconnect indicates whether it shall wait for the disconnection of ePDN. */ private void exitEmergencyMode(Phone phone, @EmergencyType int emergencyType) { private void exitEmergencyMode(Phone phone, @EmergencyType int emergencyType, boolean waitForPdnDisconnect) { Rlog.i(TAG, "exitEmergencyMode for " + emergencyTypeToString(emergencyType)); if (emergencyType == EMERGENCY_TYPE_CALL) { Loading Loading @@ -687,9 +778,24 @@ public class EmergencyStateTracker { return; } synchronized (mLock) { mWasEmergencyModeSetOnModem = false; if (waitForPdnDisconnect) { registerForDataConnectionStateChanges(phone); mPhoneToExit = phone; if (mPdnDisconnectionTimeoutMs > 0) { // To avoid waiting for the disconnection indefinitely. mHandler.sendEmptyMessageDelayed(MSG_EXIT_EMERGENCY_MODE, mPdnDisconnectionTimeoutMs); } return; } else { unregisterForDataConnectionStateChanges(); mPhoneToExit = null; } phone.exitEmergencyMode(m); } } /** Returns last {@link EmergencyRegResult} as set by {@code setEmergencyMode()}. */ public EmergencyRegResult getEmergencyRegResult() { Loading Loading @@ -887,7 +993,9 @@ public class EmergencyStateTracker { gsmCdmaPhone.notifyEmergencyCallRegistrants(false); // Exit emergency mode on modem. exitEmergencyMode(gsmCdmaPhone, EMERGENCY_TYPE_CALL); // b/299866883: Wait for the disconnection of ePDN before calling exitEmergencyMode. exitEmergencyMode(gsmCdmaPhone, EMERGENCY_TYPE_CALL, mEmergencyCallDomain == NetworkRegistrationInfo.DOMAIN_PS); } mEmergencyCallDomain = NetworkRegistrationInfo.DOMAIN_UNKNOWN; Loading Loading @@ -1026,7 +1134,7 @@ public class EmergencyStateTracker { MSG_SET_EMERGENCY_CALLBACK_MODE_DONE); } } else { exitEmergencyMode(mSmsPhone, EMERGENCY_TYPE_SMS); exitEmergencyMode(mSmsPhone, EMERGENCY_TYPE_SMS, false); } clearEmergencySmsInfo(); Loading Loading @@ -1334,4 +1442,49 @@ public class EmergencyStateTracker { Rlog.i(TAG, "updateNoSimEcbmSupported preference updated slotIndex=" + slotIndex + ", supported=" + carrierConfig); } /** For test purpose only */ @VisibleForTesting public void setPdnDisconnectionTimeoutMs(int timeout) { mPdnDisconnectionTimeoutMs = timeout; } private void exitEmergencyModeIfDelayed() { synchronized (mLock) { if (mPhoneToExit != null) { unregisterForDataConnectionStateChanges(); mPhoneToExit.exitEmergencyMode( mHandler.obtainMessage(MSG_EXIT_EMERGENCY_MODE_DONE, Integer.valueOf(EMERGENCY_TYPE_CALL))); mPhoneToExit = null; } } } /** * Registers for changes to data connection state. */ private void registerForDataConnectionStateChanges(Phone phone) { if ((mDataConnectionStateListener != null) || (phone == null)) { return; } Rlog.i(TAG, "registerForDataConnectionStateChanges"); mDataConnectionStateListener = new PreciseDataConnectionStateListener(); mTelephonyManagerProxy.registerTelephonyCallback(phone.getSubId(), mHandler::post, mDataConnectionStateListener); } /** * Unregisters for changes to data connection state. */ private void unregisterForDataConnectionStateChanges() { if (mDataConnectionStateListener == null) { return; } Rlog.i(TAG, "unregisterForDataConnectionStateChanges"); mTelephonyManagerProxy.unregisterTelephonyCallback(mDataConnectionStateListener); mDataConnectionStateListener = null; } }
tests/telephonytests/src/com/android/internal/telephony/emergency/EmergencyStateTrackerTest.java +211 −0 Original line number Diff line number Diff line Loading @@ -57,6 +57,7 @@ import android.telephony.DisconnectCause; import android.telephony.EmergencyRegResult; import android.telephony.ServiceState; import android.telephony.SubscriptionManager; import android.telephony.TelephonyCallback; import android.telephony.TelephonyManager; import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; Loading Loading @@ -597,6 +598,216 @@ public class EmergencyStateTrackerTest extends TelephonyTest { assertFalse(emergencyStateTracker.isInEmergencyMode()); } /** * Test that once endCall() for IMS call is called and we enter ECM, then we exit ECM * after the specified timeout. */ @Test @SmallTest public void endCall_entersEcm_thenExitsEcmAfterTimeoutImsCall() throws Exception { // Setup EmergencyStateTracker EmergencyStateTracker emergencyStateTracker = setupEmergencyStateTracker( /* isSuplDdsSwitchRequiredForEmergencyCall= */ true); // Create test Phone Phone testPhone = setupTestPhoneForEmergencyCall(/* isRoaming= */ true, /* isRadioOn= */ true); setUpAsyncResultForSetEmergencyMode(testPhone, E_REG_RESULT); setUpAsyncResultForExitEmergencyMode(testPhone); CompletableFuture<Integer> unused = emergencyStateTracker.startEmergencyCall(testPhone, TEST_CALL_ID, false); // Set call to ACTIVE emergencyStateTracker.onEmergencyCallStateChanged(Call.State.ACTIVE, TEST_CALL_ID); emergencyStateTracker.onEmergencyCallDomainUpdated( PhoneConstants.PHONE_TYPE_IMS, TEST_CALL_ID); // Set ecm as supported setEcmSupportedConfig(testPhone, /* ecmSupported= */ true); processAllMessages(); emergencyStateTracker.endCall(TEST_CALL_ID); assertTrue(emergencyStateTracker.isInEcm()); Context mockContext = mock(Context.class); replaceInstance(EmergencyStateTracker.class, "mContext", emergencyStateTracker, mockContext); processAllFutureMessages(); ArgumentCaptor<TelephonyCallback> callbackCaptor = ArgumentCaptor.forClass(TelephonyCallback.class); verify(mTelephonyManagerProxy).registerTelephonyCallback(eq(testPhone.getSubId()), any(), callbackCaptor.capture()); TelephonyCallback callback = callbackCaptor.getValue(); assertNotNull(callback); // Verify exitEmergencyMode() is called after timeout verify(testPhone).exitEmergencyMode(any(Message.class)); assertFalse(emergencyStateTracker.isInEmergencyMode()); assertFalse(emergencyStateTracker.isInEcm()); verify(mTelephonyManagerProxy).unregisterTelephonyCallback(eq(callback)); } /** * Test that startEmergencyCall() is called right after exiting ECM on the same slot. */ @Test @SmallTest public void exitEcm_thenDialEmergencyCallOnTheSameSlotRightAfter() throws Exception { // Setup EmergencyStateTracker EmergencyStateTracker emergencyStateTracker = setupEmergencyStateTracker( /* isSuplDdsSwitchRequiredForEmergencyCall= */ true); emergencyStateTracker.setPdnDisconnectionTimeoutMs(0); // Create test Phone Phone testPhone = setupTestPhoneForEmergencyCall(/* isRoaming= */ true, /* isRadioOn= */ true); setUpAsyncResultForSetEmergencyMode(testPhone, E_REG_RESULT); setUpAsyncResultForExitEmergencyMode(testPhone); verify(testPhone, times(0)).setEmergencyMode(anyInt(), any(Message.class)); verify(testPhone, times(0)).exitEmergencyMode(any(Message.class)); CompletableFuture<Integer> unused = emergencyStateTracker.startEmergencyCall(testPhone, TEST_CALL_ID, false); processAllMessages(); verify(testPhone, times(1)).setEmergencyMode(eq(MODE_EMERGENCY_WWAN), any(Message.class)); verify(testPhone, times(0)).exitEmergencyMode(any(Message.class)); // Set call to ACTIVE emergencyStateTracker.onEmergencyCallStateChanged(Call.State.ACTIVE, TEST_CALL_ID); emergencyStateTracker.onEmergencyCallDomainUpdated( PhoneConstants.PHONE_TYPE_IMS, TEST_CALL_ID); // Set ecm as supported setEcmSupportedConfig(testPhone, /* ecmSupported= */ true); processAllMessages(); emergencyStateTracker.endCall(TEST_CALL_ID); assertTrue(emergencyStateTracker.isInEcm()); Context mockContext = mock(Context.class); replaceInstance(EmergencyStateTracker.class, "mContext", emergencyStateTracker, mockContext); processAllFutureMessages(); ArgumentCaptor<TelephonyCallback> callbackCaptor = ArgumentCaptor.forClass(TelephonyCallback.class); verify(mTelephonyManagerProxy).registerTelephonyCallback(eq(testPhone.getSubId()), any(), callbackCaptor.capture()); TelephonyCallback callback = callbackCaptor.getValue(); assertNotNull(callback); assertFalse(emergencyStateTracker.isInEmergencyMode()); assertFalse(emergencyStateTracker.isInEcm()); verify(mTelephonyManagerProxy, times(0)).unregisterTelephonyCallback(eq(callback)); verify(testPhone, times(1)).setEmergencyMode(eq(MODE_EMERGENCY_WWAN), any(Message.class)); verify(testPhone, times(1)).setEmergencyMode(eq(MODE_EMERGENCY_CALLBACK), any(Message.class)); verify(testPhone, times(0)).exitEmergencyMode(any(Message.class)); replaceInstance(EmergencyStateTracker.class, "mContext", emergencyStateTracker, mContext); // dial on the same slot unused = emergencyStateTracker.startEmergencyCall(testPhone, TEST_CALL_ID, false); processAllMessages(); assertTrue(emergencyStateTracker.isInEmergencyMode()); assertFalse(emergencyStateTracker.isInEcm()); verify(mTelephonyManagerProxy, times(1)).unregisterTelephonyCallback(eq(callback)); verify(testPhone, times(2)).setEmergencyMode(eq(MODE_EMERGENCY_WWAN), any(Message.class)); verify(testPhone, times(1)).setEmergencyMode(eq(MODE_EMERGENCY_CALLBACK), any(Message.class)); verify(testPhone, times(0)).exitEmergencyMode(any(Message.class)); } /** * Test that startEmergencyCall() is called right after exiting ECM on the other slot. */ @Test @SmallTest public void exitEcm_thenDialEmergencyCallOnTheOtherSlotRightAfter() throws Exception { // Setup EmergencyStateTracker EmergencyStateTracker emergencyStateTracker = setupEmergencyStateTracker( /* isSuplDdsSwitchRequiredForEmergencyCall= */ true); emergencyStateTracker.setPdnDisconnectionTimeoutMs(0); // Create test Phone Phone testPhone = setupTestPhoneForEmergencyCall(/* isRoaming= */ true, /* isRadioOn= */ true); setUpAsyncResultForSetEmergencyMode(testPhone, E_REG_RESULT); setUpAsyncResultForExitEmergencyMode(testPhone); verify(testPhone, times(0)).setEmergencyMode(anyInt(), any(Message.class)); verify(testPhone, times(0)).exitEmergencyMode(any(Message.class)); CompletableFuture<Integer> unused = emergencyStateTracker.startEmergencyCall(testPhone, TEST_CALL_ID, false); processAllMessages(); verify(testPhone, times(1)).setEmergencyMode(eq(MODE_EMERGENCY_WWAN), any(Message.class)); verify(testPhone, times(0)).exitEmergencyMode(any(Message.class)); // Set call to ACTIVE emergencyStateTracker.onEmergencyCallStateChanged(Call.State.ACTIVE, TEST_CALL_ID); emergencyStateTracker.onEmergencyCallDomainUpdated( PhoneConstants.PHONE_TYPE_IMS, TEST_CALL_ID); // Set ecm as supported setEcmSupportedConfig(testPhone, /* ecmSupported= */ true); processAllMessages(); emergencyStateTracker.endCall(TEST_CALL_ID); assertTrue(emergencyStateTracker.isInEcm()); Context mockContext = mock(Context.class); replaceInstance(EmergencyStateTracker.class, "mContext", emergencyStateTracker, mockContext); processAllFutureMessages(); ArgumentCaptor<TelephonyCallback> callbackCaptor = ArgumentCaptor.forClass(TelephonyCallback.class); verify(mTelephonyManagerProxy).registerTelephonyCallback(eq(testPhone.getSubId()), any(), callbackCaptor.capture()); TelephonyCallback callback = callbackCaptor.getValue(); assertNotNull(callback); assertFalse(emergencyStateTracker.isInEmergencyMode()); assertFalse(emergencyStateTracker.isInEcm()); verify(mTelephonyManagerProxy, times(0)).unregisterTelephonyCallback(eq(callback)); verify(testPhone, times(1)).setEmergencyMode(eq(MODE_EMERGENCY_WWAN), any(Message.class)); verify(testPhone, times(1)).setEmergencyMode(eq(MODE_EMERGENCY_CALLBACK), any(Message.class)); verify(testPhone, times(0)).exitEmergencyMode(any(Message.class)); Phone phone1 = getPhone(1); verify(phone1, times(0)).setEmergencyMode(anyInt(), any(Message.class)); verify(phone1, times(0)).exitEmergencyMode(any(Message.class)); replaceInstance(EmergencyStateTracker.class, "mContext", emergencyStateTracker, mContext); // dial on the other slot unused = emergencyStateTracker.startEmergencyCall(phone1, TEST_CALL_ID, false); processAllMessages(); assertTrue(emergencyStateTracker.isInEmergencyMode()); assertFalse(emergencyStateTracker.isInEcm()); verify(mTelephonyManagerProxy, times(1)).unregisterTelephonyCallback(eq(callback)); verify(testPhone, times(1)).setEmergencyMode(eq(MODE_EMERGENCY_WWAN), any(Message.class)); verify(testPhone, times(1)).setEmergencyMode(eq(MODE_EMERGENCY_CALLBACK), any(Message.class)); verify(testPhone, times(1)).exitEmergencyMode(any(Message.class)); verify(phone1, times(1)).setEmergencyMode(eq(MODE_EMERGENCY_WWAN), any(Message.class)); verify(phone1, times(0)).exitEmergencyMode(any(Message.class)); } /** * Test that after exitEmergencyCallbackMode() is called, the correct intents are sent and * emergency mode is exited on the modem. Loading