feat(contacts): синхронный показ карточки + статус обмена
This commit is contained in:
@@ -36,6 +36,7 @@ class BleContactExchange(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var onReceived: ((Long) -> Unit)? = null
|
var onReceived: ((Long) -> Unit)? = null
|
||||||
|
var onSent: ((Long) -> Unit)? = null
|
||||||
var onError: ((String) -> Unit)? = null
|
var onError: ((String) -> Unit)? = null
|
||||||
|
|
||||||
private val main = Handler(Looper.getMainLooper())
|
private val main = Handler(Looper.getMainLooper())
|
||||||
@@ -53,6 +54,7 @@ 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 peerIdForWrite: Long = 0L
|
||||||
@Volatile private var connecting = false
|
@Volatile private var connecting = false
|
||||||
@Volatile private var running = false
|
@Volatile private var running = false
|
||||||
|
|
||||||
@@ -68,8 +70,9 @@ class BleContactExchange(private val context: Context) {
|
|||||||
startGattServer()
|
startGattServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun connectTo(peerSession: String) {
|
fun connectTo(peerSession: String, peerId: Long) {
|
||||||
if (!running || connecting) return
|
if (!running || connecting) return
|
||||||
|
peerIdForWrite = peerId
|
||||||
startScan(peerSession)
|
startScan(peerSession)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,6 +316,10 @@ class BleContactExchange(private val context: Context) {
|
|||||||
characteristic: BluetoothGattCharacteristic?,
|
characteristic: BluetoothGattCharacteristic?,
|
||||||
status: Int,
|
status: Int,
|
||||||
) {
|
) {
|
||||||
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||||
|
val id = peerIdForWrite
|
||||||
|
if (id > 0L) emitSent(id)
|
||||||
|
}
|
||||||
gatt?.disconnect()
|
gatt?.disconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -321,6 +328,10 @@ class BleContactExchange(private val context: Context) {
|
|||||||
main.post { onReceived?.invoke(id) }
|
main.post { onReceived?.invoke(id) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun emitSent(id: Long) {
|
||||||
|
main.post { onSent?.invoke(id) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun emitError(reason: String) {
|
private fun emitError(reason: String) {
|
||||||
main.post { onError?.invoke(reason) }
|
main.post { onError?.invoke(reason) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class MainActivity : FlutterActivity() {
|
|||||||
private var ble: BleContactExchange? = null
|
private var ble: BleContactExchange? = null
|
||||||
private var pendingSelfId = 0L
|
private var pendingSelfId = 0L
|
||||||
private var pendingSession = ""
|
private var pendingSession = ""
|
||||||
|
@Volatile private var exchangingEmitted = false
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val LOG_TAG = "VpnBypass"
|
const val LOG_TAG = "VpnBypass"
|
||||||
@@ -212,6 +213,7 @@ class MainActivity : FlutterActivity() {
|
|||||||
NfcExchange.active = true
|
NfcExchange.active = true
|
||||||
NfcExchange.onServed = { onNfcServed() }
|
NfcExchange.onServed = { onNfcServed() }
|
||||||
seenPeers.clear()
|
seenPeers.clear()
|
||||||
|
exchangingEmitted = false
|
||||||
pendingSelfId = selfId
|
pendingSelfId = selfId
|
||||||
pendingSession = session
|
pendingSession = session
|
||||||
nfcCycling = true
|
nfcCycling = true
|
||||||
@@ -258,12 +260,19 @@ 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 -> onBleReceived(id) }
|
||||||
|
it.onSent = { id -> onBleReceived(id) }
|
||||||
it.onError = { reason -> onBleError(reason) }
|
it.onError = { reason -> onBleError(reason) }
|
||||||
ble = it
|
ble = it
|
||||||
}
|
}
|
||||||
exchange.start(pendingSelfId, pendingSession)
|
exchange.start(pendingSelfId, pendingSession)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun emitExchanging() {
|
||||||
|
if (exchangingEmitted) return
|
||||||
|
exchangingEmitted = true
|
||||||
|
nfcEvents?.success(mapOf("event" to "exchanging"))
|
||||||
|
}
|
||||||
|
|
||||||
private fun onBleReceived(id: Long) {
|
private fun onBleReceived(id: 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))
|
||||||
@@ -278,6 +287,7 @@ class MainActivity : FlutterActivity() {
|
|||||||
if (!NfcExchange.active) return@post
|
if (!NfcExchange.active) return@post
|
||||||
nfcCycling = false
|
nfcCycling = false
|
||||||
nfcReaderDisable()
|
nfcReaderDisable()
|
||||||
|
emitExchanging()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,10 +359,9 @@ class MainActivity : FlutterActivity() {
|
|||||||
if (peer.id == NfcExchange.selfId) return@post
|
if (peer.id == NfcExchange.selfId) return@post
|
||||||
nfcCycling = false
|
nfcCycling = false
|
||||||
nfcReaderDisable()
|
nfcReaderDisable()
|
||||||
ble?.connectTo(peer.session)
|
emitExchanging()
|
||||||
if (seenPeers.add(peer.id)) {
|
ble?.connectTo(peer.session, peer.id)
|
||||||
nfcEvents?.success(mapOf("event" to "received", "id" to peer.id))
|
nfcHandler.postDelayed({ onBleReceived(peer.id) }, 3000L)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
enum NfcEventType { received, cancelled, error }
|
enum NfcEventType { received, exchanging, cancelled, error }
|
||||||
|
|
||||||
class NfcEvent {
|
class NfcEvent {
|
||||||
final NfcEventType type;
|
final NfcEventType type;
|
||||||
@@ -64,6 +64,8 @@ class NfcExchangeService {
|
|||||||
switch (map['event']) {
|
switch (map['event']) {
|
||||||
case 'received':
|
case 'received':
|
||||||
return NfcEvent(NfcEventType.received, parsedId);
|
return NfcEvent(NfcEventType.received, parsedId);
|
||||||
|
case 'exchanging':
|
||||||
|
return const NfcEvent(NfcEventType.exchanging, null);
|
||||||
case 'error':
|
case 'error':
|
||||||
return NfcEvent(NfcEventType.error, null, reason: map['reason'] as String?);
|
return NfcEvent(NfcEventType.error, null, reason: map['reason'] as String?);
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -12,7 +12,17 @@ import '../../../main.dart';
|
|||||||
import '../../widgets/custom_notification.dart';
|
import '../../widgets/custom_notification.dart';
|
||||||
import '../../widgets/komet_avatar.dart';
|
import '../../widgets/komet_avatar.dart';
|
||||||
|
|
||||||
enum _Stage { checking, unsupported, disabled, failed, scanning, found, adding, added }
|
enum _Stage {
|
||||||
|
checking,
|
||||||
|
unsupported,
|
||||||
|
disabled,
|
||||||
|
failed,
|
||||||
|
scanning,
|
||||||
|
exchanging,
|
||||||
|
found,
|
||||||
|
adding,
|
||||||
|
added,
|
||||||
|
}
|
||||||
|
|
||||||
class NfcExchangeSheet extends StatefulWidget {
|
class NfcExchangeSheet extends StatefulWidget {
|
||||||
const NfcExchangeSheet({super.key});
|
const NfcExchangeSheet({super.key});
|
||||||
@@ -88,6 +98,12 @@ class _NfcExchangeSheetState extends State<NfcExchangeSheet>
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (event.type == NfcEventType.exchanging) {
|
||||||
|
if (mounted && _peerId == null && _stage == _Stage.scanning) {
|
||||||
|
setState(() => _stage = _Stage.exchanging);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
final id = event.id;
|
final id = event.id;
|
||||||
if (id == null || _peerId != null) return;
|
if (id == null || _peerId != null) return;
|
||||||
_peerId = id;
|
_peerId = id;
|
||||||
@@ -190,7 +206,19 @@ class _NfcExchangeSheetState extends State<NfcExchangeSheet>
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildContent(cs),
|
AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 350),
|
||||||
|
switchInCurve: Curves.easeOutBack,
|
||||||
|
switchOutCurve: Curves.easeIn,
|
||||||
|
transitionBuilder: (child, animation) => FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: ScaleTransition(scale: animation, child: child),
|
||||||
|
),
|
||||||
|
child: KeyedSubtree(
|
||||||
|
key: ValueKey(_stage == _Stage.found ? 'found' : _stage.name),
|
||||||
|
child: _buildContent(cs),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -216,6 +244,8 @@ class _NfcExchangeSheetState extends State<NfcExchangeSheet>
|
|||||||
return _message(cs, Symbols.bluetooth_disabled, _failReason);
|
return _message(cs, Symbols.bluetooth_disabled, _failReason);
|
||||||
case _Stage.scanning:
|
case _Stage.scanning:
|
||||||
return _scanning(cs);
|
return _scanning(cs);
|
||||||
|
case _Stage.exchanging:
|
||||||
|
return _exchanging(cs);
|
||||||
case _Stage.found:
|
case _Stage.found:
|
||||||
case _Stage.adding:
|
case _Stage.adding:
|
||||||
case _Stage.added:
|
case _Stage.added:
|
||||||
@@ -280,6 +310,46 @@ class _NfcExchangeSheetState extends State<NfcExchangeSheet>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _exchanging(ColorScheme cs) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 24),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 120,
|
||||||
|
height: 120,
|
||||||
|
child: AnimatedBuilder(
|
||||||
|
animation: _pulse,
|
||||||
|
builder: (context, child) => CustomPaint(
|
||||||
|
painter: _RadarPainter(_pulse.value, cs.primary),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Icon(Symbols.sync, color: cs.primary, size: 40),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 22),
|
||||||
|
Text(
|
||||||
|
'Идёт обмен контактами…',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: cs.onSurface,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text(
|
||||||
|
'Почти готово',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _foundCard(ColorScheme cs) {
|
Widget _foundCard(ColorScheme cs) {
|
||||||
final loading = _peerInfo == null && _stage == _Stage.found;
|
final loading = _peerInfo == null && _stage == _Stage.found;
|
||||||
return Padding(
|
return Padding(
|
||||||
|
|||||||
Reference in New Issue
Block a user