From 5314036bb183d9f1be62b13d47574638efcdd9cb Mon Sep 17 00:00:00 2001 From: Hall Liu Date: Wed, 28 Nov 2018 10:44:22 -0800 Subject: [PATCH] Add API scaffolding for number verification Add the PhoneNumberRange and NumberVerificationCallback classes. Add a method in TelephonyManager to activate the API, but it does nothing for now. Bug: 119675160 Test: todo Change-Id: I3ccd62b47f02a3aa324b675fdb16c8e7a1e9feec --- Android.bp | 1 + api/system-current.txt | 24 ++- .../telephony/NumberVerificationCallback.java | 88 +++++++++ .../android/telephony/PhoneNumberRange.aidl | 19 ++ .../android/telephony/PhoneNumberRange.java | 176 ++++++++++++++++++ .../android/telephony/TelephonyManager.java | 54 ++++++ .../INumberVerificationCallback.aidl | 22 +++ 7 files changed, 383 insertions(+), 1 deletion(-) create mode 100644 telephony/java/android/telephony/NumberVerificationCallback.java create mode 100644 telephony/java/android/telephony/PhoneNumberRange.aidl create mode 100644 telephony/java/android/telephony/PhoneNumberRange.java create mode 100644 telephony/java/com/android/internal/telephony/INumberVerificationCallback.aidl diff --git a/Android.bp b/Android.bp index 8b607459a98d..8cfa67e797b1 100644 --- a/Android.bp +++ b/Android.bp @@ -544,6 +544,7 @@ java_defaults { "telephony/java/com/android/internal/telephony/IApnSourceService.aidl", "telephony/java/com/android/internal/telephony/ICarrierConfigLoader.aidl", "telephony/java/com/android/internal/telephony/IMms.aidl", + "telephony/java/com/android/internal/telephony/INumberVerificationCallback.aidl", "telephony/java/com/android/internal/telephony/IOnSubscriptionsChangedListener.aidl", "telephony/java/com/android/internal/telephony/IPhoneStateListener.aidl", "telephony/java/com/android/internal/telephony/IPhoneSubInfo.aidl", diff --git a/api/system-current.txt b/api/system-current.txt index 8729354fd1bc..d024224997d1 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -4466,8 +4466,8 @@ package android.service.carrier { public abstract class ApnService extends android.app.Service { ctor public ApnService(); - method public abstract java.util.List onRestoreApns(int); method public android.os.IBinder onBind(android.content.Intent); + method public abstract java.util.List onRestoreApns(int); } } @@ -5181,6 +5181,26 @@ package android.telephony { field public static final int RESULT_SUCCESS = 0; // 0x0 } + public abstract interface NumberVerificationCallback { + method public default void onCallReceived(java.lang.String); + method public default void onVerificationFailed(int); + field public static final int REASON_CONCURRENT_REQUESTS = 4; // 0x4 + field public static final int REASON_IN_ECBM = 5; // 0x5 + field public static final int REASON_IN_EMERGENCY_CALL = 6; // 0x6 + field public static final int REASON_NETWORK_NOT_AVAILABLE = 2; // 0x2 + field public static final int REASON_TIMED_OUT = 1; // 0x1 + field public static final int REASON_TOO_MANY_CALLS = 3; // 0x3 + field public static final int REASON_UNSPECIFIED = 0; // 0x0 + } + + public final class PhoneNumberRange implements android.os.Parcelable { + ctor public PhoneNumberRange(java.lang.String, java.lang.String, java.lang.String, java.lang.String); + method public int describeContents(); + method public boolean matches(java.lang.String); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + } + public class PhoneStateListener { method public void onRadioPowerStateChanged(int); method public void onSrvccStateChanged(int); @@ -5345,6 +5365,7 @@ package android.telephony { method public boolean needsOtaServiceProvisioning(); method public boolean rebootRadio(); method public void requestCellInfoUpdate(android.os.WorkSource, java.util.concurrent.Executor, android.telephony.TelephonyManager.CellInfoCallback); + method public void requestNumberVerification(android.telephony.PhoneNumberRange, long, java.util.concurrent.Executor, android.telephony.NumberVerificationCallback); method public boolean resetRadioConfig(); method public int setAllowedCarriers(int, java.util.List); method public void setCarrierDataEnabled(boolean); @@ -5374,6 +5395,7 @@ package android.telephony { field public static final java.lang.String EXTRA_SIM_STATE = "android.telephony.extra.SIM_STATE"; field public static final java.lang.String EXTRA_VISUAL_VOICEMAIL_ENABLED_BY_USER_BOOL = "android.telephony.extra.VISUAL_VOICEMAIL_ENABLED_BY_USER_BOOL"; field public static final java.lang.String EXTRA_VOICEMAIL_SCRAMBLED_PIN_STRING = "android.telephony.extra.VOICEMAIL_SCRAMBLED_PIN_STRING"; + field public static final long MAX_NUMBER_VERIFICATION_TIMEOUT_MILLIS = 60000L; // 0xea60L field public static final int NETWORK_MODE_CDMA_EVDO = 4; // 0x4 field public static final int NETWORK_MODE_CDMA_NO_EVDO = 5; // 0x5 field public static final int NETWORK_MODE_EVDO_NO_CDMA = 6; // 0x6 diff --git a/telephony/java/android/telephony/NumberVerificationCallback.java b/telephony/java/android/telephony/NumberVerificationCallback.java new file mode 100644 index 000000000000..b00c57351589 --- /dev/null +++ b/telephony/java/android/telephony/NumberVerificationCallback.java @@ -0,0 +1,88 @@ +/* + * 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 android.telephony; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.SystemApi; + +/** + * A callback for number verification. After a request for number verification is received, + * the system will call {@link #onCallReceived(String)} if a phone call was received from a number + * matching the provided {@link PhoneNumberRange} or it will call {@link #onVerificationFailed(int)} + * if an error occurs. + * @hide + */ +@SystemApi +public interface NumberVerificationCallback { + /** @hide */ + @IntDef(value = {REASON_UNSPECIFIED, REASON_TIMED_OUT, REASON_NETWORK_NOT_AVAILABLE, + REASON_TOO_MANY_CALLS, REASON_CONCURRENT_REQUESTS, REASON_IN_ECBM, + REASON_IN_EMERGENCY_CALL}, + prefix = {"REASON_"}) + @interface NumberVerificationFailureReason {} + + /** + * Verification failed for an unspecified reason. + */ + int REASON_UNSPECIFIED = 0; + + /** + * Verification failed because no phone call was received from a matching number within the + * provided timeout. + */ + int REASON_TIMED_OUT = 1; + + /** + * Verification failed because no cellular voice network is available. + */ + int REASON_NETWORK_NOT_AVAILABLE = 2; + + /** + * Verification failed because there are currently too many ongoing phone calls for a new + * incoming phone call to be received. + */ + int REASON_TOO_MANY_CALLS = 3; + + /** + * Verification failed because a previous request for verification has not yet completed. + */ + int REASON_CONCURRENT_REQUESTS = 4; + + /** + * Verification failed because the phone is in emergency callback mode. + */ + int REASON_IN_ECBM = 5; + + /** + * Verification failed because the phone is currently in an emergency call. + */ + int REASON_IN_EMERGENCY_CALL = 6; + + /** + * Called when the device receives a phone call from the provided {@link PhoneNumberRange}. + * @param phoneNumber The phone number within the range that called. May or may not contain the + * country code, but will be entirely numeric. + */ + default void onCallReceived(@NonNull String phoneNumber) { } + + /** + * Called when verification fails for some reason. + * @param reason The reason for failure. + */ + default void onVerificationFailed(@NumberVerificationFailureReason int reason) { } +} diff --git a/telephony/java/android/telephony/PhoneNumberRange.aidl b/telephony/java/android/telephony/PhoneNumberRange.aidl new file mode 100644 index 000000000000..b0727be50e0b --- /dev/null +++ b/telephony/java/android/telephony/PhoneNumberRange.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 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 android.telephony; + +parcelable PhoneNumberRange; diff --git a/telephony/java/android/telephony/PhoneNumberRange.java b/telephony/java/android/telephony/PhoneNumberRange.java new file mode 100644 index 000000000000..d65156fd3ca2 --- /dev/null +++ b/telephony/java/android/telephony/PhoneNumberRange.java @@ -0,0 +1,176 @@ +/* + * 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 android.telephony; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +import java.util.Objects; +import java.util.regex.Pattern; + +/** + * This class is used to represent a range of phone numbers. Each range corresponds to a contiguous + * block of phone numbers. + * + * Example: + * {@code + * { + * mCountryCode = "1" + * mPrefix = "650555" + * mLowerBound = "0055" + * mUpperBound = "0899" + * } + * } + * would match 16505550089 and 6505550472, but not 63827593759 or 16505550900 + * @hide + */ +@SystemApi +public final class PhoneNumberRange implements Parcelable { + public static final Creator CREATOR = new Creator() { + @Override + public PhoneNumberRange createFromParcel(Parcel in) { + return new PhoneNumberRange(in); + } + + @Override + public PhoneNumberRange[] newArray(int size) { + return new PhoneNumberRange[size]; + } + }; + + private final String mCountryCode; + private final String mPrefix; + private final String mLowerBound; + private final String mUpperBound; + + /** + * @param countryCode The country code, omitting the leading "+" + * @param prefix A prefix that all numbers matching the range must have. + * @param lowerBound When concatenated with the prefix, represents the lower bound of phone + * numbers that match this range. + * @param upperBound When concatenated with the prefix, represents the upper bound of phone + * numbers that match this range. + */ + public PhoneNumberRange(@NonNull String countryCode, @NonNull String prefix, + @NonNull String lowerBound, @NonNull String upperBound) { + validateLowerAndUpperBounds(lowerBound, upperBound); + if (!Pattern.matches("[0-9]+", countryCode)) { + throw new IllegalArgumentException("Country code must be all numeric"); + } + if (!Pattern.matches("[0-9]+", prefix)) { + throw new IllegalArgumentException("Prefix must be all numeric"); + } + mCountryCode = countryCode; + mPrefix = prefix; + mLowerBound = lowerBound; + mUpperBound = upperBound; + } + + private PhoneNumberRange(Parcel in) { + mCountryCode = in.readStringNoHelper(); + mPrefix = in.readStringNoHelper(); + mLowerBound = in.readStringNoHelper(); + mUpperBound = in.readStringNoHelper(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeStringNoHelper(mCountryCode); + dest.writeStringNoHelper(mPrefix); + dest.writeStringNoHelper(mLowerBound); + dest.writeStringNoHelper(mUpperBound); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PhoneNumberRange that = (PhoneNumberRange) o; + return Objects.equals(mCountryCode, that.mCountryCode) + && Objects.equals(mPrefix, that.mPrefix) + && Objects.equals(mLowerBound, that.mLowerBound) + && Objects.equals(mUpperBound, that.mUpperBound); + } + + @Override + public int hashCode() { + return Objects.hash(mCountryCode, mPrefix, mLowerBound, mUpperBound); + } + + @Override + public String toString() { + return "PhoneNumberRange{" + + "mCountryCode='" + mCountryCode + '\'' + + ", mPrefix='" + mPrefix + '\'' + + ", mLowerBound='" + mLowerBound + '\'' + + ", mUpperBound='" + mUpperBound + '\'' + + '}'; + } + + private void validateLowerAndUpperBounds(String lowerBound, String upperBound) { + if (lowerBound.length() != upperBound.length()) { + throw new IllegalArgumentException("Lower and upper bounds must have the same length"); + } + if (!Pattern.matches("[0-9]+", lowerBound)) { + throw new IllegalArgumentException("Lower bound must be all numeric"); + } + if (!Pattern.matches("[0-9]+", upperBound)) { + throw new IllegalArgumentException("Upper bound must be all numeric"); + } + if (Integer.parseInt(lowerBound) > Integer.parseInt(upperBound)) { + throw new IllegalArgumentException("Lower bound must be lower than upper bound"); + } + } + + /** + * Checks to see if the provided phone number matches this range. + * @param number A phone number, with or without separators or a country code. + * @return {@code true} if the number matches, {@code false} otherwise. + */ + public boolean matches(String number) { + // Check the prefix, make sure it matches either with or without the country code. + String normalizedNumber = number.replaceAll("[^0-9]", ""); + String prefixWithCountryCode = mCountryCode + mPrefix; + String numberPostfix; + if (normalizedNumber.startsWith(prefixWithCountryCode)) { + numberPostfix = normalizedNumber.substring(prefixWithCountryCode.length()); + } else if (normalizedNumber.startsWith(mPrefix)) { + numberPostfix = normalizedNumber.substring(mPrefix.length()); + } else { + return false; + } + + // Next check the postfix to make sure it lies within the bounds. + try { + int lower = Integer.parseInt(mLowerBound); + int upper = Integer.parseInt(mUpperBound); + int numberToCheck = Integer.parseInt(numberPostfix); + return numberToCheck <= upper && numberToCheck >= lower; + } catch (NumberFormatException e) { + Log.e(PhoneNumberRange.class.getSimpleName(), "Invalid bounds or number.", e); + return false; + } + } +} diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index e7b4f6a98bbc..83ac0d07bd96 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -77,6 +77,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.telecom.ITelecomService; import com.android.internal.telephony.CellNetworkScanResult; import com.android.internal.telephony.IAns; +import com.android.internal.telephony.INumberVerificationCallback; import com.android.internal.telephony.IPhoneSubInfo; import com.android.internal.telephony.ITelephony; import com.android.internal.telephony.ITelephonyRegistry; @@ -1332,6 +1333,13 @@ public class TelephonyManager { */ public static final String EXTRA_RECOVERY_ACTION = "recoveryAction"; + /** + * The max value for the timeout passed in {@link #requestNumberVerification}. + * @hide + */ + @SystemApi + public static final long MAX_NUMBER_VERIFICATION_TIMEOUT_MILLIS = 60000; + // // // Device Info @@ -5476,6 +5484,52 @@ public class TelephonyManager { return slotIndex; } + /** + * Request that the next incoming call from a number matching {@code range} be intercepted. + * + * This API is intended for OEMs to provide a service for apps to verify the device's phone + * number. When called, the Telephony stack will store the provided {@link PhoneNumberRange} and + * intercept the next incoming call from a number that lies within the range, within a timeout + * specified by {@code timeoutMillis}. + * + * If such a phone call is received, the caller will be notified via + * {@link NumberVerificationCallback#onCallReceived(String)} on the provided {@link Executor}. + * If verification fails for any reason, the caller will be notified via + * {@link NumberVerificationCallback#onVerificationFailed(int)} + * on the provided {@link Executor}. + * + * In addition to the {@link Manifest.permission#MODIFY_PHONE_STATE} permission, callers of this + * API must also be listed in the device configuration as an authorized app in + * {@code packages/services/Telephony/res/values/config.xml} under the + * {@code config_number_verification_package_name} key. + * + * @hide + * @param range The range of phone numbers the caller expects a phone call from. + * @param timeoutMillis The amount of time to wait for such a call, or + * {@link #MAX_NUMBER_VERIFICATION_TIMEOUT_MILLIS}, whichever is lesser. + * @param executor The {@link Executor} that callbacks should be executed on. + * @param callback The callback to use for delivering results. + */ + @SystemApi + @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE) + public void requestNumberVerification(@NonNull PhoneNumberRange range, long timeoutMillis, + @NonNull @CallbackExecutor Executor executor, + @NonNull NumberVerificationCallback callback) { + INumberVerificationCallback internalCallback = new INumberVerificationCallback.Stub() { + @Override + public void onCallReceived(String phoneNumber) throws RemoteException { + Binder.withCleanCallingIdentity(() -> callback.onCallReceived(phoneNumber)); + } + + @Override + public void onVerificationFailed(int reason) throws RemoteException { + Binder.withCleanCallingIdentity(() -> callback.onVerificationFailed(reason)); + } + }; + + // TODO -- call the aidl method + } + /** * Sets a per-phone telephony property with the value specified. * diff --git a/telephony/java/com/android/internal/telephony/INumberVerificationCallback.aidl b/telephony/java/com/android/internal/telephony/INumberVerificationCallback.aidl new file mode 100644 index 000000000000..76918afb564a --- /dev/null +++ b/telephony/java/com/android/internal/telephony/INumberVerificationCallback.aidl @@ -0,0 +1,22 @@ +/* + * 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.internal.telephony; + +oneway interface INumberVerificationCallback { + void onCallReceived(String phoneNumber); + void onVerificationFailed(int reason); +}