ScanFilter.kt

/*
 * Copyright 2023 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 androidx.bluetooth

import android.bluetooth.le.ScanFilter as FwkScanFilter
import android.os.Build
import android.os.ParcelUuid
import androidx.annotation.DoNotInline
import androidx.annotation.RequiresApi
import java.util.UUID

/**
 * Criteria for filtering result from Bluetooth LE scans. A ScanFilter allows clients to restrict
 * scan results to only those that are of interest to them.
 */
class ScanFilter(
    /** The scan filter for the remote device address. `null` if filter is not set. */
    val deviceAddress: BluetoothAddress? = null,

    /** The scan filter for the remote device name. `null` if filter is not set. */
    val deviceName: String? = null,

    /** The scan filter for manufacturer id. [MANUFACTURER_FILTER_NONE] if filter is not set. */
    val manufacturerId: Int = MANUFACTURER_FILTER_NONE,

    /** The scan filter for manufacturer data. `null` if filter is not set. */
    val manufacturerData: ByteArray? = null,

    /** The partial filter on manufacturerData. `null` if filter is not set. */
    val manufacturerDataMask: ByteArray? = null,

    /** The scan filter for service data uuid. `null` if filter is not set. */
    val serviceDataUuid: UUID? = null,

    /** The scan filter for service data. `null` if filter is not set. */
    val serviceData: ByteArray? = null,

    /** The partial filter on service data. `null` if filter is not set. */
    val serviceDataMask: ByteArray? = null,

    /** The scan filter for service uuid. `null` if filter is not set. */
    val serviceUuid: UUID? = null,

    /**
     * The partial filter on service uuid. `null` if filter is not set.
     * @throws IllegalArgumentException if this bit mask [serviceUuidMask] is set but
     * [serviceUuid] is null
     */
    val serviceUuidMask: UUID? = null,

    /**
     * The scan filter for service Solicitation uuid. `null` if filter is not set.
     *
     * Please note that this will be ignored on versions before [android.os.Build.VERSION_CODES.Q].
     */
    val serviceSolicitationUuid: UUID? = null,

    /**
     * The partial filter on service Solicitation uuid. This bit mask is for
     * [serviceSolicitationUuid]. Set any bit in the mask to 1 to indicate a match is needed
     * for the bit in [serviceSolicitationUuid], and 0 to ignore that bit.
     * `null` if filter is not set.
     * @throws IllegalArgumentException if this bit mask [serviceSolicitationUuidMask] is set but
     * [serviceSolicitationUuid] is null
     *
     * Please note that this will be ignored on versions before [android.os.Build.VERSION_CODES.Q].
     */
    val serviceSolicitationUuidMask: UUID? = null
) {

    companion object {
        const val MANUFACTURER_FILTER_NONE: Int = -1
    }

    @RequiresApi(29)
    private object ScanFilterApi29Impl {
        @JvmStatic
        @DoNotInline
        fun setServiceSolicitationUuid(
            builder: FwkScanFilter.Builder,
            serviceSolicitationUuid: UUID,
            serviceSolicitationUuidMask: UUID?
        ) {
            if (serviceSolicitationUuidMask == null) {
                builder.setServiceSolicitationUuid(ParcelUuid(serviceSolicitationUuid))
            } else {
                builder.setServiceSolicitationUuid(
                    ParcelUuid(serviceSolicitationUuid),
                    ParcelUuid(serviceSolicitationUuidMask)
                )
            }
        }
    }

    init {
        if (manufacturerId < 0 && manufacturerId != MANUFACTURER_FILTER_NONE) {
            throw IllegalArgumentException("Invalid manufacturerId")
        }

        if (manufacturerDataMask != null) {
            if (manufacturerData == null) {
                throw IllegalArgumentException(
                    "ManufacturerData is null while manufacturerDataMask is not null"
                )
            }

            if (manufacturerData.size != manufacturerDataMask.size) {
                throw IllegalArgumentException(
                    "Size mismatch for manufacturerData and manufacturerDataMask"
                )
            }
        }

        if (serviceDataMask != null) {
            if (serviceData == null) {
                throw IllegalArgumentException(
                    "ServiceData is null while serviceDataMask is not null"
                )
            }

            if (serviceData.size != serviceDataMask.size) {
                throw IllegalArgumentException(
                    "Size mismatch for service data and service data mask"
                )
            }
        }

        if (serviceUuid == null && serviceUuidMask != null) {
            throw IllegalArgumentException("ServiceUuid is null while ServiceUuidMask is not null")
        }

        if (serviceSolicitationUuid == null && serviceSolicitationUuidMask != null) {
            throw IllegalArgumentException(
                "ServiceSolicitationUuid is null while ServiceSolicitationUuidMask is not null"
            )
        }
    }

    internal val fwkScanFilter: FwkScanFilter by lazy(LazyThreadSafetyMode.PUBLICATION) {
        FwkScanFilter.Builder().run {
            deviceAddress?.let { setDeviceAddress(it.address) }

            deviceName?.let { setDeviceName(it) }

            if (manufacturerId != MANUFACTURER_FILTER_NONE && manufacturerData != null) {
                if (Build.VERSION.SDK_INT >= 33) {
                    setManufacturerData(
                        manufacturerId,
                        manufacturerData,
                        manufacturerDataMask
                    )
                } else {
                    setManufacturerData(manufacturerId, manufacturerData)
                }
            }

            if (serviceDataUuid != null) {
                if (Build.VERSION.SDK_INT >= 33) {
                    setServiceData(
                        ParcelUuid(
                            serviceDataUuid
                        ),
                        serviceData,
                        serviceDataMask
                    )
                } else {
                    setServiceData(ParcelUuid(serviceDataUuid), serviceData)
                }
            }

            serviceUuid?.let {
                if (Build.VERSION.SDK_INT >= 33) {
                    setServiceUuid(ParcelUuid(it), ParcelUuid(serviceUuidMask))
                } else {
                    setServiceUuid(ParcelUuid(it))
                }
            }

            serviceSolicitationUuid?.let {
                if (Build.VERSION.SDK_INT >= 29) {
                    ScanFilterApi29Impl.setServiceSolicitationUuid(
                        this,
                        it,
                        serviceSolicitationUuidMask
                    )
                }
            }
            build()
        }
    }
}