/*
 * Copyright (C) 2020 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.ims.rcs.uce.request;

import android.content.Context;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.telephony.TelephonyManager;
import android.telephony.ims.RcsContactUceCapability;
import android.telephony.ims.RcsContactUceCapability.CapabilityMechanism;
import android.telephony.ims.RcsUceAdapter;
import android.telephony.ims.aidl.IOptionsRequestCallback;
import android.telephony.ims.aidl.IRcsUceControllerCallback;
import android.text.TextUtils;
import android.util.Log;

import com.android.i18n.phonenumbers.NumberParseException;
import com.android.i18n.phonenumbers.PhoneNumberUtil;
import com.android.i18n.phonenumbers.Phonenumber;
import com.android.ims.rcs.uce.UceController;
import com.android.ims.rcs.uce.UceController.UceControllerCallback;
import com.android.ims.rcs.uce.UceDeviceState;
import com.android.ims.rcs.uce.UceDeviceState.DeviceStateResult;
import com.android.ims.rcs.uce.eab.EabCapabilityResult;
import com.android.ims.rcs.uce.options.OptionsController;
import com.android.ims.rcs.uce.presence.subscribe.SubscribeController;
import com.android.ims.rcs.uce.request.UceRequest.UceRequestType;
import com.android.ims.rcs.uce.request.UceRequestCoordinator.UceRequestUpdate;
import com.android.ims.rcs.uce.util.UceUtils;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.SomeArgs;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * Managers the capabilities requests and the availability requests from UceController.
 */
public class UceRequestManager {

    private static final String LOG_TAG = UceUtils.getLogPrefix() + "UceRequestManager";

    /**
     * When enabled, skip the request queue for requests that have numbers with valid cached
     * capabilities and return that cached info directly.
     * Note: This also has a CTS test associated with it, so this can not be disabled without
     * disabling the corresponding RcsUceAdapterTest#testCacheQuerySuccessWhenNetworkBlocked test.
     */
    private static final boolean FEATURE_SHORTCUT_QUEUE_FOR_CACHED_CAPS = true;

    /**
     * Testing interface used to mock UceUtils in testing.
     */
    @VisibleForTesting
    public interface UceUtilsProxy {
        /**
         * The interface for {@link UceUtils#isPresenceCapExchangeEnabled(Context, int)} used for
         * testing.
         */
        boolean isPresenceCapExchangeEnabled(Context context, int subId);

        /**
         * The interface for {@link UceUtils#isPresenceSupported(Context, int)} used for testing.
         */
        boolean isPresenceSupported(Context context, int subId);

        /**
         * The interface for {@link UceUtils#isSipOptionsSupported(Context, int)} used for testing.
         */
        boolean isSipOptionsSupported(Context context, int subId);

        /**
         * @return true when the Presence group subscribe is enabled.
         */
        boolean isPresenceGroupSubscribeEnabled(Context context, int subId);

        /**
         * Retrieve the maximum number of contacts that can be included in a request.
         */
        int getRclMaxNumberEntries(int subId);

        /**
         * @return true if the given phone number is blocked by the network.
         */
        boolean isNumberBlocked(Context context, String phoneNumber);
    }

    private static UceUtilsProxy sUceUtilsProxy = new UceUtilsProxy() {
        @Override
        public boolean isPresenceCapExchangeEnabled(Context context, int subId) {
            return UceUtils.isPresenceCapExchangeEnabled(context, subId);
        }

        @Override
        public boolean isPresenceSupported(Context context, int subId) {
            return UceUtils.isPresenceSupported(context, subId);
        }

        @Override
        public boolean isSipOptionsSupported(Context context, int subId) {
            return UceUtils.isSipOptionsSupported(context, subId);
        }

        @Override
        public boolean isPresenceGroupSubscribeEnabled(Context context, int subId) {
            return UceUtils.isPresenceGroupSubscribeEnabled(context, subId);
        }

        @Override
        public int getRclMaxNumberEntries(int subId) {
            return UceUtils.getRclMaxNumberEntries(subId);
        }

        @Override
        public boolean isNumberBlocked(Context context, String phoneNumber) {
            return UceUtils.isNumberBlocked(context, phoneNumber);
        }
    };

    @VisibleForTesting
    public void setsUceUtilsProxy(UceUtilsProxy uceUtilsProxy) {
        sUceUtilsProxy = uceUtilsProxy;
    }

    /**
     * The callback interface to receive the request and the result from the UceRequest.
     */
    public interface RequestManagerCallback {
        /**
         * Notify sending the UceRequest
         */
        void notifySendingRequest(long coordinator, long taskId, long delayTimeMs);

        /**
         * Retrieve the contact capabilities from the cache.
         */
        List<EabCapabilityResult> getCapabilitiesFromCache(List<Uri> uriList);

