/*
 * Copyright 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 android.uwb;

import android.Manifest;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.os.Binder;
import android.os.Build;
import android.os.PersistableBundle;
import android.os.RemoteException;
import android.util.Log;

import androidx.annotation.RequiresApi;

import com.android.modules.utils.build.SdkLevel;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.concurrent.Executor;

/**
 * This class provides a way to control an active UWB ranging session.
 * <p>It also defines the required {@link RangingSession.Callback} that must be implemented
 * in order to be notified of UWB ranging results and status events related to the
 * {@link RangingSession}.
 *
 * <p>To get an instance of {@link RangingSession}, first use
 * {@link UwbManager#openRangingSession(PersistableBundle, Executor, Callback)} to request to open a
 * session. Once the session is opened, a {@link RangingSession} object is provided through
 * {@link RangingSession.Callback#onOpened(RangingSession)}. If opening a session fails, the failure
 * is reported through {@link RangingSession.Callback#onOpenFailed(int, PersistableBundle)} with the
 * failure reason.
 *
 * @hide
 */
@SystemApi
public final class RangingSession implements AutoCloseable {
    private final String mTag = "Uwb.RangingSession[" + this + "]";
    private final SessionHandle mSessionHandle;
    private final IUwbAdapter mAdapter;
    private final Executor mExecutor;
    private final Callback mCallback;
    private final String mChipId;

    private enum State {
        /**
         * The state of the {@link RangingSession} until
         * {@link RangingSession.Callback#onOpened(RangingSession)} is invoked
         */
        INIT,

        /**
         * The {@link RangingSession} is initialized and ready to begin ranging
         */
        IDLE,

        /**
         * The {@link RangingSession} is actively ranging
         */
        ACTIVE,

        /**
         * The {@link RangingSession} is closed and may not be used for ranging.
         */
        CLOSED
    }

    private State mState = State.INIT;

    /**
     * Interface for receiving {@link RangingSession} events
     */
    public interface Callback {
        /**
         * @hide
         */
        @Retention(RetentionPolicy.SOURCE)
        @IntDef(value = {
                REASON_UNKNOWN,
                REASON_LOCAL_REQUEST,
                REASON_REMOTE_REQUEST,
                REASON_BAD_PARAMETERS,
                REASON_GENERIC_ERROR,
                REASON_MAX_SESSIONS_REACHED,
                REASON_SYSTEM_POLICY,
                REASON_PROTOCOL_SPECIFIC_ERROR,
                REASON_MAX_RR_RETRY_REACHED,
                REASON_SERVICE_DISCOVERY_FAILURE,
                REASON_SERVICE_CONNECTION_FAILURE,
                REASON_SE_NOT_SUPPORTED,
                REASON_SE_INTERACTION_FAILURE,
                REASON_INSUFFICIENT_SLOTS_PER_RR,
                REASON_SYSTEM_REGULATION,
                REASON_INBAND_SESSION_STOP,
        })
        @interface Reason {}

        /**
         * Indicates that the session was closed or failed to open due to an unknown reason
         */
        int REASON_UNKNOWN = 0;

        /**
         * Indicates that the session was closed or failed to open because
         * {@link AutoCloseable#close()} or {@link RangingSession#close()} was called
         */
        int REASON_LOCAL_REQUEST = 1;

        /**
         * Indicates that the session was closed or failed to open due to an explicit request from
         * the remote device.
         */
        int REASON_REMOTE_REQUEST = 2;

        /**
         * Indicates that the session was closed or failed to open due to erroneous parameters
         */
        int REASON_BAD_PARAMETERS = 3;

        /**
         * Indicates an error on this device besides the error code already listed
         */
        int REASON_GENERIC_ERROR = 4;

        /**
         * Indicates that the number of currently open sessions supported by the device and
         * additional sessions may not be opened.
         */
        int REASON_MAX_SESSIONS_REACHED = 5;

        /**
         * Indicates that the local system policy caused the change, such
         * as privacy policy, power management policy, permissions, and more.
         */
        int REASON_SYSTEM_POLICY = 6;

        /**
         * Indicates a protocol specific error. The associated {@link PersistableBundle} should be
         * consulted for additional information.
         */
        int REASON_PROTOCOL_SPECIFIC_ERROR = 7;

        /**
         * Indicates that the max number of retry attempts for a ranging attempt has been reached.
         */
        int REASON_MAX_RR_RETRY_REACHED = 9;

        /**
         * Indicates a failure to discover the service after activation.
         */
        int REASON_SERVICE_DISCOVERY_FAILURE = 10;

        /**
         * Indicates a failure to connect to the service after discovery.
         */
        int REASON_SERVICE_CONNECTION_FAILURE = 11;

        /**
         * The device doesn’t support FiRA Applet.
         */
        int REASON_SE_NOT_SUPPORTED = 12;

        /**
         * SE interactions failed.
         */
        int REASON_SE_INTERACTION_FAILURE = 13;

        /**
         * Indicate insufficient slots per ranging round.
         */
        int REASON_INSUFFICIENT_SLOTS_PER_RR = 14;

        /**
         * Indicate that a system regulation caused the change, such as no allowed UWB channels in
         * the country.
         */
        int REASON_SYSTEM_REGULATION = 15;

        /**
         * Indicates session was stopped due to inband signal.
         */
        @FlaggedApi("com.android.uwb.flags.reason_inband_session_stop")
        int REASON_INBAND_SESSION_STOP = 16;

        /**
         * @hide
         */
        @Retention(RetentionPolicy.SOURCE)
        @IntDef(value = {
                CONTROLEE_FAILURE_REASON_MAX_CONTROLEE_REACHED,
        })
        @interface ControleeFailureReason {}

