AdvertiseParams.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.AdvertiseData as FwkAdvertiseData
import android.bluetooth.le.AdvertiseSettings as FwkAdvertiseSettings
import android.bluetooth.le.AdvertisingSetParameters as FwkAdvertisingSetParameters
import android.os.Build
import android.os.ParcelUuid
import androidx.annotation.DoNotInline
import androidx.annotation.IntRange
import androidx.annotation.RequiresApi
import java.util.UUID

/**
 * A class to provide a way to adjust advertising preferences and advertise data packet.
 */
class AdvertiseParams(
    /** Whether the device address will be included in the advertisement packet. */
    @get:JvmName("shouldIncludeDeviceAddress")
    val shouldIncludeDeviceAddress: Boolean = false,
    /** Whether the device name will be included in the advertisement packet. */
    @get:JvmName("shouldIncludeDeviceName")
    val shouldIncludeDeviceName: Boolean = false,
    /** Whether the advertisement will indicate connectable. */
    val isConnectable: Boolean = false,
    /**
     * Whether the advertisement will be discoverable.
     *
     * Please note that it would be ignored under API level 34 and [isConnectable] would be
     * used instead.
     */
    val isDiscoverable: Boolean = false,
    /**
     * Advertising duration in milliseconds.
     *
     * It must not exceed 180000 milliseconds. A value of 0 means advertising continues
     * until it is stopped explicitly.
     * @throws IllegalArgumentException if it is not in the range [0..180000].
     */
    @IntRange(from = 0, to = 180000) val durationMillis: Long = 0,
    /**
     * A map of company identifiers to manufacturer specific data.
     * <p>
     * Please refer to the Bluetooth Assigned Numbers document provided by the <a
     * href="https://www.bluetooth.org">Bluetooth SIG</a> for the list of existing company
     * identifiers.
     */
    val manufacturerData: Map<Int, ByteArray> = emptyMap(),
    /**
     * A map of 16-bit UUIDs of the services to corresponding additional service data.
     */
    val serviceData: Map<UUID, ByteArray> = emptyMap(),
    /**
     * A list of service UUIDs to advertise.
     */
    val serviceUuids: List<UUID> = emptyList(),
    /**
     * A list of service solicitation UUIDs to advertise that we invite to connect.
     */
    val serviceSolicitationUuids: List<UUID> = emptyList()
) {
    @RequiresApi(34)
    private object AdvertiseParamsApi34Impl {
        @JvmStatic
        @DoNotInline
        fun setDiscoverable(builder: FwkAdvertiseSettings.Builder, isDiscoverable: Boolean) {
            builder.setDiscoverable(isDiscoverable)
        }

        @JvmStatic
        @DoNotInline
        fun setDiscoverable(builder: FwkAdvertisingSetParameters.Builder, isDiscoverable: Boolean) {
            builder.setDiscoverable(isDiscoverable)
        }
    }

    @RequiresApi(31)
    private object AdvertiseParamsApi31Impl {
        @JvmStatic
        @DoNotInline
        fun addServiceSolicitationUuid(builder: FwkAdvertiseData.Builder, parcelUuid: ParcelUuid) {
            builder.addServiceSolicitationUuid(parcelUuid)
        }
    }

    @RequiresApi(26)
    private object AdvertiseParamsApi26Impl {
        @JvmStatic
        @DoNotInline
        fun fwkAdvertiseSetParams(
            isConnectable: Boolean,
            isDiscoverable: Boolean
        ): FwkAdvertisingSetParameters = FwkAdvertisingSetParameters.Builder().run {
            setConnectable(isConnectable)
            if (Build.VERSION.SDK_INT >= 34) {
                AdvertiseParamsApi34Impl.setDiscoverable(this, isDiscoverable)
            }
            build()
        }
    }

    internal val fwkAdvertiseSettings: FwkAdvertiseSettings
        get() = FwkAdvertiseSettings.Builder().run {
            setConnectable(isConnectable)
            if (durationMillis > 0) {
                setTimeout(durationMillis.toInt())
            }
            if (Build.VERSION.SDK_INT >= 34) {
                AdvertiseParamsApi34Impl.setDiscoverable(this, isDiscoverable)
            }
            build()
        }

    @RequiresApi(26)
    internal fun fwkAdvertiseSetParams(): FwkAdvertisingSetParameters {
        return AdvertiseParamsApi26Impl.fwkAdvertiseSetParams(isConnectable, isDiscoverable)
    }

    internal val fwkAdvertiseData: FwkAdvertiseData
        get() = FwkAdvertiseData.Builder().run {
            setIncludeDeviceName(shouldIncludeDeviceName)
            serviceData.forEach {
                addServiceData(ParcelUuid(it.key), it.value)
            }
            manufacturerData.forEach {
                addManufacturerData(it.key, it.value)
            }
            serviceUuids.forEach {
                addServiceUuid(ParcelUuid(it))
            }
            if (Build.VERSION.SDK_INT >= 31) {
                serviceSolicitationUuids.forEach {
                    AdvertiseParamsApi31Impl.addServiceSolicitationUuid(this, ParcelUuid(it))
                }
            }
            build()
        }

    init {
        if (durationMillis !in 0..180000)
            throw IllegalArgumentException("Advertise duration must be in [0, 180000]")
    }
}