        /**
         * Retrieve the contact capabilities from the cache including the expired capabilities.
         */
        List<EabCapabilityResult> getCapabilitiesFromCacheIncludingExpired(List<Uri> uriList);

        /**
         * Retrieve the contact availability from the cache.
         */
        EabCapabilityResult getAvailabilityFromCache(Uri uri);

        /**
         * Retrieve the contact availability from the cache including the expired capabilities.
         */
        EabCapabilityResult getAvailabilityFromCacheIncludingExpired(Uri uri);

        /**
         * Store the given contact capabilities to the cache.
         */
        void saveCapabilities(List<RcsContactUceCapability> contactCapabilities);

        /**
         * Retrieve the device's capabilities.
         */
        RcsContactUceCapability getDeviceCapabilities(@CapabilityMechanism int capMechanism);

        /**
         * Get the device state to check whether the device is disallowed by the network or not.
         */
        DeviceStateResult getDeviceState();

        /**
         * Refresh the device state. It is called when receive the UCE request response.
         */
        void refreshDeviceState(int sipCode, String reason);

        /**
         * Notify that the UceRequest associated with the given taskId encounters error.
         */
        void notifyRequestError(long requestCoordinatorId, long taskId);

        /**
         * Notify that the UceRequest received the onCommandError callback from the ImsService.
         */
        void notifyCommandError(long requestCoordinatorId, long taskId);

        /**
         * Notify that the UceRequest received the onNetworkResponse callback from the ImsService.
         */
        void notifyNetworkResponse(long requestCoordinatorId, long taskId);

        /**
         * Notify that the UceRequest received the onTerminated callback from the ImsService.
         */
        void notifyTerminated(long requestCoordinatorId, long taskId);

        /**
         * Notify that some contacts are not RCS anymore. It will updated the cached capabilities
         * and trigger the callback IRcsUceControllerCallback#onCapabilitiesReceived
         */
        void notifyResourceTerminated(long requestCoordinatorId, long taskId);

        /**
         * Notify that the capabilities updates. It will update the cached and trigger the callback
         * IRcsUceControllerCallback#onCapabilitiesReceived
         */
        void notifyCapabilitiesUpdated(long requestCoordinatorId, long taskId);

        /**
         * Notify that some of the request capabilities can be retrieved from the cached.
         */
        void notifyCachedCapabilitiesUpdated(long requestCoordinatorId, long taskId);

        /**
         * Notify that all the requested capabilities can be retrieved from the cache. It does not
         * need to request capabilities from the network.
         */
        void notifyNoNeedRequestFromNetwork(long requestCoordinatorId, long taskId);

        /**
         * Notify that the remote options request is done. This is sent by RemoteOptionsRequest and
         * it will notify the RemoteOptionsCoordinator to handle it.
         */
        void notifyRemoteRequestDone(long requestCoordinatorId, long taskId);

        /**
         * Set the timer for the request timeout. It will cancel the request when the time is up.
         */
        void setRequestTimeoutTimer(long requestCoordinatorId, long taskId, long timeoutAfterMs);

        /**
         * Remove the timeout timer of the capabilities request.
         */
        void removeRequestTimeoutTimer(long taskId);

        /**
         * Notify that the UceRequest has finished. This is sent by UceRequestCoordinator.
         */
        void notifyUceRequestFinished(long requestCoordinatorId, long taskId);

        /**
         * Notify that the RequestCoordinator has finished. This is sent by UceRequestCoordinator
         * to remove the coordinator from the UceRequestRepository.
         */
        void notifyRequestCoordinatorFinished(long requestCoordinatorId);

        /**
         * Check whether the given uris are in the throttling list.
         * @param uriList the uris to check if it is in the throttling list
         * @return the uris in the throttling list
         */
        List<Uri> getInThrottlingListUris(List<Uri> uriList);

        /**
         * Add the given uris to the throttling list because the capabilities request result
         * is inconclusive.
         */
        void addToThrottlingList(List<Uri> uriList, int sipCode);
    }

