Search code examples
androidibeaconaltbeaconibeacon-android

Monitoring beacons that are advertising from my application through AltBeacon library


I'm working on a solution that advertises and scans in the iBeacon format using the AltBeacon library. The concern that i have is that the library scans all the devices which is fine but after parsing through the scanned devices it also tracks the advertising devices that are not advertising from my application. Is there anyway to solve this through using the library? If not what could be the alternate solution to this. It is very important for me to track the advertising beacons that are only advertising from my application.

This is the code is use while advertising in iBeacon format through the AltBeacon library:

BluetoothManager bluetoothManager =
        (BluetoothManager) applicationContext.getSystemService(Context.BLUETOOTH_SERVICE);
if (bluetoothManager != null) {
    BluetoothAdapter mBluetoothAdapter = bluetoothManager.getAdapter();
    BluetoothLeAdvertiser mBluetoothLeAdvertiser = mBluetoothAdapter.getBluetoothLeAdvertiser();
    if (mBluetoothLeAdvertiser != null) {
        beacon = new Beacon.Builder()
                .setId1(userId)
                .setId2("1")
                .setId3("1")
                .setManufacturer(0x004C)
                .setTxPower(-75)
                .setDataFields(Arrays.asList(new Long[]{0l}))
                .build();
        beaconParser = new BeaconParser()
                .setBeaconLayout("m:2-3=0215,i:4-19,i:20-21,i:22-23,p:24-24");
        beaconTransmitter = new BeaconTransmitter(InventaSdk.getContext(), beaconParser);
        beaconTransmitter.setBeacon(beacon);
    }
}

Edit:

Parsing Beacon code:

/**
 * Construct a Beacon from a Bluetooth LE packet collected by Android's Bluetooth APIs,
 * including the raw Bluetooth device info
 *
 * @param scanData The actual packet bytes
 * @param rssi The measured signal strength of the packet
 * @param device The Bluetooth device that was detected
 * @return An instance of a <code>Beacon</code>
 */
public Beacon fromScanData(byte[] scanData, int rssi, BluetoothDevice device) {
    return fromScanData(scanData, rssi, device, new Beacon());
}

