Use location mcc to load the resource

Provide a way for OEM to provide their customization when
device doesn't have a sim card inserted.

Bug: 141406258
Test: 1. Build pass
      2. atest NetworkStackTests

Change-Id: Iad8340e643580f8efa536884d22f0ea0a97cd9a5
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 9628b3f..b1603d8 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -30,6 +30,7 @@
          permissions added would cause crashes on startup unless they are also added to the
          privileged permissions whitelist for that package. -->
     <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
     <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
     <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
diff --git a/res/values/config.xml b/res/values/config.xml
index 478ed6b..13ab04e 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -43,4 +43,7 @@
     <!-- Customized default DNS Servers address. -->
     <string-array name="config_default_dns_servers" translatable="false">
     </string-array>
+    <!-- Set to true if NetworkMonitor needs to load the resource by neighbor mcc when device
+         doesn't have a SIM card inserted. -->
+    <bool name="config_no_sim_card_uses_neighbor_mcc">false</bool>
 </resources>
diff --git a/res/values/overlayable.xml b/res/values/overlayable.xml
index b9d5337..4727215 100644
--- a/res/values/overlayable.xml
+++ b/res/values/overlayable.xml
@@ -21,6 +21,7 @@
             <item type="string" name="config_captive_portal_http_url"/>
             <item type="string" name="config_captive_portal_https_url"/>
             <item type="array" name="config_captive_portal_fallback_urls"/>
+            <item type="bool" name="config_no_sim_card_uses_neighbor_mcc"/>
             <!-- Configuration value for DhcpResults -->
             <item type="array" name="config_default_dns_servers"/>
         </policy>
diff --git a/src/com/android/server/connectivity/NetworkMonitor.java b/src/com/android/server/connectivity/NetworkMonitor.java
index 3dfe004..2c9ffd6 100644
--- a/src/com/android/server/connectivity/NetworkMonitor.java
+++ b/src/com/android/server/connectivity/NetworkMonitor.java
@@ -76,6 +76,8 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.net.ConnectivityManager;
 import android.net.DnsResolver;
@@ -101,11 +103,19 @@
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Message;
+import android.os.Process;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.telephony.AccessNetworkConstants;
+import android.telephony.CellIdentityNr;
+import android.telephony.CellInfo;
+import android.telephony.CellInfoGsm;
+import android.telephony.CellInfoLte;
+import android.telephony.CellInfoNr;
+import android.telephony.CellInfoTdscdma;
+import android.telephony.CellInfoWcdma;
 import android.telephony.CellSignalStrength;
 import android.telephony.NetworkRegistrationInfo;
 import android.telephony.ServiceState;
@@ -116,6 +126,7 @@
 import android.util.Pair;
 
 import androidx.annotation.ArrayRes;
+import androidx.annotation.BoolRes;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.StringRes;
@@ -141,8 +152,10 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Random;
 import java.util.UUID;
 import java.util.concurrent.CountDownLatch;
@@ -1315,8 +1328,78 @@
                 CAPTIVE_PORTAL_USE_HTTPS, 1) == 1;
     }
 
