/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.googlecode.android_scripting.facade.wifi;

import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.net.NetworkSpecifier;
import android.net.wifi.RttManager;
import android.net.wifi.RttManager.RttResult;
import android.net.wifi.WifiScanner;
import android.net.wifi.aware.AttachCallback;
import android.net.wifi.aware.ConfigRequest;
import android.net.wifi.aware.DiscoverySession;
import android.net.wifi.aware.DiscoverySessionCallback;
import android.net.wifi.aware.IdentityChangedListener;
import android.net.wifi.aware.PeerHandle;
import android.net.wifi.aware.PublishConfig;
import android.net.wifi.aware.PublishDiscoverySession;
import android.net.wifi.aware.SubscribeConfig;
import android.net.wifi.aware.SubscribeDiscoverySession;
import android.net.wifi.aware.TlvBufferUtils;
import android.net.wifi.aware.WifiAwareManager;
import android.net.wifi.aware.WifiAwareNetworkSpecifier;
import android.net.wifi.aware.WifiAwareSession;
import android.os.Bundle;
import android.os.Parcelable;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Base64;
import android.util.SparseArray;

import com.android.internal.annotations.GuardedBy;
import com.android.modules.utils.build.SdkLevel;

import libcore.util.HexEncoding;

import com.googlecode.android_scripting.facade.EventFacade;
import com.googlecode.android_scripting.facade.FacadeManager;
import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
import com.googlecode.android_scripting.rpc.Rpc;
import com.googlecode.android_scripting.rpc.RpcOptional;
import com.googlecode.android_scripting.rpc.RpcParameter;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * WifiAwareManager functions.
 */
public class WifiAwareManagerFacade extends RpcReceiver {
    private final Service mService;
    private final EventFacade mEventFacade;
    private final WifiAwareStateChangedReceiver mStateChangedReceiver;

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

    @GuardedBy("mLock")
    private WifiAwareManager mMgr;

    @GuardedBy("mLock")
    private int mNextDiscoverySessionId = 1;
    @GuardedBy("mLock")
    private SparseArray<DiscoverySession> mDiscoverySessions = new SparseArray<>();
    private int getNextDiscoverySessionId() {
        synchronized (mLock) {
            return mNextDiscoverySessionId++;
        }
    }

    @GuardedBy("mLock")
    private int mNextSessionId = 1;
    @GuardedBy("mLock")
    private SparseArray<WifiAwareSession> mSessions = new SparseArray<>();
    private int getNextSessionId() {
        synchronized (mLock) {
            return mNextSessionId++;
        }
    }

    @GuardedBy("mLock")
    private SparseArray<Long> mMessageStartTime = new SparseArray<>();

    private static final String NS_KEY_TYPE = "type";
    private static final String NS_KEY_ROLE = "role";
    private static final String NS_KEY_CLIENT_ID = "client_id";
    private static final String NS_KEY_SESSION_ID = "session_id";
    private static final String NS_KEY_PEER_ID = "peer_id";
    private static final String NS_KEY_PEER_MAC = "peer_mac";
    private static final String NS_KEY_PMK = "pmk";
    private static final String NS_KEY_PASSPHRASE = "passphrase";
    private static final String NS_KEY_PORT = "port";
    private static final String NS_KEY_TRANSPORT_PROTOCOL = "transport_protocol";

    private static String getJsonString(WifiAwareNetworkSpecifier ns) throws JSONException {
        JSONObject j = new JSONObject();

        j.put(NS_KEY_TYPE, ns.type);
        j.put(NS_KEY_ROLE, ns.role);
        j.put(NS_KEY_CLIENT_ID, ns.clientId);
        j.put(NS_KEY_SESSION_ID, ns.sessionId);
        j.put(NS_KEY_PEER_ID, ns.peerId);
        if (ns.peerMac != null) {
            j.put(NS_KEY_PEER_MAC, Base64.encodeToString(ns.peerMac, Base64.DEFAULT));
        }
        if (ns.pmk != null) {
            j.put(NS_KEY_PMK, Base64.encodeToString(ns.pmk, Base64.DEFAULT));
        }
        if (ns.passphrase != null) {
            j.put(NS_KEY_PASSPHRASE, ns.passphrase);
        }
        if (ns.port != 0) {
            j.put(NS_KEY_PORT, ns.port);
        }
        if (ns.transportProtocol != -1) {
            j.put(NS_KEY_TRANSPORT_PROTOCOL, ns.transportProtocol);
        }

        return j.toString();
    }

