/*
 * Copyright (C) 2022 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;

import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.app.Service;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.CancellationSignal;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
import android.telephony.Annotation.DisconnectCauses;
import android.telephony.Annotation.PreciseDisconnectCauses;
import android.telephony.ims.ImsReasonInfo;
import android.text.TextUtils;
import android.util.Log;

import com.android.internal.telephony.IDomainSelectionServiceController;
import com.android.internal.telephony.IDomainSelector;
import com.android.internal.telephony.ITransportSelectorCallback;
import com.android.internal.telephony.ITransportSelectorResultCallback;
import com.android.internal.telephony.IWwanSelectorCallback;
import com.android.internal.telephony.IWwanSelectorResultCallback;
import com.android.internal.telephony.flags.Flags;
import com.android.internal.telephony.util.TelephonyUtils;
import com.android.telephony.Rlog;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.Executor;
import java.util.function.Consumer;

/**
 * Base domain selection implementation.
 * <p>
 * Services that extend {@link DomainSelectionService} must register the service in their
 * AndroidManifest.xml to be detected by the framework.
 * <p>
 * 1) The application must declare that they use the
 * android.permission.BIND_DOMAIN_SELECTION_SERVICE permission.
 * <p>
 * 2) The DomainSelectionService definition in the manifest must follow this format:
 * <pre>
 * {@code
 * ...
 * <service android:name=".EgDomainSelectionService"
 *    android:permission="android.permission.BIND_DOMAIN_SELECTION_SERVICE" >
 *    <intent-filter>
 *       <action android:name="android.telephony.DomainSelectionService" />
 *    </intent-filter>
 * </service>
 * ...
 * }
 * </pre>
 * <p>
 * The ComponentName corresponding to this DomainSelectionService component MUST also be set
 * as the system domain selection implementation in order to be bound.
 * The system domain selection implementation is set in the device overlay for
 * {@code config_domain_selection_service_component_name}
 * in {@code packages/services/Telephony/res/values/config.xml}.
 *
 * @hide
 */
@SystemApi
@FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
public abstract class DomainSelectionService extends Service {

    private static final String LOG_TAG = "DomainSelectionService";

    /**
     * The intent that must be defined as an intent-filter in the AndroidManifest of the
     * {@link DomainSelectionService}.
     *
     * @hide
     */
    public static final String SERVICE_INTERFACE = "android.telephony.DomainSelectionService";

    /** @hide */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef(prefix = "SELECTOR_TYPE_",
            value = {
                    SELECTOR_TYPE_CALLING,
                    SELECTOR_TYPE_SMS})
    public @interface SelectorType {}

    /** Indicates the domain selector type for calling. */
    public static final int SELECTOR_TYPE_CALLING = 1;
    /** Indicates the domain selector type for sms. */
    public static final int SELECTOR_TYPE_SMS = 2;

    /** Indicates that the modem can scan for emergency service as per modem’s implementation. */
    public static final int SCAN_TYPE_NO_PREFERENCE = 0;

    /** Indicates that the modem will scan for emergency service in limited service mode. */
    public static final int SCAN_TYPE_LIMITED_SERVICE = 1;

    /** Indicates that the modem will scan for emergency service in full service mode. */
    public static final int SCAN_TYPE_FULL_SERVICE = 2;

    /** @hide */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef(prefix = "SCAN_TYPE_",
            value = {
                    SCAN_TYPE_NO_PREFERENCE,
                    SCAN_TYPE_LIMITED_SERVICE,
                    SCAN_TYPE_FULL_SERVICE})
    public @interface EmergencyScanType {}

    /**
     * Contains attributes required to determine the domain for a telephony service.
     */
    @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
    public static final class SelectionAttributes implements Parcelable {

        private static final String TAG = "SelectionAttributes";

        private int mSlotIndex;
        private int mSubId;
        private @Nullable String mCallId;
        private @Nullable Uri mAddress;
        private @SelectorType int mSelectorType;
        private boolean mIsVideoCall;
        private boolean mIsEmergency;
        private boolean mIsTestEmergencyNumber;
        private boolean mIsExitedFromAirplaneMode;
        private @Nullable ImsReasonInfo mImsReasonInfo;
        private @PreciseDisconnectCauses int mCause;
        private @Nullable EmergencyRegistrationResult mEmergencyRegistrationResult;

