/*
 * Copyright 2017 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 android.telephony.data;

import android.annotation.CallbackExecutor;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SdkConstant;
import android.annotation.SystemApi;
import android.app.Service;
import android.content.Intent;
import android.net.LinkProperties;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.telephony.AccessNetworkConstants.RadioAccessNetworkType;
import android.util.Log;
import android.util.SparseArray;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.IIntegerConsumer;
import com.android.internal.telephony.flags.Flags;
import com.android.internal.util.FunctionalUtils;
import com.android.telephony.Rlog;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.Consumer;

/**
 * Base class of data service. Services that extend DataService must register the service in
 * their AndroidManifest to be detected by the framework. They must be protected by the permission
 * "android.permission.BIND_TELEPHONY_DATA_SERVICE". The data service definition in the manifest
 * must follow the following format:
 * ...
 * <service android:name=".xxxDataService"
 *     android:permission="android.permission.BIND_TELEPHONY_DATA_SERVICE" >
 *     <intent-filter>
 *         <action android:name="android.telephony.data.DataService" />
 *     </intent-filter>
 * </service>
 * @hide
 */
@SystemApi
public abstract class DataService extends Service {
    private static final String TAG = DataService.class.getSimpleName();

    @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
    public static final String SERVICE_INTERFACE = "android.telephony.data.DataService";