    public static NetworkSpecifier getNetworkSpecifier(JSONObject j) throws JSONException {
        if (j == null) {
            return null;
        }

        int type = 0, role = 0, clientId = 0, sessionId = 0, peerId = 0;
        byte[] peerMac = null;
        byte[] pmk = null;
        String passphrase = null;
        int port = 0, transportProtocol = -1;

        if (j.has(NS_KEY_TYPE)) {
            type = j.getInt((NS_KEY_TYPE));
        }
        if (j.has(NS_KEY_ROLE)) {
            role = j.getInt((NS_KEY_ROLE));
        }
        if (j.has(NS_KEY_CLIENT_ID)) {
            clientId = j.getInt((NS_KEY_CLIENT_ID));
        }
        if (j.has(NS_KEY_SESSION_ID)) {
            sessionId = j.getInt((NS_KEY_SESSION_ID));
        }
        if (j.has(NS_KEY_PEER_ID)) {
            peerId = j.getInt((NS_KEY_PEER_ID));
        }
        if (j.has(NS_KEY_PEER_MAC)) {
            peerMac = Base64.decode(j.getString(NS_KEY_PEER_MAC), Base64.DEFAULT);
        }
        if (j.has(NS_KEY_PMK)) {
            pmk = Base64.decode(j.getString(NS_KEY_PMK), Base64.DEFAULT);
        }
        if (j.has(NS_KEY_PASSPHRASE)) {
            passphrase = j.getString(NS_KEY_PASSPHRASE);
        }
        if (j.has(NS_KEY_PORT)) {
            port = j.getInt(NS_KEY_PORT);
        }
        if (j.has(NS_KEY_TRANSPORT_PROTOCOL)) {
            transportProtocol = j.getInt(NS_KEY_TRANSPORT_PROTOCOL);
        }

        return new WifiAwareNetworkSpecifier(type, role, clientId, sessionId, peerId, peerMac, pmk,
                passphrase, port, transportProtocol);
    }

    private static String getStringOrNull(JSONObject j, String name) throws JSONException {
        if (j.isNull(name)) {
            return null;
        }
        return j.getString(name);
    }

    private static ConfigRequest getConfigRequest(JSONObject j) throws JSONException {
        if (j == null) {
            return null;
        }

        ConfigRequest.Builder builder = new ConfigRequest.Builder();

        if (j.has("Support5gBand")) {
            builder.setSupport5gBand(j.getBoolean("Support5gBand"));
        }
        if (j.has("MasterPreference")) {
            builder.setMasterPreference(j.getInt("MasterPreference"));
        }
        if (j.has("ClusterLow")) {
            builder.setClusterLow(j.getInt("ClusterLow"));
        }
        if (j.has("ClusterHigh")) {
            builder.setClusterHigh(j.getInt("ClusterHigh"));
        }
        if (j.has("DiscoveryWindowInterval")) {
            JSONArray interval = j.getJSONArray("DiscoveryWindowInterval");
            if (interval.length() != 2) {
                throw new JSONException(
                        "Expect 'DiscoveryWindowInterval' to be an array with 2 elements!");
            }
            int intervalValue = interval.getInt(ConfigRequest.NAN_BAND_24GHZ);
            if (intervalValue != ConfigRequest.DW_INTERVAL_NOT_INIT) {
                builder.setDiscoveryWindowInterval(ConfigRequest.NAN_BAND_24GHZ, intervalValue);
            }
            intervalValue = interval.getInt(ConfigRequest.NAN_BAND_5GHZ);
            if (intervalValue != ConfigRequest.DW_INTERVAL_NOT_INIT) {
                builder.setDiscoveryWindowInterval(ConfigRequest.NAN_BAND_5GHZ, intervalValue);
            }
        }

        return builder.build();
    }

    private static List<byte[]> getMatchFilter(JSONArray ja) throws JSONException {
        List<byte[]> la = new ArrayList<>();
        for (int i = 0; i < ja.length(); ++i) {
            la.add(Base64.decode(ja.getString(i).getBytes(StandardCharsets.UTF_8), Base64.DEFAULT));
        }
        return la;
    }

