/* * Copyright (C) 2016 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 android.net.wifi.aware; import static android.Manifest.permission.MANAGE_WIFI_NETWORK_SELECTION; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.net.wifi.OuiKeyedData; import android.net.wifi.ParcelUtil; import android.net.wifi.ScanResult; import android.net.wifi.WifiScanner; import android.net.wifi.util.HexEncoding; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.RequiresApi; import com.android.modules.utils.build.SdkLevel; import com.android.wifi.flags.Flags; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; /** * Defines the configuration of an Aware subscribe session. Built using * {@link SubscribeConfig.Builder}. Subscribe is done using * {@link WifiAwareSession#subscribe(SubscribeConfig, DiscoverySessionCallback, * android.os.Handler)} or * {@link SubscribeDiscoverySession#updateSubscribe(SubscribeConfig)}. */ public final class SubscribeConfig implements Parcelable { /** @hide */ @IntDef({ SUBSCRIBE_TYPE_PASSIVE, SUBSCRIBE_TYPE_ACTIVE }) @Retention(RetentionPolicy.SOURCE) public @interface SubscribeTypes { } /** * Defines a passive subscribe session - a subscribe session where * subscribe packets are not transmitted over-the-air and the device listens * and matches to transmitted publish packets. Configuration is done using * {@link SubscribeConfig.Builder#setSubscribeType(int)}. */ public static final int SUBSCRIBE_TYPE_PASSIVE = 0; /** * Defines an active subscribe session - a subscribe session where * subscribe packets are transmitted over-the-air. Configuration is done * using {@link SubscribeConfig.Builder#setSubscribeType(int)}. */ public static final int SUBSCRIBE_TYPE_ACTIVE = 1; /** @hide */ public final byte[] mServiceName; /** @hide */ public final byte[] mServiceSpecificInfo; /** @hide */ public final byte[] mMatchFilter; /** @hide */ public final int mSubscribeType; /** @hide */ public final int mTtlSec; /** @hide */ public final boolean mEnableTerminateNotification; /** @hide */ public final boolean mMinDistanceMmSet; /** @hide */ public final int mMinDistanceMm; /** @hide */ public final boolean mMaxDistanceMmSet; /** @hide */ public final int mMaxDistanceMm; private final boolean mEnableInstantMode; private final int mBand; private final AwarePairingConfig mPairingConfig; private final boolean mIsSuspendable; private final List mVendorData; /** @hide */ public SubscribeConfig(byte[] serviceName, byte[] serviceSpecificInfo, byte[] matchFilter, int subscribeType, int ttlSec, boolean enableTerminateNotification, boolean minDistanceMmSet, int minDistanceMm, boolean maxDistanceMmSet, int maxDistanceMm, boolean enableInstantMode, @WifiScanner.WifiBand int band, AwarePairingConfig pairingConfig, boolean isSuspendable, @NonNull List vendorData) { mServiceName = serviceName; mServiceSpecificInfo = serviceSpecificInfo; mMatchFilter = matchFilter; mSubscribeType = subscribeType; mTtlSec = ttlSec; mEnableTerminateNotification = enableTerminateNotification; mMinDistanceMm = minDistanceMm; mMinDistanceMmSet = minDistanceMmSet; mMaxDistanceMm = maxDistanceMm; mMaxDistanceMmSet = maxDistanceMmSet; mEnableInstantMode = enableInstantMode; mBand = band; mPairingConfig = pairingConfig; mIsSuspendable = isSuspendable; mVendorData = vendorData; } @Override public String toString() { return "SubscribeConfig [mServiceName='" + (mServiceName == null ? "" : String.valueOf(HexEncoding.encode(mServiceName))) + ", mServiceName.length=" + ( mServiceName == null ? 0 : mServiceName.length) + ", mServiceSpecificInfo='" + ( (mServiceSpecificInfo == null) ? "" : String.valueOf( HexEncoding.encode(mServiceSpecificInfo))) + ", mServiceSpecificInfo.length=" + (mServiceSpecificInfo == null ? 0 : mServiceSpecificInfo.length) + ", mMatchFilter=" + (new TlvBufferUtils.TlvIterable(0, 1, mMatchFilter)).toString() + ", mMatchFilter.length=" + (mMatchFilter == null ? 0 : mMatchFilter.length) + ", mSubscribeType=" + mSubscribeType + ", mTtlSec=" + mTtlSec + ", mEnableTerminateNotification=" + mEnableTerminateNotification + ", mMinDistanceMm=" + mMinDistanceMm + ", mMinDistanceMmSet=" + mMinDistanceMmSet + ", mMaxDistanceMm=" + mMaxDistanceMm + ", mMaxDistanceMmSet=" + mMaxDistanceMmSet + "]" + ", mEnableInstantMode=" + mEnableInstantMode + ", mBand=" + mBand + ", mPairingConfig" + mPairingConfig + ", mIsSuspendable=" + mIsSuspendable + ", mVendorData=" + mVendorData + "]"; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeByteArray(mServiceName); dest.writeByteArray(mServiceSpecificInfo); dest.writeByteArray(mMatchFilter); dest.writeInt(mSubscribeType); dest.writeInt(mTtlSec); dest.writeInt(mEnableTerminateNotification ? 1 : 0); dest.writeInt(mMinDistanceMm); dest.writeInt(mMinDistanceMmSet ? 1 : 0); dest.writeInt(mMaxDistanceMm); dest.writeInt(mMaxDistanceMmSet ? 1 : 0); dest.writeBoolean(mEnableInstantMode); dest.writeInt(mBand); dest.writeParcelable(mPairingConfig, flags); dest.writeBoolean(mIsSuspendable); dest.writeList(mVendorData); } @NonNull public static final Creator CREATOR = new Creator<>() { @Override public SubscribeConfig[] newArray(int size) { return new SubscribeConfig[size]; } @Override public SubscribeConfig createFromParcel(Parcel in) { byte[] serviceName = in.createByteArray(); byte[] ssi = in.createByteArray(); byte[] matchFilter = in.createByteArray(); int subscribeType = in.readInt(); int ttlSec = in.readInt(); boolean enableTerminateNotification = in.readInt() != 0; int minDistanceMm = in.readInt(); boolean minDistanceMmSet = in.readInt() != 0; int maxDistanceMm = in.readInt(); boolean maxDistanceMmSet = in.readInt() != 0; boolean enableInstantMode = in.readBoolean(); int band = in.readInt(); AwarePairingConfig pairingConfig = in.readParcelable( AwarePairingConfig.class.getClassLoader()); boolean isSuspendable = in.readBoolean(); List vendorData = ParcelUtil.readOuiKeyedDataList(in); return new SubscribeConfig(serviceName, ssi, matchFilter, subscribeType, ttlSec, enableTerminateNotification, minDistanceMmSet, minDistanceMm, maxDistanceMmSet, maxDistanceMm, enableInstantMode, band, pairingConfig, isSuspendable, vendorData); } }; @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof SubscribeConfig)) { return false; } SubscribeConfig lhs = (SubscribeConfig) o; if (!(Arrays.equals(mServiceName, lhs.mServiceName) && Arrays.equals( mServiceSpecificInfo, lhs.mServiceSpecificInfo) && Arrays.equals(mMatchFilter, lhs.mMatchFilter) && mSubscribeType == lhs.mSubscribeType && mTtlSec == lhs.mTtlSec && mEnableTerminateNotification == lhs.mEnableTerminateNotification && mMinDistanceMmSet == lhs.mMinDistanceMmSet && mMaxDistanceMmSet == lhs.mMaxDistanceMmSet && mEnableInstantMode == lhs.mEnableInstantMode && mBand == lhs.mBand && mIsSuspendable == lhs.mIsSuspendable && Objects.equals(mVendorData, lhs.mVendorData))) { return false; } if (mMinDistanceMmSet && mMinDistanceMm != lhs.mMinDistanceMm) { return false; } if (mMaxDistanceMmSet && mMaxDistanceMm != lhs.mMaxDistanceMm) { return false; } return true; } @Override public int hashCode() { int result = Objects.hash(Arrays.hashCode(mServiceName), Arrays.hashCode(mServiceSpecificInfo), Arrays.hashCode(mMatchFilter), mSubscribeType, mTtlSec, mEnableTerminateNotification, mMinDistanceMmSet, mMaxDistanceMmSet, mEnableInstantMode, mBand, mIsSuspendable, mVendorData); if (mMinDistanceMmSet) { result = Objects.hash(result, mMinDistanceMm); } if (mMaxDistanceMmSet) { result = Objects.hash(result, mMaxDistanceMm); } return result; } /** * Verifies that the contents of the SubscribeConfig are valid. Otherwise * throws an IllegalArgumentException. * * @hide */ public void assertValid(Characteristics characteristics, boolean rttSupported) throws IllegalArgumentException { WifiAwareUtils.validateServiceName(mServiceName); if (!TlvBufferUtils.isValid(mMatchFilter, 0, 1)) { throw new IllegalArgumentException( "Invalid matchFilter configuration - LV fields do not match up to length"); } if (mSubscribeType < SUBSCRIBE_TYPE_PASSIVE || mSubscribeType > SUBSCRIBE_TYPE_ACTIVE) { throw new IllegalArgumentException("Invalid subscribeType - " + mSubscribeType); } if (mTtlSec < 0) { throw new IllegalArgumentException("Invalid ttlSec - must be non-negative"); } if (characteristics != null) { int maxServiceNameLength = characteristics.getMaxServiceNameLength(); if (maxServiceNameLength != 0 && mServiceName.length > maxServiceNameLength) { throw new IllegalArgumentException( "Service name longer than supported by device characteristics"); } int maxServiceSpecificInfoLength = characteristics.getMaxServiceSpecificInfoLength(); if (maxServiceSpecificInfoLength != 0 && mServiceSpecificInfo != null && mServiceSpecificInfo.length > maxServiceSpecificInfoLength) { throw new IllegalArgumentException( "Service specific info longer than supported by device characteristics"); } int maxMatchFilterLength = characteristics.getMaxMatchFilterLength(); if (maxMatchFilterLength != 0 && mMatchFilter != null && mMatchFilter.length > maxMatchFilterLength) { throw new IllegalArgumentException( "Match filter longer than supported by device characteristics"); } if (mEnableInstantMode) { if (SdkLevel.isAtLeastT() && characteristics.isInstantCommunicationModeSupported()) { // Valid to use instant communication mode } else { throw new IllegalArgumentException("instant mode is not supported"); } } if (mIsSuspendable && !characteristics.isSuspensionSupported()) { throw new IllegalArgumentException("Aware Suspension is not supported"); } if (mPairingConfig != null && !characteristics.isAwarePairingSupported()) { throw new IllegalArgumentException("Aware Pairing is not supported"); } } if (mMinDistanceMmSet && mMinDistanceMm < 0) { throw new IllegalArgumentException("Minimum distance must be non-negative"); } if (mMaxDistanceMmSet && mMaxDistanceMm < 0) { throw new IllegalArgumentException("Maximum distance must be non-negative"); } if (mMinDistanceMmSet && mMaxDistanceMmSet && mMaxDistanceMm <= mMinDistanceMm) { throw new IllegalArgumentException( "Maximum distance must be greater than minimum distance"); } if (!rttSupported && (mMinDistanceMmSet || mMaxDistanceMmSet)) { throw new IllegalArgumentException("Ranging is not supported"); } } /** * Check if instant mode is enabled for this subscribe session. * @see Builder#setInstantCommunicationModeEnabled(boolean, int) * @return true for enabled, false otherwise. */ public boolean isInstantCommunicationModeEnabled() { return mEnableInstantMode; } /** * Check if enable instant mode on 5G for this subscribe session * * @see Builder#setInstantCommunicationModeEnabled(boolean, int) * @return If instant communication mode is not enabled will return {@link * ScanResult#WIFI_BAND_24_GHZ} as default. */ @WifiAwareManager.InstantModeBand public int getInstantCommunicationBand() { return mBand; } /** * Get the Aware Pairing config for this subscribe session * @see Builder#setPairingConfig(AwarePairingConfig) * @return A {@link AwarePairingConfig} specified in this config. */ @Nullable public AwarePairingConfig getPairingConfig() { return mPairingConfig; } /** * Check if suspension is supported for this subscribe session. * @see Builder#setSuspendable(boolean) * @return true for supported, false otherwise. * @hide */ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @SystemApi public boolean isSuspendable() { if (!SdkLevel.isAtLeastU()) { throw new UnsupportedOperationException(); } return mIsSuspendable; } /** * Return the vendor-provided configuration data, if it exists. See also {@link * Builder#setVendorData(List)} * * @return Vendor configuration data, or empty list if it does not exist. * @hide */ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) @FlaggedApi(Flags.FLAG_ANDROID_V_WIFI_API) @NonNull @SystemApi public List getVendorData() { if (!SdkLevel.isAtLeastV()) { throw new UnsupportedOperationException(); } return mVendorData != null ? mVendorData : Collections.emptyList(); } /** * Builder used to build {@link SubscribeConfig} objects. */ public static final class Builder { private byte[] mServiceName; private byte[] mServiceSpecificInfo; private byte[] mMatchFilter; private int mSubscribeType = SUBSCRIBE_TYPE_PASSIVE; private int mTtlSec = 0; private boolean mEnableTerminateNotification = true; private boolean mMinDistanceMmSet = false; private int mMinDistanceMm; private boolean mMaxDistanceMmSet = false; private int mMaxDistanceMm; private boolean mEnableInstantMode; private int mBand = WifiScanner.WIFI_BAND_24_GHZ; private AwarePairingConfig mPairingConfig; private boolean mIsSuspendable = false; private @NonNull List mVendorData = Collections.emptyList(); /** * Specify the service name of the subscribe session. The actual on-air * value is a 6 byte hashed representation of this string. *

* The Service Name is a UTF-8 encoded string from 1 to 255 bytes in length. * The only acceptable single-byte UTF-8 symbols for a Service Name are alphanumeric * values (A-Z, a-z, 0-9), the hyphen ('-'), the period ('.') and the underscore ('_'). All * valid multi-byte UTF-8 characters are acceptable in a Service Name. *

* Must be called - an empty ServiceName is not valid. * * @param serviceName The service name for the subscribe session. * * @return The builder to facilitate chaining * {@code builder.setXXX(..).setXXX(..)}. */ public Builder setServiceName(@NonNull String serviceName) { if (serviceName == null) { throw new IllegalArgumentException("Invalid service name - must be non-null"); } mServiceName = serviceName.getBytes(StandardCharsets.UTF_8); return this; } /** * Specify service specific information for the subscribe session. This is * a free-form byte array available to the application to send * additional information as part of the discovery operation - i.e. it * will not be used to determine whether a publish/subscribe match * occurs. *

* Optional. Empty by default. * * @param serviceSpecificInfo A byte-array for the service-specific * information field. * * @return The builder to facilitate chaining * {@code builder.setXXX(..).setXXX(..)}. */ public Builder setServiceSpecificInfo(@Nullable byte[] serviceSpecificInfo) { mServiceSpecificInfo = serviceSpecificInfo; return this; } /** * The match filter for a subscribe session. Used to determine whether a service * discovery occurred - in addition to relying on the service name. *

* Optional. Empty by default. * * @param matchFilter A list of match filter entries (each of which is an arbitrary byte * array). * * @return The builder to facilitate chaining * {@code builder.setXXX(..).setXXX(..)}. */ public Builder setMatchFilter(@Nullable List matchFilter) { mMatchFilter = new TlvBufferUtils.TlvConstructor(0, 1).allocateAndPut( matchFilter).getArray(); return this; } /** * Sets the type of the subscribe session: active (subscribe packets are * transmitted over-the-air), or passive (no subscribe packets are * transmitted, a match is made against a solicited/active publish * session whose packets are transmitted over-the-air). * * @param subscribeType Subscribe session type: * {@link SubscribeConfig#SUBSCRIBE_TYPE_ACTIVE} or * {@link SubscribeConfig#SUBSCRIBE_TYPE_PASSIVE}. * * @return The builder to facilitate chaining * {@code builder.setXXX(..).setXXX(..)}. */ public Builder setSubscribeType(@SubscribeTypes int subscribeType) { if (subscribeType < SUBSCRIBE_TYPE_PASSIVE || subscribeType > SUBSCRIBE_TYPE_ACTIVE) { throw new IllegalArgumentException("Invalid subscribeType - " + subscribeType); } mSubscribeType = subscribeType; return this; } /** * Sets the time interval (in seconds) an active ( * {@link SubscribeConfig.Builder#setSubscribeType(int)}) subscribe session * will be alive - i.e. broadcasting a packet. When the TTL is reached * an event will be generated for * {@link DiscoverySessionCallback#onSessionTerminated()}. *

* Optional. 0 by default - indicating the session doesn't terminate on its own. * Session will be terminated when {@link DiscoverySession#close()} is * called. * * @param ttlSec Lifetime of a subscribe session in seconds. * * @return The builder to facilitate chaining * {@code builder.setXXX(..).setXXX(..)}. */ public Builder setTtlSec(int ttlSec) { if (ttlSec < 0) { throw new IllegalArgumentException("Invalid ttlSec - must be non-negative"); } mTtlSec = ttlSec; return this; } /** * Configure whether a subscribe terminate notification * {@link DiscoverySessionCallback#onSessionTerminated()} is reported * back to the callback. * * @param enable If true the terminate callback will be called when the * subscribe is terminated. Otherwise it will not be called. * * @return The builder to facilitate chaining * {@code builder.setXXX(..).setXXX(..)}. */ public Builder setTerminateNotificationEnabled(boolean enable) { mEnableTerminateNotification = enable; return this; } /** * Configure the minimum distance to a discovered publisher at which to trigger a discovery * notification. I.e. discovery will be triggered if we've found a matching publisher * (based on the other criteria in this configuration) and the distance to the * publisher is larger than the value specified in this API. Can be used in conjunction with * {@link #setMaxDistanceMm(int)} to specify a geofence, i.e. discovery with min <= * distance <= max. *

* For ranging to be used in discovery it must also be enabled on the publisher using * {@link PublishConfig.Builder#setRangingEnabled(boolean)}. However, ranging may * not be available or enabled on the publisher or may be temporarily disabled on either * subscriber or publisher - in such cases discovery will proceed without ranging. *

* When ranging is enabled and available on both publisher and subscriber and a service * is discovered based on geofence constraints the * {@link DiscoverySessionCallback#onServiceDiscoveredWithinRange(PeerHandle, byte[], List, int)} * is called, otherwise the * {@link DiscoverySessionCallback#onServiceDiscovered(PeerHandle, byte[], List)} * is called. *

* The device must support Wi-Fi RTT for this feature to be used. Feature support is checked * as described in {@link android.net.wifi.rtt}. * * @param minDistanceMm Minimum distance, in mm, to the publisher above which to trigger * discovery. * * @return The builder to facilitate chaining * {@code builder.setXXX(..).setXXX(..)}. */ public Builder setMinDistanceMm(int minDistanceMm) { mMinDistanceMm = minDistanceMm; mMinDistanceMmSet = true; return this; } /** * Configure the maximum distance to a discovered publisher at which to trigger a discovery * notification. I.e. discovery will be triggered if we've found a matching publisher * (based on the other criteria in this configuration) and the distance to the * publisher is smaller than the value specified in this API. Can be used in conjunction * with {@link #setMinDistanceMm(int)} to specify a geofence, i.e. discovery with min <= * distance <= max. *

* For ranging to be used in discovery it must also be enabled on the publisher using * {@link PublishConfig.Builder#setRangingEnabled(boolean)}. However, ranging may * not be available or enabled on the publisher or may be temporarily disabled on either * subscriber or publisher - in such cases discovery will proceed without ranging. *

* When ranging is enabled and available on both publisher and subscriber and a service * is discovered based on geofence constraints the * {@link DiscoverySessionCallback#onServiceDiscoveredWithinRange(PeerHandle, byte[], List, int)} * is called, otherwise the * {@link DiscoverySessionCallback#onServiceDiscovered(PeerHandle, byte[], List)} * is called. *

* The device must support Wi-Fi RTT for this feature to be used. Feature support is checked * as described in {@link android.net.wifi.rtt}. * * @param maxDistanceMm Maximum distance, in mm, to the publisher below which to trigger * discovery. * * @return The builder to facilitate chaining * {@code builder.setXXX(..).setXXX(..)}. */ public Builder setMaxDistanceMm(int maxDistanceMm) { mMaxDistanceMm = maxDistanceMm; mMaxDistanceMmSet = true; return this; } /** * Configure whether to enable and use instant communication for this subscribe session. * Instant communication will speed up service discovery and any data-path set up as part of * this session. Use {@link Characteristics#isInstantCommunicationModeSupported()} to check * if the device supports this feature. * *

Note: due to increased power requirements of this mode - it will only remain enabled * for 30 seconds from the time the discovery session is started. * * @param enabled true for enable instant communication mode, default is false. * @param band When setting to {@link ScanResult#WIFI_BAND_5_GHZ}, device will try to enable * instant communication mode on 5Ghz, but may fall back to 2.4Ghz due to regulatory * requirements. * @return the current {@link Builder} builder, enabling chaining of builder methods. */ @RequiresApi(Build.VERSION_CODES.TIRAMISU) @NonNull public Builder setInstantCommunicationModeEnabled( boolean enabled, @WifiAwareManager.InstantModeBand int band) { if (!SdkLevel.isAtLeastT()) { throw new UnsupportedOperationException(); } if (band != ScanResult.WIFI_BAND_24_GHZ && band != ScanResult.WIFI_BAND_5_GHZ) { throw new IllegalArgumentException(); } mBand = band; mEnableInstantMode = enabled; return this; } /** * Set the {@link AwarePairingConfig} for this subscribe session, the peer can use this info * to determine the config of the following bootstrapping, pairing setup/verification * request. * @see AwarePairingConfig * @param config The pairing config set to the peer. Only valid when * {@link Characteristics#isAwarePairingSupported()} is true. * @return the current {@link Builder} builder, enabling chaining of builder methods. */ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @NonNull public Builder setPairingConfig(@Nullable AwarePairingConfig config) { if (!SdkLevel.isAtLeastU()) { throw new UnsupportedOperationException(); } mPairingConfig = config; return this; } /** * Specify whether to configure the subscribe discovery session to be suspendable. This API * doesn't suspend the session, it allows it to be suspended and resumed in the future using * {@link DiscoverySession#suspend()} and {@link DiscoverySession#resume()} respectively. *

* Optional. Not suspendable by default. *

* The device must support Wi-Fi Aware suspension for a subscribe session to be * suspendable. Feature support check is determined by * {@link Characteristics#isSuspensionSupported()}. * * @param isSuspendable If true, then this subscribe session can be suspended. * * @return the current {@link Builder} builder, enabling chaining of builder methods. * * @see DiscoverySession#suspend() * @see DiscoverySession#resume() * @hide */ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @RequiresPermission(value = MANAGE_WIFI_NETWORK_SELECTION) @SystemApi @NonNull public Builder setSuspendable(boolean isSuspendable) { if (!SdkLevel.isAtLeastU()) { throw new UnsupportedOperationException(); } mIsSuspendable = isSuspendable; return this; } /** * Set additional vendor-provided configuration data. * * @param vendorData List of {@link OuiKeyedData} containing the vendor-provided * configuration data. Note that multiple elements with the same OUI are allowed. * @return Builder for chaining. * @hide */ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) @FlaggedApi(Flags.FLAG_ANDROID_V_WIFI_API) @NonNull @SystemApi public Builder setVendorData(@NonNull List vendorData) { if (!SdkLevel.isAtLeastV()) { throw new UnsupportedOperationException(); } if (vendorData == null) { throw new IllegalArgumentException("setVendorData received a null value"); } mVendorData = vendorData; return this; } /** * Build {@link SubscribeConfig} given the current requests made on the * builder. */ public SubscribeConfig build() { return new SubscribeConfig(mServiceName, mServiceSpecificInfo, mMatchFilter, mSubscribeType, mTtlSec, mEnableTerminateNotification, mMinDistanceMmSet, mMinDistanceMm, mMaxDistanceMmSet, mMaxDistanceMm, mEnableInstantMode, mBand, mPairingConfig, mIsSuspendable, mVendorData); } } }