/*
 * 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.AvailableNetworkInfo.PRIORITY_HIGH;
import static android.telephony.AvailableNetworkInfo.PRIORITY_LOW;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.PendingIntent;
import android.compat.Compatibility;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.telephony.AvailableNetworkInfo;
import android.telephony.CellInfo;
import android.telephony.CellInfoLte;
import android.telephony.CellInfoNr;
import android.telephony.SignalStrength;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.telephony.UiccCardInfo;
import android.telephony.UiccPortInfo;
import android.telephony.euicc.EuiccManager;
import android.text.TextUtils;
import android.util.IndentingPrintWriter;
import android.util.Log;

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

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;

/**
 * Profile selector class which will select the right profile based upon geographic information
 * input and network scan results.
 *
 * Thread safety: ONSProfileSelctor instantiates a handler using the ONS worker thread. State that
 * is accessed by external calls that cannot be posted to this handler are annotated with @GuardedBy
 * and protected in dedicated helpers.
 *
 * 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. In the older model,
 * ONSProfileSelector has its own HandlerThread to post work to. Locking and which thread work
 * should be run on is not consistent and should be altered with caution.
 */
public class ONSProfileSelector {
    private static final String TAG = "ONSProfileSelector";
    private static final boolean DBG = true;
    private final FlaggedLock mFlaggedLock =
            new FlaggedLock(new ReentrantLock(), Flags.onsThreadConsolidation());

    private static final int INVALID_SEQUENCE_ID = -1;
    private static final int START_SEQUENCE_ID = 1;

    /** Message to indicate profile update */
    private static final int MSG_PROFILE_UPDATE = 1;

    /** Message to indicate start of profile selection process */
    private static final int MSG_START_PROFILE_SELECTION = 2;

    /** Message to indicate Subscription switch completion */
    private static final int MSG_SUB_SWITCH_COMPLETE = 3;

    /** Message to stop profile selection process */
    private static final int MSG_STOP_PROFILE_SELECTION = 4;

    private boolean mIsEnabled = false;

    @VisibleForTesting
    protected Context mContext;

    @VisibleForTesting
    protected TelephonyManager mTelephonyManager;
    @VisibleForTesting
    protected TelephonyManager mSubscriptionBoundTelephonyManager;
    @VisibleForTesting
    protected EuiccManager mEuiccManager;

    @VisibleForTesting
    protected ONSNetworkScanCtlr mNetworkScanCtlr;

    @VisibleForTesting
    protected SubscriptionManager mSubscriptionManager;
    @GuardedBy("mFlaggedLock")
    @VisibleForTesting
    protected List<SubscriptionInfo> mOppSubscriptionInfos;
    @GuardedBy("mFlaggedLock")
    @VisibleForTesting
    protected List<SubscriptionInfo> mStandaloneOppSubInfos;
    private ONSProfileSelectionCallback mProfileSelectionCallback;
    private int mSequenceId;
    private int mSubId;
    @VisibleForTesting
    protected int mCurrentDataSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
    private List<AvailableNetworkInfo> mAvailableNetworkInfos;
    private IUpdateAvailableNetworksCallback mNetworkScanCallback;

    public static final String ACTION_SUB_SWITCH =
            "android.intent.action.SUBSCRIPTION_SWITCH_REPLY";

    HandlerThread mThread;
    @VisibleForTesting
    protected Handler mHandler;

    /**
     * Network scan callback handler
     */
    @VisibleForTesting
    protected ONSNetworkScanCtlr.NetworkAvailableCallBack mNetworkAvailableCallBack =
            new ONSNetworkScanCtlr.NetworkAvailableCallBack() {
                @Override
                public void onNetworkAvailability(List<CellInfo> results) {
                    int subId = retrieveBestSubscription(results);
                    if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
                        sendUpdateNetworksCallbackHelper(mNetworkScanCallback,
                                TelephonyManager.UPDATE_AVAILABLE_NETWORKS_INVALID_ARGUMENTS);
                        // With this flag, mNetworScanCallback no longer needs protected.
                        mFlaggedLock.runLockedWhenDisabled(() -> mNetworkScanCallback = null);
                        return;
                    }

                    // stop scanning further
                    mNetworkScanCtlr.stopNetworkScan();
                    handleNetworkScanResult(subId);
                }

