/*
 * Copyright (C) 2018 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.ons;

import static android.telephony.TelephonyManager.ENABLE_FEATURE_MAPPING;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.Service;
import android.app.compat.CompatChanges;
import android.compat.Compatibility;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledAfter;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.os.SystemProperties;
import android.os.TelephonyServiceManager.ServiceRegisterer;
import android.os.UserHandle;
import android.os.UserManager;
import android.telephony.AvailableNetworkInfo;
import android.telephony.CarrierConfigManager;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyFrameworkInitializer;
import android.telephony.TelephonyManager;
import android.util.Log;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.IOns;
import com.android.internal.telephony.ISetOpportunisticDataCallback;
import com.android.internal.telephony.IUpdateAvailableNetworksCallback;
import com.android.internal.telephony.TelephonyIntents;
import com.android.internal.telephony.TelephonyPermissions;
import com.android.internal.telephony.flags.Flags;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;

/**
 * OpportunisticNetworkService implements ions.
 * It scans network and matches the results with opportunistic subscriptions.
 * Use the same to provide user opportunistic data in areas with corresponding networks
 *
 * Thread safety: The OpportunisticNetworkService (ONS) instantiates a single worker HandlerThread
 * on initialization. This thread is used by all handlers in the ONS, which allows most ONS state to
 * be thread-safe by default. To keep this true, adhere to the following policies:
 *
 * - Do not create new threads - use the ONS worker thread to create new handlers
 * - Do not lock longer than is necessary to update state
 * - If possible, isolate read/mutation of proteced state to helpers to ensure locking is short
 * - Post work for external calls to a handler as soon as preconditions are checked
 * - External calls that cannot be posted to a handler must ensure they are locking any accessed
 *   state
 *
 * Note: Until com.android.internal.telephony.flags.ons_thread_consolidation is cleaned up, an older
 * threading model exists in parallel to the previously described model. This model involves
 * separate threads for OpportunisticNetworkService, ONSProfileSelector, and
 * ONSNetworkScanCtrl. Locking and which thread work should be run on is not consistent and should
 * be altered with caution.
 */
public class OpportunisticNetworkService extends Service {
    @VisibleForTesting protected Context mContext;
    private TelephonyManager mTelephonyManager;
    @VisibleForTesting protected PackageManager mPackageManager;
    @VisibleForTesting protected SubscriptionManager mSubscriptionManager;
    @VisibleForTesting protected ONSProfileActivator mONSProfileActivator;
    private ONSStats mONSStats;
    private Handler mHandler = null;

    private final FlaggedLock mFlaggedLock =
            new FlaggedLock(new ReentrantLock(), Flags.onsThreadConsolidation());
    @GuardedBy("mFlaggedLock")
    @VisibleForTesting protected boolean mIsEnabled;
    @VisibleForTesting protected ONSProfileSelector mProfileSelector;
    @GuardedBy("mFlaggedLock")
    private SharedPreferences mSharedPref;
    @GuardedBy("mFlaggedLock")
    @VisibleForTesting protected HashMap<String, ONSConfigInput> mONSConfigInputHashMap;
    private int mVendorApiLevel;

    private static final String TAG = "ONS";
    private static final String PREF_NAME = TAG;
    private static final String PREF_ENABLED = "isEnabled";
    private static final String CARRIER_APP_CONFIG_NAME = "carrierApp";
    private static final String SYSTEM_APP_CONFIG_NAME = "systemApp";
    private static final boolean DBG = true;
    /** Message to indicate sim state update */
    private static final int MSG_SIM_STATE_CHANGE = 1;
    @VisibleForTesting protected CarrierConfigManager mCarrierConfigManager;
    @VisibleForTesting protected UserManager mUserManager;