    /** {@hide} */
    @IntDef(prefix = "REQUEST_REASON_", value = {
            REQUEST_REASON_UNKNOWN,
            REQUEST_REASON_NORMAL,
            REQUEST_REASON_HANDOVER,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface SetupDataReason {}

    /** {@hide} */
    @IntDef(prefix = "REQUEST_REASON_", value = {
            REQUEST_REASON_UNKNOWN,
            REQUEST_REASON_NORMAL,
            REQUEST_REASON_SHUTDOWN,
            REQUEST_REASON_HANDOVER,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface DeactivateDataReason {}

    /** The reason of the data request is unknown */
    public static final int REQUEST_REASON_UNKNOWN = 0;

    /** The reason of the data request is normal */
    public static final int REQUEST_REASON_NORMAL = 1;

    /** The reason of the data request is device shutdown */
    public static final int REQUEST_REASON_SHUTDOWN = 2;

    /** The reason of the data request is IWLAN handover */
    public static final int REQUEST_REASON_HANDOVER = 3;

    private static final int DATA_SERVICE_CREATE_DATA_SERVICE_PROVIDER                 = 1;
    private static final int DATA_SERVICE_REMOVE_DATA_SERVICE_PROVIDER                 = 2;
    private static final int DATA_SERVICE_REMOVE_ALL_DATA_SERVICE_PROVIDERS            = 3;
    private static final int DATA_SERVICE_REQUEST_SETUP_DATA_CALL                      = 4;
    private static final int DATA_SERVICE_REQUEST_DEACTIVATE_DATA_CALL                 = 5;
    private static final int DATA_SERVICE_REQUEST_SET_INITIAL_ATTACH_APN               = 6;
    private static final int DATA_SERVICE_REQUEST_SET_DATA_PROFILE                     = 7;
    private static final int DATA_SERVICE_REQUEST_REQUEST_DATA_CALL_LIST               = 8;
    private static final int DATA_SERVICE_REQUEST_REGISTER_DATA_CALL_LIST_CHANGED      = 9;
    private static final int DATA_SERVICE_REQUEST_UNREGISTER_DATA_CALL_LIST_CHANGED    = 10;
    private static final int DATA_SERVICE_INDICATION_DATA_CALL_LIST_CHANGED            = 11;
    private static final int DATA_SERVICE_REQUEST_START_HANDOVER                       = 12;
    private static final int DATA_SERVICE_REQUEST_CANCEL_HANDOVER                      = 13;
    private static final int DATA_SERVICE_REQUEST_REGISTER_APN_UNTHROTTLED             = 14;
    private static final int DATA_SERVICE_REQUEST_UNREGISTER_APN_UNTHROTTLED           = 15;
    private static final int DATA_SERVICE_INDICATION_APN_UNTHROTTLED                   = 16;
    private static final int DATA_SERVICE_REQUEST_VALIDATION                           = 17;

    private final HandlerThread mHandlerThread;

    private final DataServiceHandler mHandler;

    private final Executor mHandlerExecutor;

    private final SparseArray<DataServiceProvider> mServiceMap = new SparseArray<>();

    /** @hide */
    @VisibleForTesting
    public final IDataServiceWrapper mBinder = new IDataServiceWrapper();

    /**
     * The abstract class of the actual data service implementation. The data service provider
     * must extend this class to support data connection. Note that each instance of data service
     * provider is associated with one physical SIM slot.
     */
    public abstract class DataServiceProvider implements AutoCloseable {

        private final int mSlotIndex;

        private final List<IDataServiceCallback> mDataCallListChangedCallbacks = new ArrayList<>();

        private final List<IDataServiceCallback> mApnUnthrottledCallbacks = new ArrayList<>();

        /**
         * Constructor
         * @param slotIndex SIM slot index the data service provider associated with.
         */
        public DataServiceProvider(int slotIndex) {
            mSlotIndex = slotIndex;
        }

        /**
         * @return SIM slot index the data service provider associated with.
         */
        public final int getSlotIndex() {
            return mSlotIndex;
        }

        /**
         * Setup a data connection. The data service provider must implement this method to support
         * establishing a packet data connection. When completed or error, the service must invoke
         * the provided callback to notify the platform.
         *
         * @param accessNetworkType Access network type that the data call will be established on.
         *        Must be one of {@link android.telephony.AccessNetworkConstants.AccessNetworkType}.
         * @param dataProfile Data profile used for data call setup. See {@link DataProfile}
         * @param isRoaming True if the device is data roaming.
         * @param allowRoaming True if data roaming is allowed by the user.
         * @param reason The reason for data setup. Must be {@link #REQUEST_REASON_NORMAL} or
         *        {@link #REQUEST_REASON_HANDOVER}.
         * @param linkProperties If {@code reason} is {@link #REQUEST_REASON_HANDOVER}, this is the
         *        link properties of the existing data connection, otherwise null.
         * @param callback The result callback for this request.
         */
        public void setupDataCall(
                @RadioAccessNetworkType int accessNetworkType, @NonNull DataProfile dataProfile,
                boolean isRoaming, boolean allowRoaming,
                @SetupDataReason int reason, @Nullable LinkProperties linkProperties,
                @NonNull DataServiceCallback callback) {
            // The default implementation is to return unsupported.
            if (callback != null) {
                callback.onSetupDataCallComplete(DataServiceCallback.RESULT_ERROR_UNSUPPORTED,
                        null);
            }
        }

        /**
         * Setup a data connection. The data service provider must implement this method to support
         * establishing a packet data connection. When completed or error, the service must invoke
         * the provided callback to notify the platform.
         *
         * @param accessNetworkType Access network type that the data call will be established on.
         *        Must be one of {@link android.telephony.AccessNetworkConstants.AccessNetworkType}.
         * @param dataProfile Data profile used for data call setup. See {@link DataProfile}
         * @param isRoaming True if the device is data roaming.
         * @param allowRoaming True if data roaming is allowed by the user.
         * @param reason The reason for data setup. Must be {@link #REQUEST_REASON_NORMAL} or
         *        {@link #REQUEST_REASON_HANDOVER}.
         * @param linkProperties If {@code reason} is {@link #REQUEST_REASON_HANDOVER}, this is the
         *        link properties of the existing data connection, otherwise null.
         * @param pduSessionId The pdu session id to be used for this data call.
         *                     The standard range of values are 1-15 while 0 means no pdu session id
         *                     was attached to this call.  Reference: 3GPP TS 24.007 section
         *                     11.2.3.1b.
         * @param sliceInfo used within the data connection when a handover occurs from EPDG to 5G.
         *        The value is null unless the access network is
         *        {@link android.telephony.AccessNetworkConstants.AccessNetworkType#NGRAN} and a
         *        handover is occurring from EPDG to 5G.  If the slice passed is rejected, then
         *        {@link DataCallResponse#getCause()} is
         *        {@link android.telephony.DataFailCause#SLICE_REJECTED}.
         * @param trafficDescriptor {@link TrafficDescriptor} for which data connection needs to be
         *        established. It is used for URSP traffic matching as described in 3GPP TS 24.526
         *        Section 4.2.2. It includes an optional DNN which, if present, must be used for
         *        traffic matching; it does not specify the end point to be used for the data call.
         * @param matchAllRuleAllowed Indicates if using default match-all URSP rule for this
         *        request is allowed. If false, this request must not use the match-all URSP rule
         *        and if a non-match-all rule is not found (or if URSP rules are not available) then
         *        {@link DataCallResponse#getCause()} is
         *        {@link android.telephony.DataFailCause#MATCH_ALL_RULE_NOT_ALLOWED}. This is needed
         *        as some requests need to have a hard failure if the intention cannot be met,
         *        for example, a zero-rating slice.
         * @param callback The result callback for this request.
         */
        public void setupDataCall(
                @RadioAccessNetworkType int accessNetworkType, @NonNull DataProfile dataProfile,
                boolean isRoaming, boolean allowRoaming,
                @SetupDataReason int reason,
                @Nullable LinkProperties linkProperties,
                @IntRange(from = 0, to = 15) int pduSessionId, @Nullable NetworkSliceInfo sliceInfo,
                @Nullable TrafficDescriptor trafficDescriptor, boolean matchAllRuleAllowed,
                @NonNull DataServiceCallback callback) {
            /* Call the old version since the new version isn't supported */
            setupDataCall(accessNetworkType, dataProfile, isRoaming, allowRoaming, reason,
                    linkProperties, callback);
        }

        /**
         * Deactivate a data connection. The data service provider must implement this method to
         * support data connection tear down. When completed or error, the service must invoke the
         * provided callback to notify the platform.
         *
         * @param cid Call id returned in the callback of {@link DataServiceProvider#setupDataCall(
         *        int, DataProfile, boolean, boolean, int, LinkProperties, DataServiceCallback)}.
         * @param reason The reason for data deactivation. Must be {@link #REQUEST_REASON_NORMAL},
         *        {@link #REQUEST_REASON_SHUTDOWN} or {@link #REQUEST_REASON_HANDOVER}.
         * @param callback The result callback for this request. Null if the client does not care
         *        about the result.
         *
         */
        public void deactivateDataCall(int cid, @DeactivateDataReason int reason,
                                       @Nullable DataServiceCallback callback) {
            // The default implementation is to return unsupported.
            if (callback != null) {
                callback.onDeactivateDataCallComplete(DataServiceCallback.RESULT_ERROR_UNSUPPORTED);
            }
        }

        /**
         * Set an APN to initial attach network.
         *
         * @param dataProfile Data profile used for data call setup. See {@link DataProfile}.
         * @param isRoaming True if the device is data roaming.
         * @param callback The result callback for this request.
         */
        public void setInitialAttachApn(@NonNull DataProfile dataProfile, boolean isRoaming,
                                        @NonNull DataServiceCallback callback) {
            // The default implementation is to return unsupported.
            if (callback != null) {
                callback.onSetInitialAttachApnComplete(
                        DataServiceCallback.RESULT_ERROR_UNSUPPORTED);
            }
        }

        /**
         * Send current carrier's data profiles to the data service for data call setup. This is
         * only for CDMA carrier that can change the profile through OTA. The data service should
         * always uses the latest data profile sent by the framework.
         *
         * @param dps A list of data profiles.
         * @param isRoaming True if the device is data roaming.
         * @param callback The result callback for this request.
         */
        public void setDataProfile(@NonNull List<DataProfile> dps, boolean isRoaming,
                                   @NonNull DataServiceCallback callback) {
            // The default implementation is to return unsupported.
            if (callback != null) {
                callback.onSetDataProfileComplete(DataServiceCallback.RESULT_ERROR_UNSUPPORTED);
            }
        }

        /**
         * Indicates that a handover has begun.  This is called on the source transport.
         *
         * Any resources being transferred cannot be released while a
         * handover is underway.
         * <p/>
         * If a handover was unsuccessful, then the framework calls
         * {@link DataService#cancelHandover}.  The target transport retains ownership over any of
         * the resources being transferred.
         * <p/>
         * If a handover was successful, the framework calls {@link DataService#deactivateDataCall}
         * with reason {@link DataService.REQUEST_REASON_HANDOVER}. The target transport now owns
         * the transferred resources and is responsible for releasing them.
         *
         * <p/>
         * Note that the callback will be executed on binder thread.
         *
         * @param cid The identifier of the data call which is provided in {@link DataCallResponse}
         * @param callback The result callback for this request.
         *
         * @hide
         */
        public void startHandover(int cid, @NonNull DataServiceCallback callback) {
            Objects.requireNonNull(callback, "callback cannot be null");
            // The default implementation is to return unsupported.
            Log.d(TAG, "startHandover: " + cid);
            callback.onHandoverStarted(DataServiceCallback.RESULT_ERROR_UNSUPPORTED);
        }

        /**
         * Indicates that a handover was cancelled after a call to
         * {@link DataService#startHandover}. This is called on the source transport.
         * <p/>
         * Since the handover was unsuccessful, the source transport retains ownership over any of
         * the resources being transferred and is still responsible for releasing them.
         * <p/>
         * The handover can be cancelled up until either:
         * <ul><li>
         *     The handover was successful after receiving a successful response from
         *     {@link DataService#setupDataCall} on the target transport.
         * </li><li>
         *     The data call on the source transport was lost.
         * </li>
         * </ul>
         *
         * <p/>
         * Note that the callback will be executed on binder thread.
         *
         * @param cid The identifier of the data call which is provided in {@link DataCallResponse}
         * @param callback The result callback for this request.
         *
         * @hide
         */
        public void cancelHandover(int cid, @NonNull DataServiceCallback callback) {
            Objects.requireNonNull(callback, "callback cannot be null");
            // The default implementation is to return unsupported.
            Log.d(TAG, "cancelHandover: " + cid);
            callback.onHandoverCancelled(DataServiceCallback.RESULT_ERROR_UNSUPPORTED);
        }

        /**
         * Get the active data call list.
         *
         * @param callback The result callback for this request.
         */
        public void requestDataCallList(@NonNull DataServiceCallback callback) {
            // The default implementation is to return unsupported.
            callback.onRequestDataCallListComplete(DataServiceCallback.RESULT_ERROR_UNSUPPORTED,
                    Collections.EMPTY_LIST);
        }

        private void registerForDataCallListChanged(IDataServiceCallback callback) {
            synchronized (mDataCallListChangedCallbacks) {
                mDataCallListChangedCallbacks.add(callback);
            }
        }

        private void unregisterForDataCallListChanged(IDataServiceCallback callback) {
            synchronized (mDataCallListChangedCallbacks) {
                mDataCallListChangedCallbacks.remove(callback);
            }
        }

        private void registerForApnUnthrottled(IDataServiceCallback callback) {
            synchronized (mApnUnthrottledCallbacks) {
                mApnUnthrottledCallbacks.add(callback);
            }
        }

        private void unregisterForApnUnthrottled(IDataServiceCallback callback) {
            synchronized (mApnUnthrottledCallbacks) {
                mApnUnthrottledCallbacks.remove(callback);
            }
        }

        /**
         * Request validation check to see if the network is working properly for a given data call.
         *
         * <p>This request is completed immediately after submitting the request to the data service
         * provider and receiving {@link DataServiceCallback.ResultCode}, and progress status or
         * validation results are notified through {@link
         * DataCallResponse#getNetworkValidationStatus}.
         *
         * <p> If the network validation request is submitted successfully, {@link
         * DataServiceCallback#RESULT_SUCCESS} is passed to {@code resultCodeCallback}. If the
         * network validation feature is not supported by the data service provider itself, {@link
         * DataServiceCallback#RESULT_ERROR_UNSUPPORTED} is passed to {@code resultCodeCallback}.
         * See {@link DataServiceCallback.ResultCode} for the type of response that indicates
         * whether the request was successfully submitted or had an error.
         *
         * <p>In response to this network validation request, providers can validate the data call
         * in their own way. For example, in IWLAN, the DPD (Dead Peer Detection) can be used as a
         * tool to check whether a data call is alive.
         *
         * @param cid The identifier of the data call which is provided in {@link DataCallResponse}
         * @param executor The callback executor for the response.
         * @param resultCodeCallback Listener for the {@link DataServiceCallback.ResultCode} that
         *     request validation to the DataService and checks if the request has been submitted.
         */
        @FlaggedApi(Flags.FLAG_NETWORK_VALIDATION)
        public void requestNetworkValidation(int cid,
                @NonNull @CallbackExecutor Executor executor,
                @NonNull @DataServiceCallback.ResultCode Consumer<Integer> resultCodeCallback) {
            Objects.requireNonNull(executor, "executor cannot be null");
            Objects.requireNonNull(resultCodeCallback, "resultCodeCallback cannot be null");

            Log.d(TAG, "requestNetworkValidation: " + cid);

            // The default implementation is to return unsupported.
            executor.execute(() -> resultCodeCallback
                    .accept(DataServiceCallback.RESULT_ERROR_UNSUPPORTED));
        }

        /**
         * Notify the system that current data call list changed. Data service must invoke this
         * method whenever there is any data call status changed.
         *
         * @param dataCallList List of the current active data call.
         */
        public final void notifyDataCallListChanged(List<DataCallResponse> dataCallList) {
            synchronized (mDataCallListChangedCallbacks) {
                for (IDataServiceCallback callback : mDataCallListChangedCallbacks) {
                    mHandler.obtainMessage(DATA_SERVICE_INDICATION_DATA_CALL_LIST_CHANGED,
                            mSlotIndex, 0, new DataCallListChangedIndication(dataCallList,
                                    callback)).sendToTarget();
                }
            }
        }

        /**
         * Notify the system that a given APN was unthrottled.
         *
         * @param apn Access Point Name defined by the carrier.
         */
        public final void notifyApnUnthrottled(@NonNull String apn) {
            synchronized (mApnUnthrottledCallbacks) {
                for (IDataServiceCallback callback : mApnUnthrottledCallbacks) {
                    mHandler.obtainMessage(DATA_SERVICE_INDICATION_APN_UNTHROTTLED,
                            mSlotIndex, 0, new ApnUnthrottledIndication(apn,
                                    callback)).sendToTarget();
                }
            }
        }

        /**
         * Notify the system that a given DataProfile was unthrottled.
         *
         * @param dataProfile DataProfile associated with an APN returned from the modem
         */
        public final void notifyDataProfileUnthrottled(@NonNull DataProfile dataProfile) {
            synchronized (mApnUnthrottledCallbacks) {
                for (IDataServiceCallback callback : mApnUnthrottledCallbacks) {
                    mHandler.obtainMessage(DATA_SERVICE_INDICATION_APN_UNTHROTTLED,
                            mSlotIndex, 0, new ApnUnthrottledIndication(dataProfile,
                                    callback)).sendToTarget();
                }
            }
        }

        /**
         * Called when the instance of data service is destroyed (e.g. got unbind or binder died)
         * or when the data service provider is removed. The extended class should implement this
         * method to perform cleanup works.
         */
        @Override
        public abstract void close();
    }

    private static final class SetupDataCallRequest {
        public final int accessNetworkType;
        public final DataProfile dataProfile;
        public final boolean isRoaming;
        public final boolean allowRoaming;
        public final int reason;
        public final LinkProperties linkProperties;
        public final int pduSessionId;
        public final NetworkSliceInfo sliceInfo;
        public final TrafficDescriptor trafficDescriptor;
        public final boolean matchAllRuleAllowed;
        public final IDataServiceCallback callback;
        SetupDataCallRequest(int accessNetworkType, DataProfile dataProfile, boolean isRoaming,
                boolean allowRoaming, int reason, LinkProperties linkProperties, int pduSessionId,
                NetworkSliceInfo sliceInfo, TrafficDescriptor trafficDescriptor,
                boolean matchAllRuleAllowed, IDataServiceCallback callback) {
            this.accessNetworkType = accessNetworkType;
            this.dataProfile = dataProfile;
            this.isRoaming = isRoaming;
            this.allowRoaming = allowRoaming;
            this.linkProperties = linkProperties;
            this.reason = reason;
            this.pduSessionId = pduSessionId;
            this.sliceInfo = sliceInfo;
            this.trafficDescriptor = trafficDescriptor;
            this.matchAllRuleAllowed = matchAllRuleAllowed;
            this.callback = callback;
        }
    }

    private static final class DeactivateDataCallRequest {
        public final int cid;
        public final int reason;
        public final IDataServiceCallback callback;
        DeactivateDataCallRequest(int cid, int reason, IDataServiceCallback callback) {
            this.cid = cid;
            this.reason = reason;
            this.callback = callback;
        }
    }

    private static final class SetInitialAttachApnRequest {
        public final DataProfile dataProfile;
        public final boolean isRoaming;
        public final IDataServiceCallback callback;
        SetInitialAttachApnRequest(DataProfile dataProfile, boolean isRoaming,
                                   IDataServiceCallback callback) {
            this.dataProfile = dataProfile;
            this.isRoaming = isRoaming;
            this.callback = callback;
        }
    }

    private static final class SetDataProfileRequest {
        public final List<DataProfile> dps;
        public final boolean isRoaming;
        public final IDataServiceCallback callback;
        SetDataProfileRequest(List<DataProfile> dps, boolean isRoaming,
                              IDataServiceCallback callback) {
            this.dps = dps;
            this.isRoaming = isRoaming;
            this.callback = callback;
        }
    }

    private static final class BeginCancelHandoverRequest {
        public final int cid;
        public final IDataServiceCallback callback;
        BeginCancelHandoverRequest(int cid,
                IDataServiceCallback callback) {
            this.cid = cid;
            this.callback = callback;
        }
    }

    private static final class DataCallListChangedIndication {
        public final List<DataCallResponse> dataCallList;
        public final IDataServiceCallback callback;
        DataCallListChangedIndication(List<DataCallResponse> dataCallList,
                                      IDataServiceCallback callback) {
            this.dataCallList = dataCallList;
            this.callback = callback;
        }
    }

    private static final class ApnUnthrottledIndication {
        public final DataProfile dataProfile;
        public final String apn;
        public final IDataServiceCallback callback;
        ApnUnthrottledIndication(String apn,
                IDataServiceCallback callback) {
            this.dataProfile = null;
            this.apn = apn;
            this.callback = callback;
        }
        ApnUnthrottledIndication(DataProfile dataProfile, IDataServiceCallback callback) {
            this.dataProfile = dataProfile;
            this.apn = null;
            this.callback = callback;
        }
    }

    private static final class ValidationRequest {
        public final int cid;
        public final Executor executor;
        public final IIntegerConsumer callback;
        ValidationRequest(int cid, Executor executor, IIntegerConsumer callback) {
            this.cid = cid;
            this.executor = executor;
            this.callback = callback;
        }
    }

    private class DataServiceHandler extends Handler {

        DataServiceHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message message) {
            IDataServiceCallback callback;
            final int slotIndex = message.arg1;
            DataServiceProvider serviceProvider = mServiceMap.get(slotIndex);

            switch (message.what) {
                case DATA_SERVICE_CREATE_DATA_SERVICE_PROVIDER:
                    serviceProvider = onCreateDataServiceProvider(message.arg1);
                    if (serviceProvider != null) {
                        mServiceMap.put(slotIndex, serviceProvider);
                    }
                    break;
                case DATA_SERVICE_REMOVE_DATA_SERVICE_PROVIDER:
                    if (serviceProvider != null) {
                        serviceProvider.close();
                        mServiceMap.remove(slotIndex);
                    }
                    break;
                case DATA_SERVICE_REMOVE_ALL_DATA_SERVICE_PROVIDERS:
                    for (int i = 0; i < mServiceMap.size(); i++) {
                        serviceProvider = mServiceMap.get(i);
                        if (serviceProvider != null) {
                            serviceProvider.close();
                        }
                    }
                    mServiceMap.clear();
                    break;
                case DATA_SERVICE_REQUEST_SETUP_DATA_CALL:
                    if (serviceProvider == null) break;
                    SetupDataCallRequest setupDataCallRequest = (SetupDataCallRequest) message.obj;
                    serviceProvider.setupDataCall(setupDataCallRequest.accessNetworkType,
                            setupDataCallRequest.dataProfile, setupDataCallRequest.isRoaming,
                            setupDataCallRequest.allowRoaming, setupDataCallRequest.reason,
                            setupDataCallRequest.linkProperties, setupDataCallRequest.pduSessionId,
                            setupDataCallRequest.sliceInfo, setupDataCallRequest.trafficDescriptor,
                            setupDataCallRequest.matchAllRuleAllowed,
                            (setupDataCallRequest.callback != null)
                                    ? new DataServiceCallback(setupDataCallRequest.callback)
                                    : null);

                    break;
                case DATA_SERVICE_REQUEST_DEACTIVATE_DATA_CALL:
                    if (serviceProvider == null) break;
                    DeactivateDataCallRequest deactivateDataCallRequest =
                            (DeactivateDataCallRequest) message.obj;
                    serviceProvider.deactivateDataCall(deactivateDataCallRequest.cid,
                            deactivateDataCallRequest.reason,
                            (deactivateDataCallRequest.callback != null)
                                    ? new DataServiceCallback(deactivateDataCallRequest.callback)
                                    : null);
                    break;
                case DATA_SERVICE_REQUEST_SET_INITIAL_ATTACH_APN:
                    if (serviceProvider == null) break;
                    SetInitialAttachApnRequest setInitialAttachApnRequest =
                            (SetInitialAttachApnRequest) message.obj;
                    serviceProvider.setInitialAttachApn(setInitialAttachApnRequest.dataProfile,
                            setInitialAttachApnRequest.isRoaming,
                            (setInitialAttachApnRequest.callback != null)
                                    ? new DataServiceCallback(setInitialAttachApnRequest.callback)
                                    : null);
                    break;
                case DATA_SERVICE_REQUEST_SET_DATA_PROFILE:
                    if (serviceProvider == null) break;
                    SetDataProfileRequest setDataProfileRequest =
                            (SetDataProfileRequest) message.obj;
                    serviceProvider.setDataProfile(setDataProfileRequest.dps,
                            setDataProfileRequest.isRoaming,
                            (setDataProfileRequest.callback != null)
                                    ? new DataServiceCallback(setDataProfileRequest.callback)
                                    : null);
                    break;
                case DATA_SERVICE_REQUEST_REQUEST_DATA_CALL_LIST:
                    if (serviceProvider == null) break;

                    serviceProvider.requestDataCallList(new DataServiceCallback(
                            (IDataServiceCallback) message.obj));
                    break;
                case DATA_SERVICE_REQUEST_REGISTER_DATA_CALL_LIST_CHANGED:
                    if (serviceProvider == null) break;
                    serviceProvider.registerForDataCallListChanged((IDataServiceCallback) message.obj);
                    break;
                case DATA_SERVICE_REQUEST_UNREGISTER_DATA_CALL_LIST_CHANGED:
                    if (serviceProvider == null) break;
                    callback = (IDataServiceCallback) message.obj;
                    serviceProvider.unregisterForDataCallListChanged(callback);
                    break;
                case DATA_SERVICE_INDICATION_DATA_CALL_LIST_CHANGED:
                    if (serviceProvider == null) break;
                    DataCallListChangedIndication indication =
                            (DataCallListChangedIndication) message.obj;
                    try {
                        indication.callback.onDataCallListChanged(indication.dataCallList);
                    } catch (RemoteException e) {
                        loge("Failed to call onDataCallListChanged. " + e);
                    }
                    break;
                case DATA_SERVICE_REQUEST_START_HANDOVER:
                    if (serviceProvider == null) break;
                    BeginCancelHandoverRequest bReq = (BeginCancelHandoverRequest) message.obj;
                    serviceProvider.startHandover(bReq.cid,
                            (bReq.callback != null)
                                    ? new DataServiceCallback(bReq.callback) : null);
                    break;
                case DATA_SERVICE_REQUEST_CANCEL_HANDOVER:
                    if (serviceProvider == null) break;
                    BeginCancelHandoverRequest cReq = (BeginCancelHandoverRequest) message.obj;
                    serviceProvider.cancelHandover(cReq.cid,
                            (cReq.callback != null)
                                    ? new DataServiceCallback(cReq.callback) : null);
                    break;
                case DATA_SERVICE_REQUEST_REGISTER_APN_UNTHROTTLED:
                    if (serviceProvider == null) break;
                    serviceProvider.registerForApnUnthrottled((IDataServiceCallback) message.obj);
                    break;
                case DATA_SERVICE_REQUEST_UNREGISTER_APN_UNTHROTTLED:
                    if (serviceProvider == null) break;
                    callback = (IDataServiceCallback) message.obj;
                    serviceProvider.unregisterForApnUnthrottled(callback);
                    break;
                case DATA_SERVICE_INDICATION_APN_UNTHROTTLED:
                    if (serviceProvider == null) break;
                    ApnUnthrottledIndication apnUnthrottledIndication =
                            (ApnUnthrottledIndication) message.obj;
                    try {
                        if (apnUnthrottledIndication.dataProfile != null) {
                            apnUnthrottledIndication.callback
                                    .onDataProfileUnthrottled(apnUnthrottledIndication.dataProfile);
                        } else {
                            apnUnthrottledIndication.callback
                                    .onApnUnthrottled(apnUnthrottledIndication.apn);
                        }
                    } catch (RemoteException e) {
                        loge("Failed to call onApnUnthrottled. " + e);
                    }
                    break;
                case DATA_SERVICE_REQUEST_VALIDATION:
                    if (serviceProvider == null) break;
                    ValidationRequest validationRequest = (ValidationRequest) message.obj;
                    serviceProvider.requestNetworkValidation(
                            validationRequest.cid,
                            validationRequest.executor,
                            FunctionalUtils
                                    .ignoreRemoteException(validationRequest.callback::accept));
                    break;
            }
        }
    }

    /**
     * Default constructor.
     */
    public DataService() {
        mHandlerThread = new HandlerThread(TAG);
        mHandlerThread.start();

        mHandler = new DataServiceHandler(mHandlerThread.getLooper());
        mHandlerExecutor = new HandlerExecutor(mHandler);
        log("Data service created");
    }

    /**
     * Create the instance of {@link DataServiceProvider}. Data service provider must override
     * this method to facilitate the creation of {@link DataServiceProvider} instances. The system
     * will call this method after binding the data service for each active SIM slot id.
     *
     * This methead is guaranteed to be invoked in {@link DataService}'s internal handler thread
     * whose looper can be retrieved with {@link Looper.myLooper()} when override this method.
     *
     * @param slotIndex SIM slot id the data service associated with.
     * @return Data service object. Null if failed to create the provider (e.g. invalid slot index)
     */
    @Nullable
    public abstract DataServiceProvider onCreateDataServiceProvider(int slotIndex);

    @Override
    public IBinder onBind(Intent intent) {
        if (intent == null || !SERVICE_INTERFACE.equals(intent.getAction())) {
            loge("Unexpected intent " + intent);
            return null;
        }
        return mBinder;
    }

    @Override
    public boolean onUnbind(Intent intent) {
        mHandler.obtainMessage(DATA_SERVICE_REMOVE_ALL_DATA_SERVICE_PROVIDERS).sendToTarget();
        return false;
    }

    @Override
    public void onDestroy() {
        mHandlerThread.quitSafely();
        super.onDestroy();
    }

    /**
     * A wrapper around IDataService that forwards calls to implementations of {@link DataService}.
     */
    private class IDataServiceWrapper extends IDataService.Stub {
        @Override
        public void createDataServiceProvider(int slotIndex) {
            mHandler.obtainMessage(DATA_SERVICE_CREATE_DATA_SERVICE_PROVIDER, slotIndex, 0)
                    .sendToTarget();
        }

        @Override
        public void removeDataServiceProvider(int slotIndex) {
            mHandler.obtainMessage(DATA_SERVICE_REMOVE_DATA_SERVICE_PROVIDER, slotIndex, 0)
                    .sendToTarget();
        }

        @Override
        public void setupDataCall(int slotIndex, int accessNetworkType, DataProfile dataProfile,
                boolean isRoaming, boolean allowRoaming, int reason,
                LinkProperties linkProperties, int pduSessionId, NetworkSliceInfo sliceInfo,
                TrafficDescriptor trafficDescriptor, boolean matchAllRuleAllowed,
                IDataServiceCallback callback) {
            mHandler.obtainMessage(DATA_SERVICE_REQUEST_SETUP_DATA_CALL, slotIndex, 0,
                    new SetupDataCallRequest(accessNetworkType, dataProfile, isRoaming,
                            allowRoaming, reason, linkProperties, pduSessionId, sliceInfo,
                            trafficDescriptor, matchAllRuleAllowed, callback))
                    .sendToTarget();
        }

        @Override
        public void deactivateDataCall(int slotIndex, int cid, int reason,
                                       IDataServiceCallback callback) {
            mHandler.obtainMessage(DATA_SERVICE_REQUEST_DEACTIVATE_DATA_CALL, slotIndex, 0,
                    new DeactivateDataCallRequest(cid, reason, callback))
                    .sendToTarget();
        }

        @Override
        public void setInitialAttachApn(int slotIndex, DataProfile dataProfile, boolean isRoaming,
                                        IDataServiceCallback callback) {
            mHandler.obtainMessage(DATA_SERVICE_REQUEST_SET_INITIAL_ATTACH_APN, slotIndex, 0,
                    new SetInitialAttachApnRequest(dataProfile, isRoaming, callback))
                    .sendToTarget();
        }

        @Override
        public void setDataProfile(int slotIndex, List<DataProfile> dps, boolean isRoaming,
                                   IDataServiceCallback callback) {
            mHandler.obtainMessage(DATA_SERVICE_REQUEST_SET_DATA_PROFILE, slotIndex, 0,
                    new SetDataProfileRequest(dps, isRoaming, callback)).sendToTarget();
        }

        @Override
        public void requestDataCallList(int slotIndex, IDataServiceCallback callback) {
            if (callback == null) {
                loge("requestDataCallList: callback is null");
                return;
            }
            mHandler.obtainMessage(DATA_SERVICE_REQUEST_REQUEST_DATA_CALL_LIST, slotIndex, 0,
                    callback).sendToTarget();
        }

        @Override
        public void registerForDataCallListChanged(int slotIndex, IDataServiceCallback callback) {
            if (callback == null) {
                loge("registerForDataCallListChanged: callback is null");
                return;
            }
            mHandler.obtainMessage(DATA_SERVICE_REQUEST_REGISTER_DATA_CALL_LIST_CHANGED, slotIndex,
                    0, callback).sendToTarget();
        }

        @Override
        public void unregisterForDataCallListChanged(int slotIndex, IDataServiceCallback callback) {
            if (callback == null) {
                loge("unregisterForDataCallListChanged: callback is null");
                return;
            }
            mHandler.obtainMessage(DATA_SERVICE_REQUEST_UNREGISTER_DATA_CALL_LIST_CHANGED,
                    slotIndex, 0, callback).sendToTarget();
        }

        @Override
        public void startHandover(int slotIndex, int cid, IDataServiceCallback callback) {
            if (callback == null) {
                loge("startHandover: callback is null");
                return;
            }
            BeginCancelHandoverRequest req = new BeginCancelHandoverRequest(cid, callback);
            mHandler.obtainMessage(DATA_SERVICE_REQUEST_START_HANDOVER,
                    slotIndex, 0, req)
                    .sendToTarget();
        }

        @Override
        public void cancelHandover(int slotIndex, int cid, IDataServiceCallback callback) {
            if (callback == null) {
                loge("cancelHandover: callback is null");
                return;
            }
            BeginCancelHandoverRequest req = new BeginCancelHandoverRequest(cid, callback);
            mHandler.obtainMessage(DATA_SERVICE_REQUEST_CANCEL_HANDOVER,
                    slotIndex, 0, req).sendToTarget();
        }

        @Override
        public void registerForUnthrottleApn(int slotIndex, IDataServiceCallback callback) {
            if (callback == null) {
                loge("registerForUnthrottleApn: callback is null");
                return;
            }
            mHandler.obtainMessage(DATA_SERVICE_REQUEST_REGISTER_APN_UNTHROTTLED, slotIndex,
                    0, callback).sendToTarget();
        }

        @Override
        public void unregisterForUnthrottleApn(int slotIndex, IDataServiceCallback callback) {
            if (callback == null) {
                loge("uregisterForUnthrottleApn: callback is null");
                return;
            }
            mHandler.obtainMessage(DATA_SERVICE_REQUEST_UNREGISTER_APN_UNTHROTTLED,
                    slotIndex, 0, callback).sendToTarget();
        }

        @Override
        public void requestNetworkValidation(int slotIndex, int cid,
                IIntegerConsumer resultCodeCallback) {
            if (resultCodeCallback == null) {
                loge("requestNetworkValidation: resultCodeCallback is null");
                return;
            }
            ValidationRequest validationRequest =
                    new ValidationRequest(cid, mHandlerExecutor, resultCodeCallback);
            mHandler.obtainMessage(DATA_SERVICE_REQUEST_VALIDATION,
                    slotIndex, 0, validationRequest).sendToTarget();
        }
    }

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

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