        /**
         * Indicates that the session has reached the max number of controlees supported by the
         * device. This is applicable to only one to many sessions and sent in response to a
         * request to add a new controlee to an ongoing session.
         */
        int CONTROLEE_FAILURE_REASON_MAX_CONTROLEE_REACHED = 0;

        /**
         * @hide
         */
        @Retention(RetentionPolicy.SOURCE)
        @IntDef(value = {
                DATA_FAILURE_REASON_DATA_SIZE_TOO_LARGE,
        })
        @interface DataFailureReason {}

        /**
         * Indicates that the size of the data being sent or received is too large.
         */
        int DATA_FAILURE_REASON_DATA_SIZE_TOO_LARGE = 10;

        /**
         * Invoked when {@link UwbManager#openRangingSession(PersistableBundle, Executor, Callback)}
         * is successful
         *
         * @param session the newly opened {@link RangingSession}
         */
        void onOpened(@NonNull RangingSession session);

        /**
         * Invoked if {@link UwbManager#openRangingSession(PersistableBundle, Executor, Callback)}}
         * fails
         *
         * @param reason the failure reason
         * @param params protocol specific parameters
         */
        void onOpenFailed(@Reason int reason, @NonNull PersistableBundle params);

        /**
         * Invoked either,
         *  - when {@link RangingSession#start(PersistableBundle)} is successful if the session is
         *    using a custom profile, OR
         *  - when platform starts ranging after OOB discovery + negotiation if the session is
         *    using a platform defined profile.
         * @param sessionInfo session specific parameters from the lower layers
         */
        void onStarted(@NonNull PersistableBundle sessionInfo);

        /**
         * Invoked either,
         *   - when {@link RangingSession#start(PersistableBundle)} fails if
         *     the session is using a custom profile, OR
         *   - when platform fails ranging after OOB discovery + negotiation if the
         *     session is using a platform defined profile.
         *
         * @param reason the failure reason
         * @param params protocol specific parameters
         */
        void onStartFailed(@Reason int reason, @NonNull PersistableBundle params);

        /**
         * Invoked when a request to reconfigure the session succeeds
         *
         * @param params the updated ranging configuration
         */
        void onReconfigured(@NonNull PersistableBundle params);

        /**
         * Invoked when a request to reconfigure the session fails
         *
         * @param reason reason the session failed to be reconfigured
         * @param params protocol specific failure reasons
         */
        void onReconfigureFailed(@Reason int reason, @NonNull PersistableBundle params);

        /**
         * Invoked when a request to stop the session succeeds
         *
         * @param reason reason for the session stop
         * @param parameters protocol specific parameters related to the stop reason
         */
        void onStopped(@Reason int reason, @NonNull PersistableBundle parameters);

        /**
         * Invoked when a request to stop the session fails
         *
         * @param reason reason the session failed to be stopped
         * @param params protocol specific failure reasons
         */
        void onStopFailed(@Reason int reason, @NonNull PersistableBundle params);

       /**
         * Invoked when session is either closed spontaneously, or per user request via
         * {@link RangingSession#close()} or {@link AutoCloseable#close()}.
         *
         * @param reason reason for the session closure
         * @param parameters protocol specific parameters related to the close reason
         */
        void onClosed(@Reason int reason, @NonNull PersistableBundle parameters);

        /**
         * Called once per ranging interval even when a ranging measurement fails
         *
         * @param rangingReport ranging report for this interval's measurements
         */
        void onReportReceived(@NonNull RangingReport rangingReport);

        /**
         * Invoked when a new controlee is added to an ongoing one-to many session.
         *
         * @param parameters protocol specific parameters for the new controlee
         */
        default void onControleeAdded(@NonNull PersistableBundle parameters) {}

        /**
         * Invoked when a new controlee is added to an ongoing one-to many session.
         *
         * @param reason reason for the controlee add failure
         * @param parameters protocol specific parameters related to the failure
         */
        default void onControleeAddFailed(
                @ControleeFailureReason int reason, @NonNull PersistableBundle parameters) {}

        /**
         * Invoked when an existing controlee is removed from an ongoing one-to many session.
         *
         * @param parameters protocol specific parameters for the existing controlee
         */
        default void onControleeRemoved(@NonNull PersistableBundle parameters) {}

        /**
         * Invoked when a new controlee is added to an ongoing one-to many session.
         *
         * @param reason reason for the controlee remove failure
         * @param parameters protocol specific parameters related to the failure
         */
        default void onControleeRemoveFailed(
                @ControleeFailureReason int reason, @NonNull PersistableBundle parameters) {}

        /**
         * Invoked when an ongoing session is successfully pauseed.
         *
         * @param parameters protocol specific parameters sent for suspension
         */
        default void onPaused(@NonNull PersistableBundle parameters) {}

        /**
         * Invoked when an ongoing session suspension fails.
         *
         * @param reason reason for the suspension failure
         * @param parameters protocol specific parameters for suspension failure
         */
        default void onPauseFailed(@Reason int reason, @NonNull PersistableBundle parameters) {}

        /**
         * Invoked when a pauseed session is successfully resumed.
         *
         * @param parameters protocol specific parameters sent for suspension
         */
        default void onResumed(@NonNull PersistableBundle parameters) {}

        /**
         * Invoked when a pauseed session resumption fails.
         *
         * @param reason reason for the resumption failure
         * @param parameters protocol specific parameters for resumption failure
         */
        default void onResumeFailed(@Reason int reason, @NonNull PersistableBundle parameters) {}

        /**
         * Invoked when data is successfully sent via {@link RangingSession#sendData(UwbAddress,
         * PersistableBundle, byte[])}.
         *
         * @param remoteDeviceAddress remote device's address
         * @param parameters protocol specific parameters sent for suspension
         */
        default void onDataSent(@NonNull UwbAddress remoteDeviceAddress,
                @NonNull PersistableBundle parameters) {}