protected Beacon fromScanData(byte[] bytesToProcess, int rssi, BluetoothDevice device, Beacon beacon) {
    BleAdvertisement advert = new BleAdvertisement(bytesToProcess);
    boolean parseFailed = false;
    Pdu pduToParse = null;
    int startByte = 0;
    ArrayList<Identifier> identifiers = new ArrayList<Identifier>();
    ArrayList<Long> dataFields = new ArrayList<Long>();

    for (Pdu pdu: advert.getPdus()) {
        if (pdu.getType() == Pdu.GATT_SERVICE_UUID_PDU_TYPE ||
                pdu.getType() == Pdu.MANUFACTURER_DATA_PDU_TYPE) {
            pduToParse = pdu;
            LogHelper.d(TAG, "Processing pdu type: "+pdu.getType()+bytesToHex(bytesToProcess)+" with startIndex: "+pdu.getStartIndex()+" endIndex: "+pdu.getEndIndex());
            break;
        }
        else {
            LogHelper.d(TAG, "Ignoring pdu type %02X "+ pdu.getType());
        }
    }
    if (pduToParse == null) {
        LogHelper.d(TAG, "No PDUs to process in this packet.");
        parseFailed = true;
    }
    else {
        byte[] serviceUuidBytes = null;
        byte[] typeCodeBytes = longToByteArray(getMatchingBeaconTypeCode(), mMatchingBeaconTypeCodeEndOffset - mMatchingBeaconTypeCodeStartOffset + 1);
        if (getServiceUuid() != null) {
            serviceUuidBytes = longToByteArray(getServiceUuid(), mServiceUuidEndOffset - mServiceUuidStartOffset + 1, false);
        }
        startByte = pduToParse.getStartIndex();
        boolean patternFound = false;

        if (getServiceUuid() == null) {
            if (byteArraysMatch(bytesToProcess, startByte + mMatchingBeaconTypeCodeStartOffset, typeCodeBytes)) {
                patternFound = true;
            }
        } else {
            if (byteArraysMatch(bytesToProcess, startByte + mServiceUuidStartOffset, serviceUuidBytes) &&
                    byteArraysMatch(bytesToProcess, startByte + mMatchingBeaconTypeCodeStartOffset, typeCodeBytes)) {
                patternFound = true;
            }
        }



        if (patternFound == false) {
            // This is not a beacon
            if (getServiceUuid() == null) {
                LogHelper.d(TAG, "This is not a matching Beacon advertisement. (Was expecting   "+byteArrayToString(typeCodeBytes)
                                    + ".The bytes I see are: "+
                            bytesToHex(bytesToProcess));

            } else {
                LogHelper.d(TAG, "This is not a matching Beacon advertisement. Was expecting "+
                        byteArrayToString(serviceUuidBytes)+
                        " at offset "+startByte + mServiceUuidStartOffset+"and "+byteArrayToString(typeCodeBytes)+
                        " at offset "+ startByte + mMatchingBeaconTypeCodeStartOffset + "The bytes I see are: "
                        + bytesToHex(bytesToProcess));
            }
            parseFailed = true;
            beacon =  null;
        } else {
            LogHelper.d(TAG, "This is a recognized beacon advertisement -- "+
                        byteArrayToString(typeCodeBytes)+"seen");
            LogHelper.d(TAG, "Bytes are: "+ bytesToHex(bytesToProcess));
        }

        if (patternFound) {
            if (bytesToProcess.length <= startByte+mLayoutSize && mAllowPduOverflow) {
                // If the layout size is bigger than this PDU, and we allow overflow.  Make sure
                // the byte buffer is big enough by zero padding the end so we don't try to read
                // outside the byte array of the advertisement
                LogHelper.d(TAG, "Expanding buffer because it is too short to parse: "+bytesToProcess.length+", needed: "+(startByte+mLayoutSize));
                bytesToProcess = ensureMaxSize(bytesToProcess, startByte+mLayoutSize);
            }
            for (int i = 0; i < mIdentifierEndOffsets.size(); i++) {
                int endIndex = mIdentifierEndOffsets.get(i) + startByte;

                if (endIndex > pduToParse.getEndIndex() && mIdentifierVariableLengthFlags.get(i)) {
                    LogHelper.d(TAG, "Need to truncate identifier by "+(endIndex-pduToParse.getEndIndex()));
                    // If this is a variable length identifier, we truncate it to the size that
                    // is available in the packet
                    int start = mIdentifierStartOffsets.get(i) + startByte;
                    int end = pduToParse.getEndIndex()+1;
                    if (end <= start) {
                        LogHelper.d(TAG, "PDU is too short for identifer.  Packet is malformed");
                        return null;
                    }
                    Identifier identifier = Identifier.fromBytes(bytesToProcess, start, end, mIdentifierLittleEndianFlags.get(i));
                    identifiers.add(identifier);
                }
                else if (endIndex > pduToParse.getEndIndex() && !mAllowPduOverflow) {
                    parseFailed = true;
                    LogHelper.d(TAG, "Cannot parse identifier "+i+" because PDU is too short.  endIndex: " + endIndex + " PDU endIndex: " + pduToParse.getEndIndex());
                }
                else {
                    Identifier identifier = Identifier.fromBytes(bytesToProcess, mIdentifierStartOffsets.get(i) + startByte, endIndex+1, mIdentifierLittleEndianFlags.get(i));
                    identifiers.add(identifier);
                }
            }
            for (int i = 0; i < mDataEndOffsets.size(); i++) {
                int endIndex = mDataEndOffsets.get(i) + startByte;
                if (endIndex > pduToParse.getEndIndex() && !mAllowPduOverflow) {
                    LogHelper.d(TAG, "Cannot parse data field "+i+" because PDU is too short.  endIndex: " + endIndex + " PDU endIndex: " + pduToParse.getEndIndex()+".  Setting value to 0");
                    dataFields.add(new Long(0l));
                }
                else {
                    String dataString = byteArrayToFormattedString(bytesToProcess, mDataStartOffsets.get(i) + startByte, endIndex, mDataLittleEndianFlags.get(i));
                    dataFields.add(Long.decode(dataString));
                }
            }

            if (mPowerStartOffset != null) {
                int endIndex = mPowerEndOffset + startByte;
                int txPower = 0;
                try {
                    if (endIndex > pduToParse.getEndIndex() && !mAllowPduOverflow) {
                        parseFailed = true;
                        LogHelper.d(TAG, "Cannot parse power field because PDU is too short.  endIndex: " + endIndex + " PDU endIndex: " + pduToParse.getEndIndex());
                    }
                    else {
                        String powerString = byteArrayToFormattedString(bytesToProcess, mPowerStartOffset + startByte, mPowerEndOffset + startByte, false);
                        txPower = Integer.parseInt(powerString)+mDBmCorrection;
                        // make sure it is a signed integer
                        if (txPower > 127) {
                            txPower -= 256;
                        }
                        beacon.mTxPower = txPower;
                    }
                }
                catch (NumberFormatException e1) {
                    // keep default value
                }
                catch (NullPointerException e2) {
                    // keep default value
                }
            }
        }
    }

    if (parseFailed) {
        beacon = null;
    }
    else {
        int beaconTypeCode = 0;
        String beaconTypeString = byteArrayToFormattedString(bytesToProcess, mMatchingBeaconTypeCodeStartOffset+startByte, mMatchingBeaconTypeCodeEndOffset+startByte, false);
        beaconTypeCode = Integer.parseInt(beaconTypeString);
        // TODO: error handling needed on the parse

        int manufacturer = 0;
        String manufacturerString = byteArrayToFormattedString(bytesToProcess, startByte, startByte+1, true);
        manufacturer = Integer.parseInt(manufacturerString);

        String macAddress = null;
        String name = null;
        if (device != null) {
            macAddress = device.getAddress();
            name = device.getName();
        }

        beacon.mIdentifiers = identifiers;
        beacon.mDataFields = dataFields;
        beacon.mRssi = rssi;
        beacon.mBeaconTypeCode = beaconTypeCode;
        if (mServiceUuid != null) {
            beacon.mServiceUuid = (int) mServiceUuid.longValue();
        }
        else {
            beacon.mServiceUuid = -1;
        }

        beacon.mBluetoothAddress = macAddress;
        beacon.mBluetoothName= name;
        beacon.mManufacturer = manufacturer;
        beacon.mParserIdentifier = mIdentifier;
        beacon.mMultiFrameBeacon = extraParsers.size() > 0 || mExtraFrame;
    }
    return beacon;
}

