All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.gluonhq.charm.down.android.ble.BleServiceImpl Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2015, Gluon
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution.
 *     * Neither the name of Gluon, any associated website, nor the
 * names of its contributors may be used to endorse or promote products
 * derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL GLUON BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.gluonhq.charm.down.android.ble;

import static android.app.Activity.RESULT_OK;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanRecord;
import android.bluetooth.le.ScanResult;
import android.content.Intent;
import com.gluonhq.charm.down.common.BleService;
import com.gluonhq.charm.down.common.ble.Configuration;
import com.gluonhq.charm.down.common.ble.ScanDetection;
import com.gluonhq.charm.down.common.ble.ScanDetection.PROXIMITY;
import java.util.Arrays;
import java.util.Formatter;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Consumer;
import javafxports.android.FXActivity;

/**
 * Android implementation of BleService
 *
 * Android apps using BleService require the permissions BLUETOOTH and BLUETOOTH_ADMIN in the AndroidManifest file
 */
public class BleServiceImpl implements BleService {
    private BluetoothLeScanner scanner;
    private ScanCallback scanCallback;

    private List uuids = new LinkedList<>();
    private final static int REQUEST_ENABLE_BT = 1001;

    /**
     * Check if Bluetooth is enabled and triggers a notification in case it is not, returning
     * to the application after the user enables it
     */
    public BleServiceImpl() {
        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();

        if (adapter == null) {
            System.out.println("Device doesn't support Bluetooth");
        } else if (!adapter.isEnabled()) {
            Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
            FXActivity activity=FXActivity.getInstance();

            activity.setOnActivityResultHandler((requestCode, resultCode, data) -> {
                if (requestCode == REQUEST_ENABLE_BT && resultCode == RESULT_OK) {
                    scanner = adapter.getBluetoothLeScanner();
                }
            });

            // A dialog will appear requesting user permission to enable Bluetooth
            activity.startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);

        } else {
            scanner = adapter.getBluetoothLeScanner();
        }
    }

    /**
     * startScanning is called with a given uuid and a callback for the beacon found.
     * Android allows scanning for any beacon without knowing the uuid, so an empty
     * uuid can be set (this is not allowed in iOS implementation)
     *
     * @param region Containing the beacon uuid
     * @param callback Callback added to the beacon
     */
    @Override
    public void startScanning(Configuration region, Consumer callback) {
        List requested =  region.getUuids();
        for (String candidate: requested) {
            uuids.add(candidate.toLowerCase());
        }
        if (scanner != null) {
            this.scanCallback = createCallback(callback);
            scanner.startScan(scanCallback);
        }
    }

    /**
     * stopScanning, if the scanner is initialized
     */
    @Override
    public void stopScanning() {
        if (scanner != null && scanCallback != null) {
            scanner.stopScan(scanCallback);
        }
    }

    private ScanCallback createCallback(Consumer callback) {
        ScanCallback answer = new ScanCallback() {
            @Override
            public void onScanResult(int callbackType, ScanResult result) {
                ScanRecord mScanRecord = result.getScanRecord();
                byte[] scanRecord = mScanRecord.getBytes();

                // https://www.pubnub.com/blog/2015-04-14-building-android-beacon-android-ibeacon-tutorial-overview/
                int index = 0;
                while (index < scanRecord.length) {
                    int length = scanRecord[index++];
                    if (length == 0) {
                        break;
                    }

                    int type = (scanRecord[index] & 0xff);

                    // process data
                    if (type == 0xff) {
                        ScanDetection detection = processAD(scanRecord, index + 1, result.getRssi());
                        if (detection != null) {
                            callback.accept(detection);
                        }
                    }

                    index += length;
                }
            }
        };
        return answer;
    }

    private ScanDetection processAD(byte[] scanRecord, int init, int mRssi) {
        // AD: (Length FF) mID0 mID1 bID1 bID0 uuid15 ... uuid0 M1 M0 m1 m0 tx
        int startByte = init;
        // Manufacturer ID (little endian)
        // https://www.bluetooth.org/en-us/specification/assigned-numbers/company-identifiers
        int mID = ((scanRecord[startByte+1] & 0xff) << 8) | (scanRecord[startByte] & 0xff);

        startByte += 2;
        // Beacon ID (big endian)
        int beaconID = ((scanRecord[startByte] & 0xff) << 8) | (scanRecord[startByte+1] & 0xff);
        startByte += 2;
        // UUID (big endian)
        byte[] uuidBytes = Arrays.copyOfRange(scanRecord, startByte, startByte+16);
        String scannedUuid = ByteArrayToUUIDString(uuidBytes);

        if (uuids.isEmpty() || uuids.contains(scannedUuid.toLowerCase())) {
            startByte += 16;
            // major (big endian)
            int major = ((scanRecord[startByte] & 0xff) << 8) | (scanRecord[startByte+1] & 0xff);
            startByte += 2;
            // minor (big endian)
            int minor = ((scanRecord[startByte] & 0xff) << 8) | (scanRecord[startByte+1] & 0xff);
            startByte += 2;
            // power in dB
            int power = (scanRecord[startByte] & 0xff);
            power -= 256;
            PROXIMITY distance = calculateProximity(power, mRssi);

//            System.out.println("Scan: mID: "+mID+", beaconID: "+beaconID+", uuid: "+scannedUuid+
//                    ", major: "+major+", minor: "+minor+", power: "+power+", distance: "+distance.name());

            ScanDetection detection = new ScanDetection();
            detection.setUuid(scannedUuid);
            detection.setMajor(major);
            detection.setMinor(minor);
            detection.setRssi(mRssi);
            detection.setProximity(distance.getProximity());
            return detection;
        }
        return null;
    }

    private static String ByteArrayToUUIDString(byte[] ba) {
        StringBuilder hex = new StringBuilder();
        for (byte b : ba) {
            hex.append(new Formatter().format("%02x", b));
        }
        return hex.toString().replaceFirst("(\\p{XDigit}{8})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}+)",
                "$1-$2-$3-$4-$5" );
    }

    // https://github.com/RadiusNetworks/android-ibeacon-service/blob/tip/src/main/java/com/radiusnetworks/ibeacon/IBeacon.java
    private PROXIMITY calculateProximity(int txPower, double rssi) {
        double accuracy = calculateAccuracy(txPower, rssi);
        if (accuracy < 0) {
            return PROXIMITY.UNKNOWN;
        }
        if (accuracy < 0.5) {
            return PROXIMITY.IMMEDIATE;
        }
        if (accuracy <= 4.0) {
            return PROXIMITY.NEAR;
        }
        return PROXIMITY.FAR;
    }

    private static double calculateAccuracy(int txPower, double rssi) {
        if (rssi == 0 || txPower == 0) {
            return -1.0; // if we cannot determine accuracy, return -1.
        }

        double ratio = rssi * 1.0 / txPower;
        if (ratio < 1.0) {
            return Math.pow(ratio, 10);
        } else {
            double accuracy = 0.89976 * Math.pow(ratio, 7.7095) + 0.111;
            return accuracy;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy