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