        /**
         * @param slotIndex The logical slot index.
         * @param subscriptionId The subscription identifier.
         * @param callId The call identifier.
         * @param address The dialed address.
         * @param selectorType Indicates the requested domain selector type.
         * @param video Indicates it's a video call.
         * @param emergency Indicates it's emergency service.
         * @param isTest Indicates it's a test emergency number.
         * @param exited {@code true} if the request caused the device to move out of airplane mode.
         * @param imsReasonInfo The reason why the last PS attempt failed.
         * @param cause The reason why the last CS attempt failed.
         * @param regResult The current registration result for emergency services.
         */
        private SelectionAttributes(int slotIndex, int subscriptionId, @Nullable String callId,
                @Nullable Uri address, @SelectorType int selectorType,
                boolean video, boolean emergency, boolean isTest, boolean exited,
                @Nullable ImsReasonInfo imsReasonInfo, @PreciseDisconnectCauses int cause,
                @Nullable EmergencyRegistrationResult regResult) {
            mSlotIndex = slotIndex;
            mSubId = subscriptionId;
            mCallId = callId;
            mAddress = address;
            mSelectorType = selectorType;
            mIsVideoCall = video;
            mIsEmergency = emergency;
            mIsTestEmergencyNumber = isTest;
            mIsExitedFromAirplaneMode = exited;
            mImsReasonInfo = imsReasonInfo;
            mCause = cause;
            mEmergencyRegistrationResult = regResult;
        }

        /**
         * Copy constructor.
         *
         * @param s Source selection attributes.
         * @hide
         */
        public SelectionAttributes(@NonNull SelectionAttributes s) {
            mSlotIndex = s.mSlotIndex;
            mSubId = s.mSubId;
            mCallId = s.mCallId;
            mAddress = s.mAddress;
            mSelectorType = s.mSelectorType;
            mIsEmergency = s.mIsEmergency;
            mIsTestEmergencyNumber = s.mIsTestEmergencyNumber;
            mIsExitedFromAirplaneMode = s.mIsExitedFromAirplaneMode;
            mImsReasonInfo = s.mImsReasonInfo;
            mCause = s.mCause;
            mEmergencyRegistrationResult = s.mEmergencyRegistrationResult;
        }

        /**
         * Constructs a SelectionAttributes object from the given parcel.
         */
        private SelectionAttributes(@NonNull Parcel in) {
            readFromParcel(in);
        }

        /**
         * @return The logical slot index.
         */
        public int getSlotIndex() {
            return mSlotIndex;
        }

        /**
         * @return The subscription identifier.
         */
        public int getSubscriptionId() {
            return mSubId;
        }

        /**
         * @return The call identifier.
         */
        public @Nullable String getCallId() {
            return mCallId;
        }

        /**
         * @return The dialed address.
         */
        public @Nullable Uri getAddress() {
            return mAddress;
        }

        /**
         * @return The domain selector type.
         */
        public @SelectorType int getSelectorType() {
            return mSelectorType;
        }

        /**
         * @return {@code true} if the request is for a video call.
         */
        public boolean isVideoCall() {
            return mIsVideoCall;
        }

        /**
         * @return {@code true} if the request is for emergency services.
         */
        public boolean isEmergency() {
            return mIsEmergency;
        }

        /**
         * @return {@code true} if the dialed number is a test emergency number.
         */
        public boolean isTestEmergencyNumber() {
            return mIsTestEmergencyNumber;
        }

        /**
         * @return {@code true} if the request caused the device to move out of airplane mode.
         */
        public boolean isExitedFromAirplaneMode() {
            return mIsExitedFromAirplaneMode;
        }

        /**
         * @return The PS disconnect cause if trying over PS resulted in a failure and
         *         reselection is required.
         */
        public @Nullable ImsReasonInfo getPsDisconnectCause() {
            return mImsReasonInfo;
        }

        /**
         * @return The CS disconnect cause if trying over CS resulted in a failure and
         *         reselection is required.
         */
        public @PreciseDisconnectCauses int getCsDisconnectCause() {
            return mCause;
        }

