Merge "Add functionality for number verification" am: 74eb4c0beb am: 4b8a02c487
am: ce47dd9dd0

Change-Id: I9b51c381a2c060ae930b73ab1034492665ef03b1
diff --git a/res/values/config.xml b/res/values/config.xml
index 38a0b18..2244fe3 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -235,6 +235,9 @@
     <!-- Flag indicating whether the device supports RTT (real-time text) -->
     <bool name="config_support_rtt">false</bool>
 
+    <!-- The package name for the platform number verification supplier app. -->
+    <string name="platform_number_verification_package" translatable="false"></string>
+
     <!-- Flag indicating whether a system app can use video calling fallback if carrier video
          calling is not available. -->
     <bool name="config_support_video_calling_fallback">true</bool>
diff --git a/src/com/android/phone/NumberVerificationManager.java b/src/com/android/phone/NumberVerificationManager.java
new file mode 100644
index 0000000..9ec16f8
--- /dev/null
+++ b/src/com/android/phone/NumberVerificationManager.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2018 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;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.telephony.NumberVerificationCallback;
+import android.telephony.PhoneNumberRange;
+import android.telephony.ServiceState;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.internal.telephony.Call;
+import com.android.internal.telephony.INumberVerificationCallback;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneFactory;
+
+/**
+ * Singleton for managing the call based number verification requests.
+ */
+public class NumberVerificationManager {
+    interface PhoneListSupplier {
+        Phone[] getPhones();
+    }
+
+    private static NumberVerificationManager sInstance;
+    private static String sAuthorizedPackageOverride;
+
+    private PhoneNumberRange mCurrentRange;
+    private INumberVerificationCallback mCallback;
+    private final PhoneListSupplier mPhoneListSupplier;
+
+    // We don't really care what thread this runs on, since it's only used for a non-blocking
+    // timeout.
+    private Handler mHandler;
+
+    NumberVerificationManager(PhoneListSupplier phoneListSupplier) {
+        mPhoneListSupplier = phoneListSupplier;
+        mHandler = new Handler(Looper.getMainLooper());
+    }
+
+    private NumberVerificationManager() {
+        this(PhoneFactory::getPhones);
+    }
+
+    /**
+     * 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
+     * @return true if the number matches, false otherwise
+     */
+    public synchronized boolean checkIncomingCall(String number) {
+        if (mCurrentRange == null || mCallback == null) {
+            return false;
+        }
+
+        if (mCurrentRange.matches(number)) {
+            mCurrentRange = null;
+            try {
+                mCallback.onCallReceived(number);
+                return true;
+            } catch (RemoteException e) {
+                Log.w(NumberVerificationManager.class.getSimpleName(),
+                        "Remote exception calling verification complete callback");
+                // Intercept the call even if there was a remote exception -- it's still going to be
+                // a strange call from a robot number
+                return true;
+            } finally {
+                mCallback = null;
+            }
+        }
+        return false;
+    }
+
+    synchronized void requestVerification(PhoneNumberRange numberRange,
+            INumberVerificationCallback callback, long timeoutMillis) {
+        if (!checkNumberVerificationFeasibility(callback)) {
+            return;
+        }
+
+        mCallback = callback;
+        mCurrentRange = numberRange;
+
+        mHandler.postDelayed(() -> {
+            synchronized (NumberVerificationManager.this) {
+                // Check whether the verification finished already -- if so, don't call anything.
+                if (mCallback != null && mCurrentRange != null) {
+                    try {
+                        mCallback.onVerificationFailed(NumberVerificationCallback.REASON_TIMED_OUT);
+                    } catch (RemoteException e) {
+                        Log.w(NumberVerificationManager.class.getSimpleName(),
+                                "Remote exception calling verification error callback");
+                    }
+                    mCallback = null;
+                    mCurrentRange = null;
+                }
+            }
+        }, timeoutMillis);
+    }
+
+    private boolean checkNumberVerificationFeasibility(INumberVerificationCallback callback) {
+        int reason = -1;
+        try {
+            if (mCurrentRange != null || mCallback != null) {
+                reason = NumberVerificationCallback.REASON_CONCURRENT_REQUESTS;
+                return false;
+            }
+            boolean doesAnyPhoneHaveRoomForIncomingCall = false;
+            boolean isAnyPhoneVoiceRegistered = false;
+            for (Phone phone : mPhoneListSupplier.getPhones()) {
+                // abort if any phone is in an emergency call or ecbm
+                if (phone.isInEmergencyCall()) {
+                    reason = NumberVerificationCallback.REASON_IN_EMERGENCY_CALL;
+                    return false;
+                }
+                if (phone.isInEcm()) {
+                    reason = NumberVerificationCallback.REASON_IN_ECBM;
+                    return false;
+                }
+
+                // make sure at least one phone is registered for voice
+                if (phone.getServiceState().getVoiceRegState() == ServiceState.STATE_IN_SERVICE) {
+                    isAnyPhoneVoiceRegistered = true;
+                }
+                // make sure at least one phone has room for an incoming call.
+                if (phone.getRingingCall().getState() == Call.State.IDLE
+                        && (phone.getForegroundCall().getState() == Call.State.IDLE
+                        || phone.getBackgroundCall().getState() == Call.State.IDLE)) {
+                    doesAnyPhoneHaveRoomForIncomingCall = true;
+                }
+            }
+            if (!isAnyPhoneVoiceRegistered) {
+                reason = NumberVerificationCallback.REASON_NETWORK_NOT_AVAILABLE;
+                return false;
+            }
+            if (!doesAnyPhoneHaveRoomForIncomingCall) {
+                reason = NumberVerificationCallback.REASON_TOO_MANY_CALLS;
+                return false;
+            }
+        } finally {
+            if (reason >= 0) {
+                try {
+                    callback.onVerificationFailed(reason);
+                } catch (RemoteException e) {
+                    Log.w(NumberVerificationManager.class.getSimpleName(),
+                            "Remote exception calling verification error callback");
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Get the singleton instance of NumberVerificationManager.
+     * @return
+     */
+    public static NumberVerificationManager getInstance() {
+        if (sInstance == null) {
+            sInstance = new NumberVerificationManager();
+        }
+        return sInstance;
+    }
+
+    static String getAuthorizedPackage(Context context) {
+        return !TextUtils.isEmpty(sAuthorizedPackageOverride) ? sAuthorizedPackageOverride :
+                context.getResources().getString(R.string.platform_number_verification_package);
+    }
+
+    /**
+     * Used by shell commands to override the authorized package name for number verification.
+     * @param pkgName
+     */
+    static void overrideAuthorizedPackage(String pkgName) {
+        sAuthorizedPackageOverride = pkgName;
+    }
+}
diff --git a/src/com/android/phone/PhoneInterfaceManager.java b/src/com/android/phone/PhoneInterfaceManager.java
index f76720d..973ddf0 100755
--- a/src/com/android/phone/PhoneInterfaceManager.java
+++ b/src/com/android/phone/PhoneInterfaceManager.java
@@ -16,6 +16,8 @@
 
 package com.android.phone;
 
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
 import static com.android.internal.telephony.PhoneConstants.SUBSCRIPTION_KEY;
 
 import android.Manifest.permission;
@@ -66,6 +68,7 @@
 import android.telephony.ModemActivityInfo;
 import android.telephony.NeighboringCellInfo;
 import android.telephony.NetworkScanRequest;
+import android.telephony.PhoneNumberRange;
 import android.telephony.RadioAccessFamily;
 import android.telephony.Rlog;
 import android.telephony.ServiceState;
@@ -106,6 +109,7 @@
 import com.android.internal.telephony.CellNetworkScanResult;
 import com.android.internal.telephony.CommandException;
 import com.android.internal.telephony.DefaultPhoneNotifier;
+import com.android.internal.telephony.INumberVerificationCallback;
 import com.android.internal.telephony.ITelephony;
 import com.android.internal.telephony.IccCard;
 import com.android.internal.telephony.LocaleTracker;
@@ -2375,6 +2379,30 @@
         }
     }
 
+    @Override
+    public void requestNumberVerification(PhoneNumberRange range, long timeoutMillis,
+            INumberVerificationCallback callback, String callingPackage) {
+        if (mApp.checkCallingOrSelfPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
+                != PERMISSION_GRANTED) {
+            throw new SecurityException("Caller must hold the MODIFY_PHONE_STATE permission");
+        }
+        mAppOps.checkPackage(Binder.getCallingUid(), callingPackage);
+
+        String authorizedPackage = NumberVerificationManager.getAuthorizedPackage(mApp);
+        if (!TextUtils.equals(callingPackage, authorizedPackage)) {
+            throw new SecurityException("Calling package must be configured in the device config");
+        }
+
+        if (range == null) {
+            throw new NullPointerException("Range must be non-null");
+        }
+
+        timeoutMillis = Math.min(timeoutMillis,
+                TelephonyManager.MAX_NUMBER_VERIFICATION_TIMEOUT_MILLIS);
+
+        NumberVerificationManager.getInstance().requestVerification(range, callback, timeoutMillis);
+    }
+
     /**
      * Returns true if CDMA provisioning needs to run.
      */
@@ -5339,7 +5367,7 @@
     @Override
     protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
         if (mPhone.getContext().checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
-                != PackageManager.PERMISSION_GRANTED) {
+                != PERMISSION_GRANTED) {
             writer.println("Permission Denial: can't dump Phone from pid="
                     + Binder.getCallingPid()
                     + ", uid=" + Binder.getCallingUid()
diff --git a/src/com/android/phone/TelephonyShellCommand.java b/src/com/android/phone/TelephonyShellCommand.java
index 1a25ae3..dc1e7d1 100644
--- a/src/com/android/phone/TelephonyShellCommand.java
+++ b/src/com/android/phone/TelephonyShellCommand.java
@@ -16,6 +16,8 @@
 
 package com.android.phone;
 
+import android.os.Binder;
+import android.os.Process;
 import android.os.RemoteException;
 import android.os.ShellCommand;
 import android.os.UserHandle;
@@ -41,6 +43,8 @@
 
     private static final String IMS_SUBCOMMAND = "ims";
     private static final String SMS_SUBCOMMAND = "sms";
+    private static final String NUMBER_VERIFICATION_SUBCOMMAND = "numverify";
+
     private static final String IMS_SET_CARRIER_SERVICE = "set-ims-service";
     private static final String IMS_GET_CARRIER_SERVICE = "get-ims-service";
     private static final String IMS_ENABLE = "enable";
@@ -50,6 +54,8 @@
     private static final String SMS_GET_DEFAULT_APP = "get-default-app";
     private static final String SMS_SET_DEFAULT_APP = "set-default-app";
 
+    private static final String NUMBER_VERIFICATION_OVERRIDE_PACKAGE = "override-package";
+
     // Take advantage of existing methods that already contain permissions checks when possible.
     private final ITelephony mInterface;
 
@@ -70,6 +76,8 @@
             case SMS_SUBCOMMAND: {
                 return handleSmsCommand();
             }
+            case NUMBER_VERIFICATION_SUBCOMMAND:
+                return handleNumberVerificationCommand();
             default: {
                 return handleDefaultCommands(cmd);
             }
@@ -126,6 +134,15 @@
         pw.println("    Set PACKAGE_NAME as the default SMS app.");
     }
 
+
+    private void onHelpNumberVerification() {
+        PrintWriter pw = getOutPrintWriter();
+        pw.println("Number verification commands");
+        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.");
+    }
+
     private int handleImsCommand() {
         String arg = getNextArg();
         if (arg == null) {
@@ -151,6 +168,26 @@
         return -1;
     }
 
+    private int handleNumberVerificationCommand() {
+        String arg = getNextArg();
+        if (arg == null) {
+            onHelpNumberVerification();
+            return 0;
+        }
+
+        switch (arg) {
+            case NUMBER_VERIFICATION_OVERRIDE_PACKAGE: {
+                if (!checkShellUid()) {
+                    return -1;
+                }
+                NumberVerificationManager.overrideAuthorizedPackage(getNextArg());
+                return 0;
+            }
+        }
+
+        return -1;
+    }
+
     // ims set-ims-service
     private int handleImsSetServiceCommand() {
         PrintWriter errPw = getErrPrintWriter();
@@ -400,4 +437,8 @@
         getOutPrintWriter().println("SMS app set to " + mInterface.getDefaultSmsApp(userId));
         return 0;
     }
+
+    private boolean checkShellUid() {
+        return Binder.getCallingUid() == Process.SHELL_UID;
+    }
 }
diff --git a/src/com/android/services/telephony/PstnIncomingCallNotifier.java b/src/com/android/services/telephony/PstnIncomingCallNotifier.java
index 4dfaf44..223616f 100644
--- a/src/com/android/services/telephony/PstnIncomingCallNotifier.java
+++ b/src/com/android/services/telephony/PstnIncomingCallNotifier.java
@@ -37,6 +37,7 @@
 import com.android.internal.telephony.imsphone.ImsExternalCallTracker;
 import com.android.internal.telephony.imsphone.ImsExternalConnection;
 import com.android.internal.telephony.imsphone.ImsPhoneConnection;
+import com.android.phone.NumberVerificationManager;
 import com.android.phone.PhoneUtils;
 
 import com.google.common.base.Preconditions;
@@ -126,6 +127,19 @@
         Connection connection = (Connection) asyncResult.result;
         if (connection != null) {
             Call call = connection.getCall();
+            // Check if we have a pending number verification request.
+            if (connection.getAddress() != null) {
+                if (NumberVerificationManager.getInstance()
+                        .checkIncomingCall(connection.getAddress())) {
+                    // Disconnect the call if it matches
+                    try {
+                        connection.hangup();
+                    } catch (CallStateException e) {
+                        Log.e(this, e, "Error hanging up potential number verification call");
+                    }
+                    return;
+                }
+            }
 
             // Final verification of the ringing state before sending the intent to Telecom.
             if (call != null && call.getState().isRinging()) {
diff --git a/testapps/TelephonyManagerTestApp/res/layout/calling_method.xml b/testapps/TelephonyManagerTestApp/res/layout/calling_method.xml
index 668b708..5145a63 100644
--- a/testapps/TelephonyManagerTestApp/res/layout/calling_method.xml
+++ b/testapps/TelephonyManagerTestApp/res/layout/calling_method.xml
@@ -58,11 +58,12 @@
         android:ellipsize="marquee"
         android:textSize="30sp" />
 
-    <ListView
-        android:id="@android:id/list"
+    <LinearLayout
+        android:id="@+id/method_params"
+        android:orientation="vertical"
         android:layout_height="wrap_content"
         android:layout_width="fill_parent">
-    </ListView>
+    </LinearLayout>
 
     <Button
         android:id="@+id/go_button"
@@ -72,10 +73,18 @@
         android:layout_height="50dip">
     </Button>
 
-    <TextView
-        android:id="@+id/return_value"
+
+    <ScrollView
+        android:id="@+id/return_value_wrapper"
         android:layout_width="fill_parent"
         android:layout_height="wrap_content"
-        android:ellipsize="marquee"
-        android:textSize="15sp" />
+        android:scrollbars="vertical"
+        android:fillViewport="true">
+        <TextView
+            android:id="@+id/return_value"
+            android:layout_width="fill_parent"
+            android:layout_height="fill_parent"
+            android:ellipsize="marquee"
+            android:textSize="15sp" />
+    </ScrollView>
 </LinearLayout>
diff --git a/testapps/TelephonyManagerTestApp/src/com/android/phone/testapps/telephonymanagertestapp/CallingMethodActivity.java b/testapps/TelephonyManagerTestApp/src/com/android/phone/testapps/telephonymanagertestapp/CallingMethodActivity.java
index aa9dbc0..4bf8220 100644
--- a/testapps/TelephonyManagerTestApp/src/com/android/phone/testapps/telephonymanagertestapp/CallingMethodActivity.java
+++ b/testapps/TelephonyManagerTestApp/src/com/android/phone/testapps/telephonymanagertestapp/CallingMethodActivity.java
@@ -16,27 +16,28 @@
 
 package com.android.phone.testapps.telephonymanagertestapp;
 
-import android.app.ListActivity;
+import android.app.Activity;
 import android.os.Bundle;
 import android.telephony.TelephonyManager;
 import android.util.Log;
 import android.view.View;
-import android.view.ViewGroup;
-import android.widget.BaseAdapter;
 import android.widget.Button;
 import android.widget.EditText;
+import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import java.io.PrintWriter;
+import java.io.StringWriter;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
-import java.util.ArrayList;
 
 /**
  * Activity to call a specific method of TelephonyManager.
  */
-public class CallingMethodActivity extends ListActivity {
+public class CallingMethodActivity extends Activity {
     private Class[] mParameterTypes;
     private Object[] mParameterValues;
+    EditText[] mEditTexts;
     private Button mGoButton;
     private Method mMethod;
     private TextView mReturnValue;
@@ -57,10 +58,11 @@
         mGoButton = findViewById(R.id.go_button);
         mReturnValue = findViewById(R.id.return_value);
         mSubIdField = findViewById(R.id.sub_id_value);
-        setListAdapter(new ParameterListAdapter());
 
         mParameterTypes = mMethod.getParameterTypes();
         mParameterValues = new Object[mParameterTypes.length];
+        mEditTexts = new EditText[mParameterTypes.length];
+        populateParamList();
 
         String tags = Modifier.toString(mMethod.getModifiers()) + ' '
                 + TelephonyManagerTestApp.getShortTypeName(mMethod.getReturnType().toString());
@@ -76,13 +78,18 @@
             int subId = Integer.parseInt(mSubIdField.getText().toString());
 
             for (int i = 0; i < mParameterTypes.length; i++) {
-                String text = ((EditText) getListAdapter().getItem(i)).getText().toString();
+                String text = mEditTexts[i].getText().toString();
                 if (mParameterTypes[i] == int.class) {
                     mParameterValues[i] = Integer.parseInt(text);
                 } else if (mParameterTypes[i] == boolean.class) {
                     mParameterValues[i] = Boolean.parseBoolean(text);
-                } else if (mParameterTypes[i] == Long.class) {
+                } else if (mParameterTypes[i] == long.class) {
                     mParameterValues[i] = Long.parseLong(text);
+                } else if (mParameterTypes[i] == String.class) {
+                    mParameterValues[i] = text;
+                } else {
+                    mParameterValues[i] =
+                            ParameterParser.get(this).executeParser(mParameterTypes[i], text);
                 }
             }
             Log.d(TelephonyManagerTestApp.TAG, "Invoking method " + mMethod.getName());
@@ -103,50 +110,22 @@
             }
 
         } catch (Exception exception) {
-            Log.d(TelephonyManagerTestApp.TAG, "NoSuchMethodException " + exception);
-            mReturnValue.setText("NoSuchMethodException " + exception);
+            Log.d(TelephonyManagerTestApp.TAG, "Exception: " + exception);
+            StringWriter s = new StringWriter();
+            PrintWriter stack = new PrintWriter(s);
+            exception.printStackTrace(stack);
+            mReturnValue.setText("Exception " + exception.getMessage() + "\n" + s.toString());
         }
     }
 
-    private class ParameterListAdapter extends BaseAdapter {
-        ArrayList<EditText> mEditTexts = new ArrayList<>();
-        @Override
-        public int getCount() {
-            return mParameterTypes == null ? 0 : mParameterTypes.length;
-        }
-
-        @Override
-        public View getView(int position, View convertView, ViewGroup container) {
-            if (mParameterTypes == null || mParameterTypes.length <= position) {
-                return null;
-            }
-
-            if (convertView == null) {
-                convertView = getLayoutInflater().inflate(
-                        R.layout.parameter_field, container, false);
-            }
-
-            Class aClass = mParameterTypes[position];
-
-            ((TextView) convertView.findViewById(R.id.field_name)).setText(
+    private void populateParamList() {
+        for (int i = 0; i < mParameterTypes.length; i++) {
+            View view = getLayoutInflater().inflate(R.layout.parameter_field, null);
+            Class aClass = mParameterTypes[i];
+            ((TextView) view.findViewById(R.id.field_name)).setText(
                     TelephonyManagerTestApp.getShortTypeName(aClass.toString()) + ": ");
-            mEditTexts.add(convertView.findViewById(R.id.field_value));
-
-            return convertView;
-        }
-
-        @Override
-        public Object getItem(int position) {
-            if (mEditTexts == null || mEditTexts.size() <= position) {
-                return null;
-            }
-
-            return mEditTexts.get(position);
-        }
-
-        @Override
-        public long getItemId(int position) {
-            return position;
+            mEditTexts[i] = view.findViewById(R.id.field_value);
+            ((LinearLayout) findViewById(R.id.method_params)).addView(view);
         }
     }
 }
diff --git a/testapps/TelephonyManagerTestApp/src/com/android/phone/testapps/telephonymanagertestapp/ParameterParser.java b/testapps/TelephonyManagerTestApp/src/com/android/phone/testapps/telephonymanagertestapp/ParameterParser.java
new file mode 100644
index 0000000..097c90a
--- /dev/null
+++ b/testapps/TelephonyManagerTestApp/src/com/android/phone/testapps/telephonymanagertestapp/ParameterParser.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2018 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.content.Context;
+import android.telephony.NumberVerificationCallback;
+import android.telephony.PhoneNumberRange;
+import android.widget.Toast;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.function.Function;
+
+class ParameterParser {
+    private static ParameterParser sInstance;
+
+    static ParameterParser get(Context context) {
+        if (sInstance == null) {
+            sInstance = new ParameterParser(context);
+        }
+        return sInstance;
+    }
+
+    private final Context mContext;
+    private final Map<Class, Function<String, Object>> mParsers =
+            new HashMap<Class, Function<String, Object>>() {{
+                put(PhoneNumberRange.class, ParameterParser::parsePhoneNumberRange);
+                put(Executor.class, s -> parseExecutor(s));
+                put(NumberVerificationCallback.class, s -> parseNumberVerificationCallback(s));
+            }};
+
+    private ParameterParser(Context context) {
+        mContext = context;
+    }
+
+    Object executeParser(Class type, String input) {
+        return mParsers.getOrDefault(type, s -> null).apply(input);
+    }
+
+    private static PhoneNumberRange parsePhoneNumberRange(String input) {
+        String[] parts = input.split(" ");
+        if (parts.length != 4) {
+            return null;
+        }
+        return new PhoneNumberRange(parts[0], parts[1], parts[2], parts[3]);
+    }
+
+    private Executor parseExecutor(String input) {
+        return mContext.getMainExecutor();
+    }
+
+    private NumberVerificationCallback parseNumberVerificationCallback(String input) {
+        return new NumberVerificationCallback() {
+            @Override
+            public void onCallReceived(String phoneNumber) {
+                Toast.makeText(mContext, "Received verification " + phoneNumber,
+                        Toast.LENGTH_SHORT).show();
+            }
+
+            @Override
+            public void onVerificationFailed(int reason) {
+                Toast.makeText(mContext, "Verification failed " + reason,
+                        Toast.LENGTH_SHORT).show();
+            }
+        };
+    }
+}
diff --git a/testapps/TelephonyManagerTestApp/src/com/android/phone/testapps/telephonymanagertestapp/TelephonyManagerTestApp.java b/testapps/TelephonyManagerTestApp/src/com/android/phone/testapps/telephonymanagertestapp/TelephonyManagerTestApp.java
index 45c76a7..760c3bd 100644
--- a/testapps/TelephonyManagerTestApp/src/com/android/phone/testapps/telephonymanagertestapp/TelephonyManagerTestApp.java
+++ b/testapps/TelephonyManagerTestApp/src/com/android/phone/testapps/telephonymanagertestapp/TelephonyManagerTestApp.java
@@ -158,7 +158,7 @@
             mFilteredMethods.addAll(mMethods);
         } else {
             for (Method method : mMethods) {
-                if (method.getName().contains(text)) {
+                if (method.getName().toLowerCase().contains(text.toLowerCase())) {
                     mFilteredMethods.add(method);
                 }
             }
diff --git a/tests/src/com/android/phone/NumberVerificationManagerTest.java b/tests/src/com/android/phone/NumberVerificationManagerTest.java
new file mode 100644
index 0000000..d476ba5
--- /dev/null
+++ b/tests/src/com/android/phone/NumberVerificationManagerTest.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2018 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;
+
+import static junit.framework.TestCase.assertFalse;
+
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.telephony.NumberVerificationCallback;
+import android.telephony.PhoneNumberRange;
+import android.telephony.ServiceState;
+
+import com.android.internal.telephony.Call;
+import com.android.internal.telephony.INumberVerificationCallback;
+import com.android.internal.telephony.Phone;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(JUnit4.class)
+public class NumberVerificationManagerTest {
+    private static final PhoneNumberRange SAMPLE_RANGE =
+            new PhoneNumberRange("1", "650555", "0000", "8999");
+    private static final long DEFAULT_VERIFICATION_TIMEOUT = 100;
+    @Mock private Phone mPhone1;
+    @Mock private Phone mPhone2;
+    @Mock private Call mRingingCall;
+    @Mock private Call mForegroundCall;
+    @Mock private Call mBackgroundCall;
+    @Mock private INumberVerificationCallback mCallback;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        ServiceState ss = mock(ServiceState.class);
+        when(ss.getVoiceRegState()).thenReturn(ServiceState.STATE_IN_SERVICE);
+        when(mPhone1.getServiceState()).thenReturn(ss);
+        when(mPhone1.getForegroundCall()).thenReturn(mForegroundCall);
+        when(mPhone1.getRingingCall()).thenReturn(mRingingCall);
+        when(mPhone1.getBackgroundCall()).thenReturn(mBackgroundCall);
+        when(mPhone2.getServiceState()).thenReturn(ss);
+        when(mPhone2.getForegroundCall()).thenReturn(mForegroundCall);
+        when(mPhone2.getRingingCall()).thenReturn(mRingingCall);
+        when(mPhone2.getBackgroundCall()).thenReturn(mBackgroundCall);
+
+        when(mForegroundCall.getState()).thenReturn(Call.State.IDLE);
+        when(mRingingCall.getState()).thenReturn(Call.State.IDLE);
+        when(mBackgroundCall.getState()).thenReturn(Call.State.IDLE);
+    }
+
+    @Test
+    public void testConcurrentRequestFailure() throws Exception {
+        NumberVerificationManager manager =
+                new NumberVerificationManager(() -> new Phone[]{mPhone1});
+        manager.requestVerification(SAMPLE_RANGE, mCallback, DEFAULT_VERIFICATION_TIMEOUT);
+        manager.requestVerification(SAMPLE_RANGE, mCallback, DEFAULT_VERIFICATION_TIMEOUT);
+        verify(mCallback, times(1)).onVerificationFailed(
+                NumberVerificationCallback.REASON_CONCURRENT_REQUESTS);
+    }
+
+    @Test
+    public void testEcbmFailure() throws Exception {
+        NumberVerificationManager manager =
+                new NumberVerificationManager(() -> new Phone[]{mPhone1});
+        when(mPhone1.isInEcm()).thenReturn(true);
+
+        manager.requestVerification(SAMPLE_RANGE, mCallback, DEFAULT_VERIFICATION_TIMEOUT);
+        verify(mCallback, times(1)).onVerificationFailed(
+                NumberVerificationCallback.REASON_IN_ECBM);
+    }
+
+    @Test
+    public void testEmergencyCallFailure() throws Exception {
+        NumberVerificationManager manager =
+                new NumberVerificationManager(() -> new Phone[]{mPhone1});
+        when(mPhone1.isInEmergencyCall()).thenReturn(true);
+
+        manager.requestVerification(SAMPLE_RANGE, mCallback, DEFAULT_VERIFICATION_TIMEOUT);
+        verify(mCallback, times(1)).onVerificationFailed(
+                NumberVerificationCallback.REASON_IN_EMERGENCY_CALL);
+    }
+
+    @Test
+    public void testNoPhoneInServiceFailure() throws Exception {
+        ServiceState ss = mock(ServiceState.class);
+        when(ss.getVoiceRegState()).thenReturn(ServiceState.STATE_POWER_OFF);
+        when(mPhone1.getServiceState()).thenReturn(ss);
+        when(mPhone2.getServiceState()).thenReturn(ss);
+        NumberVerificationManager manager =
+                new NumberVerificationManager(() -> new Phone[]{mPhone1, mPhone2});
+
+        manager.requestVerification(SAMPLE_RANGE, mCallback, DEFAULT_VERIFICATION_TIMEOUT);
+        verify(mCallback, times(1)).onVerificationFailed(
+                NumberVerificationCallback.REASON_NETWORK_NOT_AVAILABLE);
+    }
+
+    @Test
+    public void testAllLinesFullFailure() throws Exception {
+        NumberVerificationManager manager =
+                new NumberVerificationManager(() -> new Phone[]{mPhone1, mPhone2});
+        when(mRingingCall.getState()).thenReturn(Call.State.ALERTING);
+
+        manager.requestVerification(SAMPLE_RANGE, mCallback, DEFAULT_VERIFICATION_TIMEOUT);
+        verify(mCallback, times(1)).onVerificationFailed(
+                NumberVerificationCallback.REASON_TOO_MANY_CALLS);
+    }
+
+    private void verifyDefaultRangeMatching(NumberVerificationManager manager) throws Exception {
+        String testNumber = "6505550000";
+        assertTrue(manager.checkIncomingCall(testNumber));
+        verify(mCallback).onCallReceived(testNumber);
+    }
+
+    @Test
+    public void testVerificationWorksWithOnePhoneInService() throws Exception {
+        ServiceState ss = mock(ServiceState.class);
+        when(ss.getVoiceRegState()).thenReturn(ServiceState.STATE_POWER_OFF);
+        when(mPhone1.getServiceState()).thenReturn(ss);
+        NumberVerificationManager manager =
+                new NumberVerificationManager(() -> new Phone[]{mPhone1, mPhone2});
+
+        manager.requestVerification(SAMPLE_RANGE, mCallback, DEFAULT_VERIFICATION_TIMEOUT);
+        verify(mCallback, never()).onVerificationFailed(anyInt());
+        verifyDefaultRangeMatching(manager);
+    }
+
+    @Test
+    public void testVerificationWorksWithOnePhoneFull() throws Exception {
+        Call fakeCall = mock(Call.class);
+        when(fakeCall.getState()).thenReturn(Call.State.ACTIVE);
+        when(mPhone1.getForegroundCall()).thenReturn(fakeCall);
+        when(mPhone1.getRingingCall()).thenReturn(fakeCall);
+        when(mPhone1.getBackgroundCall()).thenReturn(fakeCall);
+        NumberVerificationManager manager =
+                new NumberVerificationManager(() -> new Phone[]{mPhone1, mPhone2});
+
+        manager.requestVerification(SAMPLE_RANGE, mCallback, DEFAULT_VERIFICATION_TIMEOUT);
+        verify(mCallback, never()).onVerificationFailed(anyInt());
+        verifyDefaultRangeMatching(manager);
+    }
+
+    @Test
+    public void testDoubleVerificationFailure() throws Exception {
+        NumberVerificationManager manager =
+                new NumberVerificationManager(() -> new Phone[]{mPhone1, mPhone2});
+        manager.requestVerification(SAMPLE_RANGE, mCallback, DEFAULT_VERIFICATION_TIMEOUT);
+        verifyDefaultRangeMatching(manager);
+        assertFalse(manager.checkIncomingCall("this doesn't even matter"));
+    }
+}