    private RequestManagerCallback mRequestMgrCallback = new RequestManagerCallback() {
        @Override
        public void notifySendingRequest(long coordinatorId, long taskId, long delayTimeMs) {
            mHandler.sendRequestMessage(coordinatorId, taskId, delayTimeMs);
        }

        @Override
        public List<EabCapabilityResult> getCapabilitiesFromCache(List<Uri> uriList) {
            return mControllerCallback.getCapabilitiesFromCache(uriList);
        }

        @Override
        public List<EabCapabilityResult> getCapabilitiesFromCacheIncludingExpired(List<Uri> uris) {
            return mControllerCallback.getCapabilitiesFromCacheIncludingExpired(uris);
        }

        @Override
        public EabCapabilityResult getAvailabilityFromCache(Uri uri) {
            return mControllerCallback.getAvailabilityFromCache(uri);
        }

        @Override
        public EabCapabilityResult getAvailabilityFromCacheIncludingExpired(Uri uri) {
            return mControllerCallback.getAvailabilityFromCacheIncludingExpired(uri);
        }

        @Override
        public void saveCapabilities(List<RcsContactUceCapability> contactCapabilities) {
            mControllerCallback.saveCapabilities(contactCapabilities);
        }

        @Override
        public RcsContactUceCapability getDeviceCapabilities(@CapabilityMechanism int mechanism) {
            return mControllerCallback.getDeviceCapabilities(mechanism);
        }

        @Override
        public DeviceStateResult getDeviceState() {
            return mControllerCallback.getDeviceState();
        }

        @Override
        public void refreshDeviceState(int sipCode, String reason) {
            mControllerCallback.refreshDeviceState(sipCode, reason,
                    UceController.REQUEST_TYPE_CAPABILITY);
        }

        @Override
        public void notifyRequestError(long requestCoordinatorId, long taskId) {
            mHandler.sendRequestUpdatedMessage(requestCoordinatorId, taskId,
                    UceRequestCoordinator.REQUEST_UPDATE_ERROR);
        }

        @Override
        public void notifyCommandError(long requestCoordinatorId, long taskId) {
            mHandler.sendRequestUpdatedMessage(requestCoordinatorId, taskId,
                    UceRequestCoordinator.REQUEST_UPDATE_COMMAND_ERROR);
        }

        @Override
        public void notifyNetworkResponse(long requestCoordinatorId, long taskId) {
            mHandler.sendRequestUpdatedMessage(requestCoordinatorId, taskId,
                    UceRequestCoordinator.REQUEST_UPDATE_NETWORK_RESPONSE);
        }
        @Override
        public void notifyTerminated(long requestCoordinatorId, long taskId) {
            mHandler.sendRequestUpdatedMessage(requestCoordinatorId, taskId,
                    UceRequestCoordinator.REQUEST_UPDATE_TERMINATED);
        }
        @Override
        public void notifyResourceTerminated(long requestCoordinatorId, long taskId) {
            mHandler.sendRequestUpdatedMessage(requestCoordinatorId, taskId,
                    UceRequestCoordinator.REQUEST_UPDATE_RESOURCE_TERMINATED);
        }
        @Override
        public void notifyCapabilitiesUpdated(long requestCoordinatorId, long taskId) {
            mHandler.sendRequestUpdatedMessage(requestCoordinatorId, taskId,
                    UceRequestCoordinator.REQUEST_UPDATE_CAPABILITY_UPDATE);
        }

        @Override
        public void notifyCachedCapabilitiesUpdated(long requestCoordinatorId, long taskId) {
            mHandler.sendRequestUpdatedMessage(requestCoordinatorId, taskId,
                    UceRequestCoordinator.REQUEST_UPDATE_CACHED_CAPABILITY_UPDATE);
        }

        @Override
        public void notifyNoNeedRequestFromNetwork(long requestCoordinatorId, long taskId) {
            mHandler.sendRequestUpdatedMessage(requestCoordinatorId, taskId,
                    UceRequestCoordinator.REQUEST_UPDATE_NO_NEED_REQUEST_FROM_NETWORK);
        }

        @Override
        public void notifyRemoteRequestDone(long requestCoordinatorId, long taskId) {
            mHandler.sendRequestUpdatedMessage(requestCoordinatorId, taskId,
                    UceRequestCoordinator.REQUEST_UPDATE_REMOTE_REQUEST_DONE);
        }

        @Override
        public void setRequestTimeoutTimer(long coordinatorId, long taskId, long timeoutAfterMs) {
            mHandler.sendRequestTimeoutTimerMessage(coordinatorId, taskId, timeoutAfterMs);
        }

        @Override
        public void removeRequestTimeoutTimer(long taskId) {
            mHandler.removeRequestTimeoutTimer(taskId);
        }

        @Override
        public void notifyUceRequestFinished(long requestCoordinatorId, long taskId) {
            mHandler.sendRequestFinishedMessage(requestCoordinatorId, taskId);
        }

        @Override
        public void notifyRequestCoordinatorFinished(long requestCoordinatorId) {
            mHandler.sendRequestCoordinatorFinishedMessage(requestCoordinatorId);
        }

        @Override
        public List<Uri> getInThrottlingListUris(List<Uri> uriList) {
            return mThrottlingList.getInThrottlingListUris(uriList);
        }

        @Override
        public void addToThrottlingList(List<Uri> uriList, int sipCode) {
            mThrottlingList.addToThrottlingList(uriList, sipCode);
        }
    };

    private final int mSubId;
    private final Context mContext;
    private final UceRequestHandler mHandler;
    private final UceRequestRepository mRequestRepository;
    private final ContactThrottlingList mThrottlingList;
    private volatile boolean mIsDestroyed;

    private OptionsController mOptionsCtrl;
    private SubscribeController mSubscribeCtrl;
    private UceControllerCallback mControllerCallback;