        /**
         * @return The current registration state of cellular network.
         */
        public @Nullable EmergencyRegistrationResult getEmergencyRegistrationResult() {
            return mEmergencyRegistrationResult;
        }

        @Override
        public @NonNull String toString() {
            return "{ slotIndex=" + mSlotIndex
                    + ", subId=" + mSubId
                    + ", callId=" + mCallId
                    + ", address=" + (Build.IS_DEBUGGABLE ? mAddress : "***")
                    + ", type=" + mSelectorType
                    + ", videoCall=" + mIsVideoCall
                    + ", emergency=" + mIsEmergency
                    + ", isTest=" + mIsTestEmergencyNumber
                    + ", airplaneMode=" + mIsExitedFromAirplaneMode
                    + ", reasonInfo=" + mImsReasonInfo
                    + ", cause=" + mCause
                    + ", regResult=" + mEmergencyRegistrationResult
                    + " }";
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            SelectionAttributes that = (SelectionAttributes) o;
            return mSlotIndex == that.mSlotIndex && mSubId == that.mSubId
                    && TextUtils.equals(mCallId, that.mCallId)
                    && equalsHandlesNulls(mAddress, that.mAddress)
                    && mSelectorType == that.mSelectorType && mIsVideoCall == that.mIsVideoCall
                    && mIsEmergency == that.mIsEmergency
                    && mIsTestEmergencyNumber == that.mIsTestEmergencyNumber
                    && mIsExitedFromAirplaneMode == that.mIsExitedFromAirplaneMode
                    && equalsHandlesNulls(mImsReasonInfo, that.mImsReasonInfo)
                    && mCause == that.mCause
                    && equalsHandlesNulls(mEmergencyRegistrationResult,
                            that.mEmergencyRegistrationResult);
        }

        @Override
        public int hashCode() {
            return Objects.hash(mCallId, mAddress, mImsReasonInfo,
                    mIsVideoCall, mIsEmergency, mIsTestEmergencyNumber, mIsExitedFromAirplaneMode,
                    mEmergencyRegistrationResult, mSlotIndex, mSubId, mSelectorType, mCause);
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(@NonNull Parcel out, int flags) {
            out.writeInt(mSlotIndex);
            out.writeInt(mSubId);
            out.writeString8(mCallId);
            out.writeParcelable(mAddress, 0);
            out.writeInt(mSelectorType);
            out.writeBoolean(mIsVideoCall);
            out.writeBoolean(mIsEmergency);
            out.writeBoolean(mIsTestEmergencyNumber);
            out.writeBoolean(mIsExitedFromAirplaneMode);
            out.writeParcelable(mImsReasonInfo, 0);
            out.writeInt(mCause);
            out.writeParcelable(mEmergencyRegistrationResult, 0);
        }

        private void readFromParcel(@NonNull Parcel in) {
            mSlotIndex = in.readInt();
            mSubId = in.readInt();
            mCallId = in.readString8();
            mAddress = in.readParcelable(Uri.class.getClassLoader(),
                    android.net.Uri.class);
            mSelectorType = in.readInt();
            mIsVideoCall = in.readBoolean();
            mIsEmergency = in.readBoolean();
            mIsTestEmergencyNumber = in.readBoolean();
            mIsExitedFromAirplaneMode = in.readBoolean();
            mImsReasonInfo = in.readParcelable(ImsReasonInfo.class.getClassLoader(),
                    android.telephony.ims.ImsReasonInfo.class);
            mCause = in.readInt();
            mEmergencyRegistrationResult = in.readParcelable(
                    EmergencyRegistrationResult.class.getClassLoader(),
                    EmergencyRegistrationResult.class);
        }

        public static final @NonNull Creator<SelectionAttributes> CREATOR =
                new Creator<SelectionAttributes>() {
            @Override
            public SelectionAttributes createFromParcel(@NonNull Parcel in) {
                return new SelectionAttributes(in);
            }

            @Override
            public SelectionAttributes[] newArray(int size) {
                return new SelectionAttributes[size];
            }
        };

        private static boolean equalsHandlesNulls(Object a, Object b) {
            return (a == null) ? (b == null) : a.equals(b);
        }

        /**
         * Builder class creating a new instance.
         */
        @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE)
        public static final class Builder {
            private final int mSlotIndex;
            private final int mSubId;
            private @Nullable String mCallId;
            private @Nullable Uri mAddress;
            private final @SelectorType int mSelectorType;
            private boolean mIsVideoCall;
            private boolean mIsEmergency;
            private boolean mIsTestEmergencyNumber;
            private boolean mIsExitedFromAirplaneMode;
            private @Nullable ImsReasonInfo mImsReasonInfo;
            private @PreciseDisconnectCauses int mCause;
            private @Nullable EmergencyRegistrationResult mEmergencyRegistrationResult;

