/*
 * Copyright (C) 2013 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.android.bluetooth.gatt;

import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.IBluetoothGatt;
import android.bluetooth.IBluetoothGattCallback;
import android.bluetooth.IBluetoothGattServerCallback;
import android.content.Intent;
import android.os.IBinder;
import android.os.IBinder.DeathRecipient;
import android.os.ParcelUuid;
import android.os.RemoteException;
import android.util.Log;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

import com.android.bluetooth.btservice.ProfileService;
import com.android.bluetooth.btservice.ProfileService.IProfileServiceBinder;

/**
 * Provides Bluetooth Gatt profile, as a service in
 * the Bluetooth application.
 * @hide
 */
public class GattService extends ProfileService {
    private static final boolean DBG = GattServiceConfig.DBG;
    private static final String TAG = GattServiceConfig.TAG_PREFIX + "GattService";
    BluetoothAdapter mAdapter = BluetoothAdapter.getDefaultAdapter();

    /**
     * Search queue to serialize remote onbject inspection.
     */
    SearchQueue mSearchQueue = new SearchQueue();

    /**
     * List of our registered clients.
     */

    class ClientMap extends ContextMap<IBluetoothGattCallback> {}
    ClientMap mClientMap = new ClientMap();

    /**
     * List of our registered server apps.
     */
    class ServerMap extends ContextMap<IBluetoothGattServerCallback> {}
    ServerMap mServerMap = new ServerMap();

    /**
     * Server handle map.
     */
    HandleMap mHandleMap = new HandleMap();

    /**
     * Pending service declaration queue
     */
    private List<ServiceDeclaration> mServiceDeclarations = new ArrayList<ServiceDeclaration>();

    private ServiceDeclaration addDeclaration() {
        synchronized (mServiceDeclarations) {
            mServiceDeclarations.add(new ServiceDeclaration());
        }
        return getActiveDeclaration();
    }

    private ServiceDeclaration getActiveDeclaration() {
        synchronized (mServiceDeclarations) {
            if (mServiceDeclarations.size() > 0)
                return mServiceDeclarations.get(mServiceDeclarations.size() - 1);
        }
        return null;
    }

    private ServiceDeclaration getPendingDeclaration() {
        synchronized (mServiceDeclarations) {
            if (mServiceDeclarations.size() > 0)
                return mServiceDeclarations.get(0);
        }
        return null;
    }

    private void removePendingDeclaration() {
        synchronized (mServiceDeclarations) {
            if (mServiceDeclarations.size() > 0)
                mServiceDeclarations.remove(0);
        }
    }

    /**
     * List of clients intereste in scan results.
     */
    private List<ScanClient> mScanQueue = new ArrayList<ScanClient>();

    private ScanClient getScanClient(int appIf, boolean isServer) {
        for(ScanClient client : mScanQueue) {
            if (client.appIf == appIf && client.isServer == isServer) {
                return client;
            }
        }
        return null;
    }

    private void removeScanClient(int appIf, boolean isServer) {
        for(ScanClient client : mScanQueue) {
            if (client.appIf == appIf && client.isServer == isServer) {
                mScanQueue.remove(client);
                break;
            }
        }
    }

    /**
     * Reliable write queue
     */
    private Set<String> mReliableQueue = new HashSet<String>();

    static {
        classInitNative();
    }

    protected String getName() {
        return TAG;
    }

    protected IProfileServiceBinder initBinder() {
        return new BluetoothGattBinder(this);
    }

    protected boolean start() {
        if (DBG) Log.d(TAG, "start()");
        initializeNative();
        return true;
    }

    protected boolean stop() {
        if (DBG) Log.d(TAG, "stop()");
        mClientMap.clear();
        mServerMap.clear();
        mSearchQueue.clear();
        mScanQueue.clear();
        mHandleMap.clear();
        mServiceDeclarations.clear();
        mReliableQueue.clear();
        return true;
    }

    protected boolean cleanup() {
        if (DBG) Log.d(TAG, "cleanup()");
        cleanupNative();
        return true;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (GattDebugUtils.handleDebugAction(this, intent)) {
            return Service.START_NOT_STICKY;
        }
        return super.onStartCommand(intent, flags, startId);
    }

    /**
     * DeathReceipient handlers used to unregister applications that
     * disconnect ungracefully (ie. crash or forced close).
     */

    class ClientDeathRecipient implements IBinder.DeathRecipient {
        int mAppIf;

        public ClientDeathRecipient(int appIf) {
            mAppIf = appIf;
        }

        public void binderDied() {
            if (DBG) Log.d(TAG, "Binder is dead - unregistering client (" + mAppIf + ")!");
            unregisterClient(mAppIf);
        }
    }

    class ServerDeathRecipient implements IBinder.DeathRecipient {
        int mAppIf;

        public ServerDeathRecipient(int appIf) {
            mAppIf = appIf;
        }

        public void binderDied() {
            if (DBG) Log.d(TAG, "Binder is dead - unregistering server (" + mAppIf + ")!");
            unregisterServer(mAppIf);
        }
    }

    /**
     * Handlers for incoming service calls
     */
    private static class BluetoothGattBinder extends IBluetoothGatt.Stub implements IProfileServiceBinder {
        private GattService mService;

        public BluetoothGattBinder(GattService svc) {
            mService = svc;
        }

        public boolean cleanup()  {
            mService = null;
            return true;
        }

        private GattService getService() {
            if (mService  != null && mService.isAvailable()) return mService;
            Log.e(TAG, "getService() - Service requested, but not available!");
            return null;
        }

        public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
            GattService service = getService();
            if (service == null) return new ArrayList<BluetoothDevice>();
            return service.getDevicesMatchingConnectionStates(states);
        }

        public void registerClient(ParcelUuid uuid, IBluetoothGattCallback callback) {
            GattService service = getService();
            if (service == null) return;
            service.registerClient(uuid.getUuid(), callback);
        }

        public void unregisterClient(int clientIf) {
            GattService service = getService();
            if (service == null) return;
            service.unregisterClient(clientIf);
        }

        public void startScan(int appIf, boolean isServer) {
            GattService service = getService();
            if (service == null) return;
            service.startScan(appIf, isServer);
        }

        public void startScanWithUuids(int appIf, boolean isServer, ParcelUuid[] ids) {
            GattService service = getService();
            if (service == null) return;
            UUID[] uuids = new UUID[ids.length];
            for(int i = 0; i != ids.length; ++i) {
                uuids[i] = ids[i].getUuid();
            }
            service.startScanWithUuids(appIf, isServer, uuids);
        }

        public void stopScan(int appIf, boolean isServer) {
            GattService service = getService();
            if (service == null) return;
            service.stopScan(appIf, isServer);
        }

        public void clientConnect(int clientIf, String address, boolean isDirect) {
            GattService service = getService();
            if (service == null) return;
            service.clientConnect(clientIf, address, isDirect);
        }

        public void clientDisconnect(int clientIf, String address) {
            GattService service = getService();
            if (service == null) return;
            service.clientDisconnect(clientIf, address);
        }

        public void refreshDevice(int clientIf, String address) {
            GattService service = getService();
            if (service == null) return;
            service.refreshDevice(clientIf, address);
        }

        public void discoverServices(int clientIf, String address) {
            GattService service = getService();
            if (service == null) return;
            service.discoverServices(clientIf, address);
        }

        public void readCharacteristic(int clientIf, String address, int srvcType,
                                       int srvcInstanceId, ParcelUuid srvcId,
                                       int charInstanceId, ParcelUuid charId,
                                       int authReq) {
            GattService service = getService();
            if (service == null) return;
            service.readCharacteristic(clientIf, address, srvcType, srvcInstanceId,
                                       srvcId.getUuid(), charInstanceId,
                                       charId.getUuid(), authReq);
        }