    public UceRequestManager(Context context, int subId, Looper looper, UceControllerCallback c) {
        mSubId = subId;
        mContext = context;
        mControllerCallback = c;
        mHandler = new UceRequestHandler(this, looper);
        mThrottlingList = new ContactThrottlingList(mSubId);
        mRequestRepository = new UceRequestRepository(subId, mRequestMgrCallback);
        logi("create");
    }

    @VisibleForTesting
    public UceRequestManager(Context context, int subId, Looper looper, UceControllerCallback c,
            UceRequestRepository requestRepository) {
        mSubId = subId;
        mContext = context;
        mControllerCallback = c;
        mHandler = new UceRequestHandler(this, looper);
        mRequestRepository = requestRepository;
        mThrottlingList = new ContactThrottlingList(mSubId);
    }

    /**
     * Set the OptionsController for requestiong capabilities by OPTIONS mechanism.
     */
    public void setOptionsController(OptionsController controller) {
        mOptionsCtrl = controller;
    }

    /**
     * Set the SubscribeController for requesting capabilities by Subscribe mechanism.
     */
    public void setSubscribeController(SubscribeController controller) {
        mSubscribeCtrl = controller;
    }

    /**
     * Notify that the request manager instance is destroyed.
     */
    public void onDestroy() {
        logi("onDestroy");
        mIsDestroyed = true;
        mHandler.onDestroy();
        mThrottlingList.reset();
        mRequestRepository.onDestroy();
    }

    /**
     * Clear the throttling list.
     */
    public void resetThrottlingList() {
        mThrottlingList.reset();
    }

    /**
     * Send a new capability request. It is called by UceController.
     */
    public void sendCapabilityRequest(List<Uri> uriList, boolean skipFromCache,
            IRcsUceControllerCallback callback) throws RemoteException {
        if (mIsDestroyed) {
            callback.onError(RcsUceAdapter.ERROR_GENERIC_FAILURE, 0L, null);
            return;
        }
        sendRequestInternal(UceRequest.REQUEST_TYPE_CAPABILITY, uriList, skipFromCache, callback);
    }

    /**
     * Send a new availability request. It is called by UceController.
     */
    public void sendAvailabilityRequest(Uri uri, IRcsUceControllerCallback callback)
            throws RemoteException {
        if (mIsDestroyed) {
            callback.onError(RcsUceAdapter.ERROR_GENERIC_FAILURE, 0L, null);
            return;
        }
        sendRequestInternal(UceRequest.REQUEST_TYPE_AVAILABILITY,
                Collections.singletonList(uri), false /* skipFromCache */, callback);
    }

    private void sendRequestInternal(@UceRequestType int type, List<Uri> uriList,
            boolean skipFromCache, IRcsUceControllerCallback callback) throws RemoteException {
        UceRequestCoordinator requestCoordinator = null;
        List<Uri> nonCachedUris = uriList;
        if (FEATURE_SHORTCUT_QUEUE_FOR_CACHED_CAPS && !skipFromCache) {
            nonCachedUris = sendCachedCapInfoToRequester(type, uriList, callback);
            if (uriList.size() != nonCachedUris.size()) {
                logd("sendRequestInternal: shortcut queue for caps - request reduced from "
                        + uriList.size() + " entries to " + nonCachedUris.size() + " entries");
            } else {
                logd("sendRequestInternal: shortcut queue for caps - no cached caps.");
            }
            if (nonCachedUris.isEmpty()) {
                logd("sendRequestInternal: shortcut complete, sending success result");
                callback.onComplete(null);
                return;
            }
        }
        if (sUceUtilsProxy.isPresenceCapExchangeEnabled(mContext, mSubId) &&
                sUceUtilsProxy.isPresenceSupported(mContext, mSubId)) {
            requestCoordinator = createSubscribeRequestCoordinator(type, nonCachedUris,
                    skipFromCache, callback);
        } else if (sUceUtilsProxy.isSipOptionsSupported(mContext, mSubId)) {
            requestCoordinator = createOptionsRequestCoordinator(type, nonCachedUris, callback);
        }

        if (requestCoordinator == null) {
            logw("sendRequestInternal: Neither Presence nor OPTIONS are supported");
            callback.onError(RcsUceAdapter.ERROR_NOT_ENABLED, 0L, null);
            return;
        }

        StringBuilder builder = new StringBuilder("sendRequestInternal: ");
        builder.append("requestType=").append(type)
                .append(", requestCoordinatorId=").append(requestCoordinator.getCoordinatorId())
                .append(", taskId={")
                .append(requestCoordinator.getActivatedRequestTaskIds().stream()
                        .map(Object::toString).collect(Collectors.joining(","))).append("}");
        logd(builder.toString());

        // Add this RequestCoordinator to the UceRequestRepository.
        addRequestCoordinator(requestCoordinator);
    }

