flutter_nearby_http 0.0.2
flutter_nearby_http: ^0.0.2 copied to clipboard
A package for communication in intermittent connectivity scenarios using a reputation-based mechanism.
import 'dart:typed_data';
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:io';
import 'package:ReCoopsys_Example/widgets.dart';
import 'package:flutter_nearby_connections_plus/flutter_nearby_connections_plus.dart';
import 'package:flutter_nearby_http/services/image_service.dart';
import 'package:flutter_nearby_http/utils/permissions_manager.dart';
import 'package:path_provider/path_provider.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:flutter_nearby_http/models/node_reputation.dart';
import 'package:flutter_nearby_http/models/packet_metrics.dart';
import 'package:flutter_nearby_http/services/nearby_http_service.dart';
import 'package:flutter_nearby_http/services/nearby_service_instance.dart';
import 'package:flutter_nearby_http/services/nearby_service_manager.dart';
import 'package:flutter_nearby_http/utils/device_status_utils.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(MaterialApp(
home: DevicesListScreen(),
));
}
enum DeviceType { advertiser, browser }
class DevicesListScreen extends StatefulWidget {
const DevicesListScreen();
@override
_DevicesListScreenState createState() => _DevicesListScreenState();
}
class _DevicesListScreenState extends State<DevicesListScreen> {
List<Device> devices = [];
List<Device> connectedDevices = [];
late NearbyService nearbyService;
late StreamSubscription subscription;
late StreamSubscription receivedDataSubscription;
List<Uint8List> receivedImages = [];
DeviceRole? _role;
bool _canCooperate = true;
bool _isInitialized = false;
final List<Device> _discoveredDevices = [];
List<Device> get discoveredDevices => List.unmodifiable(_discoveredDevices);
final NearbyServiceManager repManager = NearbyServiceManager();
late Future<NodeReputation> currentReputation;
Future<List<NodeReputation>>? history;
late Future<NodeReputation> lastHistoryRecord;
late String filePath;
NearbyService get nearbyServiceGet => nearbyService;
Map<String, List<int>> delays = HashMap();
Map<String, int> sentPackets = HashMap();
Map<String, int> failedPackets = HashMap();
Map<String, int> reputationScores = HashMap();
List<PacketMetrics> packetMetricsTable = [];
bool isInit = false;
Future<void> _checkStatus() async {
final nearbyHttpService = repManager.getService();
await DeviceStatusUtils.checkDeviceStatus(context, nearbyHttpService);
}
Future<List<NodeReputation>> loadHistory() async {
final historyRaw = await repManager.readHistory();
return historyRaw.map((map) => NodeReputation.fromMap(map)).toList();
}
String cooperate = "";
Future<NodeReputation> loadLastRecord() async {
final historyRaw = await repManager.readHistory();
DateTime date = DateTime.now();
if (historyRaw.isEmpty) return NodeReputation(deviceId: "0", battery: 0, storage: 0, successfulInteractions: 0, totalInteractions: 0, requestedPackets: 0, deliveredPackets: 0, avgLatency: 0, diversity: 0, daysSinceLastInteraction: 0, weights: [], timestamp: date.toString());
final lastMap = historyRaw.last;
cooperate = NodeReputation.cooperationStatusString(NodeReputation.fromMap(lastMap));
return NodeReputation.fromMap(lastMap);
}
Future<void> _determineRole() async {
try {
final hasInternet = await PermissionsManager.checkInternetConnection();
setState(() {
_role = hasInternet ? DeviceRole.advertiser : DeviceRole.browser;
});
} catch (e) {
print('Error: $e');
setState(() {
_role = DeviceRole.browser; // Fallback
});
}
}
Future<void> _initializeApp() async {
try {
await _determineRole();
await PermissionsManager.requestBluetoothPermissions();
await PermissionsManager.requestLocationPermissions();
await init();
} catch (e) {
print('Error: $e');
}
}
@override
void initState() {
super.initState();
currentReputation = repManager.calculateCurrentReputation();
history = loadHistory();
lastHistoryRecord = loadLastRecord();
_checkStatus();
_initializeApp();
}
@override
void dispose() {
subscription.cancel();
receivedDataSubscription.cancel();
nearbyService.stopBrowsingForPeers();
nearbyService.stopAdvertisingPeer();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (_role == null) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
return Scaffold(
appBar: AppBar(
title:
Text(_role.toString().substring(11).toUpperCase()),
),
backgroundColor: Colors.white,
body: ListView.builder(
itemCount: getItemCount(),
itemBuilder: (context, index) {
final device = _role == DeviceType.advertiser
? connectedDevices[index]
: devices[index];
return Container(
margin: EdgeInsets.all(8.0),
child: Column(
children: [
Row(
children: [
Expanded(
child: Column(
children: [
Text(device.deviceName),
Text(
getStateName(device.state),
style: TextStyle(
color: getStateColor(device.state)),
),
_role == DeviceRole.advertiser ? Text('this device') : Container(),
],
crossAxisAlignment: CrossAxisAlignment.start,
)),
_role == DeviceRole.browser ? GestureDetector(
onTap: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text("Reputation History"),
content: SizedBox(
width:
MediaQuery.of(context).size.width * 0.8,
child: HistoryTable(history: history),
),
actions: [
TextButton(
onPressed: () async {
final dir = await getApplicationDocumentsDirectory();
final nearbyService = NearbyHttpService();
final devId = nearbyService.devInfo;
filePath = '${dir.path}/reputation_$devId.txt';
NearbyHttpService().resetPacketCounters();
history = Future.value(<NodeReputation>[]);
final file = File(filePath);
if (await file.exists()) {
await file.writeAsString(''); // Esvazia o ficheiro
print("Reset History to $devId.");
}
Navigator.of(context).pop();
},
child: Text('Reset'),
),
TextButton(
child: Text("Exit"),
onPressed: () =>
Navigator.of(context).pop(),
),
],
),
);
},
child: Container(
margin: EdgeInsets.symmetric(horizontal: 8.0),
padding: EdgeInsets.all(8.0),
height: 35,
width: 100,
color: getButtonColor(device.state),
child: Center(
child: Text(
'Cooperation',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold),
),
),
),
) : GestureDetector(
onTap: () async {
final lastRecord = await loadLastRecord();
ReputationDialogs.showLastRecordPopup(context, cooperate, lastRecord);
},
child: Container(
margin: EdgeInsets.symmetric(horizontal: 8.0),
padding: EdgeInsets.all(8.0),
height: 35,
width: 100,
color: getButtonColor(device.state),
child: Center(
child: Text(
'Cooperation',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold),
),
),
),
),
// Request connect
GestureDetector(
onTap: () => _onButtonClicked(device),
child: Container(
margin: EdgeInsets.symmetric(horizontal: 8.0),
padding: EdgeInsets.all(8.0),
height: 35,
width: 100,
color: getButtonColor(device.state),
child: Center(
child: Text(
getButtonStateName(device.state),
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold),
),
),
),
)
],
),
SizedBox(
height: 8.0,
),
_role == DeviceRole.browser ? GestureDetector(
onTap: () => _onTabItemListener(device),
child: Container(
margin: EdgeInsets.symmetric(horizontal: 8.0),
padding: EdgeInsets.all(8.0),
height: 35,
width: 300,
color: getButtonColor(device.state),
child: Center(
child: Text(
"AskForPicture (HTTPS | JPG)",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold),
),
),
),
) : Container(),
SizedBox(
height: 8.0,
),
Divider(
height: 1,
color: Colors.grey,
)
],
),
);
}));
}
String getStateName(SessionState state) {
switch (state) {
case SessionState.notConnected:
return "disconnected";
case SessionState.connecting:
return "waiting";
default:
return "connected";
}
}
String getButtonStateName(SessionState state) {
switch (state) {
case SessionState.notConnected:
case SessionState.connecting:
return "Connect";
default:
return "Disconnect";
}
}
Color getStateColor(SessionState state) {
switch (state) {
case SessionState.notConnected:
return Colors.black;
case SessionState.connecting:
return Colors.grey;
default:
return Colors.green;
}
}
Color getButtonColor(SessionState state) {
switch (state) {
case SessionState.notConnected:
case SessionState.connecting:
return Colors.red;
default:
return Colors.green;
}
}
void sendImageBackToDevice(String deviceId, String imageUrl) async {
final startTime = DateTime.now();
startTimes.putIfAbsent(deviceId, () => []).add(startTime);
try {
Uint8List imageBytes = await ImageService.downloadImage(imageUrl);
String base64Image = base64Encode(await ImageService.resizeImage(imageBytes));
Device foundDevice =
connectedDevices.firstWhere((device) => device.deviceId == deviceId);
NearbyHttpService()
.sentPackets
.update(deviceId, (v) => v + 1, ifAbsent: () => 1);
nearbyService.sendMessage(
foundDevice.deviceId,
base64Image,
);
_role == DeviceRole.advertiser
? Container()
: showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text("Reputation History"),
content: SizedBox(
width: MediaQuery.of(context).size.width * 0.8,
child: HistoryTable(history: history),
),
actions: [
TextButton(
child: Text("Exit"),
onPressed: () => Navigator.of(context).pop(),
),
],
),
);
ReputationDialogs.showImageDialog(context, imageBytes);
await repManager.init();
await repManager.calculateCurrentReputation();
setState(() {
history = loadHistory();
});
} catch (e) {
NearbyHttpService()
.failedPackets
.update(deviceId, (v) => v + 1, ifAbsent: () => 1);
packetMetricsTable.add(PacketMetrics(
deviceId: deviceId,
packetSent: true,
packetDiscarded: true,
delay: 0,
dateTime: startTime,
packetSize: 0,
));
}
packetMetricsTable.forEach((m) {
m.dateTime.add(Duration(milliseconds: m.delay));
});
}
_onTabItemListener(Device device) {
if (device.state == SessionState.connected) {
showDialog(
context: context,
builder: (BuildContext context) {
final myController = TextEditingController();
return AlertDialog(
title: Text("Send Message:"),
content: TextField(controller: myController),
actions: [
TextButton(
child: Text("Cancel"),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: Text("Send"),
onPressed: () {
final startTime = DateTime.now();
startTimes
.putIfAbsent(device.deviceId, () => [])
.add(startTime);
nearbyService.sendMessage(
device.deviceId, myController.text);
myController.text = '';
},
)
],
);
});
}
}
_onButtonClicked(Device device) {
switch (device.state) {
case SessionState.notConnected:
nearbyService.invitePeer(
deviceID: device.deviceId,
deviceName: device.deviceName,
);
break;
case SessionState.connected:
nearbyService.disconnectPeer(deviceID: device.deviceId);
break;
case SessionState.connecting:
break;
}
}
Future<void> inviteClosestPeer() async {
if (_discoveredDevices.isEmpty) return;
final closest = _discoveredDevices.first;
await nearbyService.invitePeer(
deviceID: closest.deviceId,
deviceName: closest.deviceName,
);
}
Future<void> _handleDeviceRole() async {
if (_role == DeviceRole.advertiser && _canCooperate) {
print("The device is an advertiser.");
await nearbyService.stopAdvertisingPeer();
await nearbyService.stopBrowsingForPeers();
await Future.delayed(Duration(microseconds: 200));
await nearbyService.startAdvertisingPeer();
// await nearbyService.startBrowsingForPeers();
} else if (_role == DeviceRole.browser) {
print("The device is a browser.");
await nearbyService.stopBrowsingForPeers();
await Future.delayed(Duration(microseconds: 200));
await nearbyService.startBrowsingForPeers();
} else {
await nearbyService.stopBrowsingForPeers();
await nearbyService.stopAdvertisingPeer();
}
}
int getItemCount() {
if (_role == DeviceType.advertiser) {
return connectedDevices.length;
} else {
return devices.length;
}
}
Map<String, List<DateTime>> startTimes = {};
Future<void> init() async {
if (_isInitialized) return;
nearbyService = NearbyServiceInstance().service;
String devInfo = '';
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
if (Platform.isAndroid) {
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
devInfo = androidInfo.model;
}
if (Platform.isIOS) {
IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
devInfo = iosInfo.localizedModel;
}
await nearbyService.init(
serviceType: 'mp-connection',
deviceName: devInfo,
strategy: Strategy.P2P_CLUSTER,
callback: (isRunning) async {
if (isRunning) {
await _handleDeviceRole();
_isInitialized = true;
} else {
print("Error starting the Nearby service...");
}
});
subscription =
nearbyService.stateChangedSubscription(callback: (devicesList) {
devicesList.forEach((element) {
_discoveredDevices.add(element);
if (Platform.isAndroid) {
if (element.state == SessionState.connected) {
nearbyService.stopBrowsingForPeers();
} else {
inviteClosestPeer();
}
}
});
devices.forEach((device) {
NearbyHttpService()
.reputationScores
.putIfAbsent(device.deviceId, () => 0);
NearbyHttpService().sentPackets.putIfAbsent(device.deviceId, () => 0);
NearbyHttpService().failedPackets.putIfAbsent(device.deviceId, () => 0);
});
setState(() {
devices.clear();
devices.addAll(devicesList);
connectedDevices.clear();
connectedDevices.addAll(devicesList
.where((d) => d.state == SessionState.connected)
.toList());
});
});
receivedDataSubscription =
nearbyService.dataReceivedSubscription(callback: (data) async {
Map<String, dynamic> jsonMap = jsonDecode(jsonEncode(data));
String deviceId = jsonMap['deviceId'];
DateTime endTime = DateTime.now();
int delay = 0;
DateTime startTime = endTime;
if (startTimes.containsKey(deviceId) &&
startTimes[deviceId]!.isNotEmpty) {
startTime = startTimes[deviceId]!.removeAt(0);
delay = endTime.difference(startTime).inMilliseconds;
delays.putIfAbsent(deviceId, () => []).add(delay);
}
int packetSize = 0;
String message = jsonMap['message'];
if (!message.startsWith('http')) {
try {
packetSize = base64Decode(message).length;
} catch (_) {
packetSize = 0;
}
}
packetMetricsTable.add(PacketMetrics(
deviceId: deviceId,
packetSent: true,
packetDiscarded: false,
delay: delay,
dateTime: startTime,
packetSize: packetSize,
));
if (message.startsWith('http://') || message.startsWith('https://')) {
ImageService.downloadImage(message).then((_) {
sendImageBackToDevice(deviceId, message);
});
} else {
Uint8List imageBytes = base64Decode(message);
NearbyHttpService()
.sentPackets
.update(deviceId, (v) => v + 1, ifAbsent: () => 1);
await repManager.init();
repManager.updateMetrics(
sent: NearbyHttpService().sentPackets,
failed: NearbyHttpService().failedPackets,
delays: delays,
metrics: NearbyHttpService().packetMetricsTable,
);
await repManager.calculateCurrentReputation();
setState(() {
history = loadHistory(); // força rebuild da tabela
});
ReputationDialogs.showImageDialog(context, imageBytes);
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text("Reputation History"),
content: SizedBox(
width:
MediaQuery.of(context).size.width * 0.8,
child: HistoryTable(history: history),
),
actions: [
TextButton(
onPressed: () async {
final dir = await getApplicationDocumentsDirectory();
final nearbyService = NearbyHttpService();
final devId = nearbyService.devInfo;
filePath = '${dir.path}/reputation_$devId.txt';
NearbyHttpService().resetPacketCounters();
history = Future.value(<NodeReputation>[]);
final file = File(filePath);
if (await file.exists()) {
await file.writeAsString(''); // Esvazia o ficheiro
print("Reset History to $devId.");
}
Navigator.of(context).pop();
},
child: Text('Reset'),
),
TextButton(
child: Text("Exit"),
onPressed: () =>
Navigator.of(context).pop(),
),
],
),
);
}
});
_role = repManager.getService().role!;
}
}