Scan callbacks:

private ScanCallback getNewLeScanCallback() {
    if (leScanCallback == null) {
        leScanCallback = new ScanCallback() {
            @MainThread
            @Override
            public void onScanResult(int callbackType, ScanResult scanResult) {
                    LogHelper.d(TAG, "got record");
                    List<ParcelUuid> uuids = scanResult.getScanRecord().getServiceUuids();
                    if (uuids != null) {
                        for (ParcelUuid uuid : uuids) {
                            LogHelper.d(TAG, "with service uuid: "+uuid);
                        }
                    }

                    try {
                        LogHelper.d("ScanRecord", "Raw Data: " + scanResult.toString());
                        LogHelper.d("ScanRecord", "Device Data Name: " + scanResult.getDevice().getName() + "Rssi: " + scanResult.getRssi() + "Address: " + scanResult.getDevice().getAddress() + "Service uuid: " + scanResult.getScanRecord().getServiceUuids());
                    }catch (Exception e){
                        LogHelper.d("ScanRecord",e.getMessage());
                        e.printStackTrace();
                    }
                mCycledLeScanCallback.onLeScan(scanResult.getDevice(),
                        scanResult.getRssi(), scanResult.getScanRecord().getBytes());
                if (mBackgroundLScanStartTime > 0) {
                    LogHelper.d(TAG, "got a filtered scan result in the background.");
                }
            }

            @MainThread
            @Override
            public void onBatchScanResults(List<ScanResult> results) {
                LogHelper.d(TAG, "got batch records");
                for (ScanResult scanResult : results) {
                    mCycledLeScanCallback.onLeScan(scanResult.getDevice(),
                            scanResult.getRssi(), scanResult.getScanRecord().getBytes());
                }
                if (mBackgroundLScanStartTime > 0) {
                    LogHelper.d(TAG, "got a filtered batch scan result in the background.");
                }
            }

            @MainThread
            @Override
            public void onScanFailed(int errorCode) {
                Intent intent = new Intent("onScanFailed");
                intent.putExtra("errorCode", errorCode);
                LocalBroadcastManager.getInstance(CycledLeScannerForLollipop.this.mContext).sendBroadcast(intent);
                switch (errorCode) {
                    case SCAN_FAILED_ALREADY_STARTED:
                        LogHelper.e(TAG, "Scan failed: a BLE scan with the same settings is already started by the app");
                        break;
                    case SCAN_FAILED_APPLICATION_REGISTRATION_FAILED:
                        LogHelper.e(TAG, "Scan failed: app cannot be registered");
                        break;
                    case SCAN_FAILED_FEATURE_UNSUPPORTED:
                        LogHelper.e(TAG, "Scan failed: power optimized scan feature is not supported");
                        break;
                    case SCAN_FAILED_INTERNAL_ERROR:
                        LogHelper.e(TAG, "Scan failed: internal error");
                        break;
                    default:
                        LogHelper.e(TAG, "Scan failed with unknown error (errorCode=" + errorCode + ")");
                        break;
                }
            }
        };
    }
    return leScanCallback;
}

Solution

  • The general approach to filter for “your” beacons is to see an an identifier prefix that is common to all your beacons. You then tell if it is your beacon by filtering on beacons that match this identifier prefix.

    Two ways to do the filtering:

    A) Software filtering after scan results come in.

    With this approach, you wait until you parse the beacons and then use an if statement to see if the beacon identifiers match your prefix. If not, do not process it. The Android Beacon Library has this as a built-in feature by using Region objects to provide matching patterns for “your” beacons.

        // replace uuid with your own
        beaconManager.startRangingBeaconsInRegion(new Region("matchOnlyMyBeacons", Identifier.parse(“2F234454-CF6D-4A0F-ADF2-F4911BA9”)), null, null));
    
        beaconManager.addRangeNotifier(new RangeNotifier() {
            @Override
            public void didRangeBeaconsInRegion(Collection<Beacon> beacons, Region region) {
               // only beacons matching the identifiers in the Region are included here
            }
        });
    

    Since you are mot using the library as a whole but copying some of its code, you may have to build similar logic yourself like this:

      // replace the uuid with yours below
      if (beacon.getID1().equals(Identifier.parse(“2F234454-CF6D-4A0F-ADF2-F4911BA9”)){
        // only process matching beacons here
      }
    

    This is a simple approach as very flexible. It works well in cases where your app runs only in the foreground or in the background when usually there are few BLE devices around that are not of interest.

    The disadvantage is that it can burn cpu and battery if lots of beacons are around that are not of interest.

    B) Use hardware scan filters

    Android 6+ APIs allow you to put similar matching functions into the Bluetooth chip itself so all scan callbacks you get already match the identifier prefix. This is us less taxing on CPU and battery but has disadvantages:

    1. Not all devices support this, though most devices built since 2018 do.

    2. Hardware filters are a limited resource. If other apps take all of them up, you will not get scan results.

    3. Filters are inflexible. If even a single byte of the advertisement prefix doesn’t match (commonly due to a differing manufacturer code) you will not get a scan result.

       ScanFilter.Builder builder = new ScanFilter.Builder();
       builder.setServiceUuid(null);
       byte[] filterBytes = new byte[]{
               /* 0215 are the start of iBeacon.  Use beac for AltBeacon */
               (byte) 0x02, (byte) 0x15,
               // These bytes are your 16 byte proximityUUID (ID1)
               (byte) 0x2F, (byte) 0x23, (byte) 0x44, (byte) 0x54, (byte) 0xCF, (byte) 0x6D, (byte) 0x4A, (byte) 0x0F, (byte) 0xAD, (byte) 0xF2, (byte) 0xF4, (byte) 0x91, (byte) 0x1B, (byte) 0xA9, (byte) 0xFF, (byte) 0xA6
       };
      
       byte[] maskBytes = new byte[]{
               /* Make this the same length as your filter bytes, and set every value to 0xff to match all bytes */
               (byte) 0xff, (byte) 0xff,
               (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff
       };
       builder.setManufacturerData((int) 0x004c /* apple for iBeacon, use 0x0118 for AltBeacon */, filterBytes, maskBytes);
       ScanFilter[] scanFilters = new ScanFilter[] { builder.build() };
       scanner.startScan(scanFilters, scanSettings, scanCallback);