Merge "Add entitlement capability for satellite" into main
diff --git a/src/com/android/phone/PhoneInterfaceManager.java b/src/com/android/phone/PhoneInterfaceManager.java
index de14c83..5fb2d57 100644
--- a/src/com/android/phone/PhoneInterfaceManager.java
+++ b/src/com/android/phone/PhoneInterfaceManager.java
@@ -248,6 +248,7 @@
 import com.android.phone.callcomposer.CallComposerPictureTransfer;
 import com.android.phone.callcomposer.ImageData;
 import com.android.phone.satellite.accesscontrol.SatelliteAccessController;
+import com.android.phone.satellite.entitlement.SatelliteEntitlementController;
 import com.android.phone.settings.PickSmsSubscriptionActivity;
 import com.android.phone.slice.SlicePurchaseController;
 import com.android.phone.utils.CarrierAllowListInfo;
@@ -2477,6 +2478,10 @@
         PropertyInvalidatedCache.invalidateCache(TelephonyManager.CACHE_KEY_PHONE_ACCOUNT_TO_SUBID);
         publish();
         CarrierAllowListInfo.loadInstance(mApp);
+
+        // Create the SatelliteEntitlementController singleton, for using the get the
+        // entitlementStatus for satellite service.
+        SatelliteEntitlementController.make(mApp, mFeatureFlags);
     }
 
     @VisibleForTesting
