/*
 * 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 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.GuardedBy;
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.SatelliteConstants;
import com.android.internal.telephony.satellite.SatelliteController;
import com.android.internal.telephony.satellite.metrics.EntitlementMetricsStats;
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.ArrayList;
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_SIM_REFRESH = 3;

    /** Retry on next trigger event. */
    private static final int HTTP_RESPONSE_500 = 500;
    /** Retry after the time specified in the 鈥淩etry-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 = 7;
    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. */
    @GuardedBy("mLock")
    private Map<Integer, ExponentialBackoff> mExponentialBackoffPerSub = new HashMap<>();
    /** Map key : subId, value : SatelliteEntitlementResult. */
    @GuardedBy("mLock")
    private Map<Integer, SatelliteEntitlementResult> mSatelliteEntitlementResultPerSub =
            new HashMap<>();
    /** Map key : subId, value : the last query time to millis. */
    @GuardedBy("mLock")
    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'. */
    @GuardedBy("mLock")
    private Map<Integer, Integer> mRetryCountPerSub = new HashMap<>();
    /** Map key : subId, value : Whether query is in progress. */
    @GuardedBy("mLock")
    private Map<Integer, Boolean> mIsEntitlementInProgressPerSub = new HashMap<>();
    /** Map key : slotId, value : The last used subId. */
    @GuardedBy("mLock")
    private Map<Integer, Integer> mSubIdPerSlot = new HashMap<>();
    @NonNull private final EntitlementMetricsStats mEntitlementMetricsStats;

    /**
     * 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();
            }
        };
        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);
        mEntitlementMetricsStats = EntitlementMetricsStats.getOrCreateInstance();
        SatelliteController.getInstance().registerIccRefresh(this, CMD_SIM_REFRESH);
    }

    @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_SIM_REFRESH:
                handleSimRefresh();
                break;
            default:
                logd("do not used this message");
        }
    }

    private void handleCarrierConfigChanged(int slotIndex, int subId, int carrierId,
            int specificCarrierId) {
        logd("handleCarrierConfigChanged(): slotIndex(" + slotIndex + "), subId("
                + subId + "), carrierId(" + carrierId + "), specificCarrierId("
                + specificCarrierId + ")");
        processSimChanged(slotIndex, subId);
        if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
            return;
        }

        sendEmptyMessage(CMD_START_QUERY_ENTITLEMENT);
        synchronized (mLock) {
            mSubIdPerSlot.put(slotIndex, subId);
        }
    }

    // When SIM is removed or changed, then reset the previous subId's retry related objects.
    private void processSimChanged(int slotIndex, int subId) {
        int previousSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
        synchronized (mLock) {
            previousSubId = mSubIdPerSlot.getOrDefault(slotIndex,
                    SubscriptionManager.INVALID_SUBSCRIPTION_ID);
        }
        logd("processSimChanged prev subId:" + previousSubId);
        if (previousSubId != subId) {
            synchronized (mLock) {
                mSubIdPerSlot.remove(slotIndex);
            }
            logd("processSimChanged resetEntitlementQueryPerSubId");
            resetEntitlementQueryPerSubId(previousSubId);
        }
    }

    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 void handleSimRefresh() {
        resetEntitlementQueryCounts(cmdToString(CMD_SIM_REFRESH));
        sendMessageDelayed(obtainMessage(CMD_START_QUERY_ENTITLEMENT),
                TimeUnit.SECONDS.toMillis(10));
    }

    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);
    }

    /**
     * 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() {
        for (int subId : mSubscriptionManagerService.getActiveSubIdList(true)) {
            if (!shouldStartQueryEntitlement(subId)) {
                continue;
            }

            // Check the satellite service query result from the entitlement server for the
            // satellite service.
            try {
                synchronized (mLock) {
                    mIsEntitlementInProgressPerSub.put(subId, true);
                    SatelliteEntitlementResult entitlementResult =  getSatelliteEntitlementApi(
                            subId).checkEntitlementStatus();
                    mSatelliteEntitlementResultPerSub.put(subId, entitlementResult);
                    mEntitlementMetricsStats.reportSuccess(subId,
                            getEntitlementStatus(entitlementResult), false);
                }
            } catch (ServiceEntitlementException e) {
                loge(e.toString());
                mEntitlementMetricsStats.reportError(subId, e.getErrorCode(), false);
                if (!isInternetConnected()) {
                    logd("StartQuery: disconnected. " + e);
                    synchronized (mLock) {
                        mIsEntitlementInProgressPerSub.remove(subId);
                    }
                    return;
                }
                if (isPermanentError(e)) {
                    logd("StartQuery: shouldPermanentError.");
                    queryCompleted(subId);
                    continue;
                } else if (isRetryAfterError(e)) {
                    long retryAfterSeconds = parseSecondsFromRetryAfter(e.getRetryAfter());
                    logd("StartQuery: next retry will be in " + TimeUnit.SECONDS.toMillis(
                            retryAfterSeconds) + " sec");
                    sendMessageDelayed(obtainMessage(CMD_RETRY_QUERY_ENTITLEMENT, subId, 0),
                            TimeUnit.SECONDS.toMillis(retryAfterSeconds));
                    stopExponentialBackoff(subId);
                    continue;
                } else {
                    startExponentialBackoff(subId);
                    continue;
                }
            }
            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);
        synchronized (mLock) {
            mLastQueryTimePerSub = new HashMap<>();
            mExponentialBackoffPerSub = new HashMap<>();
            mRetryCountPerSub = new HashMap<>();
            mIsEntitlementInProgressPerSub = 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) {
        if (!shouldRetryQueryEntitlement(subId)) {
            return;
        }
        try {
            synchronized (mLock) {
                int currentRetryCount = getRetryCount(subId);
                mRetryCountPerSub.put(subId, currentRetryCount + 1);
                logd("[" + subId + "] retry cnt:" + getRetryCount(subId));
                SatelliteEntitlementResult entitlementResult =  getSatelliteEntitlementApi(
                        subId).checkEntitlementStatus();
                mSatelliteEntitlementResultPerSub.put(subId, entitlementResult);
                mEntitlementMetricsStats.reportSuccess(subId,
                        getEntitlementStatus(entitlementResult), true);
            }
        } catch (ServiceEntitlementException e) {
            loge(e.toString());
            mEntitlementMetricsStats.reportError(subId, e.getErrorCode(), true);
            if (!isRetryAvailable(subId)) {
                logd("retryQuery: unavailable.");
                queryCompleted(subId);
                return;
            }
            if (!isInternetConnected()) {
                logd("retryQuery: Internet disconnected.");
                stopExponentialBackoff(subId);
                synchronized (mLock) {
                    mIsEntitlementInProgressPerSub.remove(subId);
                }
                return;
            }
            if (isPermanentError(e)) {
                logd("retryQuery: shouldPermanentError.");
                queryCompleted(subId);
                return;
            } else if (isRetryAfterError(e)) {
                long retryAfterSeconds = parseSecondsFromRetryAfter(e.getRetryAfter());
                logd("retryQuery: next retry will be in " + TimeUnit.SECONDS.toMillis(
                        retryAfterSeconds) + " sec");
                sendMessageDelayed(obtainMessage(CMD_RETRY_QUERY_ENTITLEMENT, subId, 0),
                        TimeUnit.SECONDS.toMillis(retryAfterSeconds));
                stopExponentialBackoff(subId);
                return;
            } else {
                ExponentialBackoff exponentialBackoff = null;
                synchronized (mLock) {
                    exponentialBackoff = mExponentialBackoffPerSub.get(subId);
                }
                if (exponentialBackoff == null) {
                    startExponentialBackoff(subId);
                } else {
                    exponentialBackoff.notifyFailed();
                    logd("retryQuery: The next retry will be in "
                            + exponentialBackoff.getCurrentDelay() + " ms.");
                }
                return;
            }
        }
        queryCompleted(subId);
    }

    // If the 500 response is received, no retry until the next trigger event occurs.
    private boolean isPermanentError(ServiceEntitlementException e) {
        return e.getHttpStatus() == HTTP_RESPONSE_500;
    }

    /** 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. */
    private boolean isRetryAfterError(ServiceEntitlementException e) {
        int responseCode = e.getHttpStatus();
        logd("shouldRetryAfterError: received the " + responseCode);
        if (responseCode == HTTP_RESPONSE_503 && e.getRetryAfter() != null
                && !e.getRetryAfter().isEmpty()) {
            long retryAfterSeconds = parseSecondsFromRetryAfter(e.getRetryAfter());
            if (retryAfterSeconds == -1) {
                logd("Unable parsing the retry-after. try to exponential backoff.");
                return false;
            }
            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) {
        ExponentialBackoff exponentialBackoff = null;
        stopExponentialBackoff(subId);
        synchronized (mLock) {
            mExponentialBackoffPerSub.put(subId,
                    new ExponentialBackoff(INITIAL_DELAY_MILLIS, MAX_DELAY_MILLIS,
                            MULTIPLIER, this.getLooper(), () -> {
                        synchronized (mLock) {
                            sendMessage(obtainMessage(CMD_RETRY_QUERY_ENTITLEMENT, subId, 0));
                        }
                    }));
            exponentialBackoff = mExponentialBackoffPerSub.get(subId);
        }
        if (exponentialBackoff != null) {
            exponentialBackoff.start();
            logd("start ExponentialBackoff, cnt: " + getRetryCount(subId) + ". Retrying in "
                    + exponentialBackoff.getCurrentDelay() + " ms.");
        }
    }

    /** If the Internet connection is lost during the ExponentialBackoff, stop the
     * ExponentialBackoff and reset it. */
    private void stopExponentialBackoff(int subId) {
        synchronized (mLock) {
            if (mExponentialBackoffPerSub.get(subId) != null) {
                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) {
        SatelliteEntitlementResult entitlementResult;
        synchronized (mLock) {
            if (!mSatelliteEntitlementResultPerSub.containsKey(subId)) {
                logd("queryCompleted: create default SatelliteEntitlementResult");
                mSatelliteEntitlementResultPerSub.put(subId,
                        SatelliteEntitlementResult.getDefaultResult());
            }
            entitlementResult = mSatelliteEntitlementResultPerSub.get(subId);
            stopExponentialBackoff(subId);
            mIsEntitlementInProgressPerSub.remove(subId);
            logd("reset retry count for refresh query");
            mRetryCountPerSub.remove(subId);
        }

        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, entitlementResult.getEntitlementStatus() ==
                        SatelliteEntitlementResult.SATELLITE_ENTITLEMENT_STATUS_ENABLED,
                entitlementResult.getAllowedPLMNList(), entitlementResult.getBarredPLMNList());
    }

    private boolean shouldStartQueryEntitlement(int subId) {
        logd("shouldStartQueryEntitlement " + subId);
        if (!shouldRetryQueryEntitlement(subId)) {
            return false;
        }

        synchronized (mLock) {
            if (mIsEntitlementInProgressPerSub.getOrDefault(subId, false)) {
                logd("In progress retry");
                return false;
            }
        }
        return true;
    }

    private boolean shouldRetryQueryEntitlement(int subId) {
        if (!isSatelliteEntitlementSupported(subId)) {
            logd("Doesn't support entitlement query for satellite.");
            resetSatelliteEntitlementRestrictedReason(subId);
            return false;
        }

        if (!isInternetConnected()) {
            stopExponentialBackoff(subId);
            synchronized (mLock) {
                mIsEntitlementInProgressPerSub.remove(subId);
            }
            logd("Internet disconnected");
            return false;
        }

        if (!shouldRefreshEntitlementStatus(subId)) {
            return false;
        }

        return isRetryAvailable(subId);
    }

    // update for removing the satellite entitlement restricted reason
    private void resetSatelliteEntitlementRestrictedReason(int subId) {
        SatelliteEntitlementResult previousResult;
        SatelliteEntitlementResult enabledResult = new SatelliteEntitlementResult(
                SatelliteEntitlementResult.SATELLITE_ENTITLEMENT_STATUS_ENABLED,
                new ArrayList<>(), new ArrayList<>());
        synchronized (mLock) {
            previousResult = mSatelliteEntitlementResultPerSub.get(subId);
        }
        if (previousResult != null && previousResult.getEntitlementStatus()
                != SatelliteEntitlementResult.SATELLITE_ENTITLEMENT_STATUS_ENABLED) {
            logd("set enabled status for removing satellite entitlement restricted reason");
            synchronized (mLock) {
                mSatelliteEntitlementResultPerSub.put(subId, enabledResult);
            }
            updateSatelliteEntitlementStatus(subId, true, enabledResult.getAllowedPLMNList(),
                    enabledResult.getBarredPLMNList());
        }
        resetEntitlementQueryPerSubId(subId);
    }

    private void resetEntitlementQueryPerSubId(int subId) {
        logd("resetEntitlementQueryPerSubId: " + subId);
        stopExponentialBackoff(subId);
        synchronized (mLock) {
            mLastQueryTimePerSub.remove(subId);
            mRetryCountPerSub.remove(subId);
            mIsEntitlementInProgressPerSub.remove(subId);
        }
        removeMessages(CMD_RETRY_QUERY_ENTITLEMENT,
                obtainMessage(CMD_RETRY_QUERY_ENTITLEMENT, subId, 0));
    }

    /**
     * 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) {
        synchronized (mLock) {
            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);
    }

    private boolean isRetryAvailable(int subId) {
        if (getRetryCount(subId) >= MAX_RETRY_COUNT) {
            logd("The retry will not be attempted until the next trigger event.");
            return false;
        }
        return true;
    }

    /** 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,
                CarrierConfigManager.KEY_SATELLITE_ENTITLEMENT_APP_NAME_STRING);
        if (config == null || config.isEmpty()) {
            config = CarrierConfigManager.getDefaultConfig();
        }
        return config;
    }

    private void saveLastQueryTime(int subId) {
        long lastQueryTimeMillis = System.currentTimeMillis();
        synchronized (mLock) {
            mLastQueryTimePerSub.put(subId, lastQueryTimeMillis);
        }
    }

    private int getRetryCount(int subId) {
        synchronized (mLock) {
            return mRetryCountPerSub.getOrDefault(subId, 0);
        }
    }

    /**
     * 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, List<String> plmnBarredList) {
        SatelliteController.getInstance().onSatelliteEntitlementStatusUpdated(subId, enabled,
                plmnAllowedList, plmnBarredList, null);
    }

    private @SatelliteConstants.SatelliteEntitlementStatus int getEntitlementStatus(
            SatelliteEntitlementResult entitlementResult) {
        switch (entitlementResult.getEntitlementStatus()) {
            case SatelliteEntitlementResult.SATELLITE_ENTITLEMENT_STATUS_DISABLED:
                return SatelliteConstants.SATELLITE_ENTITLEMENT_STATUS_DISABLED;
            case SatelliteEntitlementResult.SATELLITE_ENTITLEMENT_STATUS_ENABLED:
                return SatelliteConstants.SATELLITE_ENTITLEMENT_STATUS_ENABLED;
            case SatelliteEntitlementResult.SATELLITE_ENTITLEMENT_STATUS_INCOMPATIBLE:
                return SatelliteConstants.SATELLITE_ENTITLEMENT_STATUS_INCOMPATIBLE;
            case SatelliteEntitlementResult.SATELLITE_ENTITLEMENT_STATUS_PROVISIONING:
                return SatelliteConstants.SATELLITE_ENTITLEMENT_STATUS_PROVISIONING;
            default:
                return SatelliteConstants.SATELLITE_ENTITLEMENT_STATUS_UNKNOWN;
        }
    }

    private static String cmdToString(int cmd) {
        switch (cmd) {
            case CMD_SIM_REFRESH:
                return "SIM_REFRESH";
            default:
                return "UNKNOWN(" + cmd + ")";
        }
    }

    private static void logd(String log) {
        Rlog.d(TAG, log);
    }

    private static void loge(String log) {
        Rlog.e(TAG, log);
    }
}