    /**
     * Try to get the valid capabilities associated with the URI List specified from the EAB cache.
     * If one or more of the numbers from the URI List have valid cached capabilities, return them
     * to the requester now and remove them from the returned List of URIs that will require a
     * network query.
     * @param type The type of query
     * @param uriList The List of URIs that we want to send cached capabilities for
     * @param callback The callback used to communicate with the remote requester
     * @return The List of URIs that were not found in the capability cache and will require a
     * network query.
     */
    private List<Uri> sendCachedCapInfoToRequester(int type, List<Uri> uriList,
            IRcsUceControllerCallback callback) {
        List<Uri> nonCachedUris = new ArrayList<>(uriList);
        List<RcsContactUceCapability> numbersWithCachedCaps =
                getCapabilitiesFromCache(type, nonCachedUris);
        try {
            if (!numbersWithCachedCaps.isEmpty()) {
                logd("sendCachedCapInfoToRequester: cached caps found for "
                        + numbersWithCachedCaps.size() + " entries. Notifying requester.");
                // Notify caller of the numbers that have cached caps
                callback.onCapabilitiesReceived(numbersWithCachedCaps);
            }
        } catch (RemoteException e) {
            logw("sendCachedCapInfoToRequester, error sending cap info back to requester: " + e);
        }
        // remove these numbers from the numbers pending a cap query from the network.
        for (RcsContactUceCapability c : numbersWithCachedCaps) {
            nonCachedUris.removeIf(uri -> c.getContactUri().equals(uri));
        }
        return nonCachedUris;
    }

    /**
     * Get the capabilities for the List of given URIs
     * @param requestType The request type, used to determine if the cached info is "fresh" enough.
     * @param uriList The List of URIs that we will be requesting cached capabilities for.
     * @return A list of capabilities corresponding to the subset of numbers that still have
     * valid cache data associated with them.
     */
    private List<RcsContactUceCapability> getCapabilitiesFromCache(int requestType,
            List<Uri> uriList) {
        List<EabCapabilityResult> resultList = Collections.emptyList();
        if (requestType == UceRequest.REQUEST_TYPE_CAPABILITY) {
            resultList = mRequestMgrCallback.getCapabilitiesFromCache(uriList);
        } else if (requestType == UceRequest.REQUEST_TYPE_AVAILABILITY) {
            // Always get the first element if the request type is availability.
            resultList = Collections.singletonList(
                    mRequestMgrCallback.getAvailabilityFromCache(uriList.get(0)));
        }
        // Map from EabCapabilityResult -> RcsContactUceCapability.
        // Pull out only items that have valid cache data.
        return resultList.stream().filter(Objects::nonNull)
                .filter(result -> result.getStatus() == EabCapabilityResult.EAB_QUERY_SUCCESSFUL)
                .map(EabCapabilityResult::getContactCapabilities)
                .filter(Objects::nonNull).collect(Collectors.toList());
    }

    private UceRequestCoordinator createSubscribeRequestCoordinator(final @UceRequestType int type,
            final List<Uri> uriList, boolean skipFromCache, IRcsUceControllerCallback callback) {
        SubscribeRequestCoordinator.Builder builder;

        if (!sUceUtilsProxy.isPresenceGroupSubscribeEnabled(mContext, mSubId)) {
            // When the group subscribe is disabled, each contact is required to be encapsulated
            // into individual UceRequest.
            List<UceRequest> requestList = new ArrayList<>();
            uriList.forEach(uri -> {
                List<Uri> individualUri = Collections.singletonList(uri);
                // Entity-uri, which is used as a request-uri, uses only a single subscription case
                List<RcsContactUceCapability> capabilities =
                        getCapabilitiesFromCache(type, individualUri);
                if (!capabilities.isEmpty()) {
                    RcsContactUceCapability capability = capabilities.get(0);
                    Uri entityUri = capability.getEntityUri();
                    if (entityUri != null) {
                        // The query uri has been replaced by the stored entity uri.
                        individualUri = Collections.singletonList(entityUri);
                    } else {
                        if (UceUtils.isSipUriForPresenceSubscribeEnabled(mContext, mSubId)) {
                            individualUri = Collections.singletonList(getSipUriFromUri(uri));
                        }
                    }
                } else {
                    if (UceUtils.isSipUriForPresenceSubscribeEnabled(mContext, mSubId)) {
                        individualUri = Collections.singletonList(getSipUriFromUri(uri));
                    }
                }
                UceRequest request = createSubscribeRequest(type, individualUri, skipFromCache);
                requestList.add(request);
            });
            builder = new SubscribeRequestCoordinator.Builder(mSubId, requestList,
                    mRequestMgrCallback);
            builder.setCapabilitiesCallback(callback);
        } else {
            // Even when the group subscribe is supported by the network, the number of contacts in
            // a UceRequest still cannot exceed the maximum.
            List<UceRequest> requestList = new ArrayList<>();
            final int rclMaxNumber = sUceUtilsProxy.getRclMaxNumberEntries(mSubId);
            int numRequestCoordinators = uriList.size() / rclMaxNumber;
            for (int count = 0; count < numRequestCoordinators; count++) {
                List<Uri> subUriList = new ArrayList<>();
                for (int index = 0; index < rclMaxNumber; index++) {
                    subUriList.add(uriList.get(count * rclMaxNumber + index));
                }
                requestList.add(createSubscribeRequest(type, subUriList, skipFromCache));
            }

            List<Uri> subUriList = new ArrayList<>();
            for (int i = numRequestCoordinators * rclMaxNumber; i < uriList.size(); i++) {
                subUriList.add(uriList.get(i));
            }
            requestList.add(createSubscribeRequest(type, subUriList, skipFromCache));

            builder = new SubscribeRequestCoordinator.Builder(mSubId, requestList,
                    mRequestMgrCallback);
            builder.setCapabilitiesCallback(callback);
        }
        return builder.build();
    }

