Add logic to SatelliteAccessController class

Bug: 313773568
Test: atest SatelliteAccessControllerTest
atest SatelliteManagerTestOnMockService

Change-Id: I5eb3651baed905187cb27040a2135199e9f5c24d
diff --git a/Android.bp b/Android.bp
index ffd2292..a943299 100644
--- a/Android.bp
+++ b/Android.bp
@@ -95,18 +95,6 @@
     ],
 }
 
-// Used by satellite unit tests temporarily during the development phase.
-// TODO: Remove this once the satellite code is wired into Telephony code.
-java_library {
-    name: "telephony-satellite",
-    srcs: ["src/com/android/phone/satellite/**/*.java"],
-    libs: [
-        "satellite-s2storage-ro",
-        "s2-geometry-library-java",
-        "telephony-common",
-    ],
-}
-
 platform_compat_config {
     name: "TeleService-platform-compat-config",
     src: ":TeleService",
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 7e56e8b..04e3706 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -71,6 +71,7 @@
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+    <uses-permission android:name="android.permission.LOCATION_BYPASS" />
     <uses-permission android:name="android.permission.WRITE_APN_SETTINGS" />
     <uses-permission android:name="android.permission.BROADCAST_SMS"/>
     <uses-permission android:name="android.permission.BROADCAST_WAP_PUSH"/>
@@ -137,6 +138,7 @@
     <uses-permission android:name="android.permission.BIND_TELEPHONY_DATA_SERVICE" />
     <uses-permission android:name="android.permission.BIND_SATELLITE_GATEWAY_SERVICE" />
     <uses-permission android:name="android.permission.BIND_SATELLITE_SERVICE" />
+    <uses-permission android:name="android.permission.SATELLITE_COMMUNICATION" />
     <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
     <uses-permission android:name="android.permission.READ_PRECISE_PHONE_STATE" />
     <uses-permission android:name="android.permission.MANAGE_ROLE_HOLDERS" />
diff --git a/assets/google_us_san_sat_s2.dat b/assets/google_us_san_sat_s2.dat
new file mode 100644
index 0000000..60b00df
--- /dev/null
+++ b/assets/google_us_san_sat_s2.dat
Binary files differ
diff --git a/src/com/android/phone/PhoneGlobals.java b/src/com/android/phone/PhoneGlobals.java
index 2f372ce..76cf979 100644
--- a/src/com/android/phone/PhoneGlobals.java
+++ b/src/com/android/phone/PhoneGlobals.java
@@ -994,6 +994,11 @@
         }
 
         ServiceState serviceState = phone.getServiceState();
+        if (serviceState == null) {
+            Log.e(LOG_TAG, "updateDataRoamingStatus: serviceState is null");
+            return;
+        }
+
         String roamingNumeric = serviceState.getOperatorNumeric();
         String roamingNumericReason = "RoamingNumeric=" + roamingNumeric;
         String callingReason = "CallingReason=" + reason;
diff --git a/src/com/android/phone/PhoneInterfaceManager.java b/src/com/android/phone/PhoneInterfaceManager.java
index ec85361..d8d8450 100644
--- a/src/com/android/phone/PhoneInterfaceManager.java
+++ b/src/com/android/phone/PhoneInterfaceManager.java
@@ -23,6 +23,9 @@
 import static android.telephony.TelephonyManager.ENABLE_FEATURE_MAPPING;
 import static android.telephony.TelephonyManager.HAL_SERVICE_NETWORK;
 import static android.telephony.TelephonyManager.HAL_SERVICE_RADIO;
+import static android.telephony.satellite.SatelliteManager.KEY_SATELLITE_COMMUNICATION_ALLOWED;
+import static android.telephony.satellite.SatelliteManager.SATELLITE_RESULT_ACCESS_BARRED;
+import static android.telephony.satellite.SatelliteManager.SATELLITE_RESULT_SUCCESS;
 
 import static com.android.internal.telephony.PhoneConstants.PHONE_TYPE_CDMA;
 import static com.android.internal.telephony.PhoneConstants.PHONE_TYPE_GSM;
@@ -210,6 +213,7 @@
 import com.android.internal.telephony.SmsApplication;
 import com.android.internal.telephony.SmsController;
 import com.android.internal.telephony.SmsPermissions;
+import com.android.internal.telephony.TelephonyCountryDetector;
 import com.android.internal.telephony.TelephonyIntents;
 import com.android.internal.telephony.TelephonyPermissions;
 import com.android.internal.telephony.data.DataUtils;
@@ -243,6 +247,7 @@
 import com.android.phone.callcomposer.CallComposerPictureManager;
 import com.android.phone.callcomposer.CallComposerPictureTransfer;
 import com.android.phone.callcomposer.ImageData;
+import com.android.phone.satellite.accesscontrol.SatelliteAccessController;
 import com.android.phone.settings.PickSmsSubscriptionActivity;
 import com.android.phone.slice.SlicePurchaseController;
 import com.android.phone.utils.CarrierAllowListInfo;
@@ -417,6 +422,7 @@
     private final ImsResolver mImsResolver;
 
     private final SatelliteController mSatelliteController;
+    private final SatelliteAccessController mSatelliteAccessController;
     private final UserManager mUserManager;
     private final AppOpsManager mAppOps;
     private final MainThreadHandler mMainThreadHandler;
@@ -2464,6 +2470,8 @@
         mRadioInterfaceCapabilities = RadioInterfaceCapabilityController.getInstance();
         mNotifyUserActivity = new AtomicBoolean(false);
         mPackageManager = app.getPackageManager();
+        mSatelliteAccessController = SatelliteAccessController.getOrCreateInstance(
+                getDefaultPhone().getContext(), featureFlags);
         PropertyInvalidatedCache.invalidateCache(TelephonyManager.CACHE_KEY_PHONE_ACCOUNT_TO_SUBID);
         publish();
         CarrierAllowListInfo.loadInstance(mApp);
