/*
 * 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.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.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.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";

    /**
     * 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);
            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);
            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;
        if (sUceUtilsProxy.isPresenceCapExchangeEnabled(mContext, mSubId) &&
                sUceUtilsProxy.isPresenceSupported(mContext, mSubId)) {
            requestCoordinator = createSubscribeRequestCoordinator(type, uriList, skipFromCache,
                    callback);
        } else if (sUceUtilsProxy.isSipOptionsSupported(mContext, mSubId)) {
            requestCoordinator = createOptionsRequestCoordinator(type, uriList, callback);
        }

        if (requestCoordinator == null) {
            logw("sendRequestInternal: Neither Presence nor OPTIONS are supported");
            callback.onError(RcsUceAdapter.ERROR_NOT_ENABLED, 0L);
            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);
    }

    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);
                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.of(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);
    }

    @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];
    }
}