    private static PublishConfig getPublishConfig(JSONObject j) throws JSONException {
        if (j == null) {
            return null;
        }

        PublishConfig.Builder builder = new PublishConfig.Builder();

        if (j.has("ServiceName")) {
            builder.setServiceName(getStringOrNull(j, "ServiceName"));
        }

        if (j.has("ServiceSpecificInfo")) {
            String ssi = getStringOrNull(j, "ServiceSpecificInfo");
            if (ssi != null) {
                builder.setServiceSpecificInfo(ssi.getBytes());
            }
        }

        if (j.has("MatchFilter")) {
            byte[] bytes = Base64.decode(
                    j.getString("MatchFilter").getBytes(StandardCharsets.UTF_8), Base64.DEFAULT);
            List<byte[]> mf = new TlvBufferUtils.TlvIterable(0, 1, bytes).toList();
            builder.setMatchFilter(mf);

        }

        if (!j.isNull("MatchFilterList")) {
            builder.setMatchFilter(getMatchFilter(j.getJSONArray("MatchFilterList")));
        }

        if (j.has("DiscoveryType")) {
            builder.setPublishType(j.getInt("DiscoveryType"));
        }
        if (j.has("TtlSec")) {
            builder.setTtlSec(j.getInt("TtlSec"));
        }
        if (j.has("TerminateNotificationEnabled")) {
            builder.setTerminateNotificationEnabled(j.getBoolean("TerminateNotificationEnabled"));
        }
        if (j.has("RangingEnabled")) {
            builder.setRangingEnabled(j.getBoolean("RangingEnabled"));
        }
        if (SdkLevel.isAtLeastT() && j.has("InstantModeEnabled")) {
            builder.setInstantCommunicationModeEnabled(true,
                    Objects.equals(j.getString("InstantModeEnabled"), "5G")
                            ? WifiScanner.WIFI_BAND_5_GHZ : WifiScanner.WIFI_BAND_24_GHZ);
        }

        return builder.build();
    }

    private static SubscribeConfig getSubscribeConfig(JSONObject j) throws JSONException {
        if (j == null) {
            return null;
        }

        SubscribeConfig.Builder builder = new SubscribeConfig.Builder();

        if (j.has("ServiceName")) {
            builder.setServiceName(j.getString("ServiceName"));
        }

        if (j.has("ServiceSpecificInfo")) {
            String ssi = getStringOrNull(j, "ServiceSpecificInfo");
            if (ssi != null) {
                builder.setServiceSpecificInfo(ssi.getBytes());
            }
        }

        if (j.has("MatchFilter")) {
            byte[] bytes = Base64.decode(
                    j.getString("MatchFilter").getBytes(StandardCharsets.UTF_8), Base64.DEFAULT);
            List<byte[]> mf = new TlvBufferUtils.TlvIterable(0, 1, bytes).toList();
            builder.setMatchFilter(mf);
        }

        if (!j.isNull("MatchFilterList")) {
            builder.setMatchFilter(getMatchFilter(j.getJSONArray("MatchFilterList")));
        }

        if (j.has("DiscoveryType")) {
            builder.setSubscribeType(j.getInt("DiscoveryType"));
        }
        if (j.has("TtlSec")) {
            builder.setTtlSec(j.getInt("TtlSec"));
        }
        if (j.has("TerminateNotificationEnabled")) {
            builder.setTerminateNotificationEnabled(j.getBoolean("TerminateNotificationEnabled"));
        }
        if (j.has("MinDistanceMm")) {
            builder.setMinDistanceMm(j.getInt("MinDistanceMm"));
        }
        if (j.has("MaxDistanceMm")) {
            builder.setMaxDistanceMm(j.getInt("MaxDistanceMm"));
        }
        if (SdkLevel.isAtLeastT() && j.has("InstantModeEnabled")) {
            builder.setInstantCommunicationModeEnabled(true,
                    Objects.equals(j.getString("InstantModeEnabled"), "5G")
                            ? WifiScanner.WIFI_BAND_5_GHZ : WifiScanner.WIFI_BAND_24_GHZ);
        }

        return builder.build();
    }

    public WifiAwareManagerFacade(FacadeManager manager) {
        super(manager);
        mService = manager.getService();

        mMgr = (WifiAwareManager) mService.getSystemService(Context.WIFI_AWARE_SERVICE);

        mEventFacade = manager.getReceiver(EventFacade.class);

        mStateChangedReceiver = new WifiAwareStateChangedReceiver();
        IntentFilter filter = new IntentFilter(WifiAwareManager.ACTION_WIFI_AWARE_STATE_CHANGED);
        mService.registerReceiver(mStateChangedReceiver, filter);
    }

    @Override
    public void shutdown() {
        wifiAwareDestroyAll();
        mService.unregisterReceiver(mStateChangedReceiver);
    }

