/*
 * Copyright (C) 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.phone;

import static android.telephony.ims.ImsRcsManager.CAPABILITY_TYPE_OPTIONS_UCE;
import static android.telephony.ims.ImsRcsManager.CAPABILITY_TYPE_PRESENCE_UCE;
import static android.telephony.ims.ProvisioningManager.KEY_EAB_PROVISIONING_STATUS;
import static android.telephony.ims.ProvisioningManager.KEY_VOICE_OVER_WIFI_ENABLED_OVERRIDE;
import static android.telephony.ims.ProvisioningManager.KEY_VOLTE_PROVISIONING_STATUS;
import static android.telephony.ims.ProvisioningManager.KEY_VT_PROVISIONING_STATUS;
import static android.telephony.ims.ProvisioningManager.PROVISIONING_VALUE_DISABLED;
import static android.telephony.ims.ProvisioningManager.PROVISIONING_VALUE_ENABLED;
import static android.telephony.ims.feature.ImsFeature.FEATURE_MMTEL;
import static android.telephony.ims.feature.ImsFeature.FEATURE_RCS;
import static android.telephony.ims.feature.MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_CALL_COMPOSER;
import static android.telephony.ims.feature.MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_SMS;
import static android.telephony.ims.feature.MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_UT;
import static android.telephony.ims.feature.MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO;
import static android.telephony.ims.feature.MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE;
import static android.telephony.ims.stub.ImsRegistrationImplBase.REGISTRATION_TECH_CROSS_SIM;
import static android.telephony.ims.stub.ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN;
import static android.telephony.ims.stub.ImsRegistrationImplBase.REGISTRATION_TECH_LTE;
import static android.telephony.ims.stub.ImsRegistrationImplBase.REGISTRATION_TECH_MAX;
import static android.telephony.ims.stub.ImsRegistrationImplBase.REGISTRATION_TECH_NONE;
import static android.telephony.ims.stub.ImsRegistrationImplBase.REGISTRATION_TECH_NR;

import android.annotation.Nullable;
import android.content.Context;
import android.os.AsyncResult;
import android.os.Binder;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.PersistableBundle;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.telephony.CarrierConfigManager;
import android.telephony.CarrierConfigManager.Ims;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyRegistryManager;
import android.telephony.ims.ProvisioningManager;
import android.telephony.ims.aidl.IFeatureProvisioningCallback;
import android.telephony.ims.aidl.IImsConfig;
import android.telephony.ims.aidl.IImsConfigCallback;
import android.telephony.ims.feature.MmTelFeature.MmTelCapabilities;
import android.telephony.ims.feature.RcsFeature.RcsImsCapabilities;
import android.telephony.ims.stub.ImsConfigImplBase;
import android.telephony.ims.stub.ImsRegistrationImplBase;
import android.util.SparseArray;

import com.android.ims.FeatureConnector;
import com.android.ims.ImsConfig;
import com.android.ims.ImsException;
import com.android.ims.ImsManager;
import com.android.ims.RcsFeatureManager;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.PhoneConfigurationManager;
import com.android.internal.telephony.flags.FeatureFlags;
import com.android.internal.telephony.util.HandlerExecutor;
import com.android.telephony.Rlog;

import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.Executor;

/**
 * Provides APIs for MMTEL and RCS provisioning status. This class handles provisioning status and
 * notifies the status changing for each capability
 * {{@link MmTelCapabilities.MmTelCapability} for MMTel services}
 * {{@link RcsImsCapabilities.RcsImsCapabilityFlag} for RCS services}
 */
public class ImsProvisioningController {
    private static final String TAG = "ImsProvisioningController";
    private static final int INVALID_VALUE = -1;

    private static final int EVENT_SUB_CHANGED = 1;
    private static final int EVENT_PROVISIONING_CAPABILITY_CHANGED = 2;
    @VisibleForTesting
    protected static final int EVENT_MULTI_SIM_CONFIGURATION_CHANGE = 3;
    private static final int EVENT_PROVISIONING_VALUE_CHANGED = 4;
    private static final int EVENT_NOTIFY_INIT_PROVISIONED_VALUE = 5;

    // Provisioning Keys that are handled via AOSP cache and not sent to the ImsService
    private static final int[] LOCAL_IMS_CONFIG_KEYS = {
            KEY_VOLTE_PROVISIONING_STATUS,
            KEY_VT_PROVISIONING_STATUS,
            KEY_VOICE_OVER_WIFI_ENABLED_OVERRIDE,
            KEY_EAB_PROVISIONING_STATUS
    };
    private static final int[] LOCAL_RADIO_TECHS = {
            REGISTRATION_TECH_LTE,
            REGISTRATION_TECH_IWLAN,
            REGISTRATION_TECH_CROSS_SIM,
            REGISTRATION_TECH_NR
    };

    private static final int MMTEL_CAPABILITY_MIN = MmTelCapabilities.CAPABILITY_TYPE_NONE;
    private static final int MMTEL_CAPABILITY_MAX = MmTelCapabilities.CAPABILITY_TYPE_MAX;

    private static final int RCS_CAPABILITY_MIN = RcsImsCapabilities.CAPABILITY_TYPE_NONE;
    private static final int RCS_CAPABILITY_MAX = RcsImsCapabilities.CAPABILITY_TYPE_MAX;

    private static final int[] LOCAL_MMTEL_CAPABILITY = {
            CAPABILITY_TYPE_VOICE,
            CAPABILITY_TYPE_VIDEO,
            CAPABILITY_TYPE_UT,
            CAPABILITY_TYPE_SMS,
            CAPABILITY_TYPE_CALL_COMPOSER
    };

    private static final int[] LOCAL_RCS_CAPABILITY = {
            CAPABILITY_TYPE_OPTIONS_UCE,
            CAPABILITY_TYPE_PRESENCE_UCE
    };

    /**
     * map the MmTelCapabilities.MmTelCapability and
     * CarrierConfigManager.Ims.KEY_CAPABILITY_TYPE_VOICE_INT
     * CarrierConfigManager.Ims.KEY_CAPABILITY_TYPE_VIDEO_INT
     * CarrierConfigManager.Ims.KEY_CAPABILITY_TYPE_UT_INT
     * CarrierConfigManager.Ims.KEY_CAPABILITY_TYPE_SMS_INT
     * CarrierConfigManager.Ims.KEY_CAPABILITY_TYPE_CALL_COMPOSER_INT
     */
    private static final Map<Integer, String> KEYS_MMTEL_CAPABILITY = Map.of(
            CAPABILITY_TYPE_VOICE, Ims.KEY_CAPABILITY_TYPE_VOICE_INT_ARRAY,
            CAPABILITY_TYPE_VIDEO, Ims.KEY_CAPABILITY_TYPE_VIDEO_INT_ARRAY,
            CAPABILITY_TYPE_UT, Ims.KEY_CAPABILITY_TYPE_UT_INT_ARRAY,
            CAPABILITY_TYPE_SMS, Ims.KEY_CAPABILITY_TYPE_SMS_INT_ARRAY,
            CAPABILITY_TYPE_CALL_COMPOSER, Ims.KEY_CAPABILITY_TYPE_CALL_COMPOSER_INT_ARRAY
    );

    /**
     * map the RcsImsCapabilities.RcsImsCapabilityFlag and
     * CarrierConfigManager.Ims.KEY_CAPABILITY_TYPE_OPTIONS_UCE
     * CarrierConfigManager.Ims.KEY_CAPABILITY_TYPE_PRESENCE_UCE
     */
    private static final Map<Integer, String> KEYS_RCS_CAPABILITY = Map.of(
            CAPABILITY_TYPE_OPTIONS_UCE, Ims.KEY_CAPABILITY_TYPE_OPTIONS_UCE_INT_ARRAY,
            CAPABILITY_TYPE_PRESENCE_UCE, Ims.KEY_CAPABILITY_TYPE_PRESENCE_UCE_INT_ARRAY
    );

    /**
     * Create a FeatureConnector for this class to use to connect to an ImsManager.
     */
    @VisibleForTesting
    public interface MmTelFeatureConnector {
        /**
         * Create a FeatureConnector for this class to use to connect to an ImsManager.
         * @param listener will receive ImsManager instance.
         * @param executor that the Listener callbacks will be called on.
         * @return A FeatureConnector
         */
        FeatureConnector<ImsManager> create(Context context, int slotId,
                String logPrefix, FeatureConnector.Listener<ImsManager> listener,
                Executor executor);
    }

    /**
     * Create a FeatureConnector for this class to use to connect to an RcsFeatureManager.
     */
    @VisibleForTesting
    public interface RcsFeatureConnector {
        /**
         * Create a FeatureConnector for this class to use to connect to an RcsFeatureManager.
         * @param listener will receive RcsFeatureManager instance.
         * @param executor that the Listener callbacks will be called on.
         * @return A FeatureConnector
         */
        FeatureConnector<RcsFeatureManager> create(Context context, int slotId,
                FeatureConnector.Listener<RcsFeatureManager> listener,
                Executor executor, String logPrefix);
    }