+    @Nullable
+    private String getMccFromCellInfo(final CellInfo cell) {
+        if (cell instanceof CellInfoGsm) {
+            return ((CellInfoGsm) cell).getCellIdentity().getMccString();
+        } else if (cell instanceof CellInfoLte) {
+            return ((CellInfoLte) cell).getCellIdentity().getMccString();
+        } else if (cell instanceof CellInfoWcdma) {
+            return ((CellInfoWcdma) cell).getCellIdentity().getMccString();
+        } else if (cell instanceof CellInfoTdscdma) {
+            return ((CellInfoTdscdma) cell).getCellIdentity().getMccString();
+        } else if (cell instanceof CellInfoNr) {
+            return ((CellIdentityNr) ((CellInfoNr) cell).getCellIdentity()).getMccString();
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Return location mcc.
+     */
+    @VisibleForTesting
+    @Nullable
+    protected String getLocationMcc() {
+        // Adding this check is because the new permission won't be granted by mainline update,
+        // the new permission only be granted by OTA for current design. Tracking: b/145774617.
+        if (mContext.checkPermission(android.Manifest.permission.ACCESS_FINE_LOCATION,
+                Process.myPid(), Process.myUid())
+                == PackageManager.PERMISSION_DENIED) {
+            log("getLocationMcc : NetworkStack does not hold ACCESS_FINE_LOCATION");
+            return null;
+        }
+        try {
+            final List<CellInfo> cells = mTelephonyManager.getAllCellInfo();
+            final Map<String, Integer> countryCodeMap = new HashMap<>();
+            int maxCount = 0;
+            for (final CellInfo cell : cells) {
+                final String mcc = getMccFromCellInfo(cell);
+                if (mcc != null) {
+                    final int count = countryCodeMap.getOrDefault(mcc, 0) + 1;
+                    countryCodeMap.put(mcc, count);
+                }
+            }
+            // Return the MCC which occurs most.
+            if (countryCodeMap.size() <= 0) return null;
+            return Collections.max(countryCodeMap.entrySet(),
+                    (e1, e2) -> e1.getValue().compareTo(e2.getValue())).getKey();
+        } catch (SecurityException e) {
+            log("Permission is not granted:" + e);
+            return null;
+        }
+    }
+
+    @VisibleForTesting
+    protected Context getContextByMccIfNoSimCardOrDefault() {
+        final boolean useNeighborResource =
+                getResBooleanConfig(mContext, R.bool.config_no_sim_card_uses_neighbor_mcc);
+        if (!useNeighborResource
+                || TelephonyManager.SIM_STATE_READY == mTelephonyManager.getSimState()) {
+            return mContext;
+        }
+        final String mcc = getLocationMcc();
+        if (TextUtils.isEmpty(mcc)) {
+            return mContext;
+        }
+        final Configuration config = mContext.getResources().getConfiguration();
+        config.mcc = Integer.parseInt(mcc);
+        return mContext.createConfigurationContext(config);
+    }
+
     private String getCaptivePortalServerHttpsUrl() {
-        return getSettingFromResource(mContext, R.string.config_captive_portal_https_url,
+        final Context targetContext = getContextByMccIfNoSimCardOrDefault();
+        return getSettingFromResource(targetContext, R.string.config_captive_portal_https_url,
                 R.string.default_captive_portal_https_url, CAPTIVE_PORTAL_HTTPS_URL);
     }
 
@@ -1347,6 +1430,17 @@
         }
     }
 
+    @VisibleForTesting
+    protected boolean getResBooleanConfig(@NonNull final Context context,
+            @BoolRes int configResource) {
+        final Resources res = context.getResources();
+        try {
+            return res.getBoolean(configResource);
+        } catch (Resources.NotFoundException e) {
+            return false;
+        }
+    }
+
     /**
      * Get the captive portal server HTTP URL that is configured on the device.
      *
@@ -1355,7 +1449,8 @@
      * on one URL that can be used, while NetworkMonitor may implement more complex logic.
      */
     public String getCaptivePortalServerHttpUrl() {
-        return getSettingFromResource(mContext, R.string.config_captive_portal_http_url,
+        final Context targetContext = getContextByMccIfNoSimCardOrDefault();
+        return getSettingFromResource(targetContext, R.string.config_captive_portal_http_url,
                 R.string.default_captive_portal_http_url, CAPTIVE_PORTAL_HTTP_URL);
     }
 
diff --git a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
index 75491ec..b72c579 100644
--- a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
+++ b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
@@ -64,7 +64,10 @@
 import android.annotation.NonNull;
 import android.content.BroadcastReceiver;
 import android.content.Context;
+import android.content.ContextWrapper;
 import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.net.ConnectivityManager;
 import android.net.DnsResolver;
@@ -87,6 +90,11 @@
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.provider.Settings;
+import android.telephony.CellIdentityGsm;
+import android.telephony.CellIdentityLte;
+import android.telephony.CellInfo;
+import android.telephony.CellInfoGsm;
+import android.telephony.CellInfoLte;
 import android.telephony.CellSignalStrength;
 import android.telephony.TelephonyManager;
 import android.util.ArrayMap;
@@ -133,6 +141,7 @@
     private static final String LOCATION_HEADER = "location";
 
     private @Mock Context mContext;
+    private @Mock Configuration mConfiguration;
     private @Mock Resources mResources;
     private @Mock IpConnectivityLog mLogger;
     private @Mock SharedLog mValidationLogger;
@@ -484,6 +493,10 @@
             assertTrue("NetworkMonitor did not quit after " + HANDLER_TIMEOUT_MS + "ms",
                     mQuitCv.block(HANDLER_TIMEOUT_MS));
         }
+
+        protected Context getContext() {
+            return mContext;
+        }
     }
 
     private WrappedNetworkMonitor makeMonitor(NetworkCapabilities nc) {
@@ -513,6 +526,45 @@
     }
 
     @Test