@@ -12956,8 +12964,34 @@
     public void requestSatelliteEnabled(int subId, boolean enableSatellite, boolean enableDemoMode,
             @NonNull IIntegerConsumer callback) {
         enforceSatelliteCommunicationPermission("requestSatelliteEnabled");
-        mSatelliteController.requestSatelliteEnabled(subId, enableSatellite, enableDemoMode,
-                callback);
+        ResultReceiver resultReceiver = new ResultReceiver(mMainThreadHandler) {
+            @Override
+            protected void onReceiveResult(int resultCode, Bundle resultData) {
+                Log.d(LOG_TAG, "Satellite access restriction resultCode=" + resultCode
+                        + ", resultData=" + resultData);
+                boolean isAllowed = false;
+                Consumer<Integer> result = FunctionalUtils.ignoreRemoteException(callback::accept);
+                if (resultCode == SATELLITE_RESULT_SUCCESS) {
+                    if (resultData != null
+                            && resultData.containsKey(KEY_SATELLITE_COMMUNICATION_ALLOWED)) {
+                        isAllowed = resultData.getBoolean(KEY_SATELLITE_COMMUNICATION_ALLOWED);
+                    } else {
+                        loge("KEY_SATELLITE_COMMUNICATION_ALLOWED does not exist.");
+                    }
+                } else {
+                    result.accept(resultCode);
+                    return;
+                }
+                if (isAllowed) {
+                    mSatelliteController.requestSatelliteEnabled(
+                            subId, enableSatellite, enableDemoMode, callback);
+                } else {
+                    result.accept(SATELLITE_RESULT_ACCESS_BARRED);
+                }
+            }
+        };
+        mSatelliteAccessController.requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                subId, resultReceiver);
     }
 
     /**
@@ -13278,8 +13312,8 @@
             @NonNull ResultReceiver result) {
         enforceSatelliteCommunicationPermission(
                 "requestIsSatelliteCommunicationAllowedForCurrentLocation");
-        mSatelliteController.requestIsSatelliteCommunicationAllowedForCurrentLocation(subId,
-                result);
+        mSatelliteAccessController.requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                subId, result);
     }
 
     /**
@@ -13611,6 +13645,55 @@
     }
 
     /**
+     * This API should be used by only CTS tests to forcefully set telephony country codes.
+     *
+     * @return {@code true} if the country code is set successfully, {@code false} otherwise.
+     */
+    public boolean setCountryCodes(boolean reset, List<String> currentNetworkCountryCodes,
+            Map cachedNetworkCountryCodes, String locationCountryCode,
+            long locationCountryCodeTimestampNanos) {
+        Log.d(LOG_TAG, "setCountryCodes: currentNetworkCountryCodes="
+                + String.join(", ", currentNetworkCountryCodes)
+                + ", locationCountryCode=" + locationCountryCode
+                + ", locationCountryCodeTimestampNanos" + locationCountryCodeTimestampNanos
+                + ", reset=" + reset + ", cachedNetworkCountryCodes="
+                + String.join(", ", cachedNetworkCountryCodes.keySet()));
+        TelephonyPermissions.enforceShellOnly(
+                Binder.getCallingUid(), "setCachedLocationCountryCode");
+        TelephonyPermissions.enforceCallingOrSelfModifyPermissionOrCarrierPrivilege(mApp,
+                SubscriptionManager.INVALID_SUBSCRIPTION_ID,
+                "setCachedLocationCountryCode");
+        return TelephonyCountryDetector.getInstance(getDefaultPhone().getContext()).setCountryCodes(
+                reset, currentNetworkCountryCodes, cachedNetworkCountryCodes, locationCountryCode,
+                locationCountryCodeTimestampNanos);
+    }
+
+    /**
+     * This API should be used by only CTS tests to override the overlay configs of satellite
+     * access controller.
+     *
+     * @param reset {@code true} mean the overridden configs should not be used, {@code false}
+     *              otherwise.
+     * @return {@code true} if the overlay configs are set successfully, {@code false} otherwise.
+     */
+    public boolean setSatelliteAccessControlOverlayConfigs(boolean reset, boolean isAllowed,
+            String s2CellFile, long locationFreshDurationNanos,
+            List<String> satelliteCountryCodes) {
+        Log.d(LOG_TAG, "setSatelliteAccessControlOverlayConfigs: reset=" + reset
+                + ", isAllowed" + isAllowed + ", s2CellFile=" + s2CellFile
+                + ", locationFreshDurationNanos=" + locationFreshDurationNanos
+                + ", satelliteCountryCodes=" + ((satelliteCountryCodes != null)
+                ? String.join(", ", satelliteCountryCodes) : null));
+        TelephonyPermissions.enforceShellOnly(
+                Binder.getCallingUid(), "setSatelliteAccessControlOverlayConfigs");
+        TelephonyPermissions.enforceCallingOrSelfModifyPermissionOrCarrierPrivilege(mApp,
+                SubscriptionManager.INVALID_SUBSCRIPTION_ID,
+                "setSatelliteAccessControlOverlayConfigs");
+        return mSatelliteAccessController.setSatelliteAccessControlOverlayConfigs(reset, isAllowed,
+                s2CellFile, locationFreshDurationNanos, satelliteCountryCodes);
+    }
+
+    /**
      * This API can be used by only CTS to override the cached value for the device overlay config
      * value : config_send_satellite_datagram_to_modem_in_demo_mode, which determines whether
      * outgoing satellite datagrams should be sent to modem in demo mode.
diff --git a/src/com/android/phone/TelephonyShellCommand.java b/src/com/android/phone/TelephonyShellCommand.java
index 5986a7c..80b7cf6 100644
--- a/src/com/android/phone/TelephonyShellCommand.java
+++ b/src/com/android/phone/TelephonyShellCommand.java
@@ -24,6 +24,8 @@
 import static java.util.Map.entry;
 
 import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.Context;
 import android.net.Uri;
 import android.os.Binder;
@@ -65,6 +67,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -191,6 +194,9 @@
             "set-satellite-device-aligned-timeout-duration";
     private static final String SET_EMERGENCY_CALL_TO_SATELLITE_HANDOVER_TYPE =
             "set-emergency-call-to-satellite-handover-type";
+    private static final String SET_COUNTRY_CODES = "set-country-codes";
+    private static final String SET_SATELLITE_ACCESS_CONTROL_OVERLAY_CONFIGS =
+            "set-satellite-access-control-overlay-configs";
     private static final String SET_SHOULD_SEND_DATAGRAM_TO_MODEM_IN_DEMO_MODE =
             "set-should-send-datagram-to-modem-in-demo-mode";
 
@@ -388,6 +394,10 @@
                 return handleSetEmergencyCallToSatelliteHandoverType();
             case SET_SHOULD_SEND_DATAGRAM_TO_MODEM_IN_DEMO_MODE:
                 return handleSetShouldSendDatagramToModemInDemoMode();
+            case SET_SATELLITE_ACCESS_CONTROL_OVERLAY_CONFIGS:
+                return handleSetSatelliteAccessControlOverlayConfigs();
+            case SET_COUNTRY_CODES:
+                return handleSetCountryCodes();
             default: {
                 return handleDefaultCommands(cmd);
             }
@@ -795,6 +805,25 @@
         pw.println("          If no option is specified, override is disabled.");
         pw.println("      -d: the delay in seconds in sending EVENT_DISPLAY_EMERGENCY_MESSAGE.");
         pw.println("          If no option is specified, there is no delay in sending the event.");
+        pw.println("  set-satellite-access-control-overlay-configs [-r -a -f SATELLITE_S2_FILE ");
+        pw.println("    -d LOCATION_FRESH_DURATION_NANOS -c COUNTRY_CODES] Override the overlay");
+        pw.println("    configs of satellite access controller.");
+        pw.println("    Options are:");
+        pw.println("      -r: clear the overriding. Absent means enable overriding.");
+        pw.println("      -a: the country codes is an allowed list. Absent means disallowed.");
+        pw.println("      -f: the satellite s2 file.");
+        pw.println("      -d: the location fresh duration nanos.");
+        pw.println("      -c: the list of satellite country codes separated by comma.");
+        pw.println("  set-country-codes [-r -n CURRENT_NETWORK_COUNTRY_CODES -c");
+        pw.println("    CACHED_NETWORK_COUNTRY_CODES -l LOCATION_COUNTRY_CODE -t");
+        pw.println("    LOCATION_COUNTRY_CODE_TIMESTAMP] ");
+        pw.println("    Override the cached location country code and its update timestamp. ");
+        pw.println("    Options are:");
+        pw.println("      -r: clear the overriding. Absent means enable overriding.");
+        pw.println("      -n: the current network country code ISOs.");
+        pw.println("      -c: the cached network country code ISOs.");
+        pw.println("      -l: the location country code ISO.");
+        pw.println("      -t: the update timestamp nanos of the location country code.");
     }
 
     private void onHelpImei() {
@@ -3394,6 +3423,149 @@
         return 0;
     }
 
+    private int handleSetSatelliteAccessControlOverlayConfigs() {
+        PrintWriter errPw = getErrPrintWriter();
+        boolean reset = false;
+        boolean isAllowed = false;
+        String s2CellFile = null;
+        long locationFreshDurationNanos = 0;
+        List<String> satelliteCountryCodes = null;
+
+        String opt;
+        while ((opt = getNextOption()) != null) {
+            switch (opt) {
+                case "-r": {
+                    reset = true;
+                    break;
+                }
+                case "-a": {
+                    isAllowed = true;
+                    break;
+                }
+                case "-f": {
+                    s2CellFile = getNextArgRequired();
+                    break;
+                }
+                case "-d": {
+                    locationFreshDurationNanos = Long.parseLong(getNextArgRequired());
+                    break;
+                }
+                case "-c": {
+                    String countryCodeStr = getNextArgRequired();
+                    satelliteCountryCodes = Arrays.asList(countryCodeStr.split(","));
+                    break;
+                }
+            }
+        }
+        Log.d(LOG_TAG, "handleSetSatelliteAccessControlOverlayConfigs: reset=" + reset
+                + ", isAllowed=" + isAllowed + ", s2CellFile=" + s2CellFile
+                + ", locationFreshDurationNanos=" + locationFreshDurationNanos
+                + ", satelliteCountryCodes=" + satelliteCountryCodes);
+
+        try {
+            boolean result = mInterface.setSatelliteAccessControlOverlayConfigs(reset, isAllowed,
+                    s2CellFile, locationFreshDurationNanos, satelliteCountryCodes);
+            if (VDBG) {
+                Log.v(LOG_TAG, "setSatelliteAccessControlOverlayConfigs result =" + result);
+            }
+            getOutPrintWriter().println(result);
+        } catch (RemoteException e) {
+            Log.e(LOG_TAG, "setSatelliteAccessControlOverlayConfigs: ex=" + e.getMessage());
+            errPw.println("Exception: " + e.getMessage());
+            return -1;
+        }
+        return 0;
+    }
+
+    private int handleSetCountryCodes() {
+        PrintWriter errPw = getErrPrintWriter();
+        List<String> currentNetworkCountryCodes = new ArrayList<>();
+        String locationCountryCode = null;
+        long locationCountryCodeTimestampNanos = 0;
+        Map<String, Long> cachedNetworkCountryCodes = new HashMap<>();
+        boolean reset = false;
+
+        String opt;
+        while ((opt = getNextOption()) != null) {
+            switch (opt) {
+                case "-r": {
+                    reset = true;
+                    break;
+                }
+                case "-n": {
+                    String countryCodeStr = getNextArgRequired();
+                    currentNetworkCountryCodes = Arrays.asList(countryCodeStr.split(","));
+                    break;
+                }
+                case "-c": {
+                    String cachedNetworkCountryCodeStr = getNextArgRequired();
+                    cachedNetworkCountryCodes = parseStringLongMap(cachedNetworkCountryCodeStr);
+                    break;
+                }
+                case "-l": {
+                    locationCountryCode = getNextArgRequired();
+                    break;
+                }
+                case "-t": {
+                    locationCountryCodeTimestampNanos = Long.parseLong(getNextArgRequired());
+                    break;
+                }
+            }
+        }
+        Log.d(LOG_TAG, "setCountryCodes: locationCountryCode="
+                + locationCountryCode + ", locationCountryCodeTimestampNanos="
+                + locationCountryCodeTimestampNanos + ", currentNetworkCountryCodes="
+                + currentNetworkCountryCodes);
+
+        try {
+            boolean result = mInterface.setCountryCodes(reset, currentNetworkCountryCodes,
+                    cachedNetworkCountryCodes, locationCountryCode,
+                    locationCountryCodeTimestampNanos);
+            if (VDBG) {
+                Log.v(LOG_TAG, "setCountryCodes result =" + result);
+            }
+            getOutPrintWriter().println(result);
+        } catch (RemoteException e) {
+            Log.e(LOG_TAG, "setCountryCodes: ex=" + e.getMessage());
+            errPw.println("Exception: " + e.getMessage());
+            return -1;
+        }
+        return 0;
+    }
+
+    /**
+     * Sample inputStr = "US,UK,CA;2,1,3"
+     * Sample output: {[US,2], [UK,1], [CA,3]}
+     */
+    @NonNull private Map<String, Long> parseStringLongMap(@Nullable String inputStr) {
+        Map<String, Long> result = new HashMap<>();
+        if (!TextUtils.isEmpty(inputStr)) {
+            String[] stringLongArr = inputStr.split(";");
+            if (stringLongArr.length != 2) {
+                Log.e(LOG_TAG, "parseStringLongMap: invalid inputStr=" + inputStr);
+                return result;
+            }
+
+            String[] stringArr = stringLongArr[0].split(",");
+            String[] longArr = stringLongArr[1].split(",");
+            if (stringArr.length != longArr.length) {
+                Log.e(LOG_TAG, "parseStringLongMap: invalid inputStr=" + inputStr);
+                return result;
+            }
+
+            for (int i = 0; i < stringArr.length; i++) {
+                try {
+                    result.put(stringArr[i], Long.parseLong(longArr[i]));
+                } catch (Exception ex) {
+                    Log.e(LOG_TAG, "parseStringLongMap: invalid inputStr=" + inputStr
+                            + ", ex=" + ex);
+                    return result;
+                }
+            }
+        }
+        return result;
+    }
+
     private int handleCarrierRestrictionStatusCommand() {
         try {
             String MOCK_MODEM_SERVICE_NAME = "android.telephony.mockmodem.MockModemService";
diff --git a/src/com/android/phone/satellite/accesscontrol/S2RangeSatelliteOnDeviceAccessController.java b/src/com/android/phone/satellite/accesscontrol/S2RangeSatelliteOnDeviceAccessController.java
index 62fbd18..4490460 100644
--- a/src/com/android/phone/satellite/accesscontrol/S2RangeSatelliteOnDeviceAccessController.java
+++ b/src/com/android/phone/satellite/accesscontrol/S2RangeSatelliteOnDeviceAccessController.java
@@ -63,9 +63,9 @@
         return new S2RangeSatelliteOnDeviceAccessController(reader, s2Level);
     }
 
-    @Override
-    public LocationToken createLocationTokenForLatLng(double latDegrees, double lngDegrees) {
-        return new LocationTokenImpl(getS2CellId(latDegrees, lngDegrees).id());
+    public static LocationToken createLocationTokenForLatLng(
+            double latDegrees, double lngDegrees, int s2Level) {
+        return new LocationTokenImpl(getS2CellId(latDegrees, lngDegrees, s2Level).id());
     }
 
     @Override
@@ -78,6 +78,11 @@
         return isSatCommunicationAllowedAtLocation(locationTokenImpl.getS2CellId());
     }
 
+    @Override
+    public int getS2Level() {
+        return mS2Level;
+    }
+
     private boolean isSatCommunicationAllowedAtLocation(long s2CellId) throws IOException {
         S2LevelRange entry = mSatS2RangeFileReader.findEntryByCellId(s2CellId);
         if (mSatS2RangeFileReader.isAllowedList()) {
@@ -91,12 +96,12 @@
         }
     }
 
-    private S2CellId getS2CellId(double latDegrees, double lngDegrees) {
+    private static S2CellId getS2CellId(double latDegrees, double lngDegrees, int s2Level) {
         // Create the leaf S2 cell containing the given S2LatLng
         S2CellId cellId = S2CellId.fromLatLng(S2LatLng.fromDegrees(latDegrees, lngDegrees));
 
         // Return the S2 cell at the expected S2 level
-        return cellId.parent(mS2Level);
+        return cellId.parent(s2Level);
     }
 
     @Override
diff --git a/src/com/android/phone/satellite/accesscontrol/SatelliteAccessController.java b/src/com/android/phone/satellite/accesscontrol/SatelliteAccessController.java
index 7f9c1aa..047c0a3 100644
--- a/src/com/android/phone/satellite/accesscontrol/SatelliteAccessController.java
+++ b/src/com/android/phone/satellite/accesscontrol/SatelliteAccessController.java
@@ -16,17 +16,62 @@
 
 package com.android.phone.satellite.accesscontrol;
 
+import static android.telephony.satellite.SatelliteManager.KEY_SATELLITE_COMMUNICATION_ALLOWED;
+import static android.telephony.satellite.SatelliteManager.SATELLITE_RESULT_REQUEST_NOT_SUPPORTED;
+import static android.telephony.satellite.SatelliteManager.SATELLITE_RESULT_SUCCESS;
+
+import android.annotation.ArrayRes;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Resources;
+import android.location.Location;
+import android.location.LocationManager;
+import android.location.LocationRequest;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.CancellationSignal;
 import android.os.Handler;
+import android.os.HandlerThread;
 import android.os.Looper;
 import android.os.Message;
 import android.os.ResultReceiver;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.provider.DeviceConfig;
+import android.telecom.TelecomManager;
+import android.telephony.AnomalyReporter;
 import android.telephony.Rlog;
-import android.telephony.satellite.SatelliteManager;
+import android.text.TextUtils;
+import android.util.Pair;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneFactory;
+import com.android.internal.telephony.TelephonyCountryDetector;
 import com.android.internal.telephony.flags.FeatureFlags;
+import com.android.internal.telephony.satellite.SatelliteController;
+import com.android.phone.PhoneGlobals;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 
 /**
  * This module is responsible for making sure that satellite communication can be used by devices
@@ -34,34 +79,141 @@
  */
 public class SatelliteAccessController extends Handler {
     private static final String TAG = "SatelliteAccessController";
+    /**
+     * UUID to report an anomaly when getting an exception in looking up on-device data for the
+     * current location.
+     */
+    private static final String UUID_ON_DEVICE_LOOKUP_EXCEPTION =
+            "dbea1641-630e-4780-9f25-8337ba6c3563";
+    /**
+     * UUID to report an anomaly when getting an exception in creating the on-device access
+     * controller.
+     */
+    private static final String UUID_CREATE_ON_DEVICE_ACCESS_CONTROLLER_EXCEPTION =
+            "3ac767d8-2867-4d60-97c2-ae9d378a5521";
+    protected static final long WAIT_FOR_CURRENT_LOCATION_TIMEOUT_MILLIS =
+            TimeUnit.SECONDS.toMillis(180);
+    protected static final long KEEP_ON_DEVICE_ACCESS_CONTROLLER_RESOURCES_TIMEOUT_MILLIS =
+            TimeUnit.MINUTES.toMillis(30);
+    protected static final int DEFAULT_S2_LEVEL = 12;
+    private static final int DEFAULT_LOCATION_FRESH_DURATION_SECONDS = 600;
+    private static final boolean DEFAULT_SATELLITE_ACCESS_ALLOW = true;
+    private static final String ALLOW_MOCK_MODEM_PROPERTY = "persist.radio.allow_mock_modem";
+    private static final String BOOT_ALLOW_MOCK_MODEM_PROPERTY = "ro.boot.radio.allow_mock_modem";
+    private static final boolean DEBUG = !"user".equals(Build.TYPE);
+    private static final int MAX_CACHE_SIZE = 50;
 
     private static final int CMD_IS_SATELLITE_COMMUNICATION_ALLOWED = 1;
+    protected static final int EVENT_WAIT_FOR_CURRENT_LOCATION_TIMEOUT = 2;
+    protected static final int EVENT_KEEP_ON_DEVICE_ACCESS_CONTROLLER_RESOURCES_TIMEOUT = 3;
+
+    private static SatelliteAccessController sInstance;
 
     /** Feature flags to control behavior and errors. */
     @NonNull private final FeatureFlags mFeatureFlags;
-    @Nullable private final SatelliteOnDeviceAccessController mSatelliteOnDeviceAccessController;
+    @GuardedBy("mLock")
+    @Nullable protected SatelliteOnDeviceAccessController mSatelliteOnDeviceAccessController;
+    @NonNull private final LocationManager mLocationManager;
+    @NonNull private final TelecomManager mTelecomManager;
+    @NonNull private final TelephonyCountryDetector mCountryDetector;
+    @NonNull private final SatelliteController mSatelliteController;
+    @NonNull private final ResultReceiver mInternalSatelliteAllowResultReceiver;
+    @NonNull protected final Object mLock = new Object();
+    @GuardedBy("mLock")
+    @NonNull
+    private final Set<ResultReceiver> mSatelliteAllowResultReceivers = new HashSet<>();
+    @NonNull private List<String> mSatelliteCountryCodes;
+    private boolean mIsSatelliteAllowAccessControl;
+    @Nullable private File mSatelliteS2CellFile;
+    private long mLocationFreshDurationNanos;
+    @GuardedBy("mLock")
+    private boolean mIsOverlayConfigOverridden = false;
+    @NonNull private List<String> mOverriddenSatelliteCountryCodes;
+    private boolean mOverriddenIsSatelliteAllowAccessControl;
+    @Nullable private File mOverriddenSatelliteS2CellFile;
+    private long mOverriddenLocationFreshDurationNanos;
+    @GuardedBy("mLock")
+    @NonNull
+    private final Map<SatelliteOnDeviceAccessController.LocationToken, Boolean>
+            mCachedAccessRestrictionMap = new LinkedHashMap<>() {
+                @Override
+                protected boolean removeEldestEntry(
+                        Entry<SatelliteOnDeviceAccessController.LocationToken, Boolean> eldest) {
+                    return size() > MAX_CACHE_SIZE;
+                }
+            };
+    @GuardedBy("mLock")
+    @Nullable
+    CancellationSignal mLocationRequestCancellationSignal = null;
+    private int mS2Level = DEFAULT_S2_LEVEL;
+    @GuardedBy("mLock")
+    @Nullable private Location mFreshLastKnownLocation = null;
+
+    /** These are used for CTS test */
+    private Path mCtsSatS2FilePath = null;
+    private static final String GOOGLE_US_SAN_SAT_S2_FILE_NAME = "google_us_san_sat_s2.dat";
 
     /**
      * Create a SatelliteAccessController instance.
      *
+     * @param context The context associated with the {@link SatelliteAccessController} instance.
      * @param featureFlags The FeatureFlags that are supported.
+     * @param locationManager The LocationManager for querying current location of the device.
      * @param looper The Looper to run the SatelliteAccessController on.
-     * @param satelliteOnDeviceAccessController The location-based satellite restriction lookup.
+     * @param satelliteOnDeviceAccessController The on-device satellite access controller instance.
      */
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
-    public SatelliteAccessController(@NonNull FeatureFlags featureFlags, @NonNull Looper looper,
-            @Nullable SatelliteOnDeviceAccessController satelliteOnDeviceAccessController) {
+    protected SatelliteAccessController(@NonNull Context context,
+            @NonNull FeatureFlags featureFlags, @NonNull Looper looper,
+            @NonNull LocationManager locationManager, @NonNull TelecomManager telecomManager,
+            @Nullable SatelliteOnDeviceAccessController satelliteOnDeviceAccessController,
+            @Nullable File s2CellFile) {
         super(looper);
         mFeatureFlags = featureFlags;
+        mLocationManager = locationManager;
+        mTelecomManager = telecomManager;
         mSatelliteOnDeviceAccessController = satelliteOnDeviceAccessController;
+        mCountryDetector = TelephonyCountryDetector.getInstance(context);
+        mSatelliteController = SatelliteController.getInstance();
+        loadOverlayConfigs(context);
+        if (s2CellFile != null) {
+            mSatelliteS2CellFile = s2CellFile;
+        }
+        mInternalSatelliteAllowResultReceiver = new ResultReceiver(this) {
+            @Override
+            protected void onReceiveResult(int resultCode, Bundle resultData) {
+                handleSatelliteAllowResultFromSatelliteController(resultCode, resultData);
+            }
+        };
+        // Init the SatelliteOnDeviceAccessController so that the S2 level can be cached
+        initSatelliteOnDeviceAccessController();
+    }
+
+    /** @return the singleton instance of {@link SatelliteAccessController} */
+    public static synchronized SatelliteAccessController getOrCreateInstance(
+            @NonNull Context context, @NonNull FeatureFlags featureFlags) {
+        if (sInstance == null) {
+            HandlerThread handlerThread = new HandlerThread("SatelliteAccessController");
+            handlerThread.start();
+            sInstance = new SatelliteAccessController(context, featureFlags,
+                    handlerThread.getLooper(), context.getSystemService(LocationManager.class),
+                    context.getSystemService(TelecomManager.class), null, null);
+        }
+        return sInstance;
     }
 
     @Override
     public void handleMessage(Message msg) {
         switch (msg.what) {
             case CMD_IS_SATELLITE_COMMUNICATION_ALLOWED:
-                handleRequestIsSatelliteCommunicationAllowedForCurrentLocation(
-                        (ResultReceiver) msg.obj);
+                handleCmdIsSatelliteAllowedForCurrentLocation(
+                        (Pair<Integer, ResultReceiver>) msg.obj);
+                break;
+            case EVENT_WAIT_FOR_CURRENT_LOCATION_TIMEOUT:
+                handleWaitForCurrentLocationTimedOutEvent();
+                break;
+            case EVENT_KEEP_ON_DEVICE_ACCESS_CONTROLLER_RESOURCES_TIMEOUT:
+                cleanupOnDeviceAccessControllerResources();
                 break;
             default:
                 logw("SatelliteAccessControllerHandler: unexpected message code: " + msg.what);
@@ -72,23 +224,677 @@
     /**
      * Request to get whether satellite communication is allowed for the current location.
      *
+     * @param subId The subId of the subscription to check whether satellite communication is
+     *              allowed for the current location for.
      * @param result The result receiver that returns whether satellite communication is allowed
      *               for the current location if the request is successful or an error code
      *               if the request failed.
      */
-    public void requestIsSatelliteCommunicationAllowedForCurrentLocation(
+    public void requestIsSatelliteCommunicationAllowedForCurrentLocation(int subId,
             @NonNull ResultReceiver result) {
         if (!mFeatureFlags.oemEnabledSatelliteFlag()) {
             logd("oemEnabledSatelliteFlag is disabled");
-            result.send(SatelliteManager.SATELLITE_RESULT_REQUEST_NOT_SUPPORTED, null);
+            result.send(SATELLITE_RESULT_REQUEST_NOT_SUPPORTED, null);
             return;
         }
-        sendRequestAsync(CMD_IS_SATELLITE_COMMUNICATION_ALLOWED, result);
+        sendRequestAsync(CMD_IS_SATELLITE_COMMUNICATION_ALLOWED, new Pair<>(subId, result));
     }
 
-    private void handleRequestIsSatelliteCommunicationAllowedForCurrentLocation(
-            @NonNull ResultReceiver result) {
-        // To be implemented
+    /**
+     * This API should be used by only CTS tests to override the overlay configs of satellite
+     * access controller.
+     */
+    public boolean setSatelliteAccessControlOverlayConfigs(boolean reset, boolean isAllowed,
+            @Nullable String s2CellFile, long locationFreshDurationNanos,
+            @Nullable List<String> satelliteCountryCodes) {
+        if (!isMockModemAllowed()) {
+            logd("setSatelliteAccessControllerOverlayConfigs: mock modem is not allowed");
+            return false;
+        }
+        logd("setSatelliteAccessControlOverlayConfigs: reset=" + reset
+                + ", isAllowed" + isAllowed + ", s2CellFile=" + s2CellFile
+                + ", locationFreshDurationNanos=" + locationFreshDurationNanos
+                + ", satelliteCountryCodes=" + ((satelliteCountryCodes != null)
+                ? String.join(", ", satelliteCountryCodes) : null));
+        synchronized (mLock) {
+            if (reset) {
+                mIsOverlayConfigOverridden = false;
+                cleanUpCtsResources();
+            } else {
+                mIsOverlayConfigOverridden = true;
+                mOverriddenIsSatelliteAllowAccessControl = isAllowed;
+                if (!TextUtils.isEmpty(s2CellFile)) {
+                    mOverriddenSatelliteS2CellFile = getTestSatelliteS2File(s2CellFile);
+                    if (!mOverriddenSatelliteS2CellFile.exists()) {
+                        logd("The overriding file "
+                                + mOverriddenSatelliteS2CellFile.getAbsolutePath()
+                                + " does not exist");
+                        mOverriddenSatelliteS2CellFile = null;
+                    }
+                } else {
+                    mOverriddenSatelliteS2CellFile = null;
+                }
+                mOverriddenLocationFreshDurationNanos = locationFreshDurationNanos;
+                if (satelliteCountryCodes != null) {
+                    mOverriddenSatelliteCountryCodes = satelliteCountryCodes;
+                } else {
+                    mOverriddenSatelliteCountryCodes = new ArrayList<>();
+                }
+            }
+            cleanupOnDeviceAccessControllerResources();
+            initSatelliteOnDeviceAccessController();
+        }
+        return true;
+    }
+
+    private File getTestSatelliteS2File(String fileName) {
+        logd("getTestSatelliteS2File: fileName=" + fileName);
+        if (TextUtils.equals(fileName, GOOGLE_US_SAN_SAT_S2_FILE_NAME)) {
+            mCtsSatS2FilePath = copyTestSatS2FileToPhoneDirectory(GOOGLE_US_SAN_SAT_S2_FILE_NAME);
+            if (mCtsSatS2FilePath != null) {
+                return mCtsSatS2FilePath.toFile();
+            } else {
+                loge("getTestSatelliteS2File: mCtsSatS2FilePath is null");
+            }
+        }
+        return new File(fileName);
+    }
+
+    @Nullable private static Path copyTestSatS2FileToPhoneDirectory(String sourceFileName) {
+        PhoneGlobals phoneGlobals = PhoneGlobals.getInstance();
+        File ctsFile = phoneGlobals.getDir("cts", Context.MODE_PRIVATE);
+        if (!ctsFile.exists()) {
+            ctsFile.mkdirs();
+        }
+
+        Path targetDir = ctsFile.toPath();
+        Path targetSatS2FilePath = targetDir.resolve(sourceFileName);
+        try {
+            InputStream inputStream = phoneGlobals.getAssets().open(sourceFileName);
+            if (inputStream == null) {
+                loge("copyTestSatS2FileToPhoneDirectory: Resource=" + sourceFileName
+                        + " not found");
+            } else {
+                Files.copy(inputStream, targetSatS2FilePath, StandardCopyOption.REPLACE_EXISTING);
+            }
+        } catch (IOException ex) {
+            loge("copyTestSatS2FileToPhoneDirectory: ex=" + ex);
+        }
+        return targetSatS2FilePath;
+    }
+
+    private void cleanUpCtsResources() {
+        if (mCtsSatS2FilePath != null) {
+            try {
+                Files.delete(mCtsSatS2FilePath);
+            } catch (IOException ex) {
+                loge("cleanUpCtsResources: ex=" + ex);
+            }
+        }
+    }
+
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    protected long getElapsedRealtimeNanos() {
+        return SystemClock.elapsedRealtimeNanos();
+    }
+
+    private void loadOverlayConfigs(@NonNull Context context) {
+        mSatelliteCountryCodes = getSatelliteCountryCodesFromOverlayConfig(context);
+        mIsSatelliteAllowAccessControl = getSatelliteAccessAllowFromOverlayConfig(context);
+        String satelliteS2CellFileName = getSatelliteS2CellFileFromOverlayConfig(context);
+        mSatelliteS2CellFile = TextUtils.isEmpty(satelliteS2CellFileName)
+                ? null : new File(satelliteS2CellFileName);
+        if (mSatelliteS2CellFile != null && !mSatelliteS2CellFile.exists()) {
+            loge("The satellite S2 cell file " + satelliteS2CellFileName + " does not exist");
+            mSatelliteS2CellFile = null;
+        }
+        mLocationFreshDurationNanos = getSatelliteLocationFreshDurationFromOverlayConfig(context);
+    }
+
+    private long getLocationFreshDurationNanos() {
+        synchronized (mLock) {
+            if (mIsOverlayConfigOverridden) {
+                return mOverriddenLocationFreshDurationNanos;
+            }
+            return mLocationFreshDurationNanos;
+        }
+    }
+
+    @NonNull private List<String> getSatelliteCountryCodes() {
+        synchronized (mLock) {
+            if (mIsOverlayConfigOverridden) {
+                return mOverriddenSatelliteCountryCodes;
+            }
+            return mSatelliteCountryCodes;
+        }
+    }
+
+    @Nullable private File getSatelliteS2CellFile() {
+        synchronized (mLock) {
+            if (mIsOverlayConfigOverridden) {
+                return mOverriddenSatelliteS2CellFile;
+            }
+            return mSatelliteS2CellFile;
+        }
+    }
+
+    private boolean isSatelliteAllowAccessControl() {
+        synchronized (mLock) {
+            if (mIsOverlayConfigOverridden) {
+                return mOverriddenIsSatelliteAllowAccessControl;
+            }
+            return mIsSatelliteAllowAccessControl;
+        }
+    }
+
+    private void handleCmdIsSatelliteAllowedForCurrentLocation(
+            @NonNull Pair<Integer, ResultReceiver> requestArguments) {
+        synchronized (mLock) {
+            mSatelliteAllowResultReceivers.add(requestArguments.second);
+            if (mSatelliteAllowResultReceivers.size() > 1) {
+                logd("requestIsSatelliteCommunicationAllowedForCurrentLocation is already being "
+                        + "processed");
+                return;
+            }
+            mSatelliteController.requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                    requestArguments.first, mInternalSatelliteAllowResultReceiver);
+        }
+    }
+
+    private void handleWaitForCurrentLocationTimedOutEvent() {
+        logd("Timed out to wait for current location");
+        synchronized (mLock) {
+            if (mLocationRequestCancellationSignal != null) {
+                mLocationRequestCancellationSignal.cancel();
+                mLocationRequestCancellationSignal = null;
+                onCurrentLocationAvailable(null);
+            } else {
+                loge("handleWaitForCurrentLocationTimedOutEvent: "
+                        + "mLocationRequestCancellationSignal is null");
+            }
+        }
+    }
+
+    private void handleSatelliteAllowResultFromSatelliteController(
+            int resultCode, Bundle resultData) {
+        logd("handleSatelliteAllowResultFromSatelliteController: resultCode=" + resultCode);
+        synchronized (mLock) {
+            if (resultCode == SATELLITE_RESULT_SUCCESS) {
+                if (resultData.containsKey(KEY_SATELLITE_COMMUNICATION_ALLOWED)) {
+                    boolean isSatelliteAllowed = resultData.getBoolean(
+                            KEY_SATELLITE_COMMUNICATION_ALLOWED);
+                    if (!isSatelliteAllowed) {
+                        logd("Satellite is not allowed by modem");
+                        sendSatelliteAllowResultToReceivers(resultCode, resultData);
+                    } else {
+                        checkSatelliteAccessRestrictionForCurrentLocation();
+                    }
+                } else {
+                    loge("KEY_SATELLITE_COMMUNICATION_ALLOWED does not exist.");
+                    sendSatelliteAllowResultToReceivers(resultCode, resultData);
+                }
+            } else if (resultCode == SATELLITE_RESULT_REQUEST_NOT_SUPPORTED) {
+                checkSatelliteAccessRestrictionForCurrentLocation();
+            } else {
+                sendSatelliteAllowResultToReceivers(resultCode, resultData);
+            }
+        }
+    }
+
+    private void sendSatelliteAllowResultToReceivers(int resultCode, Bundle resultData) {
+        synchronized (mLock) {
+            for (ResultReceiver resultReceiver : mSatelliteAllowResultReceivers) {
+                resultReceiver.send(resultCode, resultData);
+            }
+            mSatelliteAllowResultReceivers.clear();
+        }
+    }
+
+    /**
+     * Telephony-internal logic to verify if satellite access is restricted at the current location.
+     */
+    private void checkSatelliteAccessRestrictionForCurrentLocation() {
+        synchronized (mLock) {
+            List<String> networkCountryIsoList = mCountryDetector.getCurrentNetworkCountryIso();
+            if (!networkCountryIsoList.isEmpty()) {
+                logd("Use current network country codes=" + String.join(", ",
+                        networkCountryIsoList));
+
+                Bundle bundle = new Bundle();
+                bundle.putBoolean(KEY_SATELLITE_COMMUNICATION_ALLOWED,
+                        isSatelliteAccessAllowedForLocation(networkCountryIsoList));
+                sendSatelliteAllowResultToReceivers(SATELLITE_RESULT_SUCCESS, bundle);
+            } else {
+                if (shouldUseOnDeviceAccessController()) {
+                    // This will be an asynchronous check when it needs to wait for the current
+                    // location from location service
+                    checkSatelliteAccessRestrictionUsingOnDeviceData();
+                } else {
+                    // This is always a synchronous check
+                    checkSatelliteAccessRestrictionUsingCachedCountryCodes();
+                }
+            }
+        }
+    }
+
+    /**
+     * This function synchronously checks if satellite is allowed at current location using cached
+     * country codes.
+     */
+    private void checkSatelliteAccessRestrictionUsingCachedCountryCodes() {
+        Pair<String, Long> locationCountryCodeInfo =
+                mCountryDetector.getCachedLocationCountryIsoInfo();
+        Map<String, Long> networkCountryCodeInfoMap =
+                mCountryDetector.getCachedNetworkCountryIsoInfo();
+        List<String> countryCodeList;
+
+        // Check if the cached location country code's timestamp is newer than all cached network
+        // country codes
+        if (!TextUtils.isEmpty(locationCountryCodeInfo.first) && isGreaterThanAll(
+                locationCountryCodeInfo.second, networkCountryCodeInfoMap.values())) {
+            // Use cached location country code
+            countryCodeList = Arrays.asList(locationCountryCodeInfo.first);
+        } else {
+            // Use cached network country codes
+            countryCodeList = networkCountryCodeInfoMap.keySet().stream().toList();
+        }
+        logd("Use cached country codes=" + String.join(", ", countryCodeList));
+
+        Bundle bundle = new Bundle();
+        bundle.putBoolean(KEY_SATELLITE_COMMUNICATION_ALLOWED,
+                isSatelliteAccessAllowedForLocation(countryCodeList));
+        sendSatelliteAllowResultToReceivers(SATELLITE_RESULT_SUCCESS, bundle);
+    }
+
+    /**
+     * This function asynchronously checks if satellite is allowed at the current location using
+     * on-device data. Asynchronous check happens when it needs to wait for the current location
+     * from location service.
+     */
+    private void checkSatelliteAccessRestrictionUsingOnDeviceData() {
+        synchronized (mLock) {
+            logd("Use on-device data");
+            if (mFreshLastKnownLocation != null) {
+                checkSatelliteAccessRestrictionForLocation(mFreshLastKnownLocation);
+                mFreshLastKnownLocation = null;
+            } else {
+                Location freshLastKnownLocation = getFreshLastKnownLocation();
+                if (freshLastKnownLocation != null) {
+                    checkSatelliteAccessRestrictionForLocation(freshLastKnownLocation);
+                } else {
+                    queryCurrentLocation();
+                }
+            }
+        }
+    }
+
+    private void queryCurrentLocation() {
+        synchronized (mLock) {
+            if (mLocationRequestCancellationSignal != null) {
+                logd("Request for current location was already sent to LocationManager");
+                return;
+            }
+            mLocationRequestCancellationSignal = new CancellationSignal();
+            mLocationManager.getCurrentLocation(LocationManager.GPS_PROVIDER,
+                    new LocationRequest.Builder(0)
+                            .setQuality(LocationRequest.QUALITY_HIGH_ACCURACY)
+                            .setLocationSettingsIgnored(true)
+                            .build(),
+                    mLocationRequestCancellationSignal, this::post,
+                    this::onCurrentLocationAvailable);
+            startWaitForCurrentLocationTimer();
+        }
+    }
+
+    private void onCurrentLocationAvailable(@Nullable Location location) {
+        logd("onCurrentLocationAvailable " + (location != null));
+        synchronized (mLock) {
+            stopWaitForCurrentLocationTimer();
+            mLocationRequestCancellationSignal = null;
+            if (location != null) {
+                checkSatelliteAccessRestrictionForLocation(location);
+            } else {
+                checkSatelliteAccessRestrictionUsingCachedCountryCodes();
+            }
+        }
+    }
+
+    private void checkSatelliteAccessRestrictionForLocation(@NonNull Location location) {
+        synchronized (mLock) {
+            try {
+                SatelliteOnDeviceAccessController.LocationToken locationToken =
+                        SatelliteOnDeviceAccessController.createLocationTokenForLatLng(
+                                location.getLatitude(),
+                                location.getLongitude(), mS2Level);
+                boolean satelliteAllowed;
+                if (mCachedAccessRestrictionMap.containsKey(locationToken)) {
+                    satelliteAllowed = mCachedAccessRestrictionMap.get(locationToken);
+                } else {
+                    if (!initSatelliteOnDeviceAccessController()) {
+                        loge("Failed to init SatelliteOnDeviceAccessController");
+                        checkSatelliteAccessRestrictionUsingCachedCountryCodes();
+                        return;
+                    }
+                    satelliteAllowed = mSatelliteOnDeviceAccessController
+                            .isSatCommunicationAllowedAtLocation(locationToken);
+                    updateCachedAccessRestrictionMap(locationToken, satelliteAllowed);
+                }
+                Bundle bundle = new Bundle();
+                bundle.putBoolean(KEY_SATELLITE_COMMUNICATION_ALLOWED, satelliteAllowed);
+                sendSatelliteAllowResultToReceivers(SATELLITE_RESULT_SUCCESS, bundle);
+            } catch (Exception ex) {
+                loge("checkSatelliteAccessRestrictionForLocation: ex=" + ex);
+                reportAnomaly(UUID_ON_DEVICE_LOOKUP_EXCEPTION,
+                        "On-device satellite lookup exception");
+                checkSatelliteAccessRestrictionUsingCachedCountryCodes();
+            }
+        }
+    }
+
+    private void updateCachedAccessRestrictionMap(@NonNull
+            SatelliteOnDeviceAccessController.LocationToken locationToken,
+            boolean satelliteAllowed) {
+        synchronized (mLock) {
+            mCachedAccessRestrictionMap.put(locationToken, satelliteAllowed);
+        }
+    }
+
+    private boolean isGreaterThanAll(
+            long comparedItem, @NonNull Collection<Long> itemCollection) {
+        for (long item : itemCollection) {
+            if (comparedItem <= item) return false;
+        }
+        return true;
+    }
+
+    private boolean isSatelliteAccessAllowedForLocation(
+            @NonNull List<String> networkCountryIsoList) {
+        if (isSatelliteAllowAccessControl()) {
+            // The current country is unidentified, we're uncertain and thus returning false
+            if (networkCountryIsoList.isEmpty()) {
+                return false;
+            }
+
+            // In case of allowed list, satellite is allowed if all country codes are be in the
+            // allowed list
+            return getSatelliteCountryCodes().containsAll(networkCountryIsoList);
+        } else {
+            // No country is barred, thus returning true
+            if (getSatelliteCountryCodes().isEmpty()) {
+                return true;
+            }
+
+            // The current country is unidentified, we're uncertain and thus returning false
+            if (networkCountryIsoList.isEmpty()) {
+                return false;
+            }
+
+            // In case of disallowed list, if any country code is in the list, satellite will be
+            // disallowed
+            for (String countryCode : networkCountryIsoList) {
+                if (getSatelliteCountryCodes().contains(countryCode)) {
+                    return false;
+                }
+            }
+            return true;
+        }
+    }
+
+    private boolean shouldUseOnDeviceAccessController() {
+        if (getSatelliteS2CellFile() == null) {
+            return false;
+        }
+
+        if (isInEmergency() || mLocationManager.isLocationEnabled()) {
+            return true;
+        }
+
+        Location freshLastKnownLocation = getFreshLastKnownLocation();
+        if (freshLastKnownLocation != null) {
+            synchronized (mLock) {
+                mFreshLastKnownLocation = freshLastKnownLocation;
+            }
+            return true;
+        } else {
+            synchronized (mLock) {
+                mFreshLastKnownLocation = null;
+            }
+        }
+        return false;
+    }
+
+    @Nullable private Location getFreshLastKnownLocation() {
+        Location lastKnownLocation = getLastKnownLocation();
+        if (lastKnownLocation != null) {
+            long lastKnownLocationAge =
+                    getElapsedRealtimeNanos() - lastKnownLocation.getElapsedRealtimeNanos();
+            if (lastKnownLocationAge <= getLocationFreshDurationNanos()) {
+                return lastKnownLocation;
+            }
+        }
+        return null;
+    }
+
+    private boolean isInEmergency() {
+        // Check if emergency call is ongoing
+        if (mTelecomManager.isInEmergencyCall()) {
+            return true;
+        }
+        // Check if the device is in emergency callback mode
+        for (Phone phone : PhoneFactory.getPhones()) {
+            if (phone.isInEcm()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Nullable
+    private Location getLastKnownLocation() {
+        Location result = null;
+        for (String provider : mLocationManager.getProviders(true)) {
+            Location location = mLocationManager.getLastKnownLocation(provider);
+            if (location != null && (result == null
+                    || result.getElapsedRealtimeNanos() < location.getElapsedRealtimeNanos())) {
+                result = location;
+            }
+        }
+        return result;
+    }
+
+    /**
+     * @return {@code true} if successfully initialize the {@link SatelliteOnDeviceAccessController}
+     * instance, {@code false} otherwise.
+     * @throws IllegalStateException in case of getting any exception in creating the
+     * {@link SatelliteOnDeviceAccessController} instance and the device is using a user build.
+     */
+    private boolean initSatelliteOnDeviceAccessController() throws IllegalStateException {
+        synchronized (mLock) {
+            if (getSatelliteS2CellFile() == null) return false;
+
+            // mSatelliteOnDeviceAccessController was already initialized successfully
+            if (mSatelliteOnDeviceAccessController != null) {
+                restartKeepOnDeviceAccessControllerResourcesTimer();
+                return true;
+            }
+
+            try {
+                mSatelliteOnDeviceAccessController =
+                        SatelliteOnDeviceAccessController.create(getSatelliteS2CellFile());
+                restartKeepOnDeviceAccessControllerResourcesTimer();
+                mS2Level = mSatelliteOnDeviceAccessController.getS2Level();
+                logd("mS2Level=" + mS2Level);
+            } catch (Exception ex) {
+                loge("Got exception in creating an instance of SatelliteOnDeviceAccessController,"
+                        + " ex=" + ex + ", sat s2 file="
+                        + getSatelliteS2CellFile().getAbsolutePath());
+                reportAnomaly(UUID_CREATE_ON_DEVICE_ACCESS_CONTROLLER_EXCEPTION,
+                        "Exception in creating on-device satellite access controller");
+                mSatelliteOnDeviceAccessController = null;
+                if (!mIsOverlayConfigOverridden) {
+                    mSatelliteS2CellFile = null;
+                }
+                return false;
+            }
+            return true;
+        }
+    }
+
+    private void cleanupOnDeviceAccessControllerResources() {
+        synchronized (mLock) {
+            logd("cleanupOnDeviceAccessControllerResources="
+                    + (mSatelliteOnDeviceAccessController != null));
+            if (mSatelliteOnDeviceAccessController != null) {
+                try {
+                    mSatelliteOnDeviceAccessController.close();
+                } catch (Exception ex) {
+                    loge("cleanupOnDeviceAccessControllerResources: ex=" + ex);
+                }
+                mSatelliteOnDeviceAccessController = null;
+                stopKeepOnDeviceAccessControllerResourcesTimer();
+            }
+        }
+    }
+
+    private static boolean getSatelliteAccessAllowFromOverlayConfig(@NonNull Context context) {
+        Boolean accessAllowed = null;
+        try {
+            accessAllowed = context.getResources().getBoolean(
+                    com.android.internal.R.bool.config_oem_enabled_satellite_access_allow);
+        } catch (Resources.NotFoundException ex) {
+            loge("getSatelliteAccessAllowFromOverlayConfig: got ex=" + ex);
+        }
+        if (accessAllowed == null && isMockModemAllowed()) {
+            logd("getSatelliteAccessAllowFromOverlayConfig: Read "
+                    + "config_oem_enabled_satellite_access_allow from device config");
+            accessAllowed = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_TELEPHONY,
+                    "config_oem_enabled_satellite_access_allow", DEFAULT_SATELLITE_ACCESS_ALLOW);
+        }
+        if (accessAllowed == null) {
+            logd("Use default satellite access allow=true control");
+            accessAllowed = true;
+        }
+        return accessAllowed;
+    }
+
+    @Nullable
+    private static String getSatelliteS2CellFileFromOverlayConfig(@NonNull Context context) {
+        String s2CellFile = null;
+        try {
+            s2CellFile = context.getResources().getString(
+                    com.android.internal.R.string.config_oem_enabled_satellite_s2cell_file);
+        } catch (Resources.NotFoundException ex) {
+            loge("getSatelliteS2CellFileFromOverlayConfig: got ex=" + ex);
+        }
+        if (TextUtils.isEmpty(s2CellFile) && isMockModemAllowed()) {
+            logd("getSatelliteS2CellFileFromOverlayConfig: Read "
+                    + "config_oem_enabled_satellite_s2cell_file from device config");
+            s2CellFile = DeviceConfig.getString(DeviceConfig.NAMESPACE_TELEPHONY,
+                    "config_oem_enabled_satellite_s2cell_file", null);
+        }
+        logd("s2CellFile=" + s2CellFile);
+        return s2CellFile;
+    }
+
+    @NonNull
+    private static List<String> getSatelliteCountryCodesFromOverlayConfig(
+            @NonNull Context context) {
+        String[] countryCodes = readStringArrayFromOverlayConfig(context,
+                com.android.internal.R.array.config_oem_enabled_satellite_country_codes);
+        if (countryCodes.length == 0 && isMockModemAllowed()) {
+            logd("getSatelliteCountryCodesFromOverlayConfig: Read "
+                    + "config_oem_enabled_satellite_country_codes from device config");
+            String countryCodesStr = DeviceConfig.getString(DeviceConfig.NAMESPACE_TELEPHONY,
+                    "config_oem_enabled_satellite_country_codes", "");
+            countryCodes = countryCodesStr.split(",");
+        }
+        return Arrays.stream(countryCodes)
+                .map(x -> x.toUpperCase(Locale.US))
+                .collect(Collectors.toList());
+    }
+
+    @NonNull
+    private static String[] readStringArrayFromOverlayConfig(
+            @NonNull Context context, @ArrayRes int id) {
+        String[] strArray = null;
+        try {
+            strArray = context.getResources().getStringArray(id);
+        } catch (Resources.NotFoundException ex) {
+            loge("readStringArrayFromOverlayConfig: id= " + id + ", ex=" + ex);
+        }
+        if (strArray == null) {
+            strArray = new String[0];
+        }
+        return strArray;
+    }
+
+    private static long getSatelliteLocationFreshDurationFromOverlayConfig(
+            @NonNull Context context) {
+        Integer freshDuration = null;
+        try {
+            freshDuration = context.getResources().getInteger(com.android.internal.R.integer
+                    .config_oem_enabled_satellite_location_fresh_duration);
+        } catch (Resources.NotFoundException ex) {
+            loge("getSatelliteLocationFreshDurationFromOverlayConfig: got ex=" + ex);
+        }
+        if (freshDuration == null && isMockModemAllowed()) {
+            logd("getSatelliteLocationFreshDurationFromOverlayConfig: Read "
+                    + "config_oem_enabled_satellite_location_fresh_duration from device config");
+            freshDuration = DeviceConfig.getInt(DeviceConfig.NAMESPACE_TELEPHONY,
+                    "config_oem_enabled_satellite_location_fresh_duration",
+                    DEFAULT_LOCATION_FRESH_DURATION_SECONDS);
+        }
+        if (freshDuration == null) {
+            logd("Use default satellite location fresh duration="
+                    + DEFAULT_LOCATION_FRESH_DURATION_SECONDS);
+            freshDuration = DEFAULT_LOCATION_FRESH_DURATION_SECONDS;
+        }
+        return TimeUnit.SECONDS.toNanos(freshDuration);
+    }
+
+    private void startWaitForCurrentLocationTimer() {
+        synchronized (mLock) {
+            if (hasMessages(EVENT_WAIT_FOR_CURRENT_LOCATION_TIMEOUT)) {
+                logw("WaitForCurrentLocationTimer is already started");
+                removeMessages(EVENT_WAIT_FOR_CURRENT_LOCATION_TIMEOUT);
+            }
+            sendEmptyMessageDelayed(EVENT_WAIT_FOR_CURRENT_LOCATION_TIMEOUT,
+                    WAIT_FOR_CURRENT_LOCATION_TIMEOUT_MILLIS);
+        }
+    }
+
+    private void stopWaitForCurrentLocationTimer() {
+        synchronized (mLock) {
+            removeMessages(EVENT_WAIT_FOR_CURRENT_LOCATION_TIMEOUT);
+        }
+    }
+
+    private void restartKeepOnDeviceAccessControllerResourcesTimer() {
+        synchronized (mLock) {
+            if (hasMessages(EVENT_KEEP_ON_DEVICE_ACCESS_CONTROLLER_RESOURCES_TIMEOUT)) {
+                logd("KeepOnDeviceAccessControllerResourcesTimer is already started. "
+                        + "Restarting it...");
+                removeMessages(EVENT_KEEP_ON_DEVICE_ACCESS_CONTROLLER_RESOURCES_TIMEOUT);
+            }
+            sendEmptyMessageDelayed(EVENT_KEEP_ON_DEVICE_ACCESS_CONTROLLER_RESOURCES_TIMEOUT,
+                    KEEP_ON_DEVICE_ACCESS_CONTROLLER_RESOURCES_TIMEOUT_MILLIS);
+        }
+    }
+
+    private void stopKeepOnDeviceAccessControllerResourcesTimer() {
+        synchronized (mLock) {
+            removeMessages(EVENT_KEEP_ON_DEVICE_ACCESS_CONTROLLER_RESOURCES_TIMEOUT);
+        }
+    }
+
+    private void reportAnomaly(@NonNull String uuid, @NonNull String log) {
+        loge(log);
+        AnomalyReporter.reportAnomaly(UUID.fromString(uuid), log);
+    }
+
+    private static boolean isMockModemAllowed() {
+        return (DEBUG || SystemProperties.getBoolean(ALLOW_MOCK_MODEM_PROPERTY, false)
+                || SystemProperties.getBoolean(BOOT_ALLOW_MOCK_MODEM_PROPERTY, false));
     }
 
     /**
diff --git a/src/com/android/phone/satellite/accesscontrol/SatelliteOnDeviceAccessController.java b/src/com/android/phone/satellite/accesscontrol/SatelliteOnDeviceAccessController.java
index 9292f33..520699f 100644
--- a/src/com/android/phone/satellite/accesscontrol/SatelliteOnDeviceAccessController.java
+++ b/src/com/android/phone/satellite/accesscontrol/SatelliteOnDeviceAccessController.java
@@ -45,11 +45,12 @@
 
     /**
      * Returns a token for a given location. See {@link LocationToken} for details.
-     *
-     * @throws IOException in the unlikely event of errors when reading the underlying file
      */
-    public abstract LocationToken createLocationTokenForLatLng(double latDegrees, double lngDegrees)
-            throws IOException;
+    public static LocationToken createLocationTokenForLatLng(double latDegrees, double lngDegrees,
+            int s2Level) {
+        return S2RangeSatelliteOnDeviceAccessController
+                .createLocationTokenForLatLng(latDegrees, lngDegrees, s2Level);
+    }
 
     /**
      * Returns {@code true} if the satellite communication is allowed at the provided location,
@@ -61,6 +62,11 @@
             throws IOException;
 
     /**
+     * Returns the S2 level of the file.
+     */
+    public abstract int getS2Level();
+
+    /**
      * A class that represents an area with the same value. Two locations with tokens that
      * {@link #equals(Object) equal each other} will definitely return the same value.
      *
diff --git a/tests/Android.bp b/tests/Android.bp
index a0304f6..6914839 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -54,7 +54,6 @@
         "satellite-s2storage-rw",
         "satellite-s2storage-testutils",
         "s2-geometry-library-java",
-        "telephony-satellite",
     ],
 
     test_suites: [
diff --git a/tests/src/com/android/phone/satellite/accesscontrol/S2RangeSatelliteOnDeviceAccessControllerTest.java b/tests/src/com/android/phone/satellite/accesscontrol/S2RangeSatelliteOnDeviceAccessControllerTest.java
index 84c233a..16a256d 100644
--- a/tests/src/com/android/phone/satellite/accesscontrol/S2RangeSatelliteOnDeviceAccessControllerTest.java
+++ b/tests/src/com/android/phone/satellite/accesscontrol/S2RangeSatelliteOnDeviceAccessControllerTest.java
@@ -80,13 +80,14 @@
         SatelliteOnDeviceAccessController accessController = null;
         try {
             accessController = SatelliteOnDeviceAccessController.create(mFile);
+            int s2Level = accessController.getS2Level();
 
             // Verify an edge cell of range 1 not in the output file
             S2CellId s2CellId = new S2CellId(TestUtils.createCellId(fileFormat, 1, 1000, 999));
             S2LatLng s2LatLng = s2CellId.toLatLng();
             SatelliteOnDeviceAccessController.LocationToken locationToken =
-                    accessController.createLocationTokenForLatLng(
-                            s2LatLng.latDegrees(), s2LatLng.lngDegrees());
+                    SatelliteOnDeviceAccessController.createLocationTokenForLatLng(
+                            s2LatLng.latDegrees(), s2LatLng.lngDegrees(), s2Level);
             boolean isAllowed = accessController.isSatCommunicationAllowedAtLocation(locationToken);
             assertTrue(isAllowed != isAllowedList);
 
@@ -96,8 +97,8 @@
                 s2LatLng = s2CellId.toLatLng();
 
                 // Lookup using location token
-                locationToken = accessController.createLocationTokenForLatLng(
-                                s2LatLng.latDegrees(), s2LatLng.lngDegrees());
+                locationToken = SatelliteOnDeviceAccessController.createLocationTokenForLatLng(
+                                s2LatLng.latDegrees(), s2LatLng.lngDegrees(), s2Level);
                 isAllowed = accessController.isSatCommunicationAllowedAtLocation(locationToken);
                 assertTrue(isAllowed == isAllowedList);
             }
@@ -105,8 +106,8 @@
             // Verify the middle cell not in the output file
             s2CellId = new S2CellId(TestUtils.createCellId(fileFormat, 1, 1000, 2000));
             s2LatLng = s2CellId.toLatLng();
-            locationToken = accessController.createLocationTokenForLatLng(
-                    s2LatLng.latDegrees(), s2LatLng.lngDegrees());
+            locationToken = SatelliteOnDeviceAccessController.createLocationTokenForLatLng(
+                    s2LatLng.latDegrees(), s2LatLng.lngDegrees(), s2Level);
             isAllowed = accessController.isSatCommunicationAllowedAtLocation(locationToken);
             assertTrue(isAllowed != isAllowedList);
 
@@ -114,8 +115,8 @@
             for (int suffix = 2001; suffix < 3000; suffix++) {
                 s2CellId = new S2CellId(TestUtils.createCellId(fileFormat, 1, 1000, suffix));
                 s2LatLng = s2CellId.toLatLng();
-                locationToken = accessController.createLocationTokenForLatLng(
-                        s2LatLng.latDegrees(), s2LatLng.lngDegrees());
+                locationToken = SatelliteOnDeviceAccessController.createLocationTokenForLatLng(
+                        s2LatLng.latDegrees(), s2LatLng.lngDegrees(), s2Level);
                 isAllowed = accessController.isSatCommunicationAllowedAtLocation(locationToken);
                 assertTrue(isAllowed == isAllowedList);
             }
@@ -123,16 +124,16 @@
             // Verify an edge cell of range 2 not in the output file
             s2CellId = new S2CellId(TestUtils.createCellId(fileFormat, 1, 1000, 3000));
             s2LatLng = s2CellId.toLatLng();
-            locationToken = accessController.createLocationTokenForLatLng(
-                    s2LatLng.latDegrees(), s2LatLng.lngDegrees());
+            locationToken = SatelliteOnDeviceAccessController.createLocationTokenForLatLng(
+                    s2LatLng.latDegrees(), s2LatLng.lngDegrees(), s2Level);
             isAllowed = accessController.isSatCommunicationAllowedAtLocation(locationToken);
             assertTrue(isAllowed != isAllowedList);
 
             // Verify an edge cell of range 3 not in the output file
             s2CellId = new S2CellId(TestUtils.createCellId(fileFormat, 1, 1001, 999));
             s2LatLng = s2CellId.toLatLng();
-            locationToken = accessController.createLocationTokenForLatLng(
-                    s2LatLng.latDegrees(), s2LatLng.lngDegrees());
+            locationToken = SatelliteOnDeviceAccessController.createLocationTokenForLatLng(
+                    s2LatLng.latDegrees(), s2LatLng.lngDegrees(), s2Level);
             isAllowed = accessController.isSatCommunicationAllowedAtLocation(locationToken);
             assertTrue(isAllowed != isAllowedList);
 
@@ -140,8 +141,8 @@
             for (int suffix = 1000; suffix < 2000; suffix++) {
                 s2CellId = new S2CellId(TestUtils.createCellId(fileFormat, 1, 1001, suffix));
                 s2LatLng = s2CellId.toLatLng();
-                locationToken = accessController.createLocationTokenForLatLng(
-                        s2LatLng.latDegrees(), s2LatLng.lngDegrees());
+                locationToken = SatelliteOnDeviceAccessController.createLocationTokenForLatLng(
+                        s2LatLng.latDegrees(), s2LatLng.lngDegrees(), s2Level);
                 isAllowed = accessController.isSatCommunicationAllowedAtLocation(locationToken);
                 assertTrue(isAllowed == isAllowedList);
             }
@@ -149,8 +150,8 @@
             // Verify an edge cell of range 3 not in the output file
             s2CellId = new S2CellId(TestUtils.createCellId(fileFormat, 1, 1001, 2000));
             s2LatLng = s2CellId.toLatLng();
-            locationToken = accessController.createLocationTokenForLatLng(
-                    s2LatLng.latDegrees(), s2LatLng.lngDegrees());
+            locationToken = SatelliteOnDeviceAccessController.createLocationTokenForLatLng(
+                    s2LatLng.latDegrees(), s2LatLng.lngDegrees(), s2Level);
             isAllowed = accessController.isSatCommunicationAllowedAtLocation(locationToken);
             assertTrue(isAllowed != isAllowedList);
         } catch (Exception ex) {
diff --git a/tests/src/com/android/phone/satellite/accesscontrol/SatelliteAccessControllerTest.java b/tests/src/com/android/phone/satellite/accesscontrol/SatelliteAccessControllerTest.java
new file mode 100644
index 0000000..f8c5051
--- /dev/null
+++ b/tests/src/com/android/phone/satellite/accesscontrol/SatelliteAccessControllerTest.java
@@ -0,0 +1,658 @@
+/*
+ * 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.satellite.accesscontrol;
+
+import static android.telephony.satellite.SatelliteManager.KEY_SATELLITE_COMMUNICATION_ALLOWED;
+import static android.telephony.satellite.SatelliteManager.SATELLITE_RESULT_ERROR;
+import static android.telephony.satellite.SatelliteManager.SATELLITE_RESULT_REQUEST_NOT_SUPPORTED;
+import static android.telephony.satellite.SatelliteManager.SATELLITE_RESULT_SUCCESS;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+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.Mockito.clearInvocations;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Resources;
+import android.location.Location;
+import android.location.LocationManager;
+import android.location.LocationRequest;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.ResultReceiver;
+import android.telecom.TelecomManager;
+import android.testing.TestableLooper;
+import android.util.Log;
+import android.util.Pair;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneFactory;
+import com.android.internal.telephony.TelephonyCountryDetector;
+import com.android.internal.telephony.flags.FeatureFlags;
+import com.android.internal.telephony.satellite.SatelliteController;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.File;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+/** Unit test for {@link SatelliteAccessController} */
+@RunWith(AndroidJUnit4.class)
+public class SatelliteAccessControllerTest {
+    private static final String TAG = "SatelliteAccessControllerTest";
+    private static final String[] TEST_SATELLITE_COUNTRY_CODES = {"US", "CA", "UK"};
+    private static final String TEST_SATELLITE_S2_FILE = "sat_s2_file.dat";
+    private static final boolean TEST_SATELLITE_ALLOW = true;
+    private static final int TEST_LOCATION_FRESH_DURATION_SECONDS = 10;
+    private static final long TEST_LOCATION_FRESH_DURATION_NANOS =
+            TimeUnit.SECONDS.toNanos(TEST_LOCATION_FRESH_DURATION_SECONDS);
+    private static final long TIMEOUT = 500;
+    private static final List<String> EMPTY_STRING_LIST = new ArrayList<>();
+    private static final List<String> LOCATION_PROVIDERS =
+            listOf(LocationManager.NETWORK_PROVIDER, LocationManager.GPS_PROVIDER);
+    private static final int SUB_ID = 0;
+
+    @Mock
+    private LocationManager mMockLocationManager;
+    @Mock
+    private TelecomManager mMockTelecomManager;
+    @Mock
+    private TelephonyCountryDetector mMockCountryDetector;
+    @Mock
+    private SatelliteController mMockSatelliteController;
+    @Mock
+    private Context mMockContext;
+    @Mock private Phone mMockPhone;
+    @Mock private Phone mMockPhone2;
+    @Mock private FeatureFlags mMockFeatureFlags;
+    @Mock private Resources mMockResources;
+    @Mock private SatelliteOnDeviceAccessController mMockSatelliteOnDeviceAccessController;
+    @Mock Location mMockLocation0;
+    @Mock Location mMockLocation1;
+    @Mock File mMockSatS2File;
+
+    private Looper mLooper;
+    private TestableLooper mTestableLooper;
+    private Phone[] mPhones;
+    private TestSatelliteAccessController mSatelliteAccessControllerUT;
+    @Captor
+    private ArgumentCaptor<CancellationSignal> mLocationRequestCancellationSignalCaptor;
+    @Captor
+    private ArgumentCaptor<Consumer<Location>> mLocationRequestConsumerCaptor;
+    @Captor
+    private ArgumentCaptor<ResultReceiver> mResultReceiverFromSatelliteControllerCaptor;
+    private boolean mQueriedSatelliteAllowed = false;
+    private int mQueriedSatelliteAllowedResultCode = SATELLITE_RESULT_SUCCESS;
+    private Semaphore mSatelliteAllowedSemaphore = new Semaphore(0);
+    private ResultReceiver mSatelliteAllowedReceiver = new ResultReceiver(null) {
+        @Override
+        protected void onReceiveResult(int resultCode, Bundle resultData) {
+            mQueriedSatelliteAllowedResultCode = resultCode;
+            if (resultCode == SATELLITE_RESULT_SUCCESS) {
+                if (resultData.containsKey(KEY_SATELLITE_COMMUNICATION_ALLOWED)) {
+                    mQueriedSatelliteAllowed = resultData.getBoolean(
+                            KEY_SATELLITE_COMMUNICATION_ALLOWED);
+                } else {
+                    logd("KEY_SATELLITE_COMMUNICATION_ALLOWED does not exist.");
+                    mQueriedSatelliteAllowed = false;
+                }
+            } else {
+                logd("mSatelliteAllowedReceiver: resultCode=" + resultCode);
+                mQueriedSatelliteAllowed = false;
+            }
+            try {
+                mSatelliteAllowedSemaphore.release();
+            } catch (Exception ex) {
+                fail("mSatelliteAllowedReceiver: Got exception in releasing semaphore, ex=" + ex);
+            }
+        }
+    };
+
+    private boolean mQueriedSatelliteAllowed2 = false;
+    private int mQueriedSatelliteAllowedResultCode2 = SATELLITE_RESULT_SUCCESS;
+    private Semaphore mSatelliteAllowedSemaphore2 = new Semaphore(0);
+    private ResultReceiver mSatelliteAllowedReceiver2 = new ResultReceiver(null) {
+        @Override
+        protected void onReceiveResult(int resultCode, Bundle resultData) {
+            mQueriedSatelliteAllowedResultCode2 = resultCode;
+            if (resultCode == SATELLITE_RESULT_SUCCESS) {
+                if (resultData.containsKey(KEY_SATELLITE_COMMUNICATION_ALLOWED)) {
+                    mQueriedSatelliteAllowed2 = resultData.getBoolean(
+                            KEY_SATELLITE_COMMUNICATION_ALLOWED);
+                } else {
+                    logd("KEY_SATELLITE_COMMUNICATION_ALLOWED does not exist.");
+                    mQueriedSatelliteAllowed2 = false;
+                }
+            } else {
+                logd("mSatelliteAllowedReceiver2: resultCode=" + resultCode);
+                mQueriedSatelliteAllowed2 = false;
+            }
+            try {
+                mSatelliteAllowedSemaphore2.release();
+            } catch (Exception ex) {
+                fail("mSatelliteAllowedReceiver2: Got exception in releasing semaphore, ex=" + ex);
+            }
+        }
+    };
+
+    @Before
+    public void setUp() throws Exception {
+        logd("setUp");
+        MockitoAnnotations.initMocks(this);
+
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+
+        HandlerThread handlerThread = new HandlerThread("SatelliteAccessControllerTest");
+        handlerThread.start();
+        mLooper = handlerThread.getLooper();
+        mTestableLooper = new TestableLooper(mLooper);
+        when(mMockContext.getSystemServiceName(LocationManager.class)).thenReturn(
+                Context.LOCATION_SERVICE);
+        when(mMockContext.getSystemServiceName(TelecomManager.class)).thenReturn(
+                Context.TELECOM_SERVICE);
+        when(mMockContext.getSystemService(LocationManager.class)).thenReturn(
+                mMockLocationManager);
+        when(mMockContext.getSystemService(TelecomManager.class)).thenReturn(
+                mMockTelecomManager);
+        mPhones = new Phone[] {mMockPhone, mMockPhone2};
+        replaceInstance(PhoneFactory.class, "sPhones", null, mPhones);
+        replaceInstance(SatelliteController.class, "sInstance", null,
+                mMockSatelliteController);
+        replaceInstance(TelephonyCountryDetector.class, "sInstance", null,
+                mMockCountryDetector);
+        when(mMockContext.getResources()).thenReturn(mMockResources);
+        when(mMockResources.getStringArray(
+                com.android.internal.R.array.config_oem_enabled_satellite_country_codes))
+                .thenReturn(TEST_SATELLITE_COUNTRY_CODES);
+        when(mMockResources.getBoolean(
+                com.android.internal.R.bool.config_oem_enabled_satellite_access_allow))
+                .thenReturn(TEST_SATELLITE_ALLOW);
+        when(mMockResources.getString(
+                com.android.internal.R.string.config_oem_enabled_satellite_s2cell_file))
+                .thenReturn(TEST_SATELLITE_S2_FILE);
+        when(mMockResources.getInteger(com.android.internal.R.integer
+                .config_oem_enabled_satellite_location_fresh_duration))
+                .thenReturn(TEST_LOCATION_FRESH_DURATION_SECONDS);
+        doNothing().when(mMockSatelliteController)
+                .requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                        anyInt(), any(ResultReceiver.class));
+
+        when(mMockLocationManager.getProviders(true)).thenReturn(LOCATION_PROVIDERS);
+        when(mMockLocationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER))
+                .thenReturn(mMockLocation0);
+        when(mMockLocationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER))
+                .thenReturn(mMockLocation1);
+        when(mMockLocation0.getLatitude()).thenReturn(0.0);
+        when(mMockLocation0.getLongitude()).thenReturn(0.0);
+        when(mMockLocation1.getLatitude()).thenReturn(1.0);
+        when(mMockLocation1.getLongitude()).thenReturn(1.0);
+        when(mMockSatelliteOnDeviceAccessController.isSatCommunicationAllowedAtLocation(
+                any(SatelliteOnDeviceAccessController.LocationToken.class))).thenReturn(true);
+
+        mSatelliteAccessControllerUT = new TestSatelliteAccessController(mMockContext,
+                mMockFeatureFlags, mLooper, mMockLocationManager, mMockTelecomManager,
+                mMockSatelliteOnDeviceAccessController, mMockSatS2File);
+        mTestableLooper.processAllMessages();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        logd("tearDown");
+        if (mTestableLooper != null) {
+            mTestableLooper.destroy();
+            mTestableLooper = null;
+        }
+
+        if (mLooper != null) {
+            mLooper.quit();
+            mLooper = null;
+        }
+    }
+
+    @Test
+    public void testGetInstance() {
+        SatelliteAccessController inst1 =
+                SatelliteAccessController.getOrCreateInstance(mMockContext, mMockFeatureFlags);
+        SatelliteAccessController inst2 =
+                SatelliteAccessController.getOrCreateInstance(mMockContext, mMockFeatureFlags);
+        assertEquals(inst1, inst2);
+    }
+
+    @Test
+    public void testRequestIsSatelliteCommunicationAllowedForCurrentLocation() throws Exception {
+        // OEM-enabled satellite is not supported
+        when(mMockFeatureFlags.oemEnabledSatelliteFlag()).thenReturn(false);
+        mSatelliteAccessControllerUT.requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                SUB_ID, mSatelliteAllowedReceiver);
+        mTestableLooper.processAllMessages();
+        assertTrue(waitForRequestIsSatelliteAllowedForCurrentLocationResult(
+                mSatelliteAllowedSemaphore, 1));
+        assertEquals(SATELLITE_RESULT_REQUEST_NOT_SUPPORTED, mQueriedSatelliteAllowedResultCode);
+
+        // OEM-enabled satellite is supported, but SatelliteController returns error for the query
+        when(mMockFeatureFlags.oemEnabledSatelliteFlag()).thenReturn(true);
+        mSatelliteAccessControllerUT.requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                SUB_ID, mSatelliteAllowedReceiver);
+        mTestableLooper.processAllMessages();
+        verify(mMockSatelliteController).requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                anyInt(), mResultReceiverFromSatelliteControllerCaptor.capture());
+
+        clearInvocations(mMockSatelliteController);
+        mSatelliteAccessControllerUT.requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                SUB_ID, mSatelliteAllowedReceiver2);
+        mTestableLooper.processAllMessages();
+        verify(mMockSatelliteController, never())
+                .requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                        anyInt(), any(ResultReceiver.class));
+
+        sendSatelliteAllowResultFromSatelliteController(SATELLITE_RESULT_ERROR, null);
+        assertTrue(waitForRequestIsSatelliteAllowedForCurrentLocationResult(
+                mSatelliteAllowedSemaphore, 1));
+        assertTrue(waitForRequestIsSatelliteAllowedForCurrentLocationResult(
+                mSatelliteAllowedSemaphore2, 1));
+        assertEquals(SATELLITE_RESULT_ERROR, mQueriedSatelliteAllowedResultCode);
+        assertEquals(SATELLITE_RESULT_ERROR, mQueriedSatelliteAllowedResultCode2);
+        assertFalse(mQueriedSatelliteAllowed);
+        assertFalse(mQueriedSatelliteAllowed2);
+
+        // SatelliteController returns success result but the result bundle does not have
+        // KEY_SATELLITE_COMMUNICATION_ALLOWED
+        clearAllInvocations();
+        mSatelliteAccessControllerUT.requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                SUB_ID, mSatelliteAllowedReceiver);
+        mTestableLooper.processAllMessages();
+        verify(mMockSatelliteController).requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                anyInt(), mResultReceiverFromSatelliteControllerCaptor.capture());
+        sendSatelliteAllowResultFromSatelliteController(SATELLITE_RESULT_SUCCESS, null);
+        assertTrue(waitForRequestIsSatelliteAllowedForCurrentLocationResult(
+                mSatelliteAllowedSemaphore, 1));
+        assertEquals(SATELLITE_RESULT_SUCCESS, mQueriedSatelliteAllowedResultCode);
+        assertFalse(mQueriedSatelliteAllowed);
+
+        // SatelliteController returns disallowed result
+        clearAllInvocations();
+        mSatelliteAccessControllerUT.requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                SUB_ID, mSatelliteAllowedReceiver);
+        mTestableLooper.processAllMessages();
+        verify(mMockSatelliteController).requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                anyInt(), mResultReceiverFromSatelliteControllerCaptor.capture());
+        sendSatelliteAllowResultFromSatelliteController(SATELLITE_RESULT_SUCCESS, false);
+        assertTrue(waitForRequestIsSatelliteAllowedForCurrentLocationResult(
+                mSatelliteAllowedSemaphore, 1));
+        assertEquals(SATELLITE_RESULT_SUCCESS, mQueriedSatelliteAllowedResultCode);
+        assertFalse(mQueriedSatelliteAllowed);
+
+        // SatelliteController returns allowed result. Network country codes are available, but one
+        // country code is not in the allowed list
+        clearAllInvocations();
+        when(mMockCountryDetector.getCurrentNetworkCountryIso()).thenReturn(listOf("US", "IN"));
+        mSatelliteAccessControllerUT.requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                SUB_ID, mSatelliteAllowedReceiver);
+        mTestableLooper.processAllMessages();
+        verify(mMockSatelliteController).requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                anyInt(), mResultReceiverFromSatelliteControllerCaptor.capture());
+        sendSatelliteAllowResultFromSatelliteController(SATELLITE_RESULT_SUCCESS, true);
+        assertTrue(waitForRequestIsSatelliteAllowedForCurrentLocationResult(
+                mSatelliteAllowedSemaphore, 1));
+        assertEquals(SATELLITE_RESULT_SUCCESS, mQueriedSatelliteAllowedResultCode);
+        assertFalse(mQueriedSatelliteAllowed);
+
+        // SatelliteController returns allowed result. Network country codes are available, and all
+        // country codes are in the allowed list
+        clearAllInvocations();
+        when(mMockCountryDetector.getCurrentNetworkCountryIso()).thenReturn(listOf("US", "CA"));
+        mSatelliteAccessControllerUT.requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                SUB_ID, mSatelliteAllowedReceiver);
+        mTestableLooper.processAllMessages();
+        verify(mMockSatelliteController).requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                anyInt(), mResultReceiverFromSatelliteControllerCaptor.capture());
+        sendSatelliteAllowResultFromSatelliteController(SATELLITE_RESULT_SUCCESS, true);
+        assertTrue(waitForRequestIsSatelliteAllowedForCurrentLocationResult(
+                mSatelliteAllowedSemaphore, 1));
+        assertEquals(SATELLITE_RESULT_SUCCESS, mQueriedSatelliteAllowedResultCode);
+        assertTrue(mQueriedSatelliteAllowed);
+
+        // SatelliteController returns allowed result. Network country codes are not available.
+        // TelecomManager.isInEmergencyCall() returns true. On-device access controller will be
+        // used. Last known location is available and fresh.
+        clearAllInvocations();
+        when(mMockCountryDetector.getCurrentNetworkCountryIso()).thenReturn(EMPTY_STRING_LIST);
+        when(mMockTelecomManager.isInEmergencyCall()).thenReturn(true);
+        mSatelliteAccessControllerUT.elapsedRealtimeNanos = TEST_LOCATION_FRESH_DURATION_NANOS + 1;
+        when(mMockLocation0.getElapsedRealtimeNanos()).thenReturn(2L);
+        when(mMockLocation1.getElapsedRealtimeNanos()).thenReturn(0L);
+        mSatelliteAccessControllerUT.requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                SUB_ID, mSatelliteAllowedReceiver);
+        mTestableLooper.processAllMessages();
+        verify(mMockSatelliteController).requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                anyInt(), mResultReceiverFromSatelliteControllerCaptor.capture());
+        sendSatelliteAllowResultFromSatelliteController(SATELLITE_RESULT_SUCCESS, true);
+        assertTrue(
+                mSatelliteAccessControllerUT.isKeepOnDeviceAccessControllerResourcesTimerStarted());
+        verify(mMockSatelliteOnDeviceAccessController).isSatCommunicationAllowedAtLocation(
+                any(SatelliteOnDeviceAccessController.LocationToken.class));
+        assertTrue(waitForRequestIsSatelliteAllowedForCurrentLocationResult(
+                mSatelliteAllowedSemaphore, 1));
+        assertEquals(SATELLITE_RESULT_SUCCESS, mQueriedSatelliteAllowedResultCode);
+        assertTrue(mQueriedSatelliteAllowed);
+
+        // Move time forward and verify resources are cleaned up
+        clearAllInvocations();
+        mTestableLooper.moveTimeForward(mSatelliteAccessControllerUT
+                .getKeepOnDeviceAccessControllerResourcesTimeoutMillis());
+        mTestableLooper.processAllMessages();
+        assertFalse(
+                mSatelliteAccessControllerUT.isKeepOnDeviceAccessControllerResourcesTimerStarted());
+        assertTrue(mSatelliteAccessControllerUT.isSatelliteOnDeviceAccessControllerReset());
+        verify(mMockSatelliteOnDeviceAccessController).close();
+
+        // Restore SatelliteOnDeviceAccessController for next verification
+        mSatelliteAccessControllerUT.setSatelliteOnDeviceAccessController(
+                mMockSatelliteOnDeviceAccessController);
+
+        // SatelliteController returns allowed result. Network country codes are not available.
+        // TelecomManager.isInEmergencyCall() returns false. Phone0 is in ECM. On-device access
+        // controller will be used. Last known location is not fresh.
+        clearAllInvocations();
+        when(mMockCountryDetector.getCurrentNetworkCountryIso()).thenReturn(EMPTY_STRING_LIST);
+        when(mMockTelecomManager.isInEmergencyCall()).thenReturn(false);
+        when(mMockPhone.isInEcm()).thenReturn(true);
+        mSatelliteAccessControllerUT.elapsedRealtimeNanos = TEST_LOCATION_FRESH_DURATION_NANOS + 1;
+        when(mMockLocation0.getElapsedRealtimeNanos()).thenReturn(0L);
+        when(mMockLocation1.getElapsedRealtimeNanos()).thenReturn(0L);
+        mSatelliteAccessControllerUT.requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                SUB_ID, mSatelliteAllowedReceiver);
+        mTestableLooper.processAllMessages();
+        verify(mMockSatelliteController).requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                anyInt(), mResultReceiverFromSatelliteControllerCaptor.capture());
+        sendSatelliteAllowResultFromSatelliteController(SATELLITE_RESULT_SUCCESS, true);
+        assertFalse(
+                mSatelliteAccessControllerUT.isKeepOnDeviceAccessControllerResourcesTimerStarted());
+        verify(mMockLocationManager).getCurrentLocation(eq(LocationManager.GPS_PROVIDER),
+                any(LocationRequest.class), mLocationRequestCancellationSignalCaptor.capture(),
+                any(Executor.class), mLocationRequestConsumerCaptor.capture());
+        assertTrue(mSatelliteAccessControllerUT.isWaitForCurrentLocationTimerStarted());
+        sendLocationRequestResult(mMockLocation0);
+        assertFalse(mSatelliteAccessControllerUT.isWaitForCurrentLocationTimerStarted());
+        // The LocationToken should be already in the cache
+        verify(mMockSatelliteOnDeviceAccessController, never()).isSatCommunicationAllowedAtLocation(
+                any(SatelliteOnDeviceAccessController.LocationToken.class));
+        assertTrue(waitForRequestIsSatelliteAllowedForCurrentLocationResult(
+                mSatelliteAllowedSemaphore, 1));
+        assertEquals(SATELLITE_RESULT_SUCCESS, mQueriedSatelliteAllowedResultCode);
+        assertTrue(mQueriedSatelliteAllowed);
+
+        // Timed out to wait for current location. No cached country codes.
+        clearAllInvocations();
+        when(mMockCountryDetector.getCurrentNetworkCountryIso()).thenReturn(EMPTY_STRING_LIST);
+        when(mMockTelecomManager.isInEmergencyCall()).thenReturn(false);
+        when(mMockPhone.isInEcm()).thenReturn(true);
+        mSatelliteAccessControllerUT.elapsedRealtimeNanos = TEST_LOCATION_FRESH_DURATION_NANOS + 1;
+        when(mMockLocation0.getElapsedRealtimeNanos()).thenReturn(0L);
+        when(mMockLocation1.getElapsedRealtimeNanos()).thenReturn(0L);
+        when(mMockCountryDetector.getCachedLocationCountryIsoInfo()).thenReturn(new Pair<>("", 0L));
+        when(mMockCountryDetector.getCachedNetworkCountryIsoInfo()).thenReturn(new HashMap<>());
+        mSatelliteAccessControllerUT.requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                SUB_ID, mSatelliteAllowedReceiver);
+        mTestableLooper.processAllMessages();
+        verify(mMockSatelliteController).requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                anyInt(), mResultReceiverFromSatelliteControllerCaptor.capture());
+        sendSatelliteAllowResultFromSatelliteController(SATELLITE_RESULT_SUCCESS, true);
+        assertFalse(
+                mSatelliteAccessControllerUT.isKeepOnDeviceAccessControllerResourcesTimerStarted());
+        verify(mMockLocationManager).getCurrentLocation(anyString(), any(LocationRequest.class),
+                any(CancellationSignal.class), any(Executor.class), any(Consumer.class));
+        assertTrue(mSatelliteAccessControllerUT.isWaitForCurrentLocationTimerStarted());
+        // Timed out
+        mTestableLooper.moveTimeForward(
+                mSatelliteAccessControllerUT.getWaitForCurrentLocationTimeoutMillis());
+        mTestableLooper.processAllMessages();
+        assertFalse(mSatelliteAccessControllerUT.isWaitForCurrentLocationTimerStarted());
+        verify(mMockSatelliteOnDeviceAccessController, never()).isSatCommunicationAllowedAtLocation(
+                any(SatelliteOnDeviceAccessController.LocationToken.class));
+        verifyCountryDetectorApisCalled();
+        assertTrue(waitForRequestIsSatelliteAllowedForCurrentLocationResult(
+                mSatelliteAllowedSemaphore, 1));
+        assertEquals(SATELLITE_RESULT_SUCCESS,
+                mQueriedSatelliteAllowedResultCode);
+        assertFalse(mQueriedSatelliteAllowed);
+
+        // SatelliteController returns allowed result. Network country codes are not available.
+        // TelecomManager.isInEmergencyCall() returns false. No phone is in ECM. Last known location
+        // is not fresh. Cached country codes should be used for verifying satellite allow. No
+        // cached country codes are available.
+        clearAllInvocations();
+        when(mMockCountryDetector.getCurrentNetworkCountryIso()).thenReturn(EMPTY_STRING_LIST);
+        when(mMockCountryDetector.getCachedLocationCountryIsoInfo()).thenReturn(new Pair<>("", 0L));
+        when(mMockCountryDetector.getCachedNetworkCountryIsoInfo()).thenReturn(new HashMap<>());
+        when(mMockTelecomManager.isInEmergencyCall()).thenReturn(false);
+        when(mMockPhone.isInEcm()).thenReturn(false);
+        when(mMockPhone2.isInEcm()).thenReturn(false);
+        mSatelliteAccessControllerUT.elapsedRealtimeNanos = TEST_LOCATION_FRESH_DURATION_NANOS + 1;
+        when(mMockLocation0.getElapsedRealtimeNanos()).thenReturn(0L);
+        when(mMockLocation1.getElapsedRealtimeNanos()).thenReturn(0L);
+        mSatelliteAccessControllerUT.requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                SUB_ID, mSatelliteAllowedReceiver);
+        mTestableLooper.processAllMessages();
+        verify(mMockSatelliteController).requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                anyInt(), mResultReceiverFromSatelliteControllerCaptor.capture());
+        sendSatelliteAllowResultFromSatelliteController(SATELLITE_RESULT_SUCCESS, true);
+        verify(mMockLocationManager, never()).getCurrentLocation(anyString(),
+                any(LocationRequest.class), any(CancellationSignal.class), any(Executor.class),
+                any(Consumer.class));
+        verify(mMockSatelliteOnDeviceAccessController, never()).isSatCommunicationAllowedAtLocation(
+                any(SatelliteOnDeviceAccessController.LocationToken.class));
+        verifyCountryDetectorApisCalled();
+        assertTrue(waitForRequestIsSatelliteAllowedForCurrentLocationResult(
+                mSatelliteAllowedSemaphore, 1));
+        assertEquals(SATELLITE_RESULT_SUCCESS, mQueriedSatelliteAllowedResultCode);
+        assertFalse(mQueriedSatelliteAllowed);
+
+        // SatelliteController returns allowed result. Network country codes are not available.
+        // TelecomManager.isInEmergencyCall() returns false. No phone is in ECM. Last known location
+        // is not fresh. Cached country codes should be used for verifying satellite allow. Cached
+        // country codes are available.
+        clearAllInvocations();
+        when(mMockCountryDetector.getCurrentNetworkCountryIso()).thenReturn(EMPTY_STRING_LIST);
+        when(mMockCountryDetector.getCachedLocationCountryIsoInfo())
+                .thenReturn(new Pair<>("US", 5L));
+        Map<String, Long> cachedNetworkCountryCodes = new HashMap<>();
+        cachedNetworkCountryCodes.put("UK", 1L);
+        cachedNetworkCountryCodes.put("US", 3L);
+        when(mMockCountryDetector.getCachedNetworkCountryIsoInfo())
+                .thenReturn(cachedNetworkCountryCodes);
+        when(mMockTelecomManager.isInEmergencyCall()).thenReturn(false);
+        when(mMockPhone.isInEcm()).thenReturn(false);
+        when(mMockPhone2.isInEcm()).thenReturn(false);
+        mSatelliteAccessControllerUT.elapsedRealtimeNanos = TEST_LOCATION_FRESH_DURATION_NANOS + 1;
+        when(mMockLocation0.getElapsedRealtimeNanos()).thenReturn(0L);
+        when(mMockLocation1.getElapsedRealtimeNanos()).thenReturn(0L);
+        mSatelliteAccessControllerUT.requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                SUB_ID, mSatelliteAllowedReceiver);
+        mTestableLooper.processAllMessages();
+        verify(mMockSatelliteController).requestIsSatelliteCommunicationAllowedForCurrentLocation(
+                anyInt(), mResultReceiverFromSatelliteControllerCaptor.capture());
+        sendSatelliteAllowResultFromSatelliteController(SATELLITE_RESULT_SUCCESS, true);
+        verify(mMockLocationManager, never()).getCurrentLocation(anyString(),
+                any(LocationRequest.class), any(CancellationSignal.class), any(Executor.class),
+                any(Consumer.class));
+        verify(mMockSatelliteOnDeviceAccessController, never()).isSatCommunicationAllowedAtLocation(
+                any(SatelliteOnDeviceAccessController.LocationToken.class));
+        verifyCountryDetectorApisCalled();
+        assertTrue(waitForRequestIsSatelliteAllowedForCurrentLocationResult(
+                mSatelliteAllowedSemaphore, 1));
+        assertEquals(SATELLITE_RESULT_SUCCESS, mQueriedSatelliteAllowedResultCode);
+        assertTrue(mQueriedSatelliteAllowed);
+    }
+
+    private void clearAllInvocations() {
+        clearInvocations(mMockSatelliteController);
+        clearInvocations(mMockSatelliteOnDeviceAccessController);
+        clearInvocations(mMockLocationManager);
+        clearInvocations(mMockCountryDetector);
+    }
+
+    private void verifyCountryDetectorApisCalled() {
+        verify(mMockCountryDetector).getCurrentNetworkCountryIso();
+        verify(mMockCountryDetector).getCachedLocationCountryIsoInfo();
+        verify(mMockCountryDetector).getCachedLocationCountryIsoInfo();
+    }
+
+    private boolean waitForRequestIsSatelliteAllowedForCurrentLocationResult(Semaphore semaphore,
+            int expectedNumberOfEvents) {
+        for (int i = 0; i < expectedNumberOfEvents; i++) {
+            try {
+                if (!semaphore.tryAcquire(TIMEOUT, TimeUnit.MILLISECONDS)) {
+                    logd("Timeout to receive "
+                            + "requestIsSatelliteCommunicationAllowedForCurrentLocation()"
+                            + " callback");
+                    return false;
+                }
+            } catch (Exception ex) {
+                logd("waitForRequestIsSatelliteSupportedResult: Got exception=" + ex);
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private void sendSatelliteAllowResultFromSatelliteController(
+            int resultCode, Boolean satelliteAllowed) {
+        Bundle bundle = null;
+        if (resultCode == SATELLITE_RESULT_SUCCESS) {
+            bundle = new Bundle();
+            if (satelliteAllowed != null) {
+                bundle.putBoolean(KEY_SATELLITE_COMMUNICATION_ALLOWED, satelliteAllowed);
+            }
+        }
+        mResultReceiverFromSatelliteControllerCaptor.getValue().send(resultCode, bundle);
+        mTestableLooper.processAllMessages();
+    }
+
+    private void sendLocationRequestResult(Location location) {
+        mLocationRequestConsumerCaptor.getValue().accept(location);
+        mTestableLooper.processAllMessages();
+    }
+
+    @SafeVarargs
+    private static <E> List<E> listOf(E... values) {
+        return Arrays.asList(values);
+    }
+
+    private static void logd(String message) {
+        Log.d(TAG, message);
+    }
+
+    private static void replaceInstance(final Class c,
+            final String instanceName, final Object obj, final Object newValue) throws Exception {
+        Field field = c.getDeclaredField(instanceName);
+        field.setAccessible(true);
+        field.set(obj, newValue);
+    }
+
+    private static class TestSatelliteAccessController extends SatelliteAccessController {
+        public long elapsedRealtimeNanos = 0;
+
+        /**
+         * Create a SatelliteAccessController instance.
+         *
+         * @param context                           The context associated with the
+         *                                          {@link SatelliteAccessController} instance.
+         * @param featureFlags                      The FeatureFlags that are supported.
+         * @param looper                            The Looper to run the SatelliteAccessController
+         *                                          on.
+         * @param locationManager                   The LocationManager for querying current
+         *                                          location of the
+         *                                          device.
+         * @param satelliteOnDeviceAccessController The on-device satellite access controller
+         *                                          instance.
+         */
+        protected TestSatelliteAccessController(Context context, FeatureFlags featureFlags,
+                Looper looper, LocationManager locationManager, TelecomManager telecomManager,
+                SatelliteOnDeviceAccessController satelliteOnDeviceAccessController,
+                File s2CellFile) {
+            super(context, featureFlags, looper, locationManager, telecomManager,
+                    satelliteOnDeviceAccessController, s2CellFile);
+        }
+
+        @Override
+        protected long getElapsedRealtimeNanos() {
+            return elapsedRealtimeNanos;
+        }
+
+        public boolean isKeepOnDeviceAccessControllerResourcesTimerStarted() {
+            return hasMessages(EVENT_KEEP_ON_DEVICE_ACCESS_CONTROLLER_RESOURCES_TIMEOUT);
+        }
+
+        public boolean isSatelliteOnDeviceAccessControllerReset() {
+            synchronized (mLock) {
+                return (mSatelliteOnDeviceAccessController == null);
+            }
+        }
+
+        public void setSatelliteOnDeviceAccessController(
+                @Nullable SatelliteOnDeviceAccessController accessController) {
+            synchronized (mLock) {
+                mSatelliteOnDeviceAccessController = accessController;
+            }
+        }
+
+        public long getKeepOnDeviceAccessControllerResourcesTimeoutMillis() {
+            return KEEP_ON_DEVICE_ACCESS_CONTROLLER_RESOURCES_TIMEOUT_MILLIS;
+        }
+
+        public long getWaitForCurrentLocationTimeoutMillis() {
+            return WAIT_FOR_CURRENT_LOCATION_TIMEOUT_MILLIS;
+        }
+
+        public boolean isWaitForCurrentLocationTimerStarted() {
+            return hasMessages(EVENT_WAIT_FOR_CURRENT_LOCATION_TIMEOUT);
+        }
+    }
+}
diff --git a/utils/satellite/README.md b/utils/satellite/README.md
index e219823..77ee0fb 100644
--- a/utils/satellite/README.md
+++ b/utils/satellite/README.md
@@ -8,7 +8,7 @@
 - `src/write` S2 write code used by tools to write the s2 cells into a
   binary file. This code is also used by `TeleServiceTests`.
 - `src/readonly` S2 read-only code used by the above read-write code and the class
- `S2RangeFileBasedSatelliteLocationLookup`.
+ `S2RangeSatelliteOnDeviceAccessController`.
 
 `tools`
 - `src/main` Contains the tools for generating binary satellite s2 file, and tools
@@ -29,7 +29,7 @@
   list of S2 cells ID.
 - Command: `$satellite_createsats2file --input-file <s2cells.txt> --s2-level <12>
   --is-allowed-list <true> --output-file <sats2.dat>`
-  - `--input-file` Each line in the file contains a `signed-64bit` number which represents
+  - `--input-file` Each line in the file contains a `unsigned-64bit` number which represents
     the ID of a S2 cell.
   - `--s2-level` The S2 level of all the cells in the input file.
   - `--is-allowed-list` Should be either `trrue` or `false`
@@ -51,7 +51,7 @@
   - [(prefix=0b100_11111111, suffix=1000), (prefix=0b100_11111111, suffix=2000))
   - [(prefix=0b100_11111111, suffix=2000), (prefix=0b100_11111111, suffix=3000))
   - [(prefix=0b101_11111111, suffix=1000), (prefix=0b101_11111111, suffix=2000))
-- Run the test tool: `$satellite_createtestsats2file /tmp/foo.dat`
+- Run the test tool: `satellite_createsats2file_test /tmp/foo.dat`
   - This command will generate the binary satellite S2 cell file `/tmp/foo.dat` with
   the above S2 ranges.
 
@@ -59,4 +59,9 @@
 - Dump the input binary satellite S2 cell file into human-readable text format.
 - Run the tool: `$satellite_dumpsats2file /tmp/foo.dat /tmp/foo`
   - `/tmp/foo.dat` Input binary satellite S2 cell file.
-  - `/tmp/foo` Output directory which contains the output text files.
\ No newline at end of file
+  - `/tmp/foo` Output directory which contains the output text files.
+
+`satellite_location_lookup`
+- Check if a location is present in the input satellite S2 file.
+- Run the tool: `$satellite_location_lookup --input-file <...> --lat-degrees <...>
+  --lng-degrees <...>`
\ No newline at end of file
diff --git a/utils/satellite/s2storage/src/testutils/java/com/android/telephony/sats2range/testutils/TestUtils.java b/utils/satellite/s2storage/src/testutils/java/com/android/telephony/sats2range/testutils/TestUtils.java
index 4b8a026..3cf2c78 100644
--- a/utils/satellite/s2storage/src/testutils/java/com/android/telephony/sats2range/testutils/TestUtils.java
+++ b/utils/satellite/s2storage/src/testutils/java/com/android/telephony/sats2range/testutils/TestUtils.java
@@ -107,19 +107,22 @@
         try (PrintStream printer = new PrintStream(outputFile)) {
             // Range 1
             for (int suffix = 1000; suffix < 2000; suffix++) {
-                printer.println(String.valueOf(fileFormat.createCellId(0b100_11111111, suffix)));
+                printer.println(
+                        Long.toUnsignedString(fileFormat.createCellId(0b100_11111111, suffix)));
             }
 
             // Range 2
             for (int suffix = 2001; suffix < 3000; suffix++) {
-                printer.println(String.valueOf(fileFormat.createCellId(0b100_11111111, suffix)));
+                printer.println(
+                        Long.toUnsignedString(fileFormat.createCellId(0b100_11111111, suffix)));
             }
 
             // Range 3
             for (int suffix = 1000; suffix < 2000; suffix++) {
-                printer.println(String.valueOf(fileFormat.createCellId(0b101_11111111, suffix)));
+                printer.println(
+                        Long.toUnsignedString(fileFormat.createCellId(0b101_11111111, suffix)));
             }
-            printer.print(String.valueOf(fileFormat.createCellId(0b101_11111111, 2000)));
+            printer.print(Long.toUnsignedString(fileFormat.createCellId(0b101_11111111, 2000)));
 
             printer.close();
         }
@@ -130,13 +133,13 @@
             File outputFile, SatS2RangeFileFormat fileFormat) throws Exception {
         try (PrintStream printer = new PrintStream(outputFile)) {
             // Valid line
-            printer.println(String.valueOf(fileFormat.createCellId(0b100_11111111, 100)));
+            printer.println(Long.toUnsignedString(fileFormat.createCellId(0b100_11111111, 100)));
 
             // Invalid line
             printer.print("Invalid line");
 
             // Another valid line
-            printer.println(String.valueOf(fileFormat.createCellId(0b100_11111111, 200)));
+            printer.println(Long.toUnsignedString(fileFormat.createCellId(0b100_11111111, 200)));
 
             printer.close();
         }
diff --git a/utils/satellite/tools/Android.bp b/utils/satellite/tools/Android.bp
index 9aacdd9..d48b911 100644
--- a/utils/satellite/tools/Android.bp
+++ b/utils/satellite/tools/Android.bp
@@ -39,6 +39,15 @@
     ],
 }
 