    private UceRequestCoordinator createOptionsRequestCoordinator(@UceRequestType int type,
            List<Uri> uriList, IRcsUceControllerCallback callback) {
        OptionsRequestCoordinator.Builder builder;
        List<UceRequest> requestList = new ArrayList<>();
        uriList.forEach(uri -> {
            List<Uri> individualUri = Collections.singletonList(uri);
            UceRequest request = createOptionsRequest(type, individualUri, false);
            requestList.add(request);
        });
        builder = new OptionsRequestCoordinator.Builder(mSubId, requestList, mRequestMgrCallback);
        builder.setCapabilitiesCallback(callback);
        return builder.build();
    }

    private CapabilityRequest createSubscribeRequest(int type, List<Uri> uriList,
            boolean skipFromCache) {
        CapabilityRequest request = new SubscribeRequest(mSubId, type, mRequestMgrCallback,
                mSubscribeCtrl);
        request.setContactUri(uriList);
        request.setSkipGettingFromCache(skipFromCache);
        return request;
    }

    private CapabilityRequest createOptionsRequest(int type, List<Uri> uriList,
            boolean skipFromCache) {
        CapabilityRequest request = new OptionsRequest(mSubId, type, mRequestMgrCallback,
                mOptionsCtrl);
        request.setContactUri(uriList);
        request.setSkipGettingFromCache(skipFromCache);
        return request;
    }

    /**
     * Retrieve the device's capabilities. This request is from the ImsService to send the
     * capabilities to the remote side.
     */
    public void retrieveCapabilitiesForRemote(Uri contactUri, List<String> remoteCapabilities,
            IOptionsRequestCallback requestCallback) {
        RemoteOptionsRequest request = new RemoteOptionsRequest(mSubId, mRequestMgrCallback);
        request.setContactUri(Collections.singletonList(contactUri));
        request.setRemoteFeatureTags(remoteCapabilities);

        // If the remote number is blocked, do not send capabilities back.
        String number = getNumberFromUri(contactUri);
        if (!TextUtils.isEmpty(number)) {
            request.setIsRemoteNumberBlocked(sUceUtilsProxy.isNumberBlocked(mContext, number));
        }

        // Create the RemoteOptionsCoordinator instance
        RemoteOptionsCoordinator.Builder CoordBuilder = new RemoteOptionsCoordinator.Builder(
                mSubId, Collections.singletonList(request), mRequestMgrCallback);
        CoordBuilder.setOptionsRequestCallback(requestCallback);
        RemoteOptionsCoordinator requestCoordinator = CoordBuilder.build();

        StringBuilder builder = new StringBuilder("retrieveCapabilitiesForRemote: ");
        builder.append("requestCoordinatorId ").append(requestCoordinator.getCoordinatorId())
                .append(", taskId={")
                .append(requestCoordinator.getActivatedRequestTaskIds().stream()
                        .map(Object::toString).collect(Collectors.joining(","))).append("}");
        logd(builder.toString());

        // Add this RequestCoordinator to the UceRequestRepository.
        addRequestCoordinator(requestCoordinator);
    }

    private static class UceRequestHandler extends Handler {
        private static final int EVENT_EXECUTE_REQUEST = 1;
        private static final int EVENT_REQUEST_UPDATED = 2;
        private static final int EVENT_REQUEST_TIMEOUT = 3;
        private static final int EVENT_REQUEST_FINISHED = 4;
        private static final int EVENT_COORDINATOR_FINISHED = 5;

        private final Map<Long, SomeArgs> mRequestTimeoutTimers;
        private final WeakReference<UceRequestManager> mUceRequestMgrRef;

        public UceRequestHandler(UceRequestManager requestManager, Looper looper) {
            super(looper);
            mRequestTimeoutTimers = new HashMap<>();
            mUceRequestMgrRef = new WeakReference<>(requestManager);
        }

