/*
 * Copyright (C) 2016 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.net.wifi.aware;

import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import static android.Manifest.permission.ACCESS_WIFI_STATE;
import static android.Manifest.permission.CHANGE_WIFI_STATE;
import static android.Manifest.permission.MANAGE_WIFI_NETWORK_SELECTION;
import static android.Manifest.permission.NEARBY_WIFI_DEVICES;
import static android.Manifest.permission.OVERRIDE_WIFI_CONFIG;
import static android.net.wifi.ScanResult.WIFI_BAND_24_GHZ;
import static android.net.wifi.ScanResult.WIFI_BAND_5_GHZ;

import android.annotation.CallbackExecutor;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.MacAddress;
import android.net.NetworkRequest;
import android.net.NetworkSpecifier;
import android.net.wifi.IBooleanListener;
import android.net.wifi.IIntegerListener;
import android.net.wifi.IListListener;
import android.net.wifi.OuiKeyedData;
import android.net.wifi.WifiManager;
import android.net.wifi.util.HexEncoding;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
import android.util.Log;

import androidx.annotation.RequiresApi;

import com.android.modules.utils.HandlerExecutor;
import com.android.modules.utils.build.SdkLevel;
import com.android.wifi.flags.Flags;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.nio.BufferOverflowException;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.Consumer;

/**
 * This class provides the primary API for managing Wi-Fi Aware operations:
 * discovery and peer-to-peer data connections.
 * <p>
 * The class provides access to:
 * <ul>
 * <li>Initialize a Aware cluster (peer-to-peer synchronization). Refer to
 * {@link #attach(AttachCallback, Handler)}.
 * <li>Create discovery sessions (publish or subscribe sessions). Refer to
 * {@link WifiAwareSession#publish(PublishConfig, DiscoverySessionCallback, Handler)} and
 * {@link WifiAwareSession#subscribe(SubscribeConfig, DiscoverySessionCallback, Handler)}.
 * <li>Create a Aware network specifier to be used with
 * {@link ConnectivityManager#requestNetwork(NetworkRequest, ConnectivityManager.NetworkCallback)}
 * to set-up a Aware connection with a peer. Refer to {@link WifiAwareNetworkSpecifier.Builder}.
 * </ul>
 * <p>
 *     Aware may not be usable when Wi-Fi is disabled (and other conditions). To validate that
 *     the functionality is available use the {@link #isAvailable()} function. To track
 *     changes in Aware usability register for the {@link #ACTION_WIFI_AWARE_STATE_CHANGED}
 *     broadcast. Note that this broadcast is not sticky - you should register for it and then
 *     check the above API to avoid a race condition.
 * <p>
 *     An application must use {@link #attach(AttachCallback, Handler)} to initialize a
 *     Aware cluster - before making any other Aware operation. Aware cluster membership is a
 *     device-wide operation - the API guarantees that the device is in a cluster or joins a
 *     Aware cluster (or starts one if none can be found). Information about attach success (or
 *     failure) are returned in callbacks of {@link AttachCallback}. Proceed with Aware
 *     discovery or connection setup only after receiving confirmation that Aware attach
 *     succeeded - {@link AttachCallback#onAttached(WifiAwareSession)}. When an
 *     application is finished using Aware it <b>must</b> use the
 *     {@link WifiAwareSession#close()} API to indicate to the Aware service that the device
 *     may detach from the Aware cluster. The device will actually disable Aware once the last
 *     application detaches.
 * <p>
 *     Once a Aware attach is confirmed use the
 *     {@link WifiAwareSession#publish(PublishConfig, DiscoverySessionCallback, Handler)}
 *     or
 *     {@link WifiAwareSession#subscribe(SubscribeConfig, DiscoverySessionCallback,
 *     Handler)} to create publish or subscribe Aware discovery sessions. Events are called on the
 *     provided callback object {@link DiscoverySessionCallback}. Specifically, the
 *     {@link DiscoverySessionCallback#onPublishStarted(PublishDiscoverySession)}
 *     and
 *     {@link DiscoverySessionCallback#onSubscribeStarted(
 *SubscribeDiscoverySession)}
 *     return {@link PublishDiscoverySession} and
 *     {@link SubscribeDiscoverySession}
 *     objects respectively on which additional session operations can be performed, e.g. updating
 *     the session {@link PublishDiscoverySession#updatePublish(PublishConfig)} and
 *     {@link SubscribeDiscoverySession#updateSubscribe(SubscribeConfig)}. Sessions can
 *     also be used to send messages using the
 *     {@link DiscoverySession#sendMessage(PeerHandle, int, byte[])} APIs. When an
 *     application is finished with a discovery session it <b>must</b> terminate it using the
 *     {@link DiscoverySession#close()} API.
 * <p>
 *    Creating connections between Aware devices is managed by the standard
 *    {@link ConnectivityManager#requestNetwork(NetworkRequest,
 *    ConnectivityManager.NetworkCallback)}.
 *    The {@link NetworkRequest} object should be constructed with:
 *    <ul>
 *        <li>{@link NetworkRequest.Builder#addTransportType(int)} of
 *        {@link android.net.NetworkCapabilities#TRANSPORT_WIFI_AWARE}.
 *        <li>{@link NetworkRequest.Builder#setNetworkSpecifier(String)} using
 *        {@link WifiAwareNetworkSpecifier.Builder}.
 *    </ul>
 */
@SystemService(Context.WIFI_AWARE_SERVICE)
public class WifiAwareManager {
    private static final String TAG = "WifiAwareManager";
    private static final boolean DBG = false;
    private static final boolean VDBG = false; // STOPSHIP if true

    /**
     * Broadcast intent action to indicate that the state of Wi-Fi Aware availability has changed
     * and all active Aware sessions are no longer usable. Use the {@link #isAvailable()} to query
     * the current status.
     * This broadcast is <b>not</b> sticky, use the {@link #isAvailable()} API after registering
     * the broadcast to check the current state of Wi-Fi Aware.
     * <p>Note: The broadcast is only delivered to registered receivers - no manifest registered
     * components will be launched.
     */
    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
    public static final String ACTION_WIFI_AWARE_STATE_CHANGED =
            "android.net.wifi.aware.action.WIFI_AWARE_STATE_CHANGED";
    /**
     * Intent broadcast sent whenever Wi-Fi Aware resource availability has changed. The resources
     * are attached with the {@link #EXTRA_AWARE_RESOURCES} extra. The resources can also be
     * obtained using the {@link #getAvailableAwareResources()} method. To receive this broadcast,
     * apps must hold {@link android.Manifest.permission#ACCESS_WIFI_STATE}.
     * <p>Note: The broadcast is only delivered to registered receivers - no manifest registered
     * components will be launched.
     */
    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
    @RequiresPermission(ACCESS_WIFI_STATE)
    public static final String ACTION_WIFI_AWARE_RESOURCE_CHANGED =
            "android.net.wifi.aware.action.WIFI_AWARE_RESOURCE_CHANGED";