+// A tool to look up a location in the input binary satellite S2 file.
+java_binary_host {
+    name: "satellite_location_lookup",
+    main_class: "com.android.telephony.tools.sats2.SatS2LocationLookup",
+    static_libs: [
+        "satellite-s2storage-tools",
+    ],
+}
+
 // A tool to create a test satellite S2 file.
 java_binary_host {
     name: "satellite_createsats2file_test",
diff --git a/utils/satellite/tools/src/main/java/com/android/telephony/tools/sats2/SatS2FileCreator.java b/utils/satellite/tools/src/main/java/com/android/telephony/tools/sats2/SatS2FileCreator.java
index b701a7b..bc25d6b 100644
--- a/utils/satellite/tools/src/main/java/com/android/telephony/tools/sats2/SatS2FileCreator.java
+++ b/utils/satellite/tools/src/main/java/com/android/telephony/tools/sats2/SatS2FileCreator.java
@@ -30,18 +30,19 @@
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Objects;
 import java.util.Scanner;
+import java.util.Set;
 import java.util.concurrent.TimeUnit;
-import java.util.stream.Collectors;
 
 /** A util class for creating a satellite S2 file from the list of S2 cells. */
 public final class SatS2FileCreator {
     /**
      * @param inputFile The input text file containing the list of S2 Cell IDs. Each line in the
-     *                  file contains a number in the range of a signed-64bit number which
+     *                  file contains a number in the range of an unsigned-64bit number which
      *                  represents the ID of a S2 cell.
      * @param s2Level The S2 level of all S2 cells in the input file.
      * @param isAllowedList {@code true} means the input file contains an allowed list of S2 cells.
@@ -57,12 +58,12 @@
         System.out.println("Number of S2 cells read from file:" + s2Cells.size());
 
         // Convert the input list of S2 Cells into the list of sorted S2CellId
-        List<S2CellId> sortedS2CellIds = s2Cells.stream()
-                .map(x -> new S2CellId(x))
-                .collect(Collectors.toList());
+        System.out.println("Denormalizing S2 Cell IDs to the expected s2 level=" + s2Level);
+        List<S2CellId> sortedS2CellIds = denormalize(s2Cells, s2Level);
         // IDs of S2CellId are converted to unsigned long numbers, which will be then used to
         // compare S2CellId.
         Collections.sort(sortedS2CellIds);
+        System.out.println("Number of S2 cell IDs:" + sortedS2CellIds.size());
 
         // Compress the list of S2CellId into S2 ranges
         List<SatS2Range> satS2Ranges = createSatS2Ranges(sortedS2CellIds, s2Level);
@@ -132,25 +133,56 @@
      * Read a list of S2 cells from the inputFile.
      *
      * @param inputFile A file containing the list of S2 cells. Each line in the inputFile contains
-     *                  a long number - the ID of a S2 cell.
+     *                  an unsigned long number - the ID of a S2 cell.
      * @return A list of S2 cells.
      */
     private static List<Long> readS2CellsFromFile(String inputFile) throws Exception {
         List<Long> s2Cells = new ArrayList();
         InputStream inputStream = new FileInputStream(inputFile);
         try (Scanner scanner = new Scanner(inputStream, StandardCharsets.UTF_8.name())) {
-            while (scanner.hasNextLong()) {
-                s2Cells.add(scanner.nextLong());
-            }
-            if (scanner.hasNextLine()) {
-                throw new IllegalStateException("Input s2 cell file has invalid format, "
-                        + "current line=" + scanner.nextLine());
+            while (scanner.hasNextLine()) {
+                String line = scanner.nextLine();
+                try {
+                    s2Cells.add(Long.parseUnsignedLong(line));
+                } catch (Exception ex) {
+                    throw new IllegalStateException("Input s2 cell file has invalid format, "
+                            + "current line=" + line);
+                }
             }
         }
         return s2Cells;
     }
 
     /**
+     * Convert the list of S2 Cell numbers into the list of S2 Cell IDs at the expected level.
+     */
+    private static List<S2CellId> denormalize(List<Long> s2CellNumbers, int s2Level) {
+        Set<S2CellId> result = new HashSet<>();
+        for (long s2CellNumber : s2CellNumbers) {
+            S2CellId s2CellId = new S2CellId(s2CellNumber);
+            if (s2CellId.level() == s2Level) {
+                if (!result.contains(s2CellId)) {
+                    result.add(s2CellId);
+                }
+            } else if (s2CellId.level() < s2Level) {
+                S2CellId childEnd = s2CellId.childEnd(s2Level);
+                for (s2CellId = s2CellId.childBegin(s2Level); !s2CellId.equals(childEnd);
+                        s2CellId = s2CellId.next()) {
+                    if (!result.contains(s2CellId)) {
+                        result.add(s2CellId);
+                    }
+                }
+            } else {
+                S2CellId parent = s2CellId.parent(s2Level);
+                if (!result.contains(parent)) {
+                    result.add(parent);
+                }
+            }
+        }
+        return new ArrayList(result);
+    }
+
+    /**
      * Compress the list of sorted S2CellId into S2 ranges.
      *
      * @param sortedS2CellIds List of S2CellId sorted in ascending order.
diff --git a/utils/satellite/tools/src/main/java/com/android/telephony/tools/sats2/SatS2LocationLookup.java b/utils/satellite/tools/src/main/java/com/android/telephony/tools/sats2/SatS2LocationLookup.java
new file mode 100644
index 0000000..444ff8d
--- /dev/null
+++ b/utils/satellite/tools/src/main/java/com/android/telephony/tools/sats2/SatS2LocationLookup.java
@@ -0,0 +1,77 @@
+/*
+ * 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.telephony.tools.sats2;
+
+import com.android.telephony.sats2range.read.SatS2RangeFileReader;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.google.common.geometry.S2CellId;
+import com.google.common.geometry.S2LatLng;
+
+import java.io.File;
+
+/** A util class for checking if a location is in the input satellite S2 file. */
+public final class SatS2LocationLookup {
+    /**
+     *  A util method for checking if a location is in the input satellite S2 file.
+     */
+    public static void main(String[] args) throws Exception {
+        Arguments arguments = new Arguments();
+        JCommander.newBuilder()
+                .addObject(arguments)
+                .build()
+                .parse(args);
+
+        try (SatS2RangeFileReader satS2RangeFileReader =
+                     SatS2RangeFileReader.open(new File(arguments.inputFile))) {
+            S2CellId s2CellId = getS2CellId(arguments.latDegrees, arguments.lngDegrees,
+                    satS2RangeFileReader.getS2Level());
+            System.out.println("s2CellId=" + Long.toUnsignedString(s2CellId.id()));
+            if (satS2RangeFileReader.findEntryByCellId(s2CellId.id()) == null) {
+                System.out.println("The input file does not contain the input location");
+            } else {
+                System.out.println("The input file contains the input location");
+            }
+        }
+    }
+
+    private static S2CellId getS2CellId(double latDegrees, double lngDegrees, int s2Level) {
+        // Create the leaf S2 cell containing the given S2LatLng
+        S2CellId cellId = S2CellId.fromLatLng(S2LatLng.fromDegrees(latDegrees, lngDegrees));
+
+        // Return the S2 cell at the expected S2 level
+        return cellId.parent(s2Level);
+    }
+
+    private static class Arguments {
+        @Parameter(names = "--input-file",
+                description = "sat s2 file",
+                required = true)
+        public String inputFile;
+
+        @Parameter(names = "--lat-degrees",
+                description = "lat degress of the location",
+                required = true)
+        public double latDegrees;
+
+        @Parameter(names = "--lng-degrees",
+                description = "lng degress of the location",
+                required = true)
+        public double lngDegrees;
+    }
+}