        public void writeCharacteristic(int clientIf, String address, int srvcType,
                             int srvcInstanceId, ParcelUuid srvcId,
                             int charInstanceId, ParcelUuid charId,
                             int writeType, int authReq, byte[] value) {
            GattService service = getService();
            if (service == null) return;
            service.writeCharacteristic(clientIf, address, srvcType, srvcInstanceId,
                                       srvcId.getUuid(), charInstanceId,
                                       charId.getUuid(), writeType, authReq,
                                       value);
        }

        public void readDescriptor(int clientIf, String address, int srvcType,
                            int srvcInstanceId, ParcelUuid srvcId,
                            int charInstanceId, ParcelUuid charId,
                            ParcelUuid descrId, int authReq) {
            GattService service = getService();
            if (service == null) return;
            service.readDescriptor(clientIf, address, srvcType, srvcInstanceId,
                                       srvcId.getUuid(), charInstanceId,
                                       charId.getUuid(), descrId.getUuid(),
                                       authReq);
        }

        public void writeDescriptor(int clientIf, String address, int srvcType,
                            int srvcInstanceId, ParcelUuid srvcId,
                            int charInstanceId, ParcelUuid charId,
                            ParcelUuid descrId, int writeType,
                            int authReq, byte[] value) {
            GattService service = getService();
            if (service == null) return;
            service.writeDescriptor(clientIf, address, srvcType, srvcInstanceId,
                                       srvcId.getUuid(), charInstanceId,
                                       charId.getUuid(), descrId.getUuid(),
                                       writeType, authReq, value);
        }

        public void beginReliableWrite(int clientIf, String address) {
            GattService service = getService();
            if (service == null) return;
            service.beginReliableWrite(clientIf, address);
        }

        public void endReliableWrite(int clientIf, String address, boolean execute) {
            GattService service = getService();
            if (service == null) return;
            service.endReliableWrite(clientIf, address, execute);
        }

        public void registerForNotification(int clientIf, String address, int srvcType,
                            int srvcInstanceId, ParcelUuid srvcId,
                            int charInstanceId, ParcelUuid charId,
                            boolean enable) {
            GattService service = getService();
            if (service == null) return;
            service.registerForNotification(clientIf, address, srvcType, srvcInstanceId,
                                       srvcId.getUuid(), charInstanceId,
                                       charId.getUuid(), enable);
        }

        public void readRemoteRssi(int clientIf, String address) {
            GattService service = getService();
            if (service == null) return;
            service.readRemoteRssi(clientIf, address);
        }

        public void registerServer(ParcelUuid uuid, IBluetoothGattServerCallback callback) {
            GattService service = getService();
            if (service == null) return;
            service.registerServer(uuid.getUuid(), callback);
        }

        public void unregisterServer(int serverIf) {
            GattService service = getService();
            if (service == null) return;
            service.unregisterServer(serverIf);
        }

        public void serverConnect(int serverIf, String address, boolean isDirect) {
            GattService service = getService();
            if (service == null) return;
            service.serverConnect(serverIf, address, isDirect);
        }

        public void serverDisconnect(int serverIf, String address) {
            GattService service = getService();
            if (service == null) return;
            service.serverDisconnect(serverIf, address);
        }

        public void beginServiceDeclaration(int serverIf, int srvcType,
                                            int srvcInstanceId, int minHandles,
                                            ParcelUuid srvcId) {
            GattService service = getService();
            if (service == null) return;
            service.beginServiceDeclaration(serverIf, srvcType, srvcInstanceId,
                               minHandles, srvcId.getUuid());
        }

        public void addIncludedService(int serverIf, int srvcType,
                            int srvcInstanceId, ParcelUuid srvcId) {
            GattService service = getService();
            if (service == null) return;
            service.addIncludedService(serverIf, srvcType, srvcInstanceId,
                                            srvcId.getUuid());
        }

        public void addCharacteristic(int serverIf, ParcelUuid charId,
                            int properties, int permissions) {
            GattService service = getService();
            if (service == null) return;
            service.addCharacteristic(serverIf, charId.getUuid(), properties,
                                      permissions);
        }

        public void addDescriptor(int serverIf, ParcelUuid descId,
                           int permissions) {
            GattService service = getService();
            if (service == null) return;
            service.addDescriptor(serverIf, descId.getUuid(), permissions);
        }

        public void endServiceDeclaration(int serverIf) {
            GattService service = getService();
            if (service == null) return;
            service.endServiceDeclaration(serverIf);
        }

        public void removeService(int serverIf, int srvcType,
                           int srvcInstanceId, ParcelUuid srvcId) {
            GattService service = getService();
            if (service == null) return;
            service.removeService(serverIf, srvcType, srvcInstanceId,
                                  srvcId.getUuid());
        }

        public void clearServices(int serverIf) {
            GattService service = getService();
            if (service == null) return;
            service.clearServices(serverIf);
        }

        public void sendResponse(int serverIf, String address, int requestId,
                                 int status, int offset, byte[] value) {
            GattService service = getService();
            if (service == null) return;
            service.sendResponse(serverIf, address, requestId, status, offset, value);
        }