        /**
         * Invoked when data send to a remote device via {@link RangingSession#sendData(UwbAddress,
         * PersistableBundle, byte[])} fails.
         *
         * @param remoteDeviceAddress remote device's address
         * @param reason reason for the resumption failure
         * @param parameters protocol specific parameters for resumption failure
         */
        default void onDataSendFailed(@NonNull UwbAddress remoteDeviceAddress,
                @DataFailureReason int reason, @NonNull PersistableBundle parameters) {}

        /**
         * Invoked when data is received successfully from a remote device.
         * The data is received piggybacked over RRM (initiator -> responder) or
         * RIM (responder -> initiator).
         * <p> This is only functional on a FIRA 2.0 compliant device.
         *
         * @param remoteDeviceAddress remote device's address
         * @param data Raw data received
         * @param parameters protocol specific parameters for the received data
         */
        default void onDataReceived(@NonNull UwbAddress remoteDeviceAddress,
                @NonNull PersistableBundle parameters, @NonNull byte[] data) {}

        /**
         * Invoked when data receive from a remote device fails.
         *
         * @param remoteDeviceAddress remote device's address
         * @param reason reason for the reception failure
         * @param parameters protocol specific parameters for resumption failure
         */
        default void onDataReceiveFailed(@NonNull UwbAddress remoteDeviceAddress,
                @DataFailureReason int reason, @NonNull PersistableBundle parameters) {}

        /**
         * Invoked when set data transfer phase config is called successfully
         *
         * @param parameters protocol specific parameters for set data transfer phase config success
         */
        @FlaggedApi("com.android.uwb.flags.data_transfer_phase_config")
        default void onDataTransferPhaseConfigured(@NonNull PersistableBundle parameters) {}

        /**
         * Invoked when set data transfer phase config is failed
         *
         * @param parameters protocol specific parameters for set data transfer phase config failure
         */
        @FlaggedApi("com.android.uwb.flags.data_transfer_phase_config")
        default void onDataTransferPhaseConfigFailed(@Reason int reason,
                @NonNull PersistableBundle parameters) {}

        /**
         * Invoked when service is discovered via OOB.
         * <p>
         * If this a one to many session, this can be invoked multiple times to indicate different
         * peers being discovered.
         * </p>
         *
         * @param parameters protocol specific params for discovered service.
         */
        default void onServiceDiscovered(@NonNull PersistableBundle parameters) {}

        /**
         * Invoked when service is connected via OOB.
         * <p>
         * If this a one to many session, this can be invoked multiple times to indicate different
         * peers being connected.
         * </p>
         *
         * @param parameters protocol specific params for connected service.
         */
        default void onServiceConnected(@NonNull PersistableBundle parameters) {}

        /**
         * Invoked when a response/status is received for active ranging rounds update.
         *
         * @param parameters bundle of ranging rounds update status
         */
        @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
        default void onRangingRoundsUpdateDtTagStatus(@NonNull PersistableBundle parameters) {}

        /**
         * Invoked when hybrid session controller is successfully configured.
         *
         * @param parameters protocol specific parameters sent for HUS session configuration
         */
        @FlaggedApi("com.android.uwb.flags.hybrid_session_support")
        default void onHybridSessionControllerConfigured(
                @NonNull PersistableBundle parameters) {}

        /**
         * Invoked when hybrid session controller configuration fails.
         *
         * @param parameters protocol specific parameters for configuration failure
         */
        @FlaggedApi("com.android.uwb.flags.hybrid_session_support")
        default void onHybridSessionControllerConfigurationFailed(
                @RangingChangeReason int reason, @NonNull PersistableBundle parameters) {
        }

        /**
         * Invoked when hybrid session controlee is successfully configured.
         *
         * @param parameters protocol specific parameters sent for HUS session configuration
         */
        @FlaggedApi("com.android.uwb.flags.hybrid_session_support")
        default void onHybridSessionControleeConfigured(
                @NonNull PersistableBundle parameters) {}

        /**
         *Invoked when hybrid session controlee configuration fails.
         *
         * @param parameters protocol specific parameters for configuration failure
         */
        @FlaggedApi("com.android.uwb.flags.hybrid_session_support")
        default void onHybridSessionControleeConfigurationFailed(
                @RangingChangeReason int reason, @NonNull PersistableBundle parameters) {}
    }

    /**
     * @hide
     */
    public RangingSession(Executor executor, Callback callback, IUwbAdapter adapter,
            SessionHandle sessionHandle) {
        this(executor, callback, adapter, sessionHandle, /* chipId= */ null);
    }

    /**
     * @hide
     */
    public RangingSession(Executor executor, Callback callback, IUwbAdapter adapter,
            SessionHandle sessionHandle, String chipId) {
        mState = State.INIT;
        mExecutor = executor;
        mCallback = callback;
        mAdapter = adapter;
        mSessionHandle = sessionHandle;
        mChipId = chipId;
    }

    /**
     * @hide
     */
    public boolean isOpen() {
        return mState == State.IDLE || mState == State.ACTIVE;
    }