    /**
     * Sent as a part of {@link #ACTION_WIFI_AWARE_RESOURCE_CHANGED} that contains an instance of
     * {@link AwareResources} representing the current Wi-Fi Aware resources.
     */
    public static final String EXTRA_AWARE_RESOURCES =
            "android.net.wifi.aware.extra.AWARE_RESOURCES";

    /** @hide */
    @IntDef({
            WIFI_AWARE_DATA_PATH_ROLE_INITIATOR, WIFI_AWARE_DATA_PATH_ROLE_RESPONDER})
    @Retention(RetentionPolicy.SOURCE)
    public @interface DataPathRole {
    }

    /**
     * Connection creation role is that of INITIATOR. Used to create a network specifier string
     * when requesting a Aware network.
     *
     * @see WifiAwareSession#createNetworkSpecifierOpen(int, byte[])
     * @see WifiAwareSession#createNetworkSpecifierPassphrase(int, byte[], String)
     */
    public static final int WIFI_AWARE_DATA_PATH_ROLE_INITIATOR = 0;

    /**
     * Connection creation role is that of RESPONDER. Used to create a network specifier string
     * when requesting a Aware network.
     *
     * @see WifiAwareSession#createNetworkSpecifierOpen(int, byte[])
     * @see WifiAwareSession#createNetworkSpecifierPassphrase(int, byte[], String)
     */
    public static final int WIFI_AWARE_DATA_PATH_ROLE_RESPONDER = 1;

    /** @hide */
    @IntDef({
            WIFI_AWARE_DISCOVERY_LOST_REASON_UNKNOWN,
            WIFI_AWARE_DISCOVERY_LOST_REASON_PEER_NOT_VISIBLE})
    @Retention(RetentionPolicy.SOURCE)
    public @interface DiscoveryLostReasonCode {
    }

    /**
     * Reason code provided in {@link DiscoverySessionCallback#onServiceLost(PeerHandle, int)}
     * indicating that the service was lost for unknown reason.
     */
    public static final int WIFI_AWARE_DISCOVERY_LOST_REASON_UNKNOWN = 0;

    /**
     * Reason code provided in {@link DiscoverySessionCallback#onServiceLost(PeerHandle, int)}
     * indicating that the service advertised by the peer is no longer visible. This may be because
     * the peer is out of range or because the peer stopped advertising this service.
     */
    public static final int WIFI_AWARE_DISCOVERY_LOST_REASON_PEER_NOT_VISIBLE = 1;

    /** @hide */
    @IntDef({
            WIFI_AWARE_SUSPEND_REDUNDANT_REQUEST,
            WIFI_AWARE_SUSPEND_INVALID_SESSION,
            WIFI_AWARE_SUSPEND_CANNOT_SUSPEND,
            WIFI_AWARE_SUSPEND_INTERNAL_ERROR})
    @Retention(RetentionPolicy.SOURCE)
    public @interface SessionSuspensionFailedReasonCode {}

    /**
     * Reason code provided in {@link DiscoverySessionCallback#onSessionSuspendFailed(int)} when the
     * session is already suspended.
     * @hide
     */
    @SystemApi
    public static final int WIFI_AWARE_SUSPEND_REDUNDANT_REQUEST = 0;

    /**
     * Reason code provided in {@link DiscoverySessionCallback#onSessionSuspendFailed(int)} when the
     * specified session does not support suspension.
      @hide
     */
    @SystemApi
    public static final int WIFI_AWARE_SUSPEND_INVALID_SESSION = 1;

    /**
     * Reason code provided in {@link DiscoverySessionCallback#onSessionSuspendFailed(int)} when the
     * session could not be suspended due to more than one app using it.
      @hide
     */
    @SystemApi
    public static final int WIFI_AWARE_SUSPEND_CANNOT_SUSPEND = 2;

    /**
     * Reason code provided in {@link DiscoverySessionCallback#onSessionSuspendFailed(int)} when an
     * error is encountered with the request.
      @hide
     */
    @SystemApi
    public static final int WIFI_AWARE_SUSPEND_INTERNAL_ERROR = 3;

    /** @hide */
    @IntDef({
            WIFI_AWARE_RESUME_REDUNDANT_REQUEST,
            WIFI_AWARE_RESUME_INVALID_SESSION,
            WIFI_AWARE_RESUME_INTERNAL_ERROR})
    @Retention(RetentionPolicy.SOURCE)
    public @interface SessionResumptionFailedReasonCode {}

    /**
     * Reason code provided in {@link DiscoverySessionCallback#onSessionResumeFailed(int)} when the
     * session is not suspended.
     * @hide
     */
    @SystemApi
    public static final int WIFI_AWARE_RESUME_REDUNDANT_REQUEST = 0;

    /**
     * Reason code provided in {@link DiscoverySessionCallback#onSessionResumeFailed(int)} when the
     * specified session does not support suspension.
      @hide
     */
    @SystemApi
    public static final int WIFI_AWARE_RESUME_INVALID_SESSION = 1;

    /**
     * Reason code provided in {@link DiscoverySessionCallback#onSessionResumeFailed(int)} when an
     * error is encountered with the request.
      @hide
     */
    @SystemApi
    public static final int WIFI_AWARE_RESUME_INTERNAL_ERROR = 2;

    /** @hide */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef(
            prefix = {"WIFI_BAND_"},
            value = {WIFI_BAND_24_GHZ, WIFI_BAND_5_GHZ})
    public @interface InstantModeBand {};

    private final Context mContext;
    private final IWifiAwareManager mService;

    private final Object mLock = new Object(); // lock access to the following vars

    /** @hide */
    public WifiAwareManager(@NonNull Context context, @NonNull IWifiAwareManager service) {
        mContext = context;
        mService = service;
    }

