From ad04225a624af12e58c2fd332abe3e4e560edd35 Mon Sep 17 00:00:00 2001 From: klockky Date: Sun, 21 Jun 2026 12:26:01 +0000 Subject: [PATCH] =?UTF-8?q?feat(contacts):=20=D0=BE=D0=B1=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=BD=D0=BE=D0=BC=D0=B5=D1=80=D0=BE=D0=BC=20=D1=82?= =?UTF-8?q?=D0=B5=D0=BB=D0=B5=D1=84=D0=BE=D0=BD=D0=B0=20=D0=BF=D0=BE=20NFC?= =?UTF-8?q?/BLE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/ru/komet/app/BleContactExchange.kt | 18 +++++---- .../main/kotlin/ru/komet/app/MainActivity.kt | 37 ++++++++++++------- .../main/kotlin/ru/komet/app/NfcExchange.kt | 9 +++-- lib/backend/modules/contacts.dart | 8 +++- lib/core/nfc/nfc_exchange_service.dart | 11 ++++-- .../screens/contacts/nfc_exchange_sheet.dart | 14 +++++-- 6 files changed, 65 insertions(+), 32 deletions(-) diff --git a/android/app/src/main/kotlin/ru/komet/app/BleContactExchange.kt b/android/app/src/main/kotlin/ru/komet/app/BleContactExchange.kt index 4a4140f..ac24beb 100644 --- a/android/app/src/main/kotlin/ru/komet/app/BleContactExchange.kt +++ b/android/app/src/main/kotlin/ru/komet/app/BleContactExchange.kt @@ -35,7 +35,7 @@ class BleContactExchange(private val context: Context) { const val MFG_ID = 0x4B4D } - var onReceived: ((Long) -> Unit)? = null + var onReceived: ((Long, Long) -> Unit)? = null var onSent: ((Long) -> 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 selfSession: String = "" + @Volatile private var selfPhone: Long = 0L @Volatile private var peerIdForWrite: Long = 0L @Volatile private var connecting = false @Volatile private var running = false - fun start(selfId: Long, selfSession: String) { + fun start(selfId: Long, selfSession: String, selfPhone: Long) { val adapter = this.adapter if (adapter == null || !adapter.isEnabled) { emitError("bluetooth_off") @@ -66,6 +67,7 @@ class BleContactExchange(private val context: Context) { } this.selfId = selfId this.selfSession = selfSession + this.selfPhone = selfPhone running = true startGattServer() } @@ -266,8 +268,10 @@ class BleContactExchange(private val context: Context) { } } if (characteristic?.uuid != CHAR_UUID || value == null) return - val peerId = String(value, Charsets.UTF_8).trim().toLongOrNull() ?: return - if (peerId > 0L) emitReceived(peerId) + val parts = String(value, Charsets.UTF_8).trim().split(":") + 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 @Suppress("DEPRECATION") - characteristic.value = selfId.toString().toByteArray(Charsets.UTF_8) + characteristic.value = "$selfId:$selfPhone".toByteArray(Charsets.UTF_8) try { @Suppress("DEPRECATION") gatt.writeCharacteristic(characteristic) @@ -324,8 +328,8 @@ class BleContactExchange(private val context: Context) { } } - private fun emitReceived(id: Long) { - main.post { onReceived?.invoke(id) } + private fun emitReceived(id: Long, phone: Long) { + main.post { onReceived?.invoke(id, phone) } } private fun emitSent(id: Long) { diff --git a/android/app/src/main/kotlin/ru/komet/app/MainActivity.kt b/android/app/src/main/kotlin/ru/komet/app/MainActivity.kt index 4b39664..18a6d69 100644 --- a/android/app/src/main/kotlin/ru/komet/app/MainActivity.kt +++ b/android/app/src/main/kotlin/ru/komet/app/MainActivity.kt @@ -42,7 +42,9 @@ class MainActivity : FlutterActivity() { private var ble: BleContactExchange? = null private var pendingSelfId = 0L + private var pendingSelfPhone = 0L private var pendingSession = "" + private var pendingPeer: NfcExchange.Peer? = null @Volatile private var exchangingEmitted = false private companion object { @@ -88,15 +90,12 @@ class MainActivity : FlutterActivity() { when (call.method) { "status" -> result.success(nfcStatus()) "start" -> { - val selfId = when (val v = call.argument("selfId")) { - is Int -> v.toLong() - is Long -> v - else -> 0L - } + val selfId = longArg(call.argument("selfId")) + val selfPhone = longArg(call.argument("selfPhone")) if (selfId <= 0L) { result.error("INVALID_ID", "selfId must be positive", null) } else { - startNfcExchange(selfId) + startNfcExchange(selfId, selfPhone) 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()) NfcExchange.selfId = selfId NfcExchange.selfSession = session + NfcExchange.selfPhone = selfPhone NfcExchange.active = true NfcExchange.onServed = { onNfcServed() } seenPeers.clear() exchangingEmitted = false + pendingPeer = null pendingSelfId = selfId + pendingSelfPhone = selfPhone pendingSession = session nfcCycling = true nfcHandler.removeCallbacksAndMessages(null) @@ -227,7 +229,9 @@ class MainActivity : FlutterActivity() { NfcExchange.active = false NfcExchange.selfId = 0L NfcExchange.selfSession = "" + NfcExchange.selfPhone = 0L NfcExchange.onServed = null + pendingPeer = null nfcHandler.removeCallbacksAndMessages(null) nfcReaderDisable() ble?.stop() @@ -259,12 +263,12 @@ class MainActivity : FlutterActivity() { private fun startBle() { val exchange = ble ?: BleContactExchange(applicationContext).also { - it.onReceived = { id -> onBleReceived(id) } - it.onSent = { id -> onBleReceived(id) } + it.onReceived = { id, phone -> revealPeer(id, phone) } + it.onSent = { _ -> pendingPeer?.let { p -> revealPeer(p.id, p.phone) } } it.onError = { reason -> onBleError(reason) } ble = it } - exchange.start(pendingSelfId, pendingSession) + exchange.start(pendingSelfId, pendingSession, pendingSelfPhone) } private fun emitExchanging() { @@ -273,9 +277,15 @@ class MainActivity : FlutterActivity() { 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 - 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) { @@ -360,8 +370,9 @@ class MainActivity : FlutterActivity() { nfcCycling = false nfcReaderDisable() emitExchanging() + pendingPeer = peer ble?.connectTo(peer.session, peer.id) - nfcHandler.postDelayed({ onBleReceived(peer.id) }, 3000L) + nfcHandler.postDelayed({ revealPeer(peer.id, peer.phone) }, 3000L) } } diff --git a/android/app/src/main/kotlin/ru/komet/app/NfcExchange.kt b/android/app/src/main/kotlin/ru/komet/app/NfcExchange.kt index 9bf557f..ab5ccaa 100644 --- a/android/app/src/main/kotlin/ru/komet/app/NfcExchange.kt +++ b/android/app/src/main/kotlin/ru/komet/app/NfcExchange.kt @@ -11,17 +11,19 @@ object NfcExchange { @Volatile var active: Boolean = false @Volatile var selfId: Long = 0L @Volatile var selfSession: String = "" + @Volatile var selfPhone: Long = 0L @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 { val id = selfId val session = selfSession if (!active || id <= 0L || session.isEmpty()) return STATUS_NOT_FOUND 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 { @@ -42,7 +44,8 @@ object NfcExchange { val id = parts[0].toLongOrNull() ?: return null val session = parts[1] 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 { diff --git a/lib/backend/modules/contacts.dart b/lib/backend/modules/contacts.dart index 025d169..dac35e8 100644 --- a/lib/backend/modules/contacts.dart +++ b/lib/backend/modules/contacts.dart @@ -60,8 +60,9 @@ class ContactsModule { static Future addContact( Api api, int id, - String firstName, - ) async { + String firstName, { + int phone = 0, + }) async { final resp = await api.sendRequest(Opcode.contactUpdate, { 'action': 'ADD', 'contactId': id, @@ -92,6 +93,9 @@ class ContactsModule { }; if (row == null) return null; + if (phone > 0 && ((row['phone'] as int?) ?? 0) == 0) { + row['phone'] = phone; + } await AppDatabase.saveContacts([row]); if (contact != null) _primeContactCache(contact); revision.value++; diff --git a/lib/core/nfc/nfc_exchange_service.dart b/lib/core/nfc/nfc_exchange_service.dart index f7ea9af..898218f 100644 --- a/lib/core/nfc/nfc_exchange_service.dart +++ b/lib/core/nfc/nfc_exchange_service.dart @@ -8,9 +8,10 @@ enum NfcEventType { received, exchanging, cancelled, error } class NfcEvent { final NfcEventType type; final int? id; + final int? phone; final String? reason; - const NfcEvent(this.type, this.id, {this.reason}); + const NfcEvent(this.type, this.id, {this.phone, this.reason}); } class NfcStatus { @@ -47,8 +48,8 @@ class NfcExchangeService { Stream get events => _events.receiveBroadcastStream().map(_decodeEvent); - Future start(int selfId) => - _method.invokeMethod('start', {'selfId': selfId}); + Future start(int selfId, int selfPhone) => + _method.invokeMethod('start', {'selfId': selfId, 'selfPhone': selfPhone}); Future stop() async { if (!_supported) return; @@ -61,9 +62,11 @@ class NfcExchangeService { final map = raw is Map ? raw : const {}; final id = map['id']; 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']) { case 'received': - return NfcEvent(NfcEventType.received, parsedId); + return NfcEvent(NfcEventType.received, parsedId, phone: parsedPhone); case 'exchanging': return const NfcEvent(NfcEventType.exchanging, null); case 'error': diff --git a/lib/frontend/screens/contacts/nfc_exchange_sheet.dart b/lib/frontend/screens/contacts/nfc_exchange_sheet.dart index 0520876..ba2127a 100644 --- a/lib/frontend/screens/contacts/nfc_exchange_sheet.dart +++ b/lib/frontend/screens/contacts/nfc_exchange_sheet.dart @@ -9,6 +9,7 @@ import '../../../backend/modules/contacts.dart'; import '../../../core/cache/info_cache.dart'; import '../../../core/nfc/nfc_exchange_service.dart'; import '../../../core/storage/app_database.dart'; +import '../../../core/utils/format.dart'; import '../../../main.dart'; import '../../widgets/custom_notification.dart'; import '../../widgets/komet_avatar.dart'; @@ -41,6 +42,7 @@ class _NfcExchangeSheetState extends State _Stage _stage = _Stage.checking; int? _peerId; + int? _peerPhone; Map? _peerInfo; String _failReason = ''; @@ -85,7 +87,7 @@ class _NfcExchangeSheetState extends State return; } _sub = _nfc.events.listen(_onEvent); - await _nfc.start(profile.id); + await _nfc.start(profile.id, profile.phone); if (mounted) setState(() => _stage = _Stage.scanning); } @@ -114,6 +116,7 @@ class _NfcExchangeSheetState extends State final id = event.id; if (id == null || _peerId != null) return; _peerId = id; + _peerPhone = (event.phone != null && event.phone! > 0) ? event.phone : null; HapticFeedback.mediumImpact(); _reveal.forward(from: 0); setState(() => _stage = _Stage.found); @@ -163,7 +166,12 @@ class _NfcExchangeSheetState extends State if (id == null) return; setState(() => _stage = _Stage.adding); try { - await ContactsModule.addContact(api, id, _firstNameForAdd()); + await ContactsModule.addContact( + api, + id, + _firstNameForAdd(), + phone: _peerPhone ?? 0, + ); if (!mounted) return; setState(() => _stage = _Stage.added); showCustomNotification(context, 'Контакт добавлен'); @@ -413,7 +421,7 @@ class _NfcExchangeSheetState extends State ), const SizedBox(height: 4), Text( - 'ID ${_peerId ?? ''}', + formatPhone(_peerPhone) ?? 'ID ${_peerId ?? ''}', style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13), ), const SizedBox(height: 24),