        /**
         * Send the capabilities request message.
         */
        public void sendRequestMessage(Long coordinatorId, Long taskId, long delayTimeMs) {
            SomeArgs args = SomeArgs.obtain();
            args.arg1 = coordinatorId;
            args.arg2 = taskId;

            Message message = obtainMessage();
            message.what = EVENT_EXECUTE_REQUEST;
            message.obj = args;
            sendMessageDelayed(message, delayTimeMs);
        }

        /**
         * Send the Uce request updated message.
         */
        public void sendRequestUpdatedMessage(Long coordinatorId, Long taskId,
                @UceRequestUpdate int requestEvent) {
            SomeArgs args = SomeArgs.obtain();
            args.arg1 = coordinatorId;
            args.arg2 = taskId;
            args.argi1 = requestEvent;

            Message message = obtainMessage();
            message.what = EVENT_REQUEST_UPDATED;
            message.obj = args;
            sendMessage(message);
        }

        /**
         * Set the timeout timer to cancel the capabilities request.
         */
        public void sendRequestTimeoutTimerMessage(Long coordId, Long taskId, Long timeoutAfterMs) {
            synchronized (mRequestTimeoutTimers) {
                SomeArgs args = SomeArgs.obtain();
                args.arg1 = coordId;
                args.arg2 = taskId;

                // Add the message object to the collection. It can be used to find this message
                // when the request is completed and remove the timeout timer.
                mRequestTimeoutTimers.put(taskId, args);

                Message message = obtainMessage();
                message.what = EVENT_REQUEST_TIMEOUT;
                message.obj = args;
                sendMessageDelayed(message, timeoutAfterMs);
            }
        }

        /**
         * Remove the timeout timer because the capabilities request is finished.
         */
        public void removeRequestTimeoutTimer(Long taskId) {
            synchronized (mRequestTimeoutTimers) {
                SomeArgs args = mRequestTimeoutTimers.remove(taskId);
                if (args == null) {
                    return;
                }
                Log.d(LOG_TAG, "removeRequestTimeoutTimer: taskId=" + taskId);
                removeMessages(EVENT_REQUEST_TIMEOUT, args);
                args.recycle();
            }
        }

        public void sendRequestFinishedMessage(Long coordinatorId, Long taskId) {
            SomeArgs args = SomeArgs.obtain();
            args.arg1 = coordinatorId;
            args.arg2 = taskId;

            Message message = obtainMessage();
            message.what = EVENT_REQUEST_FINISHED;
            message.obj = args;
            sendMessage(message);
        }

        /**
         * Finish the UceRequestCoordinator associated with the given id.
         */
        public void sendRequestCoordinatorFinishedMessage(Long coordinatorId) {
            SomeArgs args = SomeArgs.obtain();
            args.arg1 = coordinatorId;

            Message message = obtainMessage();
            message.what = EVENT_COORDINATOR_FINISHED;
            message.obj = args;
            sendMessage(message);
        }

        /**
         * Remove all the messages from the handler
         */
        public void onDestroy() {
            removeCallbacksAndMessages(null);
            // Recycle all the arguments in the mRequestTimeoutTimers
            synchronized (mRequestTimeoutTimers) {
                mRequestTimeoutTimers.forEach((taskId, args) -> {
                    try {
                        args.recycle();
                    } catch (Exception e) {}
                });
                mRequestTimeoutTimers.clear();
            }
        }

        @Override
        public void handleMessage(Message msg) {
            UceRequestManager requestManager = mUceRequestMgrRef.get();
            if (requestManager == null) {
                return;
            }
            SomeArgs args = (SomeArgs) msg.obj;
            final Long coordinatorId = (Long) args.arg1;
            final Long taskId = (Long) Optional.ofNullable(args.arg2).orElse(-1L);
            final Integer requestEvent = Optional.ofNullable(args.argi1).orElse(-1);
            args.recycle();

            requestManager.logd("handleMessage: " + EVENT_DESCRIPTION.get(msg.what)
                    + ", coordinatorId=" + coordinatorId + ", taskId=" + taskId);
            switch (msg.what) {
                case EVENT_EXECUTE_REQUEST: {
                    UceRequest request = requestManager.getUceRequest(taskId);
                    if (request == null) {
                        requestManager.logw("handleMessage: cannot find request, taskId=" + taskId);
                        return;
                    }
                    request.executeRequest();
                    break;
                }
                case EVENT_REQUEST_UPDATED: {
                    UceRequestCoordinator requestCoordinator =
                            requestManager.getRequestCoordinator(coordinatorId);
                    if (requestCoordinator == null) {
                        requestManager.logw("handleMessage: cannot find UceRequestCoordinator");
                        return;
                    }
                    requestCoordinator.onRequestUpdated(taskId, requestEvent);
                    break;
                }
                case EVENT_REQUEST_TIMEOUT: {
                    UceRequestCoordinator requestCoordinator =
                            requestManager.getRequestCoordinator(coordinatorId);
                    if (requestCoordinator == null) {
                        requestManager.logw("handleMessage: cannot find UceRequestCoordinator");
                        return;
                    }
                    // The timeout timer is triggered, remove this record from the collection.
                    synchronized (mRequestTimeoutTimers) {
                        mRequestTimeoutTimers.remove(taskId);
                    }
                    // Notify that the request is timeout.
                    requestCoordinator.onRequestUpdated(taskId,
                            UceRequestCoordinator.REQUEST_UPDATE_TIMEOUT);
                    break;
                }
                case EVENT_REQUEST_FINISHED: {
                    // Notify the repository that the request is finished.
                    requestManager.notifyRepositoryRequestFinished(taskId);
                    break;
                }
                case EVENT_COORDINATOR_FINISHED: {
                    UceRequestCoordinator requestCoordinator =
                            requestManager.removeRequestCoordinator(coordinatorId);
                    if (requestCoordinator != null) {
                        requestCoordinator.onFinish();
                    }
                    break;
                }
                default: {
                    break;
                }
            }
        }

