Add requestConnectionPriority support (#221)
* feat: add requestConnectionPriority - Add BleConnectionPriority enum (balanced, highPerformance, lowPower) - Add BleCapabilities.supportsConnectionPriorityApi flag - Implement on Android via BluetoothGatt.requestConnectionPriority() - Throw notSupported on iOS, macOS, Windows, Linux, and Web - Update pigeon definition and regenerate platform channel files - Bump version 1.2.0 -> 1.3.0 * fix: improve requestConnectionPriority implementation - Remove redundant BleCapabilities guard in Dart layer (native handles it) - Add BleConnectionPriority Kotlin enum mirroring Dart enum - Map to explicit Android SDK constants via when expression - Add firstOrNull with ILLEGAL_ARGUMENT for unknown priority values - Add catch (e: Exception) block for broader error handling - Fix enum doc comment (throws belongs on method, not enum) - Remove stale capability check guidance from docs and README --------- Co-authored-by: aqeel-bmec-co <aqeel@bmec.co> Co-authored-by: aqeel-bmec-co <85945830+aqeel-bmec-co@users.noreply.github.com>
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
## 1.3.0
|
||||
* Add `requestConnectionPriority` to allow tuning BLE connection intervals on Android
|
||||
|
||||
## 1.2.0
|
||||
* Add `autoConnect` parameter to `connect()` method for automatic reconnection support on Android and iOS/macOS
|
||||
* Add `serviceData` in `BleDevice`
|
||||
|
||||
@@ -54,6 +54,7 @@ A cross-platform (Android/iOS/macOS/Windows/Linux/Web) Bluetooth Low Energy (BLE
|
||||
| enable/disable Bluetooth | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ❌ |
|
||||
| onAvailabilityChange | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
| requestMtu | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
|
||||
| requestConnectionPriority | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| readRssi | ✔️ | ✔️ | ✔️ | ❌ | 🚧 | ❌ |
|
||||
| requestPermissions | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
|
||||
@@ -470,6 +471,21 @@ When developing cross-platform BLE applications and devices:
|
||||
* Take advantage of higher MTUs when available, without depending on them
|
||||
|
||||
|
||||
### Requesting Connection Priority
|
||||
|
||||
On Android, you can request a connection parameter update to tune the BLE connection interval. This can yield a 3–7× throughput improvement for data-intensive transfers.
|
||||
|
||||
```dart
|
||||
// Before starting high-throughput data transfer:
|
||||
await UniversalBle.requestConnectionPriority(
|
||||
deviceId,
|
||||
BleConnectionPriority.highPerformance,
|
||||
);
|
||||
```
|
||||
|
||||
> **Note:** Only supported on Android. On all other platforms this throws `UniversalBleException` with code `notSupported`.
|
||||
> Call this after connecting and after `requestMtu()`, before beginning data transfer.
|
||||
|
||||
### Reading RSSI
|
||||
|
||||
Read the signal strength (RSSI) of a connected device.
|
||||
|
||||
@@ -645,6 +645,7 @@ interface UniversalBlePlatformChannel {
|
||||
fun getSystemDevices(withServices: List<String>, callback: (Result<List<UniversalBleScanResult>>) -> Unit)
|
||||
fun getConnectionState(deviceId: String): Long
|
||||
fun readRssi(deviceId: String, callback: (Result<Long>) -> Unit)
|
||||
fun requestConnectionPriority(deviceId: String, priority: Long, callback: (Result<Unit>) -> Unit)
|
||||
fun setLogLevel(logLevel: UniversalBleLogLevel)
|
||||
|
||||
companion object {
|
||||
@@ -1057,6 +1058,26 @@ interface UniversalBlePlatformChannel {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.requestConnectionPriority$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val deviceIdArg = args[0] as String
|
||||
val priorityArg = args[1] as Long
|
||||
api.requestConnectionPriority(deviceIdArg, priorityArg) { result: Result<Unit> ->
|
||||
val error = result.exceptionOrNull()
|
||||
if (error != null) {
|
||||
reply.reply(UniversalBlePigeonUtils.wrapError(error))
|
||||
} else {
|
||||
reply.reply(UniversalBlePigeonUtils.wrapResult(null))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.setLogLevel$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
|
||||
@@ -53,6 +53,11 @@ enum class BleOutputProperty(val value: Long) {
|
||||
WithoutResponse(1);
|
||||
}
|
||||
|
||||
enum class BleConnectionPriority(val value: Long) {
|
||||
Balanced(0),
|
||||
HighPerformance(1),
|
||||
LowPower(2);
|
||||
}
|
||||
|
||||
enum class CharacteristicProperty(val value: Long) {
|
||||
Broadcast(0),
|
||||
|
||||
@@ -752,6 +752,55 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
|
||||
}
|
||||
}
|
||||
|
||||
override fun requestConnectionPriority(
|
||||
deviceId: String,
|
||||
priority: Long,
|
||||
callback: (Result<Unit>) -> Unit,
|
||||
) {
|
||||
try {
|
||||
val gatt = deviceId.toBluetoothGatt()
|
||||
val priorityEnum = BleConnectionPriority.entries.firstOrNull { it.value == priority }
|
||||
?: return callback(
|
||||
Result.failure(
|
||||
createFlutterError(
|
||||
UniversalBleErrorCode.ILLEGAL_ARGUMENT,
|
||||
"Unknown priority: $priority"
|
||||
)
|
||||
)
|
||||
)
|
||||
val androidPriority = when (priorityEnum) {
|
||||
BleConnectionPriority.Balanced -> BluetoothGatt.CONNECTION_PRIORITY_BALANCED
|
||||
BleConnectionPriority.HighPerformance -> BluetoothGatt.CONNECTION_PRIORITY_HIGH
|
||||
BleConnectionPriority.LowPower -> BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER
|
||||
}
|
||||
val success = gatt.requestConnectionPriority(androidPriority)
|
||||
if (success) {
|
||||
callback(Result.success(Unit))
|
||||
} else {
|
||||
callback(
|
||||
Result.failure(
|
||||
createFlutterError(
|
||||
UniversalBleErrorCode.FAILED,
|
||||
"requestConnectionPriority returned false",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
} catch (e: FlutterError) {
|
||||
callback(Result.failure(e))
|
||||
} catch (e: Exception) {
|
||||
callback(
|
||||
Result.failure(
|
||||
createFlutterError(
|
||||
UniversalBleErrorCode.FAILED,
|
||||
"requestConnectionPriority failed",
|
||||
e.toString()
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) {
|
||||
val deviceId = gatt?.device?.address ?: return
|
||||
mtuResultFutureList.removeAll {
|
||||
|
||||
@@ -645,6 +645,7 @@ protocol UniversalBlePlatformChannel {
|
||||
func getSystemDevices(withServices: [String], completion: @escaping (Result<[UniversalBleScanResult], Error>) -> Void)
|
||||
func getConnectionState(deviceId: String) throws -> Int64
|
||||
func readRssi(deviceId: String, completion: @escaping (Result<Int64, Error>) -> Void)
|
||||
func requestConnectionPriority(deviceId: String, priority: Int64, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
func setLogLevel(logLevel: UniversalBleLogLevel) throws
|
||||
}
|
||||
|
||||
@@ -998,6 +999,24 @@ class UniversalBlePlatformChannelSetup {
|
||||
} else {
|
||||
readRssiChannel.setMessageHandler(nil)
|
||||
}
|
||||
let requestConnectionPriorityChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.requestConnectionPriority\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
if let api = api {
|
||||
requestConnectionPriorityChannel.setMessageHandler { message, reply in
|
||||
let args = message as! [Any?]
|
||||
let deviceIdArg = args[0] as! String
|
||||
let priorityArg = args[1] as! Int64
|
||||
api.requestConnectionPriority(deviceId: deviceIdArg, priority: priorityArg) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
reply(wrapResult(nil))
|
||||
case .failure(let error):
|
||||
reply(wrapError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
requestConnectionPriorityChannel.setMessageHandler(nil)
|
||||
}
|
||||
let setLogLevelChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.setLogLevel\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
if let api = api {
|
||||
setLogLevelChannel.setMessageHandler { message, reply in
|
||||
|
||||
@@ -371,6 +371,14 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral
|
||||
completion(Result.success(mtuResult))
|
||||
}
|
||||
|
||||
func requestConnectionPriority(
|
||||
deviceId _: String,
|
||||
priority _: Int64,
|
||||
completion: @escaping (Result<Void, Error>) -> Void
|
||||
) {
|
||||
completion(.failure(createFlutterError(code: .notSupported, message: "requestConnectionPriority is not supported on Apple platforms")))
|
||||
}
|
||||
|
||||
func readRssi(deviceId: String, completion: @escaping (Result<Int64, Error>) -> Void) {
|
||||
UniversalBleLogger.shared.logDebug("READ_RSSI -> \(deviceId)")
|
||||
guard let peripheral = deviceId.findPeripheral(manager: manager) else {
|
||||
|
||||
@@ -103,6 +103,12 @@ class MockUniversalBle extends UniversalBlePlatform {
|
||||
return 512;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> requestConnectionPriority(
|
||||
String deviceId,
|
||||
BleConnectionPriority priority,
|
||||
) async {}
|
||||
|
||||
@override
|
||||
Future<void> setNotifiable(String deviceId, String service,
|
||||
String characteristic, BleInputProperty bleInputProperty) async {}
|
||||
|
||||
@@ -40,6 +40,9 @@ class BleCapabilities {
|
||||
static bool supportsConnectedDevicesApi = !_Platform.isWeb;
|
||||
|
||||
static bool supportsRequestMtuApi = !_Platform.isWeb;
|
||||
|
||||
static bool supportsConnectionPriorityApi =
|
||||
!_Platform.isWeb && defaultTargetPlatform == TargetPlatform.android;
|
||||
}
|
||||
|
||||
class _Platform {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/// Connection priority hint passed to [UniversalBle.requestConnectionPriority].
|
||||
///
|
||||
/// Maps to Android `BluetoothGatt.CONNECTION_PRIORITY_*` constants.
|
||||
enum BleConnectionPriority {
|
||||
/// Default OS-managed interval (~30-50 ms). Android constant: 0.
|
||||
balanced,
|
||||
|
||||
/// Low-latency interval (~7.5-15 ms), higher power draw. Android constant: 1.
|
||||
highPerformance,
|
||||
|
||||
/// Power-optimised interval (~100-125 ms). Android constant: 2.
|
||||
lowPower,
|
||||
}
|
||||
@@ -12,3 +12,4 @@ export 'package:universal_ble/src/models/ble_connection_state.dart';
|
||||
export 'package:universal_ble/src/models/ble_device.dart';
|
||||
export 'package:universal_ble/src/models/ble_command.dart';
|
||||
export 'package:universal_ble/src/models/ble_capabilities.dart';
|
||||
export 'package:universal_ble/src/models/ble_connection_priority.dart';
|
||||
|
||||
@@ -351,6 +351,30 @@ class UniversalBle {
|
||||
);
|
||||
}
|
||||
|
||||
/// Requests a connection parameter update for [deviceId].
|
||||
///
|
||||
/// [priority] controls the BLE connection interval:
|
||||
/// - [BleConnectionPriority.balanced] - default OS behaviour (~30-50 ms interval).
|
||||
/// - [BleConnectionPriority.highPerformance] - low latency, higher power (~7.5-15 ms interval).
|
||||
/// - [BleConnectionPriority.lowPower] - power-optimised (~100-125 ms interval).
|
||||
///
|
||||
/// Only supported on Android. On all other platforms this throws
|
||||
/// [UniversalBleException] with code [UniversalBleErrorCode.notSupported].
|
||||
///
|
||||
/// Should be called after a successful connection and MTU negotiation, before
|
||||
/// beginning high-throughput data transfer.
|
||||
static Future<void> requestConnectionPriority(
|
||||
String deviceId,
|
||||
BleConnectionPriority priority, {
|
||||
Duration? timeout,
|
||||
}) async {
|
||||
return await _bleCommandQueue.queueCommand(
|
||||
() => _platform.requestConnectionPriority(deviceId, priority),
|
||||
timeout: timeout,
|
||||
deviceId: deviceId,
|
||||
);
|
||||
}
|
||||
|
||||
/// Read the RSSI value of a connected device.
|
||||
///
|
||||
/// Returns the current RSSI value in dBm. This value indicates the signal strength
|
||||
|
||||
@@ -397,6 +397,17 @@ class UniversalBleLinux extends UniversalBlePlatform {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> requestConnectionPriority(
|
||||
String deviceId,
|
||||
BleConnectionPriority priority,
|
||||
) {
|
||||
throw UniversalBleException(
|
||||
code: UniversalBleErrorCode.notSupported,
|
||||
message: "requestConnectionPriority is not supported on Linux platform",
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> readRssi(String deviceId) async {
|
||||
throw UniversalBleException(
|
||||
|
||||
@@ -1256,6 +1256,30 @@ class UniversalBlePlatformChannel {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> requestConnectionPriority(String deviceId, int priority) async {
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.requestConnectionPriority$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger);
|
||||
final Future<Object?> pigeonVar_sendFuture =
|
||||
pigeonVar_channel.send(<Object?>[deviceId, priority]);
|
||||
final pigeonVar_replyList =
|
||||
await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setLogLevel(UniversalBleLogLevel logLevel) async {
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.setLogLevel$pigeonVar_messageChannelSuffix';
|
||||
|
||||
@@ -158,6 +158,14 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform {
|
||||
Future<int> readRssi(String deviceId) =>
|
||||
_executeWithErrorHandling(() => _channel.readRssi(deviceId));
|
||||
|
||||
@override
|
||||
Future<void> requestConnectionPriority(
|
||||
String deviceId,
|
||||
BleConnectionPriority priority,
|
||||
) => _executeWithErrorHandling(
|
||||
() => _channel.requestConnectionPriority(deviceId, priority.index),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<bool> isPaired(String deviceId) =>
|
||||
_executeWithErrorHandling(() => _channel.isPaired(deviceId));
|
||||
|
||||
@@ -97,6 +97,11 @@ abstract class UniversalBlePlatform {
|
||||
|
||||
Future<int> readRssi(String deviceId);
|
||||
|
||||
Future<void> requestConnectionPriority(
|
||||
String deviceId,
|
||||
BleConnectionPriority priority,
|
||||
);
|
||||
|
||||
Future<bool> isPaired(String deviceId);
|
||||
|
||||
Future<bool> pair(String deviceId);
|
||||
|
||||
@@ -292,6 +292,18 @@ class UniversalBleWeb extends UniversalBlePlatform {
|
||||
);
|
||||
}
|
||||
|
||||
/// `Unimplemented`
|
||||
@override
|
||||
Future<void> requestConnectionPriority(
|
||||
String deviceId,
|
||||
BleConnectionPriority priority,
|
||||
) {
|
||||
throw UniversalBleException(
|
||||
code: UniversalBleErrorCode.notSupported,
|
||||
message: "requestConnectionPriority is not supported on Web platform",
|
||||
);
|
||||
}
|
||||
|
||||
/// `Unimplemented`
|
||||
@override
|
||||
Future<int> readRssi(String deviceId) {
|
||||
|
||||
@@ -96,6 +96,9 @@ abstract class UniversalBlePlatformChannel {
|
||||
@async
|
||||
int readRssi(String deviceId);
|
||||
|
||||
@async
|
||||
void requestConnectionPriority(String deviceId, int priority);
|
||||
|
||||
void setLogLevel(UniversalBleLogLevel logLevel);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
name: universal_ble
|
||||
description: A cross-platform (Android/iOS/macOS/Windows/Linux/Web) Bluetooth Low Energy (BLE) plugin for Flutter
|
||||
version: 1.2.0
|
||||
version: 1.3.0
|
||||
homepage: https://navideck.com
|
||||
repository: https://github.com/Navideck/universal_ble
|
||||
issue_tracker: https://github.com/Navideck/universal_ble/issues
|
||||
|
||||
@@ -66,6 +66,14 @@ abstract class UniversalBlePlatformMock extends UniversalBlePlatform {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> requestConnectionPriority(
|
||||
String deviceId,
|
||||
BleConnectionPriority priority,
|
||||
) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setNotifiable(String deviceId, String service,
|
||||
String characteristic, BleInputProperty bleInputProperty) {
|
||||
|
||||
@@ -1389,6 +1389,41 @@ void UniversalBlePlatformChannel::SetUp(
|
||||
channel.SetMessageHandler(nullptr);
|
||||
}
|
||||
}
|
||||
{
|
||||
BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.requestConnectionPriority" + prepended_suffix, &GetCodec());
|
||||
if (api != nullptr) {
|
||||
channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply<EncodableValue>& reply) {
|
||||
try {
|
||||
const auto& args = std::get<EncodableList>(message);
|
||||
const auto& encodable_device_id_arg = args.at(0);
|
||||
if (encodable_device_id_arg.IsNull()) {
|
||||
reply(WrapError("device_id_arg unexpectedly null."));
|
||||
return;
|
||||
}
|
||||
const auto& device_id_arg = std::get<std::string>(encodable_device_id_arg);
|
||||
const auto& encodable_priority_arg = args.at(1);
|
||||
if (encodable_priority_arg.IsNull()) {
|
||||
reply(WrapError("priority_arg unexpectedly null."));
|
||||
return;
|
||||
}
|
||||
const int64_t priority_arg = encodable_priority_arg.LongValue();
|
||||
api->RequestConnectionPriority(device_id_arg, priority_arg, [reply](std::optional<FlutterError>&& output) {
|
||||
if (output.has_value()) {
|
||||
reply(WrapError(output.value()));
|
||||
return;
|
||||
}
|
||||
EncodableList wrapped;
|
||||
wrapped.push_back(EncodableValue());
|
||||
reply(EncodableValue(std::move(wrapped)));
|
||||
});
|
||||
} catch (const std::exception& exception) {
|
||||
reply(WrapError(exception.what()));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
channel.SetMessageHandler(nullptr);
|
||||
}
|
||||
}
|
||||
{
|
||||
BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.setLogLevel" + prepended_suffix, &GetCodec());
|
||||
if (api != nullptr) {
|
||||
|
||||
@@ -528,6 +528,10 @@ class UniversalBlePlatformChannel {
|
||||
virtual void ReadRssi(
|
||||
const std::string& device_id,
|
||||
std::function<void(ErrorOr<int64_t> reply)> result) = 0;
|
||||
virtual void RequestConnectionPriority(
|
||||
const std::string& device_id,
|
||||
int64_t priority,
|
||||
std::function<void(std::optional<FlutterError> reply)> result) = 0;
|
||||
virtual std::optional<FlutterError> SetLogLevel(const UniversalBleLogLevel& log_level) = 0;
|
||||
|
||||
// The codec used by UniversalBlePlatformChannel.
|
||||
|
||||
@@ -436,6 +436,14 @@ void UniversalBlePlugin::RequestMtu(
|
||||
}
|
||||
}
|
||||
|
||||
void UniversalBlePlugin::RequestConnectionPriority(
|
||||
const std::string &device_id, int64_t priority,
|
||||
std::function<void(std::optional<FlutterError> reply)> result) {
|
||||
result(create_flutter_error(
|
||||
UniversalBleErrorCode::kNotSupported,
|
||||
"requestConnectionPriority is not supported on Windows platform"));
|
||||
}
|
||||
|
||||
void UniversalBlePlugin::ReadRssi(
|
||||
const std::string &device_id,
|
||||
std::function<void(ErrorOr<int64_t> reply)> result) {
|
||||
|
||||
@@ -207,6 +207,9 @@ private:
|
||||
std::function<void(std::optional<FlutterError> reply)> result) override;
|
||||
void RequestMtu(const std::string &device_id, int64_t expected_mtu,
|
||||
std::function<void(ErrorOr<int64_t> reply)> result) override;
|
||||
void RequestConnectionPriority(
|
||||
const std::string &device_id, int64_t priority,
|
||||
std::function<void(std::optional<FlutterError> reply)> result) override;
|
||||
void ReadRssi(const std::string &device_id,
|
||||
std::function<void(ErrorOr<int64_t> reply)> result) override;
|
||||
void IsPaired(const std::string &device_id,
|
||||
|
||||
Reference in New Issue
Block a user