            /**
             * Default constructor for Builder.
             */
            public Builder(int slotIndex, int subscriptionId, @SelectorType int selectorType) {
                mSlotIndex = slotIndex;
                mSubId = subscriptionId;
                mSelectorType = selectorType;
            }

            /**
             * Sets the call identifier.
             *
             * @param callId The call identifier.
             * @return The same instance of the builder.
             */
            public @NonNull Builder setCallId(@Nullable String callId) {
                mCallId = callId;
                return this;
            }

            /**
             * Sets the dialed address.
             *
             * @param address The dialed address.
             * @return The same instance of the builder.
             */
            public @NonNull Builder setAddress(@Nullable Uri address) {
                mAddress = address;
                return this;
            }

            /**
             * Sets whether it's a video call or not.
             *
             * @param isVideo Indicates it's a video call.
             * @return The same instance of the builder.
             */
            public @NonNull Builder setVideoCall(boolean isVideo) {
                mIsVideoCall = isVideo;
                return this;
            }

            /**
             * Sets whether it's an emergency service or not.
             *
             * @param isEmergency Indicates it's emergency service.
             * @return The same instance of the builder.
             */
            public @NonNull Builder setEmergency(boolean isEmergency) {
                mIsEmergency = isEmergency;
                return this;
            }

            /**
             * Sets whether it's a test emergency number or not.
             *
             * @param isTest Indicates it's a test emergency number.
             * @return The same instance of the builder.
             */
            public @NonNull Builder setTestEmergencyNumber(boolean isTest) {
                mIsTestEmergencyNumber = isTest;
                return this;
            }

            /**
             * Sets whether the request caused the device to move out of airplane mode.
             *
             * @param exited {@code true} if the request caused the device to move out of
             *        airplane mode.
             * @return The same instance of the builder.
             */
            public @NonNull Builder setExitedFromAirplaneMode(boolean exited) {
                mIsExitedFromAirplaneMode = exited;
                return this;
            }

            /**
             * Sets an optional reason why the last PS attempt failed.
             *
             * @param info The reason why the last PS attempt failed.
             * @return The same instance of the builder.
             */
            public @NonNull Builder setPsDisconnectCause(@Nullable ImsReasonInfo info) {
                mImsReasonInfo = info;
                return this;
            }

            /**
             * Sets an optional reason why the last CS attempt failed.
             *
             * @param cause The reason why the last CS attempt failed.
             * @return The same instance of the builder.
             */
            public @NonNull Builder setCsDisconnectCause(@PreciseDisconnectCauses int cause) {
                mCause = cause;
                return this;
            }

            /**
             * Sets the current registration result for emergency services.
             *
             * @param regResult The current registration result for emergency services.
             * @return The same instance of the builder.
             */
            public @NonNull Builder setEmergencyRegistrationResult(
                    @Nullable EmergencyRegistrationResult regResult) {
                mEmergencyRegistrationResult = regResult;
                return this;
            }

            /**
             * Build the SelectionAttributes.
             * @return The SelectionAttributes object.
             */
            public @NonNull SelectionAttributes build() {
                return new SelectionAttributes(mSlotIndex, mSubId, mCallId, mAddress,
                        mSelectorType, mIsVideoCall, mIsEmergency, mIsTestEmergencyNumber,
                        mIsExitedFromAirplaneMode, mImsReasonInfo,
                        mCause, mEmergencyRegistrationResult);
            }
        }
    }