    /**
     * If the session uses custom profile,
     *    Begins ranging for the session.
     *    <p>On successfully starting a ranging session,
     *    {@link RangingSession.Callback#onStarted(PersistableBundle)} is invoked.
     *    <p>On failure to start the session,
     *    {@link RangingSession.Callback#onStartFailed(int, PersistableBundle)}
     *    is invoked.
     *
     * If the session uses platform defined profile (like PACS),
     *    Begins OOB discovery for the service. Once the service is discovered,
     *    UWB session params are negotiated via OOB and a UWB session will be
     *    started.
     *    <p>On successfully discovering a service,
     *    {@link RangingSession.Callback#onServiceDiscovered(PersistableBundle)} is invoked.
     *    <p>On successfully connecting to a service,
     *    {@link RangingSession.Callback#onServiceConnected(PersistableBundle)} is invoked.
     *    <p>On successfully starting a ranging session,
     *    {@link RangingSession.Callback#onStarted(PersistableBundle)} is invoked.
     *    <p>On failure to start the session,
     *    {@link RangingSession.Callback#onStartFailed(int, PersistableBundle)}
     *    is invoked.
     *
     * @param params configuration parameters for starting the session
     */
    @RequiresPermission(Manifest.permission.UWB_PRIVILEGED)
    public void start(@NonNull PersistableBundle params) {
        if (mState != State.IDLE) {
            throw new IllegalStateException();
        }

        Log.v(mTag, "start - sessionHandle: " + mSessionHandle);
        try {
            mAdapter.startRanging(mSessionHandle, params);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Attempts to reconfigure the session with the given parameters
     * <p>This call may be made when the session is open.
     *
     * <p>On successfully reconfiguring the session
     * {@link RangingSession.Callback#onReconfigured(PersistableBundle)} is invoked.
     *
     * <p>On failure to reconfigure the session,
     * {@link RangingSession.Callback#onReconfigureFailed(int, PersistableBundle)} is invoked.
     *
     * @param params the parameters to reconfigure and their new values
     */
    @RequiresPermission(Manifest.permission.UWB_PRIVILEGED)
    public void reconfigure(@NonNull PersistableBundle params) {
        if (mState != State.ACTIVE && mState != State.IDLE) {
            throw new IllegalStateException();
        }

        Log.v(mTag, "reconfigure - sessionHandle: " + mSessionHandle);
        try {
            mAdapter.reconfigureRanging(mSessionHandle, params);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Stops actively ranging
     *
     * <p>A session that has been stopped may be resumed by calling
     * {@link RangingSession#start(PersistableBundle)} without the need to open a new session.
     *
     * <p>Stopping a {@link RangingSession} is useful when the lower layers should not discard
     * the parameters of the session, or when a session needs to be able to be resumed quickly.
     *
     * <p>If the {@link RangingSession} is no longer needed, use {@link RangingSession#close()} to
     * completely close the session and allow lower layers of the stack to perform necessarily
     * cleanup.
     *
     * <p>Stopped sessions may be closed by the system at any time. In such a case,
     * {@link RangingSession.Callback#onClosed(int, PersistableBundle)} is invoked.
     *
     * <p>On failure to stop the session,
     * {@link RangingSession.Callback#onStopFailed(int, PersistableBundle)} is invoked.
     */
    @RequiresPermission(Manifest.permission.UWB_PRIVILEGED)
    public void stop() {
        if (mState != State.ACTIVE) {
            throw new IllegalStateException();
        }

        Log.v(mTag, "stop - sessionHandle: " + mSessionHandle);
        try {
            mAdapter.stopRanging(mSessionHandle);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Close the ranging session
     *
     * <p>After calling this function, in order resume ranging, a new {@link RangingSession} must
     * be opened by calling
     * {@link UwbManager#openRangingSession(PersistableBundle, Executor, Callback)}.
     *
     * <p>If this session is currently ranging, it will stop and close the session.
     * <p>If the session is in the process of being opened, it will attempt to stop the session from
     * being opened.
     * <p>If the session is already closed, the registered
     * {@link Callback#onClosed(int, PersistableBundle)} callback will still be invoked.
     *
     * <p>{@link Callback#onClosed(int, PersistableBundle)} will be invoked using the same callback
     * object given to {@link UwbManager#openRangingSession(PersistableBundle, Executor, Callback)}
     * when the {@link RangingSession} was opened. The callback will be invoked after each call to
     * {@link #close()}, even if the {@link RangingSession} is already closed.
     */
    @Override
    @RequiresPermission(Manifest.permission.UWB_PRIVILEGED)
    public void close() {
        if (mState == State.CLOSED) {
            mExecutor.execute(() -> mCallback.onClosed(
                    Callback.REASON_LOCAL_REQUEST, new PersistableBundle()));
            return;
        }

        Log.v(mTag, "close - sessionHandle: " + mSessionHandle);
        try {
            mAdapter.closeRanging(mSessionHandle);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Add a new controlee to an ongoing session.
     * <p>This call may be made when the session is open.
     *
     * <p>On successfully adding a new controlee to the session
     * {@link RangingSession.Callback#onControleeAdded(PersistableBundle)} is invoked.
     *
     * <p>On failure to add a new controlee to the session,
     * {@link RangingSession.Callback#onControleeAddFailed(int, PersistableBundle)} is invoked.
     *
     * @param params the parameters for the new controlee
     */
    @RequiresPermission(Manifest.permission.UWB_PRIVILEGED)
    public void addControlee(@NonNull PersistableBundle params) {
        if (mState != State.ACTIVE && mState != State.IDLE) {
            throw new IllegalStateException();
        }

        Log.v(mTag, "addControlee - sessionHandle: " + mSessionHandle);
        try {
            mAdapter.addControlee(mSessionHandle, params);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Remove an existing controlee from an ongoing session.
     * <p>This call may be made when the session is open.
     *
     * <p>On successfully removing an existing controlee from the session
     * {@link RangingSession.Callback#onControleeRemoved(PersistableBundle)} is invoked.
     *
     * <p>On failure to remove an existing controlee from the session,
     * {@link RangingSession.Callback#onControleeRemoveFailed(int, PersistableBundle)} is invoked.
     *
     * @param params the parameters for the existing controlee
     */
    @RequiresPermission(Manifest.permission.UWB_PRIVILEGED)
    public void removeControlee(@NonNull PersistableBundle params) {
        if (mState != State.ACTIVE && mState != State.IDLE) {
            throw new IllegalStateException();
        }

        Log.v(mTag, "removeControlee - sessionHandle: " + mSessionHandle);
        try {
            mAdapter.removeControlee(mSessionHandle, params);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Pauses an ongoing ranging session.
     *
     * <p>A session that has been pauseed may be resumed by calling
     * {@link RangingSession#resume(PersistableBundle)} without the need to open a new session.
     *
     * <p>Pauseing a {@link RangingSession} is useful when the lower layers should skip a few
     * ranging rounds for a session without stopping it.
     *
     * <p>If the {@link RangingSession} is no longer needed, use {@link RangingSession#stop()} or
     * {@link RangingSession#close()} to completely close the session.
     *
     * <p>On successfully pausing the session,
     * {@link RangingSession.Callback#onRangingPaused(PersistableBundle)} is invoked.
     *
     * <p>On failure to pause the session,
     * {@link RangingSession.Callback#onRangingPauseFailed(int, PersistableBundle)} is invoked.
     *
     * @param params protocol specific parameters for pausing the session
     */
    @RequiresPermission(Manifest.permission.UWB_PRIVILEGED)
    public void pause(@NonNull PersistableBundle params) {
        if (mState != State.ACTIVE) {
            throw new IllegalStateException();
        }

        Log.v(mTag, "pause - sessionHandle: " + mSessionHandle);
        try {
            mAdapter.pause(mSessionHandle, params);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Resumes a pauseed ranging session.
     *
     * <p>A session that has been previously pauseed using
     * {@link RangingSession#pause(PersistableBundle)} can be resumed by calling
     * {@link RangingSession#resume(PersistableBundle)}.
     *
     * <p>On successfully resuming the session,
     * {@link RangingSession.Callback#onRangingResumed(PersistableBundle)} is invoked.
     *
     * <p>On failure to resume the session,
     * {@link RangingSession.Callback#onRangingResumeFailed(int, PersistableBundle)} is invoked.
     *
     * @param params protocol specific parameters the resuming the session
     */
    @RequiresPermission(Manifest.permission.UWB_PRIVILEGED)
    public void resume(@NonNull PersistableBundle params) {
        if (mState != State.ACTIVE) {
            throw new IllegalStateException();
        }

        Log.v(mTag, "resume - sessionHandle: " + mSessionHandle);
        try {
            mAdapter.resume(mSessionHandle, params);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Send data to a remote device which is part of this ongoing session.
     * The data is sent by piggybacking the provided data over RRM (initiator -> responder) or
     * RIM (responder -> initiator).
     * <p>This is only functional on a FIRA 2.0 compliant device.
     *
     * <p>On successfully sending the data,
     * {@link RangingSession.Callback#onDataSent(UwbAddress, PersistableBundle)} is invoked.
     *
     * <p>On failure to send the data,
     * {@link RangingSession.Callback#onDataSendFailed(UwbAddress, int, PersistableBundle)} is
     * invoked.
     *
     * @param remoteDeviceAddress remote device's address
     * @param params protocol specific parameters the sending the data
     * @param data Raw data to be sent
     */
    @RequiresPermission(Manifest.permission.UWB_PRIVILEGED)
    public void sendData(@NonNull UwbAddress remoteDeviceAddress,
            @NonNull PersistableBundle params, @NonNull byte[] data) {
        if (mState != State.ACTIVE) {
            throw new IllegalStateException();
        }

        Log.v(mTag, "sendData - sessionHandle: " + mSessionHandle);
        try {
            mAdapter.sendData(mSessionHandle, remoteDeviceAddress, params, data);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Set data transfer phase configuration during ranging as well as dedicated data transfer.
     * <p>This is only functional on a FIRA 2.0 compliant device.
     *
     * <p>On successfully sending the data transfer phase config,
     * {@link RangingSession.Callback#onDataTransferPhaseConfigured(PersistableBundle)} is
     * invoked.
     *
     * <p>On failure to send the data transfer phase config,
     * {@link RangingSession.Callback#onDataTransferPhaseConfigFailed(PersistableBundle)} is
     * invoked.
     *
     * @param params Protocol specific data transfer phase configuration parameters
     */
    @RequiresPermission(Manifest.permission.UWB_PRIVILEGED)
    @FlaggedApi("com.android.uwb.flags.data_transfer_phase_config")
    public void setDataTransferPhaseConfig(@NonNull PersistableBundle params) {
        if (!isOpen()) {
            throw new IllegalStateException();
        }

        Log.v(mTag, "setDataTransferPhaseConfig - sessionHandle: " + mSessionHandle);
        try {
            mAdapter.setDataTransferPhaseConfig(mSessionHandle, params);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Update active ranging rounds for DT Tag.
     *
     * <p> On successfully sending the command,
     * {@link RangingSession.Callback#onRangingRoundsUpdateDtTagStatus(PersistableBundle)}
     * is invoked.
     * @param params Parameters to configure active ranging rounds
     */
    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    @RequiresPermission(Manifest.permission.UWB_PRIVILEGED)
    public void updateRangingRoundsDtTag(@NonNull PersistableBundle params) {
        if (mState != State.ACTIVE && mState != State.IDLE) {
            throw new IllegalStateException();
        }

        Log.v(mTag, "onRangingRoundsUpdateDtTag - sessionHandle: " + mSessionHandle);
        try {
            mAdapter.updateRangingRoundsDtTag(mSessionHandle, params);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Query max application data size which can be sent by UWBS in one ranging round.
     *
     * @throws IllegalStateException, when the ranging session is not in the appropriate state for
     * this API to be called.
     * @return max application data size
     */
    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    @RequiresPermission(Manifest.permission.UWB_PRIVILEGED)
    public int queryMaxDataSizeBytes() {
        if (!isOpen()) {
            throw new IllegalStateException("Ranging session is not open");
        }

        Log.v(mTag, "QueryMaxDataSizeBytes - sessionHandle: " + mSessionHandle);
        try {
            return mAdapter.queryMaxDataSizeBytes(mSessionHandle);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Sets the Hybrid UWB Session Controller Configuration.
     *
     * <p>On successfully setting the hybrid controller configuration,
     * {@link RangingSession.Callback#onHybridSessionControllerConfigured(PersistableBundle)}
     * is invoked.
     *
     * <p>On failing to set the hybrid controller configuration,
     * {@link RangingSession.Callback#onHybridSessionControllerConfigurationFailed(int,
     * PersistableBundle)} is invoked.
     *
     * @param params protocol specific parameters to configure the hybrid session controller
     * @throws RemoteException if a remote error occurred
     */
    @RequiresPermission(Manifest.permission.UWB_PRIVILEGED)
    @FlaggedApi("com.android.uwb.flags.hybrid_session_support")
    public void setHybridSessionControllerConfiguration(@NonNull PersistableBundle params) {
        if (!isOpen()) {
            throw new IllegalStateException("Ranging session is not open");
        }

        Log.v(mTag, "setHybridSessionControllerConfiguration - sessionHandle: " + mSessionHandle);
        try {
            mAdapter.setHybridSessionControllerConfiguration(mSessionHandle, params);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Sets the Hybrid UWB Session Controlee Configuration.
     *
     * <p>On successfully setting the hybrid Controlee configuration,
     * {@link RangingSession.Callback#onHybridSessionControleeConfigured(PersistableBundle)}
     * is invoked.
     *
     * <p>On failing to set the hybrid Controlee configuration,
     * {@link RangingSession.Callback#onHybridSessionControleeConfigurationFailed(int,
     * PersistableBundle)} is invoked.
     *
     * @param params protocol specific parameters to configure the hybrid session Controlee
     * @throws RemoteException if a remote error occurred
     */
    @RequiresPermission(Manifest.permission.UWB_PRIVILEGED)
    @FlaggedApi("com.android.uwb.flags.hybrid_session_support")
    public void setHybridSessionControleeConfiguration(@NonNull PersistableBundle params) {
        if (!isOpen()) {
            throw new IllegalStateException("Ranging session is not open");
        }

        Log.v(mTag, "setHybridSessionControleeConfiguration - sessionHandle: " + mSessionHandle);
        try {
            mAdapter.setHybridSessionControleeConfiguration(mSessionHandle, params);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * @hide
     */
    public void onRangingOpened() {
        if (mState == State.CLOSED) {
            Log.w(mTag, "onRangingOpened invoked for a closed session");
            return;
        }

        Log.v(mTag, "onRangingOpened - sessionHandle: " + mSessionHandle);
        mState = State.IDLE;
        executeCallback(() -> mCallback.onOpened(this));
    }

    /**
     * @hide
     */
    public void onRangingOpenFailed(@Callback.Reason int reason,
            @NonNull PersistableBundle params) {
        if (mState == State.CLOSED) {
            Log.w(mTag, "onRangingOpenFailed invoked for a closed session");
            return;
        }

        Log.v(mTag, "onRangingOpenFailed - sessionHandle: " + mSessionHandle);
        mState = State.CLOSED;
        executeCallback(() -> mCallback.onOpenFailed(reason, params));
    }

    /**
     * @hide
     */
    public void onRangingStarted(@NonNull PersistableBundle parameters) {
        if (mState == State.CLOSED) {
            Log.w(mTag, "onRangingStarted invoked for a closed session");
            return;
        }

        Log.v(mTag, "onRangingStarted - sessionHandle: " + mSessionHandle);
        mState = State.ACTIVE;
        executeCallback(() -> mCallback.onStarted(parameters));
    }

    /**
     * @hide
     */
    public void onRangingStartFailed(@Callback.Reason int reason,
            @NonNull PersistableBundle params) {
        if (mState == State.CLOSED) {
            Log.w(mTag, "onRangingStartFailed invoked for a closed session");
            return;
        }

        Log.v(mTag, "onRangingStartFailed - sessionHandle: " + mSessionHandle);
        executeCallback(() -> mCallback.onStartFailed(reason, params));
    }

    /**
     * @hide
     */
    public void onRangingReconfigured(@NonNull PersistableBundle params) {
        if (mState == State.CLOSED) {
            Log.w(mTag, "onRangingReconfigured invoked for a closed session");
            return;
        }

        Log.v(mTag, "onRangingReconfigured - sessionHandle: " + mSessionHandle);
        executeCallback(() -> mCallback.onReconfigured(params));
    }

    /**
     * @hide
     */
    public void onRangingReconfigureFailed(@Callback.Reason int reason,
            @NonNull PersistableBundle params) {
        if (mState == State.CLOSED) {
            Log.w(mTag, "onRangingReconfigureFailed invoked for a closed session");
            return;
        }

        Log.v(mTag, "onRangingReconfigureFailed - sessionHandle: " + mSessionHandle);
        executeCallback(() -> mCallback.onReconfigureFailed(reason, params));
    }

    /**
     * @hide
     */
    public void onRangingStopped(@Callback.Reason int reason,
            @NonNull PersistableBundle params) {
        if (mState == State.CLOSED) {
            Log.w(mTag, "onRangingStopped invoked for a closed session");
            return;
        }

        Log.v(mTag, "onRangingStopped - sessionHandle: " + mSessionHandle);
        mState = State.IDLE;
        executeCallback(() -> mCallback.onStopped(reason, params));
    }

    /**
     * @hide
     */
    public void onRangingStopFailed(@Callback.Reason int reason,
            @NonNull PersistableBundle params) {
        if (mState == State.CLOSED) {
            Log.w(mTag, "onRangingStopFailed invoked for a closed session");
            return;
        }

        Log.v(mTag, "onRangingStopFailed - sessionHandle: " + mSessionHandle);
        executeCallback(() -> mCallback.onStopFailed(reason, params));
    }

    /**
     * @hide
     */
    public void onRangingClosed(@Callback.Reason int reason,
            @NonNull PersistableBundle parameters) {
        mState = State.CLOSED;
        Log.v(mTag, "onRangingClosed - sessionHandle: " + mSessionHandle);
        executeCallback(() -> mCallback.onClosed(reason, parameters));
    }

    /**
     * @hide
     */
    public void onRangingResult(@NonNull RangingReport report) {
        if (!isOpen()) {
            Log.w(mTag, "onRangingResult invoked for non-open session");
            return;
        }

        Log.v(mTag, "onRangingResult - sessionHandle: " + mSessionHandle);
        executeCallback(() -> mCallback.onReportReceived(report));
    }

    /**
     * @hide
     */
    public void onControleeAdded(@NonNull PersistableBundle params) {
        if (!isOpen()) {
            Log.w(mTag, "onControleeAdded invoked for non-open session");
            return;
        }

        Log.v(mTag, "onControleeAdded - sessionHandle: " + mSessionHandle);
        executeCallback(() -> mCallback.onControleeAdded(params));
    }

    /**
     * @hide
     */
    public void onControleeAddFailed(@Callback.ControleeFailureReason int reason,
            @NonNull PersistableBundle params) {
        if (!isOpen()) {
            Log.w(mTag, "onControleeAddFailed invoked for non-open session");
            return;
        }

        Log.v(mTag, "onControleeAddFailed - sessionHandle: " + mSessionHandle);
        executeCallback(() -> mCallback.onControleeAddFailed(reason, params));
    }

    /**
     * @hide
     */
    public void onControleeRemoved(@NonNull PersistableBundle params) {
        if (!isOpen()) {
            Log.w(mTag, "onControleeRemoved invoked for non-open session");
            return;
        }

        Log.v(mTag, "onControleeRemoved - sessionHandle: " + mSessionHandle);
        executeCallback(() -> mCallback.onControleeRemoved(params));
    }

    /**
     * @hide
     */
    public void onControleeRemoveFailed(@Callback.ControleeFailureReason int reason,
            @NonNull PersistableBundle params) {
        if (!isOpen()) {
            Log.w(mTag, "onControleeRemoveFailed invoked for non-open session");
            return;
        }

        Log.v(mTag, "onControleeRemoveFailed - sessionHandle: " + mSessionHandle);
        executeCallback(() -> mCallback.onControleeRemoveFailed(reason, params));
    }

    /**
     * @hide
     */
    public void onRangingPaused(@NonNull PersistableBundle params) {
        if (!isOpen()) {
            Log.w(mTag, "onRangingPaused invoked for non-open session");
            return;
        }

        Log.v(mTag, "onRangingPaused - sessionHandle: " + mSessionHandle);
        executeCallback(() -> mCallback.onPaused(params));
    }

    /**
     * @hide
     */
    public void onRangingPauseFailed(@Callback.Reason int reason,
            @NonNull PersistableBundle params) {
        if (!isOpen()) {
            Log.w(mTag, "onRangingPauseFailed invoked for non-open session");
            return;
        }

        Log.v(mTag, "onRangingPauseFailed - sessionHandle: " + mSessionHandle);
        executeCallback(() -> mCallback.onPauseFailed(reason, params));
    }

    /**
     * @hide
     */
    public void onRangingResumed(@NonNull PersistableBundle params) {
        if (!isOpen()) {
            Log.w(mTag, "onRangingResumed invoked for non-open session");
            return;
        }

        Log.v(mTag, "onRangingResumed - sessionHandle: " + mSessionHandle);
        executeCallback(() -> mCallback.onResumed(params));
    }

    /**
     * @hide
     */
    public void onRangingResumeFailed(@Callback.Reason int reason,
            @NonNull PersistableBundle params) {
        if (!isOpen()) {
            Log.w(mTag, "onRangingResumeFailed invoked for non-open session");
            return;
        }

        Log.v(mTag, "onRangingResumeFailed - sessionHandle: " + mSessionHandle);
        executeCallback(() -> mCallback.onResumeFailed(reason, params));
    }

    /**
     * @hide
     */
    public void onDataSent(@NonNull UwbAddress remoteDeviceAddress,
            @NonNull PersistableBundle params) {
        if (!isOpen()) {
            Log.w(mTag, "onDataSent invoked for non-open session");
            return;
        }

        Log.v(mTag, "onDataSent - sessionHandle: " + mSessionHandle);
        executeCallback(() -> mCallback.onDataSent(remoteDeviceAddress, params));
    }

    /**
     * @hide
     */
    public void onDataSendFailed(@NonNull UwbAddress remoteDeviceAddress,
            @Callback.DataFailureReason int reason, @NonNull PersistableBundle params) {
        if (!isOpen()) {
            Log.w(mTag, "onDataSendFailed invoked for non-open session");
            return;
        }

        Log.v(mTag, "onDataSendFailed - sessionHandle: " + mSessionHandle);
        executeCallback(() -> mCallback.onDataSendFailed(remoteDeviceAddress, reason, params));
    }

    /**
     * @hide
     */
    public void onDataReceived(@NonNull UwbAddress remoteDeviceAddress,
            @NonNull PersistableBundle params, @NonNull byte[] data) {
        if (!isOpen()) {
            Log.w(mTag, "onDataReceived invoked for non-open session");
            return;
        }

        Log.v(mTag, "onDataReceived - sessionHandle: " + mSessionHandle);
        executeCallback(() -> mCallback.onDataReceived(remoteDeviceAddress, params, data));
    }

    /**
     * @hide
     */
    public void onDataReceiveFailed(@NonNull UwbAddress remoteDeviceAddress,
            @Callback.DataFailureReason int reason, @NonNull PersistableBundle params) {
        if (!isOpen()) {
            Log.w(mTag, "onDataReceiveFailed invoked for non-open session");
            return;
        }

        Log.v(mTag, "onDataReceiveFailed - sessionHandle: " + mSessionHandle);
        executeCallback(() -> mCallback.onDataReceiveFailed(remoteDeviceAddress, reason, params));
    }

    /**
     * @hide
     */
    public void onDataTransferPhaseConfigured(@NonNull PersistableBundle params) {
        if (!isOpen()) {
            Log.w(mTag, "onDataTransferPhaseConfigured invoked for non-open session");
            return;
        }

        Log.v(mTag, "onDataTransferPhaseConfigured - sessionHandle: " + mSessionHandle);
        executeCallback(() -> mCallback.onDataTransferPhaseConfigured(params));
    }

    /**
     * @hide
     */
    public void onDataTransferPhaseConfigFailed(@Callback.Reason int reason,
            @NonNull PersistableBundle params) {
        if (!isOpen()) {
            Log.w(mTag, "onDataTransferPhaseConfigFailed invoked for non-open session");
            return;
        }

        Log.v(mTag, "onDataTransferPhaseConfigFailed - sessionHandle: " + mSessionHandle);
        executeCallback(() -> mCallback.onDataTransferPhaseConfigFailed(reason, params));
    }

    /**
     * @hide
     */
    public void onServiceDiscovered(@NonNull PersistableBundle params) {
        if (!isOpen()) {
            Log.w(mTag, "onServiceDiscovered invoked for non-open session");
            return;
        }

        Log.v(mTag, "onServiceDiscovered - sessionHandle: " + mSessionHandle);
        executeCallback(() -> mCallback.onServiceDiscovered(params));
    }

    /**
     * @hide
     */
    public void onServiceConnected(@NonNull PersistableBundle params) {
        if (!isOpen()) {
            Log.w(mTag, "onServiceConnected invoked for non-open session");
            return;
        }

        Log.v(mTag, "onServiceConnected - sessionHandle: " + mSessionHandle);
        executeCallback(() -> mCallback.onServiceConnected(params));
    }

    /**
     * @hide
     */
    public void onRangingRoundsUpdateDtTagStatus(@NonNull PersistableBundle params) {
        if (!isOpen()) {
            Log.w(mTag, "onDlTDoARangingRoundsUpdateStatus invoked for non-open session");
            return;
        }

        Log.v(mTag, "onDlTDoARangingRoundsUpdateStatus - sessionHandle: " + mSessionHandle);
        if (SdkLevel.isAtLeastU()) {
            executeCallback(() -> mCallback.onRangingRoundsUpdateDtTagStatus(params));
        }
    }

    /**
     * @hide
     */
    public void onHybridSessionControllerConfigured(@NonNull PersistableBundle params) {
        if (!isOpen()) {
            Log.w(mTag, "onHybridSessionControllerConfigured invoked for non-open session");
            return;
        }

        Log.v(mTag, "onHybridSessionControllerConfigured - sessionHandle: " + mSessionHandle);
        executeCallback(() -> mCallback.onHybridSessionControllerConfigured(params));
    }

    /**
     * @hide
     */
    public void onHybridSessionControllerConfigurationFailed(@Callback.Reason int reason,
            @NonNull PersistableBundle params) {
        if (!isOpen()) {
            Log.w(mTag, "onHybridSessionControllerConfigurationFailed invoked for non-open"
                    + "session");
            return;
        }

        Log.v(mTag, "onHybridSessionControllerConfigurationFailed - sessionHandle: "
                + mSessionHandle);
        executeCallback(() -> mCallback.onHybridSessionControllerConfigurationFailed(
                reason, params));
    }

    /**
     * @hide
     */
    public void onHybridSessionControleeConfigured(@NonNull PersistableBundle params) {
        if (!isOpen()) {
            Log.w(mTag, "onHybridSessionControleeConfigured invoked for non-open session");
            return;
        }

        Log.v(mTag, "onHybridSessionControleeConfigured - sessionHandle: " + mSessionHandle);
        executeCallback(() -> mCallback.onHybridSessionControleeConfigured(params));
    }

    /**
     * @hide
     */
    public void onHybridSessionControleeConfigurationFailed(@Callback.Reason int reason,
            @NonNull PersistableBundle params) {
        if (!isOpen()) {
            Log.w(mTag, "onHybridSessionControleeConfigurationFailed invoked for non-open"
                    + "session");
            return;
        }

        Log.v(mTag, "onHybridSessionControleeConfigurationFailed - sessionHandle: "
                + mSessionHandle);
        executeCallback(() -> mCallback.onHybridSessionControleeConfigurationFailed(
                reason, params));
    }

    /**
     * @hide
     */
    private void executeCallback(@NonNull Runnable runnable) {
        final long identity = Binder.clearCallingIdentity();
        try {
            mExecutor.execute(runnable);
        } finally {
            Binder.restoreCallingIdentity(identity);
        }
    }

    /**
     * Updates the UWB filter engine's pose information. This requires that the call to
     * {@link UwbManager#openRangingSession} indicated an application pose source.
     *
     * @param parameters Parameters representing the session to update, and the pose information.
     */
    @RequiresPermission(Manifest.permission.UWB_PRIVILEGED)
    public void updatePose(@NonNull PersistableBundle parameters) {
        try {
            mAdapter.updatePose(mSessionHandle, parameters);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }
}