diff --git a/src/com/android/phone/satellite/entitlement/SatelliteEntitlementApi.java b/src/com/android/phone/satellite/entitlement/SatelliteEntitlementApi.java
new file mode 100644
index 0000000..c856eb5
--- /dev/null
+++ b/src/com/android/phone/satellite/entitlement/SatelliteEntitlementApi.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 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.entitlement;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.PersistableBundle;
+import android.telephony.CarrierConfigManager;
+
+import com.android.libraries.entitlement.CarrierConfig;
+import com.android.libraries.entitlement.ServiceEntitlement;
+import com.android.libraries.entitlement.ServiceEntitlementException;
+import com.android.libraries.entitlement.ServiceEntitlementRequest;
+
+/**
+ * Class that sends an HTTP request to the entitlement server and processes the response to check
+ * whether satellite service can be activated.
+ * @hide
+ */
+public class SatelliteEntitlementApi {
+    @NonNull
+    private final ServiceEntitlement mServiceEntitlement;
+    private final Context mContext;
+
+    public SatelliteEntitlementApi(@NonNull Context context,
+            @NonNull PersistableBundle carrierConfig, @NonNull int subId) {
+        mContext = context;
+        mServiceEntitlement = new ServiceEntitlement(mContext,
+                getCarrierConfigFromEntitlementServerUrl(carrierConfig), subId);
+    }
+
+    /**
+     * Returns satellite entitlement result from the entitlement server.
+     * @return The SatelliteEntitlementResult
+     */
+    public SatelliteEntitlementResult checkEntitlementStatus() throws ServiceEntitlementException {
+        ServiceEntitlementRequest.Builder requestBuilder = ServiceEntitlementRequest.builder();
+        requestBuilder.setAcceptContentType(ServiceEntitlementRequest.ACCEPT_CONTENT_TYPE_JSON);
+        ServiceEntitlementRequest request = requestBuilder.build();
+
+        String response = mServiceEntitlement.queryEntitlementStatus(
+                ServiceEntitlement.APP_SATELLITE_ENTITLEMENT, request);
+        SatelliteEntitlementResponse satelliteEntitlementResponse =
+                new SatelliteEntitlementResponse(response);
+        return new SatelliteEntitlementResult(satelliteEntitlementResponse.getEntitlementStatus(),
+                satelliteEntitlementResponse.getPlmnAllowed());
+    }
+
+    @NonNull
+    private CarrierConfig getCarrierConfigFromEntitlementServerUrl(
+            @NonNull PersistableBundle carrierConfig) {
+        String entitlementServiceUrl = carrierConfig.getString(
+                CarrierConfigManager.ImsServiceEntitlement.KEY_ENTITLEMENT_SERVER_URL_STRING,
+                "");
+        return CarrierConfig.builder().setServerUrl(entitlementServiceUrl).build();
+    }
+}
diff --git a/src/com/android/phone/satellite/entitlement/SatelliteEntitlementController.java b/src/com/android/phone/satellite/entitlement/SatelliteEntitlementController.java
new file mode 100644
index 0000000..8227c84
--- /dev/null
+++ b/src/com/android/phone/satellite/entitlement/SatelliteEntitlementController.java
@@ -0,0 +1,542 @@
+/*
+ * Copyright (C) 2024 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.entitlement;
+
+import static com.android.phone.satellite.entitlement.SatelliteEntitlementResult.SATELLITE_ENTITLEMENT_STATUS_ENABLED;
+
+import static java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME;
+import static java.time.temporal.ChronoUnit.SECONDS;
+
+import android.annotation.NonNull;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PersistableBundle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.Rlog;
+import android.telephony.SubscriptionManager;
+
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.ExponentialBackoff;
+import com.android.internal.telephony.flags.FeatureFlags;
+import com.android.internal.telephony.satellite.SatelliteController;
+import com.android.internal.telephony.subscription.SubscriptionManagerService;
+import com.android.libraries.entitlement.ServiceEntitlementException;
+
+import java.time.Instant;
+import java.time.format.DateTimeParseException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * This class query the entitlement server to receive values for satellite services and passes the
+ * response to the {@link com.android.internal.telephony.satellite.SatelliteController}.
+ * @hide
+ */
+public class SatelliteEntitlementController extends Handler {
+    private static final String TAG = "SatelliteEntitlementController";
+    @NonNull private static SatelliteEntitlementController sInstance;
+    /** Message code used in handleMessage() */
+    private static final int CMD_START_QUERY_ENTITLEMENT = 1;
+    private static final int CMD_RETRY_QUERY_ENTITLEMENT = 2;
+    private static final int CMD_STOP_RETRY_QUERY_ENTITLEMENT = 3;
+
+    /** Retry on next trigger event. */
+    private static final int HTTP_RESPONSE_500 = 500;
+    /** Retry after the time specified in the “Retry-After” header. After retry count doesn't exceed
+     * MAX_RETRY_COUNT. */
+    private static final int HTTP_RESPONSE_503 = 503;
+    /** Default query refresh time is 1 month. */
+
+    private static final int DEFAULT_QUERY_REFRESH_DAYS = 30;
+    private static final long INITIAL_DELAY_MILLIS = TimeUnit.MINUTES.toMillis(10); // 10 min
+    private static final long MAX_DELAY_MILLIS = TimeUnit.DAYS.toMillis(5); // 5 days
+    private static final int MULTIPLIER = 2;
+    private static final int MAX_RETRY_COUNT = 5;
+    @NonNull private final SubscriptionManagerService mSubscriptionManagerService;
+    @NonNull private final CarrierConfigManager mCarrierConfigManager;
+    @NonNull private final CarrierConfigManager.CarrierConfigChangeListener
+            mCarrierConfigChangeListener;
+    @NonNull private final ConnectivityManager mConnectivityManager;
+    @NonNull private final ConnectivityManager.NetworkCallback mNetworkCallback;
+    @NonNull private final BroadcastReceiver mReceiver;
+    @NonNull private final Context mContext;
+    private final Object mLock = new Object();
+    /** Map key : subId, value : ExponentialBackoff. */
+    private Map<Integer, ExponentialBackoff> mExponentialBackoffPerSub = new HashMap<>();
+    /** Map key : subId, value : SatelliteEntitlementResult. */
+    private Map<Integer, SatelliteEntitlementResult> mSatelliteEntitlementResultPerSub =
+            new HashMap<>();
+    /** Map key : subId, value : the last query time to millis. */
+    private Map<Integer, Long> mLastQueryTimePerSub = new HashMap<>();
+    /** Map key : subId, value : Count the number of retries caused by the 'ExponentialBackoff' and
+     * '503 error case with the Retry-After header'. */
+    private Map<Integer, Integer> mRetryCountPerSub = new HashMap<>();
+
+    /**
+     * Create the SatelliteEntitlementController singleton instance.
+     * @param context      The Context to use to create the SatelliteEntitlementController.
+     * @param featureFlags The feature flag.
+     */
+    public static void make(@NonNull Context context, @NonNull FeatureFlags featureFlags) {
+        if (!featureFlags.carrierEnabledSatelliteFlag()) {
+            logd("carrierEnabledSatelliteFlag is disabled. don't created this.");
+            return;
+        }
+        if (sInstance == null) {
+            HandlerThread handlerThread = new HandlerThread(TAG);
+            handlerThread.start();
+            sInstance =
+                    new SatelliteEntitlementController(context, handlerThread.getLooper());
+        }
+    }
+
+    /**
+     * Create a SatelliteEntitlementController to request query to the entitlement server for
+     * satellite services and receive responses.
+     *
+     * @param context      The Context for the SatelliteEntitlementController.
+     * @param looper       The looper for the handler. It does not run on main thread.
+     */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    public SatelliteEntitlementController(@NonNull Context context, @NonNull Looper looper) {
+        super(looper);
+        mContext = context;
+        mSubscriptionManagerService = SubscriptionManagerService.getInstance();
+        mCarrierConfigManager = context.getSystemService(CarrierConfigManager.class);
+        mCarrierConfigChangeListener = (slotIndex, subId, carrierId, specificCarrierId) ->
+                handleCarrierConfigChanged(slotIndex, subId, carrierId, specificCarrierId);
+        mCarrierConfigManager.registerCarrierConfigChangeListener(this::post,
+                mCarrierConfigChangeListener);
+        mConnectivityManager = context.getSystemService(ConnectivityManager.class);
+        mNetworkCallback = new ConnectivityManager.NetworkCallback() {
+            @Override
+            public void onAvailable(Network network) {
+                handleInternetConnected();
+            }
+
+            @Override
+            public void onLost(Network network) {
+                handleInternetDisconnected();
+            }
+        };
+        NetworkRequest networkrequest = new NetworkRequest.Builder()
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET).build();
+        mConnectivityManager.registerNetworkCallback(networkrequest, mNetworkCallback, this);
+        mReceiver = new SatelliteEntitlementControllerReceiver();
+        IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        context.registerReceiver(mReceiver, intentFilter);
+    }
+
+    @Override
+    public void handleMessage(@NonNull Message msg) {
+        switch (msg.what) {
+            case CMD_START_QUERY_ENTITLEMENT:
+                handleCmdStartQueryEntitlement();
+                break;
+            case CMD_RETRY_QUERY_ENTITLEMENT:
+                handleCmdRetryQueryEntitlement(msg.arg1);
+                break;
+            case CMD_STOP_RETRY_QUERY_ENTITLEMENT:
+                stopExponentialBackoff(msg.arg1);
+                break;
+            default:
+                logd("do not used this message");
+        }
+    }
+
+    private void handleCarrierConfigChanged(int slotIndex, int subId, int carrierId,
+            int specificCarrierId) {
+        if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+            return;
+        }
+        logd("handleCarrierConfigChanged(): slotIndex(" + slotIndex + "), subId("
+                + subId + "), carrierId(" + carrierId + "), specificCarrierId("
+                + specificCarrierId + ")");
+
+        sendEmptyMessage(CMD_START_QUERY_ENTITLEMENT);
+    }
+
+    private class SatelliteEntitlementControllerReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (action.equals(Intent.ACTION_AIRPLANE_MODE_CHANGED)) {
+                boolean airplaneMode = intent.getBooleanExtra("state", false);
+                handleAirplaneModeChange(airplaneMode);
+            }
+        }
+    }
+
+    private void handleAirplaneModeChange(boolean airplaneMode) {
+        if (!airplaneMode) {
+            resetEntitlementQueryCounts(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        }
+    }
+
+    private boolean isInternetConnected() {
+        Network activeNetwork = mConnectivityManager.getActiveNetwork();
+        NetworkCapabilities networkCapabilities =
+                mConnectivityManager.getNetworkCapabilities(activeNetwork);
+        // TODO b/319780796 Add checking if it is not a satellite.
+        return networkCapabilities != null
+                && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+    }
+
+    private void handleInternetConnected() {
+        sendEmptyMessage(CMD_START_QUERY_ENTITLEMENT);
+    }
+
+    private void handleInternetDisconnected() {
+        mExponentialBackoffPerSub.forEach((key, value) -> {
+            Message message = obtainMessage();
+            message.what = CMD_STOP_RETRY_QUERY_ENTITLEMENT;
+            message.arg1 = key;
+            sendMessage(message);
+        });
+    }
+
+    /**
+     * Check if the device can request to entitlement server (if there is an internet connection and
+     * if the throttle time has passed since the last request), and then pass the response to
+     * SatelliteController if the response is received.
+     */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    public void handleCmdStartQueryEntitlement() {
+        if (!isInternetConnected()) {
+            logd("Internet disconnected");
+            return;
+        }
+
+        for (int subId : mSubscriptionManagerService.getActiveSubIdList(true)) {
+            if (!shouldQueryEntitlement(subId)) {
+                return;
+            }
+
+            // Check the satellite service query result from the entitlement server for the
+            // satellite service.
+            try {
+                mSatelliteEntitlementResultPerSub.remove(subId);
+                mSatelliteEntitlementResultPerSub.put(subId, getSatelliteEntitlementApi(
+                        subId).checkEntitlementStatus());
+            } catch (ServiceEntitlementException e) {
+                loge(e.toString());
+                if (!isInternetConnected()) {
+                    logd("handleCmdStartQueryEntitlement: disconnected. " + e);
+                    return;
+                }
+                if (shouldHandleErrorResponse(e, subId)) {
+                    logd("handleCmdStartQueryEntitlement: handle response.");
+                    return;
+                }
+                startExponentialBackoff(subId);
+                return;
+            }
+            queryCompleted(subId);
+        }
+    }
+
+    /** When airplane mode changes from on to off, reset the values required to start the first
+     * query. */
+    private void resetEntitlementQueryCounts(String event) {
+        logd("resetEntitlementQueryCounts: " + event);
+        mLastQueryTimePerSub = new HashMap<>();
+        mExponentialBackoffPerSub = new HashMap<>();
+        mRetryCountPerSub = new HashMap<>();
+    }
+
+    /**
+     * If the HTTP response does not receive a body containing the 200 ok with sat mode
+     * configuration,
+     *
+     * 1. If the 500 response received, then no more retry until next event occurred.
+     * 2. If the 503 response with Retry-After header received, then the query is retried until
+     * MAX_RETRY_COUNT.
+     * 3. If other response or exception is occurred, then the query is retried until
+     * MAX_RETRY_COUNT is reached using the ExponentialBackoff.
+     */
+    private void handleCmdRetryQueryEntitlement(int subId) {
+        logd("handleCmdRetryQueryEntitlement: " + subId);
+        try {
+            synchronized (mLock) {
+                mSatelliteEntitlementResultPerSub.put(subId, getSatelliteEntitlementApi(
+                        subId).checkEntitlementStatus());
+            }
+        } catch (ServiceEntitlementException e) {
+            if (!isInternetConnected()) {
+                logd("retryQuery: Internet disconnected. reset the retry and after the "
+                        + "internet is connected then the first query is triggered." + e);
+                stopExponentialBackoff(subId);
+                return;
+            }
+            if (shouldHandleErrorResponse(e, subId)) {
+                logd("retryQuery: handle response.");
+                stopExponentialBackoff(subId);
+                return;
+            }
+            mExponentialBackoffPerSub.get(subId).notifyFailed();
+            mRetryCountPerSub.put(subId,
+                    mRetryCountPerSub.getOrDefault(subId, 0) + 1);
+            logd("handleCmdRetryQueryEntitlement:" + e + "[" + subId + "] cnt="
+                    + mRetryCountPerSub.getOrDefault(subId, 0) + "] Retrying in "
+                    + mExponentialBackoffPerSub.get(subId).getCurrentDelay() + " ms.");
+        }
+    }
+
+    /** Only handle '500' and '503 with retry-after header' error responses received.
+     * If the 500 response is received, no retry until the next trigger event occurs.
+     * If the 503 response with Retry-After header, retry is attempted according to the value in the
+     * Retry-After header up to MAX_RETRY_COUNT.
+     * In other cases, it performs an exponential backoff process. */
+    private boolean shouldHandleErrorResponse(ServiceEntitlementException e, int subId) {
+        int responseCode = e.getHttpStatus();
+        logd("shouldHandleErrorResponse: received the " + responseCode);
+        if (responseCode == HTTP_RESPONSE_503 && e.getRetryAfter() != null
+                && !e.getRetryAfter().isEmpty()) {
+            if (mRetryCountPerSub.getOrDefault(subId, 0) >= MAX_RETRY_COUNT) {
+                logd("The 503 retry after reaching the " + MAX_RETRY_COUNT
+                        + "The retry will not be attempted until the next trigger event.");
+                queryCompleted(subId);
+                return true;
+            }
+            long retryAfterSeconds = parseSecondsFromRetryAfter(e.getRetryAfter());
+            if (retryAfterSeconds == -1) {
+                logd("Unable parsing the retry-after. try to exponential backoff.");
+                return false;
+            }
+            mRetryCountPerSub.put(subId, mRetryCountPerSub.getOrDefault(subId, 0) + 1);
+            logd("[" + subId + "] cnt=" + mRetryCountPerSub.getOrDefault(subId, 0)
+                    + " Retrying in " + TimeUnit.SECONDS.toMillis(retryAfterSeconds) + " sec");
+            Message message = obtainMessage();
+            message.what = CMD_RETRY_QUERY_ENTITLEMENT;
+            message.arg1 = subId;
+            sendMessageDelayed(message, TimeUnit.SECONDS.toMillis(retryAfterSeconds));
+            return true;
+        } else if (responseCode == HTTP_RESPONSE_500) {
+            logd("The retry on the next trigger event.");
+            queryCompleted(subId);
+            return true;
+        }
+        return false;
+    }
+
+    /** Parse the HTTP-date or a number of seconds in the retry-after value. */
+    private long parseSecondsFromRetryAfter(String retryAfter) {
+        try {
+            return Long.parseLong(retryAfter);
+        } catch (NumberFormatException numberFormatException) {
+        }
+
+        try {
+            return SECONDS.between(
+                    Instant.now(), RFC_1123_DATE_TIME.parse(retryAfter, Instant::from));
+        } catch (DateTimeParseException dateTimeParseException) {
+        }
+
+        return -1;
+    }
+
+    private void startExponentialBackoff(int subId) {
+        stopExponentialBackoff(subId);
+        mExponentialBackoffPerSub.put(subId,
+                new ExponentialBackoff(INITIAL_DELAY_MILLIS, MAX_DELAY_MILLIS,
+                        MULTIPLIER, this.getLooper(), () -> {
+                    synchronized (mLock) {
+                        if (mSatelliteEntitlementResultPerSub.containsKey(subId)) {
+                            logd("handleCmdStartQueryEntitlement: get the response "
+                                    + "successfully.");
+                            mExponentialBackoffPerSub.get(subId).stop();
+                            queryCompleted(subId);
+                            return;
+                        }
+
+                        if (mRetryCountPerSub.getOrDefault(subId, 0) >= MAX_RETRY_COUNT) {
+                            logd("The ExponentialBackoff is  stopped after reaching the "
+                                    + MAX_RETRY_COUNT + ". The retry don't attempted until the"
+                                    + " refresh time expires.");
+                            mExponentialBackoffPerSub.get(subId).stop();
+                            queryCompleted(subId);
+                            return;
+                        }
+                        if (!mSatelliteEntitlementResultPerSub.containsKey(subId)) {
+                            handleCmdRetryQueryEntitlement(subId);
+                        }
+                    }
+                }));
+        mExponentialBackoffPerSub.get(subId).start();
+        mRetryCountPerSub.put(subId, mRetryCountPerSub.getOrDefault(subId, 0) + 1);
+        logd("start ExponentialBackoff [" + mRetryCountPerSub.getOrDefault(subId, 0)
+                + "] Retrying in " + mExponentialBackoffPerSub.get(subId).getCurrentDelay()
+                + " ms.");
+    }
+
+    /** If the Internet connection is lost during the ExponentialBackoff, stop the
+     * ExponentialBackoff and reset it. */
+    private void stopExponentialBackoff(int subId) {
+        if (isExponentialBackoffInProgress(subId)) {
+            logd("stopExponentialBackoff: reset ExponentialBackoff");
+            mExponentialBackoffPerSub.get(subId).stop();
+            mExponentialBackoffPerSub.remove(subId);
+        }
+    }
+
+    /**
+     * No more query retry, update the result. If there is no response from the server, then used
+     * the default value - 'satellite disabled' and empty 'PLMN allowed list'.
+     * And then it send a delayed message to trigger the query again after A refresh day has passed.
+     */
+    private void queryCompleted(int subId) {
+        if (!mSatelliteEntitlementResultPerSub.containsKey(subId)) {
+            logd("queryCompleted: create default SatelliteEntitlementResult");
+            mSatelliteEntitlementResultPerSub.put(subId,
+                    SatelliteEntitlementResult.getDefaultResult());
+        }
+
+        saveLastQueryTime(subId);
+        Message message = obtainMessage();
+        message.what = CMD_START_QUERY_ENTITLEMENT;
+        message.arg1 = subId;
+        sendMessageDelayed(message, TimeUnit.DAYS.toMillis(
+                getSatelliteEntitlementStatusRefreshDays(subId)));
+        logd("queryCompleted: updateSatelliteEntitlementStatus");
+        updateSatelliteEntitlementStatus(subId,
+                mSatelliteEntitlementResultPerSub.get(subId).getEntitlementStatus()
+                        == SATELLITE_ENTITLEMENT_STATUS_ENABLED,
+                mSatelliteEntitlementResultPerSub.get(subId).getAllowedPLMNList());
+        stopExponentialBackoff(subId);
+        mRetryCountPerSub.remove(subId);
+    }
+
+    /** Check whether there is a saved subId. Returns true if there is a saved subId,
+     * otherwise return false.*/
+    private boolean isExponentialBackoffInProgress(int subId) {
+        return mExponentialBackoffPerSub.containsKey(subId);
+    }
+
+    /**
+     * Check if the subId can query the entitlement server to get the satellite configuration.
+     */
+    private boolean shouldQueryEntitlement(int subId) {
+        if (!isSatelliteEntitlementSupported(subId)) {
+            logd("Doesn't support entitlement query for satellite.");
+            return false;
+        }
+
+        if (isExponentialBackoffInProgress(subId)) {
+            logd("In progress ExponentialBackoff.");
+            return false;
+        }
+
+        return shouldRefreshEntitlementStatus(subId);
+    }
+
+    /**
+     * Compare the last query time to the refresh time from the CarrierConfig to see if the device
+     * can query the entitlement server.
+     */
+    private boolean shouldRefreshEntitlementStatus(int subId) {
+        long lastQueryTimeMillis = getLastQueryTime(subId);
+        long refreshTimeMillis = TimeUnit.DAYS.toMillis(
+                getSatelliteEntitlementStatusRefreshDays(subId));
+        boolean isAvailable =
+                (System.currentTimeMillis() - lastQueryTimeMillis) > refreshTimeMillis;
+        if (!isAvailable) {
+            logd("query is already done. can query after " + Instant.ofEpochMilli(
+                    refreshTimeMillis + lastQueryTimeMillis));
+        }
+        return isAvailable;
+    }
+
+    /**
+     * Get the SatelliteEntitlementApi.
+     *
+     * @param subId The subId of the subscription for creating SatelliteEntitlementApi
+     * @return A new SatelliteEntitlementApi object.
+     */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    public SatelliteEntitlementApi getSatelliteEntitlementApi(int subId) {
+        return new SatelliteEntitlementApi(mContext, getConfigForSubId(subId), subId);
+    }
+
+    /** If there is a value stored in the cache, it is used. If there is no value stored in the
+     * cache, it is considered the first query. */
+    private long getLastQueryTime(int subId) {
+        return mLastQueryTimePerSub.getOrDefault(subId, 0L);
+    }
+
+    /** Return the satellite entitlement status refresh days from carrier config. */
+    private int getSatelliteEntitlementStatusRefreshDays(int subId) {
+        return getConfigForSubId(subId).getInt(
+                CarrierConfigManager.KEY_SATELLITE_ENTITLEMENT_STATUS_REFRESH_DAYS_INT,
+                DEFAULT_QUERY_REFRESH_DAYS);
+    }
+
+    /** Return the satellite entitlement supported bool from carrier config. */
+    private boolean isSatelliteEntitlementSupported(int subId) {
+        return getConfigForSubId(subId).getBoolean(
+                CarrierConfigManager.KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL);
+    }
+
+    @NonNull
+    private PersistableBundle getConfigForSubId(int subId) {
+        PersistableBundle config = mCarrierConfigManager.getConfigForSubId(subId,
+                CarrierConfigManager.ImsServiceEntitlement.KEY_ENTITLEMENT_SERVER_URL_STRING,
+                CarrierConfigManager.KEY_SATELLITE_ENTITLEMENT_STATUS_REFRESH_DAYS_INT,
+                CarrierConfigManager.KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL);
+        if (config == null || config.isEmpty()) {
+            config = CarrierConfigManager.getDefaultConfig();
+        }
+        return config;
+    }
+
+    private void saveLastQueryTime(int subId) {
+        long lastQueryTimeMillis = System.currentTimeMillis();
+        mLastQueryTimePerSub.put(subId, lastQueryTimeMillis);
+    }
+
+    /**
+     * Send to satelliteController for update the satellite service enabled or not and plmn Allowed
+     * list.
+     */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    public void updateSatelliteEntitlementStatus(int subId, boolean enabled,
+            List<String> plmnAllowedList) {
+        SatelliteController.getInstance().updateSatelliteEntitlementStatus(subId, enabled,
+                plmnAllowedList, null);
+    }
+
+    private static void logd(String log) {
+        Rlog.d(TAG, log);
+    }
+
+    private static void loge(String log) {
+        Rlog.e(TAG, log);
+    }
+}
diff --git a/src/com/android/phone/satellite/entitlement/SatelliteEntitlementResponse.java b/src/com/android/phone/satellite/entitlement/SatelliteEntitlementResponse.java
new file mode 100644
index 0000000..1fe0ecf
--- /dev/null
+++ b/src/com/android/phone/satellite/entitlement/SatelliteEntitlementResponse.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2024 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.entitlement;
+
+import static com.android.phone.satellite.entitlement.SatelliteEntitlementResult.SATELLITE_ENTITLEMENT_STATUS_DISABLED;
+
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.satellite.SatelliteNetworkInfo;
+import com.android.libraries.entitlement.ServiceEntitlement;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * This class parses whether the satellite service configuration.
+ * @hide
+ */
+public class SatelliteEntitlementResponse {
+    private static final String TAG = "SatelliteEntitlementResponse";
+
+    /** Overall status of the SatMode entitlement, stating if the satellite service can be offered
+     * on the device, and if it can be activated or not by the user. */
+    private static final String ENTITLEMENT_STATUS_KEY = "EntitlementStatus";
+    /** List of allowed PLMNs where the service can be used. */
+    private static final String PLMN_ALLOWED_KEY = "PLMNAllowed";
+    /** List of barred PLMNs where the service can’t be used. */
+    private static final String PLMN_BARRED_KEY = "PLMNBarred";
+    /** allowed PLMN-ID where the service can be used or is barred. */
+    private static final String PLMN_KEY = "PLMN";
+    /** The data plan is of the metered or un-metered type. This value is optional. */
+    private static final String DATA_PLAN_TYPE_KEY = "DataPlanType";
+
+    @SatelliteEntitlementResult.SatelliteEntitlementStatus private int mEntitlementStatus;
+
+    /**
+     * <p> Available options are :
+     * "PLMNAllowed":[{ "PLMN": "XXXXXX", “DataPlanType”: "unmetered"},
+     * {"PLMN": "XXXXXX", “DataPlanType”: "metered"},
+     * {"PLMN": "XXXXXX"}]
+     */
+    private List<SatelliteNetworkInfo> mPlmnAllowedList;
+    /**
+     * <p> Available option is :
+     * "PLMNBarred":[{"PLMN": "XXXXXX"}, {"PLMN”:"XXXXXX"}]
+     */
+    private List<String> mPlmnBarredList;
+
+    public SatelliteEntitlementResponse(String response) {
+        mEntitlementStatus = SATELLITE_ENTITLEMENT_STATUS_DISABLED;
+        mPlmnAllowedList = new ArrayList<>();
+        mPlmnBarredList = new ArrayList<>();
+        parsingResponse(response);
+    }
+
+    /**
+     * Get the entitlement status for the satellite service
+     * @return The satellite entitlement status
+     */
+    public int getEntitlementStatus() {
+        return mEntitlementStatus;
+    }
+
+    /**
+     * Get the PLMNAllowed from the response
+     * @return The PLMNs Allowed list. PLMN and Data Plan Type(optional).
+     */
+    public List<SatelliteNetworkInfo> getPlmnAllowed() {
+        return mPlmnAllowedList.stream().map((info) -> new SatelliteNetworkInfo(info.mPlmn,
+                info.mDataPlanType)).collect(Collectors.toList());
+    }
+
+    /**
+     * Get the PLMNBarredList from the response
+     * @return The PLMNs Barred List
+     */
+    @VisibleForTesting
+    public List<String> getPlmnBarredList() {
+        return mPlmnBarredList.stream().map(String::new).collect(Collectors.toList());
+    }
+
+    private void parsingResponse(String response) {
+        JSONObject jsonAuthResponse = null;
+        try {
+            jsonAuthResponse = new JSONObject(response);
+            if (!jsonAuthResponse.has(ServiceEntitlement.APP_SATELLITE_ENTITLEMENT)) {
+                loge("parsingResponse failed with no app");
+                return;
+            }
+            JSONObject jsonToken = jsonAuthResponse.getJSONObject(
+                    ServiceEntitlement.APP_SATELLITE_ENTITLEMENT);
+            if (jsonToken.has(ENTITLEMENT_STATUS_KEY)) {
+                String entitlementStatus = jsonToken.getString(ENTITLEMENT_STATUS_KEY);
+                if (entitlementStatus == null) {
+                    loge("parsingResponse EntitlementStatus is null");
+                    return;
+                }
+                mEntitlementStatus = Integer.valueOf(entitlementStatus);
+            }
+            if (jsonToken.has(PLMN_ALLOWED_KEY)) {
+                JSONArray jsonArray = jsonToken.getJSONArray(PLMN_ALLOWED_KEY);
+                mPlmnAllowedList = new ArrayList<>();
+                for (int i = 0; i < jsonArray.length(); i++) {
+                    String dataPlanType = jsonArray.getJSONObject(i).has(DATA_PLAN_TYPE_KEY)
+                            ? jsonArray.getJSONObject(i).getString(DATA_PLAN_TYPE_KEY) : "";
+                    mPlmnAllowedList.add(new SatelliteNetworkInfo(
+                            jsonArray.getJSONObject(i).getString(PLMN_KEY), dataPlanType));
+                }
+            }
+            if (jsonToken.has(PLMN_BARRED_KEY)) {
+                mPlmnBarredList = new ArrayList<>();
+                JSONArray jsonArray = jsonToken.getJSONArray(PLMN_BARRED_KEY);
+                for (int i = 0; i < jsonArray.length(); i++) {
+                    mPlmnBarredList.add(jsonArray.getJSONObject(i).getString(PLMN_KEY));
+                }
+            }
+        } catch (JSONException e) {
+            loge("parsingResponse: failed JSONException", e);
+        } catch (NumberFormatException e) {
+            loge("parsingResponse: failed NumberFormatException", e);
+        }
+    }
+
+    private static void loge(String log) {
+        Log.e(TAG, log);
+    }
+
+    private static void loge(String log, Exception e) {
+        Log.e(TAG, log, e);
+    }
+}
diff --git a/src/com/android/phone/satellite/entitlement/SatelliteEntitlementResult.java b/src/com/android/phone/satellite/entitlement/SatelliteEntitlementResult.java
new file mode 100644
index 0000000..3289232
--- /dev/null
+++ b/src/com/android/phone/satellite/entitlement/SatelliteEntitlementResult.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2024 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.entitlement;
+
+import android.annotation.IntDef;
+
+import com.android.internal.telephony.satellite.SatelliteNetworkInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * This class stores the result of the satellite entitlement query and passes them to
+ * SatelliteEntitlementController.
+ */
+public class SatelliteEntitlementResult {
+    /** SatMode allowed, but not yet provisioned and activated on the network. */
+    public static final int SATELLITE_ENTITLEMENT_STATUS_DISABLED = 0;
+    /** SatMode service allowed, provisioned and activated on the network. User can access the
+     * satellite service. */
+    public static final int SATELLITE_ENTITLEMENT_STATUS_ENABLED = 1;
+    /** SatMode cannot be offered for network or device. */
+    public static final int SATELLITE_ENTITLEMENT_STATUS_INCOMPATIBLE = 2;
+    /** SatMode is being provisioned on the network. Not yet activated. */
+    public static final int SATELLITE_ENTITLEMENT_STATUS_PROVISIONING = 3;
+
+    @IntDef(prefix = {"SATELLITE_ENTITLEMENT_STATUS_"}, value = {
+            SATELLITE_ENTITLEMENT_STATUS_DISABLED,
+            SATELLITE_ENTITLEMENT_STATUS_ENABLED,
+            SATELLITE_ENTITLEMENT_STATUS_INCOMPATIBLE,
+            SATELLITE_ENTITLEMENT_STATUS_PROVISIONING
+    })
+    public @interface SatelliteEntitlementStatus {}
+
+    private @SatelliteEntitlementStatus int mEntitlementStatus;
+    /**
+     * An SatelliteNetworkInfo list consisting of the PLMN and the DataPlanType in the PLMNAlowed
+     * item of the satellite configuration received from the entitlement server.
+     */
+    private List<SatelliteNetworkInfo> mAllowedSatelliteNetworkInfoList;
+
+    /**
+     * Store the result of the satellite entitlement response.
+     *
+     * @param entitlementStatus The entitlement status.
+     * @param allowedSatelliteNetworkInfoList The allowedSatelliteNetworkInfoList
+     */
+    public SatelliteEntitlementResult(@SatelliteEntitlementStatus int entitlementStatus,
+            List<SatelliteNetworkInfo> allowedSatelliteNetworkInfoList) {
+        mEntitlementStatus = entitlementStatus;
+        mAllowedSatelliteNetworkInfoList = allowedSatelliteNetworkInfoList;
+    }
+
+    /**
+     * Get the entitlement status.
+     *
+     * @return The entitlement status.
+     */
+    public @SatelliteEntitlementStatus int getEntitlementStatus() {
+        return mEntitlementStatus;
+    }
+
+    /**
+     * Get the plmn allowed list
+     *
+     * @return The plmn allowed list.
+     */
+    public List<String> getAllowedPLMNList() {
+        return mAllowedSatelliteNetworkInfoList.stream().map(info -> info.mPlmn).collect(
+                Collectors.toList());
+    }
+
+    /**
+     * Get the default SatelliteEntitlementResult. EntitlementStatus set to
+     * `SATELLITE_ENTITLEMENT_STATUS_DISABLED` and SatelliteNetworkInfo list set to empty.
+     *
+     * @return If there is no response, return default SatelliteEntitlementResult
+     */
+    public static SatelliteEntitlementResult getDefaultResult() {
+        return new SatelliteEntitlementResult(SATELLITE_ENTITLEMENT_STATUS_DISABLED,
+                new ArrayList<>());
+    }
+}
diff --git a/tests/src/com/android/phone/satellite/entitlement/SatelliteEntitlementApiTest.java b/tests/src/com/android/phone/satellite/entitlement/SatelliteEntitlementApiTest.java
new file mode 100644
index 0000000..f096e0d
--- /dev/null
+++ b/tests/src/com/android/phone/satellite/entitlement/SatelliteEntitlementApiTest.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2024 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.entitlement;
+
+import static com.android.phone.satellite.entitlement.SatelliteEntitlementResult.SATELLITE_ENTITLEMENT_STATUS_DISABLED;
+import static com.android.phone.satellite.entitlement.SatelliteEntitlementResult.SATELLITE_ENTITLEMENT_STATUS_ENABLED;
+import static com.android.phone.satellite.entitlement.SatelliteEntitlementResult.SATELLITE_ENTITLEMENT_STATUS_INCOMPATIBLE;
+import static com.android.phone.satellite.entitlement.SatelliteEntitlementResult.SATELLITE_ENTITLEMENT_STATUS_PROVISIONING;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyVararg;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+
+import android.content.Context;
+import android.os.PersistableBundle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.libraries.entitlement.ServiceEntitlement;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class SatelliteEntitlementApiTest {
+    private static final String TEST_URL = "https://test.url";
+    private static final List<String> TEST_PLMN_ALLOWED = Arrays.asList("31026", "302820");
+    @Mock
+    Context mContext;
+    @Mock
+    ServiceEntitlement mServiceEntitlement;
+    @Mock
+    CarrierConfigManager mCarrierConfigManager;
+    @Mock
+    TelephonyManager mTelephonyManager;
+    private PersistableBundle mCarrierConfigBundle;
+    private SatelliteEntitlementApi mSatelliteEntitlementAPI;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        doReturn(Context.CARRIER_CONFIG_SERVICE).when(mContext).getSystemServiceName(
+                CarrierConfigManager.class);
+        doReturn(mCarrierConfigManager).when(mContext).getSystemService(
+                Context.CARRIER_CONFIG_SERVICE);
+        mCarrierConfigBundle = new PersistableBundle();
+        doReturn(mCarrierConfigBundle)
+                .when(mCarrierConfigManager).getConfigForSubId(anyInt(), anyVararg());
+        doReturn(Context.TELEPHONY_SERVICE).when(mContext).getSystemServiceName(
+                TelephonyManager.class);
+        doReturn(mTelephonyManager).when(mContext).getSystemService(Context.TELEPHONY_SERVICE);
+        doReturn(mTelephonyManager).when(mTelephonyManager).createForSubscriptionId(anyInt());
+
+        mSatelliteEntitlementAPI = new SatelliteEntitlementApi(mContext, mCarrierConfigBundle,
+                SubscriptionManager.DEFAULT_SUBSCRIPTION_ID);
+    }
+
+    @Test
+    public void testCheckEntitlementStatus() throws Exception {
+        mCarrierConfigBundle.putString(
+                CarrierConfigManager.ImsServiceEntitlement.KEY_ENTITLEMENT_SERVER_URL_STRING,
+                TEST_URL);
+        Field fieldServiceEntitlement = SatelliteEntitlementApi.class.getDeclaredField(
+                "mServiceEntitlement");
+        fieldServiceEntitlement.setAccessible(true);
+        fieldServiceEntitlement.set(mSatelliteEntitlementAPI, mServiceEntitlement);
+
+        // Get the EntitlementStatus to DISABLED
+        int expectedEntitlementStatus = SATELLITE_ENTITLEMENT_STATUS_DISABLED;
+        doReturn(getResponse(SATELLITE_ENTITLEMENT_STATUS_DISABLED))
+                .when(mServiceEntitlement)
+                .queryEntitlementStatus(eq(ServiceEntitlement.APP_SATELLITE_ENTITLEMENT), any());
+        SatelliteEntitlementResult result =
+                mSatelliteEntitlementAPI.checkEntitlementStatus();
+        assertNotNull(result);
+        assertEquals(expectedEntitlementStatus, result.getEntitlementStatus());
+        assertTrue(result.getAllowedPLMNList().size() == 0);
+
+        // Get the EntitlementStatus to ENABLED
+        expectedEntitlementStatus = SATELLITE_ENTITLEMENT_STATUS_ENABLED;
+        doReturn(getResponse(SATELLITE_ENTITLEMENT_STATUS_ENABLED))
+                .when(mServiceEntitlement)
+                .queryEntitlementStatus(eq(ServiceEntitlement.APP_SATELLITE_ENTITLEMENT), any());
+        result = mSatelliteEntitlementAPI.checkEntitlementStatus();
+        assertNotNull(result);
+        assertEquals(expectedEntitlementStatus, result.getEntitlementStatus());
+        assertEquals(TEST_PLMN_ALLOWED, result.getAllowedPLMNList());
+
+        // Get the EntitlementStatus to INCOMPATIBLE
+        expectedEntitlementStatus = SATELLITE_ENTITLEMENT_STATUS_INCOMPATIBLE;
+        doReturn(getResponse(SATELLITE_ENTITLEMENT_STATUS_INCOMPATIBLE))
+                .when(mServiceEntitlement)
+                .queryEntitlementStatus(eq(ServiceEntitlement.APP_SATELLITE_ENTITLEMENT), any());
+        result = mSatelliteEntitlementAPI.checkEntitlementStatus();
+        assertNotNull(result);
+        assertEquals(expectedEntitlementStatus, result.getEntitlementStatus());
+        assertTrue(result.getAllowedPLMNList().size() == 0);
+
+        // Get the EntitlementStatus to PROVISIONING
+        expectedEntitlementStatus = SATELLITE_ENTITLEMENT_STATUS_PROVISIONING;
+        doReturn(getResponse(SATELLITE_ENTITLEMENT_STATUS_PROVISIONING))
+                .when(mServiceEntitlement)
+                .queryEntitlementStatus(eq(ServiceEntitlement.APP_SATELLITE_ENTITLEMENT), any());
+        result = mSatelliteEntitlementAPI.checkEntitlementStatus();
+        assertNotNull(result);
+        assertEquals(expectedEntitlementStatus, result.getEntitlementStatus());
+        assertTrue(result.getAllowedPLMNList().size() == 0);
+    }
+
+    private String getResponse(int entitlementStatus) {
+        return "{\"VERS\":{\"version\":\"1\",\"validity\":\"172800\"},"
+                + "\"TOKEN\":{\"token\":\"ASH127AHHA88SF\"},\""
+                + ServiceEntitlement.APP_SATELLITE_ENTITLEMENT + "\":{"
+                + "\"EntitlementStatus\":\"" + entitlementStatus + "\""
+                + getPLMNListOrEmpty(entitlementStatus)
+                + "}}";
+    }
+
+    private String getPLMNListOrEmpty(int entitlementStatus) {
+        return entitlementStatus == SATELLITE_ENTITLEMENT_STATUS_ENABLED ? ","
+                + "\"PLMNAllowed\":[{\"PLMN\":\"31026\",\"DataPlanType\":\"unmetered\"},"
+                + "{\"PLMN\":\"302820\",\"DataPlanType\":\"metered\"}]" : "";
+    }
+}
diff --git a/tests/src/com/android/phone/satellite/entitlement/SatelliteEntitlementControllerTest.java b/tests/src/com/android/phone/satellite/entitlement/SatelliteEntitlementControllerTest.java
new file mode 100644
index 0000000..e208e6c
--- /dev/null
+++ b/tests/src/com/android/phone/satellite/entitlement/SatelliteEntitlementControllerTest.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright (C) 2024 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.entitlement;
+
+import static com.android.phone.satellite.entitlement.SatelliteEntitlementResult.SATELLITE_ENTITLEMENT_STATUS_ENABLED;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.anyVararg;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.wifi.WifiInfo;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PersistableBundle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.TelephonyManager;
+import android.testing.TestableLooper;
+import android.util.Log;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.TelephonyTestBase;
+import com.android.internal.telephony.ExponentialBackoff;
+import com.android.internal.telephony.satellite.SatelliteController;
+import com.android.internal.telephony.subscription.SubscriptionManagerService;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+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;
+
+@RunWith(AndroidJUnit4.class)
+public class SatelliteEntitlementControllerTest extends TelephonyTestBase {
+    private static final String TAG = "SatelliteEntitlementControllerTest";
+    private static final int SUB_ID = 0;
+    private static final int SUB_ID_2 = 1;
+    private static final int[] ACTIVE_SUB_ID = {SUB_ID};
+    private static final int DEFAULT_QUERY_REFRESH_DAY = 30;
+    private static final List<String> PLMN_ALLOWED_LIST = Arrays.asList("31026", "302820");
+    @Mock
+    CarrierConfigManager mCarrierConfigManager;
+    @Mock
+    ConnectivityManager mConnectivityManager;
+    @Mock Network mNetwork;
+    @Mock TelephonyManager mTelephonyManager;
+    @Mock SubscriptionManagerService mMockSubscriptionManagerService;
+    @Mock SatelliteEntitlementApi mSatelliteEntitlementApi;
+    @Mock SatelliteEntitlementResult mSatelliteEntitlementResult;
+    @Mock SatelliteController mSatelliteController;
+    @Mock ExponentialBackoff mExponentialBackoff;
+    private PersistableBundle mCarrierConfigBundle;
+    private TestSatelliteEntitlementController mSatelliteEntitlementController;
+    private Handler mHandler;
+    private TestableLooper mTestableLooper;
+    private List<Pair<Executor, CarrierConfigManager.CarrierConfigChangeListener>>
+            mCarrierConfigChangedListenerList = new ArrayList<>();
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        MockitoAnnotations.initMocks(this);
+
+        replaceInstance(SubscriptionManagerService.class, "sInstance", null,
+                mMockSubscriptionManagerService);
+        replaceInstance(SatelliteController.class, "sInstance", null, mSatelliteController);
+
+        HandlerThread handlerThread = new HandlerThread("SatelliteEntitlementController");
+        handlerThread.start();
+        mHandler = new Handler(handlerThread.getLooper()) {
+            @Override
+            public void handleMessage(Message msg) {
+            }
+        };
+        mTestableLooper = new TestableLooper(mHandler.getLooper());
+        doReturn(Context.TELEPHONY_SERVICE).when(mContext).getSystemServiceName(
+                TelephonyManager.class);
+        doReturn(mTelephonyManager).when(mContext).getSystemService(Context.TELEPHONY_SERVICE);
+        doReturn(mTelephonyManager).when(mTelephonyManager).createForSubscriptionId(anyInt());
+        doReturn(Context.CARRIER_CONFIG_SERVICE).when(mContext).getSystemServiceName(
+                CarrierConfigManager.class);
+        doReturn(mCarrierConfigManager).when(mContext).getSystemService(
+                Context.CARRIER_CONFIG_SERVICE);
+        doAnswer(invocation -> {
+            Executor executor = invocation.getArgument(0);
+            CarrierConfigManager.CarrierConfigChangeListener listener = invocation.getArgument(1);
+            mCarrierConfigChangedListenerList.add(new Pair<>(executor, listener));
+            return null;
+        }).when(mCarrierConfigManager).registerCarrierConfigChangeListener(
+                any(Executor.class),
+                any(CarrierConfigManager.CarrierConfigChangeListener.class));
+        mCarrierConfigBundle = new PersistableBundle();
+        mCarrierConfigBundle.putInt(
+                CarrierConfigManager.KEY_SATELLITE_ENTITLEMENT_STATUS_REFRESH_DAYS_INT,
+                DEFAULT_QUERY_REFRESH_DAY);
+        mCarrierConfigBundle.putBoolean(
+                CarrierConfigManager.KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL, true);
+        doReturn(mCarrierConfigBundle)
+                .when(mCarrierConfigManager).getConfigForSubId(anyInt(), anyVararg());
+        doReturn(Context.CONNECTIVITY_SERVICE).when(mContext).getSystemServiceName(
+                ConnectivityManager.class);
+        doReturn(mConnectivityManager).when(mContext).getSystemService(
+                Context.CONNECTIVITY_SERVICE);
+        doReturn(mNetwork).when(mConnectivityManager).getActiveNetwork();
+        doReturn(ACTIVE_SUB_ID).when(mMockSubscriptionManagerService).getActiveSubIdList(true);
+        mSatelliteEntitlementController = new TestSatelliteEntitlementController(mContext,
+                mHandler.getLooper(), mSatelliteEntitlementApi);
+        mSatelliteEntitlementController = spy(mSatelliteEntitlementController);
+        doReturn(mSatelliteEntitlementResult).when(
+                mSatelliteEntitlementApi).checkEntitlementStatus();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    public void testIsQueryAvailable() throws Exception {
+        doReturn(ACTIVE_SUB_ID).when(mMockSubscriptionManagerService).getActiveSubIdList(true);
+
+        // Verify don't start the query when KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL is false.
+        mCarrierConfigBundle.putBoolean(
+                CarrierConfigManager.KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL, false);
+        mSatelliteEntitlementController.handleCmdStartQueryEntitlement();
+
+        verify(mSatelliteEntitlementApi, never()).checkEntitlementStatus();
+        verify(mSatelliteController, never()).updateSatelliteEntitlementStatus(anyInt(),
+                anyBoolean(), anyList(), any());
+
+        mCarrierConfigBundle.putBoolean(
+                CarrierConfigManager.KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL, true);
+        // Verify don't start the query when ExponentialBackoff is in progressed.
+        replaceInstance(SatelliteEntitlementController.class, "mExponentialBackoffPerSub",
+                mSatelliteEntitlementController, Map.of(SUB_ID, mExponentialBackoff));
+        mSatelliteEntitlementController.handleCmdStartQueryEntitlement();
+
+        verify(mSatelliteEntitlementApi, never()).checkEntitlementStatus();
+        verify(mSatelliteController, never()).updateSatelliteEntitlementStatus(anyInt(),
+                anyBoolean(), anyList(), any());
+
+        replaceInstance(SatelliteEntitlementController.class, "mExponentialBackoffPerSub",
+                mSatelliteEntitlementController, new HashMap<>());
+        // Verify don't start the query when Internet is disconnected.
+        doReturn(ACTIVE_SUB_ID).when(mMockSubscriptionManagerService).getActiveSubIdList(true);
+        setInternetConnected(false);
+        mSatelliteEntitlementController.handleCmdStartQueryEntitlement();
+
+        verify(mSatelliteEntitlementApi, never()).checkEntitlementStatus();
+        verify(mSatelliteController, never()).updateSatelliteEntitlementStatus(anyInt(),
+                anyBoolean(), anyList(), any());
+
+        setInternetConnected(true);
+        // Verify don't start the query when last query refresh time is not expired.
+        setLastQueryTime(System.currentTimeMillis());
+        mSatelliteEntitlementController.handleCmdStartQueryEntitlement();
+
+        verify(mSatelliteEntitlementApi, never()).checkEntitlementStatus();
+        verify(mSatelliteController, never()).updateSatelliteEntitlementStatus(anyInt(),
+                anyBoolean(), anyList(), any());
+
+        // Verify start the query when isQueryAvailable return true
+        setLastQueryTime(0L);
+        doReturn(mSatelliteEntitlementResult).when(
+                mSatelliteEntitlementApi).checkEntitlementStatus();
+        setSatelliteEntitlementResult(SATELLITE_ENTITLEMENT_STATUS_ENABLED, PLMN_ALLOWED_LIST);
+        mSatelliteEntitlementController.handleCmdStartQueryEntitlement();
+
+        verify(mSatelliteEntitlementApi).checkEntitlementStatus();
+        verify(mSatelliteController).updateSatelliteEntitlementStatus(anyInt(),
+                anyBoolean(), anyList(), any());
+    }
+
+    @Test
+    public void testCheckSatelliteEntitlementStatus() throws Exception {
+        setIsQueryAvailableTrue();
+        // Verify don't call the checkSatelliteEntitlementStatus when getActiveSubIdList is empty.
+        doReturn(new int[]{}).when(mMockSubscriptionManagerService).getActiveSubIdList(true);
+        mSatelliteEntitlementController.handleCmdStartQueryEntitlement();
+
+        verify(mSatelliteEntitlementApi, never()).checkEntitlementStatus();
+        // Verify don't call the updateSatelliteEntitlementStatus.
+        verify(mSatelliteController, never()).updateSatelliteEntitlementStatus(anyInt(),
+                anyBoolean(), anyList(), any());
+
+        // Verify call the checkSatelliteEntitlementStatus with invalid response.
+        setIsQueryAvailableTrue();
+        doReturn(mSatelliteEntitlementResult).when(
+                mSatelliteEntitlementApi).checkEntitlementStatus();
+        replaceInstance(SatelliteEntitlementController.class,
+                "mSatelliteEntitlementResultPerSub", mSatelliteEntitlementController,
+                new HashMap<>());
+        mSatelliteEntitlementController.handleCmdStartQueryEntitlement();
+
+        verify(mSatelliteEntitlementApi).checkEntitlementStatus();
+        // Verify call the updateSatelliteEntitlementStatus with satellite service is disabled
+        // and empty PLMNAllowed
+        verify(mSatelliteController).updateSatelliteEntitlementStatus(eq(SUB_ID),
+                eq(false), eq(new ArrayList<>()), any());
+
+        // Verify call the checkSatelliteEntitlementStatus with the subscribed result.
+        clearInvocationsForMock();
+        setIsQueryAvailableTrue();
+        doReturn(mSatelliteEntitlementResult).when(
+                mSatelliteEntitlementApi).checkEntitlementStatus();
+        setSatelliteEntitlementResult(SATELLITE_ENTITLEMENT_STATUS_ENABLED, PLMN_ALLOWED_LIST);
+        mSatelliteEntitlementController.handleCmdStartQueryEntitlement();
+
+        verify(mSatelliteEntitlementApi).checkEntitlementStatus();
+        // Verify call the updateSatelliteEntitlementStatus with satellite service is enable and
+        // availablePLMNAllowedList
+        verify(mSatelliteController).updateSatelliteEntitlementStatus(eq(SUB_ID), eq(true),
+                eq(PLMN_ALLOWED_LIST), any());
+
+        // Change subId and verify call the updateSatelliteEntitlementStatus with  satellite
+        // service is enable and availablePLMNAllowedList
+        clearInvocationsForMock();
+        doReturn(new int[]{SUB_ID_2}).when(mMockSubscriptionManagerService).getActiveSubIdList(
+                true);
+        mSatelliteEntitlementController.handleCmdStartQueryEntitlement();
+
+        verify(mSatelliteEntitlementApi).checkEntitlementStatus();
+        verify(mSatelliteController).updateSatelliteEntitlementStatus(eq(SUB_ID_2), eq(true),
+                eq(PLMN_ALLOWED_LIST), any());
+    }
+
+    @Test
+    public void testCheckSatelliteEntitlementStatusWhenInternetConnected() throws Exception {
+        Field fieldNetworkCallback = SatelliteEntitlementController.class.getDeclaredField(
+                "mNetworkCallback");
+        fieldNetworkCallback.setAccessible(true);
+        ConnectivityManager.NetworkCallback networkCallback =
+                (ConnectivityManager.NetworkCallback) fieldNetworkCallback.get(
+                        mSatelliteEntitlementController);
+        Network mockNetwork = mock(Network.class);
+
+        // Verify the called the checkSatelliteEntitlementStatus when Internet is connected.
+        setInternetConnected(true);
+        setLastQueryTime(0L);
+        setSatelliteEntitlementResult(SATELLITE_ENTITLEMENT_STATUS_ENABLED, PLMN_ALLOWED_LIST);
+
+        networkCallback.onAvailable(mockNetwork);
+        mTestableLooper.processAllMessages();
+
+        verify(mSatelliteEntitlementApi).checkEntitlementStatus();
+        // Verify call the updateSatelliteEntitlementStatus with satellite service is available.
+        verify(mSatelliteController).updateSatelliteEntitlementStatus(eq(SUB_ID), eq(true),
+                eq(PLMN_ALLOWED_LIST), any());
+    }
+
+    @Test
+    public void testCheckSatelliteEntitlementStatusWhenCarrierConfigChanged() throws Exception {
+        // Verify the called the checkSatelliteEntitlementStatus when CarrierConfigChanged
+        // occurred and Internet is connected.
+        setInternetConnected(true);
+        setLastQueryTime(0L);
+        setSatelliteEntitlementResult(SATELLITE_ENTITLEMENT_STATUS_ENABLED, PLMN_ALLOWED_LIST);
+        triggerCarrierConfigChanged();
+
+        verify(mSatelliteEntitlementApi).checkEntitlementStatus();
+        // Verify call the updateSatelliteEntitlementStatus with satellite service is available.
+        verify(mSatelliteController).updateSatelliteEntitlementStatus(eq(SUB_ID), eq(true),
+                eq(PLMN_ALLOWED_LIST), any());
+    }
+
+    private void triggerCarrierConfigChanged() {
+        for (Pair<Executor, CarrierConfigManager.CarrierConfigChangeListener> pair
+                : mCarrierConfigChangedListenerList) {
+            pair.first.execute(() -> pair.second.onCarrierConfigChanged(
+                    /*slotIndex*/ 0, /*subId*/ SUB_ID, /*carrierId*/ 0, /*specificCarrierId*/ 0)
+            );
+        }
+        mTestableLooper.processAllMessages();
+    }
+
+    private void clearInvocationsForMock() {
+        clearInvocations(mSatelliteEntitlementApi);
+        clearInvocations(mSatelliteController);
+    }
+
+    private void setIsQueryAvailableTrue() throws Exception {
+        doReturn(ACTIVE_SUB_ID).when(mMockSubscriptionManagerService).getActiveSubIdList(true);
+        mCarrierConfigBundle.putBoolean(
+                CarrierConfigManager.KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL, true);
+        replaceInstance(SatelliteEntitlementController.class, "mRetryCountPerSub",
+                mSatelliteEntitlementController, new HashMap<>());
+        setInternetConnected(true);
+        setLastQueryTime(0L);
+        replaceInstance(SatelliteEntitlementController.class,
+                "mSatelliteEntitlementResultPerSub", mSatelliteEntitlementController,
+                new HashMap<>());
+    }
+
+    private void setInternetConnected(boolean connected) {
+        NetworkCapabilities networkCapabilities = new NetworkCapabilities.Builder().build();
+
+        if (connected) {
+            networkCapabilities = new NetworkCapabilities.Builder()
+                    .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                    .setTransportInfo(mock(WifiInfo.class))
+                    .build();
+        }
+        doReturn(networkCapabilities).when(mConnectivityManager).getNetworkCapabilities(mNetwork);
+    }
+
+    private void setSatelliteEntitlementResult(int entitlementStatus,
+            List<String> plmnAllowedList) {
+        doReturn(entitlementStatus).when(mSatelliteEntitlementResult).getEntitlementStatus();
+        doReturn(plmnAllowedList).when(mSatelliteEntitlementResult).getAllowedPLMNList();
+    }
+
+    private void setLastQueryTime(Long lastQueryTime) throws Exception {
+        Map<Integer, Long> lastQueryTimePerSub = new HashMap<>();
+        replaceInstance(SatelliteEntitlementController.class, "mLastQueryTimePerSub",
+                mSatelliteEntitlementController, lastQueryTimePerSub);
+        lastQueryTimePerSub.put(SUB_ID, lastQueryTime);
+    }
+
+    public static class TestSatelliteEntitlementController extends SatelliteEntitlementController {
+        private SatelliteEntitlementApi mInjectSatelliteEntitlementApi;
+
+        TestSatelliteEntitlementController(@NonNull Context context, @NonNull Looper looper,
+                SatelliteEntitlementApi api) {
+            super(context, looper);
+            mInjectSatelliteEntitlementApi = api;
+        }
+
+        @Override
+        public SatelliteEntitlementApi getSatelliteEntitlementApi(int subId) {
+            logd("getSatelliteEntitlementApi");
+            return mInjectSatelliteEntitlementApi;
+        }
+    }
+
+    private static void logd(String log) {
+        Log.d(TAG, log);
+    }
+}
diff --git a/tests/src/com/android/phone/satellite/entitlement/SatelliteEntitlementResponseTest.java b/tests/src/com/android/phone/satellite/entitlement/SatelliteEntitlementResponseTest.java
new file mode 100644
index 0000000..45e2a71
--- /dev/null
+++ b/tests/src/com/android/phone/satellite/entitlement/SatelliteEntitlementResponseTest.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2024 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.entitlement;
+
+import static com.android.phone.satellite.entitlement.SatelliteEntitlementResult.SATELLITE_ENTITLEMENT_STATUS_DISABLED;
+import static com.android.phone.satellite.entitlement.SatelliteEntitlementResult.SATELLITE_ENTITLEMENT_STATUS_ENABLED;
+import static com.android.phone.satellite.entitlement.SatelliteEntitlementResult.SATELLITE_ENTITLEMENT_STATUS_INCOMPATIBLE;
+import static com.android.phone.satellite.entitlement.SatelliteEntitlementResult.SATELLITE_ENTITLEMENT_STATUS_PROVISIONING;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.internal.telephony.satellite.SatelliteNetworkInfo;
+import com.android.libraries.entitlement.ServiceEntitlement;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class SatelliteEntitlementResponseTest {
+    private static final String TEST_OTHER_APP_ID = "ap201x";
+    private static final List<SatelliteNetworkInfo> TEST_PLMN_DATA_PLAN_TYPE_LIST = Arrays.asList(
+            new SatelliteNetworkInfo("31026", "unmetered"),
+            new SatelliteNetworkInfo("302820", "metered"));
+    private static final List<String> TEST_PLMN_BARRED_LIST = Arrays.asList("31017", "302020");
+    private static final String RESPONSE_WITHOUT_SATELLITE_APP_ID =
+            "{\"VERS\":{\"version\":\"1\",\"validity\":\"172800\"},"
+                    + "\"TOKEN\":{\"token\":\"ASH127AHHA88SF\"},\""
+                    + TEST_OTHER_APP_ID + "\":{"
+                    + "\"EntitlementStatus\":\"" + SATELLITE_ENTITLEMENT_STATUS_ENABLED + "\"}}";
+    private static final String RESPONSE_WITHOUT_ENTITLEMENT_STATUS =
+            "{\"VERS\":{\"version\":\"1\",\"validity\":\"172800\"},"
+                    + "\"TOKEN\":{\"token\":\"ASH127AHHA88SF\"},\""
+                    + ServiceEntitlement.APP_SATELLITE_ENTITLEMENT + "\":{}}";
+
+    @Test
+    public void testGetSatelliteEntitlementResponse() throws Exception {
+        // Received the body with satellite service enabled.
+        SatelliteEntitlementResponse response = new SatelliteEntitlementResponse(
+                getResponse(SATELLITE_ENTITLEMENT_STATUS_ENABLED));
+        assertEquals(SATELLITE_ENTITLEMENT_STATUS_ENABLED, response.getEntitlementStatus());
+        assertEquals(TEST_PLMN_DATA_PLAN_TYPE_LIST.get(0).mPlmn,
+                response.getPlmnAllowed().get(0).mPlmn);
+        assertEquals(TEST_PLMN_DATA_PLAN_TYPE_LIST.get(0).mDataPlanType,
+                response.getPlmnAllowed().get(0).mDataPlanType);
+        assertEquals(TEST_PLMN_DATA_PLAN_TYPE_LIST.get(1).mPlmn,
+                response.getPlmnAllowed().get(1).mPlmn);
+        assertEquals(TEST_PLMN_DATA_PLAN_TYPE_LIST.get(1).mDataPlanType,
+                response.getPlmnAllowed().get(1).mDataPlanType);
+        assertEquals(TEST_PLMN_BARRED_LIST, response.getPlmnBarredList());
+
+        // Received the empty body.
+        response = new SatelliteEntitlementResponse("");
+        assertEquals(SATELLITE_ENTITLEMENT_STATUS_DISABLED, response.getEntitlementStatus());
+        assertTrue(response.getPlmnAllowed().size() == 0);
+        assertTrue(response.getPlmnBarredList().size() == 0);
+
+        // Received the body without satellite app id.
+        response = new SatelliteEntitlementResponse(RESPONSE_WITHOUT_SATELLITE_APP_ID);
+        assertEquals(SATELLITE_ENTITLEMENT_STATUS_DISABLED, response.getEntitlementStatus());
+        assertTrue(response.getPlmnAllowed().size() == 0);
+        assertTrue(response.getPlmnBarredList().size() == 0);
+
+        // Received the body without EntitlementStatus.
+        response = new SatelliteEntitlementResponse(RESPONSE_WITHOUT_ENTITLEMENT_STATUS);
+        assertEquals(SATELLITE_ENTITLEMENT_STATUS_DISABLED, response.getEntitlementStatus());
+        assertTrue(response.getPlmnAllowed().size() == 0);
+        assertTrue(response.getPlmnBarredList().size() == 0);
+
+        // Received the body with an entitlementStatus value of DISABLED.
+        response = new SatelliteEntitlementResponse(
+                getResponse(SATELLITE_ENTITLEMENT_STATUS_DISABLED));
+        assertEquals(SATELLITE_ENTITLEMENT_STATUS_DISABLED, response.getEntitlementStatus());
+        assertTrue(response.getPlmnAllowed().size() == 0);
+        assertTrue(response.getPlmnBarredList().size() == 0);
+
+        // Received the body with an entitlementStatus value of INCOMPATIBLE.
+        response = new SatelliteEntitlementResponse(
+                getResponse(SATELLITE_ENTITLEMENT_STATUS_INCOMPATIBLE));
+        assertEquals(SATELLITE_ENTITLEMENT_STATUS_INCOMPATIBLE, response.getEntitlementStatus());
+        assertTrue(response.getPlmnAllowed().size() == 0);
+        assertTrue(response.getPlmnBarredList().size() == 0);
+
+        // Received the body with an entitlementStatus value of PROVISIONING.
+        response = new SatelliteEntitlementResponse(
+                getResponse(SATELLITE_ENTITLEMENT_STATUS_PROVISIONING));
+        assertEquals(SATELLITE_ENTITLEMENT_STATUS_PROVISIONING, response.getEntitlementStatus());
+        assertTrue(response.getPlmnAllowed().size() == 0);
+        assertTrue(response.getPlmnBarredList().size() == 0);
+    }
+
+    private String getResponse(int entitlementStatus) {
+        return "{\"VERS\":{\"version\":\"1\",\"validity\":\"172800\"},"
+                + "\"TOKEN\":{\"token\":\"ASH127AHHA88SF\"},\""
+                + ServiceEntitlement.APP_SATELLITE_ENTITLEMENT + "\":{"
+                + "\"EntitlementStatus\":\"" + entitlementStatus + "\""
+                + getPLMNListOrEmpty(entitlementStatus)
+                + "}}";
+    }
+
+    private String getPLMNListOrEmpty(int entitlementStatus) {
+        return entitlementStatus == SATELLITE_ENTITLEMENT_STATUS_ENABLED ? ","
+                + "\"PLMNAllowed\":[{\"PLMN\":\"31026\",\"DataPlanType\":\"unmetered\"},"
+                + "{\"PLMN\":\"302820\",\"DataPlanType\":\"metered\"}],"
+                + "\"PLMNBarred\":[{\"PLMN\":\"31017\"},"
+                + "{\"PLMN\":\"302020\"}]" : "";
+    }
+}