block call forwarding MMI codes from apps
Block call forwarding MMI codes from apps unless they are the
default dialer or system dialer
Also trampoline to default dialer using ACTION_DIAL
Bug: 268341970
Test: atest as well as manual test using adb shell
Change-Id: I4da38ff24595d4444e03ae363d3c8a51cc4087cb
diff --git a/src/com/android/server/telecom/CallIntentProcessor.java b/src/com/android/server/telecom/CallIntentProcessor.java
index 7f864b8..7953324 100644
--- a/src/com/android/server/telecom/CallIntentProcessor.java
+++ b/src/com/android/server/telecom/CallIntentProcessor.java
@@ -182,9 +182,10 @@
boolean isPrivilegedDialer = defaultDialerCache.isDefaultOrSystemDialer(callingPackage,
initiatingUser.getIdentifier());
+
NewOutgoingCallIntentBroadcaster broadcaster = new NewOutgoingCallIntentBroadcaster(
context, callsManager, intent, callsManager.getPhoneNumberUtilsAdapter(),
- isPrivilegedDialer, defaultDialerCache);
+ isPrivilegedDialer, defaultDialerCache, new MmiUtils());
// If the broadcaster comes back with an immediate error, disconnect and show a dialog.
NewOutgoingCallIntentBroadcaster.CallDisposition disposition = broadcaster.evaluateCall();
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index bc16ea3..ccc8e59 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -477,6 +477,7 @@
private AnomalyReporterAdapter mAnomalyReporter = new AnomalyReporterAdapterImpl();
+ private final MmiUtils mMmiUtils = new MmiUtils();
/**
* Listener to PhoneAccountRegistrar events.
*/
@@ -1863,7 +1864,7 @@
CompletableFuture<Call> makeRoomForCall = setAccountHandle.thenComposeAsync(
potentialPhoneAccounts -> {
Log.i(CallsManager.this, "make room for outgoing call stage");
- if (isPotentialInCallMMICode(handle) && !isSelfManaged) {
+ if (mMmiUtils.isPotentialInCallMMICode(handle) && !isSelfManaged) {
return CompletableFuture.completedFuture(finalCall);
}
// If a call is being reused, then it has already passed the
@@ -2106,7 +2107,7 @@
setIntentExtrasAndStartTime(callToUse, extras);
setCallSourceToAnalytics(callToUse, originalIntent);
- if (isPotentialMMICode(handle) && !isSelfManaged) {
+ if (mMmiUtils.isPotentialMMICode(handle) && !isSelfManaged) {
// Do not add the call if it is a potential MMI code.
callToUse.addListener(this);
} else if (!mCalls.contains(callToUse)) {
@@ -4420,37 +4421,6 @@
}
}
- private boolean isPotentialMMICode(Uri handle) {
- return (handle != null && handle.getSchemeSpecificPart() != null
- && handle.getSchemeSpecificPart().contains("#"));
- }
-
- /**
- * Determines if a dialed number is potentially an In-Call MMI code. In-Call MMI codes are
- * MMI codes which can be dialed when one or more calls are in progress.
- * <P>
- * Checks for numbers formatted similar to the MMI codes defined in:
- * {@link com.android.internal.telephony.Phone#handleInCallMmiCommands(String)}
- *
- * @param handle The URI to call.
- * @return {@code True} if the URI represents a number which could be an in-call MMI code.
- */
- private boolean isPotentialInCallMMICode(Uri handle) {
- if (handle != null && handle.getSchemeSpecificPart() != null &&
- handle.getScheme() != null &&
- handle.getScheme().equals(PhoneAccount.SCHEME_TEL)) {
-
- String dialedNumber = handle.getSchemeSpecificPart();
- return (dialedNumber.equals("0") ||
- (dialedNumber.startsWith("1") && dialedNumber.length() <= 2) ||
- (dialedNumber.startsWith("2") && dialedNumber.length() <= 2) ||
- dialedNumber.equals("3") ||
- dialedNumber.equals("4") ||
- dialedNumber.equals("5"));
- }
- return false;
- }
-
/**
* Determines if there are any ongoing self managed calls for the given package/user.
* @param packageName The package name to check.
@@ -5523,8 +5493,10 @@
* @param call The call.
*/
private void maybeShowErrorDialogOnDisconnect(Call call) {
- if (call.getState() == CallState.DISCONNECTED && (isPotentialMMICode(call.getHandle())
- || isPotentialInCallMMICode(call.getHandle())) && !mCalls.contains(call)) {
+ if (call.getState() == CallState.DISCONNECTED && (mMmiUtils.isPotentialMMICode(
+ call.getHandle())
+ || mMmiUtils.isPotentialInCallMMICode(call.getHandle())) && !mCalls.contains(
+ call)) {
DisconnectCause disconnectCause = call.getDisconnectCause();
if (!TextUtils.isEmpty(disconnectCause.getDescription()) && ((disconnectCause.getCode()
== DisconnectCause.ERROR) || (disconnectCause.getCode()
diff --git a/src/com/android/server/telecom/MmiUtils.java b/src/com/android/server/telecom/MmiUtils.java
new file mode 100644
index 0000000..11f6d59
--- /dev/null
+++ b/src/com/android/server/telecom/MmiUtils.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2023 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.server.telecom;
+
+import android.net.Uri;
+import android.telecom.PhoneAccount;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class MmiUtils {
+ // See TS 22.030 6.5.2 "Structure of the MMI"
+
+ private static Pattern sPatternSuppService = Pattern.compile(
+ "((\\*|#|\\*#|\\*\\*|##)(\\d{2,3})(\\*([^*#]*)(\\*([^*#]*)(\\*([^*#]*)(\\*([^*#]*))?)?)?)?#)(.*)");
+ /* 1 2 3 4 5 6 7 8 9 10 11
+ 12
+
+ 1 = Full string up to and including #
+ 2 = action (activation/interrogation/registration/erasure)
+ 3 = service code
+ 5 = SIA
+ 7 = SIB
+ 9 = SIC
+ 10 = dialing number
+ */
+ //regex groups
+ static final int MATCH_GROUP_POUND_STRING = 1;
+ static final int MATCH_GROUP_ACTION = 2; //(activation/interrogation/registration/erasure)
+ static final int MATCH_GROUP_SERVICE_CODE = 3;
+ static final int MATCH_GROUP_SIA = 5;
+ static final int MATCH_GROUP_SIB = 7;
+ static final int MATCH_GROUP_SIC = 9;
+ static final int MATCH_GROUP_PWD_CONFIRM = 11;
+ static final int MATCH_GROUP_DIALING_NUMBER = 12;
+ // Call Forwarding service codes
+ static final String SC_CFU = "21";
+ static final String SC_CFB = "67";
+ static final String SC_CFNRy = "61";
+ static final String SC_CFNR = "62";
+ static final String SC_CF_All = "002";
+ static final String SC_CF_All_Conditional = "004";
+
+ //see: https://nationalnanpa.com/number_resource_info/vsc_assignments.html
+ @SuppressWarnings("DoubleBraceInitialization")
+ private static Set<String> sDangerousVerticalServiceCodes = new HashSet<String>()
+ {{
+ add("*09"); //Selective Call Blocking/Reporting
+ add("*42"); //Change Forward-To Number for Cust Programmable Call Forwarding Don't Answer
+ add("*56"); //Change Forward-To Number for ISDN Call Forwarding
+ add("*60"); //Selective Call Rejection Activation
+ add("*63"); //Selective Call Forwarding Activation
+ add("*64"); //Selective Call Acceptance Activation
+ add("*68"); //Call Forwarding Busy Line/Don't Answer Activation
+ add("*72"); //Call Forwarding Activation
+ add("*77"); //Anonymous Call Rejection Activation
+ add("*78"); //Do Not Disturb Activation
+ }};
+ private final int mMinLenInDangerousSet;
+ private final int mMaxLenInDangerousSet;
+
+ public MmiUtils() {
+ mMinLenInDangerousSet = sDangerousVerticalServiceCodes.stream()
+ .mapToInt(String::length)
+ .min()
+ .getAsInt();
+ mMaxLenInDangerousSet = sDangerousVerticalServiceCodes.stream()
+ .mapToInt(String::length)
+ .max()
+ .getAsInt();
+ }
+
+ /**
+ * Determines if the Uri represents a call forwarding related mmi code
+ *
+ * @param handle The URI to call.
+ * @return {@code True} if the URI represents a call forwarding related MMI
+ */
+ private static boolean isCallForwardingMmiCode(Uri handle) {
+ Matcher m;
+ String dialString = handle.getSchemeSpecificPart();
+ m = sPatternSuppService.matcher(dialString);
+
+ if (m.matches()) {
+ String sc = m.group(MATCH_GROUP_SERVICE_CODE);
+ return sc != null &&
+ (sc.equals(SC_CFU)
+ || sc.equals(SC_CFB) || sc.equals(SC_CFNRy)
+ || sc.equals(SC_CFNR) || sc.equals(SC_CF_All)
+ || sc.equals(SC_CF_All_Conditional));
+ }
+
+ return false;
+
+ }
+
+ private static boolean isTelScheme(Uri handle) {
+ return (handle != null && handle.getSchemeSpecificPart() != null &&
+ handle.getScheme() != null &&
+ handle.getScheme().equals(PhoneAccount.SCHEME_TEL));
+ }
+
+ private boolean isDangerousVerticalServiceCode(Uri handle) {
+ if (isTelScheme(handle)) {
+ String dialedNumber = handle.getSchemeSpecificPart();
+ if (dialedNumber.length() >= mMinLenInDangerousSet && dialedNumber.charAt(0) == '*') {
+ //we only check vertical codes defined by The North American Numbering Plan Admin
+ //see: https://nationalnanpa.com/number_resource_info/vsc_assignments.html
+ //only two or 3-digit codes are valid as of today, but the code is generic enough.
+ for (int prefixLen = mMaxLenInDangerousSet; prefixLen <= mMaxLenInDangerousSet;
+ prefixLen++) {
+ String prefix = dialedNumber.substring(0, prefixLen);
+ if (sDangerousVerticalServiceCodes.contains(prefix)) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Determines if a dialed number is potentially an In-Call MMI code. In-Call MMI codes are
+ * MMI codes which can be dialed when one or more calls are in progress.
+ * <P>
+ * Checks for numbers formatted similar to the MMI codes defined in:
+ * {@link com.android.internal.telephony.Phone#handleInCallMmiCommands(String)}
+ *
+ * @param handle The URI to call.
+ * @return {@code True} if the URI represents a number which could be an in-call MMI code.
+ */
+ public boolean isPotentialInCallMMICode(Uri handle) {
+ if (isTelScheme(handle)) {
+ String dialedNumber = handle.getSchemeSpecificPart();
+ return (dialedNumber.equals("0") ||
+ (dialedNumber.startsWith("1") && dialedNumber.length() <= 2) ||
+ (dialedNumber.startsWith("2") && dialedNumber.length() <= 2) ||
+ dialedNumber.equals("3") ||
+ dialedNumber.equals("4") ||
+ dialedNumber.equals("5"));
+ }
+ return false;
+ }
+
+ public boolean isPotentialMMICode(Uri handle) {
+ return (handle != null && handle.getSchemeSpecificPart() != null
+ && handle.getSchemeSpecificPart().contains("#"));
+ }
+
+ /**
+ * Determines if the Uri represents a dangerous MMI code or Vertical Service code. Dangerous
+ * codes are ones, for which,
+ * we normally expect the user to be aware that an application has dialed them
+ *
+ * @param handle The URI to call.
+ * @return {@code True} if the URI represents a dangerous code
+ */
+ public boolean isDangerousMmiOrVerticalCode(Uri handle) {
+ if (isPotentialMMICode(handle)) {
+ return isCallForwardingMmiCode(handle);
+ //since some dangerous mmi codes could be carrier specific, in the future,
+ //we can add a carrier config item which can list carrier specific dangerous mmi codes
+ } else if (isDangerousVerticalServiceCode(handle)) {
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java b/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
index 41aa2fb..8426d1f 100644
--- a/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
+++ b/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
@@ -16,14 +16,12 @@
package com.android.server.telecom;
-import android.app.AppOpsManager;
-
import android.app.Activity;
+import android.app.AppOpsManager;
import android.app.BroadcastOptions;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
-import android.content.res.Resources;
import android.net.Uri;
import android.os.Bundle;
import android.os.Trace;
@@ -78,6 +76,7 @@
private final PhoneNumberUtilsAdapter mPhoneNumberUtilsAdapter;
private final TelecomSystem.SyncRoot mLock;
private final DefaultDialerCache mDefaultDialerCache;
+ private final MmiUtils mMmiUtils;
/*
* Whether or not the outgoing call intent originated from the default phone application. If
@@ -101,7 +100,7 @@
@VisibleForTesting
public NewOutgoingCallIntentBroadcaster(Context context, CallsManager callsManager,
Intent intent, PhoneNumberUtilsAdapter phoneNumberUtilsAdapter,
- boolean isDefaultPhoneApp, DefaultDialerCache defaultDialerCache) {
+ boolean isDefaultPhoneApp, DefaultDialerCache defaultDialerCache, MmiUtils mmiUtils) {
mContext = context;
mCallsManager = callsManager;
mIntent = intent;
@@ -109,6 +108,7 @@
mIsDefaultOrSystemPhoneApp = isDefaultPhoneApp;
mLock = mCallsManager.getLock();
mDefaultDialerCache = defaultDialerCache;
+ mMmiUtils = mmiUtils;
}
/**
@@ -291,6 +291,16 @@
result.callImmediately = true;
result.requestRedirection = false;
}
+ } else if (mMmiUtils.isDangerousMmiOrVerticalCode(intent.getData())) {
+ if (!mIsDefaultOrSystemPhoneApp) {
+ Log.w(this,
+ "Potentially dangerous MMI code %s with CALL Intent %s can only be "
+ + "sent if caller is the system or default dialer",
+ number, intent);
+ launchSystemDialer(intent.getData());
+ result.disconnectCause = DisconnectCause.OUTGOING_CANCELED;
+ return result;
+ }
}
} else if (Intent.ACTION_CALL_EMERGENCY.equals(action)) {
if (!isEmergencyNumber) {
diff --git a/tests/src/com/android/server/telecom/tests/MmiUtilsTest.java b/tests/src/com/android/server/telecom/tests/MmiUtilsTest.java
new file mode 100644
index 0000000..ed74637
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/MmiUtilsTest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2023 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.server.telecom.tests;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.net.Uri;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.server.telecom.MmiUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class MmiUtilsTest extends TelecomTestCase {
+
+ private static final String[] sDangerousDialStrings = {
+ "*21*1234567#", // fwd unconditionally to 1234567,
+ "*67*1234567#", // fwd to 1234567 when line is busy
+ "*61*1234567#", // fwd to 1234567 when no one picks up
+ "*62*1234567#", // fwd to 1234567 when out of range
+ "*004*1234567#", // fwd to 1234567 when busy, not pickup up, out of range
+ "*004*1234567#", // fwd to 1234567 conditionally
+ "**21*1234567#", // fwd unconditionally to 1234567
+
+ // north american vertical service codes
+
+ "*094565678", // Selective Call Blocking/Reporting
+ "*4278889", // Change Forward-To Number for Customer Programmable Call Forwarding Don't
+ // Answer
+ "*5644456", // Change Forward-To Number for ISDN Call Forwarding
+ "*6045677", // Selective Call Rejection Activation
+ "*635678", // Selective Call Forwarding Activation
+ "*64678899", // Selective Call Acceptance Activation
+ "*683456", // Call Forwarding Busy Line/Don't Answer Activation
+ "*721234", // Call Forwarding Activation
+ "*77", // Anonymous Call Rejection Activation
+ "*78", // Do Not Disturb Activation
+ };
+
+ private MmiUtils mMmiUtils = new MmiUtils();
+ private static final String[] sNonDangerousDialStrings = {"*6712345678", "*272", "*272911"};
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @SmallTest
+ @Test
+ public void testDangerousDialStringsDetected() throws Exception {
+ for (String s : sDangerousDialStrings) {
+ Uri.Builder b = new Uri.Builder();
+ b.scheme("tel").opaquePart(s);
+ assertTrue(mMmiUtils.isDangerousMmiOrVerticalCode(b.build()));
+ }
+ }
+
+ @SmallTest
+ @Test
+ public void testNonDangerousDialStringsNotDetected() throws Exception {
+ for (String s : sNonDangerousDialStrings) {
+ Uri.Builder b = new Uri.Builder();
+ b.scheme("tel").opaquePart(s);
+ assertFalse(mMmiUtils.isDangerousMmiOrVerticalCode(b.build()));
+ }
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java b/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java
index 169aeb2..f2bcf18 100644
--- a/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java
+++ b/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java
@@ -56,6 +56,7 @@
import com.android.server.telecom.Call;
import com.android.server.telecom.CallsManager;
import com.android.server.telecom.DefaultDialerCache;
+import com.android.server.telecom.MmiUtils;
import com.android.server.telecom.NewOutgoingCallIntentBroadcaster;
import com.android.server.telecom.PhoneAccountRegistrar;
import com.android.server.telecom.PhoneNumberUtilsAdapter;
@@ -93,6 +94,7 @@
@Mock private RoleManagerAdapter mRoleManagerAdapter;
@Mock private DefaultDialerCache mDefaultDialerCache;
+ @Mock private MmiUtils mMmiUtils;
private PhoneNumberUtilsAdapter mPhoneNumberUtilsAdapter = new PhoneNumberUtilsAdapterImpl();
@Override
@@ -261,6 +263,58 @@
assertEquals(Intent.FLAG_ACTIVITY_NEW_TASK, dialerIntent.getFlags());
}
+ @Test
+ public void testDangerousMmiCodeWithNonDefaultDialer() {
+ Uri handle = Uri.parse("tel:*21*1234567#");
+ doReturn(true).when(mMmiUtils).isDangerousMmiOrVerticalCode(handle);
+ Intent intent = new Intent(Intent.ACTION_CALL, handle);
+
+ String ui_package_string = "sample_string_1";
+ String dialer_default_class_string = "sample_string_2";
+ mComponentContextFixture.putResource(com.android.internal.R.string.config_defaultDialer,
+ ui_package_string);
+ mComponentContextFixture.putResource(R.string.dialer_default_class,
+ dialer_default_class_string);
+ when(mDefaultDialerCache.getSystemDialerApplication()).thenReturn(ui_package_string);
+ when(mDefaultDialerCache.getDialtactsSystemDialerComponent()).thenReturn(
+ new ComponentName(ui_package_string, dialer_default_class_string));
+
+ int result = processIntent(intent, false).disconnectCause;
+
+ assertEquals(DisconnectCause.OUTGOING_CANCELED, result);
+ verifyNoBroadcastSent();
+ verifyNoCallPlaced();
+
+ ArgumentCaptor<Intent> dialerIntentCaptor = ArgumentCaptor.forClass(Intent.class);
+ verify(mContext).startActivityAsUser(dialerIntentCaptor.capture(), any(UserHandle.class));
+ Intent dialerIntent = dialerIntentCaptor.getValue();
+ assertEquals(new ComponentName(ui_package_string, dialer_default_class_string),
+ dialerIntent.getComponent());
+ assertEquals(Intent.ACTION_DIAL, dialerIntent.getAction());
+ assertEquals(handle, dialerIntent.getData());
+ assertEquals(Intent.FLAG_ACTIVITY_NEW_TASK, dialerIntent.getFlags());
+ }
+
+ @Test
+ public void testNonDangerousMmiCodeWithNonDefaultDialer() {
+ Uri handle = Uri.parse("tel:*12*1234567#");
+ doReturn(false).when(mMmiUtils).isDangerousMmiOrVerticalCode(handle);
+ Intent intent = new Intent(Intent.ACTION_CALL, handle);
+
+ String ui_package_string = "sample_string_1";
+ String dialer_default_class_string = "sample_string_2";
+ mComponentContextFixture.putResource(com.android.internal.R.string.config_defaultDialer,
+ ui_package_string);
+ mComponentContextFixture.putResource(R.string.dialer_default_class,
+ dialer_default_class_string);
+ when(mDefaultDialerCache.getSystemDialerApplication()).thenReturn(ui_package_string);
+ when(mDefaultDialerCache.getDialtactsSystemDialerComponent()).thenReturn(
+ new ComponentName(ui_package_string, dialer_default_class_string));
+
+ int result = processIntent(intent, false).disconnectCause;
+ assertEquals(DisconnectCause.NOT_DISCONNECTED, result);
+ }
+
@SmallTest
@Test
public void testActionCallEmergencyCall() {
@@ -488,7 +542,7 @@
boolean isDefaultPhoneApp) {
NewOutgoingCallIntentBroadcaster b = new NewOutgoingCallIntentBroadcaster(
mContext, mCallsManager, intent, mPhoneNumberUtilsAdapter,
- isDefaultPhoneApp, mDefaultDialerCache);
+ isDefaultPhoneApp, mDefaultDialerCache, mMmiUtils);
NewOutgoingCallIntentBroadcaster.CallDisposition cd = b.evaluateCall();
if (cd.disconnectCause == DisconnectCause.NOT_DISCONNECTED) {
b.processCall(mCall, cd);