                @Override
                public void onError(int error) {
                    log("Network scan failed with error " + error);
                    // With this flag mNetworkScanCallback no longer needs protected.
                    mFlaggedLock.runLockedWhenDisabled(() -> {
                        if (mIsEnabled && mAvailableNetworkInfos != null
                                && !mAvailableNetworkInfos.isEmpty()) {
                            handleNetworkScanResult(mAvailableNetworkInfos.getFirst().getSubId());
                        } else {
                            if (mNetworkScanCallback != null) {
                                if (mIsEnabled) {
                                    sendUpdateNetworksCallbackHelper(mNetworkScanCallback,
                                            TelephonyManager
                                                    .UPDATE_AVAILABLE_NETWORKS_INVALID_ARGUMENTS);
                                } else {
                                    if (Compatibility.isChangeEnabled(
                                            OpportunisticNetworkService
                                                    .CALLBACK_ON_MORE_ERROR_CODE_CHANGE)) {
                                        sendUpdateNetworksCallbackHelper(mNetworkScanCallback,
                                                TelephonyManager
                                                        .UPDATE_AVAILABLE_NETWORKS_SERVICE_IS_DISABLED);
                                    } else {
                                        sendUpdateNetworksCallbackHelper(mNetworkScanCallback,
                                                TelephonyManager
                                                        .UPDATE_AVAILABLE_NETWORKS_UNKNOWN_FAILURE);
                                    }
                                }
                                mNetworkScanCallback = null;
                            }
                        }
                    });
                }

