Add unit test for CarrierConfigLoader

Change CarrierConfigLoader a bit to make it more testable:
1. Inject SubscriptionInfoUpdater into CarrierConfigLoader to make
it both mockable and verifible.
2. Inject Looper into CarrirConfigLoader to take advantage of
TestableLooper in UT.
3. User PersistableBundle.EMPTY instead of creating new empty
instance to make it value comparable since the class does not
override equals method.
4. Make configs readable to test case to verify the values
(intead of verifying the behavior).

Bug: 182922049
Test: atest  com.android.phone.CarrierConfigLoaderTest

Change-Id: Ia29f6daef606e13578311bfccd350cc1d891fca0
diff --git a/src/com/android/phone/CarrierConfigLoader.java b/src/com/android/phone/CarrierConfigLoader.java
index 4ff7316..e729d28 100644
--- a/src/com/android/phone/CarrierConfigLoader.java
+++ b/src/com/android/phone/CarrierConfigLoader.java
@@ -36,6 +36,7 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
+import android.os.Looper;
 import android.os.Message;
 import android.os.PersistableBundle;
 import android.os.Process;
@@ -55,6 +56,7 @@
 import android.util.LocalLog;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.telephony.ICarrierConfigLoader;
 import com.android.internal.telephony.IccCardConstants;
 import com.android.internal.telephony.Phone;
@@ -201,6 +203,10 @@
     // 3. clearing config (e.g. due to sim removal)
     // 4. encountering bind or IPC error
     private class ConfigHandler extends Handler {
+        ConfigHandler(@NonNull Looper looper) {
+            super(looper);
+        }
+
         @Override
         public void handleMessage(Message msg) {
             final int phoneId = msg.arg1;
@@ -287,7 +293,7 @@
                         } else {
                             // Put a stub bundle in place so that the rest of the logic continues
                             // smoothly.
-                            mConfigFromDefaultApp[phoneId] = new PersistableBundle();
+                            mConfigFromDefaultApp[phoneId] = PersistableBundle.EMPTY;
                             // Send broadcast if bind fails.
                             notifySubscriptionInfoUpdater(phoneId);
                             // TODO: We *must* call unbindService even if bindService returns false.
@@ -372,7 +378,7 @@
                         broadcastConfigChangedIntent(phoneId);
                     }
                     // Put a stub bundle in place so that the rest of the logic continues smoothly.
-                    mConfigFromDefaultApp[phoneId] = new PersistableBundle();
+                    mConfigFromDefaultApp[phoneId] = PersistableBundle.EMPTY;
                     notifySubscriptionInfoUpdater(phoneId);
                     break;
                 }
@@ -419,7 +425,7 @@
                         } else {
                             // Put a stub bundle in place so that the rest of the logic continues
                             // smoothly.
-                            mConfigFromCarrierApp[phoneId] = new PersistableBundle();
+                            mConfigFromCarrierApp[phoneId] = PersistableBundle.EMPTY;
                             // Send broadcast if bind fails.
                             broadcastConfigChangedIntent(phoneId);
                             loge("Bind to carrier app: " + carrierPackageName + " fails");
@@ -504,7 +510,7 @@
                         broadcastConfigChangedIntent(phoneId);
                     }
                     // Put a stub bundle in place so that the rest of the logic continues smoothly.
-                    mConfigFromCarrierApp[phoneId] = new PersistableBundle();
+                    mConfigFromCarrierApp[phoneId] = PersistableBundle.EMPTY;
                     notifySubscriptionInfoUpdater(phoneId);
                     break;
                 }
@@ -666,11 +672,13 @@
      * Constructs a CarrierConfigLoader, registers it as a service, and registers a broadcast
      * receiver for relevant events.
      */
