/*
 * 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.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.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.TelephonyFrameworkInitializer;
import android.telephony.TelephonyManager;
import android.telephony.UiccCardInfo;
import android.telephony.UiccPortInfo;
import android.telephony.euicc.EuiccManager;
import android.text.TextUtils;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.ISetOpportunisticDataCallback;
import com.android.internal.telephony.ISub;
import com.android.internal.telephony.IUpdateAvailableNetworksCallback;
import com.android.telephony.Rlog;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.stream.Collectors;

/**
 * Profile selector class which will select the right profile based upon
 * geographic information input and network scan results.
 */
public class ONSProfileSelector {
    private static final String LOG_TAG = "ONSProfileSelector";
    private static final boolean DBG = true;
    private final Object mLock = new Object();

    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;
    @VisibleForTesting
    protected List<SubscriptionInfo> mOppSubscriptionInfos;
    @VisibleForTesting
    protected List<SubscriptionInfo> mStandaloneOppSubInfos;
    private ONSProfileSelectionCallback mProfileSelectionCallback;
    private int mSequenceId;
    private int mSubId;
    @VisibleForTesting
    protected int mCurrentDataSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
    private ArrayList<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);
                        synchronized (mLock) {
                            mNetworkScanCallback = null;
                        }
                        return;
                    }

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

                @Override
                public void onError(int error) {
                    log("Network scan failed with error " + error);
                    synchronized (mLock) {
                        if (mIsEnabled && mAvailableNetworkInfos != null
                            && mAvailableNetworkInfos.size() > 0) {
                            handleNetworkScanResult(mAvailableNetworkInfos.get(0).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();
                        synchronized (mLock) {
                            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) {
        String mccMnc = mcc + mnc;
        synchronized (mLock) {
            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;
    }

    public SubscriptionInfo getOpprotunisticSubInfo(int subId) {
        if ((mOppSubscriptionInfos == null) || (mOppSubscriptionInfos.size() == 0)) {
            return null;
        }
        for (SubscriptionInfo subscriptionInfo : mOppSubscriptionInfos) {
            if (subscriptionInfo.getSubscriptionId() == subId) {
                return subscriptionInfo;
            }
        }
        return null;
    }

    public boolean isOpprotunisticSub(int subId) {
        if ((mOppSubscriptionInfos == null) || (mOppSubscriptionInfos.size() == 0)) {
            return false;
        }
        for (SubscriptionInfo subscriptionInfo : mOppSubscriptionInfos) {
            if (subscriptionInfo.getSubscriptionId() == subId) {
                return true;
            }
        }
        return false;
    }

    public boolean hasOpprotunisticSub(List<AvailableNetworkInfo> availableNetworks) {
        if ((availableNetworks == null) || (availableNetworks.size() == 0)) {
            return false;
        }
        if ((mOppSubscriptionInfos == null) || (mOppSubscriptionInfos.size() == 0)) {
            return false;
        }

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

    private boolean isAvtiveSub(int subId) {
        return mSubscriptionManager.isActiveSubscriptionId(subId);
    }

    private HashMap<Integer, IUpdateAvailableNetworksCallback> callbackStubs = new HashMap<>();

    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;
        }
        mEuiccManager.switchToSubscription(subId, availableSIMPortIndex, replyIntent);
    }

    @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() {
        synchronized (mLock) {
            mSequenceId++;
        }
    }

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

        /* 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(ArrayList<AvailableNetworkInfo> availableNetworks1,
            ArrayList<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) {
        ArrayList<AvailableNetworkInfo> availableNetworks =
                (ArrayList<AvailableNetworkInfo>) objects[0];
        IUpdateAvailableNetworksCallback callbackStub =
                (IUpdateAvailableNetworksCallback) objects[1];
        if (mOppSubscriptionInfos == 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;
        }

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

        if (mOppSubscriptionInfos.size() > 0) {
            logDebug("opportunistic subscriptions size " + mOppSubscriptionInfos.size());
            ArrayList<AvailableNetworkInfo> filteredAvailableNetworks =
                    getFilteredAvailableNetworks((ArrayList<AvailableNetworkInfo>)availableNetworks,
                            mOppSubscriptionInfos);
            if ((filteredAvailableNetworks.size() == 1)
                    && ((filteredAvailableNetworks.get(0).getMccMncs() == null)
                    || (filteredAvailableNetworks.get(0).getMccMncs().size() == 0))) {
                /* if subscription is not active, activate the sub */
                if (!mSubscriptionManager.isActiveSubId(filteredAvailableNetworks.get(0).getSubId())) {
                    mNetworkScanCallback = callbackStub;
                    switchToSubscription(filteredAvailableNetworks.get(0).getSubId(),
                            availSIMPortIndex);
                } else {
                    if (enableModem(filteredAvailableNetworks.get(0).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 (mOppSubscriptionInfos.size() == 0) {
            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();
        }
    }

    private boolean isActiveSub(int subId) {
        List<SubscriptionInfo> subscriptionInfos =
                mSubscriptionManager.getActiveSubscriptionInfoList(false);
        if (subscriptionInfos == null) {
            return false;
        }

        for (SubscriptionInfo subscriptionInfo : subscriptionInfos) {
            if (subscriptionInfo.getSubscriptionId() == subId) {
                return true;
            }
        }

        return false;
    }

    @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 boolean isOpportunisticSubEmbedded(
            ArrayList<AvailableNetworkInfo> availableNetworks) {
        List<SubscriptionInfo> subscriptionInfos =
            mSubscriptionManager.getOpportunisticSubscriptions();
        if (subscriptionInfos == null) {
            return false;
        }
        for (AvailableNetworkInfo availableNetworkInfo : availableNetworks) {
            for (SubscriptionInfo subscriptionInfo : subscriptionInfos) {
                if (subscriptionInfo.getSubscriptionId() == availableNetworkInfo.getSubId()
                        && subscriptionInfo.isEmbedded()) {
                    return true;
                }
            }
        }

        return false;
    }

    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
         *  Reenable 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) {
        stopProfileScanningPrecedure();
        logDebug("stopProfileSelection");
        disableOpportunisticModem(callbackStub);
    }

    private void stopProfileScanningPrecedure() {
        synchronized (mLock) {
            if (mNetworkScanCallback != null) {
                sendUpdateNetworksCallbackHelper(mNetworkScanCallback,
                        TelephonyManager.UPDATE_AVAILABLE_NETWORKS_ABORTED);
                mNetworkScanCallback = null;
            }
            mNetworkScanCtlr.stopNetworkScan();

            mAvailableNetworkInfos = null;
            mIsEnabled = false;
        }
    }

    public boolean containsOpportunisticSubs(ArrayList<AvailableNetworkInfo> availableNetworks) {
        if (mOppSubscriptionInfos == null) {
            logDebug("received null subscription infos");
            return false;
        }

        if (mOppSubscriptionInfos.size() > 0) {
            logDebug("opportunistic subscriptions size " + mOppSubscriptionInfos.size());
            ArrayList<AvailableNetworkInfo> filteredAvailableNetworks =
                    getFilteredAvailableNetworks(
                            (ArrayList<AvailableNetworkInfo>)availableNetworks, mOppSubscriptionInfos);
            if (filteredAvailableNetworks.size() > 0) {
                return true;
            }
        }

        return false;
    }

    public boolean containStandaloneOppSubs(ArrayList<AvailableNetworkInfo> availableNetworks) {
        if (mStandaloneOppSubInfos == null) {
            logDebug("received null subscription infos");
            return false;
        }
        if (mStandaloneOppSubInfos.size() > 0) {
            logDebug("Standalone opportunistic subInfos size " + mStandaloneOppSubInfos.size());
            ArrayList<AvailableNetworkInfo> filteredAvailableNetworks =
                    getFilteredAvailableNetworks(
                            (ArrayList<AvailableNetworkInfo>) availableNetworks,
                            mStandaloneOppSubInfos);
            if (filteredAvailableNetworks.size() > 0) {
                return true;
            }
        }
        return false;
    }

    public boolean isOpportunisticSubActive() {
        if (mOppSubscriptionInfos == null) {
            logDebug("received null subscription infos");
            return false;
        }

        if (mOppSubscriptionInfos.size() > 0) {
            logDebug("opportunistic subscriptions size " + mOppSubscriptionInfos.size());
            for (SubscriptionInfo subscriptionInfo : mOppSubscriptionInfos) {
                if (mSubscriptionManager.isActiveSubId(subscriptionInfo.getSubscriptionId())) {
                    return true;
                }
            }
        }
        return false;
    }

    public void startProfileSelection(ArrayList<AvailableNetworkInfo> availableNetworks,
            IUpdateAvailableNetworksCallback callbackStub) {
        logDebug("startProfileSelection availableNetworks: " + availableNetworks);
        if (availableNetworks == null || availableNetworks.size() == 0) {
            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)
                || (isOpprotunisticSub(subId) && mSubscriptionManager.isActiveSubId(subId))) {
            ISub iSub = ISub.Stub.asInterface(
                    TelephonyFrameworkInitializer
                            .getTelephonyServiceManager()
                            .getSubscriptionServiceRegisterer()
                            .get());
            if (iSub == null) {
                log("Could not get Subscription Service handle");
                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;
            }
            try {
                iSub.setPreferredDataSubscriptionId(subId, needValidation, callbackStub);
            } catch (RemoteException ex) {
                log("Could not connect to Subscription Service");
                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 (isOpprotunisticSub(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() {
        return mSubscriptionManager.getPreferredDataSubscriptionId();
    }

    /**
     * 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() {
        synchronized (mLock) {
            mOppSubscriptionInfos = mSubscriptionManager
                    .getOpportunisticSubscriptions().stream()
                    .filter(subInfo -> subInfo.isGroupDisabled() != true)
                    .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;

        for (int i = 0; i < phoneCount; i++) {
            boolean hasActiveOpptProfile = false;
            for (SubscriptionInfo info : mOppSubscriptionInfos) {
                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 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();
        mThread = new HandlerThread(LOG_TAG);
        mThread.start();
        mHandler = new Handler(mThread.getLooper()) {
            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case MSG_PROFILE_UPDATE:
                        synchronized (mLock) {
                            updateOpportunisticSubscriptions();
                            enableModemStackForNonOpportunisticSlots();
                        }
                        break;
                    case MSG_START_PROFILE_SELECTION:
                        logDebug("Msg received for profile update");
                        synchronized (mLock) {
                            checkProfileUpdate((Object[]) msg.obj);
                        }
                        break;
                    case MSG_STOP_PROFILE_SELECTION:
                        logDebug("Msg received to stop profile selection");
                        synchronized (mLock) {
                            stopProfileSelectionProcess((IUpdateAvailableNetworksCallback) msg.obj);
                        }
                        break;
                    case MSG_SUB_SWITCH_COMPLETE:
                        logDebug("Msg received for sub switch");
                        synchronized (mLock) {
                            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) {
        Rlog.d(LOG_TAG, msg);
    }

    private void logDebug(String msg) {
        if (DBG) {
            Rlog.d(LOG_TAG, msg);
        }
    }
}