                private void handleNetworkScanResult(int subId) {
                    // if subscription is already active, just enable modem
                    if (mSubscriptionManager.isActiveSubId(subId)) {
                        if (enableModem(subId, true)) {
                            sendUpdateNetworksCallbackHelper(mNetworkScanCallback,
                                TelephonyManager.UPDATE_AVAILABLE_NETWORKS_SUCCESS);
                        } else {
                            if (Compatibility.isChangeEnabled(
                                    OpportunisticNetworkService
                                            .CALLBACK_ON_MORE_ERROR_CODE_CHANGE)) {
                                sendUpdateNetworksCallbackHelper(mNetworkScanCallback,
                                        TelephonyManager
                                                .UPDATE_AVAILABLE_NETWORKS_ENABLE_MODEM_FAIL);
                            } else {
                                sendUpdateNetworksCallbackHelper(mNetworkScanCallback,
                                        TelephonyManager.UPDATE_AVAILABLE_NETWORKS_ABORTED);
                            }
                        }
                        mProfileSelectionCallback.onProfileSelectionDone();
                        // With this flag, this state no longer needs protected.
                        mFlaggedLock.runLockedWhenDisabled(() -> {
                            mNetworkScanCallback = null;
                            mAvailableNetworkInfos = null;
                        });
                    } else {
                        logDebug("switch to sub:" + subId);
                        switchToSubscription(subId, getAvailableESIMPortIndex());
                    }
                }
            };

    @VisibleForTesting
    protected SubscriptionManager.OnOpportunisticSubscriptionsChangedListener
            mProfileChangeListener =
            new SubscriptionManager.OnOpportunisticSubscriptionsChangedListener() {
                @Override
                public void onOpportunisticSubscriptionsChanged() {
                    logDebug("onOpportunisticSubscriptionsChanged.");
                    mHandler.sendEmptyMessage(MSG_PROFILE_UPDATE);
                }
            };

    /**
     * Interface call back to confirm profile selection
     */
    public interface ONSProfileSelectionCallback {

        /**
         * Interface call back to confirm profile selection
         */
        void onProfileSelectionDone();
    }

    class SortSubInfo implements Comparator<SubscriptionInfo>
    {
        // Used for sorting in ascending order of sub id
        public int compare(SubscriptionInfo a, SubscriptionInfo b)
        {
            return a.getSubscriptionId() - b.getSubscriptionId();
        }
    }

    class SortAvailableNetworks implements Comparator<AvailableNetworkInfo>
    {
        // Used for sorting in ascending order of sub id
        public int compare(AvailableNetworkInfo a, AvailableNetworkInfo b)
        {
            return a.getSubId() - b.getSubId();
        }
    }

    class SortAvailableNetworksInPriority implements Comparator<AvailableNetworkInfo>
    {
        // Used for sorting in descending order of priority (ascending order of priority numbers)
        public int compare(AvailableNetworkInfo a, AvailableNetworkInfo b)
        {
            return a.getPriority() - b.getPriority();
        }
    }

    /**
     * ONSProfileSelector constructor
     * @param c context
     * @param profileSelectionCallback callback to be called once selection is done
     */
    public ONSProfileSelector(Context c, ONSProfileSelectionCallback profileSelectionCallback) {
        init(c, profileSelectionCallback);
        log("ONSProfileSelector init complete");
    }

    private int getSignalLevel(CellInfo cellInfo) {
        if (cellInfo != null) {
            return cellInfo.getCellSignalStrength().getLevel();
        } else {
            return SignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN;
        }
    }

    @VisibleForTesting
    protected String getMcc(CellInfo cellInfo) {
        String mcc = "";
        if (cellInfo instanceof CellInfoLte) {
            mcc = ((CellInfoLte) cellInfo).getCellIdentity().getMccString();
        }
        else if (cellInfo instanceof CellInfoNr) {
            mcc = ((CellInfoNr) cellInfo).getCellIdentity().getMccString();
        }

        return mcc;
    }

    @VisibleForTesting
    protected String getMnc(CellInfo cellInfo) {
        String mnc = "";
        if (cellInfo instanceof CellInfoLte) {
            mnc = ((CellInfoLte) cellInfo).getCellIdentity().getMncString();
        }
        else if (cellInfo instanceof CellInfoNr) {
            mnc = ((CellInfoNr) cellInfo).getCellIdentity().getMncString();
        }

        return mnc;
    }

    private int getSubIdUsingAvailableNetworks(String mcc, String mnc, int priorityLevel) {
        // With this flag, none of the affected state needs to be protected.
        return mFlaggedLock.runLockedWhenDisabled(() -> {
            String mccMnc = mcc + mnc;
            if (mAvailableNetworkInfos != null) {
                for (AvailableNetworkInfo availableNetworkInfo : mAvailableNetworkInfos) {
                    if (availableNetworkInfo.getPriority() != priorityLevel) {
                        continue;
                    }
                    for (String availableMccMnc : availableNetworkInfo.getMccMncs()) {
                        if (TextUtils.equals(availableMccMnc, mccMnc)) {
                            return availableNetworkInfo.getSubId();
                        }
                    }
                }
            }
            return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
        });
    }

    @Nullable private List<SubscriptionInfo> getStandaloneOppSubsInfos() {
        return mFlaggedLock.runLockedWhenEnabled(() -> mStandaloneOppSubInfos);
    }

    @Nullable private List<SubscriptionInfo> getOppSubscriptionInfos() {
        return mFlaggedLock.runLockedWhenEnabled(() -> mOppSubscriptionInfos);
    }

    public SubscriptionInfo getOpportunisticSubInfo(int subId) {
        List<SubscriptionInfo> oppSubInfos = getOppSubscriptionInfos();
        if ((oppSubInfos == null) || (oppSubInfos.isEmpty())) {
            return null;
        }
        for (SubscriptionInfo subscriptionInfo : oppSubInfos) {
            if (subscriptionInfo.getSubscriptionId() == subId) {
                return subscriptionInfo;
            }
        }
        return null;
    }

    /**
     * Return whether the subId is for an opportunistic subscription.
     *
     * @param subId the subId of the subscription.
     * @return true if the subscription is opportunistic
     */
    public boolean isOpportunisticSub(int subId) {
        List<SubscriptionInfo> oppSubInfos = getOppSubscriptionInfos();
        if ((oppSubInfos == null) || (oppSubInfos.isEmpty())) {
            return false;
        }
        for (SubscriptionInfo subscriptionInfo : oppSubInfos) {
            if (subscriptionInfo.getSubscriptionId() == subId) {
                return true;
            }
        }
        return false;
    }

    public boolean hasOpportunisticSub(List<AvailableNetworkInfo> availableNetworks) {
        if ((availableNetworks == null) || (availableNetworks.isEmpty())) {
            return false;
        }
        List<SubscriptionInfo> oppSubInfos = getOppSubscriptionInfos();
        if ((oppSubInfos == null) || (oppSubInfos.isEmpty())) {
            return false;
        }

        for (AvailableNetworkInfo availableNetworkInfo : availableNetworks) {
            if (!isOpportunisticSub(availableNetworkInfo.getSubId())) {
                return false;
            }
        }
        return true;
    }

    private void switchToSubscription(int subId, int availableSIMPortIndex) {
        Intent callbackIntent = new Intent(ACTION_SUB_SWITCH);
        callbackIntent.setClass(mContext, OpportunisticNetworkService.class);
        updateToken();
        callbackIntent.putExtra("sequenceId", mSequenceId);
        callbackIntent.putExtra("subId", subId);
        mSubId = subId;
        PendingIntent replyIntent = PendingIntent.getService(mContext,
                1, callbackIntent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE);
        if (availableSIMPortIndex == TelephonyManager.INVALID_PORT_INDEX) {
            sendUpdateNetworksCallbackHelper(mNetworkScanCallback,
                    TelephonyManager.UPDATE_AVAILABLE_NETWORKS_SIM_PORT_NOT_AVAILABLE);
            return;
        }

        List<SubscriptionInfo> subInfoList = mSubscriptionManager
                .getAvailableSubscriptionInfoList();
        for (SubscriptionInfo subInfo : subInfoList) {
            if (subId == subInfo.getSubscriptionId()) {
                EuiccManager euiccManager = mEuiccManager.createForCardId(subInfo.getCardId());
                euiccManager.switchToSubscription(subId, availableSIMPortIndex, replyIntent);
                break;
            }
        }
    }

    @VisibleForTesting
    protected int getAvailableESIMPortIndex() {
        //Check if an opportunistic subscription is already active. If yes then, use the same port.
        List<SubscriptionInfo> subscriptionInfos = mSubscriptionManager
                .getCompleteActiveSubscriptionInfoList();
        if (subscriptionInfos != null) {
            logDebug("[getAvailableESIMPortIndex] subscriptionInfos size:"
                    + subscriptionInfos.size());
            for (SubscriptionInfo subscriptionInfo : subscriptionInfos) {
                if (subscriptionInfo.isEmbedded() && subscriptionInfo.isOpportunistic()) {
                    return subscriptionInfo.getPortIndex();
                }
            }
        }

        //Look for available port.
        for (UiccCardInfo uiccCardInfo : mTelephonyManager.getUiccCardsInfo()) {
            logDebug("[getAvailableESIMPortIndex] CardInfo: " + uiccCardInfo.toString());
            if (!uiccCardInfo.isEuicc()) {
                continue;
            }

            EuiccManager euiccManager = mEuiccManager.createForCardId(uiccCardInfo.getCardId());
            for (UiccPortInfo uiccPortInfo : uiccCardInfo.getPorts()) {
                logDebug("[getAvailableESIMPortIndex] PortInfo: " + uiccPortInfo.toString());
                //Port is available if no profiles enabled on it.
                if (euiccManager.isSimPortAvailable(uiccPortInfo.getPortIndex())) {
                    return uiccPortInfo.getPortIndex();
                }
            }
        }

        logDebug("[getAvailableESIMPortIndex] No Port is available.");
        return TelephonyManager.INVALID_PORT_INDEX;
    }

    void onSubSwitchComplete(Intent intent) {
        int sequenceId = intent.getIntExtra("sequenceId",  INVALID_SEQUENCE_ID);
        int subId = intent.getIntExtra("subId",
                SubscriptionManager.INVALID_SUBSCRIPTION_ID);
        logDebug("ACTION_SUB_SWITCH sequenceId: " + sequenceId
                + " mSequenceId: " + mSequenceId
                + " mSubId: " + mSubId
                + " subId: " + subId);
        Message message = Message.obtain(mHandler, MSG_SUB_SWITCH_COMPLETE, subId);
        message.sendToTarget();
    }

    private void onSubSwitchComplete(int subId) {
        // Ignore if this is callback for an older request
        if (mSubId != subId) {
            return;
        }

        if (enableModem(subId, true)) {
            sendUpdateNetworksCallbackHelper(mNetworkScanCallback,
                TelephonyManager.UPDATE_AVAILABLE_NETWORKS_SUCCESS);
        } else {
            if (Compatibility.isChangeEnabled(
                    OpportunisticNetworkService.CALLBACK_ON_MORE_ERROR_CODE_CHANGE)) {
                sendUpdateNetworksCallbackHelper(mNetworkScanCallback,
                        TelephonyManager.UPDATE_AVAILABLE_NETWORKS_ENABLE_MODEM_FAIL);
            } else {
                sendUpdateNetworksCallbackHelper(mNetworkScanCallback,
                        TelephonyManager.UPDATE_AVAILABLE_NETWORKS_ABORTED);
            }
        }
        mProfileSelectionCallback.onProfileSelectionDone();
        mNetworkScanCallback = null;
        mAvailableNetworkInfos = null;
    }

    private void updateToken() {
        // With this flag, mSequenceId does not need to be protected.
        mFlaggedLock.runLockedWhenDisabled(() -> mSequenceId++);
    }

    private List<AvailableNetworkInfo> getFilteredAvailableNetworks(
            List<AvailableNetworkInfo> availableNetworks,
            List<SubscriptionInfo> subscriptionInfoList) {
        List<AvailableNetworkInfo> filteredAvailableNetworks =
                new ArrayList<>();

        // instead of checking each element of a list every element of the other, sort them in
        // the order of sub id and compare to improve the filtering performance.
        Collections.sort(subscriptionInfoList, new SortSubInfo());
        Collections.sort(availableNetworks, new SortAvailableNetworks());
        int availableNetworksIndex = 0;
        int subscriptionInfoListIndex = 0;
        SubscriptionInfo subscriptionInfo;
        AvailableNetworkInfo availableNetwork;

        while (availableNetworksIndex < availableNetworks.size()
                && subscriptionInfoListIndex < subscriptionInfoList.size()) {
            subscriptionInfo = subscriptionInfoList.get(subscriptionInfoListIndex);
            availableNetwork = availableNetworks.get(availableNetworksIndex);
            if (subscriptionInfo.getSubscriptionId() == availableNetwork.getSubId()) {
                filteredAvailableNetworks.add(availableNetwork);
                subscriptionInfoListIndex++;
                availableNetworksIndex++;
            } else if (subscriptionInfo.getSubscriptionId() < availableNetwork.getSubId()) {
                subscriptionInfoListIndex++;
            } else {
                availableNetworksIndex++;
            }
        }
        return filteredAvailableNetworks;
    }

    private boolean isSame(List<AvailableNetworkInfo> availableNetworks1,
            List<AvailableNetworkInfo> availableNetworks2) {
        if ((availableNetworks1 == null) || (availableNetworks2 == null)) {
            return false;
        }
        return new HashSet<>(availableNetworks1).equals(new HashSet<>(availableNetworks2));
    }

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

    private void checkProfileUpdate(Object[] objects) {
        List<AvailableNetworkInfo> availableNetworks =
                (List<AvailableNetworkInfo>) objects[0];
        IUpdateAvailableNetworksCallback callbackStub =
                (IUpdateAvailableNetworksCallback) objects[1];
        List<SubscriptionInfo> oppSubInfos = getOppSubscriptionInfos();
        if (oppSubInfos == null) {
            logDebug("null subscription infos");
            if (Compatibility.isChangeEnabled(
                    OpportunisticNetworkService.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;
        }

        // Check if ports are available on the embedded slot
        int availSIMPortIndex = getAvailableESIMPortIndex();
        if (availSIMPortIndex == TelephonyManager.INVALID_PORT_INDEX) {
            logDebug("SIM port not available.");
            sendUpdateNetworksCallbackHelper(callbackStub,
                    TelephonyManager.UPDATE_AVAILABLE_NETWORKS_SIM_PORT_NOT_AVAILABLE);
            return;
        }

        if (isSame(availableNetworks, mAvailableNetworkInfos)) {
            logDebug("received duplicate requests");
            // If we receive same request more than once, send abort response for earlier one
            // and send actual response for the latest callback.
            sendUpdateNetworksCallbackHelper(mNetworkScanCallback,
                TelephonyManager.UPDATE_AVAILABLE_NETWORKS_ABORTED);
            mNetworkScanCallback = callbackStub;
            return;
        }

        stopProfileScanningProcedure();
        mIsEnabled = true;
        mAvailableNetworkInfos = availableNetworks;
        // sort in the order of priority
        Collections.sort(mAvailableNetworkInfos, new SortAvailableNetworksInPriority());
        logDebug("availableNetworks: " + availableNetworks);

        if (!oppSubInfos.isEmpty()) {
            logDebug("opportunistic subscriptions size " + oppSubInfos.size());
            List<AvailableNetworkInfo> filteredAvailableNetworks =
                    getFilteredAvailableNetworks(availableNetworks,
                            oppSubInfos);
            if ((filteredAvailableNetworks.size() == 1)
                    && ((filteredAvailableNetworks.getFirst().getMccMncs() == null)
                    || (filteredAvailableNetworks.getFirst().getMccMncs().isEmpty()))) {
                // if subscription is not active, activate the sub
                if (!mSubscriptionManager.isActiveSubId(
                        filteredAvailableNetworks.getFirst().getSubId())) {
                    mNetworkScanCallback = callbackStub;
                    switchToSubscription(filteredAvailableNetworks.getFirst().getSubId(),
                            availSIMPortIndex);
                } else {
                    if (enableModem(filteredAvailableNetworks.getFirst().getSubId(), true)) {
                        sendUpdateNetworksCallbackHelper(callbackStub,
                            TelephonyManager.UPDATE_AVAILABLE_NETWORKS_SUCCESS);
                    } else {
                        if (Compatibility.isChangeEnabled(
                                OpportunisticNetworkService.CALLBACK_ON_MORE_ERROR_CODE_CHANGE)) {
                            sendUpdateNetworksCallbackHelper(callbackStub,
                                    TelephonyManager.UPDATE_AVAILABLE_NETWORKS_ENABLE_MODEM_FAIL);
                        } else {
                            sendUpdateNetworksCallbackHelper(callbackStub,
                                    TelephonyManager.UPDATE_AVAILABLE_NETWORKS_ABORTED);
                        }
                    }
                    mProfileSelectionCallback.onProfileSelectionDone();
                    mAvailableNetworkInfos = null;
                }
            } else {
                mNetworkScanCallback = callbackStub;
                // start scan immediately
                mNetworkScanCtlr.startFastNetworkScan(filteredAvailableNetworks);
            }
        } else {
            if (Compatibility.isChangeEnabled(
                    OpportunisticNetworkService.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);
            }
            // check if no profile
            logDebug("stopping scan");
            mNetworkScanCtlr.stopNetworkScan();
        }
    }

    @VisibleForTesting
    protected int retrieveBestSubscription(List<CellInfo> results) {
        /* sort the results according to signal strength level */
        Collections.sort(results, new Comparator<CellInfo>() {
            @Override
            public int compare(CellInfo cellInfo1, CellInfo cellInfo2) {
                return getSignalLevel(cellInfo1) - getSignalLevel(cellInfo2);
            }
        });

        for (int level = PRIORITY_HIGH; level < PRIORITY_LOW; level++) {
            for (CellInfo result : results) {
                /* get subscription id for the best network scan result */
                int subId = getSubIdUsingAvailableNetworks(getMcc(result), getMnc(result), level);
                if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
                    return subId;
                }
            }
        }

        return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
    }

    private int getActiveOpportunisticSubId() {
        List<SubscriptionInfo> subscriptionInfos =
            mSubscriptionManager.getActiveSubscriptionInfoList(false);
        if (subscriptionInfos == null) {
            return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
        }
        for (SubscriptionInfo subscriptionInfo : subscriptionInfos) {
            if (subscriptionInfo.isOpportunistic()) {
                return subscriptionInfo.getSubscriptionId();
            }
        }

        return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
    }

    private void disableOpportunisticModem(IUpdateAvailableNetworksCallback callbackStub) {
        int subId = getActiveOpportunisticSubId();
        if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
            if (Compatibility.isChangeEnabled(
                    OpportunisticNetworkService.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;
        }
        if (enableModem(subId, false)) {
            sendUpdateNetworksCallbackHelper(callbackStub,
                TelephonyManager.UPDATE_AVAILABLE_NETWORKS_SUCCESS);
        } else {
            if (Compatibility.isChangeEnabled(
                    OpportunisticNetworkService.CALLBACK_ON_MORE_ERROR_CODE_CHANGE)) {
                sendUpdateNetworksCallbackHelper(callbackStub,
                        TelephonyManager.UPDATE_AVAILABLE_NETWORKS_DISABLE_MODEM_FAIL);
            } else {
                sendUpdateNetworksCallbackHelper(callbackStub,
                        TelephonyManager.UPDATE_AVAILABLE_NETWORKS_ABORTED);
            }
        }
    }

    private boolean enableModem(int subId, boolean enable) {
        SubscriptionInfo info = mSubscriptionManager.getActiveSubscriptionInfo(subId);
        if (info == null) {
            // Subscription is not active. Do nothing.
            return false;
        }

        // If disabling modem for opportunistic sub, make sure to switch data back to default sub.
        if (!enable) {
            if (mSubscriptionManager.getPreferredDataSubscriptionId() == subId) {
                selectProfileForData(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID, false, null);
            }
        }
        int phoneId = info.getSimSlotIndex();
        /*  Todo: b/135067156
         *  Re-enable this code once 135067156 is fixed
        if (mSubscriptionBoundTelephonyManager.isModemEnabledForSlot(phoneId) == enable) {
            logDebug("modem is already enabled ");
            return true;
        } */

        return mSubscriptionBoundTelephonyManager.enableModemForSlot(phoneId, enable);
    }

    private void stopProfileSelectionProcess(IUpdateAvailableNetworksCallback callbackStub) {
        stopProfileScanningProcedure();
        logDebug("stopProfileSelection");
        disableOpportunisticModem(callbackStub);
    }

    private void stopProfileScanningProcedure() {
        // With this flag, none of this state needs protected.
        mFlaggedLock.runLockedWhenDisabled(() -> {
            if (mNetworkScanCallback != null) {
                sendUpdateNetworksCallbackHelper(mNetworkScanCallback,
                        TelephonyManager.UPDATE_AVAILABLE_NETWORKS_ABORTED);
                mNetworkScanCallback = null;
            }
            mNetworkScanCtlr.stopNetworkScan();

            mAvailableNetworkInfos = null;
            mIsEnabled = false;
        });
    }

    /**
     * Return {@code true} if the provided {@code availableNetworks} contains standalone oppt subs.
     */
    public boolean containStandaloneOppSubs(List<AvailableNetworkInfo> availableNetworks) {
        List<SubscriptionInfo> standaloneOppSubInfos = getStandaloneOppSubsInfos();
        if (standaloneOppSubInfos == null || standaloneOppSubInfos.isEmpty()) {
            logDebug("received null or empty subscription infos");
            return false;
        }

        logDebug("Standalone opportunistic subInfos size " + standaloneOppSubInfos.size());
        List<AvailableNetworkInfo> filteredAvailableNetworks =
                getFilteredAvailableNetworks(availableNetworks, standaloneOppSubInfos);
        return !filteredAvailableNetworks.isEmpty();
    }

    /**
     * Start profile selection.
     */
    public void startProfileSelection(List<AvailableNetworkInfo> availableNetworks,
            IUpdateAvailableNetworksCallback callbackStub) {
        logDebug("startProfileSelection availableNetworks: " + availableNetworks);
        if (availableNetworks == null || availableNetworks.isEmpty()) {
            if (callbackStub != null) {
                sendUpdateNetworksCallbackHelper(callbackStub,
                        TelephonyManager.UPDATE_AVAILABLE_NETWORKS_INVALID_ARGUMENTS);
            }
            return;
        }
        Object[] objects = new Object[]{availableNetworks, callbackStub};
        Message message = Message.obtain(mHandler, MSG_START_PROFILE_SELECTION, objects);
        message.sendToTarget();
    }

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

    /**
     * Select opportunistic profile for data if passing a valid subId.
     * @param subId : opportunistic subId or SubscriptionManager.DEFAULT_SUBSCRIPTION_ID if
     *              deselecting previously set preference.
     */
    public void selectProfileForData(int subId, boolean needValidation,
            ISetOpportunisticDataCallback callbackStub) {
        if ((subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID)
                || (isOpportunisticSub(subId) && mSubscriptionManager.isActiveSubId(subId))) {
            try {
                mSubscriptionManager.setPreferredDataSubscriptionId(subId, needValidation,
                        mHandler::post, result -> sendSetOpptCallbackHelper(callbackStub, result));
            } catch (Exception ex) {
                log("setPreferredDataSubscriptionId failed. subId=" + subId + ", needValidation="
                        + needValidation + ", ex=" + ex);
                if (Compatibility.isChangeEnabled(
                        OpportunisticNetworkService.CALLBACK_ON_MORE_ERROR_CODE_CHANGE)) {
                    sendSetOpptCallbackHelper(callbackStub,
                            TelephonyManager.SET_OPPORTUNISTIC_SUB_REMOTE_SERVICE_EXCEPTION);
                } else {
                    sendSetOpptCallbackHelper(callbackStub,
                            TelephonyManager.SET_OPPORTUNISTIC_SUB_VALIDATION_FAILED);
                }
                return;
            }
            mCurrentDataSubId = subId;
        } else {
            log("Inactive sub passed for preferred data " + subId);
            if (Compatibility.isChangeEnabled(
                    OpportunisticNetworkService.CALLBACK_ON_MORE_ERROR_CODE_CHANGE)) {
                if (isOpportunisticSub(subId)) {
                    sendSetOpptCallbackHelper(callbackStub,
                            TelephonyManager.SET_OPPORTUNISTIC_SUB_INACTIVE_SUBSCRIPTION);
                } else {
                    sendSetOpptCallbackHelper(callbackStub,
                            TelephonyManager.SET_OPPORTUNISTIC_SUB_NO_OPPORTUNISTIC_SUB_AVAILABLE);
                }
            } else {
                sendSetOpptCallbackHelper(callbackStub,
                        TelephonyManager.SET_OPPORTUNISTIC_SUB_INACTIVE_SUBSCRIPTION);
            }
        }
    }

    public int getPreferredDataSubscriptionId() {
        final int preferredDataSubId = mSubscriptionManager.getPreferredDataSubscriptionId();
        return isOpportunisticSub(preferredDataSubId)
            ? preferredDataSubId
            : SubscriptionManager.DEFAULT_SUBSCRIPTION_ID;
    }

    /**
     * Stop profile selection procedure
     */
    public void stopProfileSelection(IUpdateAvailableNetworksCallback callbackStub) {
        logDebug("stopProfileSelection");
        Message message = Message.obtain(mHandler, MSG_STOP_PROFILE_SELECTION, callbackStub);
        message.sendToTarget();
    }

    @VisibleForTesting
    protected void updateOpportunisticSubscriptions() {
        mFlaggedLock.runLocked(() ->  {
            mOppSubscriptionInfos = mSubscriptionManager
                    .getOpportunisticSubscriptions().stream()
                    .filter(subInfo -> !subInfo.isGroupDisabled())
                    .collect(Collectors.toList());
            if (mOppSubscriptionInfos != null) {
                mStandaloneOppSubInfos = mOppSubscriptionInfos.stream()
                        .filter(subInfo -> subInfo.getGroupUuid() == null)
                        .collect(Collectors.toList());
            }
        });
    }

    private void enableModemStackForNonOpportunisticSlots() {
        int phoneCount = mTelephonyManager.getPhoneCount();
        // Do nothing in single SIM mode.
        if (phoneCount < 2) return;

        List<SubscriptionInfo> oppSubInfos = getOppSubscriptionInfos();
        for (int i = 0; i < phoneCount; i++) {
            boolean hasActiveOpptProfile = false;
            if (oppSubInfos != null) {
                for (SubscriptionInfo info : oppSubInfos) {
                    if (info.getSimSlotIndex() == i) {
                        hasActiveOpptProfile = true;
                    }
                }
            }
            // If the slot doesn't have active opportunistic profile anymore, it's back to
            // DSDS use-case. Make sure the modem stack is enabled.
            if (!hasActiveOpptProfile) mTelephonyManager.enableModemForSlot(i, true);
        }
    }

    @VisibleForTesting
    protected void init(Context c, ONSProfileSelectionCallback profileSelectionCallback) {
        mContext = c;
        mSequenceId = START_SEQUENCE_ID;
        mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
        mProfileSelectionCallback = profileSelectionCallback;
        mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
        mSubscriptionBoundTelephonyManager = mTelephonyManager.createForSubscriptionId(
                SubscriptionManager.DEFAULT_SUBSCRIPTION_ID);
        mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class);
        mNetworkScanCtlr = new ONSNetworkScanCtlr(mContext, mSubscriptionBoundTelephonyManager,
                mNetworkAvailableCallBack);
        mEuiccManager = c.getSystemService(EuiccManager.class);
        updateOpportunisticSubscriptions();
        Looper looper;
        if (!Flags.onsThreadConsolidation()) {
            mThread = new HandlerThread(TAG);
            mThread.start();
            looper = mThread.getLooper();
        } else {
            looper = Looper.myLooper();
        }
        mHandler = new Handler(looper) {
            @Override
            public void handleMessage(Message msg) {
                // All protected state is protected via helper methods
                // so it is no longer necessary to acquire mLock here.
                mFlaggedLock.runLockedWhenDisabled(() -> {
                    switch (msg.what) {
                        case MSG_PROFILE_UPDATE:
                            updateOpportunisticSubscriptions();
                            enableModemStackForNonOpportunisticSlots();
                            break;
                        case MSG_START_PROFILE_SELECTION:
                            logDebug("Msg received for profile update");
                            checkProfileUpdate((Object[]) msg.obj);
                            break;
                        case MSG_STOP_PROFILE_SELECTION:
                            logDebug("Msg received to stop profile selection");
                            stopProfileSelectionProcess((IUpdateAvailableNetworksCallback) msg.obj);
                            break;
                        case MSG_SUB_SWITCH_COMPLETE:
                            logDebug("Msg received for sub switch");
                            onSubSwitchComplete((int) msg.obj);
                            break;
                        default:
                            log("invalid message");
                            break;
                    }
                });
            }
        };
        // register for profile update events
        mSubscriptionManager.addOnOpportunisticSubscriptionsChangedListener(
                AsyncTask.SERIAL_EXECUTOR, mProfileChangeListener);
    }

    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 ONSProfileSelector}.
     */
    public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter printWriter,
            @NonNull String[] args) {
        IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, "  ");
        pw.println(TAG + ":");
        pw.println("  subId: " + mSubId);
        pw.println("  mCurrentDataSubId: " + mCurrentDataSubId);
        List<SubscriptionInfo> oppSubInfos = getOppSubscriptionInfos();
        pw.println("  mOppSubscriptionInfos: " + (
                oppSubInfos == null || oppSubInfos.isEmpty() ? "[]"
                        : Arrays.toString(oppSubInfos.stream().map(
                                SubscriptionInfo::getSubscriptionId).toArray())));
        List<SubscriptionInfo> standaloneOppSubInfos = getStandaloneOppSubsInfos();
        pw.println("  mStandaloneOppSubInfos: " + (
                standaloneOppSubInfos == null || standaloneOppSubInfos.isEmpty() ? "[]"
                        : Arrays.toString(standaloneOppSubInfos.stream().map(
                                SubscriptionInfo::getSubscriptionId).toArray())));
        pw.println("  mAvailableNetworkInfos:");
        if (mAvailableNetworkInfos != null && !mAvailableNetworkInfos.isEmpty()) {
            pw.increaseIndent();
            for (AvailableNetworkInfo availableNetworkInfo : mAvailableNetworkInfos) {
                pw.println(availableNetworkInfo);
            }
            pw.decreaseIndent();
        }
        mNetworkScanCtlr.dump(fd, printWriter, args);
    }
}
