Merge "CarrierRestrictionStatus API AIDL implementation"
diff --git a/src/com/android/phone/PhoneInterfaceManager.java b/src/com/android/phone/PhoneInterfaceManager.java
index fa06026..ccef4d8 100644
--- a/src/com/android/phone/PhoneInterfaceManager.java
+++ b/src/com/android/phone/PhoneInterfaceManager.java
@@ -72,6 +72,7 @@
import android.provider.DeviceConfig;
import android.provider.Settings;
import android.provider.Telephony;
+import android.service.carrier.CarrierIdentifier;
import android.sysprop.TelephonyProperties;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
@@ -223,6 +224,7 @@
import com.android.phone.callcomposer.ImageData;
import com.android.phone.settings.PickSmsSubscriptionActivity;
import com.android.phone.slice.SlicePurchaseController;
+import com.android.phone.utils.CarrierAllowListInfo;
import com.android.phone.vvm.PhoneAccountHandleConverter;
import com.android.phone.vvm.RemoteVvmTaskManager;
import com.android.phone.vvm.VisualVoicemailSettingsUtil;
@@ -374,7 +376,6 @@
private static final int EVENT_IS_VONR_ENABLED_DONE = 116;
private static final int CMD_PURCHASE_PREMIUM_CAPABILITY = 117;
private static final int EVENT_PURCHASE_PREMIUM_CAPABILITY_DONE = 118;
-
// Parameters of select command.
private static final int SELECT_COMMAND = 0xA4;
private static final int SELECT_P1 = 0x04;
@@ -1561,7 +1562,39 @@
loge("getAllowedCarriers: Unknown exception");
}
}
- notifyRequester(request);
+ if (request.argument != null) {
+ // This is for the implementation of carrierRestrictionStatus.
+ CallerCallbackInfo callbackInfo = (CallerCallbackInfo) request.argument;
+ Consumer<Integer> callback = callbackInfo.getConsumer();
+ int callerCarrierId = callbackInfo.getCarrierId();
+ int lockStatus = TelephonyManager.CARRIER_RESTRICTION_STATUS_UNKNOWN;
+ if (ar.exception == null && ar.result instanceof CarrierRestrictionRules) {
+ CarrierRestrictionRules carrierRestrictionRules =
+ (CarrierRestrictionRules) ar.result;
+ int carrierId = -1;
+ try {
+ CarrierIdentifier carrierIdentifier =
+ carrierRestrictionRules.getAllowedCarriers().get(0);
+ carrierId = CarrierResolver.getCarrierIdFromIdentifier(mApp,
+ carrierIdentifier);
+ } catch (NullPointerException | IndexOutOfBoundsException ex) {
+ Rlog.e(LOG_TAG, "CarrierIdentifier exception = " + ex);
+ }
+ lockStatus = carrierRestrictionRules.getCarrierRestrictionStatus();
+ if (carrierId != -1 && callerCarrierId == carrierId && lockStatus
+ == TelephonyManager.CARRIER_RESTRICTION_STATUS_RESTRICTED) {
+ lockStatus =
+ TelephonyManager.CARRIER_RESTRICTION_STATUS_RESTRICTED_TO_CALLER;
+ }
+ } else {
+ Rlog.e(LOG_TAG,
+ "getCarrierRestrictionStatus: exception ex = " + ar.exception);
+ }
+ callback.accept(lockStatus);
+ } else {
+ // This is for the implementation of getAllowedCarriers.
+ notifyRequester(request);
+ }
break;
case EVENT_GET_FORBIDDEN_PLMNS_DONE:
@@ -2428,6 +2461,7 @@
mTelephony2gUpdater = new Telephony2gUpdater(mApp);
mTelephony2gUpdater.init();
publish();
+ CarrierAllowListInfo.loadInstance(mApp);
}
@VisibleForTesting
@@ -3695,13 +3729,23 @@
}
/**
- * Make sure the caller has the MODIFY_PHONE_STATE permission.
+ * Make sure the caller has the READ_PHONE_STATE permission.
*
* @throws SecurityException if the caller does not have the required permission
*/
@VisibleForTesting
public void enforceReadPermission() {
- mApp.enforceCallingOrSelfPermission(android.Manifest.permission.READ_PHONE_STATE, null);
+ enforceReadPermission(null);
+ }
+
+ /**
+ * Make sure the caller has the READ_PHONE_STATE permissions.
+ *
+ * @throws SecurityException if the caller does not have the READ_PHONE_STATE permission.
+ */
+ @VisibleForTesting
+ public void enforceReadPermission(String msg) {
+ mApp.enforceCallingOrSelfPermission(android.Manifest.permission.READ_PHONE_STATE, msg);
}
private void enforceActiveEmergencySessionPermission() {
@@ -8402,7 +8446,8 @@
*
* @throws SecurityException if the caller does not have the required permission
*/
- private void enforceReadPrivilegedPermission(String message) {
+ @VisibleForTesting
+ public void enforceReadPrivilegedPermission(String message) {
mApp.enforceCallingOrSelfPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
message);
}
@@ -8612,6 +8657,38 @@
}
/**
+ * Fetches the carrier restriction status of the device and sends the status to the caller
+ * through the callback.
+ *
+ * @param callback The callback that will be used to send the result.
+ * @throws SecurityException if the caller does not have the required permission/privileges or
+ * the caller is not allowlisted.
+ */
+ @Override
+ public void getCarrierRestrictionStatus(IIntegerConsumer callback, String packageName) {
+ enforceReadPermission("getCarrierRestrictionStatus");
+ int carrierId = validateCallerAndGetCarrierId(packageName);
+ if (carrierId == CarrierAllowListInfo.INVALID_CARRIER_ID) {
+ Rlog.e(LOG_TAG, "getCarrierRestrictionStatus: caller is not registered");
+ throw new SecurityException("Not an authorized caller");
+ }
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ Consumer<Integer> consumer = FunctionalUtils.ignoreRemoteException(callback::accept);
+ CallerCallbackInfo callbackInfo = new CallerCallbackInfo(consumer, carrierId);
+ sendRequestAsync(CMD_GET_ALLOWED_CARRIERS, callbackInfo);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @VisibleForTesting
+ public int validateCallerAndGetCarrierId(String packageName) {
+ CarrierAllowListInfo allowListInfo = CarrierAllowListInfo.loadInstance(mApp);
+ return allowListInfo.validateCallerAndGetCarrierId(packageName);
+ }
+
+ /**
* Action set from carrier signalling broadcast receivers to enable/disable radio
* @param subId the subscription ID that this action applies to.
* @param enabled control enable or disable radio.
@@ -11974,4 +12051,25 @@
}
return false;
}
+
+ /**
+ * Class binds the consumer[callback] and carrierId.
+ */
+ private static class CallerCallbackInfo {
+ private final Consumer<Integer> mConsumer;
+ private final int mCarrierId;
+
+ public CallerCallbackInfo(Consumer<Integer> consumer, int carrierId) {
+ mConsumer = consumer;
+ mCarrierId = carrierId;
+ }
+
+ public Consumer<Integer> getConsumer() {
+ return mConsumer;
+ }
+
+ public int getCarrierId() {
+ return mCarrierId;
+ }
+ }
}
\ No newline at end of file
diff --git a/src/com/android/phone/TelephonyShellCommand.java b/src/com/android/phone/TelephonyShellCommand.java
index fdaf1bb..f002e25 100644
--- a/src/com/android/phone/TelephonyShellCommand.java
+++ b/src/com/android/phone/TelephonyShellCommand.java
@@ -57,6 +57,7 @@
import com.android.internal.telephony.util.TelephonyUtils;
import com.android.modules.utils.BasicShellCommandHandler;
import com.android.phone.callcomposer.CallComposerPictureManager;
+import com.android.phone.utils.CarrierAllowListInfo;
import java.io.IOException;
import java.io.PrintWriter;
@@ -97,6 +98,8 @@
private static final String ENABLE = "enable";
private static final String DISABLE = "disable";
private static final String QUERY = "query";
+ private static final String CARRIER_RESTRICTION_STATUS_TEST = "carrier_restriction_status_test";
+ private final String QUOTES = "\"";
private static final String CALL_COMPOSER_TEST_MODE = "test-mode";
private static final String CALL_COMPOSER_SIMULATE_CALL = "simulate-outgoing-call";
@@ -341,6 +344,8 @@
return handleGetSimSlotsMapping();
case RADIO_SUBCOMMAND:
return handleRadioCommand();
+ case CARRIER_RESTRICTION_STATUS_TEST:
+ return handleCarrierRestrictionStatusCommand();
default: {
return handleDefaultCommands(cmd);
}
@@ -3011,4 +3016,74 @@
return -1;
}
+
+ private int handleCarrierRestrictionStatusCommand() {
+ try {
+ String MOCK_MODEM_SERVICE_NAME = "android.telephony.mockmodem.MockModemService";
+ if (!(checkShellUid() && MOCK_MODEM_SERVICE_NAME.equalsIgnoreCase(
+ mInterface.getModemService()))) {
+ Log.v(LOG_TAG,
+ "handleCarrierRestrictionStatusCommand, MockModem service check fails or "
+ + " checkShellUid fails");
+ return -1;
+ }
+ } catch (RemoteException ex) {
+ ex.printStackTrace();
+ }
+ String callerInfo = getNextOption();
+ CarrierAllowListInfo allowListInfo = CarrierAllowListInfo.loadInstance(mContext);
+ if (TextUtils.isEmpty(callerInfo)) {
+ // reset the Json content after testing
+ allowListInfo.updateJsonForTest(null);
+ return 0;
+ }
+ if (callerInfo.startsWith("--")) {
+ callerInfo = callerInfo.replace("--", "");
+ }
+ String params[] = callerInfo.split(",");
+ StringBuffer jsonStrBuffer = new StringBuffer();
+ String tokens;
+ for (int index = 0; index < params.length; index++) {
+ tokens = convertToJsonString(index, params[index]);
+ if (TextUtils.isEmpty(tokens)) {
+ // received wrong format from CTS
+ if (VDBG) {
+ Log.v(LOG_TAG,
+ "handleCarrierRestrictionStatusCommand, Shell command parsing error");
+ }
+ return -1;
+ }
+ jsonStrBuffer.append(tokens);
+ }
+ int result = allowListInfo.updateJsonForTest(jsonStrBuffer.toString());
+ return result;
+ }
+
+
+ /**
+ * Building the string that can be used to build the JsonObject which supports to stub the data
+ * in CarrierAllowListInfo for CTS testing. sample format is like
+ * {"com.android.example":{"carrierId":"10000","callerSHA1Id":["XXXXXXXXXXXXXX"]}}
+ */
+ private String convertToJsonString(int index, String param) {
+
+ String token[] = param.split(":");
+ String jSonString;
+ switch (index) {
+ case 0:
+ jSonString = "{" + QUOTES + token[1] + QUOTES + ":";
+ break;
+ case 1:
+ jSonString =
+ "{" + QUOTES + token[0] + QUOTES + ":" + QUOTES + token[1] + QUOTES + ",";
+ break;
+ case 2:
+ jSonString =
+ QUOTES + token[0] + QUOTES + ":" + "[" + QUOTES + token[1] + QUOTES + "]}}";
+ break;
+ default:
+ jSonString = null;
+ }
+ return jSonString;
+ }
}
diff --git a/src/com/android/phone/utils/CarrierAllowListInfo.java b/src/com/android/phone/utils/CarrierAllowListInfo.java
new file mode 100644
index 0000000..208eff3
--- /dev/null
+++ b/src/com/android/phone/utils/CarrierAllowListInfo.java
@@ -0,0 +1,198 @@
+/*
+ * 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.phone.utils;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.Signature;
+import android.telephony.Rlog;
+import android.text.TextUtils;
+
+import com.android.internal.telephony.uicc.IccUtils;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.List;
+
+
+public class CarrierAllowListInfo {
+ private static final String LOG_TAG = "CarrierAllowListInfo";
+ private JSONObject mDataJSON;
+ private static final String JSON_CHARSET = "UTF-8";
+ private static final String MESSAGE_DIGEST_ALGORITHM = "SHA1";
+ private static final String CALLER_SHA_1_ID = "callerSHA1Id";
+ private static final String CALLER_CARRIER_ID = "carrierId";
+ public static final int INVALID_CARRIER_ID = -1;
+
+ private static final String CARRIER_RESTRICTION_OPERATOR_REGISTERED_FILE =
+ "CarrierRestrictionOperatorDetails.json";
+
+ private static CarrierAllowListInfo mInstance = null;
+ private Context mContext;
+
+ private CarrierAllowListInfo(Context context) {
+ mContext = context;
+ loadJsonFile(context);
+ }
+
+ public static CarrierAllowListInfo loadInstance(Context context) {
+ if (mInstance == null) {
+ mInstance = new CarrierAllowListInfo(context);
+ }
+ return mInstance;
+ }
+
+ public int validateCallerAndGetCarrierId(String packageName) {
+ CarrierInfo carrierInfo = parseJsonForCallerInfo(packageName);
+ boolean isValid = (carrierInfo != null) && validateCallerSignature(mContext, packageName,
+ carrierInfo.getSHAIdList());
+ return (isValid) ? carrierInfo.getCallerCarrierId() : INVALID_CARRIER_ID;
+ }
+
+ private void loadJsonFile(Context context) {
+ try {
+ String jsonString = getJsonFromAssets(context,
+ CARRIER_RESTRICTION_OPERATOR_REGISTERED_FILE, JSON_CHARSET);
+ if (!TextUtils.isEmpty(jsonString)) {
+ mDataJSON = new JSONObject(jsonString);
+ }
+ } catch (Exception ex) {
+ Rlog.e(LOG_TAG, "CarrierAllowListInfo: JSON file reading exception = " + ex);
+ }
+ }
+
+ /**
+ * Parse the JSON object to fetch the given caller's SHA-Ids and carrierId.
+ */
+ private CarrierInfo parseJsonForCallerInfo(String callerPackage) {
+ try {
+ if (mDataJSON != null && callerPackage != null) {
+ JSONObject callerJSON = mDataJSON.getJSONObject(callerPackage.trim());
+ JSONArray callerJSONArray = callerJSON.getJSONArray(CALLER_SHA_1_ID);
+ int carrierId = callerJSON.getInt(CALLER_CARRIER_ID);
+ List<String> appSignatures = new ArrayList<>();
+ for (int index = 0; index < callerJSONArray.length(); index++) {
+ appSignatures.add((String) callerJSONArray.get(index));
+ }
+ return new CarrierInfo(carrierId, appSignatures);
+ }
+ } catch (JSONException ex) {
+ Rlog.e(LOG_TAG, "getCallerSignatureInfo: JSONException = " + ex);
+ }
+ return null;
+ }
+
+ /**
+ * Read the Json file from the assert folder.
+ *
+ * @param context context
+ * @param fileName JSON file name in assets folder
+ * @param charset JSON file data format
+ * @return JSON file content in string format or null in case of IOException
+ */
+ private static String getJsonFromAssets(Context context, String fileName, String charset) {
+ String jsonStr;
+ try {
+ InputStream ipStream = context.getAssets().open(fileName);
+ int bufSize = ipStream.available();
+ byte[] fileBuffer = new byte[bufSize];
+ ipStream.read(fileBuffer);
+ ipStream.close();
+ jsonStr = new String(fileBuffer, charset);
+ } catch (IOException ex) {
+ Rlog.e(LOG_TAG, "getJsonFromAssets: Exception = " + ex);
+ return null;
+ }
+ return jsonStr;
+ }
+
+ /**
+ * API fetches all the related signatures of the given package from the packageManager
+ * and validate all the signatures.
+ *
+ * @param context context
+ * @param packageName package name of the caller to validate the signatures.
+ * @param allowListSignatures list of signatures to be validated.
+ * @return {@code true} if all the signatures are available with package manager.
+ * {@code false} if any one of the signatures won't match with package manager.
+ */
+ public static boolean validateCallerSignature(Context context, String packageName,
+ List<String> allowListSignatures) {
+ if (TextUtils.isEmpty(packageName) || allowListSignatures.size() == 0) {
+ // package name is mandatory
+ return false;
+ }
+ final PackageManager packageManager = context.getPackageManager();
+ try {
+ MessageDigest sha1MDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM);
+ final PackageInfo packageInfo = packageManager.getPackageInfo(packageName,
+ PackageManager.GET_SIGNATURES);
+ for (Signature signature : packageInfo.signatures) {
+ final byte[] signatureSha1 = sha1MDigest.digest(signature.toByteArray());
+ final String hexSignatureSha1 = IccUtils.bytesToHexString(signatureSha1);
+ if (!allowListSignatures.contains(hexSignatureSha1)) {
+ return false;
+ }
+ }
+ return true;
+ } catch (NoSuchAlgorithmException | PackageManager.NameNotFoundException ex) {
+ Rlog.e(LOG_TAG, "validateCallerSignature: Exception = " + ex);
+ return false;
+ }
+ }
+
+ public int updateJsonForTest(String callerInfo) {
+ try {
+ if (callerInfo == null) {
+ // reset the Json content after testing
+ loadJsonFile(mContext);
+ } else {
+ mDataJSON = new JSONObject(callerInfo);
+ }
+ return 0;
+ } catch (JSONException ex) {
+ Rlog.e(LOG_TAG, "updateJsonForTest: Exception = " + ex);
+ }
+ return -1;
+ }
+
+ private static class CarrierInfo {
+ final private int mCallerCarrierId;
+ final private List<String> mSHAIdList;
+
+ public CarrierInfo(int carrierId, List<String> SHAIds) {
+ mCallerCarrierId = carrierId;
+ mSHAIdList = SHAIds;
+ }
+
+ public int getCallerCarrierId() {
+ return mCallerCarrierId;
+ }
+
+ public List<String> getSHAIdList() {
+ return mSHAIdList;
+ }
+ }
+}
diff --git a/tests/src/com/android/phone/PhoneInterfaceManagerTest.java b/tests/src/com/android/phone/PhoneInterfaceManagerTest.java
index b6d1087..4cc793d 100644
--- a/tests/src/com/android/phone/PhoneInterfaceManagerTest.java
+++ b/tests/src/com/android/phone/PhoneInterfaceManagerTest.java
@@ -21,6 +21,8 @@
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
@@ -42,6 +44,7 @@
import com.android.TelephonyTestBase;
import com.android.internal.telephony.Phone;
import com.android.internal.telephony.RILConstants;
+import com.android.internal.telephony.IIntegerConsumer;
import org.junit.Before;
import org.junit.Test;
@@ -57,6 +60,7 @@
public class PhoneInterfaceManagerTest extends TelephonyTestBase {
private PhoneInterfaceManager mPhoneInterfaceManager;
private SharedPreferences mSharedPreferences;
+ private IIntegerConsumer mIIntegerConsumer;
@Mock
PhoneGlobals mPhoneGlobals;
@@ -74,6 +78,7 @@
mPhoneInterfaceManager = spy(PhoneInterfaceManager.init(mPhoneGlobals));
mSharedPreferences = mPhoneInterfaceManager.getSharedPreferences();
mSharedPreferences.edit().remove(Phone.PREF_NULL_CIPHER_AND_INTEGRITY_ENABLED).commit();
+ mIIntegerConsumer = mock(IIntegerConsumer.class);
}
@Test
@@ -249,4 +254,27 @@
doReturn(mPhone).when(
mPhoneInterfaceManager).getDefaultPhone();
}
+
+ /**
+ * Verify getCarrierRestrictionStatus throws exception for invalid caller package name.
+ */
+ @Test
+ public void getCarrierRestrictionStatus_ReadPrivilegedException2() {
+ doThrow(SecurityException.class).when(
+ mPhoneInterfaceManager).enforceReadPrivilegedPermission(anyString());
+ assertThrows(SecurityException.class, () -> {
+ mPhoneInterfaceManager.getCarrierRestrictionStatus(mIIntegerConsumer, "");
+ });
+ }
+
+ /**
+ * Verify getCarrierRestrictionStatus doesn't throw any exception with valid package name
+ * and with READ_PHONE_STATE permission granted.
+ */
+ @Test
+ public void getCarrierRestrictionStatus() {
+ when(mPhoneInterfaceManager.validateCallerAndGetCarrierId(anyString())).thenReturn(1);
+ mPhoneInterfaceManager.getCarrierRestrictionStatus(mIIntegerConsumer,
+ "com.test.package");
+ }
}
diff --git a/tests/src/com/android/services/telephony/TelephonyManagerTest.java b/tests/src/com/android/services/telephony/TelephonyManagerTest.java
index eec38ce..20c062f 100644
--- a/tests/src/com/android/services/telephony/TelephonyManagerTest.java
+++ b/tests/src/com/android/services/telephony/TelephonyManagerTest.java
@@ -56,6 +56,10 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
/** Unit tests for {@link TelephonyManager}. */
@RunWith(AndroidJUnit4.class)
@@ -271,4 +275,24 @@
assertEquals("12345", mTelephonyManager.getPrimaryImei());
verify(mMockITelephony, times(1)).getPrimaryImei(anyString(), anyString());
}
-}
+
+ /**
+ * Verify calling getCarrierRestrictionStatus() with out exception
+ */
+ @Test
+ public void getCarrierRestrictionStatus() {
+ int TIMEOUT = 2 * 60; // 2 minutes
+ LinkedBlockingQueue<Integer> carrierRestrictionStatusResult = new LinkedBlockingQueue<>(1);
+ Executor executor = Executors.newSingleThreadExecutor();
+ mTelephonyManager.getCarrierRestrictionStatus(executor,
+ carrierRestrictionStatusResult::offer);
+ executor.execute(() -> {
+ try {
+ carrierRestrictionStatusResult.poll(TIMEOUT, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ fail();
+ }
+ });
+
+ }
+}
\ No newline at end of file