feat(contacts): обмен номером телефона по NFC/BLE
This commit is contained in:
@@ -35,7 +35,7 @@ class BleContactExchange(private val context: Context) {
|
|||||||
const val MFG_ID = 0x4B4D
|
const val MFG_ID = 0x4B4D
|
||||||
}
|
}
|
||||||
|
|
||||||
var onReceived: ((Long) -> Unit)? = null
|
var onReceived: ((Long, Long) -> Unit)? = null
|
||||||
var onSent: ((Long) -> Unit)? = null
|
var onSent: ((Long) -> Unit)? = null
|
||||||
var onError: ((String) -> Unit)? = null
|
var onError: ((String) -> Unit)? = null
|
||||||
|
|
||||||
@@ -54,11 +54,12 @@ class BleContactExchange(private val context: Context) {
|
|||||||
|
|
||||||
@Volatile private var selfId: Long = 0L
|
@Volatile private var selfId: Long = 0L
|
||||||
@Volatile private var selfSession: String = ""
|
@Volatile private var selfSession: String = ""
|
||||||
|
@Volatile private var selfPhone: Long = 0L
|
||||||
@Volatile private var peerIdForWrite: Long = 0L
|
@Volatile private var peerIdForWrite: Long = 0L
|
||||||
@Volatile private var connecting = false
|
@Volatile private var connecting = false
|
||||||
@Volatile private var running = false
|
@Volatile private var running = false
|
||||||
|
|
||||||
fun start(selfId: Long, selfSession: String) {
|
fun start(selfId: Long, selfSession: String, selfPhone: Long) {
|
||||||
val adapter = this.adapter
|
val adapter = this.adapter
|
||||||
if (adapter == null || !adapter.isEnabled) {
|
if (adapter == null || !adapter.isEnabled) {
|
||||||
emitError("bluetooth_off")
|
emitError("bluetooth_off")
|
||||||
@@ -66,6 +67,7 @@ class BleContactExchange(private val context: Context) {
|
|||||||
}
|
}
|
||||||
this.selfId = selfId
|
this.selfId = selfId
|
||||||
this.selfSession = selfSession
|
this.selfSession = selfSession
|
||||||
|
this.selfPhone = selfPhone
|
||||||
running = true
|
running = true
|
||||||
startGattServer()
|
startGattServer()
|
||||||
}
|
}
|
||||||
@@ -266,8 +268,10 @@ class BleContactExchange(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (characteristic?.uuid != CHAR_UUID || value == null) return
|
if (characteristic?.uuid != CHAR_UUID || value == null) return
|
||||||
val peerId = String(value, Charsets.UTF_8).trim().toLongOrNull() ?: return
|
val parts = String(value, Charsets.UTF_8).trim().split(":")
|
||||||
if (peerId > 0L) emitReceived(peerId)
|
val peerId = parts.getOrNull(0)?.toLongOrNull() ?: return
|
||||||
|
val peerPhone = parts.getOrNull(1)?.toLongOrNull() ?: 0L
|
||||||
|
if (peerId > 0L) emitReceived(peerId, peerPhone)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,7 +305,7 @@ class BleContactExchange(private val context: Context) {
|
|||||||
}
|
}
|
||||||
characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
|
characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
characteristic.value = selfId.toString().toByteArray(Charsets.UTF_8)
|
characteristic.value = "$selfId:$selfPhone".toByteArray(Charsets.UTF_8)
|
||||||
try {
|
try {
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
gatt.writeCharacteristic(characteristic)
|
gatt.writeCharacteristic(characteristic)
|
||||||
@@ -324,8 +328,8 @@ class BleContactExchange(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun emitReceived(id: Long) {
|
private fun emitReceived(id: Long, phone: Long) {
|
||||||
main.post { onReceived?.invoke(id) }
|
main.post { onReceived?.invoke(id, phone) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun emitSent(id: Long) {
|
private fun emitSent(id: Long) {
|
||||||
|
|||||||
@@ -42,7 +42,9 @@ class MainActivity : FlutterActivity() {
|
|||||||
|
|
||||||
private var ble: BleContactExchange? = null
|
private var ble: BleContactExchange? = null
|
||||||
private var pendingSelfId = 0L
|
private var pendingSelfId = 0L
|
||||||
|
private var pendingSelfPhone = 0L
|
||||||
private var pendingSession = ""
|
private var pendingSession = ""
|
||||||
|
private var pendingPeer: NfcExchange.Peer? = null
|
||||||
@Volatile private var exchangingEmitted = false
|
@Volatile private var exchangingEmitted = false
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
@@ -88,15 +90,12 @@ class MainActivity : FlutterActivity() {
|
|||||||
when (call.method) {
|
when (call.method) {
|
||||||
"status" -> result.success(nfcStatus())
|
"status" -> result.success(nfcStatus())
|
||||||
"start" -> {
|
"start" -> {
|
||||||
val selfId = when (val v = call.argument<Any>("selfId")) {
|
val selfId = longArg(call.argument<Any>("selfId"))
|
||||||
is Int -> v.toLong()
|
val selfPhone = longArg(call.argument<Any>("selfPhone"))
|
||||||
is Long -> v
|
|
||||||
else -> 0L
|
|
||||||
}
|
|
||||||
if (selfId <= 0L) {
|
if (selfId <= 0L) {
|
||||||
result.error("INVALID_ID", "selfId must be positive", null)
|
result.error("INVALID_ID", "selfId must be positive", null)
|
||||||
} else {
|
} else {
|
||||||
startNfcExchange(selfId)
|
startNfcExchange(selfId, selfPhone)
|
||||||
result.success(null)
|
result.success(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,15 +205,18 @@ class MainActivity : FlutterActivity() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startNfcExchange(selfId: Long) {
|
private fun startNfcExchange(selfId: Long, selfPhone: Long) {
|
||||||
val session = "%08x".format(nfcJitter.nextInt())
|
val session = "%08x".format(nfcJitter.nextInt())
|
||||||
NfcExchange.selfId = selfId
|
NfcExchange.selfId = selfId
|
||||||
NfcExchange.selfSession = session
|
NfcExchange.selfSession = session
|
||||||
|
NfcExchange.selfPhone = selfPhone
|
||||||
NfcExchange.active = true
|
NfcExchange.active = true
|
||||||
NfcExchange.onServed = { onNfcServed() }
|
NfcExchange.onServed = { onNfcServed() }
|
||||||
seenPeers.clear()
|
seenPeers.clear()
|
||||||
exchangingEmitted = false
|
exchangingEmitted = false
|
||||||
|
pendingPeer = null
|
||||||
pendingSelfId = selfId
|
pendingSelfId = selfId
|
||||||
|
pendingSelfPhone = selfPhone
|
||||||
pendingSession = session
|
pendingSession = session
|
||||||
nfcCycling = true
|
nfcCycling = true
|
||||||
nfcHandler.removeCallbacksAndMessages(null)
|
nfcHandler.removeCallbacksAndMessages(null)
|
||||||
@@ -227,7 +229,9 @@ class MainActivity : FlutterActivity() {
|
|||||||
NfcExchange.active = false
|
NfcExchange.active = false
|
||||||
NfcExchange.selfId = 0L
|
NfcExchange.selfId = 0L
|
||||||
NfcExchange.selfSession = ""
|
NfcExchange.selfSession = ""
|
||||||
|
NfcExchange.selfPhone = 0L
|
||||||
NfcExchange.onServed = null
|
NfcExchange.onServed = null
|
||||||
|
pendingPeer = null
|
||||||
nfcHandler.removeCallbacksAndMessages(null)
|
nfcHandler.removeCallbacksAndMessages(null)
|
||||||
nfcReaderDisable()
|
nfcReaderDisable()
|
||||||
ble?.stop()
|
ble?.stop()
|
||||||
@@ -259,12 +263,12 @@ class MainActivity : FlutterActivity() {
|
|||||||
|
|
||||||
private fun startBle() {
|
private fun startBle() {
|
||||||
val exchange = ble ?: BleContactExchange(applicationContext).also {
|
val exchange = ble ?: BleContactExchange(applicationContext).also {
|
||||||
it.onReceived = { id -> onBleReceived(id) }
|
it.onReceived = { id, phone -> revealPeer(id, phone) }
|
||||||
it.onSent = { id -> onBleReceived(id) }
|
it.onSent = { _ -> pendingPeer?.let { p -> revealPeer(p.id, p.phone) } }
|
||||||
it.onError = { reason -> onBleError(reason) }
|
it.onError = { reason -> onBleError(reason) }
|
||||||
ble = it
|
ble = it
|
||||||
}
|
}
|
||||||
exchange.start(pendingSelfId, pendingSession)
|
exchange.start(pendingSelfId, pendingSession, pendingSelfPhone)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun emitExchanging() {
|
private fun emitExchanging() {
|
||||||
@@ -273,9 +277,15 @@ class MainActivity : FlutterActivity() {
|
|||||||
nfcEvents?.success(mapOf("event" to "exchanging"))
|
nfcEvents?.success(mapOf("event" to "exchanging"))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onBleReceived(id: Long) {
|
private fun revealPeer(id: Long, phone: Long) {
|
||||||
if (id == NfcExchange.selfId || !seenPeers.add(id)) return
|
if (id == NfcExchange.selfId || !seenPeers.add(id)) return
|
||||||
nfcEvents?.success(mapOf("event" to "received", "id" to id))
|
nfcEvents?.success(mapOf("event" to "received", "id" to id, "phone" to phone))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun longArg(value: Any?): Long = when (value) {
|
||||||
|
is Int -> value.toLong()
|
||||||
|
is Long -> value
|
||||||
|
else -> 0L
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onBleError(reason: String) {
|
private fun onBleError(reason: String) {
|
||||||
@@ -360,8 +370,9 @@ class MainActivity : FlutterActivity() {
|
|||||||
nfcCycling = false
|
nfcCycling = false
|
||||||
nfcReaderDisable()
|
nfcReaderDisable()
|
||||||
emitExchanging()
|
emitExchanging()
|
||||||
|
pendingPeer = peer
|
||||||
ble?.connectTo(peer.session, peer.id)
|
ble?.connectTo(peer.session, peer.id)
|
||||||
nfcHandler.postDelayed({ onBleReceived(peer.id) }, 3000L)
|
nfcHandler.postDelayed({ revealPeer(peer.id, peer.phone) }, 3000L)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,17 +11,19 @@ object NfcExchange {
|
|||||||
@Volatile var active: Boolean = false
|
@Volatile var active: Boolean = false
|
||||||
@Volatile var selfId: Long = 0L
|
@Volatile var selfId: Long = 0L
|
||||||
@Volatile var selfSession: String = ""
|
@Volatile var selfSession: String = ""
|
||||||
|
@Volatile var selfPhone: Long = 0L
|
||||||
|
|
||||||
@Volatile var onServed: (() -> Unit)? = null
|
@Volatile var onServed: (() -> Unit)? = null
|
||||||
|
|
||||||
data class Peer(val id: Long, val session: String)
|
data class Peer(val id: Long, val session: String, val phone: Long)
|
||||||
|
|
||||||
fun buildSelectResponse(): ByteArray {
|
fun buildSelectResponse(): ByteArray {
|
||||||
val id = selfId
|
val id = selfId
|
||||||
val session = selfSession
|
val session = selfSession
|
||||||
if (!active || id <= 0L || session.isEmpty()) return STATUS_NOT_FOUND
|
if (!active || id <= 0L || session.isEmpty()) return STATUS_NOT_FOUND
|
||||||
onServed?.invoke()
|
onServed?.invoke()
|
||||||
return (PREFIX + id + ":" + session).toByteArray(Charsets.UTF_8) + STATUS_OK
|
return (PREFIX + id + ":" + session + ":" + selfPhone)
|
||||||
|
.toByteArray(Charsets.UTF_8) + STATUS_OK
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildSelectCommand(): ByteArray {
|
fun buildSelectCommand(): ByteArray {
|
||||||
@@ -42,7 +44,8 @@ object NfcExchange {
|
|||||||
val id = parts[0].toLongOrNull() ?: return null
|
val id = parts[0].toLongOrNull() ?: return null
|
||||||
val session = parts[1]
|
val session = parts[1]
|
||||||
if (session.isEmpty()) return null
|
if (session.isEmpty()) return null
|
||||||
return Peer(id, session)
|
val phone = parts.getOrNull(2)?.toLongOrNull() ?: 0L
|
||||||
|
return Peer(id, session, phone)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hexToBytes(hex: String): ByteArray {
|
private fun hexToBytes(hex: String): ByteArray {
|
||||||
|
|||||||
@@ -60,8 +60,9 @@ class ContactsModule {
|
|||||||
static Future<CachedContact?> addContact(
|
static Future<CachedContact?> addContact(
|
||||||
Api api,
|
Api api,
|
||||||
int id,
|
int id,
|
||||||
String firstName,
|
String firstName, {
|
||||||
) async {
|
int phone = 0,
|
||||||
|
}) async {
|
||||||
final resp = await api.sendRequest(Opcode.contactUpdate, {
|
final resp = await api.sendRequest(Opcode.contactUpdate, {
|
||||||
'action': 'ADD',
|
'action': 'ADD',
|
||||||
'contactId': id,
|
'contactId': id,
|
||||||
@@ -92,6 +93,9 @@ class ContactsModule {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (row == null) return null;
|
if (row == null) return null;
|
||||||
|
if (phone > 0 && ((row['phone'] as int?) ?? 0) == 0) {
|
||||||
|
row['phone'] = phone;
|
||||||
|
}
|
||||||
await AppDatabase.saveContacts([row]);
|
await AppDatabase.saveContacts([row]);
|
||||||
if (contact != null) _primeContactCache(contact);
|
if (contact != null) _primeContactCache(contact);
|
||||||
revision.value++;
|
revision.value++;
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ enum NfcEventType { received, exchanging, cancelled, error }
|
|||||||
class NfcEvent {
|
class NfcEvent {
|
||||||
final NfcEventType type;
|
final NfcEventType type;
|
||||||
final int? id;
|
final int? id;
|
||||||
|
final int? phone;
|
||||||
final String? reason;
|
final String? reason;
|
||||||
|
|
||||||
const NfcEvent(this.type, this.id, {this.reason});
|
const NfcEvent(this.type, this.id, {this.phone, this.reason});
|
||||||
}
|
}
|
||||||
|
|
||||||
class NfcStatus {
|
class NfcStatus {
|
||||||
@@ -47,8 +48,8 @@ class NfcExchangeService {
|
|||||||
Stream<NfcEvent> get events =>
|
Stream<NfcEvent> get events =>
|
||||||
_events.receiveBroadcastStream().map(_decodeEvent);
|
_events.receiveBroadcastStream().map(_decodeEvent);
|
||||||
|
|
||||||
Future<void> start(int selfId) =>
|
Future<void> start(int selfId, int selfPhone) =>
|
||||||
_method.invokeMethod('start', {'selfId': selfId});
|
_method.invokeMethod('start', {'selfId': selfId, 'selfPhone': selfPhone});
|
||||||
|
|
||||||
Future<void> stop() async {
|
Future<void> stop() async {
|
||||||
if (!_supported) return;
|
if (!_supported) return;
|
||||||
@@ -61,9 +62,11 @@ class NfcExchangeService {
|
|||||||
final map = raw is Map ? raw : const {};
|
final map = raw is Map ? raw : const {};
|
||||||
final id = map['id'];
|
final id = map['id'];
|
||||||
final parsedId = id is int ? id : (id is num ? id.toInt() : null);
|
final parsedId = id is int ? id : (id is num ? id.toInt() : null);
|
||||||
|
final phone = map['phone'];
|
||||||
|
final parsedPhone = phone is int ? phone : (phone is num ? phone.toInt() : null);
|
||||||
switch (map['event']) {
|
switch (map['event']) {
|
||||||
case 'received':
|
case 'received':
|
||||||
return NfcEvent(NfcEventType.received, parsedId);
|
return NfcEvent(NfcEventType.received, parsedId, phone: parsedPhone);
|
||||||
case 'exchanging':
|
case 'exchanging':
|
||||||
return const NfcEvent(NfcEventType.exchanging, null);
|
return const NfcEvent(NfcEventType.exchanging, null);
|
||||||
case 'error':
|
case 'error':
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import '../../../backend/modules/contacts.dart';
|
|||||||
import '../../../core/cache/info_cache.dart';
|
import '../../../core/cache/info_cache.dart';
|
||||||
import '../../../core/nfc/nfc_exchange_service.dart';
|
import '../../../core/nfc/nfc_exchange_service.dart';
|
||||||
import '../../../core/storage/app_database.dart';
|
import '../../../core/storage/app_database.dart';
|
||||||
|
import '../../../core/utils/format.dart';
|
||||||
import '../../../main.dart';
|
import '../../../main.dart';
|
||||||
import '../../widgets/custom_notification.dart';
|
import '../../widgets/custom_notification.dart';
|
||||||
import '../../widgets/komet_avatar.dart';
|
import '../../widgets/komet_avatar.dart';
|
||||||
@@ -41,6 +42,7 @@ class _NfcExchangeSheetState extends State<NfcExchangeSheet>
|
|||||||
|
|
||||||
_Stage _stage = _Stage.checking;
|
_Stage _stage = _Stage.checking;
|
||||||
int? _peerId;
|
int? _peerId;
|
||||||
|
int? _peerPhone;
|
||||||
Map<String, dynamic>? _peerInfo;
|
Map<String, dynamic>? _peerInfo;
|
||||||
String _failReason = '';
|
String _failReason = '';
|
||||||
|
|
||||||
@@ -85,7 +87,7 @@ class _NfcExchangeSheetState extends State<NfcExchangeSheet>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_sub = _nfc.events.listen(_onEvent);
|
_sub = _nfc.events.listen(_onEvent);
|
||||||
await _nfc.start(profile.id);
|
await _nfc.start(profile.id, profile.phone);
|
||||||
if (mounted) setState(() => _stage = _Stage.scanning);
|
if (mounted) setState(() => _stage = _Stage.scanning);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +116,7 @@ class _NfcExchangeSheetState extends State<NfcExchangeSheet>
|
|||||||
final id = event.id;
|
final id = event.id;
|
||||||
if (id == null || _peerId != null) return;
|
if (id == null || _peerId != null) return;
|
||||||
_peerId = id;
|
_peerId = id;
|
||||||
|
_peerPhone = (event.phone != null && event.phone! > 0) ? event.phone : null;
|
||||||
HapticFeedback.mediumImpact();
|
HapticFeedback.mediumImpact();
|
||||||
_reveal.forward(from: 0);
|
_reveal.forward(from: 0);
|
||||||
setState(() => _stage = _Stage.found);
|
setState(() => _stage = _Stage.found);
|
||||||
@@ -163,7 +166,12 @@ class _NfcExchangeSheetState extends State<NfcExchangeSheet>
|
|||||||
if (id == null) return;
|
if (id == null) return;
|
||||||
setState(() => _stage = _Stage.adding);
|
setState(() => _stage = _Stage.adding);
|
||||||
try {
|
try {
|
||||||
await ContactsModule.addContact(api, id, _firstNameForAdd());
|
await ContactsModule.addContact(
|
||||||
|
api,
|
||||||
|
id,
|
||||||
|
_firstNameForAdd(),
|
||||||
|
phone: _peerPhone ?? 0,
|
||||||
|
);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _stage = _Stage.added);
|
setState(() => _stage = _Stage.added);
|
||||||
showCustomNotification(context, 'Контакт добавлен');
|
showCustomNotification(context, 'Контакт добавлен');
|
||||||
@@ -413,7 +421,7 @@ class _NfcExchangeSheetState extends State<NfcExchangeSheet>
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
'ID ${_peerId ?? ''}',
|
formatPhone(_peerPhone) ?? 'ID ${_peerId ?? ''}',
|
||||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
|
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|||||||
Reference in New Issue
Block a user