+    public void testGetLocationMcc() throws Exception {
+        final WrappedNetworkMonitor wnm = makeNotMeteredNetworkMonitor();
+        doReturn(PackageManager.PERMISSION_DENIED).when(mContext).checkPermission(
+                eq(android.Manifest.permission.ACCESS_FINE_LOCATION),  anyInt(), anyInt());
+        assertNull(wnm.getLocationMcc());
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mContext).checkPermission(
+                eq(android.Manifest.permission.ACCESS_FINE_LOCATION),  anyInt(), anyInt());
+        doReturn(new ContextWrapper(mContext)).when(mContext).createConfigurationContext(any());
+        // Prepare CellInfo and check if the vote mechanism is working or not.
+        final CellInfoGsm cellInfoGsm1 = new CellInfoGsm();
+        final CellInfoGsm cellInfoGsm2 = new CellInfoGsm();
+        final CellInfoLte cellInfoLte = new CellInfoLte();
+        final CellIdentityGsm cellIdentityGsm =
+                new CellIdentityGsm(0, 0, 0, 0, "460", "01", "", "");
+        final CellIdentityLte cellIdentityLte =
+                new CellIdentityLte(0, 0, 0, 0, 0, "466", "01", "", "");
+        cellInfoGsm1.setCellIdentity(cellIdentityGsm);
+        cellInfoGsm2.setCellIdentity(cellIdentityGsm);
+        cellInfoLte.setCellIdentity(cellIdentityLte);
+        final List<CellInfo> cellList = new ArrayList<CellInfo>();
+        cellList.add(cellInfoGsm1);
+        cellList.add(cellInfoGsm2);
+        cellList.add(cellInfoLte);
+        doReturn(cellList).when(mTelephony).getAllCellInfo();
+        // The count of 460 is 2 and the count of 466 is 1, so the getLocationMcc() should return
+        // 460.
+        assertEquals("460", wnm.getLocationMcc());
+        // getContextByMccIfNoSimCardOrDefault() shouldn't return mContext when using neighbor mcc
+        // is enabled and the sim is not ready.
+        doReturn(true).when(mResources).getBoolean(R.bool.config_no_sim_card_uses_neighbor_mcc);
+        doReturn(TelephonyManager.SIM_STATE_ABSENT).when(mTelephony).getSimState();
+        doReturn(mConfiguration).when(mResources).getConfiguration();
+        assertEquals(460,
+                wnm.getContextByMccIfNoSimCardOrDefault().getResources().getConfiguration().mcc);
+        doReturn(false).when(mResources).getBoolean(R.bool.config_no_sim_card_uses_neighbor_mcc);
+        assertEquals(wnm.getContext(), wnm.getContextByMccIfNoSimCardOrDefault());
+    }
+
+    @Test
     public void testGetIntSetting() throws Exception {
         WrappedNetworkMonitor wnm = makeNotMeteredNetworkMonitor();