        private static Map<Integer, String> EVENT_DESCRIPTION = new HashMap<>();
        static {
            EVENT_DESCRIPTION.put(EVENT_EXECUTE_REQUEST, "EXECUTE_REQUEST");
            EVENT_DESCRIPTION.put(EVENT_REQUEST_UPDATED, "REQUEST_UPDATE");
            EVENT_DESCRIPTION.put(EVENT_REQUEST_TIMEOUT, "REQUEST_TIMEOUT");
            EVENT_DESCRIPTION.put(EVENT_REQUEST_FINISHED, "REQUEST_FINISHED");
            EVENT_DESCRIPTION.put(EVENT_COORDINATOR_FINISHED, "REMOVE_COORDINATOR");
        }
    }

    private void addRequestCoordinator(UceRequestCoordinator coordinator) {
        mRequestRepository.addRequestCoordinator(coordinator);
    }

    private UceRequestCoordinator removeRequestCoordinator(Long coordinatorId) {
        return mRequestRepository.removeRequestCoordinator(coordinatorId);
    }

    private UceRequestCoordinator getRequestCoordinator(Long coordinatorId) {
        return mRequestRepository.getRequestCoordinator(coordinatorId);
    }

    private UceRequest getUceRequest(Long taskId) {
        return mRequestRepository.getUceRequest(taskId);
    }

    private void notifyRepositoryRequestFinished(Long taskId) {
        mRequestRepository.notifyRequestFinished(taskId);
    }

    private Uri getSipUriFromUri(Uri uri) {
        Uri convertedUri = uri;
        String number = convertedUri.getSchemeSpecificPart();
        String[] numberParts = number.split("[@;:]");
        number = numberParts[0];

        TelephonyManager manager = mContext.getSystemService(TelephonyManager.class);
        if (manager.getIsimDomain() == null) {
            return convertedUri;
        }
        String simCountryIso = manager.getSimCountryIso();
        if (TextUtils.isEmpty(simCountryIso)) {
            return convertedUri;
        }
        simCountryIso = simCountryIso.toUpperCase();
        PhoneNumberUtil util = PhoneNumberUtil.getInstance();
        try {
            Phonenumber.PhoneNumber phoneNumber = util.parse(number, simCountryIso);
            number = util.format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.E164);
            String sipUri = "sip:" + number + "@" + manager.getIsimDomain();
            convertedUri = Uri.parse(sipUri);
        } catch (NumberParseException e) {
            Log.w(LOG_TAG, "formatNumber: could not format " + number + ", error: " + e);
        }
        return convertedUri;
    }

    @VisibleForTesting
    public UceRequestHandler getUceRequestHandler() {
        return mHandler;
    }

    @VisibleForTesting
    public RequestManagerCallback getRequestManagerCallback() {
        return mRequestMgrCallback;
    }

    private void logi(String log) {
        Log.i(LOG_TAG, getLogPrefix().append(log).toString());
    }

    private void logd(String log) {
        Log.d(LOG_TAG, getLogPrefix().append(log).toString());
    }

    private void logw(String log) {
        Log.w(LOG_TAG, getLogPrefix().append(log).toString());
    }

    private StringBuilder getLogPrefix() {
        StringBuilder builder = new StringBuilder("[");
        builder.append(mSubId);
        builder.append("] ");
        return builder;
    }

    private String getNumberFromUri(Uri uri) {
        if (uri == null) return null;
        String number = uri.getSchemeSpecificPart();
        String[] numberParts = number.split("[@;:]");

        if (numberParts.length == 0) {
            return null;
        }
        return numberParts[0];
    }
}