    /**
     * A wrapper class for ITransportSelectorCallback interface.
     */
    private final class TransportSelectorCallbackWrapper implements TransportSelectorCallback {
        private static final String TAG = "TransportSelectorCallbackWrapper";

        private final @NonNull ITransportSelectorCallback mCallback;
        private final @NonNull Executor mExecutor;

        private @Nullable ITransportSelectorResultCallbackAdapter mResultCallback;
        private @Nullable DomainSelectorWrapper mSelectorWrapper;

        TransportSelectorCallbackWrapper(@NonNull ITransportSelectorCallback cb,
                @NonNull Executor executor) {
            mCallback = cb;
            mExecutor = executor;
        }

        @Override
        public void onCreated(@NonNull DomainSelector selector) {
            try {
                mSelectorWrapper = new DomainSelectorWrapper(selector, mExecutor);
                mCallback.onCreated(mSelectorWrapper.getCallbackBinder());
            } catch (Exception e) {
                Rlog.e(TAG, "onCreated e=" + e);
            }
        }

        @Override
        public void onWlanSelected(boolean useEmergencyPdn) {
            try {
                mCallback.onWlanSelected(useEmergencyPdn);
            } catch (Exception e) {
                Rlog.e(TAG, "onWlanSelected e=" + e);
            }
        }

        @Override
        public void onWwanSelected(Consumer<WwanSelectorCallback> consumer) {
            try {
                mResultCallback = new ITransportSelectorResultCallbackAdapter(consumer, mExecutor);
                mCallback.onWwanSelectedAsync(mResultCallback);
            } catch (Exception e) {
                Rlog.e(TAG, "onWwanSelected e=" + e);
                executeMethodAsyncNoException(mExecutor,
                        () -> consumer.accept(null), TAG, "onWwanSelectedAsync-Exception");
            }
        }

        @Override
        public void onSelectionTerminated(@DisconnectCauses int cause) {
            try {
                mCallback.onSelectionTerminated(cause);
                mSelectorWrapper = null;
            } catch (Exception e) {
                Rlog.e(TAG, "onSelectionTerminated e=" + e);
            }
        }

        private class ITransportSelectorResultCallbackAdapter
                extends ITransportSelectorResultCallback.Stub {
            private final @NonNull Consumer<WwanSelectorCallback> mConsumer;
            private final @NonNull Executor mExecutor;

            ITransportSelectorResultCallbackAdapter(
                    @NonNull Consumer<WwanSelectorCallback> consumer,
                    @NonNull Executor executor) {
                mConsumer = consumer;
                mExecutor = executor;
            }

            @Override
            public void onCompleted(@NonNull IWwanSelectorCallback cb) {
                if (mConsumer == null) return;

                WwanSelectorCallback callback = new WwanSelectorCallbackWrapper(cb, mExecutor);
                executeMethodAsyncNoException(mExecutor,
                        () -> mConsumer.accept(callback), TAG, "onWwanSelectedAsync-Completed");
            }
        }
    }

    /**
     * A wrapper class for IDomainSelector interface.
     */
    private final class DomainSelectorWrapper {
        private static final String TAG = "DomainSelectorWrapper";

        private @NonNull IDomainSelector mCallbackBinder;

        DomainSelectorWrapper(@NonNull DomainSelector cb, @NonNull Executor executor) {
            mCallbackBinder = new IDomainSelectorAdapter(cb, executor);
        }

        private class IDomainSelectorAdapter extends IDomainSelector.Stub {
            private final @NonNull WeakReference<DomainSelector> mDomainSelectorWeakRef;
            private final @NonNull Executor mExecutor;

            IDomainSelectorAdapter(@NonNull DomainSelector domainSelector,
                    @NonNull Executor executor) {
                mDomainSelectorWeakRef =
                        new WeakReference<DomainSelector>(domainSelector);
                mExecutor = executor;
            }

            @Override
            public void reselectDomain(@NonNull SelectionAttributes attr) {
                final DomainSelector domainSelector = mDomainSelectorWeakRef.get();
                if (domainSelector == null) return;

                executeMethodAsyncNoException(mExecutor,
                        () -> domainSelector.reselectDomain(attr), TAG, "reselectDomain");
            }