    private static ImsProvisioningController sInstance;

    private final PhoneGlobals mApp;
    private final Handler mHandler;
    private final CarrierConfigManager mCarrierConfigManager;
    private final SubscriptionManager mSubscriptionManager;
    private final TelephonyRegistryManager mTelephonyRegistryManager;
    private final MmTelFeatureConnector mMmTelFeatureConnector;
    private final RcsFeatureConnector mRcsFeatureConnector;

    // maps a slotId to a list of MmTelFeatureListeners
    private final SparseArray<MmTelFeatureListener> mMmTelFeatureListenersSlotMap =
            new SparseArray<>();
    // maps a slotId to a list of RcsFeatureListeners
    private final SparseArray<RcsFeatureListener> mRcsFeatureListenersSlotMap =
            new SparseArray<>();
    // map a slotId to a list of ProvisioningCallbackManager
    private final SparseArray<ProvisioningCallbackManager> mProvisioningCallbackManagersSlotMap =
            new SparseArray<>();
    private final ImsProvisioningLoader mImsProvisioningLoader;
    private final FeatureFlags mFeatureFlags;

    private int mNumSlot;

    /**
     * This class contains the provisioning status to notify changes.
     * {{@link MmTelCapabilities.MmTelCapability} for MMTel services}
     * {{@link android.telephony.ims.ImsRcsManager.RcsImsCapabilityFlag} for RCS services}
     * {{@link ImsRegistrationImplBase.ImsRegistrationTech} for Registration tech}
     */
    private static final class FeatureProvisioningData {
        public final int mCapability;
        public final int mTech;
        public final boolean mProvisioned;
        public final boolean mIsMmTel;

        FeatureProvisioningData(int capability, int tech, boolean provisioned, boolean isMmTel) {
            mCapability = capability;
            mTech = tech;
            mProvisioned = provisioned;
            mIsMmTel = isMmTel;
        }
    }

    private final class MessageHandler extends Handler {
        private static final String LOG_PREFIX = "Handler";
        MessageHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case EVENT_SUB_CHANGED:
                    onSubscriptionsChanged();
                    break;
                case EVENT_PROVISIONING_CAPABILITY_CHANGED:
                    try {
                        mProvisioningCallbackManagersSlotMap.get(msg.arg1)
                                .notifyProvisioningCapabilityChanged(
                                        (FeatureProvisioningData) msg.obj);
                    } catch (NullPointerException e) {
                        logw(LOG_PREFIX, msg.arg1,
                                "can not find callback manager message" + msg.what);
                    }
                    break;
                case EVENT_MULTI_SIM_CONFIGURATION_CHANGE:
                    int activeModemCount = (int) ((AsyncResult) msg.obj).result;
                    onMultiSimConfigChanged(activeModemCount);
                    break;
                case EVENT_PROVISIONING_VALUE_CHANGED:
                    log("subId " + msg.arg1 + " changed provisioning value item : " + msg.arg2
                            + " value : " + (int) msg.obj);
                    updateCapabilityTechFromKey(msg.arg1, msg.arg2, (int) msg.obj);
                    break;
                case EVENT_NOTIFY_INIT_PROVISIONED_VALUE:
                    int slotId = msg.arg1;
                    int subId = msg.arg2;
                    IFeatureProvisioningCallback callback =
                            (IFeatureProvisioningCallback) msg.obj;
                    log("slotId " + slotId + " subId " + subId
                            + " callback " + (callback != null));