    /**
     * To expand the error codes for {@link TelephonyManager#updateAvailableNetworks} and
     * {@link TelephonyManager#setPreferredOpportunisticDataSubscription}.
     */
    @ChangeId
    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.Q)
    static final long CALLBACK_ON_MORE_ERROR_CODE_CHANGE = 130595455L;

    /**
     * Profile selection callback. Will be called once Profile selector decides on
     * the opportunistic data profile.
     */
    private ONSProfileSelector.ONSProfileSelectionCallback  mProfileSelectionCallback =
            new ONSProfileSelector.ONSProfileSelectionCallback() {

                @Override
                public void onProfileSelectionDone() {
                    logDebug("profile selection done");
                }
            };

    /** Broadcast receiver to get SIM card state changed event */
    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            mHandler.sendEmptyMessage(MSG_SIM_STATE_CHANGE);
        }
    };

    private void createMsgHandler() {
        mHandler = new Handler(Looper.myLooper()) {
            @Override
            public void handleMessage(Message msg) {
                // With flag, mONSConfigInputMap locking is handled via setCarrierAppConfig
                mFlaggedLock.runLockedWhenDisabled(() -> {
                    switch (msg.what) {
                        case MSG_SIM_STATE_CHANGE:
                            handleSimStateChange();
                            break;
                        default:
                            log("invalid message");
                            break;
                    }
                });
            }
        };
    }

    private void startWorkerThreadAndInit() {
        Thread thread = new Thread() {
            @Override
            public void run() {
                super.run();
                Looper.prepare();
                Looper looper = Looper.myLooper();
                initialize(getBaseContext());
                synchronized (this) {
                    this.notifyAll();
                }
                looper.loop();
            }
        };

        synchronized (thread) {
            thread.start();
            try {
                thread.wait();
            } catch (Exception e) {
                log(e.getLocalizedMessage());
            }
        }
    }

    private static boolean enforceModifyPhoneStatePermission(Context context) {
        return context.checkCallingOrSelfPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
                == PackageManager.PERMISSION_GRANTED;
    }

    @VisibleForTesting
    protected void handleSimStateChange() {
        logDebug("SIM state changed");

        ONSConfigInput carrierAppConfigInput = getCarrierAppConfig();
        if (carrierAppConfigInput == null) {
            return;
        }
        List<SubscriptionInfo> subscriptionInfos =
            mSubscriptionManager.getActiveSubscriptionInfoList(false);
        if (subscriptionInfos == null) {
          return;
        }

        logDebug("handleSimStateChange: subscriptionInfos - " + subscriptionInfos);
        for (SubscriptionInfo subscriptionInfo : subscriptionInfos) {
            if (subscriptionInfo.getSubscriptionId() == carrierAppConfigInput.primarySub()) {
                return;
            }
        }

        logDebug("Carrier subscription is not available, removing entry");
        setCarrierAppConfig(null);
        if (!getEnableState()) {
            return;
        }
        ONSConfigInput systemConfig = getSystemAppConfig();
        if (systemConfig != null) {
            mProfileSelector.startProfileSelection(
                    systemConfig.availableNetworkInfos(),
                    systemConfig.availableNetworkCallback());
        }
    }

    private boolean hasOpportunisticSubPrivilege(String callingPackage, int subId) {
        return mTelephonyManager.hasCarrierPrivileges(subId)
                || canManageSubscription(
                mProfileSelector.getOpportunisticSubInfo(subId), callingPackage);
    }

    private final IOns.Stub mBinder = new IOns.Stub() {
        /**
         * Enable or disable Opportunistic Network service.
         *
         * This method should be called to enable or disable
         * OpportunisticNetwork service on the device.
         *
         * <p>
         * Requires Permission:
         *   {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE}
         * Or the calling app has carrier privileges. @see #hasCarrierPrivileges
         *
         * @param enable enable(True) or disable(False)
         * @param callingPackage caller's package name
         * @return returns true if successfully set.
         */
        @Override
        public boolean setEnable(boolean enable, String callingPackage) {
            TelephonyPermissions.enforceCallingOrSelfModifyPermissionOrCarrierPrivilege(
                    mContext, SubscriptionManager.getDefaultSubscriptionId(), "setEnable");

            enforceTelephonyFeatureWithException(callingPackage,
                    PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS, "setEnable");

            log("setEnable: " + enable);

            final long identity = Binder.clearCallingIdentity();
            try {
                enableOpportunisticNetwork(enable);
            } finally {
                Binder.restoreCallingIdentity(identity);
            }

            return true;
        }

        /**
         * Is Opportunistic Network service enabled
         *
         * This method should be called to determine if the Opportunistic Network service
         * is enabled
         *
         * <p>
         * Requires Permission:
         *   {@link android.Manifest.permission#READ_PRIVILEGED_PHONE_STATE}
         * Or the calling app has carrier privileges. @see #hasCarrierPrivileges
         *
         * @param callingPackage caller's package name
         */
        @Override
        public boolean isEnabled(String callingPackage) {
            TelephonyPermissions
                    .enforceCallingOrSelfReadPrivilegedPhoneStatePermissionOrCarrierPrivilege(
                            mContext, SubscriptionManager.getDefaultSubscriptionId(), "isEnabled");

            enforceTelephonyFeatureWithException(callingPackage,
                    PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS, "isEnabled");

            return getEnableState();
        }

        /**
         * Set preferred opportunistic data.
         *
         * <p>Requires that the calling app has carrier privileges on both primary and
         * secondary subscriptions (see
         * {@link TelephonyManager#hasCarrierPrivileges}), or has permission
         * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE}.
         * @param subId which opportunistic subscription
         * {@link SubscriptionManager#getOpportunisticSubscriptions} is preferred for cellular data.
         * Pass {@link SubscriptionManager#DEFAULT_SUBSCRIPTION_ID} to unset the preference
         * @param needValidation whether validation is needed before switch happens.
         * @param callback callback upon request completion.
         * @param callingPackage caller's package name
         *
         */
        public void setPreferredDataSubscriptionId(int subId, boolean needValidation,
                ISetOpportunisticDataCallback callback, String callingPackage) {
            logDebug("setPreferredDataSubscriptionId subId:" + subId
                    + " callingPackage:" + callingPackage);
            if (!enforceModifyPhoneStatePermission(mContext)) {
                TelephonyPermissions.enforceCallingOrSelfCarrierPrivilege(mContext,
                        SubscriptionManager.getDefaultSubscriptionId(),
                        "setPreferredDataSubscriptionId");
                if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
                    TelephonyPermissions.enforceCallingOrSelfCarrierPrivilege(mContext, subId,
                            "setPreferredDataSubscriptionId");
                }
            } else {
                if (getCarrierAppConfig() != null) {
                    sendSetOpptCallbackHelper(callback,
                        TelephonyManager.SET_OPPORTUNISTIC_SUB_VALIDATION_FAILED);
                    return;
                }
            }

            enforceTelephonyFeatureWithException(callingPackage,
                    PackageManager.FEATURE_TELEPHONY_DATA, "setPreferredDataSubscriptionId");

            final long identity = Binder.clearCallingIdentity();
            try {
                runOrPostToWorkerThread(() ->
                    mProfileSelector.selectProfileForData(subId, needValidation, callback));
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }

        /**
         * Get preferred default data sub Id
         *
         * <p>Requires that the calling app has carrier privileges
         * (see {@link TelephonyManager#hasCarrierPrivileges}),or has either
         * {@link android.Manifest.permission#READ_PRIVILEGED_PHONE_STATE} or.
         * {@link android.Manifest.permission#READ_PHONE_STATE} permission.
         * @return subId preferred opportunistic subscription id or
         * {@link SubscriptionManager#DEFAULT_SUBSCRIPTION_ID} if there are no preferred
         * subscription id
         *
         */
        @Override
        public int getPreferredDataSubscriptionId(String callingPackage,
                String callingFeatureId) {
            if (!TelephonyPermissions.checkReadPhoneStateOnAnyActiveSub(
                    mContext,
                    Binder.getCallingPid(),
                    Binder.getCallingUid(),
                    callingPackage,
                    callingFeatureId,
                    "getPreferredDataSubscriptionId")) {
                throw new SecurityException(
                        "getPreferredDataSubscriptionId requires READ_PHONE_STATE,"
                        + " READ_PRIVILEGED_PHONE_STATE, or carrier privileges on"
                        + " any active subscription.");
            }

            enforceTelephonyFeatureWithException(callingPackage,
                    PackageManager.FEATURE_TELEPHONY_DATA, "getPreferredDataSubscriptionId");

            final long identity = Binder.clearCallingIdentity();
            try {
                return mProfileSelector.getPreferredDataSubscriptionId();
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }

        /**
         * Update availability of a list of networks in the current location.
         *
         * This api should be called if the caller is aware of the availability of a network
         * at the current location. This information will be used by OpportunisticNetwork service
         * to decide to attach to the network. If an empty list is passed,
         * it is assumed that no network is available.
         *  @param availableNetworks is a list of available network information.
         *  @param callback callback upon request completion.
         *  @param callingPackage caller's package name
         * <p>
         * <p>Requires that the calling app has carrier privileges on both primary and
         * secondary subscriptions (see
         * {@link TelephonyManager#hasCarrierPrivileges}), or has permission
         * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE}.
         *
         */
        public void updateAvailableNetworks(List<AvailableNetworkInfo> availableNetworks,
                IUpdateAvailableNetworksCallback callback, String callingPackage) {
            logDebug("updateAvailableNetworks: " + availableNetworks);
            // check if system app
            if (enforceModifyPhoneStatePermission(mContext)) {

                enforceTelephonyFeatureWithException(callingPackage,
                        PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS, "updateAvailableNetworks");

                final long identity = Binder.clearCallingIdentity();
                try {
                    runOrPostToWorkerThread(() ->
                        handleSystemAppAvailableNetworks(availableNetworks, callback));
                } finally {
                    Binder.restoreCallingIdentity(identity);
                }
            } else {
                // check if the app has primary carrier permission
                TelephonyPermissions.enforceCallingOrSelfCarrierPrivilege(mContext,
                        SubscriptionManager.getDefaultSubscriptionId(), "updateAvailableNetworks");

                enforceTelephonyFeatureWithException(callingPackage,
                        PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS, "updateAvailableNetworks");

                if (Flags.onsThreadConsolidation() && availableNetworks != null
                        && !availableNetworks.isEmpty()) {
                    if (!enforceAvailableNetworksSize(availableNetworks, callback)) {
                        return;
                    }

                    if (!enforceAvailableNetworksContainsOppSub(availableNetworks, callback)) {
                        return;
                    }

                    if (!enforceCarrierPrivilegeOrCanManageSubPermission(
                           availableNetworks, callback, callingPackage)) {
                        return;
                    }
                }

                if (Flags.onsThreadConsolidation()) {
                    final long identity = Binder.clearCallingIdentity();
                    try {
                        runOrPostToWorkerThread(() ->
                            handleCarrierAppAvailableNetworks(availableNetworks, callback,
                                    callingPackage));
                    } finally {
                        Binder.restoreCallingIdentity(identity);
                    }
                } else {
                    runOrPostToWorkerThread(() ->
                        handleCarrierAppAvailableNetworks(availableNetworks, callback,
                                callingPackage));
                }
            }
        }

        private void runOrPostToWorkerThread(Runnable task) {
            if (Flags.onsThreadConsolidation()) {
                mHandler.post(task);
            } else {
                task.run();
            }
        }

        /**
         * Returns true if all size conditions are met, otherwise
         * returns false and invokes the appropriate callback if
         * applicable.
         */
        private boolean enforceAvailableNetworksSize(
                @NonNull List<AvailableNetworkInfo> availableNetworks,
                IUpdateAvailableNetworksCallback callbackStub) {
            /* carrier apps should report only subscription */
            if (availableNetworks.size() != 1) {
                log("Carrier app should not pass more than one subscription");
                if (Compatibility.isChangeEnabled(CALLBACK_ON_MORE_ERROR_CODE_CHANGE)) {
                    sendUpdateNetworksCallbackHelper(callbackStub,
                            TelephonyManager
                                    .UPDATE_AVAILABLE_NETWORKS_MULTIPLE_NETWORKS_NOT_SUPPORTED);
                } else {
                    sendUpdateNetworksCallbackHelper(callbackStub,
                            TelephonyManager.UPDATE_AVAILABLE_NETWORKS_INVALID_ARGUMENTS);
                }
                return false;
            }
            return true;
        }

        /**
         * Returns true if availableNetworks contains an opportunistic
         * subscription, otherwise returns false and invokes the
         * appropriate callback if applicable.
         */
        private boolean enforceAvailableNetworksContainsOppSub(
                @NonNull List<AvailableNetworkInfo> availableNetworks,
                IUpdateAvailableNetworksCallback callbackStub) {
            if (!mProfileSelector.hasOpportunisticSub(availableNetworks)) {
                log("No opportunistic subscriptions received");
                if (Compatibility.isChangeEnabled(CALLBACK_ON_MORE_ERROR_CODE_CHANGE)) {
                    sendUpdateNetworksCallbackHelper(callbackStub,
                            TelephonyManager
                                    .UPDATE_AVAILABLE_NETWORKS_NO_OPPORTUNISTIC_SUB_AVAILABLE);
                } else {
                    sendUpdateNetworksCallbackHelper(callbackStub,
                            TelephonyManager.UPDATE_AVAILABLE_NETWORKS_INVALID_ARGUMENTS);
                }
                return false;
            }
            return true;
        }

        /**
         * Returns true if all permissions requirements are met,
         * otherwise returns false and invokes the appropriate
         * callback if applicable.
         */
        private boolean enforceCarrierPrivilegeOrCanManageSubPermission(
                @NonNull List<AvailableNetworkInfo> availableNetworks,
                IUpdateAvailableNetworksCallback callbackStub, String callingPackage) {
            for (AvailableNetworkInfo availableNetworkInfo : availableNetworks) {
                final long identity = Binder.clearCallingIdentity();
                boolean isActiveSubId = false;
                try {
                    isActiveSubId =
                            mSubscriptionManager.isActiveSubId(availableNetworkInfo.getSubId());
                } finally {
                    Binder.restoreCallingIdentity(identity);
                }
                if (isActiveSubId) {
                    TelephonyPermissions.enforceCallingOrSelfCarrierPrivilege(mContext,
                        availableNetworkInfo.getSubId(), "updateAvailableNetworks");
                } else {
                    // check if the app has opportunistic carrier permission
                    if (!hasOpportunisticSubPrivilege(callingPackage,
                        availableNetworkInfo.getSubId())) {
                        log("No carrier privilege for opportunistic subscription");
                        sendUpdateNetworksCallbackHelper(callbackStub,
                            TelephonyManager.UPDATE_AVAILABLE_NETWORKS_NO_CARRIER_PRIVILEGE);
                        return false;
                    }
                }
            }
            return true;
        }

        /**
         * Dump the state of {@link IOns}.
         */
        @Override
        public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter printWriter,
                @NonNull String[] args) {
            OpportunisticNetworkService.this.dump(fd, printWriter, args);
        }
    };

    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }

    @Override
    public void onCreate() {
        startWorkerThreadAndInit();

        // register the service
        ServiceRegisterer opportunisticNetworkServiceRegisterer = TelephonyFrameworkInitializer
                .getTelephonyServiceManager()
                .getOpportunisticNetworkServiceRegisterer();
        if (opportunisticNetworkServiceRegisterer.get() == null) {
            opportunisticNetworkServiceRegisterer.register(mBinder);
        }
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        mHandler.post(new Runnable() {

            private Intent mIntent = null;
            Runnable setIntent(Intent intent) {
                mIntent = intent;
                return this;
            }

            @Override
            public void run() {
                if (mIntent == null) {
                    return;
                }

                String action = mIntent.getAction();
                if (action == null) {
                    return;
                }

                switch (action) {
                    case ONSProfileSelector.ACTION_SUB_SWITCH: {
                        if (mProfileSelector != null) {
                            mProfileSelector.onSubSwitchComplete(mIntent);
                        }
                    }
                    break;

                    case ONSProfileDownloader.ACTION_ONS_ESIM_DOWNLOAD: {
                        mONSProfileActivator.getONSProfileDownloader().onCallbackIntentReceived(
                                mIntent.getParcelableExtra(Intent.EXTRA_INTENT),
                                mIntent.getIntExtra(ONSProfileResultReceiver.EXTRA_RESULT_CODE, 0)
                        );
                    }
                    break;

                    case ONSProfileConfigurator.ACTION_ONS_ESIM_CONFIG: {
                        mONSProfileActivator.getONSProfileConfigurator().onCallbackIntentReceived(
                                mIntent.getParcelableExtra(Intent.EXTRA_INTENT),
                                mIntent.getIntExtra(ONSProfileResultReceiver.EXTRA_RESULT_CODE, 0)
                        );
                    }
                    break;
                }
            }
        }.setIntent(intent));

        return START_STICKY;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        log("Destroyed Successfully...");
        mHandler.getLooper().quitSafely();

        if (mCarrierConfigManager != null && mCarrierConfigChangeListener != null) {
            mCarrierConfigManager.unregisterCarrierConfigChangeListener(
                    mCarrierConfigChangeListener);
        }
    }

    /**
     * Initialize ONS and register as service.
     * Read persistent state to update enable state
     * Start subcomponents if already enabled.
     * @param context context instance
     */
    @VisibleForTesting
    protected void initialize(Context context) {
        mContext = context;
        createMsgHandler();
        mTelephonyManager = TelephonyManager.from(mContext);
        mProfileSelector = new ONSProfileSelector(mContext, mProfileSelectionCallback);
        mSharedPref = mContext.createDeviceProtectedStorageContext().getSharedPreferences(
                PREF_NAME, Context.MODE_PRIVATE);
        mSubscriptionManager = (SubscriptionManager) mContext.getSystemService(
                Context.TELEPHONY_SUBSCRIPTION_SERVICE);
        if (Flags.workProfileApiSplit()) {
            mSubscriptionManager = mSubscriptionManager.createForAllUserProfiles();
        }
        mONSConfigInputHashMap = new HashMap<String, ONSConfigInput>();
        mONSStats = new ONSStats(mContext, mSubscriptionManager);
        mContext.registerReceiver(mBroadcastReceiver,
            new IntentFilter(TelephonyIntents.ACTION_SIM_STATE_CHANGED));
        enableOpportunisticNetwork(getPersistentEnableState());
        mONSProfileActivator = new ONSProfileActivator(mContext, mONSStats);
        mCarrierConfigManager = mContext.getSystemService(CarrierConfigManager.class);
        if (mCarrierConfigManager != null) {
            // Registers for carrier config changes and runs on handler thread
            mCarrierConfigManager.registerCarrierConfigChangeListener(mHandler::post,
                    mCarrierConfigChangeListener);
        }
        mUserManager = mContext.getSystemService(UserManager.class);
        mPackageManager = mContext.getPackageManager();
        mVendorApiLevel = SystemProperties.getInt(
                "ro.vendor.api_level", Build.VERSION.DEVICE_INITIAL_SDK_INT);
    }

    /**
     * This is only register CarrierConfigChangeListener for testing
     */
    @VisibleForTesting
    protected void registerCarrierConfigChangListener() {
        mCarrierConfigManager.registerCarrierConfigChangeListener(mHandler::post,
                mCarrierConfigChangeListener);
    }

    private final CarrierConfigManager.CarrierConfigChangeListener mCarrierConfigChangeListener =
            new CarrierConfigManager.CarrierConfigChangeListener() {
                @Override
                public void onCarrierConfigChanged(int logicalSlotIndex, int subscriptionId,
                        int carrierId, int specificCarrierId) {

                    boolean isUserUnlocked = mUserManager.isUserUnlocked();
                    if (isUserUnlocked) {
                        mONSProfileActivator.handleCarrierConfigChange();
                    } else {
                        log("User is locked");
                        // Register the UnlockReceiver for trigger after Unlocked.
                        mContext.registerReceiver(mUserUnlockedReceiver, new IntentFilter(
                                        Intent.ACTION_USER_UNLOCKED));
                    }
                }
            };

    /**
     * This is only sent to registered receivers, not manifest receivers.
     * Note: The user's actual state might have changed by the time the broadcast is received.
     */
    private final BroadcastReceiver mUserUnlockedReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (Intent.ACTION_USER_UNLOCKED.equals(intent.getAction())) {
                log("Received UserUnlockedReceiver");
                mONSProfileActivator.handleCarrierConfigChange();
            }
        }
    };

    private void handleCarrierAppAvailableNetworks(
            List<AvailableNetworkInfo> availableNetworks,
            IUpdateAvailableNetworksCallback callbackStub, String callingPackage) {
        boolean isEnabled = getEnableState();
        if (availableNetworks != null && !availableNetworks.isEmpty()) {
            if (!Flags.onsThreadConsolidation()) {
                /* carrier apps should report only subscription */
                if (availableNetworks.size() > 1) {
                    log("Carrier app should not pass more than one subscription");
                    if (Compatibility.isChangeEnabled(CALLBACK_ON_MORE_ERROR_CODE_CHANGE)) {
                        sendUpdateNetworksCallbackHelper(callbackStub,
                                TelephonyManager
                                        .UPDATE_AVAILABLE_NETWORKS_MULTIPLE_NETWORKS_NOT_SUPPORTED);
                    } else {
                        sendUpdateNetworksCallbackHelper(callbackStub,
                                TelephonyManager.UPDATE_AVAILABLE_NETWORKS_INVALID_ARGUMENTS);
                    }
                    return;
                }

                if (!mProfileSelector.hasOpportunisticSub(availableNetworks)) {
                    log("No opportunistic subscriptions received");
                    if (Compatibility.isChangeEnabled(CALLBACK_ON_MORE_ERROR_CODE_CHANGE)) {
                        sendUpdateNetworksCallbackHelper(callbackStub,
                                TelephonyManager
                                        .UPDATE_AVAILABLE_NETWORKS_NO_OPPORTUNISTIC_SUB_AVAILABLE);
                    } else {
                        sendUpdateNetworksCallbackHelper(callbackStub,
                                TelephonyManager.UPDATE_AVAILABLE_NETWORKS_INVALID_ARGUMENTS);
                    }
                    return;
                }

                for (AvailableNetworkInfo availableNetworkInfo : availableNetworks) {
                    final long identity = Binder.clearCallingIdentity();
                    boolean isActiveSubId = false;
                    try {
                        isActiveSubId =
                                mSubscriptionManager.isActiveSubId(availableNetworkInfo.getSubId());
                    } finally {
                        Binder.restoreCallingIdentity(identity);
                    }
                    if (isActiveSubId) {
                        TelephonyPermissions.enforceCallingOrSelfCarrierPrivilege(mContext,
                            availableNetworkInfo.getSubId(), "updateAvailableNetworks");
                    } else {
                        // check if the app has opportunistic carrier permission
                        if (!hasOpportunisticSubPrivilege(callingPackage,
                            availableNetworkInfo.getSubId())) {
                            log("No carrier privilege for opportunistic subscription");
                            sendUpdateNetworksCallbackHelper(callbackStub,
                                TelephonyManager.UPDATE_AVAILABLE_NETWORKS_NO_CARRIER_PRIVILEGE);
                            return;
                        }
                    }
                }
            }

            final long identity = Binder.clearCallingIdentity();
            try {
                SubscriptionInfo subscriptionInfo = mSubscriptionManager.getDefaultVoiceSubscriptionInfo();
                if (subscriptionInfo != null) {
                    ONSConfigInput onsConfigInput =
                            new ONSConfigInput(
                                    availableNetworks,
                                    callbackStub,
                                    availableNetworks.getFirst().getSubId() /* preferredDataSub */,
                                    subscriptionInfo.getSubscriptionId() /* primarySub */);
                    setCarrierAppConfig(onsConfigInput);
                }
                // standalone opportunistic subscription should be handled in priority.
                ONSConfigInput systemConfig = getSystemAppConfig();
                if (systemConfig != null) {
                    if (mProfileSelector.containStandaloneOppSubs(
                        systemConfig.availableNetworkInfos())) {
                        log("standalone opportunistic subscription is using.");
                        return;
                    }
                }

                if (isEnabled) {
                    //  if carrier is reporting availability, then it takes higher priority.
                    mProfileSelector.startProfileSelection(availableNetworks, callbackStub);
                } else {
                    if (Compatibility.isChangeEnabled(CALLBACK_ON_MORE_ERROR_CODE_CHANGE)) {
                        sendUpdateNetworksCallbackHelper(callbackStub,
                                TelephonyManager.UPDATE_AVAILABLE_NETWORKS_SERVICE_IS_DISABLED);
                    } else {
                        sendUpdateNetworksCallbackHelper(callbackStub,
                                TelephonyManager.UPDATE_AVAILABLE_NETWORKS_ABORTED);
                    }
                }
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        } else {
            final long identity = Binder.clearCallingIdentity();
            try {
                setCarrierAppConfig(null);
                if (!isEnabled) {
                    sendUpdateNetworksCallbackHelper(callbackStub,
                        TelephonyManager.UPDATE_AVAILABLE_NETWORKS_SUCCESS);
                    return;
                }
                // If carrier is reporting unavailability, then decide whether to start
                // system app request or not.
                ONSConfigInput systemConfig = getSystemAppConfig();
                if (systemConfig != null) {
                    sendUpdateNetworksCallbackHelper(callbackStub,
                            TelephonyManager.UPDATE_AVAILABLE_NETWORKS_SUCCESS);
                    mProfileSelector.startProfileSelection(
                            systemConfig.availableNetworkInfos(),
                            systemConfig.availableNetworkCallback());
                } else {
                    mProfileSelector.stopProfileSelection(callbackStub);
                }
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }
    }

    private void sendUpdateNetworksCallbackHelper(IUpdateAvailableNetworksCallback callback, int result) {
        if (callback == null) return;
        try {
            callback.onComplete(result);
        } catch (RemoteException exception) {
            log("RemoteException " + exception);
        }
    }

    private void sendSetOpptCallbackHelper(ISetOpportunisticDataCallback callback, int result) {
        if (callback == null) return;
        try {
            callback.onComplete(result);
        } catch (RemoteException exception) {
            log("RemoteException " + exception);
        }
    }

    private boolean getPersistentEnableState() {
        return mSharedPref.getBoolean(PREF_ENABLED, true);
    }

    private void handleSystemAppAvailableNetworks(
            List<AvailableNetworkInfo> availableNetworks,
            IUpdateAvailableNetworksCallback callbackStub) {
        boolean isEnabled = getEnableState();
        if ((availableNetworks != null) && (!availableNetworks.isEmpty())) {
            // all subscriptions should be opportunistic subscriptions
            if (!mProfileSelector.hasOpportunisticSub(availableNetworks)) {
                log("No opportunistic subscriptions received");
                if (Compatibility.isChangeEnabled(CALLBACK_ON_MORE_ERROR_CODE_CHANGE)) {
                    sendUpdateNetworksCallbackHelper(callbackStub,
                            TelephonyManager
                                    .UPDATE_AVAILABLE_NETWORKS_NO_OPPORTUNISTIC_SUB_AVAILABLE);
                } else {
                    sendUpdateNetworksCallbackHelper(callbackStub,
                            TelephonyManager.UPDATE_AVAILABLE_NETWORKS_INVALID_ARGUMENTS);
                }
                return;
            }
            setSystemAppConfig(new ONSConfigInput(availableNetworks, callbackStub));
            // Reporting availability. proceed if carrier app has not requested any, but
            // standalone opportunistic subscription should be handled in priority.
            ONSConfigInput carrierConfig = getCarrierAppConfig();
            if (isEnabled) {
                if (carrierConfig == null
                        || mProfileSelector.containStandaloneOppSubs(availableNetworks)) {
                    mProfileSelector.startProfileSelection(availableNetworks, callbackStub);
                }
            } else {
                if (Compatibility.isChangeEnabled(CALLBACK_ON_MORE_ERROR_CODE_CHANGE)) {
                    sendUpdateNetworksCallbackHelper(callbackStub,
                            TelephonyManager.UPDATE_AVAILABLE_NETWORKS_SERVICE_IS_DISABLED);
                } else {
                    sendUpdateNetworksCallbackHelper(callbackStub,
                            TelephonyManager.UPDATE_AVAILABLE_NETWORKS_ABORTED);
                }
            }
        } else {
            if (!isEnabled) {
                setSystemAppConfig(null);
                sendUpdateNetworksCallbackHelper(callbackStub,
                    TelephonyManager.UPDATE_AVAILABLE_NETWORKS_SUCCESS);
                return;
            }
            // If system is reporting unavailability, then decide whether to start carrier
            // app request or not.
            setSystemAppConfig(null);
            ONSConfigInput carrierConfig = getCarrierAppConfig();
            if (carrierConfig == null) {
                mProfileSelector.stopProfileSelection(callbackStub);
            } else {
                sendUpdateNetworksCallbackHelper(callbackStub,
                        TelephonyManager.UPDATE_AVAILABLE_NETWORKS_SUCCESS);
                log("Try to start carrier app request");
                mProfileSelector.startProfileSelection(
                        carrierConfig.availableNetworkInfos(),
                        carrierConfig.availableNetworkCallback());
            }
        }
    }

    @Nullable private ONSConfigInput getCarrierAppConfig() {
        return getAppConfig(CARRIER_APP_CONFIG_NAME);
    }

    @Nullable private ONSConfigInput getSystemAppConfig() {
        return getAppConfig(SYSTEM_APP_CONFIG_NAME);
    }

    @Nullable private ONSConfigInput getAppConfig(@NonNull String key) {
        return mFlaggedLock.runLockedWhenEnabled(() -> mONSConfigInputHashMap.get(key));
    }

    private void setCarrierAppConfig(ONSConfigInput config) {
        setAppConfig(CARRIER_APP_CONFIG_NAME, config);
    }

    private void setSystemAppConfig(ONSConfigInput config) {
        setAppConfig(SYSTEM_APP_CONFIG_NAME, config);
    }

    private void setAppConfig(@NonNull String key, ONSConfigInput config) {
        mFlaggedLock.runLockedWhenEnabled(() -> mONSConfigInputHashMap.put(key, config));
    }

    private void updateEnableState(boolean enable) {
        mFlaggedLock.runLockedWhenEnabled(() -> {
            mIsEnabled = enable;
            mSharedPref.edit().putBoolean(PREF_ENABLED, mIsEnabled).apply();
        });
    }

    private boolean getEnableState() {
        return mFlaggedLock.runLockedWhenEnabled(() -> mIsEnabled);
    }

    /**
     * update the enable state
     * start profile selection if enabled.
     * @param enable enable(true) or disable(false)
     */
    private void enableOpportunisticNetwork(boolean enable) {
        mFlaggedLock.runLockedWhenDisabled(() -> {
            if (getEnableState() == enable) {
                return;
            }
            updateEnableState(enable);
            if (!getEnableState()) {
                mProfileSelector.stopProfileSelection(null);
            } else {
                ONSConfigInput carrierConfig = getCarrierAppConfig();
                ONSConfigInput systemConfig = getSystemAppConfig();
                if (carrierConfig != null
                        && carrierConfig.availableNetworkInfos() != null) {
                    mProfileSelector.startProfileSelection(
                            carrierConfig.availableNetworkInfos(),
                            carrierConfig.availableNetworkCallback());
                } else if (systemConfig != null
                        && systemConfig.availableNetworkInfos() != null) {
                    mProfileSelector.startProfileSelection(
                            systemConfig.availableNetworkInfos(),
                            systemConfig.availableNetworkCallback());
                }
            }
            logDebug("service is enable state " + getEnableState());
        });
    }

    /**
     * Make sure the device has required telephony feature
     *
     * @throws UnsupportedOperationException if the device does not have required telephony feature
     */
    private void enforceTelephonyFeatureWithException(@Nullable String callingPackage,
            @NonNull String telephonyFeature, @NonNull String methodName) {
        if (callingPackage == null || mPackageManager == null) {
            return;
        }

        if (!CompatChanges.isChangeEnabled(ENABLE_FEATURE_MAPPING, callingPackage,
                Binder.getCallingUserHandle())
                || mVendorApiLevel < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
            // Skip to check associated telephony feature,
            // if compatibility change is not enabled for the current process or
            // the SDK version of vendor partition is less than Android V.
            return;
        }

        if (!mPackageManager.hasSystemFeature(telephonyFeature)) {
            throw new UnsupportedOperationException(
                    methodName + " is unsupported without " + telephonyFeature);
        }
    }

    private boolean canManageSubscription(SubscriptionInfo subInfo, String packageName) {
        if (UserManager.isHeadlessSystemUserMode()) {
            return mSubscriptionManager.canManageSubscriptionAsUser(subInfo, packageName,
                    UserHandle.of(ActivityManager.getCurrentUser()));
        } else {
            return mSubscriptionManager.canManageSubscription(subInfo, packageName);
        }
    }

    private void log(String msg) {
        Log.d(TAG, msg);
    }

    private void logDebug(String msg) {
        if (DBG) Log.d(TAG, msg);
    }

    /**
     * Dump the state of {@link OpportunisticNetworkService}.
     *
     * @param fd File descriptor
     * @param pw Print writer
     * @param args Arguments
     */
    @Override
    public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw,
            @NonNull String[] args) {
        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP,
                "Requires android.Manifest.permission.DUMP");
        final long token = Binder.clearCallingIdentity();
        try {
            pw.println(OpportunisticNetworkService.class.getSimpleName() + ":");
            pw.println("  mIsEnabled = " + getEnableState());
            // Add null checks to prevent a race condition crash in the `dump` method.
            // A test is needed to verify the fix by calling `dump` before initialization
            // is complete, ensuring no NPE
            if (mONSProfileActivator != null) {
                mONSProfileActivator.dump(fd, pw, args);
            }
            if (mProfileSelector != null) {
                mProfileSelector.dump(fd, pw, args);
            }
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }
}
