Convert incoming numbers to E.164 prior to range matching.

TelephonyManager#requestNumberVerification performs range checking on the
original unformatted phone number from the network.  In some regions like
the UK, the country prefix may not be present and a leading `0` may be
present in the phone number.

To ensure that these numbers are properly handled, we will now use
PhoneNumberUtils to convert the incoming number from the network into
`E.164` format prior to passing for match checking.  Since a 0 prefixed
number received from a UK carrier is formatted correctly with the `+44`
country code, this ensures that these numbers are handled correctly
by the matching API.

Test: Added new unit tests to cover matching of these numbers.
Test: Manual testing using shell command to confirm correct behavior of
the API from an end-to-end standpoint.
Test: Via test app and shell override.
Flag: com.android.internal.telephony.flags.robust_number_verification
Fixes: 400984263

Change-Id: I8bef2480af3a9bb0337eb5a1bdc08cf39bc06a9d

diff --git a/src/com/android/phone/NumberVerificationManager.java b/src/com/android/phone/NumberVerificationManager.java
index 2298d40..5789fa0 100644
--- a/src/com/android/phone/NumberVerificationManager.java
+++ b/src/com/android/phone/NumberVerificationManager.java
@@ -22,6 +22,7 @@
 import android.os.RemoteException;
 import android.telephony.NumberVerificationCallback;
 import android.telephony.PhoneNumberRange;
+import android.telephony.PhoneNumberUtils;
 import android.telephony.ServiceState;
 import android.text.TextUtils;
 import android.util.Log;
@@ -30,11 +31,15 @@
 import com.android.internal.telephony.INumberVerificationCallback;
 import com.android.internal.telephony.Phone;
 import com.android.internal.telephony.PhoneFactory;