                    // Notify MmTel Provisioning Status
                    notifyMmTelProvisioningStatus(slotId, subId, callback);
                    notifyRcsProvisioningStatus(slotId, subId, callback);
                    break;
                default:
                    log("unknown message " + msg);
                    break;
            }
        }
    }

    private final SubscriptionManager.OnSubscriptionsChangedListener mSubChangedListener =
            new SubscriptionManager.OnSubscriptionsChangedListener() {
                @Override
                public void onSubscriptionsChanged() {
                    if (!mHandler.hasMessages(EVENT_SUB_CHANGED)) {
                        mHandler.sendEmptyMessage(EVENT_SUB_CHANGED);
                    }
                }
            };

    private final class ProvisioningCallbackManager {
        private static final String LOG_PREFIX = "ProvisioningCallbackManager";
        private RemoteCallbackList<IFeatureProvisioningCallback> mIFeatureProvisioningCallbackList;
        private int mSubId;
        private int mSlotId;

        ProvisioningCallbackManager(int slotId) {
            mIFeatureProvisioningCallbackList =
                    new RemoteCallbackList<IFeatureProvisioningCallback>();
            mSlotId = slotId;
            mSubId = getSubId(slotId);
            log(LOG_PREFIX, mSlotId, "ProvisioningCallbackManager create");
        }

        public void clear() {
            log(LOG_PREFIX, mSlotId, "ProvisioningCallbackManager clear ");

            mIFeatureProvisioningCallbackList.kill();

            // All registered callbacks are unregistered, and the list is disabled
            // need to create again
            mIFeatureProvisioningCallbackList =
                    new RemoteCallbackList<IFeatureProvisioningCallback>();
        }

        public void registerCallback(IFeatureProvisioningCallback localCallback) {
            if (!mIFeatureProvisioningCallbackList.register(localCallback, (Object) mSubId)) {
                log(LOG_PREFIX, mSlotId, "registration callback fail");
            }
        }

        public void unregisterCallback(IFeatureProvisioningCallback localCallback) {
            mIFeatureProvisioningCallbackList.unregister(localCallback);
        }

        public void setSubId(int subId) {
            if (mSubId == subId) {
                log(LOG_PREFIX, mSlotId, "subId is not changed ");
                return;
            }

            mSubId = subId;
            mSlotId = getSlotId(subId);

            // subId changed means the registered callbacks are not available.
            clear();
        }

        public boolean hasCallblacks() {
            int size = mIFeatureProvisioningCallbackList.beginBroadcast();
            mIFeatureProvisioningCallbackList.finishBroadcast();

            return (size > 0);
        }

        public void notifyProvisioningCapabilityChanged(FeatureProvisioningData data) {
            int size = mIFeatureProvisioningCallbackList.beginBroadcast();
            for (int index = 0; index < size; index++) {
                try {
                    IFeatureProvisioningCallback imsFeatureProvisioningCallback =
                            mIFeatureProvisioningCallbackList.getBroadcastItem(index);

                    // MMTEL
                    if (data.mIsMmTel
                            && Arrays.stream(LOCAL_MMTEL_CAPABILITY)
                            .anyMatch(value -> value == data.mCapability)) {
                        imsFeatureProvisioningCallback.onFeatureProvisioningChanged(
                                data.mCapability, data.mTech, data.mProvisioned);
                        logi(LOG_PREFIX, mSlotId, "notifyProvisioningCapabilityChanged : "
                                + "onFeatureProvisioningChanged"
                                + " capability " + data.mCapability
                                + " tech "  + data.mTech
                                + " isProvisioned " + data.mProvisioned);
                    } else if (data.mCapability == CAPABILITY_TYPE_PRESENCE_UCE) {
                        imsFeatureProvisioningCallback.onRcsFeatureProvisioningChanged(
                                data.mCapability, data.mTech, data.mProvisioned);
                        logi(LOG_PREFIX, mSlotId, "notifyProvisioningCapabilityChanged : "
                                + "onRcsFeatureProvisioningChanged"
                                + " capability " + data.mCapability
                                + " tech "  + data.mTech
                                + " isProvisioned " + data.mProvisioned);
                    } else {
                        loge(LOG_PREFIX, mSlotId, "notifyProvisioningCapabilityChanged : "
                                + "unknown capability "
                                + data.mCapability);
                    }
                } catch (RemoteException e) {
                    loge(LOG_PREFIX, mSlotId,
                            "notifyProvisioningChanged: callback #" + index + " failed");
                }
            }
            mIFeatureProvisioningCallbackList.finishBroadcast();
        }
    }

    private final class MmTelFeatureListener implements FeatureConnector.Listener<ImsManager> {
        private static final String LOG_PREFIX = "MmTelFeatureListener";
        private FeatureConnector<ImsManager> mConnector;
        private ImsManager mImsManager;
        private boolean mReady = false;
        // stores whether the initial provisioning key value should be notified to ImsService
        private boolean mRequiredNotify = false;
        private int mSubId;
        private int mSlotId;
        private ConfigCallback mConfigCallback;

        MmTelFeatureListener(int slotId) {
            log(LOG_PREFIX, slotId, "created");

            mSlotId = slotId;
            mSubId = getSubId(slotId);
            mConfigCallback = new ConfigCallback(mSubId);

            mConnector = mMmTelFeatureConnector.create(
                    mApp, slotId, TAG, this, new HandlerExecutor(mHandler));
            mConnector.connect();
        }

        public void setSubId(int subId) {
            if (mRequiredNotify && mReady) {
                mRequiredNotify = false;
                setInitialProvisioningKeys(subId);
            }
            if (mSubId == subId) {
                log(LOG_PREFIX, mSlotId, "subId is not changed");
                return;
            }

            mSubId = subId;
            mSlotId = getSlotId(subId);
            mConfigCallback.setSubId(subId);
        }

        public void destroy() {
            log("destroy");
            if (mImsManager != null) {
                try {
                    ImsConfig imsConfig = getImsConfig(mImsManager);
                    if (imsConfig != null) {
                        imsConfig.removeConfigCallback(mConfigCallback);
                    }
                } catch (ImsException e) {
                    logw(LOG_PREFIX, mSlotId, "destroy : " + e.getMessage());
                }
            }
            mConfigCallback = null;
            mConnector.disconnect();
            mConnector = null;
            mReady = false;
            mImsManager = null;
        }

        public @Nullable ImsManager getImsManager() {
            return mImsManager;
        }

        @Override
        public void connectionReady(ImsManager manager, int subId) {
            log(LOG_PREFIX, mSlotId, "connection ready");
            mReady = true;
            mImsManager = manager;

            if (mImsManager != null) {
                try {
                    ImsConfig imsConfig = getImsConfig(mImsManager);
                    if (imsConfig != null) {
                        imsConfig.addConfigCallback(mConfigCallback);
                    }
                } catch (ImsException e) {
                    logw(LOG_PREFIX, mSlotId, "addConfigCallback : " + e.getMessage());
                }
            }

            onMmTelAvailable();
        }

        @Override
        public void connectionUnavailable(int reason) {
            log(LOG_PREFIX, mSlotId, "connection unavailable " + reason);

            mReady = false;
            mImsManager = null;

            // keep the callback for other reason
            if (reason == FeatureConnector.UNAVAILABLE_REASON_IMS_UNSUPPORTED) {
                onMmTelUnavailable();
            }
        }

        public int setProvisioningValue(int key, int value) {
            int retVal = ImsConfigImplBase.CONFIG_RESULT_FAILED;

            if (!mReady) {
                loge(LOG_PREFIX, mSlotId, "service is Unavailable");
                return retVal;
            }
            try {
                // getConfigInterface() will return not null or throw the ImsException
                // need not null checking
                ImsConfig imsConfig = getImsConfig(mImsManager);
                retVal = imsConfig.setConfig(key, value);
                log(LOG_PREFIX, mSlotId, "setConfig called with key " + key + " value " + value);
            } catch (ImsException e) {
                logw(LOG_PREFIX, mSlotId,
                        "setConfig operation failed for key =" + key
                        + ", value =" + value + ". Exception:" + e.getMessage());
            }
            return retVal;
        }

        public int getProvisioningValue(int key) {
            if (!mReady) {
                loge(LOG_PREFIX, mSlotId, "service is Unavailable");
                return INVALID_VALUE;
            }

            int retValue = INVALID_VALUE;
            try {
                // getConfigInterface() will return not null or throw the ImsException
                // need not null checking
                ImsConfig imsConfig = getImsConfig(mImsManager);
                retValue = imsConfig.getConfigInt(key);
            } catch (ImsException e) {
                logw(LOG_PREFIX, mSlotId,
                        "getConfig operation failed for key =" + key
                        + ", value =" + retValue + ". Exception:" + e.getMessage());
            }
            return retValue;
        }

        public void onMmTelAvailable() {
            log(LOG_PREFIX, mSlotId, "onMmTelAvailable");

            if (isValidSubId(mSubId)) {
                mRequiredNotify = false;

                // notify provisioning key value to ImsService
                setInitialProvisioningKeys(mSubId);

                if (mFeatureFlags.notifyInitialImsProvisioningStatus()) {
                    // Notify MmTel provisioning value based on capability and radio tech.
                    if (mProvisioningCallbackManagersSlotMap.get(mSlotId).hasCallblacks()) {
                        notifyMmTelProvisioningStatus(mSlotId, mSubId, null);
                    }
                }
            } else {
                // wait until subId is valid
                mRequiredNotify = true;
            }
        }

        public void onMmTelUnavailable() {
            log(LOG_PREFIX, mSlotId, "onMmTelUnavailable");

            try {
                // delete all callbacks reference from ProvisioningManager
                mProvisioningCallbackManagersSlotMap.get(getSlotId(mSubId)).clear();
            } catch (NullPointerException e) {
                logw(LOG_PREFIX, getSlotId(mSubId), "can not find callback manager to clear");
            }
        }

        private void setInitialProvisioningKeys(int subId) {
            boolean required;
            int value = ImsProvisioningLoader.STATUS_NOT_SET;

            // updating KEY_VOLTE_PROVISIONING_STATUS
            try {
                required = isImsProvisioningRequiredForCapability(subId, CAPABILITY_TYPE_VOICE,
                        REGISTRATION_TECH_LTE);
            } catch (IllegalArgumentException e) {
                logw("setInitialProvisioningKeys: KEY_VOLTE_PROVISIONING_STATUS failed for"
                        + " subId=" + subId + ", exception: " + e.getMessage());
                return;
            }

            log(LOG_PREFIX, mSlotId,
                    "setInitialProvisioningKeys provisioning required(voice, lte) " + required);
            if (required) {
                value = mImsProvisioningLoader.getProvisioningStatus(subId, FEATURE_MMTEL,
                        CAPABILITY_TYPE_VOICE, REGISTRATION_TECH_LTE);
                if (value != ImsProvisioningLoader.STATUS_NOT_SET) {
                    value = (value == ImsProvisioningLoader.STATUS_PROVISIONED)
                            ? PROVISIONING_VALUE_ENABLED : PROVISIONING_VALUE_DISABLED;
                    setProvisioningValue(KEY_VOLTE_PROVISIONING_STATUS, value);
                }
            }

            // updating KEY_VT_PROVISIONING_STATUS
            try {
                required = isImsProvisioningRequiredForCapability(subId, CAPABILITY_TYPE_VIDEO,
                        REGISTRATION_TECH_LTE);
            } catch (IllegalArgumentException e) {
                logw("setInitialProvisioningKeys: KEY_VT_PROVISIONING_STATUS failed for"
                        + " subId=" + subId + ", exception: " + e.getMessage());
                return;
            }

            log(LOG_PREFIX, mSlotId,
                    "setInitialProvisioningKeys provisioning required(video, lte) " + required);
            if (required) {
                value = mImsProvisioningLoader.getProvisioningStatus(subId, FEATURE_MMTEL,
                        CAPABILITY_TYPE_VIDEO, REGISTRATION_TECH_LTE);
                if (value != ImsProvisioningLoader.STATUS_NOT_SET) {
                    value = (value == ImsProvisioningLoader.STATUS_PROVISIONED)
                            ? PROVISIONING_VALUE_ENABLED : PROVISIONING_VALUE_DISABLED;
                    setProvisioningValue(KEY_VT_PROVISIONING_STATUS, value);
                }
            }

            // updating KEY_VOICE_OVER_WIFI_ENABLED_OVERRIDE
            try {
                required = isImsProvisioningRequiredForCapability(subId, CAPABILITY_TYPE_VOICE,
                        REGISTRATION_TECH_IWLAN);
            } catch (IllegalArgumentException e) {
                logw("setInitialProvisioningKeys: KEY_VOICE_OVER_WIFI_ENABLED_OVERRIDE failed"
                        + " for subId=" + subId + ", exception: " + e.getMessage());
                return;
            }

            log(LOG_PREFIX, mSlotId,
                    "setInitialProvisioningKeys provisioning required(voice, iwlan) " + required);
            if (required) {
                value = mImsProvisioningLoader.getProvisioningStatus(subId, FEATURE_MMTEL,
                        CAPABILITY_TYPE_VOICE, REGISTRATION_TECH_IWLAN);
                if (value != ImsProvisioningLoader.STATUS_NOT_SET) {
                    value = (value == ImsProvisioningLoader.STATUS_PROVISIONED)
                            ? PROVISIONING_VALUE_ENABLED : PROVISIONING_VALUE_DISABLED;
                    setProvisioningValue(KEY_VOICE_OVER_WIFI_ENABLED_OVERRIDE, value);
                }
            }
        }
    }

    private final class RcsFeatureListener implements FeatureConnector.Listener<RcsFeatureManager> {
        private static final String LOG_PREFIX = "RcsFeatureListener";
        private FeatureConnector<RcsFeatureManager> mConnector;
        private RcsFeatureManager mRcsFeatureManager;
        private boolean mReady = false;
        // stores whether the initial provisioning key value should be notified to ImsService
        private boolean mRequiredNotify = false;
        private int mSubId;
        private int mSlotId;
        private ConfigCallback mConfigCallback;

        RcsFeatureListener(int slotId) {
            log(LOG_PREFIX, slotId, "created");

            mSlotId = slotId;
            mSubId = getSubId(slotId);
            mConfigCallback = new ConfigCallback(mSubId);

            mConnector = mRcsFeatureConnector.create(
                    mApp, slotId, this, new HandlerExecutor(mHandler), TAG);
            mConnector.connect();
        }

        public void setSubId(int subId) {
            if (mRequiredNotify && mReady) {
                mRequiredNotify = false;
                setInitialProvisioningKeys(subId);
            }
            if (mSubId == subId) {
                log(LOG_PREFIX, mSlotId, "subId is not changed");
                return;
            }

            mSubId = subId;
            mSlotId = getSlotId(subId);
            mConfigCallback.setSubId(subId);
        }

        public void destroy() {
            log(LOG_PREFIX, mSlotId, "destroy");
            if (mRcsFeatureManager != null) {
                try {
                    ImsConfig imsConfig = getImsConfig(mRcsFeatureManager.getConfig());
                    if (imsConfig != null) {
                        imsConfig.removeConfigCallback(mConfigCallback);
                    }
                } catch (ImsException e) {
                    logw(LOG_PREFIX, mSlotId, "destroy :" + e.getMessage());
                }
            }
            mConfigCallback = null;
            mConnector.disconnect();
            mConnector = null;
            mReady = false;
            mRcsFeatureManager = null;
        }

        @Override
        public void connectionReady(RcsFeatureManager manager, int subId) {
            log(LOG_PREFIX, mSlotId, "connection ready");
            mReady = true;
            mRcsFeatureManager = manager;

            if (mRcsFeatureManager != null) {
                try {
                    ImsConfig imsConfig = getImsConfig(mRcsFeatureManager.getConfig());
                    if (imsConfig != null) {
                        imsConfig.addConfigCallback(mConfigCallback);
                    }
                } catch (ImsException e) {
                    logw(LOG_PREFIX, mSlotId, "addConfigCallback :" + e.getMessage());
                }
            }

            onRcsAvailable();
        }

        @Override
        public void connectionUnavailable(int reason) {
            log(LOG_PREFIX, mSlotId, "connection unavailable");
            mReady = false;
            mRcsFeatureManager = null;

            // keep the callback for other reason
            if (reason == FeatureConnector.UNAVAILABLE_REASON_IMS_UNSUPPORTED) {
                onRcsUnavailable();
            }
        }

        public int setProvisioningValue(int key, int value) {
            int retVal = ImsConfigImplBase.CONFIG_RESULT_FAILED;

            if (!mReady) {
                loge(LOG_PREFIX, mSlotId, "service is Unavailable");
                return retVal;
            }

            try {
                // getConfigInterface() will return not null or throw the ImsException
                // need not null checking
                ImsConfig imsConfig = getImsConfig(mRcsFeatureManager.getConfig());
                retVal = imsConfig.setConfig(key, value);
                log(LOG_PREFIX, mSlotId, "setConfig called with key " + key + " value " + value);
            } catch (ImsException e) {
                logw(LOG_PREFIX, mSlotId,
                        "setConfig operation failed for key =" + key
                        + ", value =" + value + ". Exception:" + e.getMessage());
            }
            return retVal;
        }

        public int getProvisioningValue(int key) {
            if (!mReady) {
                loge(LOG_PREFIX, mSlotId, "service is Unavailable");
                return INVALID_VALUE;
            }

            int retValue = INVALID_VALUE;
            try {
                // getConfigInterface() will return not null or throw the ImsException
                // need not null checking
                ImsConfig imsConfig = getImsConfig(mRcsFeatureManager.getConfig());
                retValue = imsConfig.getConfigInt(key);
            } catch (ImsException e) {
                logw(LOG_PREFIX, mSlotId,
                        "getConfig operation failed for key =" + key
                        + ", value =" + retValue + ". Exception:" + e.getMessage());
            }
            return retValue;
        }

        public boolean isConnectionReady() {
            return mReady;
        }

        public void onRcsAvailable() {
            log(LOG_PREFIX, mSlotId, "onRcsAvailable");

            if (isValidSubId(mSubId)) {
                mRequiredNotify = false;

                // notify provisioning key value to ImsService
                setInitialProvisioningKeys(mSubId);

                if (mFeatureFlags.notifyInitialImsProvisioningStatus()) {
                    if (mProvisioningCallbackManagersSlotMap.get(mSlotId).hasCallblacks()) {
                        // Notify RCS provisioning value based on capability and radio tech.
                        notifyRcsProvisioningStatus(mSlotId, mSubId, null);
                    }
                }
            } else {
                // wait until subId is valid
                mRequiredNotify = true;
            }
        }

        public void onRcsUnavailable() {
            log(LOG_PREFIX, mSlotId, "onRcsUnavailable");

            try {
                // delete all callbacks reference from ProvisioningManager
                mProvisioningCallbackManagersSlotMap.get(getSlotId(mSubId)).clear();
            } catch (NullPointerException e) {
                logw(LOG_PREFIX, getSlotId(mSubId), "can not find callback manager to clear");
            }
        }

        private void setInitialProvisioningKeys(int subId) {
            boolean required;
            int value = ImsProvisioningLoader.STATUS_NOT_SET;

            // KEY_EAB_PROVISIONING_STATUS
            int capability = CAPABILITY_TYPE_PRESENCE_UCE;
            // Assume that all radio techs have the same provisioning value
            int tech = REGISTRATION_TECH_LTE;

            try {
                required = isRcsProvisioningRequiredForCapability(subId, capability, tech);
            } catch (IllegalArgumentException e) {
                logw("setInitialProvisioningKeys: KEY_EAB_PROVISIONING_STATUS failed for"
                        + " subId=" + subId + ", exception: " + e.getMessage());
                return;
            }

            if (required) {
                value = mImsProvisioningLoader.getProvisioningStatus(subId, FEATURE_RCS,
                        capability, tech);
                if (value != ImsProvisioningLoader.STATUS_NOT_SET) {
                    value = (value == ImsProvisioningLoader.STATUS_PROVISIONED)
                            ? PROVISIONING_VALUE_ENABLED : PROVISIONING_VALUE_DISABLED;
                    setProvisioningValue(KEY_EAB_PROVISIONING_STATUS, value);
                }
            }
        }
    }

    // When vendor ImsService changed provisioning data, which should be updated in AOSP.
    // Catch the event using IImsConfigCallback.
    private final class ConfigCallback extends IImsConfigCallback.Stub {
        private int mSubId;

        ConfigCallback(int subId) {
            mSubId = subId;
        }

        public void setSubId(int subId) {
            mSubId = subId;
        }

        @Override
        public void onIntConfigChanged(int item, int value) throws RemoteException {
            if (!Arrays.stream(LOCAL_IMS_CONFIG_KEYS).anyMatch(keyValue -> keyValue == item)) {
                return;
            }

            final long callingIdentity = Binder.clearCallingIdentity();
            try {
                if (mHandler != null) {
                    mHandler.sendMessage(mHandler.obtainMessage(
                            EVENT_PROVISIONING_VALUE_CHANGED, mSubId, item, (Object) value));
                }
            } finally {
                Binder.restoreCallingIdentity(callingIdentity);
            }
        }

        @Override
        public void onStringConfigChanged(int item, String value) throws RemoteException {
            // Ignore this callback.
        }
    }

    /**
     * Do NOT use this directly, instead use {@link #getInstance()}.
     */
    @VisibleForTesting
    public ImsProvisioningController(PhoneGlobals app, int numSlot, Looper looper,
            MmTelFeatureConnector mmTelFeatureConnector, RcsFeatureConnector rcsFeatureConnector,
            ImsProvisioningLoader imsProvisioningLoader, FeatureFlags featureFlags) {
        log("ImsProvisioningController");
        mApp = app;
        mNumSlot = numSlot;
        mHandler = new MessageHandler(looper);
        mMmTelFeatureConnector = mmTelFeatureConnector;
        mRcsFeatureConnector = rcsFeatureConnector;
        mCarrierConfigManager = mApp.getSystemService(CarrierConfigManager.class);
        mSubscriptionManager = mApp.getSystemService(SubscriptionManager.class);
        mTelephonyRegistryManager = mApp.getSystemService(TelephonyRegistryManager.class);
        mTelephonyRegistryManager.addOnSubscriptionsChangedListener(
                mSubChangedListener, mHandler::post);
        mImsProvisioningLoader = imsProvisioningLoader;
        mFeatureFlags = featureFlags;

        PhoneConfigurationManager.registerForMultiSimConfigChange(mHandler,
                EVENT_MULTI_SIM_CONFIGURATION_CHANGE, null);

        initialize(numSlot);
    }

    private void initialize(int numSlot) {
        for (int i = 0; i < numSlot; i++) {
            MmTelFeatureListener m = new MmTelFeatureListener(i);
            mMmTelFeatureListenersSlotMap.put(i, m);

            RcsFeatureListener r = new RcsFeatureListener(i);
            mRcsFeatureListenersSlotMap.put(i, r);

            ProvisioningCallbackManager p = new ProvisioningCallbackManager(i);
            mProvisioningCallbackManagersSlotMap.put(i, p);
        }
    }

    private void onMultiSimConfigChanged(int newNumSlot) {
        log("onMultiSimConfigChanged: NumSlot " + mNumSlot + " newNumSlot " + newNumSlot);

        if (mNumSlot < newNumSlot) {
            for (int i = mNumSlot; i < newNumSlot; i++) {
                MmTelFeatureListener m = new MmTelFeatureListener(i);
                mMmTelFeatureListenersSlotMap.put(i, m);

                RcsFeatureListener r = new RcsFeatureListener(i);
                mRcsFeatureListenersSlotMap.put(i, r);

                ProvisioningCallbackManager p = new ProvisioningCallbackManager(i);
                mProvisioningCallbackManagersSlotMap.put(i, p);
            }
        } else if (mNumSlot > newNumSlot) {
            for (int i = (mNumSlot - 1); i > (newNumSlot - 1); i--) {
                MmTelFeatureListener m = mMmTelFeatureListenersSlotMap.get(i);
                mMmTelFeatureListenersSlotMap.remove(i);
                m.destroy();

                RcsFeatureListener r = mRcsFeatureListenersSlotMap.get(i);
                mRcsFeatureListenersSlotMap.remove(i);
                r.destroy();

                ProvisioningCallbackManager p = mProvisioningCallbackManagersSlotMap.get(i);
                mProvisioningCallbackManagersSlotMap.remove(i);
                p.clear();
            }
        }

        mNumSlot = newNumSlot;
    }

    /**
     * destroy the instance
     */
    @VisibleForTesting
    public void destroy() {
        log("destroy");

        mHandler.getLooper().quit();

        mTelephonyRegistryManager.removeOnSubscriptionsChangedListener(mSubChangedListener);

        for (int i = 0; i < mMmTelFeatureListenersSlotMap.size(); i++) {
            mMmTelFeatureListenersSlotMap.get(i).destroy();
        }
        mMmTelFeatureListenersSlotMap.clear();

        for (int i = 0; i < mRcsFeatureListenersSlotMap.size(); i++) {
            mRcsFeatureListenersSlotMap.get(i).destroy();
        }
        mRcsFeatureListenersSlotMap.clear();

        for (int i = 0; i < mProvisioningCallbackManagersSlotMap.size(); i++) {
            mProvisioningCallbackManagersSlotMap.get(i).clear();
        }
    }

    /**
     * create an instance
     */
    @VisibleForTesting
    public static ImsProvisioningController make(PhoneGlobals app, int numSlot,
            FeatureFlags featureFlags) {
        synchronized (ImsProvisioningController.class) {
            if (sInstance == null) {
                Rlog.i(TAG, "ImsProvisioningController created");
                HandlerThread handlerThread = new HandlerThread(TAG);
                handlerThread.start();
                sInstance = new ImsProvisioningController(app, numSlot, handlerThread.getLooper(),
                        ImsManager::getConnector, RcsFeatureManager::getConnector,
                        new ImsProvisioningLoader(app), featureFlags);
            }
        }
        return sInstance;
    }

    /**
     * Gets a ImsProvisioningController instance
     */
    @VisibleForTesting
    public static ImsProvisioningController getInstance() {
        synchronized (ImsProvisioningController.class) {
            return sInstance;
        }
    }

    /**
     * Register IFeatureProvisioningCallback from ProvisioningManager
     */

    @VisibleForTesting
    public void addFeatureProvisioningChangedCallback(int subId,
            IFeatureProvisioningCallback callback) {
        if (callback == null) {
            throw new IllegalArgumentException("provisioning callback can't be null");
        }
        int slotId = getSlotId(subId);
        if (slotId < 0 || slotId >= mNumSlot) {
            throw new IllegalArgumentException("subscription id is not available");
        }

        try {
            mProvisioningCallbackManagersSlotMap.get(slotId).registerCallback(callback);
            log("Feature Provisioning Callback registered.");

            if (mFeatureFlags.notifyInitialImsProvisioningStatus()) {
                mHandler.sendMessage(mHandler.obtainMessage(EVENT_NOTIFY_INIT_PROVISIONED_VALUE,
                        getSlotId(subId), subId, (Object) callback));
            }
        } catch (NullPointerException e) {
            logw("can not access callback manager to add callback");
        }
    }

    /**
     * Remove IFeatureProvisioningCallback
     */
    @VisibleForTesting
    public void removeFeatureProvisioningChangedCallback(int subId,
            IFeatureProvisioningCallback callback) {
        if (callback == null) {
            throw new IllegalArgumentException("provisioning callback can't be null");
        }

        int slotId = getSlotId(subId);
        if (slotId < 0 || slotId >= mNumSlot) {
            throw new IllegalArgumentException("subscription id is not available");
        }

        try {
            mProvisioningCallbackManagersSlotMap.get(slotId).unregisterCallback(callback);
            log("Feature Provisioning Callback removed.");
        } catch (NullPointerException e) {
            logw("can not access callback manager to remove callback");
        }
    }

    /**
     * return the boolean whether MmTel capability is required provisioning or not
     */
    @VisibleForTesting
    public boolean isImsProvisioningRequiredForCapability(int subId, int capability, int tech) {
        // check subId
        int slotId = getSlotId(subId);
        if (slotId <= SubscriptionManager.INVALID_SIM_SLOT_INDEX || slotId >= mNumSlot) {
            loge("Fail to retrieve slotId from subId");
            throw new IllegalArgumentException("subscribe id is invalid");
        }

        // check valid capability
        if (!(MMTEL_CAPABILITY_MIN < capability && capability < MMTEL_CAPABILITY_MAX)) {
            throw new IllegalArgumentException("MmTel capability '" + capability + "' is invalid");
        }

        // check valid radio tech
        if (!(REGISTRATION_TECH_NONE < tech && tech < REGISTRATION_TECH_MAX)) {
            log("Ims not matched radio tech " + tech);
            throw new IllegalArgumentException("Registration technology '" + tech + "' is invalid");
        }

        // check new carrier config first KEY_MMTEL_REQUIRES_PROVISIONING_BUNDLE
        boolean retVal = isProvisioningRequired(subId, capability, tech, /*isMmTel*/true);

        // if that returns false, check deprecated carrier config
        // KEY_CARRIER_VOLTE_PROVISIONING_REQUIRED_BOOL, KEY_CARRIER_UT_PROVISIONING_REQUIRED_BOOL
        if (!retVal && (capability == CAPABILITY_TYPE_VOICE
                || capability == CAPABILITY_TYPE_VIDEO
                || capability == CAPABILITY_TYPE_UT)) {
            String key = CarrierConfigManager.KEY_CARRIER_VOLTE_PROVISIONING_REQUIRED_BOOL;
            if (capability == CAPABILITY_TYPE_UT) {
                key = CarrierConfigManager.KEY_CARRIER_UT_PROVISIONING_REQUIRED_BOOL;
            }

            PersistableBundle imsCarrierConfigs = mCarrierConfigManager.getConfigForSubId(subId);
            if (imsCarrierConfigs != null) {
                retVal = imsCarrierConfigs.getBoolean(key);
            } else {
                retVal = CarrierConfigManager.getDefaultConfig().getBoolean(key);
            }
        }

        log("isImsProvisioningRequiredForCapability capability " + capability
                + " tech " + tech + " return value " + retVal);

        return retVal;
    }

    /**
     * return the boolean whether RCS capability is required provisioning or not
     */
    @VisibleForTesting
    public boolean isRcsProvisioningRequiredForCapability(int subId, int capability, int tech) {
        // check slotId and Phone object
        int slotId = getSlotId(subId);
        if (slotId <= SubscriptionManager.INVALID_SIM_SLOT_INDEX || slotId >= mNumSlot) {
            loge("Fail to retrieve slotId from subId");
            throw new IllegalArgumentException("subscribe id is invalid");
        }

        // check valid capability
        if (!(RCS_CAPABILITY_MIN < capability && capability < RCS_CAPABILITY_MAX)) {
            throw new IllegalArgumentException("Rcs capability '" + capability + "' is invalid");
        }

        // check valid radio tech
        if (!(REGISTRATION_TECH_NONE < tech && tech < REGISTRATION_TECH_MAX)) {
            log("Rcs not matched radio tech " + tech);
            throw new IllegalArgumentException("Registration technology '" + tech + "' is invalid");
        }

        // check new carrier config first KEY_RCS_REQUIRES_PROVISIONING_BUNDLE
        boolean retVal = isProvisioningRequired(subId, capability, tech, /*isMmTel*/false);

        // if that returns false, check deprecated carrier config
        // KEY_CARRIER_RCS_PROVISIONING_REQUIRED_BOOL
        if (!retVal) {
            PersistableBundle imsCarrierConfigs = mCarrierConfigManager.getConfigForSubId(subId);
            if (imsCarrierConfigs != null) {
                retVal = imsCarrierConfigs.getBoolean(
                        CarrierConfigManager.KEY_CARRIER_RCS_PROVISIONING_REQUIRED_BOOL);
            } else {
                retVal = CarrierConfigManager.getDefaultConfig().getBoolean(
                        CarrierConfigManager.KEY_CARRIER_RCS_PROVISIONING_REQUIRED_BOOL);
            }
        }

        log("isRcsProvisioningRequiredForCapability capability " + capability
                + " tech " + tech + " return value " + retVal);

        return retVal;
    }

    /**
     * return the provisioning status for MmTel capability in specific radio tech
     */
    @VisibleForTesting
    public boolean getImsProvisioningStatusForCapability(int subId, int capability, int tech) {
        boolean mmTelProvisioned = isImsProvisioningRequiredForCapability(subId, capability, tech);
        if (!mmTelProvisioned) { // provisioning not required
            log("getImsProvisioningStatusForCapability : not required "
                    + " capability " + capability + " tech " + tech);
            return true;
        }

        // read value from ImsProvisioningLoader
        int result = mImsProvisioningLoader.getProvisioningStatus(subId, FEATURE_MMTEL,
                capability, tech);
        if (result == ImsProvisioningLoader.STATUS_NOT_SET) {
            // not set means initial value
            // read data from vendor ImsService and store that in ImsProvisioningLoader
            result = getValueFromImsService(subId, capability, tech);
            mmTelProvisioned = getBoolValue(result);
            if (result != ProvisioningManager.PROVISIONING_RESULT_UNKNOWN) {
                setAndNotifyMmTelProvisioningValue(subId, capability, tech, mmTelProvisioned);
            }
        } else {
            mmTelProvisioned = getBoolValue(result);
        }

        log("getImsProvisioningStatusForCapability : "
                + " capability " + capability
                + " tech " + tech
                + " result " + mmTelProvisioned);
        return mmTelProvisioned;
    }

    /**
     * set MmTel provisioning status in specific tech
     */
    @VisibleForTesting
    public void setImsProvisioningStatusForCapability(int subId, int capability, int tech,
            boolean isProvisioned) {
        boolean mmTelProvisioned = isImsProvisioningRequiredForCapability(subId, capability, tech);
        if (!mmTelProvisioned) { // provisioning not required
            log("setImsProvisioningStatusForCapability : not required "
                    + " capability " + capability + " tech " + tech);
            return;
        }

        // write value to ImsProvisioningLoader
        boolean isChanged = setAndNotifyMmTelProvisioningValue(subId, capability, tech,
                isProvisioned);
        if (!isChanged) {
            log("status not changed mmtel capability " + capability + " tech " + tech);
            return;
        }

        int slotId = getSlotId(subId);
        // find matched key from capability and tech
        int value = getIntValue(isProvisioned);
        int key = getKeyFromCapability(capability, tech);
        if (key != INVALID_VALUE) {
            log("setImsProvisioningStatusForCapability : matched key " + key);
            try {
                // set key and value to vendor ImsService for MmTel
                mMmTelFeatureListenersSlotMap.get(slotId).setProvisioningValue(key, value);
            } catch (NullPointerException e) {
                loge("can not access MmTelFeatureListener with capability " + capability);
            }
        }
    }

    /**
     * return the provisioning status for RCS capability in specific radio tech
     */
    @VisibleForTesting
    public boolean getRcsProvisioningStatusForCapability(int subId, int capability, int tech) {
        boolean rcsProvisioned = isRcsProvisioningRequiredForCapability(subId, capability, tech);
        if (!rcsProvisioned) { // provisioning not required
            log("getRcsProvisioningStatusForCapability : not required"
                    + " capability " + capability + " tech " + tech);
            return true;
        }

        // read data from ImsProvisioningLoader
        int result = mImsProvisioningLoader.getProvisioningStatus(subId, FEATURE_RCS,
                capability, tech);
        if (result == ImsProvisioningLoader.STATUS_NOT_SET) {
            // not set means initial value
            // read data from vendor ImsService and store that in ImsProvisioningLoader
            result = getRcsValueFromImsService(subId, capability);
            rcsProvisioned = getBoolValue(result);
            if (result != ProvisioningManager.PROVISIONING_RESULT_UNKNOWN) {
                setAndNotifyRcsProvisioningValueForAllTech(subId, capability, rcsProvisioned);
            }
        } else {
            rcsProvisioned = getBoolValue(result);
        }

        log("getRcsProvisioningStatusForCapability : "
                + " capability " + capability
                + " tech " + tech
                + " result " + rcsProvisioned);
        return rcsProvisioned;
    }

    /**
     * set RCS provisioning status in specific tech
     */
    @VisibleForTesting
    public void setRcsProvisioningStatusForCapability(int subId, int capability, int tech,
            boolean isProvisioned) {
        boolean rcsProvisioned = isRcsProvisioningRequiredForCapability(subId, capability, tech);
        if (!rcsProvisioned) { // provisioning not required
            log("set rcs provisioning status but not required");
            return;
        }

        // write status using ImsProvisioningLoader
        boolean isChanged = setAndNotifyRcsProvisioningValue(subId, capability, tech,
                isProvisioned);
        if (!isChanged) {
            log("status not changed rcs capability " + capability + " tech " + tech);
            return;
        }

        int slotId = getSlotId(subId);
        int key =  ProvisioningManager.KEY_EAB_PROVISIONING_STATUS;
        int value = getIntValue(isProvisioned);
        try {
            // On some older devices, EAB is managed on the MmTel ImsService when the RCS
            // ImsService is not configured. If there is no RCS ImsService defined, fallback to
            // MmTel. In the rare case that we hit a race condition where the RCS ImsService has
            // crashed or has not come up yet, the value will be synchronized via
            // setInitialProvisioningKeys().
            if (mRcsFeatureListenersSlotMap.get(slotId).isConnectionReady()) {
                mRcsFeatureListenersSlotMap.get(slotId).setProvisioningValue(key, value);
            }

            // EAB provisioning status should be updated to both the Rcs and MmTel ImsService,
            // because the provisioning callback is listening to only MmTel provisioning key
            // changes.
            mMmTelFeatureListenersSlotMap.get(slotId).setProvisioningValue(key, value);
        } catch (NullPointerException e) {
            loge("can not access RcsFeatureListener with capability " + capability);
        }
    }

    /**
     * set RCS provisioning status in specific key and value
     * @param key integer key, defined as one of
     * {@link ProvisioningManager#KEY_VOLTE_PROVISIONING_STATUS}
     * {@link ProvisioningManager#KEY_VT_PROVISIONING_STATUS}
     * {@link ProvisioningManager#KEY_VOICE_OVER_WIFI_ENABLED_OVERRIDE}
     * {@link ProvisioningManager#KEY_EAB_PROVISIONING_STATUS}
     * @param value in Integer format.
     * @return the result of setting the configuration value, defined as one of
     * {@link ImsConfigImplBase#CONFIG_RESULT_FAILED} or
     * {@link ImsConfigImplBase#CONFIG_RESULT_SUCCESS} or
     */
    @VisibleForTesting
    public int setProvisioningValue(int subId, int key, int value) {
        log("setProvisioningValue");

        int retVal = ImsConfigImplBase.CONFIG_RESULT_FAILED;
        // check key value
        if (!Arrays.stream(LOCAL_IMS_CONFIG_KEYS).anyMatch(keyValue -> keyValue == key)) {
            log("not matched key " + key);
            return ImsConfigImplBase.CONFIG_RESULT_UNKNOWN;
        }

        // check subId
        int slotId = getSlotId(subId);
        if (slotId <= SubscriptionManager.INVALID_SIM_SLOT_INDEX || slotId >= mNumSlot) {
            loge("Fail to retrieve slotId from subId");
            return ImsConfigImplBase.CONFIG_RESULT_FAILED;
        }

        try {
            // set key and value to vendor ImsService for MmTel
            // EAB provisioning status should be updated to both the Rcs and MmTel ImsService,
            // because the provisioning callback is listening to only MmTel provisioning key
            // changes.
            retVal = mMmTelFeatureListenersSlotMap.get(slotId).setProvisioningValue(key, value);

            // If the  Rcs ImsService is not available, the EAB provisioning status will be written
            // to the MmTel ImsService for backwards compatibility. In the rare case that this is
            // hit due to RCS ImsService temporarily unavailable, the value will be synchronized
            // via setInitialProvisioningKeys() when the RCS ImsService comes back up.
            if (key == KEY_EAB_PROVISIONING_STATUS
                    && mRcsFeatureListenersSlotMap.get(slotId).isConnectionReady()) {
                // set key and value to vendor ImsService for RCS and use retVal from RCS if
                // related to EAB when possible.
                retVal = mRcsFeatureListenersSlotMap.get(slotId).setProvisioningValue(key, value);
            }
        } catch (NullPointerException e) {
            loge("can not access FeatureListener to set provisioning value");
            return ImsConfigImplBase.CONFIG_RESULT_FAILED;
        }

        // update and notify provisioning status changed capability and tech from key
        updateCapabilityTechFromKey(subId, key, value);

        return retVal;
    }

    /**
     * get RCS provisioning status in specific key and value
     * @param key integer key, defined as one of
     * {@link ProvisioningManager#KEY_VOLTE_PROVISIONING_STATUS}
     * {@link ProvisioningManager#KEY_VT_PROVISIONING_STATUS}
     * {@link ProvisioningManager#KEY_VOICE_OVER_WIFI_ENABLED_OVERRIDE}
     * {@link ProvisioningManager#KEY_EAB_PROVISIONING_STATUS}
     * @return the result of setting the configuration value, defined as one of
     * {@link ImsConfigImplBase#CONFIG_RESULT_FAILED} or
     * {@link ImsConfigImplBase#CONFIG_RESULT_SUCCESS} or
     * {@link ImsConfigImplBase#CONFIG_RESULT_UNKNOWN}
     */
    @VisibleForTesting
    public int getProvisioningValue(int subId, int key) {
        // check key value
        if (!Arrays.stream(LOCAL_IMS_CONFIG_KEYS).anyMatch(keyValue -> keyValue == key)) {
            log("not matched key " + key);
            return ImsConfigImplBase.CONFIG_RESULT_UNKNOWN;
        }

        // check subId
        int slotId = getSlotId(subId);
        if (slotId <= SubscriptionManager.INVALID_SIM_SLOT_INDEX || slotId >= mNumSlot) {
            loge("Fail to retrieve slotId from subId");
            return ImsConfigImplBase.CONFIG_RESULT_UNKNOWN;
        }

        // check data from ImsProvisioningLoader
        int capability = getCapabilityFromKey(key);
        int tech = getTechFromKey(key);
        int result;
        if (capability != INVALID_VALUE && tech != INVALID_VALUE) {
            if (key == KEY_EAB_PROVISIONING_STATUS) {
                result = mImsProvisioningLoader.getProvisioningStatus(subId, FEATURE_RCS,
                        capability, tech);
            } else {
                result = mImsProvisioningLoader.getProvisioningStatus(subId, FEATURE_MMTEL,
                        capability, tech);
            }
            if (result != ImsProvisioningLoader.STATUS_NOT_SET) {
                log("getProvisioningValue from loader : key " + key + " result " + result);
                return result;
            }
        }

        // get data from ImsService, update it in ImsProvisioningLoader
        if (key == KEY_EAB_PROVISIONING_STATUS) {
            result = getRcsValueFromImsService(subId, capability);
            if (result == ImsConfigImplBase.CONFIG_RESULT_UNKNOWN) {
                logw("getProvisioningValue : fail to get data from ImsService capability"
                        + capability);
                return result;
            }
            log("getProvisioningValue from vendor : key " + key + " result " + result);

            setAndNotifyRcsProvisioningValueForAllTech(subId, capability, getBoolValue(result));
            return result;
        } else {
            result = getValueFromImsService(subId, capability, tech);
            if (result == ImsConfigImplBase.CONFIG_RESULT_UNKNOWN) {
                logw("getProvisioningValue : fail to get data from ImsService capability"
                        + capability);
                return result;
            }
            log("getProvisioningValue from vendor : key " + key + " result " + result);

            setAndNotifyMmTelProvisioningValue(subId, capability, tech, getBoolValue(result));
            return result;
        }
    }

    /**
     * get the handler
     */
    @VisibleForTesting
    public Handler getHandler() {
        return mHandler;
    }

    private boolean isProvisioningRequired(int subId, int capability, int tech, boolean isMmTel) {
        int[] techArray;
        techArray = getTechsFromCarrierConfig(subId, capability, isMmTel);
        if (techArray == null) {
            logw("isProvisioningRequired : getTechsFromCarrierConfig failed");
            // not exist in CarrierConfig that means provisioning is not required
            return false;
        }

        // compare with carrier config
        if (Arrays.stream(techArray).anyMatch(keyValue -> keyValue == tech)) {
            // existing same tech means provisioning required
            return true;
        }

        log("isProvisioningRequired : not matched capability " + capability + " tech " + tech);
        return false;
    }

    private int[] getTechsFromCarrierConfig(int subId, int capability, boolean isMmTel) {
        String featureKey;
        String capabilityKey;
        if (isMmTel) {
            featureKey = CarrierConfigManager.Ims.KEY_MMTEL_REQUIRES_PROVISIONING_BUNDLE;
            capabilityKey = KEYS_MMTEL_CAPABILITY.get(capability);
        } else {
            featureKey = CarrierConfigManager.Ims.KEY_RCS_REQUIRES_PROVISIONING_BUNDLE;
            capabilityKey = KEYS_RCS_CAPABILITY.get(capability);
        }

        if (capabilityKey != null) {
            PersistableBundle imsCarrierConfigs = mCarrierConfigManager.getConfigForSubId(subId);
            if (imsCarrierConfigs == null) {
                log("getTechsFromCarrierConfig : imsCarrierConfigs null");
                return null;
            }

            PersistableBundle provisioningBundle =
                    imsCarrierConfigs.getPersistableBundle(featureKey);
            if (provisioningBundle == null) {
                log("getTechsFromCarrierConfig : provisioningBundle null");
                return null;
            }

            return provisioningBundle.getIntArray(capabilityKey);
        }

        return null;
    }

    private int getValueFromImsService(int subId, int capability, int tech) {
        int config = ImsConfigImplBase.CONFIG_RESULT_UNKNOWN;

        // operation is based on capability
        switch (capability) {
            case CAPABILITY_TYPE_VOICE:
                int item = (tech == ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN)
                        ? ProvisioningManager.KEY_VOICE_OVER_WIFI_ENABLED_OVERRIDE
                        : ProvisioningManager.KEY_VOLTE_PROVISIONING_STATUS;
                // read data from vendor ImsService
                config = mMmTelFeatureListenersSlotMap.get(getSlotId(subId))
                        .getProvisioningValue(item);
                break;
            case CAPABILITY_TYPE_VIDEO:
                // read data from vendor ImsService
                config = mMmTelFeatureListenersSlotMap.get(getSlotId(subId))
                        .getProvisioningValue(ProvisioningManager.KEY_VT_PROVISIONING_STATUS);
                break;
            default:
                log("Capability " + capability + " has been provisioning");
                break;
        }

        return config;
    }

    private int getRcsValueFromImsService(int subId, int capability) {
        int config = ImsConfigImplBase.CONFIG_RESULT_UNKNOWN;
        int slotId = getSlotId(subId);

        if (capability != CAPABILITY_TYPE_PRESENCE_UCE) {
            log("Capability " + capability + " has been provisioning");
            return config;
        }
        try {
            if (mRcsFeatureListenersSlotMap.get(slotId).isConnectionReady()) {
                config = mRcsFeatureListenersSlotMap.get(slotId)
                        .getProvisioningValue(ProvisioningManager.KEY_EAB_PROVISIONING_STATUS);
            } else {
                log("Rcs ImsService is not available, "
                        + "EAB provisioning status should be read from MmTel ImsService");
                config = mMmTelFeatureListenersSlotMap.get(slotId)
                        .getProvisioningValue(ProvisioningManager.KEY_EAB_PROVISIONING_STATUS);
            }
        } catch (NullPointerException e) {
            logw("can not access FeatureListener : " + e.getMessage());
        }

        return config;
    }

    private void onSubscriptionsChanged() {
        for (int index = 0; index < mMmTelFeatureListenersSlotMap.size(); index++) {
            MmTelFeatureListener m = mMmTelFeatureListenersSlotMap.get(index);
            m.setSubId(getSubId(index));
        }
        for (int index = 0; index < mRcsFeatureListenersSlotMap.size(); index++) {
            RcsFeatureListener r = mRcsFeatureListenersSlotMap.get(index);
            r.setSubId(getSubId(index));
        }
        for (int index = 0; index < mProvisioningCallbackManagersSlotMap.size(); index++) {
            ProvisioningCallbackManager m = mProvisioningCallbackManagersSlotMap.get(index);
            m.setSubId(getSubId(index));
        }
    }

    private void  updateCapabilityTechFromKey(int subId, int key, int value) {
        boolean isProvisioned = getBoolValue(value);
        int capability = getCapabilityFromKey(key);
        int tech = getTechFromKey(key);

        if (capability == INVALID_VALUE || tech == INVALID_VALUE) {
            logw("updateCapabilityTechFromKey : unknown key " + key);
            return;
        }

        if (key == KEY_VOLTE_PROVISIONING_STATUS
                || key == KEY_VT_PROVISIONING_STATUS
                || key == KEY_VOICE_OVER_WIFI_ENABLED_OVERRIDE) {
            setAndNotifyMmTelProvisioningValue(subId, capability, tech, isProvisioned);
        }
        if (key == KEY_EAB_PROVISIONING_STATUS) {
            setAndNotifyRcsProvisioningValueForAllTech(subId, capability, isProvisioned);
        }
    }

    private int getCapabilityFromKey(int key) {
        int capability;
        switch (key) {
            case ProvisioningManager.KEY_VOLTE_PROVISIONING_STATUS:
                // intentional fallthrough
            case ProvisioningManager.KEY_VOICE_OVER_WIFI_ENABLED_OVERRIDE:
                capability = CAPABILITY_TYPE_VOICE;
                break;
            case ProvisioningManager.KEY_VT_PROVISIONING_STATUS:
                capability = CAPABILITY_TYPE_VIDEO;
                break;
            case ProvisioningManager.KEY_EAB_PROVISIONING_STATUS:
                // default CAPABILITY_TYPE_PRESENCE_UCE used for KEY_EAB_PROVISIONING_STATUS
                capability = CAPABILITY_TYPE_PRESENCE_UCE;
                break;
            default:
                capability = INVALID_VALUE;
                break;
        }
        return capability;
    }

    private int getTechFromKey(int key) {
        int tech;
        switch (key) {
            case ProvisioningManager.KEY_VOICE_OVER_WIFI_ENABLED_OVERRIDE:
                tech = ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN;
                break;
            case ProvisioningManager.KEY_VOLTE_PROVISIONING_STATUS:
                // intentional fallthrough
            case ProvisioningManager.KEY_VT_PROVISIONING_STATUS:
                // intentional fallthrough
            case ProvisioningManager.KEY_EAB_PROVISIONING_STATUS:
                tech = ImsRegistrationImplBase.REGISTRATION_TECH_LTE;
                break;
            default:
                tech = INVALID_VALUE;
                break;
        }
        return tech;
    }

    private int getKeyFromCapability(int capability, int tech) {
        int key = INVALID_VALUE;
        if (capability == CAPABILITY_TYPE_VOICE && tech == REGISTRATION_TECH_IWLAN) {
            key = ProvisioningManager.KEY_VOICE_OVER_WIFI_ENABLED_OVERRIDE;
        } else if (capability == CAPABILITY_TYPE_VOICE && tech == REGISTRATION_TECH_LTE) {
            key = ProvisioningManager.KEY_VOLTE_PROVISIONING_STATUS;
        } else if (capability == CAPABILITY_TYPE_VIDEO && tech == REGISTRATION_TECH_LTE) {
            key = ProvisioningManager.KEY_VT_PROVISIONING_STATUS;
        }

        return key;
    }

    protected int getSubId(int slotId) {
        return SubscriptionManager.getSubscriptionId(slotId);
    }

    protected int getSlotId(int subId) {
        return mSubscriptionManager.getPhoneId(subId);
    }

    protected ImsConfig getImsConfig(ImsManager imsManager) throws ImsException {
        return imsManager.getConfigInterface();
    }

    protected ImsConfig getImsConfig(IImsConfig iImsConfig) {
        return new ImsConfig(iImsConfig);
    }

    private int getIntValue(boolean isProvisioned) {
        return isProvisioned ? ProvisioningManager.PROVISIONING_VALUE_ENABLED
                : ProvisioningManager.PROVISIONING_VALUE_DISABLED;
    }

    private boolean getBoolValue(int value) {
        return value == ProvisioningManager.PROVISIONING_VALUE_ENABLED ? true : false;
    }

    private boolean setAndNotifyMmTelProvisioningValue(int subId, int capability, int tech,
            boolean isProvisioned) {
        boolean changed = mImsProvisioningLoader.setProvisioningStatus(subId, FEATURE_MMTEL,
                capability, tech, isProvisioned);
        // notify MmTel capability changed
        if (changed) {
            mHandler.sendMessage(mHandler.obtainMessage(EVENT_PROVISIONING_CAPABILITY_CHANGED,
                    getSlotId(subId), 0, (Object) new FeatureProvisioningData(
                            capability, tech, isProvisioned, /*isMmTel*/true)));
        }

        return changed;
    }

    private boolean setAndNotifyRcsProvisioningValue(int subId, int capability, int tech,
            boolean isProvisioned) {
        boolean isChanged = mImsProvisioningLoader.setProvisioningStatus(subId, FEATURE_RCS,
                capability, tech, isProvisioned);

        if (isChanged) {
            int slotId = getSlotId(subId);

            // notify RCS capability changed
            mHandler.sendMessage(mHandler.obtainMessage(EVENT_PROVISIONING_CAPABILITY_CHANGED,
                    slotId, 0, (Object) new FeatureProvisioningData(
                            capability, tech, isProvisioned, /*isMmtel*/false)));
        }

        return isChanged;
    }

    private boolean setAndNotifyRcsProvisioningValueForAllTech(int subId, int capability,
            boolean isProvisioned) {
        boolean isChanged = false;

        for (int tech : LOCAL_RADIO_TECHS) {
            isChanged |= setAndNotifyRcsProvisioningValue(subId, capability, tech, isProvisioned);
        }

        return isChanged;
    }

    protected boolean isValidSubId(int subId) {
        int slotId = getSlotId(subId);
        if (slotId <= SubscriptionManager.INVALID_SIM_SLOT_INDEX || slotId >= mNumSlot) {
            return false;
        }

        return true;
    }

    private void notifyMmTelProvisioningStatus(int slotId, int subId,
            @Nullable IFeatureProvisioningCallback callback) {
        int value = ImsProvisioningLoader.STATUS_NOT_SET;
        int[] techArray;
        for (int capability : LOCAL_MMTEL_CAPABILITY) {
            techArray = getTechsFromCarrierConfig(subId, capability, /*isMmTle*/true);
            if (techArray == null) {
                continue;
            }

            for (int radioTech : techArray) {
                value = mImsProvisioningLoader.getProvisioningStatus(subId, FEATURE_MMTEL,
                        capability, radioTech);
                if (value == ImsProvisioningLoader.STATUS_NOT_SET) {
                    // Not yet provisioned
                    continue;
                }

                value = (value == ImsProvisioningLoader.STATUS_PROVISIONED)
                        ? PROVISIONING_VALUE_ENABLED : PROVISIONING_VALUE_DISABLED;

                // Notify all registered callbacks
                if (callback == null) {
                    mProvisioningCallbackManagersSlotMap.get(slotId)
                            .notifyProvisioningCapabilityChanged(
                                    new FeatureProvisioningData(
                                            capability,
                                            radioTech,
                                            getBoolValue(value),
                                            /*isMmTle*/true));
                } else {
                    try {
                        callback.onFeatureProvisioningChanged(capability, radioTech,
                                getBoolValue(value));
                    } catch (RemoteException e) {
                        logw("notifyMmTelProvisioningStatus callback is not available");
                    }
                }
            }
        }
    }

    private void notifyRcsProvisioningStatus(int slotId, int subId,
            @Nullable IFeatureProvisioningCallback callback) {
        int value = ImsProvisioningLoader.STATUS_NOT_SET;
        int[] techArray;
        for (int capability : LOCAL_RCS_CAPABILITY) {
            techArray = getTechsFromCarrierConfig(subId, capability, /*isMmTle*/false);
            if (techArray == null) {
                continue;
            }

            for (int radioTech : techArray) {
                value = mImsProvisioningLoader.getProvisioningStatus(subId, FEATURE_RCS,
                        capability, radioTech);
                if (value == ImsProvisioningLoader.STATUS_NOT_SET) {
                    // Not yet provisioned
                    continue;
                }

                value = (value == ImsProvisioningLoader.STATUS_PROVISIONED)
                        ? PROVISIONING_VALUE_ENABLED : PROVISIONING_VALUE_DISABLED;

                // Notify all registered callbacks
                if (callback == null) {
                    mProvisioningCallbackManagersSlotMap.get(slotId)
                            .notifyProvisioningCapabilityChanged(
                                    new FeatureProvisioningData(
                                            capability,
                                            radioTech,
                                            getBoolValue(value),
                                            /*isMmTle*/false));
                } else {
                    try {
                        callback.onRcsFeatureProvisioningChanged(capability, radioTech,
                                getBoolValue(value));
                    } catch (RemoteException e) {
                        logw("notifyRcsProvisioningStatus callback is not available");
                    }
                }
            }
        }
    }

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

    private void log(String prefix, int slotId, String s) {
        Rlog.d(TAG, prefix + "[" + slotId + "] " + s);
    }

    private void logi(String prefix, int slotId, String s) {
        Rlog.i(TAG, prefix + "[" + slotId + "] " + s);
    }

    private void logw(String s) {
        Rlog.w(TAG, s);
    }

    private void logw(String prefix, int slotId, String s) {
        Rlog.w(TAG, prefix + "[" + slotId + "] " + s);
    }

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

    private void loge(String prefix, int slotId, String s) {
        Rlog.e(TAG, prefix + "[" + slotId + "] " + s);
    }
}