        public void sendNotification(int serverIf, String address, int srvcType,
                                              int srvcInstanceId, ParcelUuid srvcId,
                                              int charInstanceId, ParcelUuid charId,
                                              boolean confirm, byte[] value) {
            GattService service = getService();
            if (service == null) return;
            service.sendNotification(serverIf, address, srvcType, srvcInstanceId,
                srvcId.getUuid(), charInstanceId, charId.getUuid(), confirm, value);
        }

    };

    /**************************************************************************
     * Callback functions - CLIENT
     *************************************************************************/

    void onScanResult(String address, int rssi, byte[] adv_data) {
        if (DBG) Log.d(TAG, "onScanResult() - address=" + address
                    + ", rssi=" + rssi);

        List<UUID> remoteUuids = parseUuids(adv_data);
        for (ScanClient client : mScanQueue) {
            if (client.uuids.length > 0) {
                int matches = 0;
                for (UUID search : client.uuids) {
                    for (UUID remote: remoteUuids) {
                        if (remote.equals(search)) {
                            ++matches;
                            break; // Only count 1st match in case of duplicates
                        }
                    }
                }

                if (matches < client.uuids.length) continue;
            }

            if (!client.isServer) {
                ClientMap.App app = mClientMap.getById(client.appIf);
                if (app != null) {
                    try {
                        app.callback.onScanResult(address, rssi, adv_data);
                    } catch (RemoteException e) {
                        Log.e(TAG, "Exception: " + e);
                        mClientMap.remove(client.appIf);
                        mScanQueue.remove(client);
                    }
                }
            } else {
                ServerMap.App app = mServerMap.getById(client.appIf);
                if (app != null) {
                    try {
                        app.callback.onScanResult(address, rssi, adv_data);
                    } catch (RemoteException e) {
                        Log.e(TAG, "Exception: " + e);
                        mServerMap.remove(client.appIf);
                        mScanQueue.remove(client);
                    }
                }
            }
        }
    }

    void onClientRegistered(int status, int clientIf, long uuidLsb, long uuidMsb)
            throws RemoteException {
        UUID uuid = new UUID(uuidMsb, uuidLsb);
        if (DBG) Log.d(TAG, "onClientRegistered() - UUID=" + uuid + ", clientIf=" + clientIf);
        ClientMap.App app = mClientMap.getByUuid(uuid);
        if (app != null) {
            app.id = clientIf;
            app.linkToDeath(new ClientDeathRecipient(clientIf));
            app.callback.onClientRegistered(status, clientIf);
        }
    }

    void onConnected(int clientIf, int connId, int status, String address)
            throws RemoteException  {
        if (DBG) Log.d(TAG, "onConnected() - clientIf=" + clientIf
            + ", connId=" + connId + ", address=" + address);

        if (status == 0) mClientMap.addConnection(clientIf, connId, address);
        ClientMap.App app = mClientMap.getById(clientIf);
        if (app != null) {
            app.callback.onClientConnectionState(status, clientIf, true, address);
        }
    }

    void onDisconnected(int clientIf, int connId, int status, String address)
            throws RemoteException {
        if (DBG) Log.d(TAG, "onDisconnected() - clientIf=" + clientIf
            + ", connId=" + connId + ", address=" + address);

        mClientMap.removeConnection(clientIf, connId);
        mSearchQueue.removeConnId(connId);
        ClientMap.App app = mClientMap.getById(clientIf);
        if (app != null) {
            app.callback.onClientConnectionState(status, clientIf, false, address);
        }
    }

    void onSearchCompleted(int connId, int status) throws RemoteException {
        if (DBG) Log.d(TAG, "onSearchCompleted() - connId=" + connId+ ", status=" + status);
        // We got all services, now let's explore characteristics...
        continueSearch(connId, status);
    }

    void onSearchResult(int connId, int srvcType,
            int srvcInstId, long srvcUuidLsb, long srvcUuidMsb)
            throws RemoteException {
        UUID uuid = new UUID(srvcUuidMsb, srvcUuidLsb);
        String address = mClientMap.addressByConnId(connId);

        if (DBG) Log.d(TAG, "onSearchResult() - address=" + address + ", uuid=" + uuid);

        mSearchQueue.add(connId, srvcType, srvcInstId, srvcUuidLsb, srvcUuidMsb);

        ClientMap.App app = mClientMap.getByConnId(connId);
        if (app != null) {
            app.callback.onGetService(address, srvcType, srvcInstId,
                                        new ParcelUuid(uuid));
        }
    }

    void onGetCharacteristic(int connId, int status, int srvcType,
            int srvcInstId, long srvcUuidLsb, long srvcUuidMsb,
            int charInstId, long charUuidLsb, long charUuidMsb,
            int charProp) throws RemoteException {

        UUID srvcUuid = new UUID(srvcUuidMsb, srvcUuidLsb);
        UUID charUuid = new UUID(charUuidMsb, charUuidLsb);
        String address = mClientMap.addressByConnId(connId);

        if (DBG) Log.d(TAG, "onGetCharacteristic() - address=" + address
            + ", status=" + status + ", charUuid=" + charUuid + ", prop=" + charProp);

        if (status == 0) {
            mSearchQueue.add(connId, srvcType,
                            srvcInstId, srvcUuidLsb, srvcUuidMsb,
                            charInstId, charUuidLsb, charUuidMsb);

            ClientMap.App app = mClientMap.getByConnId(connId);
            if (app != null) {
                app.callback.onGetCharacteristic(address, srvcType,
                            srvcInstId, new ParcelUuid(srvcUuid),
                            charInstId, new ParcelUuid(charUuid), charProp);
            }

            // Get next characteristic in the current service
            gattClientGetCharacteristicNative(connId, srvcType,
                                        srvcInstId, srvcUuidLsb, srvcUuidMsb,
                                        charInstId, charUuidLsb, charUuidMsb);
        } else {
            // Check for included services next
            gattClientGetIncludedServiceNative(connId,
                srvcType, srvcInstId, srvcUuidLsb, srvcUuidMsb,
                0,0,0,0);
        }
    }

    void onGetDescriptor(int connId, int status, int srvcType,
            int srvcInstId, long srvcUuidLsb, long srvcUuidMsb,
            int charInstId, long charUuidLsb, long charUuidMsb,
            long descrUuidLsb, long descrUuidMsb) throws RemoteException {

        UUID srvcUuid = new UUID(srvcUuidMsb, srvcUuidLsb);
        UUID charUuid = new UUID(charUuidMsb, charUuidLsb);
        UUID descUuid = new UUID(descrUuidMsb, descrUuidLsb);
        String address = mClientMap.addressByConnId(connId);

        if (DBG) Log.d(TAG, "onGetDescriptor() - address=" + address
            + ", status=" + status + ", descUuid=" + descUuid);

        if (status == 0) {
            ClientMap.App app = mClientMap.getByConnId(connId);
            if (app != null) {
                app.callback.onGetDescriptor(address, srvcType,
                            srvcInstId, new ParcelUuid(srvcUuid),
                            charInstId, new ParcelUuid(charUuid),
                            new ParcelUuid(descUuid));
            }

            // Get next descriptor for the current characteristic
            gattClientGetDescriptorNative(connId, srvcType,
                                    srvcInstId, srvcUuidLsb, srvcUuidMsb,
                                    charInstId, charUuidLsb, charUuidMsb,
                                    descrUuidLsb, descrUuidMsb);
        } else {
            // Explore the next service
            continueSearch(connId, 0);
        }
    }

    void onGetIncludedService(int connId, int status, int srvcType,
            int srvcInstId, long srvcUuidLsb, long srvcUuidMsb, int inclSrvcType,
            int inclSrvcInstId, long inclSrvcUuidLsb, long inclSrvcUuidMsb)
            throws RemoteException {
        UUID srvcUuid = new UUID(srvcUuidMsb, srvcUuidLsb);
        UUID inclSrvcUuid = new UUID(inclSrvcUuidMsb, inclSrvcUuidLsb);
        String address = mClientMap.addressByConnId(connId);

        if (DBG) Log.d(TAG, "onGetIncludedService() - address=" + address
            + ", status=" + status + ", uuid=" + srvcUuid
            + ", inclUuid=" + inclSrvcUuid);

        if (status == 0) {
            ClientMap.App app = mClientMap.getByConnId(connId);
            if (app != null) {
                app.callback.onGetIncludedService(address,
                    srvcType, srvcInstId, new ParcelUuid(srvcUuid),
                    inclSrvcType, inclSrvcInstId, new ParcelUuid(inclSrvcUuid));
            }

            // Find additional included services
            gattClientGetIncludedServiceNative(connId,
                srvcType, srvcInstId, srvcUuidLsb, srvcUuidMsb,
                inclSrvcType, inclSrvcInstId, inclSrvcUuidLsb, inclSrvcUuidMsb);
        } else {
            // Discover descriptors now
            continueSearch(connId, 0);
        }
    }

    void onRegisterForNotifications(int connId, int status, int registered, int srvcType,
            int srvcInstId, long srvcUuidLsb, long srvcUuidMsb,
            int charInstId, long charUuidLsb, long charUuidMsb) {
        UUID srvcUuid = new UUID(srvcUuidMsb, srvcUuidLsb);
        UUID charUuid = new UUID(charUuidMsb, charUuidLsb);
        String address = mClientMap.addressByConnId(connId);

        if (DBG) Log.d(TAG, "onRegisterForNotifications() - address=" + address
            + ", status=" + status + ", registered=" + registered
            + ", charUuid=" + charUuid);
    }

    void onNotify(int connId, String address, int srvcType,
            int srvcInstId, long srvcUuidLsb, long srvcUuidMsb,
            int charInstId, long charUuidLsb, long charUuidMsb,
            boolean isNotify, byte[] data) throws RemoteException {
        UUID srvcUuid = new UUID(srvcUuidMsb, srvcUuidLsb);
        UUID charUuid = new UUID(charUuidMsb, charUuidLsb);

        if (DBG) Log.d(TAG, "onNotify() - address=" + address
            + ", charUuid=" + charUuid + ", length=" + data.length);

        ClientMap.App app = mClientMap.getByConnId(connId);
        if (app != null) {
            app.callback.onNotify(address, srvcType,
                        srvcInstId, new ParcelUuid(srvcUuid),
                        charInstId, new ParcelUuid(charUuid),
                        data);
        }
    }

    void onReadCharacteristic(int connId, int status, int srvcType,
            int srvcInstId, long srvcUuidLsb, long srvcUuidMsb,
            int charInstId, long charUuidLsb, long charUuidMsb,
            int charType, byte[] data) throws RemoteException {

        UUID srvcUuid = new UUID(srvcUuidMsb, srvcUuidLsb);
        UUID charUuid = new UUID(charUuidMsb, charUuidLsb);
        String address = mClientMap.addressByConnId(connId);

        if (DBG) Log.d(TAG, "onReadCharacteristic() - address=" + address
            + ", status=" + status + ", length=" + data.length);

        ClientMap.App app = mClientMap.getByConnId(connId);
        if (app != null) {
            app.callback.onCharacteristicRead(address, status, srvcType,
                        srvcInstId, new ParcelUuid(srvcUuid),
                        charInstId, new ParcelUuid(charUuid), data);
        }
    }

    void onWriteCharacteristic(int connId, int status, int srvcType,
            int srvcInstId, long srvcUuidLsb, long srvcUuidMsb,
            int charInstId, long charUuidLsb, long charUuidMsb)
            throws RemoteException {

        UUID srvcUuid = new UUID(srvcUuidMsb, srvcUuidLsb);
        UUID charUuid = new UUID(charUuidMsb, charUuidLsb);
        String address = mClientMap.addressByConnId(connId);

        if (DBG) Log.d(TAG, "onWriteCharacteristic() - address=" + address
            + ", status=" + status);

        ClientMap.App app = mClientMap.getByConnId(connId);
        if (app != null) {
            app.callback.onCharacteristicWrite(address, status, srvcType,
                        srvcInstId, new ParcelUuid(srvcUuid),
                        charInstId, new ParcelUuid(charUuid));
        }
    }

    void onExecuteCompleted(int connId, int status) throws RemoteException {
        String address = mClientMap.addressByConnId(connId);
        if (DBG) Log.d(TAG, "onExecuteCompleted() - address=" + address
            + ", status=" + status);

        ClientMap.App app = mClientMap.getByConnId(connId);
        if (app != null) {
            app.callback.onExecuteWrite(address, status);
        }
    }

    void onReadDescriptor(int connId, int status, int srvcType,
            int srvcInstId, long srvcUuidLsb, long srvcUuidMsb,
            int charInstId, long charUuidLsb, long charUuidMsb,
            long descrUuidLsb, long descrUuidMsb,
            int charType, byte[] data) throws RemoteException {

        UUID srvcUuid = new UUID(srvcUuidMsb, srvcUuidLsb);
        UUID charUuid = new UUID(charUuidMsb, charUuidLsb);
        UUID descrUuid = new UUID(descrUuidMsb, descrUuidLsb);
        String address = mClientMap.addressByConnId(connId);

        if (DBG) Log.d(TAG, "onReadDescriptor() - address=" + address
            + ", status=" + status + ", length=" + data.length);

        ClientMap.App app = mClientMap.getByConnId(connId);
        if (app != null) {
            app.callback.onDescriptorRead(address, status, srvcType,
                        srvcInstId, new ParcelUuid(srvcUuid),
                        charInstId, new ParcelUuid(charUuid),
                        new ParcelUuid(descrUuid), data);
        }
    }

    void onWriteDescriptor(int connId, int status, int srvcType,
            int srvcInstId, long srvcUuidLsb, long srvcUuidMsb,
            int charInstId, long charUuidLsb, long charUuidMsb,
            long descrUuidLsb, long descrUuidMsb) throws RemoteException {

        UUID srvcUuid = new UUID(srvcUuidMsb, srvcUuidLsb);
        UUID charUuid = new UUID(charUuidMsb, charUuidLsb);
        UUID descrUuid = new UUID(descrUuidMsb, descrUuidLsb);
        String address = mClientMap.addressByConnId(connId);

        if (DBG) Log.d(TAG, "onWriteDescriptor() - address=" + address
            + ", status=" + status);

        ClientMap.App app = mClientMap.getByConnId(connId);
        if (app != null) {
            app.callback.onDescriptorWrite(address, status, srvcType,
                        srvcInstId, new ParcelUuid(srvcUuid),
                        charInstId, new ParcelUuid(charUuid),
                        new ParcelUuid(descrUuid));
        }
    }

    void onReadRemoteRssi(int clientIf, String address,
                    int rssi, int status) throws RemoteException{
        if (DBG) Log.d(TAG, "onReadRemoteRssi() - clientIf=" + clientIf + " address=" +
                     address + ", rssi=" + rssi + ", status=" + status);

        ClientMap.App app = mClientMap.getById(clientIf);
        if (app != null) {
            app.callback.onReadRemoteRssi(address, rssi, status);
        }
    }

    /**************************************************************************
     * GATT Service functions - Shared CLIENT/SERVER
     *************************************************************************/

    List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");

        final int DEVICE_TYPE_BREDR = 0x1;

        Map<BluetoothDevice, Integer> deviceStates = new HashMap<BluetoothDevice,
                                                                 Integer>();

        // Add paired LE devices

        Set<BluetoothDevice> bondedDevices = mAdapter.getBondedDevices();
        for (BluetoothDevice device : bondedDevices) {
            if (getDeviceType(device) != DEVICE_TYPE_BREDR) {
                deviceStates.put(device, BluetoothProfile.STATE_DISCONNECTED);
            }
        }

        // Add connected deviceStates

        Set<String> connectedDevices = new HashSet<String>();
        connectedDevices.addAll(mClientMap.getConnectedDevices());
        connectedDevices.addAll(mServerMap.getConnectedDevices());

        for (String address : connectedDevices ) {
            BluetoothDevice device = mAdapter.getRemoteDevice(address);
            if (device != null) {
                deviceStates.put(device, BluetoothProfile.STATE_CONNECTED);
            }
        }

        // Create matching device sub-set

        List<BluetoothDevice> deviceList = new ArrayList<BluetoothDevice>();

        for (Map.Entry<BluetoothDevice, Integer> entry : deviceStates.entrySet()) {
            for(int state : states) {
                if (entry.getValue() == state) {
                    deviceList.add(entry.getKey());
                }
            }
        }

        return deviceList;
    }

    void startScan(int appIf, boolean isServer) {
        enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");

        if (DBG) Log.d(TAG, "startScan() - queue=" + mScanQueue.size());

        if (getScanClient(appIf, isServer) == null) {
            if (DBG) Log.d(TAG, "startScan() - adding client=" + appIf);
            mScanQueue.add(new ScanClient(appIf, isServer));
        }

        gattClientScanNative(appIf, true);
    }

    void startScanWithUuids(int appIf, boolean isServer, UUID[] uuids) {
        enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");

        if (DBG) Log.d(TAG, "startScanWithUuids() - queue=" + mScanQueue.size());

        if (getScanClient(appIf, isServer) == null) {
            if (DBG) Log.d(TAG, "startScanWithUuids() - adding client=" + appIf);
            mScanQueue.add(new ScanClient(appIf, isServer, uuids));
        }

        gattClientScanNative(appIf, true);
    }

    void stopScan(int appIf, boolean isServer) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH_ADMIN permission");

        if (DBG) Log.d(TAG, "stopScan() - queue=" + mScanQueue.size());
        removeScanClient(appIf, isServer);

        if (mScanQueue.isEmpty()) {
            if (DBG) Log.d(TAG, "stopScan() - queue empty; stopping scan");
            gattClientScanNative(appIf, false);
        }
    }

    /**************************************************************************
     * GATT Service functions - CLIENT
     *************************************************************************/

    void registerClient(UUID uuid, IBluetoothGattCallback callback) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");

        if (DBG) Log.d(TAG, "registerClient() - UUID=" + uuid);
        mClientMap.add(uuid, callback);
        gattClientRegisterAppNative(uuid.getLeastSignificantBits(),
                                    uuid.getMostSignificantBits());
    }

    void unregisterClient(int clientIf) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");

        if (DBG) Log.d(TAG, "unregisterClient() - clientIf=" + clientIf);
        removeScanClient(clientIf, false);
        mClientMap.remove(clientIf);
        gattClientUnregisterAppNative(clientIf);
    }

    void clientConnect(int clientIf, String address, boolean isDirect) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");

        if (DBG) Log.d(TAG, "clientConnect() - address=" + address + ", isDirect=" + isDirect);
        gattClientConnectNative(clientIf, address, isDirect);
    }

    void clientDisconnect(int clientIf, String address) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");

        Integer connId = mClientMap.connIdByAddress(clientIf, address);
        if (DBG) Log.d(TAG, "clientDisconnect() - address=" + address + ", connId=" + connId);

        gattClientDisconnectNative(clientIf, address, connId != null ? connId : 0);
    }

    List<String> getConnectedDevices() {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");

        Set<String> connectedDevAddress = new HashSet<String>();
        connectedDevAddress.addAll(mClientMap.getConnectedDevices());
        connectedDevAddress.addAll(mServerMap.getConnectedDevices());
        List<String> connectedDeviceList = new ArrayList<String>(connectedDevAddress);
        return connectedDeviceList;
    }

    void refreshDevice(int clientIf, String address) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");

        if (DBG) Log.d(TAG, "refreshDevice() - address=" + address);
        gattClientRefreshNative(clientIf, address);
    }

    void discoverServices(int clientIf, String address) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");

        Integer connId = mClientMap.connIdByAddress(clientIf, address);
        if (DBG) Log.d(TAG, "discoverServices() - address=" + address + ", connId=" + connId);

        if (connId != null)
            gattClientSearchServiceNative(connId, true, 0, 0);
        else
            Log.e(TAG, "discoverServices() - No connection for " + address + "...");
    }

    void readCharacteristic(int clientIf, String address, int srvcType,
                            int srvcInstanceId, UUID srvcUuid,
                            int charInstanceId, UUID charUuid, int authReq) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");

        if (DBG) Log.d(TAG, "readCharacteristic() - address=" + address);

        Integer connId = mClientMap.connIdByAddress(clientIf, address);
        if (connId != null)
            gattClientReadCharacteristicNative(connId, srvcType,
                srvcInstanceId, srvcUuid.getLeastSignificantBits(),
                srvcUuid.getMostSignificantBits(), charInstanceId,
                charUuid.getLeastSignificantBits(), charUuid.getMostSignificantBits(),
                authReq);
        else
            Log.e(TAG, "readCharacteristic() - No connection for " + address + "...");
    }

    void writeCharacteristic(int clientIf, String address, int srvcType,
                             int srvcInstanceId, UUID srvcUuid,
                             int charInstanceId, UUID charUuid, int writeType,
                             int authReq, byte[] value) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");

        if (DBG) Log.d(TAG, "writeCharacteristic() - address=" + address);

        if (mReliableQueue.contains(address)) writeType = 3; // Prepared write

        Integer connId = mClientMap.connIdByAddress(clientIf, address);
        if (connId != null)
            gattClientWriteCharacteristicNative(connId, srvcType,
                srvcInstanceId, srvcUuid.getLeastSignificantBits(),
                srvcUuid.getMostSignificantBits(), charInstanceId,
                charUuid.getLeastSignificantBits(), charUuid.getMostSignificantBits(),
                writeType, authReq, value);
        else
            Log.e(TAG, "writeCharacteristic() - No connection for " + address + "...");
    }

    void readDescriptor(int clientIf, String address, int srvcType,
                            int srvcInstanceId, UUID srvcUuid,
                            int charInstanceId, UUID charUuid,
                            UUID descrUuid, int authReq) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");

        if (DBG) Log.d(TAG, "readDescriptor() - address=" + address);

        Integer connId = mClientMap.connIdByAddress(clientIf, address);
        if (connId != null)
            gattClientReadDescriptorNative(connId, srvcType,
                srvcInstanceId, srvcUuid.getLeastSignificantBits(),
                srvcUuid.getMostSignificantBits(), charInstanceId,
                charUuid.getLeastSignificantBits(), charUuid.getMostSignificantBits(),
                descrUuid.getLeastSignificantBits(), descrUuid.getMostSignificantBits(),
                authReq);
        else
            Log.e(TAG, "readDescriptor() - No connection for " + address + "...");
    };

    void writeDescriptor(int clientIf, String address, int srvcType,
                            int srvcInstanceId, UUID srvcUuid,
                            int charInstanceId, UUID charUuid,
                            UUID descrUuid, int writeType,
                            int authReq, byte[] value) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");

        if (DBG) Log.d(TAG, "writeDescriptor() - address=" + address);

        Integer connId = mClientMap.connIdByAddress(clientIf, address);
        if (connId != null)
            gattClientWriteDescriptorNative(connId, srvcType,
                srvcInstanceId, srvcUuid.getLeastSignificantBits(),
                srvcUuid.getMostSignificantBits(), charInstanceId,
                charUuid.getLeastSignificantBits(), charUuid.getMostSignificantBits(),
                descrUuid.getLeastSignificantBits(), descrUuid.getMostSignificantBits(),
                writeType, authReq, value);
        else
            Log.e(TAG, "writeDescriptor() - No connection for " + address + "...");
    }

    void beginReliableWrite(int clientIf, String address) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");

        if (DBG) Log.d(TAG, "beginReliableWrite() - address=" + address);
        mReliableQueue.add(address);
    }

    void endReliableWrite(int clientIf, String address, boolean execute) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");

        if (DBG) Log.d(TAG, "endReliableWrite() - address=" + address
                                + " execute: " + execute);
        mReliableQueue.remove(address);

        Integer connId = mClientMap.connIdByAddress(clientIf, address);
        if (connId != null) gattClientExecuteWriteNative(connId, execute);
    }

    void registerForNotification(int clientIf, String address, int srvcType,
                int srvcInstanceId, UUID srvcUuid,
                int charInstanceId, UUID charUuid,
                boolean enable) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");

        if (DBG) Log.d(TAG, "registerForNotification() - address=" + address + " enable: " + enable);

        Integer connId = mClientMap.connIdByAddress(clientIf, address);
        if (connId != null) {
            gattClientRegisterForNotificationsNative(clientIf, address,
                srvcType, srvcInstanceId, srvcUuid.getLeastSignificantBits(),
                srvcUuid.getMostSignificantBits(), charInstanceId,
                charUuid.getLeastSignificantBits(), charUuid.getMostSignificantBits(),
                enable);
        } else {
            Log.e(TAG, "registerForNotification() - No connection for " + address + "...");
        }
    }

    void readRemoteRssi(int clientIf, String address) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");

        if (DBG) Log.d(TAG, "readRemoteRssi() - address=" + address);
        gattClientReadRemoteRssiNative(clientIf, address);
    }

    /**************************************************************************
     * Callback functions - SERVER
     *************************************************************************/

    void onServerRegistered(int status, int serverIf, long uuidLsb, long uuidMsb)
            throws RemoteException {

        UUID uuid = new UUID(uuidMsb, uuidLsb);
        if (DBG) Log.d(TAG, "onServerRegistered() - UUID=" + uuid + ", serverIf=" + serverIf);
        ServerMap.App app = mServerMap.getByUuid(uuid);
        if (app != null) {
            app.id = serverIf;
            app.linkToDeath(new ServerDeathRecipient(serverIf));
            app.callback.onServerRegistered(status, serverIf);
        }
    }

    void onServiceAdded(int status, int serverIf, int srvcType, int srvcInstId,
                        long srvcUuidLsb, long srvcUuidMsb, int srvcHandle)
                        throws RemoteException {
        UUID uuid = new UUID(srvcUuidMsb, srvcUuidLsb);
        if (DBG) Log.d(TAG, "onServiceAdded() UUID=" + uuid + ", status=" + status
            + ", handle=" + srvcHandle);
        if (status == 0)
            mHandleMap.addService(serverIf, srvcHandle, uuid, srvcType, srvcInstId);
        continueServiceDeclaration(serverIf, status, srvcHandle);
    }

    void onIncludedServiceAdded(int status, int serverIf, int srvcHandle,
                                int includedSrvcHandle) throws RemoteException {
        if (DBG) Log.d(TAG, "onIncludedServiceAdded() status=" + status
            + ", service=" + srvcHandle + ", included=" + includedSrvcHandle);
        continueServiceDeclaration(serverIf, status, srvcHandle);
    }

    void onCharacteristicAdded(int status, int serverIf,
                               long charUuidLsb, long charUuidMsb,
                               int srvcHandle, int charHandle)
                               throws RemoteException {
            UUID uuid = new UUID(charUuidMsb, charUuidLsb);
        if (DBG) Log.d(TAG, "onCharacteristicAdded() UUID=" + uuid + ", status=" + status
            + ", srvcHandle=" + srvcHandle + ", charHandle=" + charHandle);
        if (status == 0)
            mHandleMap.addCharacteristic(serverIf, charHandle, uuid, srvcHandle);
        continueServiceDeclaration(serverIf, status, srvcHandle);
    }

    void onDescriptorAdded(int status, int serverIf,
                           long descrUuidLsb, long descrUuidMsb,
                           int srvcHandle, int descrHandle)
                           throws RemoteException {
            UUID uuid = new UUID(descrUuidMsb, descrUuidLsb);
        if (DBG) Log.d(TAG, "onDescriptorAdded() UUID=" + uuid + ", status=" + status
            + ", srvcHandle=" + srvcHandle + ", descrHandle=" + descrHandle);
        if (status == 0)
            mHandleMap.addDescriptor(serverIf, descrHandle, uuid, srvcHandle);
        continueServiceDeclaration(serverIf, status, srvcHandle);
    }

    void onServiceStarted(int status, int serverIf, int srvcHandle)
            throws RemoteException {
        if (DBG) Log.d(TAG, "onServiceStarted() srvcHandle=" + srvcHandle
            + ", status=" + status);
        if (status == 0)
            mHandleMap.setStarted(serverIf, srvcHandle, true);
    }

    void onServiceStopped(int status, int serverIf, int srvcHandle)
            throws RemoteException {
        if (DBG) Log.d(TAG, "onServiceStopped() srvcHandle=" + srvcHandle
            + ", status=" + status);
        if (status == 0)
            mHandleMap.setStarted(serverIf, srvcHandle, false);
        stopNextService(serverIf, status);
    }

    void onServiceDeleted(int status, int serverIf, int srvcHandle) {
        if (DBG) Log.d(TAG, "onServiceDeleted() srvcHandle=" + srvcHandle
            + ", status=" + status);
        mHandleMap.deleteService(serverIf, srvcHandle);
    }

    void onClientConnected(String address, boolean connected, int connId, int serverIf)
            throws RemoteException {

        if (DBG) Log.d(TAG, "onConnected() connId=" + connId
            + ", address=" + address + ", connected=" + connected);

        ServerMap.App app = mServerMap.getById(serverIf);
        if (app == null) return;

        if (connected) {
            mServerMap.addConnection(serverIf, connId, address);
        } else {
            mServerMap.removeConnection(serverIf, connId);
        }

        app.callback.onServerConnectionState((byte)0, serverIf, connected, address);
    }

    void onAttributeRead(String address, int connId, int transId,
                            int attrHandle, int offset, boolean isLong)
                            throws RemoteException {
        if (DBG) Log.d(TAG, "onAttributeRead() connId=" + connId
            + ", address=" + address + ", handle=" + attrHandle
            + ", requestId=" + transId + ", offset=" + offset);

        HandleMap.Entry entry = mHandleMap.getByHandle(attrHandle);
        if (entry == null) return;

        if (DBG) Log.d(TAG, "onAttributeRead() UUID=" + entry.uuid
            + ", serverIf=" + entry.serverIf + ", type=" + entry.type);

        mHandleMap.addRequest(transId, attrHandle);

        ServerMap.App app = mServerMap.getById(entry.serverIf);
        if (app == null) return;

        switch(entry.type) {
            case HandleMap.TYPE_CHARACTERISTIC:
            {
                HandleMap.Entry serviceEntry = mHandleMap.getByHandle(entry.serviceHandle);
                app.callback.onCharacteristicReadRequest(address, transId, offset, isLong,
                    serviceEntry.serviceType, serviceEntry.instance,
                    new ParcelUuid(serviceEntry.uuid), entry.instance,
                    new ParcelUuid(entry.uuid));
                break;
            }

            case HandleMap.TYPE_DESCRIPTOR:
            {
                HandleMap.Entry serviceEntry = mHandleMap.getByHandle(entry.serviceHandle);
                HandleMap.Entry charEntry = mHandleMap.getByHandle(entry.charHandle);
                app.callback.onDescriptorReadRequest(address, transId, offset, isLong,
                    serviceEntry.serviceType, serviceEntry.instance,
                    new ParcelUuid(serviceEntry.uuid), charEntry.instance,
                    new ParcelUuid(charEntry.uuid),
                    new ParcelUuid(entry.uuid));
                break;
            }

            default:
                Log.e(TAG, "onAttributeRead() - Requested unknown attribute type.");
                break;
        }
    }

    void onAttributeWrite(String address, int connId, int transId,
                            int attrHandle, int offset, int length,
                            boolean needRsp, boolean isPrep,
                            byte[] data)
                            throws RemoteException {
        if (DBG) Log.d(TAG, "onAttributeWrite() connId=" + connId
            + ", address=" + address + ", handle=" + attrHandle
            + ", requestId=" + transId + ", isPrep=" + isPrep
            + ", offset=" + offset);

        HandleMap.Entry entry = mHandleMap.getByHandle(attrHandle);
        if (entry == null) return;

        if (DBG) Log.d(TAG, "onAttributeWrite() UUID=" + entry.uuid
            + ", serverIf=" + entry.serverIf + ", type=" + entry.type);

        mHandleMap.addRequest(transId, attrHandle);

        ServerMap.App app = mServerMap.getById(entry.serverIf);
        if (app == null) return;

        switch(entry.type) {
            case HandleMap.TYPE_CHARACTERISTIC:
            {
                HandleMap.Entry serviceEntry = mHandleMap.getByHandle(entry.serviceHandle);
                app.callback.onCharacteristicWriteRequest(address, transId,
                            offset, length, isPrep, needRsp,
                            serviceEntry.serviceType, serviceEntry.instance,
                            new ParcelUuid(serviceEntry.uuid), entry.instance,
                            new ParcelUuid(entry.uuid), data);
                break;
            }

            case HandleMap.TYPE_DESCRIPTOR:
            {
                HandleMap.Entry serviceEntry = mHandleMap.getByHandle(entry.serviceHandle);
                HandleMap.Entry charEntry = mHandleMap.getByHandle(entry.charHandle);
                app.callback.onDescriptorWriteRequest(address, transId,
                            offset, length, isPrep, needRsp,
                            serviceEntry.serviceType, serviceEntry.instance,
                            new ParcelUuid(serviceEntry.uuid), charEntry.instance,
                            new ParcelUuid(charEntry.uuid),
                            new ParcelUuid(entry.uuid), data);
                break;
            }

            default:
                Log.e(TAG, "onAttributeWrite() - Requested unknown attribute type.");
                break;
        }
    }

    void onExecuteWrite(String address, int connId, int transId, int execWrite)
            throws RemoteException {
        if (DBG) Log.d(TAG, "onExecuteWrite() connId=" + connId
            + ", address=" + address + ", transId=" + transId);

        ServerMap.App app = mServerMap.getByConnId(connId);
        if (app == null) return;

        app.callback.onExecuteWrite(address, transId, execWrite == 1);
    }

    void onResponseSendCompleted(int status, int attrHandle) {
        if (DBG) Log.d(TAG, "onResponseSendCompleted() handle=" + attrHandle);
    }

    /**************************************************************************
     * GATT Service functions - SERVER
     *************************************************************************/

    void registerServer(UUID uuid, IBluetoothGattServerCallback callback) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");

        if (DBG) Log.d(TAG, "registerServer() - UUID=" + uuid);
        mServerMap.add(uuid, callback);
        gattServerRegisterAppNative(uuid.getLeastSignificantBits(),
                                    uuid.getMostSignificantBits());
    }

    void unregisterServer(int serverIf) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");

        if (DBG) Log.d(TAG, "unregisterServer() - serverIf=" + serverIf);

        deleteServices(serverIf);

        mServerMap.remove(serverIf);
        gattServerUnregisterAppNative(serverIf);
    }

    void serverConnect(int serverIf, String address, boolean isDirect) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");

        if (DBG) Log.d(TAG, "serverConnect() - address=" + address);
        gattServerConnectNative(serverIf, address, isDirect);
    }

    void serverDisconnect(int serverIf, String address) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");

        Integer connId = mServerMap.connIdByAddress(serverIf, address);
        if (DBG) Log.d(TAG, "serverDisconnect() - address=" + address + ", connId=" + connId);

        gattServerDisconnectNative(serverIf, address, connId != null ? connId : 0);
    }

    void beginServiceDeclaration(int serverIf, int srvcType, int srvcInstanceId,
                                 int minHandles, UUID srvcUuid) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");

        if (DBG) Log.d(TAG, "beginServiceDeclaration() - uuid=" + srvcUuid);
        ServiceDeclaration serviceDeclaration = addDeclaration();
        serviceDeclaration.addService(srvcUuid, srvcType, srvcInstanceId, minHandles);
    }

    void addIncludedService(int serverIf, int srvcType, int srvcInstanceId,
                            UUID srvcUuid) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");

        if (DBG) Log.d(TAG, "addIncludedService() - uuid=" + srvcUuid);
        getActiveDeclaration().addIncludedService(srvcUuid, srvcType, srvcInstanceId);
    }

    void addCharacteristic(int serverIf, UUID charUuid, int properties,
                           int permissions) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");

        if (DBG) Log.d(TAG, "addCharacteristic() - uuid=" + charUuid);
        getActiveDeclaration().addCharacteristic(charUuid, properties, permissions);
    }

    void addDescriptor(int serverIf, UUID descUuid, int permissions) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");

        if (DBG) Log.d(TAG, "addDescriptor() - uuid=" + descUuid);
        getActiveDeclaration().addDescriptor(descUuid, permissions);
    }

    void endServiceDeclaration(int serverIf) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");

        if (DBG) Log.d(TAG, "endServiceDeclaration()");

        if (getActiveDeclaration() == getPendingDeclaration()) {
            try {
                continueServiceDeclaration(serverIf, (byte)0, 0);
            } catch (RemoteException e) {
                Log.e(TAG,""+e);
            }
        }
    }

    void removeService(int serverIf, int srvcType,
                  int srvcInstanceId, UUID srvcUuid) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");

        if (DBG) Log.d(TAG, "removeService() - uuid=" + srvcUuid);

        int srvcHandle = mHandleMap.getServiceHandle(srvcUuid, srvcType, srvcInstanceId);
        if (srvcHandle == 0) return;
        gattServerDeleteServiceNative(serverIf, srvcHandle);
    }

    void clearServices(int serverIf) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");

        if (DBG) Log.d(TAG, "clearServices()");
        deleteServices(serverIf);
    }

    void sendResponse(int serverIf, String address, int requestId,
                      int status, int offset, byte[] value) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");

        if (DBG) Log.d(TAG, "sendResponse() - address=" + address);

        int handle = 0;
        HandleMap.Entry entry = mHandleMap.getByRequestId(requestId);
        if (entry != null) handle = entry.handle;

        int connId = mServerMap.connIdByAddress(serverIf, address);
        gattServerSendResponseNative(serverIf, connId, requestId, (byte)status,
                                     handle, offset, value, (byte)0);
        mHandleMap.deleteRequest(requestId);
    }

    void sendNotification(int serverIf, String address, int srvcType,
                                 int srvcInstanceId, UUID srvcUuid,
                                 int charInstanceId, UUID charUuid,
                                 boolean confirm, byte[] value) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");

        if (DBG) Log.d(TAG, "sendNotification() - address=" + address);

        int srvcHandle = mHandleMap.getServiceHandle(srvcUuid, srvcType, srvcInstanceId);
        if (srvcHandle == 0) return;

        int charHandle = mHandleMap.getCharacteristicHandle(srvcHandle, charUuid, charInstanceId);
        if (charHandle == 0) return;

        int connId = mServerMap.connIdByAddress(serverIf, address);
        if (connId == 0) return;

        if (confirm) {
            gattServerSendIndicationNative(serverIf, charHandle, connId, value);
        } else {
            gattServerSendNotificationNative(serverIf, charHandle, connId, value);
        }
    }

    /**************************************************************************
     * Private functions
     *************************************************************************/

    private int getDeviceType(BluetoothDevice device) {
        int type = gattClientGetDeviceTypeNative(device.getAddress());
        if (DBG) Log.d(TAG, "getDeviceType() - device=" + device
            + ", type=" + type);
        return type;
    }

    private void continueSearch(int connId, int status) throws RemoteException {
        if (status == 0 && !mSearchQueue.isEmpty()) {
            SearchQueue.Entry svc = mSearchQueue.pop();

            if (svc.charUuidLsb == 0) {
                // Characteristic is up next
                gattClientGetCharacteristicNative(svc.connId, svc.srvcType,
                    svc.srvcInstId, svc.srvcUuidLsb, svc.srvcUuidMsb, 0, 0, 0);
            } else {
                // Descriptor is up next
                gattClientGetDescriptorNative(svc.connId, svc.srvcType,
                    svc.srvcInstId, svc.srvcUuidLsb, svc.srvcUuidMsb,
                    svc.charInstId, svc.charUuidLsb, svc.charUuidMsb, 0,0);
            }
        } else {
            ClientMap.App app = mClientMap.getByConnId(connId);
            if (app != null) {
                app.callback.onSearchComplete(mClientMap.addressByConnId(connId), status);
            }
        }
    }

    private void continueServiceDeclaration(int serverIf, int status, int srvcHandle) throws RemoteException {
        if (mServiceDeclarations.size() == 0) return;
        if (DBG) Log.d(TAG, "continueServiceDeclaration() - srvcHandle=" + srvcHandle);

        boolean finished = false;

        ServiceDeclaration.Entry entry = null;
        if (status == 0)
            entry = getPendingDeclaration().getNext();

        if (entry != null) {
            if (DBG) Log.d(TAG, "continueServiceDeclaration() - next entry type="
                + entry.type);
            switch(entry.type) {
                case ServiceDeclaration.TYPE_SERVICE:
                    gattServerAddServiceNative(serverIf, entry.serviceType,
                        entry.instance,
                        entry.uuid.getLeastSignificantBits(),
                        entry.uuid.getMostSignificantBits(),
                        getPendingDeclaration().getNumHandles());
                    break;

                case ServiceDeclaration.TYPE_CHARACTERISTIC:
                    gattServerAddCharacteristicNative(serverIf, srvcHandle,
                        entry.uuid.getLeastSignificantBits(),
                        entry.uuid.getMostSignificantBits(),
                        entry.properties, entry.permissions);
                    break;

                case ServiceDeclaration.TYPE_DESCRIPTOR:
                    gattServerAddDescriptorNative(serverIf, srvcHandle,
                        entry.uuid.getLeastSignificantBits(),
                        entry.uuid.getMostSignificantBits(),
                        entry.permissions);
                    break;

                case ServiceDeclaration.TYPE_INCLUDED_SERVICE:
                {
                    int inclSrvc = mHandleMap.getServiceHandle(entry.uuid,
                                            entry.serviceType, entry.instance);
                    if (inclSrvc != 0) {
                        gattServerAddIncludedServiceNative(serverIf, srvcHandle,
                                                           inclSrvc);
                    } else {
                        finished = true;
                    }
                    break;
                }
            }
        } else {
            gattServerStartServiceNative(serverIf, srvcHandle, (byte)2 /*BREDR/LE*/);
            finished = true;
        }

        if (finished) {
            if (DBG) Log.d(TAG, "continueServiceDeclaration() - completed.");
            ServerMap.App app = mServerMap.getById(serverIf);
            if (app != null) {
                HandleMap.Entry serviceEntry = mHandleMap.getByHandle(srvcHandle);
                if (serviceEntry != null) {
                    app.callback.onServiceAdded(status, serviceEntry.serviceType,
                        serviceEntry.instance, new ParcelUuid(serviceEntry.uuid));
                } else {
                    app.callback.onServiceAdded(status, 0, 0, null);
                }
            }
            removePendingDeclaration();

            if (getPendingDeclaration() != null) {
                continueServiceDeclaration(serverIf, (byte)0, 0);
            }
        }
    }

    private void stopNextService(int serverIf, int status) throws RemoteException {
        if (DBG) Log.d(TAG, "stopNextService() - serverIf=" + serverIf
            + ", status=" + status);

        if (status == 0) {
            List<HandleMap.Entry> entries = mHandleMap.getEntries();
            for(HandleMap.Entry entry : entries) {
                if (entry.type != HandleMap.TYPE_SERVICE ||
                    entry.serverIf != serverIf ||
                    entry.started == false)
                        continue;

                gattServerStopServiceNative(serverIf, entry.handle);
                return;
            }
        }
    }

    private void deleteServices(int serverIf) {
        if (DBG) Log.d(TAG, "deleteServices() - serverIf=" + serverIf);

        /*
         * Figure out which handles to delete.
         * The handles are copied into a new list to avoid race conditions.
         */
        List<Integer> handleList = new ArrayList<Integer>();
        List<HandleMap.Entry> entries = mHandleMap.getEntries();
        for(HandleMap.Entry entry : entries) {
            if (entry.type != HandleMap.TYPE_SERVICE ||
                entry.serverIf != serverIf)
                    continue;
            handleList.add(entry.handle);
        }

        /* Now actually delete the services.... */
        for(Integer handle : handleList) {
            gattServerDeleteServiceNative(serverIf, handle);
        }
    }

    private List<UUID> parseUuids(byte[] adv_data) {
        List<UUID> uuids = new ArrayList<UUID>();

        int offset = 0;
        while(offset < (adv_data.length-2)) {
            int len = adv_data[offset++];
            if (len == 0) break;

            int type = adv_data[offset++];
            switch (type) {
                case 0x02: // Partial list of 16-bit UUIDs
                case 0x03: // Complete list of 16-bit UUIDs
                    while (len > 1) {
                        int uuid16 = adv_data[offset++];
                        uuid16 += (adv_data[offset++] << 8);
                        len -= 2;
                        uuids.add(UUID.fromString(String.format(
                            "%08x-0000-1000-8000-00805f9b34fb", uuid16)));
                    }
                    break;

                default:
                    offset += (len - 1);
                    break;
            }
        }

        return uuids;
    }

    /**************************************************************************
     * GATT Test functions
     *************************************************************************/

    void gattTestCommand(int command, UUID uuid1, String bda1,
                         int p1, int p2, int p3, int p4, int p5) {
        if (bda1 == null) bda1 = "00:00:00:00:00:00";
        if (uuid1 != null)
            gattTestNative(command, uuid1.getLeastSignificantBits(),
                       uuid1.getMostSignificantBits(), bda1, p1, p2, p3, p4, p5);
        else
            gattTestNative(command, 0,0, bda1, p1, p2, p3, p4, p5);
    }

    private native void gattTestNative(int command,
                                    long uuid1_lsb, long uuid1_msb, String bda1,
                                    int p1, int p2, int p3, int p4, int p5);

    /**************************************************************************
     * Native functions prototypes
     *************************************************************************/

    private native static void classInitNative();
    private native void initializeNative();
    private native void cleanupNative();

    private native int gattClientGetDeviceTypeNative(String address);

    private native void gattClientRegisterAppNative(long app_uuid_lsb,
                                                    long app_uuid_msb);

    private native void gattClientUnregisterAppNative(int clientIf);

    private native void gattClientScanNative(int clientIf, boolean start);

    private native void gattClientConnectNative(int clientIf, String address,
            boolean isDirect);

    private native void gattClientDisconnectNative(int clientIf, String address,
            int conn_id);

    private native void gattClientRefreshNative(int clientIf, String address);

    private native void gattClientSearchServiceNative(int conn_id,
            boolean search_all, long service_uuid_lsb, long service_uuid_msb);

    private native void gattClientGetCharacteristicNative(int conn_id,
            int service_type, int service_id_inst_id, long service_id_uuid_lsb,
            long service_id_uuid_msb, int char_id_inst_id, long char_id_uuid_lsb,
            long char_id_uuid_msb);

    private native void gattClientGetDescriptorNative(int conn_id,
            int service_type, int service_id_inst_id, long service_id_uuid_lsb,
            long service_id_uuid_msb, int char_id_inst_id, long char_id_uuid_lsb,
            long char_id_uuid_msb, long descr_id_uuid_lsb, long descr_id_uuid_msb);

    private native void gattClientGetIncludedServiceNative(int conn_id,
            int service_type, int service_id_inst_id,
            long service_id_uuid_lsb, long service_id_uuid_msb,
            int incl_service_id_inst_id, int incl_service_type,
            long incl_service_id_uuid_lsb, long incl_service_id_uuid_msb);

    private native void gattClientReadCharacteristicNative(int conn_id,
            int service_type, int service_id_inst_id, long service_id_uuid_lsb,
            long service_id_uuid_msb, int char_id_inst_id, long char_id_uuid_lsb,
            long char_id_uuid_msb, int authReq);

    private native void gattClientReadDescriptorNative(int conn_id,
            int service_type, int service_id_inst_id, long service_id_uuid_lsb,
            long service_id_uuid_msb, int char_id_inst_id, long char_id_uuid_lsb,
            long char_id_uuid_msb, long descr_id_uuid_lsb, long descr_id_uuid_msb,
            int authReq);

    private native void gattClientWriteCharacteristicNative(int conn_id,
            int service_type, int service_id_inst_id, long service_id_uuid_lsb,
            long service_id_uuid_msb, int char_id_inst_id, long char_id_uuid_lsb,
            long char_id_uuid_msb, int write_type, int auth_req, byte[] value);

    private native void gattClientWriteDescriptorNative(int conn_id,
            int service_type, int service_id_inst_id, long service_id_uuid_lsb,
            long service_id_uuid_msb, int char_id_inst_id, long char_id_uuid_lsb,
            long char_id_uuid_msb, long descr_id_uuid_lsb, long descr_id_uuid_msb,
            int write_type, int auth_req, byte[] value);

    private native void gattClientExecuteWriteNative(int conn_id, boolean execute);

    private native void gattClientRegisterForNotificationsNative(int clientIf,
            String address, int service_type, int service_id_inst_id,
            long service_id_uuid_lsb, long service_id_uuid_msb,
            int char_id_inst_id, long char_id_uuid_lsb, long char_id_uuid_msb,
            boolean enable);

    private native void gattClientReadRemoteRssiNative(int clientIf,
            String address);

    private native void gattServerRegisterAppNative(long app_uuid_lsb,
                                                    long app_uuid_msb);

    private native void gattServerUnregisterAppNative(int serverIf);

    private native void gattServerConnectNative(int server_if, String address,
                                             boolean is_direct);

    private native void gattServerDisconnectNative(int serverIf, String address,
                                              int conn_id);

    private native void gattServerAddServiceNative (int server_if,
            int service_type, int service_id_inst_id,
            long service_id_uuid_lsb, long service_id_uuid_msb,
            int num_handles);

    private native void gattServerAddIncludedServiceNative (int server_if,
            int svc_handle, int included_svc_handle);

    private native void gattServerAddCharacteristicNative (int server_if,
            int svc_handle, long char_uuid_lsb, long char_uuid_msb,
            int properties, int permissions);

    private native void gattServerAddDescriptorNative (int server_if,
            int svc_handle, long desc_uuid_lsb, long desc_uuid_msb,
            int permissions);

    private native void gattServerStartServiceNative (int server_if,
            int svc_handle, int transport );

    private native void gattServerStopServiceNative (int server_if,
                                                     int svc_handle);

    private native void gattServerDeleteServiceNative (int server_if,
                                                       int svc_handle);

    private native void gattServerSendIndicationNative (int server_if,
            int attr_handle, int conn_id, byte[] val);

    private native void gattServerSendNotificationNative (int server_if,
            int attr_handle, int conn_id, byte[] val);

    private native void gattServerSendResponseNative (int server_if,
            int conn_id, int trans_id, int status, int handle, int offset,
            byte[] val, int auth_req);
}