-    private CarrierConfigLoader(Context context) {
+    @VisibleForTesting
+    /* package */ CarrierConfigLoader(Context context,
+            SubscriptionInfoUpdater subscriptionInfoUpdater, @NonNull Looper looper) {
         mContext = context;
         mPlatformCarrierConfigPackage =
                 mContext.getString(R.string.platform_carrier_config_package);
-        mHandler = new ConfigHandler();
+        mHandler = new ConfigHandler(looper);
 
         IntentFilter bootFilter = new IntentFilter();
         bootFilter.addAction(Intent.ACTION_BOOT_COMPLETED);
@@ -690,7 +698,7 @@
         mConfigFromCarrierApp = new PersistableBundle[numPhones];
         mPersistentOverrideConfigs = new PersistableBundle[numPhones];
         mOverrideConfigs = new PersistableBundle[numPhones];
-        mNoSimConfig = new PersistableBundle();
+        mNoSimConfig = PersistableBundle.EMPTY;
         mServiceConnection = new CarrierServiceConnection[numPhones];
         mServiceBound = new boolean[numPhones];
         mHasSentConfigChange = new boolean[numPhones];
@@ -701,7 +709,7 @@
         TelephonyFrameworkInitializer
                 .getTelephonyServiceManager().getCarrierConfigServiceRegisterer().register(this);
         logd("CarrierConfigLoader has started");
-        mSubscriptionInfoUpdater = PhoneFactory.getSubscriptionInfoUpdater();
+        mSubscriptionInfoUpdater = subscriptionInfoUpdater;
         mHandler.sendEmptyMessage(EVENT_CHECK_SYSTEM_UPDATE);
     }
 
@@ -710,11 +718,11 @@
      *
      * This is only done once, at startup, from {@link com.android.phone.PhoneApp#onCreate}.
      */
-    /* package */
-    static CarrierConfigLoader init(Context context) {
+    /* package */ static CarrierConfigLoader init(Context context) {
         synchronized (CarrierConfigLoader.class) {
             if (sInstance == null) {
-                sInstance = new CarrierConfigLoader(context);
+                sInstance = new CarrierConfigLoader(context,
+                        PhoneFactory.getSubscriptionInfoUpdater(), Looper.myLooper());
             } else {
                 Log.wtf(LOG_TAG, "init() called multiple times!  sInstance = " + sInstance);
             }
@@ -722,7 +730,8 @@
         }
     }
 
-    private void clearConfigForPhone(int phoneId, boolean fetchNoSimConfig) {
+    @VisibleForTesting
+    /* package */ void clearConfigForPhone(int phoneId, boolean fetchNoSimConfig) {
         /* Ignore clear configuration request if device is being shutdown. */
         Phone phone = PhoneFactory.getPhone(phoneId);
         if (phone != null) {
@@ -759,7 +768,7 @@
         }
 
         if (configToSend == null) {
-            configToSend = new PersistableBundle();
+            configToSend = PersistableBundle.EMPTY;
         }
 
         // mOverrideConfigs is for testing. And it will override current configs.
@@ -844,7 +853,8 @@
         }
     }
 
-    private CarrierIdentifier getCarrierIdentifierForPhoneId(int phoneId) {
+    @VisibleForTesting
+    /* package */ CarrierIdentifier getCarrierIdentifierForPhoneId(int phoneId) {
         String mcc = "";
         String mnc = "";
         String imsi = "";
@@ -1000,12 +1010,14 @@
         }
     }
 
-    private void saveConfigToXml(String packageName, @NonNull String extraString, int phoneId,
+    @VisibleForTesting
+    /* package */ void saveConfigToXml(String packageName, @NonNull String extraString, int phoneId,
             CarrierIdentifier carrierId, PersistableBundle config) {
         saveConfigToXml(packageName, extraString, phoneId, carrierId, config, false);
     }
 
-    private void saveNoSimConfigToXml(String packageName, PersistableBundle config) {
+    @VisibleForTesting
+    /* package */ void saveNoSimConfigToXml(String packageName, PersistableBundle config) {
         saveConfigToXml(packageName, "", -1, null, config, true);
     }
 
@@ -1176,7 +1188,7 @@
             String callingFeatureId) {
         if (!TelephonyPermissions.checkCallingOrSelfReadPhoneState(mContext, subId, callingPackage,
                 callingFeatureId, "getCarrierConfig")) {
-            return new PersistableBundle();
+            return PersistableBundle.EMPTY;
         }
 
         int phoneId = SubscriptionManager.getPhoneId(subId);
@@ -1253,7 +1265,7 @@
     private void overrideConfig(@NonNull PersistableBundle[] currentOverrides, int phoneId,
             @Nullable PersistableBundle overrides) {
         if (overrides == null) {
-            currentOverrides[phoneId] = new PersistableBundle();
+            currentOverrides[phoneId] = PersistableBundle.EMPTY;
         } else if (currentOverrides[phoneId] == null) {
             currentOverrides[phoneId] = overrides;
         } else {
@@ -1315,6 +1327,31 @@
         return mPlatformCarrierConfigPackage;
     }
 
+    @VisibleForTesting
+    /* package */ Handler getHandler() {
+        return mHandler;
+    }
+
+    @VisibleForTesting
+    /* package */ PersistableBundle getConfigFromDefaultApp(int phoneId) {
+        return mConfigFromDefaultApp[phoneId];
+    }
+
+    @VisibleForTesting
+    /* package */ PersistableBundle getConfigFromCarrierApp(int phoneId) {
+        return mConfigFromCarrierApp[phoneId];
+    }
+
+    @VisibleForTesting
+     /* package */ PersistableBundle getNoSimConfig() {
+        return mNoSimConfig;
+    }
+
+    @VisibleForTesting
+    /* package */ PersistableBundle getOverrideConfig(int phoneId) {
+        return mOverrideConfigs[phoneId];
+    }
+
     private void unbindIfBound(Context context, CarrierServiceConnection conn,
             int phoneId) {
         if (mServiceBound[phoneId]) {
diff --git a/tests/src/com/android/TestContext.java b/tests/src/com/android/TestContext.java
index 9d712d3..fc5ee4c 100644
--- a/tests/src/com/android/TestContext.java
+++ b/tests/src/com/android/TestContext.java
@@ -24,6 +24,8 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.os.Binder;
 import android.os.Handler;
 import android.os.PersistableBundle;
 import android.telecom.TelecomManager;
@@ -32,16 +34,22 @@
 import android.telephony.TelephonyManager;
 import android.telephony.ims.ImsManager;
 import android.test.mock.MockContext;
+import android.util.Log;
 import android.util.SparseArray;
 
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 import org.mockito.stubbing.Answer;
 
+import java.util.HashSet;
 import java.util.concurrent.Executor;
 
 public class TestContext extends MockContext {
 
+    private static final String TAG = "TestContext";
+    // Stub used to grant all permissions
+    public static final String STUB_PERMISSION_ENABLE_ALL = "stub_permission_enable_all";
+
     @Mock CarrierConfigManager mMockCarrierConfigManager;
     @Mock TelecomManager mMockTelecomManager;
     @Mock TelephonyManager mMockTelephonyManager;
@@ -50,6 +58,8 @@
 
     private SparseArray<PersistableBundle> mCarrierConfigs = new SparseArray<>();
 
+    private final HashSet<String> mPermissionTable = new HashSet<>();
+
     public TestContext() {
         MockitoAnnotations.initMocks(this);
         doAnswer((Answer<PersistableBundle>) invocation -> {
@@ -161,4 +171,67 @@
         }
         return b;
     }
+
+    @Override
+    public void enforceCallingOrSelfPermission(String permission, String message) {
+        if (checkCallingOrSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
+            throw new SecurityException(permission + " denied: " + message);
+        }
+    }
+
+    @Override
+    public void enforcePermission(String permission, int pid, int uid, String message) {
+        enforceCallingOrSelfPermission(permission, message);
+    }
+
+    @Override
+    public void enforceCallingPermission(String permission, String message) {
+        enforceCallingOrSelfPermission(permission, message);
+    }
+
+    @Override
+    public int checkCallingOrSelfPermission(String permission) {
+        return checkPermission(permission, Binder.getCallingPid(), Binder.getCallingUid());
+    }
+
+    @Override
+    public int checkPermission(String permission, int pid, int uid) {
+        synchronized (mPermissionTable) {
+            if (mPermissionTable.contains(permission)
+                    || mPermissionTable.contains(STUB_PERMISSION_ENABLE_ALL)) {
+                logd("checkCallingOrSelfPermission: " + permission + " return GRANTED");
+                return PackageManager.PERMISSION_GRANTED;
+            } else {
+                logd("checkCallingOrSelfPermission: " + permission + " return DENIED");
+                return PackageManager.PERMISSION_DENIED;
+            }
+        }
+    }
+
+    public void grantPermission(String permission) {
+        synchronized (mPermissionTable) {
+            if (mPermissionTable != null && permission != null) {
+                mPermissionTable.remove(STUB_PERMISSION_ENABLE_ALL);
+                mPermissionTable.add(permission);
+            }
+        }
+    }
+
+    public void revokePermission(String permission) {
+        synchronized (mPermissionTable) {
+            if (mPermissionTable != null && permission != null) {
+                mPermissionTable.remove(permission);
+            }
+        }
+    }
+
+    public void revokeAllPermissions() {
+        synchronized (mPermissionTable) {
+            mPermissionTable.clear();
+        }
+    }
+
+    private static void logd(String s) {
+        Log.d(TAG, s);
+    }
 }
diff --git a/tests/src/com/android/phone/CarrierConfigLoaderTest.java b/tests/src/com/android/phone/CarrierConfigLoaderTest.java
new file mode 100644
index 0000000..7fd9069
--- /dev/null
+++ b/tests/src/com/android/phone/CarrierConfigLoaderTest.java
@@ -0,0 +1,381 @@
+/*
+ * Copyright (C) 2021 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 com.android.TestContext.STUB_PERMISSION_ENABLE_ALL;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.os.Build;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.os.PersistableBundle;
+import android.os.UserHandle;
+import android.service.carrier.CarrierIdentifier;
+import android.telephony.CarrierConfigManager;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.testing.TestableLooper;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.TelephonyTestBase;
+import com.android.internal.telephony.IccCardConstants;
+import com.android.internal.telephony.SubscriptionInfoUpdater;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.List;
+
+/**
+ * Unit Test for CarrierConfigLoader.
+ */
+@RunWith(AndroidJUnit4.class)
+public class CarrierConfigLoaderTest extends TelephonyTestBase {
+
+    private static final int DEFAULT_PHONE_ID = 0;
+    private static final int DEFAULT_SUB_ID = SubscriptionManager.getDefaultSubscriptionId();
+    private static final String PLATFORM_CARRIER_CONFIG_PACKAGE = "com.android.carrierconfig";
+    private static final long PLATFORM_CARRIER_CONFIG_PACKAGE_VERSION_CODE = 1;
+    private static final String CARRIER_CONFIG_EXAMPLE_KEY =
+            CarrierConfigManager.KEY_CARRIER_USSD_METHOD_INT;
+    private static final int CARRIER_CONFIG_EXAMPLE_VALUE =
+            CarrierConfigManager.USSD_OVER_CS_PREFERRED;
+
+    @Mock Resources mResources;
+    @Mock PackageManager mPackageManager;
+    @Mock PackageInfo mPackageInfo;
+    @Mock SubscriptionInfoUpdater mSubscriptionInfoUpdater;
+    @Mock SharedPreferences mSharedPreferences;
+
+    private TelephonyManager mTelephonyManager;
+    private CarrierConfigLoader mCarrierConfigLoader;
+    private Handler mHandler;
+    private HandlerThread mHandlerThread;
+    private TestableLooper mTestableLooper;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+
+        doReturn(mSharedPreferences).when(mContext).getSharedPreferences(anyString(), anyInt());
+        doReturn(Build.FINGERPRINT).when(mSharedPreferences).getString(eq("build_fingerprint"),
+                any());
+        doReturn(mPackageManager).when(mContext).getPackageManager();
+        doReturn(mResources).when(mContext).getResources();
+        doReturn(InstrumentationRegistry.getTargetContext().getFilesDir()).when(
+                mContext).getFilesDir();
+        doReturn(PLATFORM_CARRIER_CONFIG_PACKAGE).when(mResources).getString(
+                eq(R.string.platform_carrier_config_package));
+        mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
+        doReturn(1).when(mTelephonyManager).getSupportedModemCount();
+        doReturn(1).when(mTelephonyManager).getActiveModemCount();
+        doReturn("spn").when(mTelephonyManager).getSimOperatorNameForPhone(anyInt());
+        doReturn("310260").when(mTelephonyManager).getSimOperatorNumericForPhone(anyInt());
+        doReturn(mPackageInfo).when(mPackageManager).getPackageInfo(
+                eq(PLATFORM_CARRIER_CONFIG_PACKAGE), eq(0) /*flags*/);
+        doReturn(PLATFORM_CARRIER_CONFIG_PACKAGE_VERSION_CODE).when(
+                mPackageInfo).getLongVersionCode();
+
+        mHandlerThread = new HandlerThread("CarrierConfigLoaderTest");
+        mHandlerThread.start();
+
+        mTestableLooper = new TestableLooper(mHandlerThread.getLooper());
+        mCarrierConfigLoader = new CarrierConfigLoader(mContext, mSubscriptionInfoUpdater,
+                mTestableLooper.getLooper());
+        mHandler = mCarrierConfigLoader.getHandler();
+
+        // Clear all configs to have the same starting point.
+        mCarrierConfigLoader.clearConfigForPhone(DEFAULT_PHONE_ID, false);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mContext.revokeAllPermissions();
+        mTestableLooper.destroy();
+        super.tearDown();
+    }
+
+    /**
+     * Verifies that SecurityException should throw when call #updateConfigForPhoneId() without
+     * MODIFY_PHONE_STATE permission.
+     */
+    @Test
+    public void testUpdateConfigForPhoneId_noPermission() throws Exception {
+        assertThrows(SecurityException.class,
+                () -> mCarrierConfigLoader.updateConfigForPhoneId(DEFAULT_PHONE_ID,
+                        IccCardConstants.INTENT_VALUE_ICC_ABSENT));
+    }
+
+    /**
+     * Verifies that when call #updateConfigForPhoneId() with SIM absence, both carrier config from
+     * default app and carrier should be cleared but no-sim config should be loaded.
+     */
+    @Test
+    public void testUpdateConfigForPhoneId_simAbsent() throws Exception {
+        // Bypass case if default subId is not supported by device to reduce flakiness
+        if (!SubscriptionManager.isValidPhoneId(SubscriptionManager.getPhoneId(DEFAULT_SUB_ID))) {
+            return;
+        }
+        mContext.grantPermission(STUB_PERMISSION_ENABLE_ALL);
+        doNothing().when(mContext).sendBroadcastAsUser(any(Intent.class), any(UserHandle.class));
+
+        // Prepare a cached config to fetch from xml
+        PersistableBundle config = getTestConfig();
+        mCarrierConfigLoader.saveNoSimConfigToXml(PLATFORM_CARRIER_CONFIG_PACKAGE, config);
+        mCarrierConfigLoader.updateConfigForPhoneId(DEFAULT_PHONE_ID,
+                IccCardConstants.INTENT_VALUE_ICC_ABSENT);
+        mTestableLooper.processAllMessages();
+
+        assertThat(mCarrierConfigLoader.getConfigFromDefaultApp(DEFAULT_PHONE_ID)).isNull();
+        assertThat(mCarrierConfigLoader.getConfigFromCarrierApp(DEFAULT_PHONE_ID)).isNull();
+        assertThat(mCarrierConfigLoader.getNoSimConfig().getInt(CARRIER_CONFIG_EXAMPLE_KEY))
+                .isEqualTo(CARRIER_CONFIG_EXAMPLE_VALUE);
+        verify(mContext).sendBroadcastAsUser(any(Intent.class), any(UserHandle.class));
+    }
+
+    /**
+     * Verifies that with cached config in XML, calling #updateConfigForPhoneId() with SIM loaded
+     * will return the right config in the XML.
+     */
+    @Test
+    public void testUpdateConfigForPhoneId_simLoaded_withCachedConfigInXml() throws Exception {
+        // Bypass case if default subId is not supported by device to reduce flakiness
+        if (!SubscriptionManager.isValidPhoneId(SubscriptionManager.getPhoneId(DEFAULT_SUB_ID))) {
+            return;
+        }
+        mContext.grantPermission(STUB_PERMISSION_ENABLE_ALL);
+
+        // Prepare to make sure we can save the config into the XML file which used as cache
+        List<String> carrierPackages = List.of(PLATFORM_CARRIER_CONFIG_PACKAGE);
+        doReturn(carrierPackages).when(mTelephonyManager).getCarrierPackageNamesForIntentAndPhone(
+                nullable(Intent.class), anyInt());
+
+        // Save the sample config into the XML file
+        PersistableBundle config = getTestConfig();
+        CarrierIdentifier carrierId = mCarrierConfigLoader.getCarrierIdentifierForPhoneId(
+                DEFAULT_PHONE_ID);
+        mCarrierConfigLoader.saveConfigToXml(PLATFORM_CARRIER_CONFIG_PACKAGE, "",
+                DEFAULT_PHONE_ID, carrierId, config);
+        mCarrierConfigLoader.updateConfigForPhoneId(DEFAULT_PHONE_ID,
+                IccCardConstants.INTENT_VALUE_ICC_LOADED);
+        mTestableLooper.processAllMessages();
+
+        assertThat(mCarrierConfigLoader.getConfigFromDefaultApp(DEFAULT_PHONE_ID).getInt(
+                CARRIER_CONFIG_EXAMPLE_KEY)).isEqualTo(CARRIER_CONFIG_EXAMPLE_VALUE);
+
+    }
+
+    /**
+     * Verifies that SecurityException should throw if call #overrideConfig() without
+     * MODIFY_PHONE_STATE permission.
+     */
+    @Test
+    public void testOverrideConfig_noPermission() throws Exception {
+        assertThrows(SecurityException.class,
+                () -> mCarrierConfigLoader.overrideConfig(DEFAULT_SUB_ID, PersistableBundle.EMPTY,
+                        false));
+    }
+
+    /**
+     * Verifies that override config is not null when calling #overrideConfig with null bundle.
+     */
+    @Test
+    public void testOverrideConfig_withNullBundle() throws Exception {
+        // Bypass case if default subId is not supported by device to reduce flakiness
+        if (!SubscriptionManager.isValidPhoneId(SubscriptionManager.getPhoneId(DEFAULT_SUB_ID))) {
+            return;
+        }
+        mContext.grantPermission(STUB_PERMISSION_ENABLE_ALL);
+
+        mCarrierConfigLoader.overrideConfig(DEFAULT_SUB_ID, null /*overrides*/,
+                false/*persistent*/);
+        mTestableLooper.processAllMessages();
+
+        assertThat(mCarrierConfigLoader.getOverrideConfig(DEFAULT_PHONE_ID)).isEqualTo(
+                PersistableBundle.EMPTY);
+        verify(mSubscriptionInfoUpdater).updateSubscriptionByCarrierConfigAndNotifyComplete(
+                eq(DEFAULT_PHONE_ID), eq(PLATFORM_CARRIER_CONFIG_PACKAGE),
+                any(PersistableBundle.class), any(Message.class));
+    }
+
+    /**
+     * Verifies that override config is not null when calling #overrideConfig with non-null bundle.
+     */
+    @Test
+    public void testOverrideConfig_withNonNullBundle() throws Exception {
+        // Bypass case if default subId is not supported by device to reduce flakiness
+        if (!SubscriptionManager.isValidPhoneId(SubscriptionManager.getPhoneId(DEFAULT_SUB_ID))) {
+            return;
+        }
+        mContext.grantPermission(STUB_PERMISSION_ENABLE_ALL);
+
+        PersistableBundle config = getTestConfig();
+        mCarrierConfigLoader.overrideConfig(DEFAULT_SUB_ID, config /*overrides*/,
+                false/*persistent*/);
+        mTestableLooper.processAllMessages();
+
+        assertThat(mCarrierConfigLoader.getOverrideConfig(DEFAULT_PHONE_ID).getInt(
+                CARRIER_CONFIG_EXAMPLE_KEY)).isEqualTo(CARRIER_CONFIG_EXAMPLE_VALUE);
+        verify(mSubscriptionInfoUpdater).updateSubscriptionByCarrierConfigAndNotifyComplete(
+                eq(DEFAULT_PHONE_ID), eq(PLATFORM_CARRIER_CONFIG_PACKAGE),
+                any(PersistableBundle.class), any(Message.class));
+    }
+
+    /**
+     * Verifies that calling #notifyConfigChangedForSubId() with invalid subId will be ignored.
+     */
+    @Test
+    public void testNotifyConfigChangedForSubId_invalidSubId() throws Exception {
+        mContext.grantPermission(STUB_PERMISSION_ENABLE_ALL);
+
+        mCarrierConfigLoader.notifyConfigChangedForSubId(
+                SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+
+        // verifying the behavior following the permission check is not actually performed.
+        // It against "verify value instead of behavior", but we don't have other alternatives.
+        verify(mPackageManager, never()).getNameForUid(anyInt());
+        verify(mContext, never()).checkCallingOrSelfPermission(
+                eq(android.Manifest.permission.MODIFY_PHONE_STATE));
+    }
+
+    // TODO(b/184040111): Enable test case when support disabling carrier privilege
+    // Phone/System UID always has carrier privilege (TelephonyPermission#getCarrierPrivilegeStatus)
+    // when running the test here.
+    /**
+     * Verifies that SecurityException should throw when calling notifyConfigChangedForSubId without
+     * MODIFY_PHONE_STATE permission.
+     */
+    @Ignore
+    public void testNotifyConfigChangedForSubId_noPermission() throws Exception {
+        setCarrierPrivilegesForSubId(false, DEFAULT_SUB_ID);
+
+        assertThrows(SecurityException.class,
+                () -> mCarrierConfigLoader.notifyConfigChangedForSubId(DEFAULT_SUB_ID));
+    }
+
+    /**
+     * Verifies that SecurityException should throw when calling getDefaultCarrierServicePackageName
+     * without READ_PRIVILEGED_PHONE_STATE permission.
+     */
+    @Test
+    public void testGetDefaultCarrierServicePackageName_noPermission() {
+        assertThrows(SecurityException.class,
+                () -> mCarrierConfigLoader.getDefaultCarrierServicePackageName());
+    }
+
+    /**
+     * Verifies that the right default carrier service package name is return when calling
+     * getDefaultCarrierServicePackageName with permission.
+     */
+    @Test
+    public void testGetDefaultCarrierServicePackageName_withPermission() {
+        mContext.grantPermission(STUB_PERMISSION_ENABLE_ALL);
+
+        assertThat(mCarrierConfigLoader.getDefaultCarrierServicePackageName())
+                .isEqualTo(PLATFORM_CARRIER_CONFIG_PACKAGE);
+    }
+
+    // TODO(b/184040111): Enable test case when support disabling carrier privilege
+    // Phone/System UID always has carrier privilege (TelephonyPermission#getCarrierPrivilegeStatus)
+    // when running the test here.
+    /**
+     * Verifies that without permission, #getConfigForSubId will return an empty PersistableBundle.
+     */
+    @Ignore
+    public void testGetConfigForSubId_noPermission() {
+        // Bypass case if default subId is not supported by device to reduce flakiness
+        if (!SubscriptionManager.isValidPhoneId(SubscriptionManager.getPhoneId(DEFAULT_SUB_ID))) {
+            return;
+        }
+        setCarrierPrivilegesForSubId(false, DEFAULT_SUB_ID);
+
+        assertThat(mCarrierConfigLoader.getConfigForSubId(DEFAULT_SUB_ID,
+                PLATFORM_CARRIER_CONFIG_PACKAGE)).isEqualTo(PersistableBundle.EMPTY);
+    }
+
+    /**
+     * Verifies that when have no DUMP permission, the #dump() method shows permission denial.
+     */
+    @Test
+    public void testDump_noPermission() {
+        StringWriter stringWriter = new StringWriter();
+        mCarrierConfigLoader.dump(new FileDescriptor(), new PrintWriter(stringWriter),
+                new String[0]);
+        stringWriter.flush();
+
+        assertThat(stringWriter.toString()).contains("Permission Denial:");
+    }
+
+    /**
+     * Verifies that when have DUMP permission, the #dump() method can dump the CarrierConfigLoader.
+     */
+    @Test
+    public void testDump_withPermission() {
+        mContext.grantPermission(android.Manifest.permission.DUMP);
+
+        StringWriter stringWriter = new StringWriter();
+        mCarrierConfigLoader.dump(new FileDescriptor(), new PrintWriter(stringWriter),
+                new String[0]);
+        stringWriter.flush();
+
+        String dumpContent = stringWriter.toString();
+        assertThat(dumpContent).contains("CarrierConfigLoader:");
+        assertThat(dumpContent).doesNotContain("Permission Denial:");
+    }
+
+    private static PersistableBundle getTestConfig() {
+        PersistableBundle config = new PersistableBundle();
+        config.putInt(CARRIER_CONFIG_EXAMPLE_KEY, CARRIER_CONFIG_EXAMPLE_VALUE);
+        return config;
+    }
+
+    private void setCarrierPrivilegesForSubId(boolean hasCarrierPrivileges, int subId) {
+        TelephonyManager mockTelephonyManager = Mockito.mock(TelephonyManager.class);
+        doReturn(mockTelephonyManager).when(mTelephonyManager).createForSubscriptionId(subId);
+        doReturn(hasCarrierPrivileges ? TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS
+                : TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS).when(
+                mockTelephonyManager).getCarrierPrivilegeStatus(anyInt());
+    }
+}