    @Rpc(description = "Does the device support the Wi-Fi Aware feature?")
    public Boolean doesDeviceSupportWifiAwareFeature() {
        return mService.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI_AWARE);
    }

    @Rpc(description = "Is Aware Usage Enabled?")
    public Boolean wifiIsAwareAvailable() throws RemoteException {
        synchronized (mLock) {
            return mMgr.isAvailable();
        }
    }

    @Rpc(description = "Destroy all Aware sessions and discovery sessions")
    public void wifiAwareDestroyAll() {
        synchronized (mLock) {
            for (int i = 0; i < mSessions.size(); ++i) {
                mSessions.valueAt(i).close();
            }
            mSessions.clear();

            /* discovery sessions automatically destroyed when containing Aware sessions
             * destroyed */
            mDiscoverySessions.clear();

            mMessageStartTime.clear();
        }
    }

    @Rpc(description = "Attach to Aware.")
    public Integer wifiAwareAttach(
            @RpcParameter(name = "identityCb",
                description = "Controls whether an identity callback is provided")
                @RpcOptional Boolean identityCb,
            @RpcParameter(name = "awareConfig",
                description = "The session configuration, or null for default config")
                @RpcOptional JSONObject awareConfig,
            @RpcParameter(name = "useIdInCallbackEvent",
                description =
                    "Specifies whether the callback events should be decorated with session Id")
                @RpcOptional Boolean useIdInCallbackEvent)
            throws RemoteException, JSONException {
        synchronized (mLock) {
            int sessionId = getNextSessionId();
            boolean useIdInCallbackEventName =
                (useIdInCallbackEvent != null) ? useIdInCallbackEvent : false;
            mMgr.attach(null, getConfigRequest(awareConfig),
                    new AwareAttachCallbackPostsEvents(sessionId, useIdInCallbackEventName),
                    (identityCb != null && identityCb.booleanValue())
                        ? new AwareIdentityChangeListenerPostsEvents(sessionId,
                        useIdInCallbackEventName) : null, false, null);
            return sessionId;
        }
    }

    @Rpc(description = "Destroy a Aware session.")
    public void wifiAwareDestroy(
            @RpcParameter(name = "clientId", description = "The client ID returned when a connection was created") Integer clientId)
            throws RemoteException, JSONException {
        WifiAwareSession session;
        synchronized (mLock) {
            session = mSessions.get(clientId);
        }
        if (session == null) {
            throw new IllegalStateException(
                    "Calling WifiAwareDisconnect before session (client ID " + clientId
                            + ") is ready/or already disconnected");
        }
        session.close();
    }

    @Rpc(description = "Publish.")
    public Integer wifiAwarePublish(
            @RpcParameter(name = "clientId", description = "The client ID returned when a connection was created") Integer clientId,
            @RpcParameter(name = "publishConfig") JSONObject publishConfig,
            @RpcParameter(name = "useIdInCallbackEvent",
            description =
                "Specifies whether the callback events should be decorated with session Id")
                @RpcOptional Boolean useIdInCallbackEvent)
            throws RemoteException, JSONException {
        synchronized (mLock) {
            WifiAwareSession session = mSessions.get(clientId);
            if (session == null) {
                throw new IllegalStateException(
                        "Calling WifiAwarePublish before session (client ID " + clientId
                                + ") is ready/or already disconnected");
            }
            boolean useIdInCallbackEventName =
                (useIdInCallbackEvent != null) ? useIdInCallbackEvent : false;

            int discoverySessionId = getNextDiscoverySessionId();
            session.publish(getPublishConfig(publishConfig),
                new AwareDiscoverySessionCallbackPostsEvents(discoverySessionId,
                    useIdInCallbackEventName), null);
            return discoverySessionId;
        }
    }

    @Rpc(description = "Update Publish.")
    public void wifiAwareUpdatePublish(
        @RpcParameter(name = "sessionId", description = "The discovery session ID")
            Integer sessionId,
        @RpcParameter(name = "publishConfig", description = "Publish configuration")
            JSONObject publishConfig)
        throws RemoteException, JSONException {
        synchronized (mLock) {
            DiscoverySession session = mDiscoverySessions.get(sessionId);
            if (session == null) {
                throw new IllegalStateException(
                    "Calling wifiAwareUpdatePublish before session (session ID "
                        + sessionId + ") is ready");
            }
            if (!(session instanceof PublishDiscoverySession)) {
                throw new IllegalArgumentException(
                    "Calling wifiAwareUpdatePublish with a subscribe session ID");
            }
            ((PublishDiscoverySession) session).updatePublish(getPublishConfig(publishConfig));
        }
    }

    @Rpc(description = "Subscribe.")
    public Integer wifiAwareSubscribe(
            @RpcParameter(name = "clientId", description = "The client ID returned when a connection was created") Integer clientId,
            @RpcParameter(name = "subscribeConfig") JSONObject subscribeConfig,
            @RpcParameter(name = "useIdInCallbackEvent",
                description =
                "Specifies whether the callback events should be decorated with session Id")
                @RpcOptional Boolean useIdInCallbackEvent)
            throws RemoteException, JSONException {
        synchronized (mLock) {
            WifiAwareSession session = mSessions.get(clientId);
            if (session == null) {
                throw new IllegalStateException(
                        "Calling WifiAwareSubscribe before session (client ID " + clientId
                                + ") is ready/or already disconnected");
            }
            boolean useIdInCallbackEventName =
                (useIdInCallbackEvent != null) ? useIdInCallbackEvent : false;

            int discoverySessionId = getNextDiscoverySessionId();
            session.subscribe(getSubscribeConfig(subscribeConfig),
                new AwareDiscoverySessionCallbackPostsEvents(discoverySessionId,
                    useIdInCallbackEventName), null);
            return discoverySessionId;
        }
    }

    @Rpc(description = "Update Subscribe.")
    public void wifiAwareUpdateSubscribe(
        @RpcParameter(name = "sessionId", description = "The discovery session ID")
            Integer sessionId,
        @RpcParameter(name = "subscribeConfig", description = "Subscribe configuration")
            JSONObject subscribeConfig)
        throws RemoteException, JSONException {
        synchronized (mLock) {
            DiscoverySession session = mDiscoverySessions.get(sessionId);
            if (session == null) {
                throw new IllegalStateException(
                    "Calling wifiAwareUpdateSubscribe before session (session ID "
                        + sessionId + ") is ready");
            }
            if (!(session instanceof SubscribeDiscoverySession)) {
                throw new IllegalArgumentException(
                    "Calling wifiAwareUpdateSubscribe with a publish session ID");
            }
            ((SubscribeDiscoverySession) session)
                .updateSubscribe(getSubscribeConfig(subscribeConfig));
        }
    }

    @Rpc(description = "Destroy a discovery Session.")
    public void wifiAwareDestroyDiscoverySession(
            @RpcParameter(name = "sessionId", description = "The discovery session ID returned when session was created using publish or subscribe") Integer sessionId)
            throws RemoteException {
        synchronized (mLock) {
            DiscoverySession session = mDiscoverySessions.get(sessionId);
            if (session == null) {
                throw new IllegalStateException(
                        "Calling WifiAwareTerminateSession before session (session ID "
                                + sessionId + ") is ready");
            }
            session.close();
            mDiscoverySessions.remove(sessionId);
        }
    }

    @Rpc(description = "Send peer-to-peer Aware message")
    public void wifiAwareSendMessage(
            @RpcParameter(name = "sessionId", description = "The session ID returned when session"
                    + " was created using publish or subscribe") Integer sessionId,
            @RpcParameter(name = "peerId", description = "The ID of the peer being communicated "
                    + "with. Obtained from a previous message or match session.") Integer peerId,
            @RpcParameter(name = "messageId", description = "Arbitrary handle used for "
                    + "identification of the message in the message status callbacks")
                    Integer messageId,
            @RpcParameter(name = "message") String message,
            @RpcParameter(name = "retryCount", description = "Number of retries (0 for none) if "
                    + "transmission fails due to no ACK reception") Integer retryCount)
                    throws RemoteException {
        DiscoverySession session;
        synchronized (mLock) {
            session = mDiscoverySessions.get(sessionId);
        }
        if (session == null) {
            throw new IllegalStateException(
                    "Calling WifiAwareSendMessage before session (session ID " + sessionId
                            + " is ready");
        }
        byte[] bytes = null;
        if (message != null) {
            bytes = message.getBytes();
        }

        synchronized (mLock) {
            mMessageStartTime.put(messageId, System.currentTimeMillis());
        }
        session.sendMessage(new PeerHandle(peerId), messageId, bytes, retryCount);
    }

    @Rpc(description = "Create a network specifier to be used when specifying a Aware network request")
    public String wifiAwareCreateNetworkSpecifier(
            @RpcParameter(name = "sessionId", description = "The session ID returned when session was created using publish or subscribe")
                    Integer sessionId,
            @RpcParameter(name = "peerId", description = "The ID of the peer (obtained through OnMatch or OnMessageReceived")
                    Integer peerId,
            @RpcParameter(name = "passphrase",
                description = "Passphrase of the data-path. Optional, can be empty/null.")
                @RpcOptional String passphrase,
            @RpcParameter(name = "pmk",
                description = "PMK of the data-path (base64 encoded). Optional, can be empty/null.")
                @RpcOptional String pmk,
            @RpcParameter(name = "port", description = "Port") @RpcOptional Integer port,
            @RpcParameter(name = "transportProtocol", description = "Transport protocol")
                @RpcOptional Integer transportProtocol) throws JSONException {
        DiscoverySession session;
        synchronized (mLock) {
            session = mDiscoverySessions.get(sessionId);
        }
        if (session == null) {
            throw new IllegalStateException(
                    "Calling wifiAwareCreateNetworkSpecifier before session (session ID "
                            + sessionId + " is ready");
        }
        PeerHandle peerHandle = null;
        if (peerId != null) {
            peerHandle = new PeerHandle(peerId);
        }
        byte[] pmkDecoded = null;
        if (!TextUtils.isEmpty(pmk)) {
            pmkDecoded = Base64.decode(pmk, Base64.DEFAULT);
        }

        WifiAwareNetworkSpecifier ns = new WifiAwareNetworkSpecifier(
                (peerHandle == null) ? WifiAwareNetworkSpecifier.NETWORK_SPECIFIER_TYPE_IB_ANY_PEER
                        : WifiAwareNetworkSpecifier.NETWORK_SPECIFIER_TYPE_IB,
                session instanceof SubscribeDiscoverySession
                        ? WifiAwareManager.WIFI_AWARE_DATA_PATH_ROLE_INITIATOR
                        : WifiAwareManager.WIFI_AWARE_DATA_PATH_ROLE_RESPONDER,
                session.getClientId(),
                session.getSessionId(),
                peerHandle != null ? peerHandle.peerId : 0, // 0 is an invalid peer ID
                null, // peerMac (not used in this method)
                pmkDecoded,
                passphrase,
                port == null ? 0 : port.intValue(),
                transportProtocol == null ? -1 : transportProtocol.intValue());

        return getJsonString(ns);
    }

    @Rpc(description = "Create a network specifier to be used when specifying an OOB Aware network request")
    public String wifiAwareCreateNetworkSpecifierOob(
            @RpcParameter(name = "clientId",
                    description = "The client ID")
                    Integer clientId,
            @RpcParameter(name = "role", description = "The role: INITIATOR(0), RESPONDER(1)")
                    Integer role,
            @RpcParameter(name = "peerMac",
                    description = "The MAC address of the peer")
                    String peerMac,
            @RpcParameter(name = "passphrase",
                    description = "Passphrase of the data-path. Optional, can be empty/null.")
            @RpcOptional String passphrase,
            @RpcParameter(name = "pmk",
                    description = "PMK of the data-path (base64). Optional, can be empty/null.")
            @RpcOptional String pmk) throws JSONException {
        WifiAwareSession session;
        synchronized (mLock) {
            session = mSessions.get(clientId);
        }
        if (session == null) {
            throw new IllegalStateException(
                    "Calling wifiAwareCreateNetworkSpecifierOob before session (client ID "
                            + clientId + " is ready");
        }
        byte[] peerMacBytes = null;
        if (peerMac != null) {
            peerMacBytes = HexEncoding.decode(peerMac.toCharArray(), false);
        }
        byte[] pmkDecoded = null;
        if (!TextUtils.isEmpty(pmk)) {
            pmkDecoded = Base64.decode(pmk, Base64.DEFAULT);
        }

        WifiAwareNetworkSpecifier ns = new WifiAwareNetworkSpecifier(
                (peerMacBytes == null) ?
                        WifiAwareNetworkSpecifier.NETWORK_SPECIFIER_TYPE_OOB_ANY_PEER
                        : WifiAwareNetworkSpecifier.NETWORK_SPECIFIER_TYPE_OOB,
                role,
                session.getClientId(),
                0, // 0 is an invalid session ID
                0, // 0 is an invalid peer ID
                peerMacBytes,
                pmkDecoded,
                passphrase,
                0, // no port for OOB
                -1); // no transport protocol for OOB

        return getJsonString(ns);
    }

    private class AwareAttachCallbackPostsEvents extends AttachCallback {
        private int mSessionId;
        private long mCreateTimestampMs;
        private boolean mUseIdInCallbackEventName;

        public AwareAttachCallbackPostsEvents(int sessionId, boolean useIdInCallbackEventName) {
            mSessionId = sessionId;
            mCreateTimestampMs = System.currentTimeMillis();
            mUseIdInCallbackEventName = useIdInCallbackEventName;
        }

        @Override
        public void onAttached(WifiAwareSession session) {
            synchronized (mLock) {
                mSessions.put(mSessionId, session);
            }

            Bundle mResults = new Bundle();
            mResults.putInt("sessionId", mSessionId);
            mResults.putLong("latencyMs", System.currentTimeMillis() - mCreateTimestampMs);
            mResults.putLong("timestampMs", System.currentTimeMillis());
            if (mUseIdInCallbackEventName) {
                mEventFacade.postEvent("WifiAwareOnAttached_" + mSessionId, mResults);
            } else {
                mEventFacade.postEvent("WifiAwareOnAttached", mResults);
            }
        }

        @Override
        public void onAttachFailed() {
            Bundle mResults = new Bundle();
            mResults.putInt("sessionId", mSessionId);
            mResults.putLong("latencyMs", System.currentTimeMillis() - mCreateTimestampMs);
            if (mUseIdInCallbackEventName) {
                mEventFacade.postEvent("WifiAwareOnAttachFailed_" + mSessionId, mResults);
            } else {
                mEventFacade.postEvent("WifiAwareOnAttachFailed", mResults);
            }
        }
    }

    private class AwareIdentityChangeListenerPostsEvents extends IdentityChangedListener {
        private int mSessionId;
        private boolean mUseIdInCallbackEventName;

        public AwareIdentityChangeListenerPostsEvents(int sessionId,
            boolean useIdInCallbackEventName) {
            mSessionId = sessionId;
            mUseIdInCallbackEventName = useIdInCallbackEventName;
        }

        @Override
        public void onIdentityChanged(byte[] mac) {
            Bundle mResults = new Bundle();
            mResults.putInt("sessionId", mSessionId);
            mResults.putString("mac", String.valueOf(HexEncoding.encode(mac)));
            mResults.putLong("timestampMs", System.currentTimeMillis());
            if (mUseIdInCallbackEventName) {
                mEventFacade.postEvent("WifiAwareOnIdentityChanged_" + mSessionId, mResults);
            } else {
                mEventFacade.postEvent("WifiAwareOnIdentityChanged", mResults);
            }
        }
    }

    private class AwareDiscoverySessionCallbackPostsEvents extends
            DiscoverySessionCallback {
        private int mDiscoverySessionId;
        private boolean mUseIdInCallbackEventName;
        private long mCreateTimestampMs;

        public AwareDiscoverySessionCallbackPostsEvents(int discoverySessionId,
                boolean useIdInCallbackEventName) {
            mDiscoverySessionId = discoverySessionId;
            mUseIdInCallbackEventName = useIdInCallbackEventName;
            mCreateTimestampMs = System.currentTimeMillis();
        }

        private void postEvent(String eventName, Bundle results) {
            String finalEventName = eventName;
            if (mUseIdInCallbackEventName) {
                finalEventName += "_" + mDiscoverySessionId;
            }

            mEventFacade.postEvent(finalEventName, results);
        }

        @Override
        public void onPublishStarted(PublishDiscoverySession discoverySession) {
            synchronized (mLock) {
                mDiscoverySessions.put(mDiscoverySessionId, discoverySession);
            }

            Bundle mResults = new Bundle();
            mResults.putInt("discoverySessionId", mDiscoverySessionId);
            mResults.putLong("latencyMs", System.currentTimeMillis() - mCreateTimestampMs);
            mResults.putLong("timestampMs", System.currentTimeMillis());
            postEvent("WifiAwareSessionOnPublishStarted", mResults);
        }

        @Override
        public void onSubscribeStarted(SubscribeDiscoverySession discoverySession) {
            synchronized (mLock) {
                mDiscoverySessions.put(mDiscoverySessionId, discoverySession);
            }

            Bundle mResults = new Bundle();
            mResults.putInt("discoverySessionId", mDiscoverySessionId);
            mResults.putLong("latencyMs", System.currentTimeMillis() - mCreateTimestampMs);
            mResults.putLong("timestampMs", System.currentTimeMillis());
            postEvent("WifiAwareSessionOnSubscribeStarted", mResults);
        }

        @Override
        public void onSessionConfigUpdated() {
            Bundle mResults = new Bundle();
            mResults.putInt("discoverySessionId", mDiscoverySessionId);
            postEvent("WifiAwareSessionOnSessionConfigUpdated", mResults);
        }

        @Override
        public void onSessionConfigFailed() {
            Bundle mResults = new Bundle();
            mResults.putInt("discoverySessionId", mDiscoverySessionId);
            postEvent("WifiAwareSessionOnSessionConfigFailed", mResults);
        }

        @Override
        public void onSessionTerminated() {
            Bundle mResults = new Bundle();
            mResults.putInt("discoverySessionId", mDiscoverySessionId);
            postEvent("WifiAwareSessionOnSessionTerminated", mResults);
        }

        private Bundle createServiceDiscoveredBaseBundle(PeerHandle peerHandle,
                byte[] serviceSpecificInfo, List<byte[]> matchFilter) {
            Bundle mResults = new Bundle();
            mResults.putInt("discoverySessionId", mDiscoverySessionId);
            mResults.putInt("peerId", peerHandle.peerId);
            mResults.putByteArray("serviceSpecificInfo", serviceSpecificInfo);
            mResults.putByteArray("matchFilter", new TlvBufferUtils.TlvConstructor(0,
                    1).allocateAndPut(matchFilter).getArray());
            ArrayList<String> matchFilterStrings = new ArrayList<>(matchFilter.size());
            for (byte[] be: matchFilter) {
                matchFilterStrings.add(Base64.encodeToString(be, Base64.DEFAULT));
            }
            mResults.putStringArrayList("matchFilterList", matchFilterStrings);
            mResults.putLong("timestampMs", System.currentTimeMillis());
            return mResults;
        }

        @Override
        public void onServiceDiscovered(PeerHandle peerHandle,
                byte[] serviceSpecificInfo, List<byte[]> matchFilter) {
            Bundle mResults = createServiceDiscoveredBaseBundle(peerHandle, serviceSpecificInfo,
                    matchFilter);
            postEvent("WifiAwareSessionOnServiceDiscovered", mResults);
        }

        @Override
        public void onServiceDiscoveredWithinRange(PeerHandle peerHandle,
                byte[] serviceSpecificInfo,
                List<byte[]> matchFilter, int distanceMm) {
            Bundle mResults = createServiceDiscoveredBaseBundle(peerHandle, serviceSpecificInfo,
                    matchFilter);
            mResults.putInt("distanceMm", distanceMm);
            postEvent("WifiAwareSessionOnServiceDiscovered", mResults);
        }

        @Override
        public void onMessageSendSucceeded(int messageId) {
            Bundle mResults = new Bundle();
            mResults.putInt("discoverySessionId", mDiscoverySessionId);
            mResults.putInt("messageId", messageId);
            synchronized (mLock) {
                Long startTime = mMessageStartTime.get(messageId);
                if (startTime != null) {
                    mResults.putLong("latencyMs",
                            System.currentTimeMillis() - startTime.longValue());
                    mMessageStartTime.remove(messageId);
                }
            }
            postEvent("WifiAwareSessionOnMessageSent", mResults);
        }

        @Override
        public void onMessageSendFailed(int messageId) {
            Bundle mResults = new Bundle();
            mResults.putInt("discoverySessionId", mDiscoverySessionId);
            mResults.putInt("messageId", messageId);
            synchronized (mLock) {
                Long startTime = mMessageStartTime.get(messageId);
                if (startTime != null) {
                    mResults.putLong("latencyMs",
                            System.currentTimeMillis() - startTime.longValue());
                    mMessageStartTime.remove(messageId);
                }
            }
            postEvent("WifiAwareSessionOnMessageSendFailed", mResults);
        }

        @Override
        public void onMessageReceived(PeerHandle peerHandle, byte[] message) {
            Bundle mResults = new Bundle();
            mResults.putInt("discoverySessionId", mDiscoverySessionId);
            mResults.putInt("peerId", peerHandle.peerId);
            mResults.putByteArray("message", message); // TODO: base64
            mResults.putString("messageAsString", new String(message));
            postEvent("WifiAwareSessionOnMessageReceived", mResults);
        }

        @Override
        public void onServiceLost(PeerHandle peerHandle, @WifiAwareManager.DiscoveryLostReasonCode
                int reason) {
            Bundle mResults = new Bundle();
            mResults.putInt("discoverySessionId", mDiscoverySessionId);
            mResults.putInt("peerId", peerHandle.peerId);
            mResults.putInt("lostReason", reason);
            postEvent("WifiAwareSessionOnServiceLost", mResults);
        }
    }

    class WifiAwareRangingListener implements RttManager.RttListener {
        private int mCallbackId;
        private int mSessionId;

        public WifiAwareRangingListener(int callbackId, int sessionId) {
            mCallbackId = callbackId;
            mSessionId = sessionId;
        }

        @Override
        public void onSuccess(RttResult[] results) {
            Bundle bundle = new Bundle();
            bundle.putInt("callbackId", mCallbackId);
            bundle.putInt("sessionId", mSessionId);

            Parcelable[] resultBundles = new Parcelable[results.length];
            for (int i = 0; i < results.length; i++) {
                resultBundles[i] = WifiRttManagerFacade.RangingListener.packRttResult(results[i]);
            }
            bundle.putParcelableArray("Results", resultBundles);

            mEventFacade.postEvent("WifiAwareRangingListenerOnSuccess", bundle);
        }

        @Override
        public void onFailure(int reason, String description) {
            Bundle bundle = new Bundle();
            bundle.putInt("callbackId", mCallbackId);
            bundle.putInt("sessionId", mSessionId);
            bundle.putInt("reason", reason);
            bundle.putString("description", description);
            mEventFacade.postEvent("WifiAwareRangingListenerOnFailure", bundle);
        }

        @Override
        public void onAborted() {
            Bundle bundle = new Bundle();
            bundle.putInt("callbackId", mCallbackId);
            bundle.putInt("sessionId", mSessionId);
            mEventFacade.postEvent("WifiAwareRangingListenerOnAborted", bundle);
        }

    }

    class WifiAwareStateChangedReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context c, Intent intent) {
            boolean isAvailable = mMgr.isAvailable();
            if (!isAvailable) {
                wifiAwareDestroyAll();
            }
            mEventFacade.postEvent(isAvailable ? "WifiAwareAvailable" : "WifiAwareNotAvailable",
                    new Bundle());
        }
    }
}
