Add functionality for number verification
Add the NumberVerificationManager and plumb it into the incoming call
flow. Also make changes to testapps to enable testing this and add a
shell command to override the authorized package.
Bug: 119675160
Test: unit, testapps. GTS later.
Change-Id: I606c4595c7ff22074314b54cafe1eff475f15ea3
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 cb59583..a41e9d9 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;
@@ -105,6 +108,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;
@@ -2374,6 +2378,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.
*/
@@ -5336,7 +5364,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()) {