    /**
     * Returns the current status of Aware API: whether or not Aware is available. To track
     * changes in the state of Aware API register for the
     * {@link #ACTION_WIFI_AWARE_STATE_CHANGED} broadcast.
     *
     * @return A boolean indicating whether the app can use the Aware API at this time (true) or
     * not (false).
     */
    @RequiresPermission(ACCESS_WIFI_STATE)
    public boolean isAvailable() {
        try {
            return mService.isUsageEnabled();
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Return the current status of the Aware service: whether or not the device is already attached
     * to an Aware cluster. To attach to an Aware cluster, please use
     * {@link #attach(AttachCallback, Handler)} or
     * {@link #attach(AttachCallback, IdentityChangedListener, Handler)}.
     * @return A boolean indicating whether the device is attached to a cluster at this time (true)
     *         or not (false).
     */
    @RequiresPermission(ACCESS_WIFI_STATE)
    public boolean isDeviceAttached() {
        try {
            return mService.isDeviceAttached();
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Return the device support for setting a channel requirement in a data-path request. If true
     * the channel set by
     * {@link WifiAwareNetworkSpecifier.Builder#setChannelFrequencyMhz(int, boolean)} will be
     * honored, otherwise it will be ignored.
     * @return True is the device support set channel on data-path request, false otherwise.
     */
    @RequiresPermission(ACCESS_WIFI_STATE)
    public boolean isSetChannelOnDataPathSupported() {
        try {
            return mService.isSetChannelOnDataPathSupported();
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Enable the Wifi Aware Instant communication mode. If the device doesn't support this feature
     * calling this API will result no action.
     * <p>
     * Note: before {@link android.os.Build.VERSION_CODES#TIRAMISU}, only system app can use this
     * API. Start with {@link android.os.Build.VERSION_CODES#TIRAMISU} apps hold
     * {@link android.Manifest.permission#OVERRIDE_WIFI_CONFIG} are allowed to use it.
     *
     * @see Characteristics#isInstantCommunicationModeSupported()
     * @param enable true for enable, false otherwise.
     * @hide
     */
    @SystemApi
    @RequiresApi(Build.VERSION_CODES.S)
    @RequiresPermission(allOf = {CHANGE_WIFI_STATE, OVERRIDE_WIFI_CONFIG})
    public void enableInstantCommunicationMode(boolean enable) {
        if (!SdkLevel.isAtLeastS()) {
            throw new UnsupportedOperationException();
        }
        try {
            mService.enableInstantCommunicationMode(mContext.getOpPackageName(), enable);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Return the current status of the Wifi Aware instant communication mode.
     * If the device doesn't support this feature, return will always be false.
     * @see Characteristics#isInstantCommunicationModeSupported()
     * @return true if it is enabled, false otherwise.
     */
    @RequiresApi(Build.VERSION_CODES.S)
    @RequiresPermission(ACCESS_WIFI_STATE)
    public boolean isInstantCommunicationModeEnabled() {
        if (!SdkLevel.isAtLeastS()) {
            throw new UnsupportedOperationException();
        }
        try {
            return mService.isInstantCommunicationModeEnabled();
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Returns the characteristics of the Wi-Fi Aware interface: a set of parameters which specify
     * limitations on configurations, e.g. the maximum service name length.
     * <p>
     * May return {@code null} if the Wi-Fi Aware service is not initialized. Use
     * {@link #attach(AttachCallback, Handler)} or
     * {@link #attach(AttachCallback, IdentityChangedListener, Handler)} to initialize the Wi-Fi
     * Aware service.
     *
     * @return An object specifying configuration limitations of Aware.
     */
    @RequiresPermission(ACCESS_WIFI_STATE)
    public @Nullable Characteristics getCharacteristics() {
        try {
            return mService.getCharacteristics();
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Return the available resources of the Wi-Fi aware service: a set of parameters which specify
     * limitations on service usage, e.g the number of data-paths which could be created.
     * <p>
     * May return {@code null} if the Wi-Fi Aware service is not initialized. Use
     * {@link #attach(AttachCallback, Handler)} or
     * {@link #attach(AttachCallback, IdentityChangedListener, Handler)} to initialize the Wi-Fi
     * Aware service.
     *
     * @return An object specifying the currently available resource of the Wi-Fi Aware service.
     */
    @RequiresPermission(ACCESS_WIFI_STATE)
    public @Nullable AwareResources getAvailableAwareResources() {
        try {
            return mService.getAvailableAwareResources();
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Attach to the Wi-Fi Aware service - enabling the application to create discovery sessions or
     * create connections to peers. The device will attach to an existing cluster if it can find
     * one or create a new cluster (if it is the first to enable Aware in its vicinity). Results
     * (e.g. successful attach to a cluster) are provided to the {@code attachCallback} object.
     * An application <b>must</b> call {@link WifiAwareSession#close()} when done with the
     * Wi-Fi Aware object.
     * <p>
     * Note: a Aware cluster is a shared resource - if the device is already attached to a cluster
     * then this function will simply indicate success immediately using the same {@code
     * attachCallback}.
     *
     * @param attachCallback A callback for attach events, extended from
     * {@link AttachCallback}.
     * @param handler The Handler on whose thread to execute the callbacks of the {@code
     * attachCallback} object. If a null is provided then the application's main thread will be
     *                used.
     */
    @RequiresPermission(allOf = {
            ACCESS_WIFI_STATE,
            CHANGE_WIFI_STATE
    })
    public void attach(@NonNull AttachCallback attachCallback, @Nullable Handler handler) {
        attach(handler, null, attachCallback, null, false, null);
    }

    /**
     * Attach to the Wi-Fi Aware service - enabling the application to create discovery sessions or
     * create connections to peers. The device will attach to an existing cluster if it can find
     * one or create a new cluster (if it is the first to enable Aware in its vicinity). Results
     * (e.g. successful attach to a cluster) are provided to the {@code attachCallback} object.
     * An application <b>must</b> call {@link WifiAwareSession#close()} when done with the
     * Wi-Fi Aware object.
     * <p>
     * Note: a Aware cluster is a shared resource - if the device is already attached to a cluster
     * then this function will simply indicate success immediately using the same {@code
     * attachCallback}.
     * <p>
     * This version of the API attaches a listener to receive the MAC address of the Aware interface
     * on startup and whenever it is updated (it is randomized at regular intervals for privacy).
     *
     * If targeting {@link android.os.Build.VERSION_CODES#TIRAMISU} or later, the application must
     * have {@link android.Manifest.permission#NEARBY_WIFI_DEVICES} with
     * android:usesPermissionFlags="neverForLocation". If the application does not declare
     * android:usesPermissionFlags="neverForLocation", then it must also have
     * {@link android.Manifest.permission#ACCESS_FINE_LOCATION}.
     *
     * If targeting an earlier release than {@link android.os.Build.VERSION_CODES#TIRAMISU}, the
     * application must have {@link android.Manifest.permission#ACCESS_FINE_LOCATION}.
     *
     * Apps without {@link android.Manifest.permission#NEARBY_WIFI_DEVICES} or
     * {@link android.Manifest.permission#ACCESS_FINE_LOCATION} can use the
     * {@link #attach(AttachCallback, Handler)} version.
     * Note that aside from permission requirements the {@link IdentityChangedListener} will wake up
     * the host at regular intervals causing higher power consumption, do not use it unless the
     * information is necessary (e.g. for out-of-band discovery).
     *
     * @param attachCallback A callback for attach events, extended from
     * {@link AttachCallback}.
     * @param identityChangedListener A callback for changed identity or cluster ID, extended from
     * {@link IdentityChangedListener}.
     * @param handler The Handler on whose thread to execute the callbacks of the {@code
     * attachCallback} and {@code identityChangedListener} objects. If a null is provided then the
     *                application's main thread will be used.
     */
    @RequiresPermission(allOf = {
            ACCESS_WIFI_STATE,
            CHANGE_WIFI_STATE,
            ACCESS_FINE_LOCATION,
            NEARBY_WIFI_DEVICES}, conditional = true)
    public void attach(@NonNull AttachCallback attachCallback,
            @NonNull IdentityChangedListener identityChangedListener,
            @Nullable Handler handler) {
        attach(handler, null, attachCallback, identityChangedListener, false, null);
    }

    /**
     * Attach to the Wi-Fi Aware service - enabling the application to create discovery sessions or
     * create connections to peers. See {@link #attach(AttachCallback, IdentityChangedListener,
     * Handler)} for more information.
     *
     * This version allows callers to provide an instance of {@link ConfigRequest}.
     *
     * @param configRequest Parameters for this request.
     * @param executor The executor to execute the listener of the {@code attachCallback} object.
     * @param attachCallback A callback for attach events, extended from {@link AttachCallback}.
     * @param identityChangedListener A callback for changed identity or cluster ID, extended from
     * {@link IdentityChangedListener}.
     * @hide
     */
    @RequiresPermission(allOf = {
            ACCESS_WIFI_STATE,
            CHANGE_WIFI_STATE,
            ACCESS_FINE_LOCATION,
            NEARBY_WIFI_DEVICES,
            MANAGE_WIFI_NETWORK_SELECTION}, conditional = true)
    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
    @FlaggedApi(Flags.FLAG_ANDROID_V_WIFI_API)
    @SystemApi
    public void attach(@NonNull ConfigRequest configRequest,
            @NonNull @CallbackExecutor Executor executor, @NonNull AttachCallback attachCallback,
            @NonNull IdentityChangedListener identityChangedListener) {
        if (!SdkLevel.isAtLeastV()) {
            throw new UnsupportedOperationException();
        }
        Objects.requireNonNull(configRequest);
        Objects.requireNonNull(executor);
        attach(null, configRequest, attachCallback, identityChangedListener, false, executor);
    }

    /** @hide */
    public void attach(Handler handler, ConfigRequest configRequest,
            AttachCallback attachCallback,
            IdentityChangedListener identityChangedListener, boolean forOffloading,
            Executor executor) {
        if (VDBG) {
            Log.v(TAG, "attach(): handler=" + handler + ", callback=" + attachCallback
                    + ", configRequest=" + configRequest + ", identityChangedListener="
                    + identityChangedListener + ", forOffloading" + forOffloading);
        }

        if (attachCallback == null) {
            throw new IllegalArgumentException("Null callback provided");
        }

        synchronized (mLock) {
            Executor localExecutor = executor;
            if (localExecutor == null) {
                localExecutor = new HandlerExecutor((handler == null)
                        ? new Handler(Looper.getMainLooper()) : handler);
            }

            try {
                Binder binder = new Binder();
                Bundle extras = new Bundle();
                if (SdkLevel.isAtLeastS()) {
                    extras.putParcelable(WifiManager.EXTRA_PARAM_KEY_ATTRIBUTION_SOURCE,
                            mContext.getAttributionSource());
                }
                mService.connect(binder, mContext.getOpPackageName(), mContext.getAttributionTag(),
                        new WifiAwareEventCallbackProxy(this, localExecutor, binder,
                                attachCallback, identityChangedListener), configRequest,
                        identityChangedListener != null, extras, forOffloading);
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        }
    }

    /** @hide */
    public void disconnect(int clientId, Binder binder) {
        if (VDBG) Log.v(TAG, "disconnect()");

        try {
            mService.disconnect(clientId, binder);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /** @hide */
    public void setMasterPreference(int clientId, Binder binder, int mp) {
        if (VDBG) Log.v(TAG, "setMasterPreference()");

        try {
            mService.setMasterPreference(clientId, binder, mp);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * @hide
     */
    public void getMasterPreference(int clientId, Binder binder, @NonNull Executor executor,
            @NonNull Consumer<Integer> resultsCallback) {
        Objects.requireNonNull(executor, "executor cannot be null");
        Objects.requireNonNull(resultsCallback, "resultsCallback cannot be null");
        try {
            mService.getMasterPreference(clientId, binder,
                    new IIntegerListener.Stub() {
                        public void onResult(int value) {
                            Binder.clearCallingIdentity();
                            executor.execute(() -> resultsCallback.accept(value));
                        }
                    });
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /** @hide */
    public void publish(int clientId, Looper looper, PublishConfig publishConfig,
            DiscoverySessionCallback callback) {
        if (VDBG) Log.v(TAG, "publish(): clientId=" + clientId + ", config=" + publishConfig);

        if (callback == null) {
            throw new IllegalArgumentException("Null callback provided");
        }

        try {
            Bundle extras = new Bundle();
            if (SdkLevel.isAtLeastS()) {
                extras.putParcelable(WifiManager.EXTRA_PARAM_KEY_ATTRIBUTION_SOURCE,
                        mContext.getAttributionSource());
            }
            mService.publish(mContext.getOpPackageName(), mContext.getAttributionTag(), clientId,
                    publishConfig,
                    new WifiAwareDiscoverySessionCallbackProxy(this, looper, true, callback,
                            clientId), extras);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /** @hide */
    public void updatePublish(int clientId, int sessionId, PublishConfig publishConfig) {
        if (VDBG) {
            Log.v(TAG, "updatePublish(): clientId=" + clientId + ",sessionId=" + sessionId
                    + ", config=" + publishConfig);
        }

        try {
            mService.updatePublish(clientId, sessionId, publishConfig);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /** @hide */
    public void subscribe(int clientId, Looper looper, SubscribeConfig subscribeConfig,
            DiscoverySessionCallback callback) {
        if (VDBG) {
            if (VDBG) {
                Log.v(TAG,
                        "subscribe(): clientId=" + clientId + ", config=" + subscribeConfig);
            }
        }

        if (callback == null) {
            throw new IllegalArgumentException("Null callback provided");
        }

        try {
            Bundle extras = new Bundle();
            if (SdkLevel.isAtLeastS()) {
                extras.putParcelable(WifiManager.EXTRA_PARAM_KEY_ATTRIBUTION_SOURCE,
                        mContext.getAttributionSource());
            }
            mService.subscribe(mContext.getOpPackageName(), mContext.getAttributionTag(), clientId,
                    subscribeConfig,
                    new WifiAwareDiscoverySessionCallbackProxy(this, looper, false, callback,
                            clientId), extras);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /** @hide */
    public void updateSubscribe(int clientId, int sessionId, SubscribeConfig subscribeConfig) {
        if (VDBG) {
            Log.v(TAG, "updateSubscribe(): clientId=" + clientId + ",sessionId=" + sessionId
                    + ", config=" + subscribeConfig);
        }

        try {
            mService.updateSubscribe(clientId, sessionId, subscribeConfig);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /** @hide */
    public void terminateSession(int clientId, int sessionId) {
        if (VDBG) {
            Log.d(TAG,
                    "terminateSession(): clientId=" + clientId + ", sessionId=" + sessionId);
        }

        try {
            mService.terminateSession(clientId, sessionId);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /** @hide */
    public void sendMessage(int clientId, int sessionId, PeerHandle peerHandle, byte[] message,
            int messageId, int retryCount) {
        if (peerHandle == null) {
            throw new IllegalArgumentException(
                    "sendMessage: invalid peerHandle - must be non-null");
        }

        if (VDBG) {
            Log.v(TAG, "sendMessage(): clientId=" + clientId + ", sessionId=" + sessionId
                    + ", peerHandle=" + peerHandle.peerId + ", messageId="
                    + messageId + ", retryCount=" + retryCount);
        }

        try {
            mService.sendMessage(clientId, sessionId, peerHandle.peerId, message, messageId,
                    retryCount);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * @hide
     */
    public void initiateNanPairingSetupRequest(int clientId, int sessionId, PeerHandle peerHandle,
            String password, String pairingDeviceAlias, int cipherSuite) {
        if (peerHandle == null) {
            throw new IllegalArgumentException(
                    "initiateNanPairingRequest: invalid peerHandle - must be non-null");
        }
        if (VDBG) {
            Log.v(TAG, "initiateNanPairingRequest(): clientId=" + clientId
                    + ", sessionId=" + sessionId + ", peerHandle=" + peerHandle.peerId);
        }
        try {
            mService.initiateNanPairingSetupRequest(clientId, sessionId, peerHandle.peerId,
                    password, pairingDeviceAlias, cipherSuite);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * @hide
     */
    public void responseNanPairingSetupRequest(int clientId, int sessionId, PeerHandle peerHandle,
            int requestId, String password, String pairingDeviceAlias, boolean accept,
            int cipherSuite) {
        if (peerHandle == null) {
            throw new IllegalArgumentException(
                    "initiateNanPairingRequest: invalid peerHandle - must be non-null");
        }
        if (VDBG) {
            Log.v(TAG, "initiateNanPairingRequest(): clientId=" + clientId
                    + ", sessionId=" + sessionId + ", peerHandle=" + peerHandle.peerId);
        }
        try {
            mService.responseNanPairingSetupRequest(clientId, sessionId, peerHandle.peerId,
                    requestId, password, pairingDeviceAlias, accept, cipherSuite);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * @hide
     */
    public void initiateBootStrappingSetupRequest(int clientId, int sessionId,
            PeerHandle peerHandle, int method) {
        if (peerHandle == null) {
            throw new IllegalArgumentException(
                    "initiateBootStrappingSetupRequest: invalid peerHandle - must be non-null");
        }
        if (VDBG) {
            Log.v(TAG, "initiateBootStrappingSetupRequest(): clientId=" + clientId
                    + ", sessionId=" + sessionId + ", peerHandle=" + peerHandle.peerId);
        }
        try {
            mService.initiateBootStrappingSetupRequest(clientId, sessionId, peerHandle.peerId,
                    method);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /** @hide */
    @RequiresPermission(android.Manifest.permission.NETWORK_STACK)
    public void requestMacAddresses(int uid, int[] peerIds,
            IWifiAwareMacAddressProvider callback) {
        try {
            mService.requestMacAddresses(uid, peerIds, callback);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /** @hide */
    public NetworkSpecifier createNetworkSpecifier(int clientId, int role, int sessionId,
            @NonNull PeerHandle peerHandle, @Nullable byte[] pmk, @Nullable String passphrase) {
        if (VDBG) {
            Log.v(TAG, "createNetworkSpecifier: role=" + role + ", sessionId=" + sessionId
                    + ", peerHandle=" + ((peerHandle == null) ? peerHandle : peerHandle.peerId)
                    + ", pmk=" + ((pmk == null) ? "null" : "non-null")
                    + ", passphrase=" + ((passphrase == null) ? "null" : "non-null"));
        }

        if (!WifiAwareUtils.isLegacyVersion(mContext, Build.VERSION_CODES.Q)) {
            throw new UnsupportedOperationException(
                    "API deprecated - use WifiAwareNetworkSpecifier.Builder");
        }

        if (role != WIFI_AWARE_DATA_PATH_ROLE_INITIATOR
                && role != WIFI_AWARE_DATA_PATH_ROLE_RESPONDER) {
            throw new IllegalArgumentException(
                    "createNetworkSpecifier: Invalid 'role' argument when creating a network "
                            + "specifier");
        }
        if (role == WIFI_AWARE_DATA_PATH_ROLE_INITIATOR || !WifiAwareUtils.isLegacyVersion(mContext,
                Build.VERSION_CODES.P)) {
            if (peerHandle == null) {
                throw new IllegalArgumentException(
                        "createNetworkSpecifier: Invalid peer handle - cannot be null");
            }
        }

        return new WifiAwareNetworkSpecifier(
                (peerHandle == null) ? WifiAwareNetworkSpecifier.NETWORK_SPECIFIER_TYPE_IB_ANY_PEER
                        : WifiAwareNetworkSpecifier.NETWORK_SPECIFIER_TYPE_IB,
                role,
                clientId,
                sessionId,
                peerHandle != null ? peerHandle.peerId : 0, // 0 is an invalid peer ID
                null, // peerMac (not used in this method)
                pmk,
                passphrase,
                0, // no port info for deprecated IB APIs
                -1); // no transport info for deprecated IB APIs
    }

    /** @hide */
    public NetworkSpecifier createNetworkSpecifier(int clientId, @DataPathRole int role,
            @NonNull byte[] peer, @Nullable byte[] pmk, @Nullable String passphrase) {
        if (VDBG) {
            Log.v(TAG, "createNetworkSpecifier: role=" + role
                    + ", pmk=" + ((pmk == null) ? "null" : "non-null")
                    + ", passphrase=" + ((passphrase == null) ? "null" : "non-null"));
        }

        if (role != WIFI_AWARE_DATA_PATH_ROLE_INITIATOR
                && role != WIFI_AWARE_DATA_PATH_ROLE_RESPONDER) {
            throw new IllegalArgumentException(
                    "createNetworkSpecifier: Invalid 'role' argument when creating a network "
                            + "specifier");
        }
        if (role == WIFI_AWARE_DATA_PATH_ROLE_INITIATOR || !WifiAwareUtils.isLegacyVersion(mContext,
                Build.VERSION_CODES.P)) {
            if (peer == null) {
                throw new IllegalArgumentException(
                        "createNetworkSpecifier: Invalid peer MAC - cannot be null");
            }
        }
        if (peer != null && peer.length != 6) {
            throw new IllegalArgumentException("createNetworkSpecifier: Invalid peer MAC address");
        }

        return new WifiAwareNetworkSpecifier(
                (peer == null) ? WifiAwareNetworkSpecifier.NETWORK_SPECIFIER_TYPE_OOB_ANY_PEER
                        : WifiAwareNetworkSpecifier.NETWORK_SPECIFIER_TYPE_OOB,
                role,
                clientId,
                0, // 0 is an invalid session ID
                0, // 0 is an invalid peer ID
                peer,
                pmk,
                passphrase,
                0, // no port info for OOB APIs
                -1); // no transport protocol info for OOB APIs
    }

    private static class WifiAwareEventCallbackProxy extends IWifiAwareEventCallback.Stub {
        private final WeakReference<WifiAwareManager> mAwareManager;
        private final Binder mBinder;
        private final Executor mExecutor;

        private final AttachCallback mAttachCallback;

        private final IdentityChangedListener mIdentityChangedListener;

        /**
         * Constructs a {@link AttachCallback} using the specified looper.
         * All callbacks will delivered on the thread of the specified looper.
         *
         * @param executor The executor to execute the callbacks.
         */
        WifiAwareEventCallbackProxy(WifiAwareManager mgr, Executor executor, Binder binder,
                final AttachCallback attachCallback,
                final IdentityChangedListener identityChangedListener) {
            mAwareManager = new WeakReference<>(mgr);
            mExecutor = executor;
            mBinder = binder;
            mAttachCallback = attachCallback;
            mIdentityChangedListener = identityChangedListener;
        }

        @Override
        public void onConnectSuccess(int clientId) {
            if (VDBG) Log.v(TAG, "onConnectSuccess");
            Binder.clearCallingIdentity();
            mExecutor.execute(() -> {
                WifiAwareManager mgr = mAwareManager.get();
                if (mgr == null) {
                    Log.w(TAG, "WifiAwareEventCallbackProxy: handleMessage post GC");
                    return;
                }
                mAttachCallback.onAttached(new WifiAwareSession(mgr, mBinder, clientId));
            });
        }

        @Override
        public void onConnectFail(int reason) {
            if (VDBG) Log.v(TAG, "onConnectFail: reason=" + reason);
            Binder.clearCallingIdentity();
            mExecutor.execute(() -> {
                WifiAwareManager mgr = mAwareManager.get();
                if (mgr == null) {
                    Log.w(TAG, "WifiAwareEventCallbackProxy: handleMessage post GC");
                    return;
                }
                mAwareManager.clear();
                mAttachCallback.onAttachFailed();
            });
        }

        @Override
        public void onIdentityChanged(byte[] mac) {
            if (VDBG) Log.v(TAG, "onIdentityChanged: mac=" + new String(HexEncoding.encode(mac)));
            Binder.clearCallingIdentity();
            mExecutor.execute(() -> {
                WifiAwareManager mgr = mAwareManager.get();
                if (mgr == null) {
                    Log.w(TAG, "WifiAwareEventCallbackProxy: handleMessage post GC");
                    return;
                }
                if (mIdentityChangedListener == null) {
                    Log.e(TAG, "CALLBACK_IDENTITY_CHANGED: null listener.");
                } else {
                    mIdentityChangedListener.onIdentityChanged(mac);
                }
            });
        }

        @Override
        public void onAttachTerminate() {
            if (VDBG) Log.v(TAG, "onAwareSessionTerminated");
            Binder.clearCallingIdentity();
            mExecutor.execute(() -> {
                WifiAwareManager mgr = mAwareManager.get();
                if (mgr == null) {
                    Log.w(TAG, "WifiAwareEventCallbackProxy: handleMessage post GC");
                    return;
                }
                mAwareManager.clear();
                mAttachCallback.onAwareSessionTerminated();
            });
        }

        @Override
        public void onClusterIdChanged(
                @IdentityChangedListener.ClusterChangeEvent int clusterEventType,
                byte[] clusterId) {
            Binder.clearCallingIdentity();
            mExecutor.execute(() -> {
                WifiAwareManager mgr = mAwareManager.get();
                if (mgr == null) {
                    Log.w(TAG, "WifiAwareEventCallbackProxy: handleMessage post GC");
                    return;
                }
                if (mIdentityChangedListener == null) {
                    Log.e(TAG, "CALLBACK_CLUSTER_ID_CHANGED: null listener.");
                } else {
                    try {
                        mIdentityChangedListener.onClusterIdChanged(
                                clusterEventType, MacAddress.fromBytes(clusterId));
                    } catch (IllegalArgumentException iae) {
                        Log.e(TAG, " Invalid MAC address, " + iae);
                    }
                }
            });
        }
    }

    private static class WifiAwareDiscoverySessionCallbackProxy extends
            IWifiAwareDiscoverySessionCallback.Stub {
        private final WeakReference<WifiAwareManager> mAwareManager;
        private final boolean mIsPublish;
        private final DiscoverySessionCallback mOriginalCallback;
        private final int mClientId;

        private final Handler mHandler;
        private DiscoverySession mSession;

        WifiAwareDiscoverySessionCallbackProxy(WifiAwareManager mgr, Looper looper,
                boolean isPublish, DiscoverySessionCallback originalCallback,
                int clientId) {
            mAwareManager = new WeakReference<>(mgr);
            mIsPublish = isPublish;
            mOriginalCallback = originalCallback;
            mClientId = clientId;

            if (VDBG) {
                Log.v(TAG, "WifiAwareDiscoverySessionCallbackProxy ctor: isPublish=" + isPublish);
            }

            mHandler = new Handler(looper);
        }

        @Override
        public void onSessionStarted(int sessionId) {
            if (VDBG) Log.v(TAG, "onSessionStarted: sessionId=" + sessionId);
            mHandler.post(() -> onProxySessionStarted(sessionId));
        }

        @Override
        public void onSessionConfigSuccess() {
            if (VDBG) Log.v(TAG, "onSessionConfigSuccess");
            mHandler.post(mOriginalCallback::onSessionConfigUpdated);
        }

        @Override
        public void onSessionConfigFail(int reason) {
            if (VDBG) Log.v(TAG, "onSessionConfigFail: reason=" + reason);
            mHandler.post(() -> {
                mOriginalCallback.onSessionConfigFailed();
                if (mSession == null) {
                    /* creation failed (as opposed to update failing) */
                    mAwareManager.clear();
                }
            });
        }

        @Override
        public void onSessionTerminated(int reason) {
            if (VDBG) Log.v(TAG, "onSessionTerminated: reason=" + reason);
            mHandler.post(() -> onProxySessionTerminated(reason));
        }

        @Override
        public void onSessionSuspendSucceeded() {
            if (VDBG) Log.v(TAG, "onSessionSuspendSucceeded");
            mHandler.post(mOriginalCallback::onSessionSuspendSucceeded);
        }

        @Override
        public void onSessionSuspendFail(int reason) {
            if (VDBG) Log.v(TAG, "onSessionSuspendFail: reason=" + reason);
            mHandler.post(() -> mOriginalCallback.onSessionSuspendFailed(reason));
        }

        @Override
        public void onSessionResumeSucceeded() {
            if (VDBG) Log.v(TAG, "onSessionResumeSucceeded");
            mHandler.post(mOriginalCallback::onSessionResumeSucceeded);
        }

        @Override
        public void onSessionResumeFail(int reason) {
            if (VDBG) Log.v(TAG, "onSessionResumeFail: reason=" + reason);
            mHandler.post(() -> mOriginalCallback.onSessionResumeFailed(reason));
        }

        @Override
        public void onMatch(int peerId, byte[] serviceSpecificInfo, byte[] matchFilter,
                int peerCipherSuite, byte[] scid, String pairingAlias,
                AwarePairingConfig pairingConfig, @Nullable OuiKeyedData[] vendorData) {
            if (VDBG) Log.v(TAG, "onMatch: peerId=" + peerId);

            mHandler.post(() -> {
                List<byte[]> matchFilterList = getMatchFilterList(matchFilter);
                mOriginalCallback.onServiceDiscovered(new PeerHandle(peerId), serviceSpecificInfo,
                        matchFilterList);
                mOriginalCallback.onServiceDiscovered(
                        new ServiceDiscoveryInfo(new PeerHandle(peerId), peerCipherSuite,
                                serviceSpecificInfo, matchFilterList, scid, pairingAlias,
                                pairingConfig, vendorData));
            });
        }

        private List<byte[]> getMatchFilterList(byte[] matchFilter) {
            List<byte[]> matchFilterList = null;
            try {
                matchFilterList = new TlvBufferUtils.TlvIterable(0, 1, matchFilter).toList();
            } catch (BufferOverflowException e) {
                matchFilterList = Collections.emptyList();
                Log.e(TAG, "onServiceDiscovered: invalid match filter byte array '"
                        + new String(HexEncoding.encode(matchFilter))
                        + "' - cannot be parsed: e=" + e);
            }
            return matchFilterList;
        }

        @Override
        public void onMatchWithDistance(int peerId, byte[] serviceSpecificInfo, byte[] matchFilter,
                int distanceMm, int peerCipherSuite, byte[] scid, String pairingAlias,
                AwarePairingConfig pairingConfig, @Nullable OuiKeyedData[] vendorData) {
            if (VDBG) {
                Log.v(TAG, "onMatchWithDistance: peerId=" + peerId + ", distanceMm=" + distanceMm);
            }
            mHandler.post(() -> {
                List<byte[]> matchFilterList = getMatchFilterList(matchFilter);
                mOriginalCallback.onServiceDiscoveredWithinRange(
                        new PeerHandle(peerId),
                        serviceSpecificInfo,
                        matchFilterList, distanceMm);
                mOriginalCallback.onServiceDiscoveredWithinRange(
                        new ServiceDiscoveryInfo(
                                new PeerHandle(peerId),
                                peerCipherSuite,
                                serviceSpecificInfo,
                                matchFilterList,
                                scid,
                                pairingAlias,
                                pairingConfig,
                                vendorData),
                        distanceMm);
            });
        }
        @Override
        public void onMatchExpired(int peerId) {
            if (VDBG) {
                Log.v(TAG, "onMatchExpired: peerId=" + peerId);
            }
            mHandler.post(() ->
                    mOriginalCallback.onServiceLost(new PeerHandle(peerId),
                            WIFI_AWARE_DISCOVERY_LOST_REASON_PEER_NOT_VISIBLE));
        }

        @Override
        public void onMessageSendSuccess(int messageId) {
            if (VDBG) Log.v(TAG, "onMessageSendSuccess");
            mHandler.post(() -> mOriginalCallback.onMessageSendSucceeded(messageId));
        }

        @Override
        public void onMessageSendFail(int messageId, int reason) {
            if (VDBG) Log.v(TAG, "onMessageSendFail: reason=" + reason);
            mHandler.post(() -> mOriginalCallback.onMessageSendFailed(messageId));
        }

        @Override
        public void onMessageReceived(int peerId, byte[] message) {
            if (VDBG) {
                Log.v(TAG, "onMessageReceived: peerId=" + peerId);
            }
            mHandler.post(() -> mOriginalCallback.onMessageReceived(new PeerHandle(peerId),
                    message));
        }

        @Override
        public void onPairingSetupRequestReceived(int peerId, int requestId) {
            mHandler.post(() ->
                    mOriginalCallback.onPairingSetupRequestReceived(new PeerHandle(peerId),
                            requestId));
        }
        @Override
        public void onPairingSetupConfirmed(int peerId, boolean accept, String alias) {
            if (accept) {
                mHandler.post(() -> mOriginalCallback
                        .onPairingSetupSucceeded(new PeerHandle(peerId), alias));
            } else {
                mHandler.post(() -> mOriginalCallback
                        .onPairingSetupFailed(new PeerHandle(peerId)));
            }
        }
        @Override
        public void onPairingVerificationConfirmed(int peerId, boolean accept, String alias) {
            if (accept) {
                mHandler.post(() -> mOriginalCallback.onPairingVerificationSucceed(
                        new PeerHandle(peerId), alias));
            } else {
                mHandler.post(() -> mOriginalCallback
                        .onPairingVerificationFailed(new PeerHandle(peerId)));
            }
        }
        @Override
        public void onBootstrappingVerificationConfirmed(int peerId, boolean accept, int method) {
            if (accept) {
                mHandler.post(() -> mOriginalCallback.onBootstrappingSucceeded(
                        new PeerHandle(peerId), method));
            } else {
                mHandler.post(() -> mOriginalCallback.onBootstrappingFailed(
                        new PeerHandle(peerId)));
            }
        }

        /*
         * Proxies methods
         */
        public void onProxySessionStarted(int sessionId) {
            if (VDBG) Log.v(TAG, "Proxy: onSessionStarted: sessionId=" + sessionId);
            if (mSession != null) {
                Log.e(TAG,
                        "onSessionStarted: sessionId=" + sessionId + ": session already created!?");
                throw new IllegalStateException(
                        "onSessionStarted: sessionId=" + sessionId + ": session already created!?");
            }

            WifiAwareManager mgr = mAwareManager.get();
            if (mgr == null) {
                Log.w(TAG, "onProxySessionStarted: mgr GC'd");
                return;
            }

            if (mIsPublish) {
                PublishDiscoverySession session = new PublishDiscoverySession(mgr,
                        mClientId, sessionId);
                mSession = session;
                mOriginalCallback.onPublishStarted(session);
            } else {
                SubscribeDiscoverySession
                        session = new SubscribeDiscoverySession(mgr, mClientId, sessionId);
                mSession = session;
                mOriginalCallback.onSubscribeStarted(session);
            }
        }

        public void onProxySessionTerminated(int reason) {
            if (VDBG) Log.v(TAG, "Proxy: onSessionTerminated: reason=" + reason);
            if (mSession != null) {
                mSession.setTerminated();
                mSession = null;
            } else {
                Log.w(TAG, "Proxy: onSessionTerminated called but mSession is null!?");
            }
            mAwareManager.clear();
            mOriginalCallback.onSessionTerminated();
        }
    }

    /**
     * Set Wi-Fi Aware protocol parameters.
     * @hide
     * @param params An object contain specified parameters. Use {@code null} to remove previously
     *               set configuration and restore default behavior.
     */
    @SystemApi
    @RequiresPermission(allOf = {OVERRIDE_WIFI_CONFIG,
            CHANGE_WIFI_STATE})
    public void setAwareParams(@Nullable AwareParams params) {
        try {
            mService.setAwareParams(params);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     *  Set all Wi-Fi Aware sessions created by the calling app to be opportunistic. Opportunistic
     *  Wi-Fi Aware sessions are considered low priority and may be torn down (the sessions or the
     *  Aware interface) if there are resource conflicts.
     *
     * @param enabled True to configure all Wi-Fi Aware sessions by the calling app as
     *                Opportunistic. False by default.
     */
    @RequiresPermission(CHANGE_WIFI_STATE)
    public void setOpportunisticModeEnabled(boolean enabled) {
        try {
            mService.setOpportunisticModeEnabled(mContext.getOpPackageName(), enabled);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Indicate whether all Wi-Fi Aware sessions created by the calling app are opportunistic as
     * defined and configured by {@link #setOpportunisticModeEnabled(boolean)}
     *
     * @param executor The executor on which callback will be invoked.
     * @param resultsCallback An asynchronous callback that will return boolean
     */
    @RequiresPermission(ACCESS_WIFI_STATE)
    public void isOpportunisticModeEnabled(@NonNull @CallbackExecutor Executor executor,
            @NonNull Consumer<Boolean> resultsCallback) {
        Objects.requireNonNull(executor, "executor cannot be null");
        Objects.requireNonNull(resultsCallback, "resultsCallback cannot be null");

        try {
            mService.isOpportunisticModeEnabled(mContext.getOpPackageName(),
                    new IBooleanListener.Stub() {
                        @Override
                        public void onResult(boolean value) {
                            Binder.clearCallingIdentity();
                            executor.execute(() -> {
                                resultsCallback.accept(value);
                            });
                        }
                    });
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Reset all paired devices setup by the caller by
     * {@link DiscoverySession#initiatePairingRequest(PeerHandle, String, int, String)} and
     * {@link DiscoverySession#acceptPairingRequest(int, PeerHandle, String, int, String)}
     */
    @RequiresPermission(CHANGE_WIFI_STATE)
    public void resetPairedDevices() {
        try {
            mService.resetPairedDevices(mContext.getOpPackageName());
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Remove the target paired device setup by the caller by
     * {@link DiscoverySession#initiatePairingRequest(PeerHandle, String, int, String)} and
     * {@link DiscoverySession#acceptPairingRequest(int, PeerHandle, String, int, String)}
     * @param alias The alias set by the caller
     */
    @RequiresPermission(CHANGE_WIFI_STATE)
    public void removePairedDevice(@NonNull String alias) {
        try {
            mService.removePairedDevice(mContext.getOpPackageName(), alias);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Get all the paired devices configured by the calling app.
     * @param executor The executor on which callback will be invoked.
     * @param resultsCallback An asynchronous callback that will return a list of paired devices'
     *                        alias
     */
    @RequiresPermission(ACCESS_WIFI_STATE)
    public void getPairedDevices(@NonNull Executor executor,
            @NonNull Consumer<List<String>> resultsCallback) {
        Objects.requireNonNull(executor, "executor cannot be null");
        Objects.requireNonNull(resultsCallback, "resultsCallback cannot be null");
        try {
            mService.getPairedDevices(
                    mContext.getOpPackageName(),
                    new IListListener.Stub() {
                        public void onResult(List value) {
                            Binder.clearCallingIdentity();
                            executor.execute(() -> resultsCallback.accept(value));
                        }
                    });
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * @hide
     */
    public void suspend(int clientId, int sessionId) {
        try {
            mService.suspend(clientId, sessionId);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * @hide
     */
    public void resume(int clientId, int sessionId) {
        try {
            mService.resume(clientId, sessionId);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }
    /**
     * Attach to the Wi-Fi Aware service as an offload session. All discovery sessions and
     * connections will be handled via out-of-band connections.
     * The Aware session created by this attach method will have the lowest priority when resource
     * conflicts arise (e.g. Aware has to be torn down to create other WiFi interfaces).
     *
     * @param executor       The executor to execute the listener of the {@code attachCallback}
     *                       object.
     * @param attachCallback A callback for attach events, extended from
     *                       {@link AttachCallback}.
     * @hide
     * @see #attach(AttachCallback, Handler)
     */
    @SystemApi
    @RequiresPermission(allOf = {ACCESS_WIFI_STATE, CHANGE_WIFI_STATE, OVERRIDE_WIFI_CONFIG})
    public void attachOffload(@NonNull @CallbackExecutor Executor executor,
            @NonNull AttachCallback attachCallback) {
        if (executor == null) {
            throw new IllegalArgumentException("Null executor provided");
        }
        attach(null, null, attachCallback, null, true, executor);
    }

}