+import com.android.internal.telephony.flags.Flags;
+import com.android.telephony.Rlog;
 
 /**
  * Singleton for managing the call based number verification requests.
  */
 public class NumberVerificationManager {
+    private static final String TAG = "NumberVerification";
+
     interface PhoneListSupplier {
         Phone[] getPhones();
     }
@@ -63,16 +68,40 @@
      * Check whether the incoming call matches one of the active filters. If so, call the callback
      * that says that the number has been successfully verified.
      * @param number A phone number
+     * @param networkCountryISO the network country ISO for the number
      * @return true if the number matches, false otherwise
      */
-    public synchronized boolean checkIncomingCall(String number) {
+    public synchronized boolean checkIncomingCall(String number, String networkCountryISO) {
         if (mCurrentRange == null || mCallback == null) {
             return false;
         }
 
-        if (mCurrentRange.matches(number)) {
+        Log.i(TAG, "checkIncomingCall: number=" + Rlog.piiHandle(number) + ", country="
+                + networkCountryISO);
+
+        String numberInE164Format;
+        if (Flags.robustNumberVerification()) {
+            // Reformat the number in E.164 format prior to performing matching.
+            numberInE164Format = PhoneNumberUtils.formatNumberToE164(number,
+                    networkCountryISO);
+            if (TextUtils.isEmpty(numberInE164Format)) {
+                // Parsing failed, so we will fall back to just passing the number as-is and hope
+                // for the best.  Chances are this number is an unknown number (ie no caller id),
+                // so it is most likely empty.
+                numberInE164Format = number;
+            }
+        } else {
+            // Default behavior.
+            numberInE164Format = number;
+        }
+
+        if (mCurrentRange.matches(numberInE164Format)) {
             mCurrentRange = null;
             try {
+                // Pass back the network-matched number as-is to the caller of
+                // TelephonyManager#requestNumberVerification -- do not send them the E.164 format
+                // number as that changes the format of the number from what the API consumer may
+                // be expecting.
                 mCallback.onCallReceived(number);
                 return true;
             } catch (RemoteException e) {
@@ -187,6 +216,12 @@
      * @param pkgName
      */
     static void overrideAuthorizedPackage(String pkgName) {
+        // Make sure we don't have any lingering callbacks kicking around as this could crash other
+        // test runs that follow; also old invocations from previous test invocations could also
+        // mess up things here too.
+        getInstance().mCallback = null;
+        getInstance().mCurrentRange = null;
+        getInstance().mHandler.removeMessages(0);
         sAuthorizedPackageOverride = pkgName;
     }
 }
diff --git a/src/com/android/phone/TelephonyShellCommand.java b/src/com/android/phone/TelephonyShellCommand.java
index cd6a369..85d4ab7 100644
--- a/src/com/android/phone/TelephonyShellCommand.java
+++ b/src/com/android/phone/TelephonyShellCommand.java
@@ -613,7 +613,7 @@
         pw.println("  numverify override-package PACKAGE_NAME;");
         pw.println("    Set the authorized package for number verification.");
         pw.println("    Leave the package name blank to reset.");
-        pw.println("  numverify fake-call NUMBER;");
+        pw.println("  numverify fake-call NUMBER <NETWORK_COUNTRY_ISO>");
         pw.println("    Fake an incoming call from NUMBER. This is for testing. Output will be");
         pw.println("    1 if the call would have been intercepted, 0 otherwise.");
     }
@@ -1091,8 +1091,16 @@
                 return 0;
             }
             case NUMBER_VERIFICATION_FAKE_CALL: {
+                String number = getNextArg();
+                String country = getNextArg();
+                if (country == null) {
+                    // No locale provided, default to current locale.
+                    Locale currentLocale = Locale.getDefault();
+                    country = currentLocale.getCountry();
+                }
+                Log.i(TAG, "numberVerificationFakeCall: " + number + " Locale: " + country);
                 boolean val = NumberVerificationManager.getInstance()
-                        .checkIncomingCall(getNextArg());
+                        .checkIncomingCall(number, country);
                 getOutPrintWriter().println(val ? "1" : "0");
                 return 0;
             }
diff --git a/src/com/android/services/telephony/PstnIncomingCallNotifier.java b/src/com/android/services/telephony/PstnIncomingCallNotifier.java
index 3b74c6f..fce3d91 100644
--- a/src/com/android/services/telephony/PstnIncomingCallNotifier.java
+++ b/src/com/android/services/telephony/PstnIncomingCallNotifier.java
@@ -33,8 +33,10 @@
 import com.android.internal.telephony.CallStateException;
 import com.android.internal.telephony.Connection;
 import com.android.internal.telephony.GsmCdmaPhone;
+import com.android.internal.telephony.LocaleTracker;
 import com.android.internal.telephony.Phone;
 import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.ServiceStateTracker;
 import com.android.internal.telephony.cdma.CdmaCallWaitingNotification;
 import com.android.internal.telephony.imsphone.ImsExternalCallTracker;
 import com.android.internal.telephony.imsphone.ImsExternalConnection;
@@ -149,6 +151,19 @@
     }
 
     /**
+     * Note: Same logic as
+     * {@link com.android.phone.PhoneInterfaceManager#getNetworkCountryIsoForPhone(int)}.
+     * @return the network country ISO for the current phone, or {@code null} if not known.
+     */
+    private String getNetworkCountryIso() {
+        ServiceStateTracker sst = mPhone.getServiceStateTracker();
+        if (sst == null) return null;
+        LocaleTracker lt = sst.getLocaleTracker();
+        if (lt == null) return null;
+        return lt.getCurrentCountry();
+    }
+
+    /**
      * Verifies the incoming call and triggers sending the incoming-call intent to Telecom.
      *
      * @param asyncResult The result object from the new ringing event.
@@ -161,7 +176,7 @@
             // Check if we have a pending number verification request.
             if (connection.getAddress() != null) {
                 if (NumberVerificationManager.getInstance()
-                        .checkIncomingCall(connection.getAddress())) {
+                        .checkIncomingCall(connection.getAddress(), getNetworkCountryIso())) {
                     // Disconnect the call if it matches, after a delay
                     mHandler.postDelayed(() -> {
                         try {
diff --git a/testapps/TelephonyManagerTestApp/Android.bp b/testapps/TelephonyManagerTestApp/Android.bp
index 0ff917e..28cad76 100644
--- a/testapps/TelephonyManagerTestApp/Android.bp
+++ b/testapps/TelephonyManagerTestApp/Android.bp
@@ -9,4 +9,7 @@
     javacflags: ["-parameters"],
     platform_apis: true,
     certificate: "platform",
+    static_libs: [
+        "androidx.appcompat_appcompat",
+    ],
 }
diff --git a/testapps/TelephonyManagerTestApp/AndroidManifest.xml b/testapps/TelephonyManagerTestApp/AndroidManifest.xml
index 6392c26..dd1d624 100644
--- a/testapps/TelephonyManagerTestApp/AndroidManifest.xml
+++ b/testapps/TelephonyManagerTestApp/AndroidManifest.xml
@@ -41,6 +41,20 @@
             </meta-data>
         </activity>
 
+        <activity android:name=".NumberVerificationActivity"
+            android:label="Number Verification"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <action android:name="android.intent.action.SEARCH"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+            <meta-data android:name="android.app.searchable"
+                android:resource="@xml/searchable">
+            </meta-data>
+        </activity>
+
         <activity android:name=".CallingMethodActivity"
              android:label="CallingMethodActivity"
              android:exported="true">
diff --git a/testapps/TelephonyManagerTestApp/res/layout/number_verification.xml b/testapps/TelephonyManagerTestApp/res/layout/number_verification.xml
new file mode 100644
index 0000000..a82e54e
--- /dev/null
+++ b/testapps/TelephonyManagerTestApp/res/layout/number_verification.xml
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical" >
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal" >
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textSize="15dp"
+            android:text="Country Code:" />
+        <EditText
+            android:id="@+id/countryCode"
+            android:inputType="text"
+            android:text="100"
+            android:layout_width="50dp"
+            android:layout_height="wrap_content" />
+    </LinearLayout>
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal" >
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textSize="15dp"
+            android:text="Prefix:" />
+        <EditText
+            android:id="@+id/prefix"
+            android:inputType="text"
+            android:text="100"
+            android:layout_width="50dp"
+            android:layout_height="wrap_content" />
+    </LinearLayout>
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal" >
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textSize="15dp"
+            android:text="Lower Bound:" />
+        <EditText
+            android:id="@+id/lowerBound"
+            android:inputType="text"
+            android:text="100"
+            android:layout_width="50dp"
+            android:layout_height="wrap_content" />
+    </LinearLayout>
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal" >
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textSize="15dp"
+            android:text="Upper Bound:" />
+        <EditText
+            android:id="@+id/upperBound"
+            android:inputType="text"
+            android:text="100"
+            android:layout_width="50dp"
+            android:layout_height="wrap_content" />
+    </LinearLayout>
+
+    <Button
+        android:id="@+id/request_verification_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="Request Verification" />
+
+    <TextView
+        android:id="@+id/verificationResult"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="15dp"
+        android:text="result" />
+</LinearLayout>
diff --git a/testapps/TelephonyManagerTestApp/src/com/android/phone/testapps/telephonymanagertestapp/NumberVerificationActivity.java b/testapps/TelephonyManagerTestApp/src/com/android/phone/testapps/telephonymanagertestapp/NumberVerificationActivity.java
new file mode 100644
index 0000000..81ffe36
--- /dev/null
+++ b/testapps/TelephonyManagerTestApp/src/com/android/phone/testapps/telephonymanagertestapp/NumberVerificationActivity.java
@@ -0,0 +1,97 @@
+/*
+ * 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.telephonymanagertestapp;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.telephony.NumberVerificationCallback;
+import android.telephony.PhoneNumberRange;
+import android.telephony.TelephonyManager;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.core.graphics.Insets;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsCompat;
+
+public class NumberVerificationActivity extends Activity {
+    private EditText mCountryCode;
+    private EditText mPrefix;
+    private EditText mLowerBound;
+    private EditText mUpperBound;
+    private Button mRequestVerificationButton;
+    private TextView mResultField;
+    private TelephonyManager mTelephonyManager;
+
+    private NumberVerificationCallback mCallback = new NumberVerificationCallback() {
+        @Override
+        public void onCallReceived(@NonNull String phoneNumber) {
+            mResultField.setText("Received call from " + phoneNumber);
+        }
+
+        @Override
+        public void onVerificationFailed(int reason) {
+            mResultField.setText("Verification failed " + reason);
+        }
+    };
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.number_verification);
+        setupEdgeToEdge(this);
+        mTelephonyManager = getSystemService(TelephonyManager.class);
+        mCountryCode = findViewById(R.id.countryCode);
+        mPrefix = findViewById(R.id.prefix);
+        mLowerBound = findViewById(R.id.lowerBound);
+        mUpperBound = findViewById(R.id.upperBound);
+        mRequestVerificationButton = findViewById(R.id.request_verification_button);
+        mRequestVerificationButton.setOnClickListener(v -> {
+            mTelephonyManager.requestNumberVerification(
+                    new PhoneNumberRange(mCountryCode.getText().toString(),
+                            mPrefix.getText().toString(), mLowerBound.getText().toString(),
+                            mUpperBound.getText().toString()),
+                    60000,
+                    getMainExecutor(),
+                    mCallback
+            );
+        });
+        mResultField = findViewById(R.id.verificationResult);
+    }
+
+    /**
+     * Given an activity, configure the activity to adjust for edge to edge restrictions.
+     *
+     * @param activity the activity.
+     */
+    public static void setupEdgeToEdge(Activity activity) {
+        ViewCompat.setOnApplyWindowInsetsListener(activity.findViewById(android.R.id.content),
+                (v, windowInsets) -> {
+                    Insets insets = windowInsets.getInsets(
+                            WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.ime());
+
+                    // Apply the insets paddings to the view.
+                    v.setPadding(insets.left, insets.top, insets.right, insets.bottom);
+
+                    // Return CONSUMED if you don't want the window insets to keep being
+                    // passed down to descendant views.
+                    return WindowInsetsCompat.CONSUMED;
+                });
+    }
+}
diff --git a/tests/src/com/android/phone/NumberVerificationManagerTest.java b/tests/src/com/android/phone/NumberVerificationManagerTest.java
index f7914ab..56df139 100644
--- a/tests/src/com/android/phone/NumberVerificationManagerTest.java
+++ b/tests/src/com/android/phone/NumberVerificationManagerTest.java
@@ -26,6 +26,9 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.telephony.NumberVerificationCallback;
 import android.telephony.PhoneNumberRange;
 import android.telephony.ServiceState;
@@ -33,8 +36,10 @@
 import com.android.internal.telephony.Call;
 import com.android.internal.telephony.INumberVerificationCallback;
 import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.flags.Flags;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -43,6 +48,10 @@
 
 @RunWith(JUnit4.class)
 public class NumberVerificationManagerTest {
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
     private static final PhoneNumberRange SAMPLE_RANGE =
             new PhoneNumberRange("1", "650555", "0000", "8999");
     private static final long DEFAULT_VERIFICATION_TIMEOUT = 100;
@@ -131,7 +140,7 @@
 
     private void verifyDefaultRangeMatching(NumberVerificationManager manager) throws Exception {
         String testNumber = "6505550000";
-        assertTrue(manager.checkIncomingCall(testNumber));
+        assertTrue(manager.checkIncomingCall(testNumber, "US"));
         verify(mCallback).onCallReceived(testNumber);
     }
 
@@ -148,6 +157,45 @@
         verifyDefaultRangeMatching(manager);
     }
 
+    /**
+     * Verifies that numbers starting with '0' prefix from the network and lacking the country code
+     * will be correctly compared to a range with the `44` country code.
+     * @throws Exception
+     */
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ROBUST_NUMBER_VERIFICATION)
+    public void testVerificationOfUkNumbersWithZeroPrefix() throws Exception {
+        NumberVerificationManager manager =
+                new NumberVerificationManager(() -> new Phone[]{mPhone1});
+
+        manager.requestVerification(new PhoneNumberRange("44", "7445", "000000", "999999"),
+                mCallback, DEFAULT_VERIFICATION_TIMEOUT);
+        verify(mCallback, never()).onVerificationFailed(anyInt());
+        String testNumber = "0 7445 032046";
+        assertTrue(manager.checkIncomingCall(testNumber, "GB"));
+        verify(mCallback).onCallReceived(testNumber);
+    }
+
+    /**
+     * Similar to {@link #testVerificationOfUkNumbersWithZeroPrefix()}, except verifies that if the
+     * network sent a full qualified UK phone number with the `+44` country code that it would
+     * match the range.
+     * @throws Exception
+     */
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ROBUST_NUMBER_VERIFICATION)
+    public void testVerificationOfUkNumbersWithCountryPrefix() throws Exception {
+        NumberVerificationManager manager =
+                new NumberVerificationManager(() -> new Phone[]{mPhone1});
+
+        manager.requestVerification(new PhoneNumberRange("44", "7445", "000000", "999999"),
+                mCallback, DEFAULT_VERIFICATION_TIMEOUT);
+        verify(mCallback, never()).onVerificationFailed(anyInt());
+        String testNumber = "+447445032046";
+        assertTrue(manager.checkIncomingCall(testNumber, "GB"));
+        verify(mCallback).onCallReceived(testNumber);
+    }
+
     @Test
     public void testVerificationWorksWithOnePhoneFull() throws Exception {
         Call fakeCall = mock(Call.class);
@@ -169,6 +217,6 @@
                 new NumberVerificationManager(() -> new Phone[]{mPhone1, mPhone2});
         manager.requestVerification(SAMPLE_RANGE, mCallback, DEFAULT_VERIFICATION_TIMEOUT);
         verifyDefaultRangeMatching(manager);
-        assertFalse(manager.checkIncomingCall("this doesn't even matter"));
+        assertFalse(manager.checkIncomingCall("this doesn't even matter", "US"));
     }
 }