            @Override
            public void finishSelection() {
                final DomainSelector domainSelector = mDomainSelectorWeakRef.get();
                if (domainSelector == null) return;

                executeMethodAsyncNoException(mExecutor,
                        () -> domainSelector.finishSelection(), TAG, "finishSelection");
            }
        }

        public @NonNull IDomainSelector getCallbackBinder() {
            return mCallbackBinder;
        }
    }

    /**
     * A wrapper class for IWwanSelectorCallback and IWwanSelectorResultCallback.
     */
    private final class WwanSelectorCallbackWrapper
            implements WwanSelectorCallback, CancellationSignal.OnCancelListener {
        private static final String TAG = "WwanSelectorCallbackWrapper";

        private final @NonNull IWwanSelectorCallback mCallback;
        private final @NonNull Executor mExecutor;

        private @Nullable IWwanSelectorResultCallbackAdapter mResultCallback;

        WwanSelectorCallbackWrapper(@NonNull IWwanSelectorCallback cb,
                @NonNull Executor executor) {
            mCallback = cb;
            mExecutor = executor;
        }

        @Override
        public void onCancel() {
            try {
                mCallback.onCancel();
            } catch (Exception e) {
                Rlog.e(TAG, "onCancel e=" + e);
            }
        }

        @Override
        public void onRequestEmergencyNetworkScan(@NonNull List<Integer> preferredNetworks,
                @EmergencyScanType int scanType, boolean resetScan,
                @NonNull CancellationSignal signal,
                @NonNull Consumer<EmergencyRegistrationResult> consumer) {
            try {
                if (signal != null) signal.setOnCancelListener(this);
                mResultCallback = new IWwanSelectorResultCallbackAdapter(consumer, mExecutor);
                mCallback.onRequestEmergencyNetworkScan(
                        preferredNetworks.stream().mapToInt(Integer::intValue).toArray(),
                        scanType, resetScan, mResultCallback);
            } catch (Exception e) {
                Rlog.e(TAG, "onRequestEmergencyNetworkScan e=" + e);
            }
        }

        @Override
        public void onDomainSelected(@NetworkRegistrationInfo.Domain int domain,
                boolean useEmergencyPdn) {
            try {
                mCallback.onDomainSelected(domain, useEmergencyPdn);
            } catch (Exception e) {
                Rlog.e(TAG, "onDomainSelected e=" + e);
            }
        }

        private class IWwanSelectorResultCallbackAdapter
                extends IWwanSelectorResultCallback.Stub {
            private final @NonNull Consumer<EmergencyRegistrationResult> mConsumer;
            private final @NonNull Executor mExecutor;

            IWwanSelectorResultCallbackAdapter(
                    @NonNull Consumer<EmergencyRegistrationResult> consumer,
                    @NonNull Executor executor) {
                mConsumer = consumer;
                mExecutor = executor;
            }

            @Override
            public void onComplete(@NonNull EmergencyRegistrationResult result) {
                if (mConsumer == null) return;

                executeMethodAsyncNoException(mExecutor,
                        () -> mConsumer.accept(result), TAG, "onScanComplete");
            }
        }
    }

    private final Object mExecutorLock = new Object();

    /** Executor used to execute methods called remotely by the framework. */
    private @NonNull Executor mExecutor;

    /**
     * Selects a calling domain given the SelectionAttributes of the call request.
     * <p>
     * When the framework generates a request to place a call, {@link #onDomainSelection}
     * will be called in order to determine the domain (CS or PS). For PS calls, the transport
     * (WWAN or WLAN) will also need to be determined.
     * <p>
     * Once the domain/transport has been selected or an error has occurred,
     * {@link TransportSelectorCallback} must be used to communicate the result back
     * to the framework.
     *
     * @param attr Required to determine the domain.
     * @param callback The callback instance being registered.
     */
    public abstract void onDomainSelection(@NonNull SelectionAttributes attr,
            @NonNull TransportSelectorCallback callback);

    /**
     * Notifies the change in {@link ServiceState} for a specific logical slot index.
     *
     * @param slotIndex For which the state changed.
     * @param subscriptionId For which the state changed.
     * @param serviceState Updated {@link ServiceState}.
     */
    public void onServiceStateUpdated(int slotIndex, int subscriptionId,
            @NonNull ServiceState serviceState) {
    }

