Add API to disable Bluetooth (#113)
* Add api to disable Bluetooth * Implement Android support * Fix apple build * Update Readme * Combine rows --------- Co-authored-by: Foti Dim <fdimanidis@gmail.com>
This commit is contained in:
@@ -36,7 +36,7 @@ A cross-platform (Android/iOS/macOS/Windows/Linux/Web) Bluetooth Low Energy (BLE
|
||||
| isPaired | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
| onPairingStateChange | ✔️ | ⏺ | ⏺ | ✔️ | ✔️ | ⏺ |
|
||||
| getBluetoothAvailabilityState | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
|
||||
| enableBluetooth | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ❌ |
|
||||
| enable/disable Bluetooth | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ❌ |
|
||||
| onAvailabilityChange | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
| requestMtu | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
|
||||
|
||||
@@ -229,6 +229,9 @@ UniversalBle.onAvailabilityChange = (state) {
|
||||
|
||||
// Enable Bluetooth programmatically
|
||||
UniversalBle.enableBluetooth();
|
||||
|
||||
// Disable Bluetooth programmatically
|
||||
UniversalBle.disableBluetooth();
|
||||
```
|
||||
|
||||
## Command Queue
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Autogenerated from Pigeon (v22.4.0), do not edit directly.
|
||||
// Autogenerated from Pigeon (v22.6.1), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
||||
|
||||
@@ -270,6 +270,7 @@ private open class UniversalBlePigeonCodec : StandardMessageCodec() {
|
||||
interface UniversalBlePlatformChannel {
|
||||
fun getBluetoothAvailabilityState(callback: (Result<Long>) -> Unit)
|
||||
fun enableBluetooth(callback: (Result<Boolean>) -> Unit)
|
||||
fun disableBluetooth(callback: (Result<Boolean>) -> Unit)
|
||||
fun startScan(filter: UniversalScanFilter?)
|
||||
fun stopScan()
|
||||
fun connect(deviceId: String)
|
||||
@@ -330,6 +331,24 @@ interface UniversalBlePlatformChannel {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.disableBluetooth$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { _, reply ->
|
||||
api.disableBluetooth{ result: Result<Boolean> ->
|
||||
val error = result.exceptionOrNull()
|
||||
if (error != null) {
|
||||
reply.reply(wrapError(error))
|
||||
} else {
|
||||
val data = result.getOrNull()
|
||||
reply.reply(wrapResult(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.startScan$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
|
||||
@@ -2,9 +2,16 @@ package com.navideck.universal_ble
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.bluetooth.*
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.bluetooth.BluetoothDevice.BOND_BONDED
|
||||
import android.bluetooth.le.BluetoothLeScanner
|
||||
import android.bluetooth.BluetoothGatt
|
||||
import android.bluetooth.BluetoothGattCallback
|
||||
import android.bluetooth.BluetoothGattCharacteristic
|
||||
import android.bluetooth.BluetoothGattDescriptor
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.bluetooth.BluetoothProfile
|
||||
import android.bluetooth.BluetoothStatusCodes
|
||||
import android.bluetooth.le.ScanCallback
|
||||
import android.bluetooth.le.ScanFilter
|
||||
import android.bluetooth.le.ScanResult
|
||||
@@ -21,7 +28,7 @@ import android.util.Log
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||
import io.flutter.plugin.common.*
|
||||
import io.flutter.plugin.common.PluginRegistry
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -33,6 +40,7 @@ private const val TAG = "UniversalBlePlugin"
|
||||
class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), FlutterPlugin,
|
||||
ActivityAware, PluginRegistry.ActivityResultListener {
|
||||
private val bluetoothEnableRequestCode = 2342313
|
||||
private val bluetoothDisableRequestCode = 2342414
|
||||
private var callbackChannel: UniversalBleCallbackChannel? = null
|
||||
private var mainThreadHandler: Handler? = null
|
||||
private lateinit var context: Context
|
||||
@@ -45,6 +53,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
|
||||
|
||||
// Flutter Futures
|
||||
private var bluetoothEnableRequestFuture: ((Result<Boolean>) -> Unit)? = null
|
||||
private var bluetoothDisableRequestFuture: ((Result<Boolean>) -> Unit)? = null
|
||||
private val discoverServicesFutureList = mutableListOf<DiscoverServicesFuture>()
|
||||
private val mtuResultFutureList = mutableListOf<MtuResultFuture>()
|
||||
private val readResultFutureList = mutableListOf<ReadResultFuture>()
|
||||
@@ -104,6 +113,24 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
|
||||
bluetoothEnableRequestFuture = callback
|
||||
}
|
||||
|
||||
override fun disableBluetooth(callback: (Result<Boolean>) -> Unit) {
|
||||
if (!bluetoothManager.adapter.isEnabled) {
|
||||
callback(Result.success(true))
|
||||
return
|
||||
}
|
||||
if (bluetoothDisableRequestFuture != null) {
|
||||
callback(
|
||||
Result.failure(
|
||||
FlutterError("Failed", "Bluetooth disable request in progress", null)
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
val disableBtIntent = Intent("android.bluetooth.adapter.action.REQUEST_DISABLE")
|
||||
activity?.startActivityForResult(disableBtIntent, bluetoothDisableRequestCode)
|
||||
bluetoothDisableRequestFuture = callback
|
||||
}
|
||||
|
||||
override fun startScan(filter: UniversalScanFilter?) {
|
||||
if (!isBluetoothAvailable()) throw FlutterError(
|
||||
"BluetoothNotEnabled",
|
||||
@@ -995,6 +1022,11 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
|
||||
future(Result.success(resultCode == Activity.RESULT_OK))
|
||||
bluetoothEnableRequestFuture = null
|
||||
return true
|
||||
} else if (requestCode == bluetoothDisableRequestCode) {
|
||||
val future = bluetoothDisableRequestFuture ?: return false
|
||||
future(Result.success(resultCode == Activity.RESULT_OK))
|
||||
bluetoothDisableRequestFuture = null
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Autogenerated from Pigeon (v22.4.0), do not edit directly.
|
||||
// Autogenerated from Pigeon (v22.6.1), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
|
||||
import Foundation
|
||||
@@ -312,6 +312,7 @@ class UniversalBlePigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable
|
||||
protocol UniversalBlePlatformChannel {
|
||||
func getBluetoothAvailabilityState(completion: @escaping (Result<Int64, Error>) -> Void)
|
||||
func enableBluetooth(completion: @escaping (Result<Bool, Error>) -> Void)
|
||||
func disableBluetooth(completion: @escaping (Result<Bool, Error>) -> Void)
|
||||
func startScan(filter: UniversalScanFilter?) throws
|
||||
func stopScan() throws
|
||||
func connect(deviceId: String) throws
|
||||
@@ -364,6 +365,21 @@ class UniversalBlePlatformChannelSetup {
|
||||
} else {
|
||||
enableBluetoothChannel.setMessageHandler(nil)
|
||||
}
|
||||
let disableBluetoothChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.disableBluetooth\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
if let api = api {
|
||||
disableBluetoothChannel.setMessageHandler { _, reply in
|
||||
api.disableBluetooth { result in
|
||||
switch result {
|
||||
case .success(let res):
|
||||
reply(wrapResult(res))
|
||||
case .failure(let error):
|
||||
reply(wrapError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
disableBluetoothChannel.setMessageHandler(nil)
|
||||
}
|
||||
let startScanChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.startScan\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
if let api = api {
|
||||
startScanChannel.setMessageHandler { message, reply in
|
||||
|
||||
@@ -52,7 +52,11 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral
|
||||
func enableBluetooth(completion: @escaping (Result<Bool, Error>) -> Void) {
|
||||
completion(Result.failure(PigeonError(code: "NotSupported", message: nil, details: nil)))
|
||||
}
|
||||
|
||||
|
||||
func disableBluetooth(completion: @escaping (Result<Bool, any Error>) -> Void) {
|
||||
completion(Result.failure(PigeonError(code: "NotSupported", message: nil, details: nil)))
|
||||
}
|
||||
|
||||
func startScan(filter: UniversalScanFilter?) throws {
|
||||
// If filter has any other filter other than official one
|
||||
let usesCustomFilters = filter?.usesCustomFilters ?? false
|
||||
|
||||
@@ -116,4 +116,9 @@ class MockUniversalBle extends UniversalBlePlatform {
|
||||
Future<BleConnectionState> getConnectionState(String deviceId) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> disableBluetooth() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,15 +147,24 @@ class _MyAppState extends State<MyApp> {
|
||||
});
|
||||
},
|
||||
),
|
||||
if (BleCapabilities.supportsBluetoothEnableApi &&
|
||||
bleAvailabilityState == AvailabilityState.poweredOff)
|
||||
PlatformButton(
|
||||
text: 'Enable Bluetooth',
|
||||
onPressed: () async {
|
||||
bool isEnabled = await UniversalBle.enableBluetooth();
|
||||
showSnackbar("BluetoothEnabled: $isEnabled");
|
||||
},
|
||||
),
|
||||
if (BleCapabilities.supportsBluetoothEnableApi)
|
||||
bleAvailabilityState != AvailabilityState.poweredOn
|
||||
? PlatformButton(
|
||||
text: 'Enable Bluetooth',
|
||||
onPressed: () async {
|
||||
bool isEnabled =
|
||||
await UniversalBle.enableBluetooth();
|
||||
showSnackbar("BluetoothEnabled: $isEnabled");
|
||||
},
|
||||
)
|
||||
: PlatformButton(
|
||||
text: 'Disable Bluetooth',
|
||||
onPressed: () async {
|
||||
bool isDisabled =
|
||||
await UniversalBle.disableBluetooth();
|
||||
showSnackbar("BluetoothDisabled: $isDisabled");
|
||||
},
|
||||
),
|
||||
if (BleCapabilities.requiresRuntimePermission)
|
||||
PlatformButton(
|
||||
text: 'Check Permissions',
|
||||
|
||||
@@ -321,6 +321,15 @@ class UniversalBle {
|
||||
);
|
||||
}
|
||||
|
||||
/// Disable Bluetooth.
|
||||
/// It might throw errors if Bluetooth is not available.
|
||||
/// Not supported on `Web` and `Apple`.
|
||||
static Future<bool> disableBluetooth() async {
|
||||
return await _bleCommandQueue.queueCommand(
|
||||
() => _platform.disableBluetooth(),
|
||||
);
|
||||
}
|
||||
|
||||
/// [receivesAdvertisements] returns true on web if the browser supports receiving advertisements from a certain `deviceId`.
|
||||
/// The rest of the platforms will always return true.
|
||||
/// If true, then you will be getting scanResult updates for this device.
|
||||
|
||||
@@ -53,6 +53,23 @@ class UniversalBleLinux extends UniversalBlePlatform {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> disableBluetooth() async {
|
||||
await _ensureInitialized();
|
||||
var adapter = _activeAdapter;
|
||||
if (adapter == null) {
|
||||
throw "Adapter not available";
|
||||
}
|
||||
if (!adapter.powered) return true;
|
||||
try {
|
||||
await adapter.setPowered(false);
|
||||
return !adapter.powered;
|
||||
} catch (e) {
|
||||
UniversalLogger.logError('Error disabling bluetooth: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> startScan({
|
||||
ScanFilter? scanFilter,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Autogenerated from Pigeon (v22.4.0), do not edit directly.
|
||||
// Autogenerated from Pigeon (v22.6.1), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
|
||||
|
||||
@@ -332,6 +332,33 @@ class UniversalBlePlatformChannel {
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> disableBluetooth() async {
|
||||
final String pigeonVar_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.disableBluetooth$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final List<Object?>? pigeonVar_replyList =
|
||||
await pigeonVar_channel.send(null) 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 if (pigeonVar_replyList[0] == null) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
} else {
|
||||
return (pigeonVar_replyList[0] as bool?)!;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> startScan(UniversalScanFilter? filter) async {
|
||||
final String pigeonVar_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.startScan$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
|
||||
@@ -28,6 +28,14 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform {
|
||||
return _channel.enableBluetooth();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> disableBluetooth() {
|
||||
if (!BleCapabilities.supportsBluetoothEnableApi) {
|
||||
throw UnsupportedError("Not supported");
|
||||
}
|
||||
return _channel.disableBluetooth();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> startScan({
|
||||
ScanFilter? scanFilter,
|
||||
|
||||
@@ -12,6 +12,8 @@ abstract class UniversalBlePlatform {
|
||||
|
||||
Future<bool> enableBluetooth();
|
||||
|
||||
Future<bool> disableBluetooth();
|
||||
|
||||
Future<void> startScan({
|
||||
ScanFilter? scanFilter,
|
||||
PlatformConfig? platformConfig,
|
||||
|
||||
@@ -335,6 +335,11 @@ class UniversalBleWeb extends UniversalBlePlatform {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> disableBluetooth() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
RequestOptionsBuilder _getRequestOptionBuilder(
|
||||
ScanFilter? scanFilter,
|
||||
WebOptions? webOptions,
|
||||
|
||||
@@ -27,6 +27,9 @@ abstract class UniversalBlePlatformChannel {
|
||||
@async
|
||||
bool enableBluetooth();
|
||||
|
||||
@async
|
||||
bool disableBluetooth();
|
||||
|
||||
void startScan(UniversalScanFilter? filter);
|
||||
|
||||
void stopScan();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Autogenerated from Pigeon (v22.4.0), do not edit directly.
|
||||
// Autogenerated from Pigeon (v22.6.1), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
|
||||
#undef _HAS_EXCEPTIONS
|
||||
@@ -546,6 +546,28 @@ void UniversalBlePlatformChannel::SetUp(
|
||||
channel.SetMessageHandler(nullptr);
|
||||
}
|
||||
}
|
||||
{
|
||||
BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.disableBluetooth" + prepended_suffix, &GetCodec());
|
||||
if (api != nullptr) {
|
||||
channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply<EncodableValue>& reply) {
|
||||
try {
|
||||
api->DisableBluetooth([reply](ErrorOr<bool>&& output) {
|
||||
if (output.has_error()) {
|
||||
reply(WrapError(output.error()));
|
||||
return;
|
||||
}
|
||||
EncodableList wrapped;
|
||||
wrapped.push_back(EncodableValue(std::move(output).TakeValue()));
|
||||
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.startScan" + prepended_suffix, &GetCodec());
|
||||
if (api != nullptr) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Autogenerated from Pigeon (v22.4.0), do not edit directly.
|
||||
// Autogenerated from Pigeon (v22.6.1), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
|
||||
#ifndef PIGEON_UNIVERSAL_BLE_G_H_
|
||||
@@ -298,6 +298,7 @@ class UniversalBlePlatformChannel {
|
||||
virtual ~UniversalBlePlatformChannel() {}
|
||||
virtual void GetBluetoothAvailabilityState(std::function<void(ErrorOr<int64_t> reply)> result) = 0;
|
||||
virtual void EnableBluetooth(std::function<void(ErrorOr<bool> reply)> result) = 0;
|
||||
virtual void DisableBluetooth(std::function<void(ErrorOr<bool> reply)> result) = 0;
|
||||
virtual std::optional<FlutterError> StartScan(const UniversalScanFilter* filter) = 0;
|
||||
virtual std::optional<FlutterError> StopScan() = 0;
|
||||
virtual std::optional<FlutterError> Connect(const std::string& device_id) = 0;
|
||||
|
||||
@@ -79,6 +79,7 @@ namespace universal_ble
|
||||
if (!bluetoothRadio)
|
||||
{
|
||||
result(FlutterError("Bluetooth is not available"));
|
||||
result(false);
|
||||
return;
|
||||
}
|
||||
if (bluetoothRadio.State() == RadioState::On)
|
||||
@@ -100,6 +101,33 @@ namespace universal_ble
|
||||
} });
|
||||
}
|
||||
|
||||
void UniversalBlePlugin::DisableBluetooth(std::function<void(ErrorOr<bool> reply)> result)
|
||||
{
|
||||
if (!bluetoothRadio)
|
||||
{
|
||||
result(FlutterError("Bluetooth is not available"));
|
||||
result(false);
|
||||
return;
|
||||
}
|
||||
if (bluetoothRadio.State() == RadioState::Off)
|
||||
{
|
||||
result(true);
|
||||
return;
|
||||
}
|
||||
auto async_c = bluetoothRadio.SetStateAsync(RadioState::Off);
|
||||
async_c.Completed([&, result](IAsyncOperation<RadioAccessStatus> const &sender, AsyncStatus const args)
|
||||
{
|
||||
auto radioAccessStatus = sender.GetResults();
|
||||
if (radioAccessStatus == RadioAccessStatus::Allowed)
|
||||
{
|
||||
result(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
result(FlutterError("Failed to disable bluetooth"));
|
||||
} });
|
||||
}
|
||||
|
||||
std::optional<FlutterError> UniversalBlePlugin::StartScan(const UniversalScanFilter *filter)
|
||||
{
|
||||
if (bluetoothRadio && bluetoothRadio.State() == RadioState::On)
|
||||
|
||||
@@ -131,6 +131,7 @@ namespace universal_ble
|
||||
// UniversalBlePlatformChannel implementation.
|
||||
void GetBluetoothAvailabilityState(std::function<void(ErrorOr<int64_t> reply)> result) override;
|
||||
void EnableBluetooth(std::function<void(ErrorOr<bool> reply)> result) override;
|
||||
void DisableBluetooth(std::function<void(ErrorOr<bool> reply)> result) override;
|
||||
ErrorOr<int64_t> GetConnectionState(const std::string &device_id) override;
|
||||
std::optional<FlutterError> StartScan(const UniversalScanFilter *filter) override;
|
||||
std::optional<FlutterError> StopScan() override;
|
||||
|
||||
Reference in New Issue
Block a user