    /**
     * Notifies the change in {@link BarringInfo} for a specific logical slot index.
     *
     * @param slotIndex For which the state changed.
     * @param subscriptionId For which the state changed.
     * @param info Updated {@link BarringInfo}.
     */
    public void onBarringInfoUpdated(int slotIndex, int subscriptionId, @NonNull BarringInfo info) {
    }

    private final IBinder mDomainSelectionServiceController =
            new IDomainSelectionServiceController.Stub() {
        @Override
        public void selectDomain(@NonNull SelectionAttributes attr,
                @NonNull ITransportSelectorCallback callback)  throws RemoteException {
            executeMethodAsync(getCachedExecutor(),
                    () -> DomainSelectionService.this.onDomainSelection(attr,
                            new TransportSelectorCallbackWrapper(callback, getCachedExecutor())),
                    LOG_TAG, "onDomainSelection");
        }

        @Override
        public void updateServiceState(int slotIndex, int subscriptionId,
                @NonNull ServiceState serviceState) {
            executeMethodAsyncNoException(getCachedExecutor(),
                    () -> DomainSelectionService.this.onServiceStateUpdated(slotIndex,
                            subscriptionId, serviceState), LOG_TAG, "onServiceStateUpdated");
        }

        @Override
        public void updateBarringInfo(int slotIndex, int subscriptionId,
                @NonNull BarringInfo info) {
            executeMethodAsyncNoException(getCachedExecutor(),
                    () -> DomainSelectionService.this.onBarringInfoUpdated(slotIndex,
                    subscriptionId, info),
                    LOG_TAG, "onBarringInfoUpdated");
        }
    };

    private static void executeMethodAsync(@NonNull Executor executor, @NonNull Runnable r,
            @NonNull String tag, @NonNull String errorLogName) throws RemoteException {
        try {
            CompletableFuture.runAsync(
                    () -> TelephonyUtils.runWithCleanCallingIdentity(r), executor).join();
        } catch (CancellationException | CompletionException e) {
            Rlog.w(tag, "Binder - " + errorLogName + " exception: " + e.getMessage());
            throw new RemoteException(e.getMessage());
        }
    }

    private void executeMethodAsyncNoException(@NonNull Executor executor, @NonNull Runnable r,
            @NonNull String tag, @NonNull String errorLogName) {
        try {
            CompletableFuture.runAsync(
                    () -> TelephonyUtils.runWithCleanCallingIdentity(r), executor);
        } catch (CancellationException | CompletionException e) {
            Rlog.w(tag, "Binder - " + errorLogName + " exception: " + e.getMessage());
        }
    }

    /** @hide */
    @Override
    public final @Nullable IBinder onBind(@Nullable Intent intent) {
        if (intent == null) return null;
        if (SERVICE_INTERFACE.equals(intent.getAction())) {
            Log.i(LOG_TAG, "DomainSelectionService Bound.");
            return mDomainSelectionServiceController;
        }
        return null;
    }

    /**
     * The Executor to use when calling callback methods from the framework.
     * <p>
     * By default, calls from the framework will use Binder threads to call these methods.
     *
     * @return an {@link Executor} used to execute methods called remotely by the framework.
     */
    @SuppressLint("OnNameExpected")
    public @NonNull Executor getCreateExecutor() {
        return Runnable::run;
    }

    /**
     * Gets the {@link Executor} which executes methods of this service.
     * This method should be private when this service is implemented in a separated process
     * other than telephony framework.
     * @return {@link Executor} instance.
     * @hide
     */
    public final @NonNull Executor getCachedExecutor() {
        synchronized (mExecutorLock) {
            if (mExecutor == null) {
                Executor e = getCreateExecutor();
                mExecutor = (e != null) ? e : Runnable::run;
            }
            return mExecutor;
        }
    }

    /**
     * Returns a string representation of the domain.
     * @param domain The domain.
     * @return The name of the domain.
     * @hide
     */
    public static @NonNull String getDomainName(@NetworkRegistrationInfo.Domain int domain) {
        return NetworkRegistrationInfo.domainToString(domain);
    }
}
