spotify and network groups working
This commit is contained in:
parent
025eee7644
commit
6b93f1206d
11 changed files with 1381 additions and 267 deletions
475
CHALLENGE_2/sleepysound/lib/services/network_group_service.dart
Normal file
475
CHALLENGE_2/sleepysound/lib/services/network_group_service.dart
Normal file
|
@ -0,0 +1,475 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:network_info_plus/network_info_plus.dart';
|
||||
import 'package:multicast_dns/multicast_dns.dart';
|
||||
|
||||
class NetworkUser {
|
||||
final String id;
|
||||
final String name;
|
||||
final String ipAddress;
|
||||
final DateTime joinedAt;
|
||||
final int votes;
|
||||
bool isOnline;
|
||||
DateTime lastSeen;
|
||||
|
||||
NetworkUser({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.ipAddress,
|
||||
required this.joinedAt,
|
||||
this.votes = 0,
|
||||
this.isOnline = true,
|
||||
DateTime? lastSeen,
|
||||
}) : lastSeen = lastSeen ?? DateTime.now();
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'ipAddress': ipAddress,
|
||||
'joinedAt': joinedAt.toIso8601String(),
|
||||
'votes': votes,
|
||||
'isOnline': isOnline,
|
||||
'lastSeen': lastSeen.toIso8601String(),
|
||||
};
|
||||
|
||||
factory NetworkUser.fromJson(Map<String, dynamic> json) => NetworkUser(
|
||||
id: json['id'],
|
||||
name: json['name'],
|
||||
ipAddress: json['ipAddress'],
|
||||
joinedAt: DateTime.parse(json['joinedAt']),
|
||||
votes: json['votes'] ?? 0,
|
||||
isOnline: json['isOnline'] ?? true,
|
||||
lastSeen: DateTime.parse(json['lastSeen']),
|
||||
);
|
||||
}
|
||||
|
||||
class NetworkGroupService extends ChangeNotifier {
|
||||
static const int _discoveryPort = 8089;
|
||||
static const int _heartbeatInterval = 10; // seconds
|
||||
|
||||
final Connectivity _connectivity = Connectivity();
|
||||
final NetworkInfo _networkInfo = NetworkInfo();
|
||||
MDnsClient? _mdnsClient;
|
||||
HttpServer? _httpServer;
|
||||
Timer? _heartbeatTimer;
|
||||
Timer? _discoveryTimer;
|
||||
|
||||
bool _isConnectedToWifi = false;
|
||||
String _currentNetworkName = '';
|
||||
String _currentNetworkSSID = '';
|
||||
String _localIpAddress = '';
|
||||
String _networkSubnet = '';
|
||||
|
||||
final Map<String, NetworkUser> _networkUsers = {};
|
||||
late NetworkUser _currentUser;
|
||||
|
||||
// Getters
|
||||
bool get isConnectedToWifi => _isConnectedToWifi;
|
||||
String get currentNetworkName => _currentNetworkName;
|
||||
String get currentNetworkSSID => _currentNetworkSSID;
|
||||
String get localIpAddress => _localIpAddress;
|
||||
List<NetworkUser> get networkUsers => _networkUsers.values.toList();
|
||||
NetworkUser get currentUser => _currentUser;
|
||||
int get onlineUsersCount => _networkUsers.values.where((u) => u.isOnline).length;
|
||||
|
||||
NetworkGroupService() {
|
||||
_initializeCurrentUser();
|
||||
_startNetworkMonitoring();
|
||||
}
|
||||
|
||||
void _initializeCurrentUser() {
|
||||
final random = Random();
|
||||
final guestNames = ['Alex', 'Sarah', 'Marco', 'Lisa', 'Tom', 'Anna', 'David', 'Emma'];
|
||||
final randomName = guestNames[random.nextInt(guestNames.length)];
|
||||
final randomId = random.nextInt(999);
|
||||
|
||||
_currentUser = NetworkUser(
|
||||
id: 'user_${DateTime.now().millisecondsSinceEpoch}',
|
||||
name: '$randomName #$randomId',
|
||||
ipAddress: _localIpAddress,
|
||||
joinedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _startNetworkMonitoring() async {
|
||||
// Monitor connectivity changes
|
||||
_connectivity.onConnectivityChanged.listen(_onConnectivityChanged);
|
||||
|
||||
// Initial connectivity check
|
||||
await _checkConnectivity();
|
||||
}
|
||||
|
||||
Future<void> _onConnectivityChanged(List<ConnectivityResult> results) async {
|
||||
await _checkConnectivity();
|
||||
}
|
||||
|
||||
Future<void> _checkConnectivity() async {
|
||||
final connectivityResult = await _connectivity.checkConnectivity();
|
||||
final wasConnected = _isConnectedToWifi;
|
||||
|
||||
_isConnectedToWifi = connectivityResult.contains(ConnectivityResult.wifi);
|
||||
|
||||
if (_isConnectedToWifi) {
|
||||
await _getNetworkInfo();
|
||||
if (!wasConnected) {
|
||||
await _startNetworkServices();
|
||||
}
|
||||
} else {
|
||||
if (wasConnected) {
|
||||
await _stopNetworkServices();
|
||||
}
|
||||
_clearNetworkInfo();
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _getNetworkInfo() async {
|
||||
try {
|
||||
_currentNetworkSSID = await _networkInfo.getWifiName() ?? 'Unknown Network';
|
||||
_currentNetworkName = _currentNetworkSSID.replaceAll('"', ''); // Remove quotes
|
||||
_localIpAddress = await _networkInfo.getWifiIP() ?? '';
|
||||
|
||||
// Calculate network subnet (assuming /24)
|
||||
if (_localIpAddress.isNotEmpty) {
|
||||
final parts = _localIpAddress.split('.');
|
||||
if (parts.length == 4) {
|
||||
_networkSubnet = '${parts[0]}.${parts[1]}.${parts[2]}';
|
||||
}
|
||||
}
|
||||
|
||||
// Update current user's IP
|
||||
_currentUser = NetworkUser(
|
||||
id: _currentUser.id,
|
||||
name: _currentUser.name,
|
||||
ipAddress: _localIpAddress,
|
||||
joinedAt: _currentUser.joinedAt,
|
||||
votes: _currentUser.votes,
|
||||
);
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error getting network info: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _clearNetworkInfo() {
|
||||
_currentNetworkName = '';
|
||||
_currentNetworkSSID = '';
|
||||
_localIpAddress = '';
|
||||
_networkSubnet = '';
|
||||
_networkUsers.clear();
|
||||
}
|
||||
|
||||
Future<void> _startNetworkServices() async {
|
||||
if (!_isConnectedToWifi || _localIpAddress.isEmpty) return;
|
||||
|
||||
try {
|
||||
// Start mDNS client for service discovery
|
||||
_mdnsClient = MDnsClient();
|
||||
await _mdnsClient!.start();
|
||||
|
||||
// Start HTTP server for peer communication
|
||||
await _startHttpServer();
|
||||
|
||||
// Start announcing our service
|
||||
await _announceService();
|
||||
|
||||
// Start discovering other users
|
||||
_startDiscovery();
|
||||
|
||||
// Start heartbeat for keeping users online
|
||||
_startHeartbeat();
|
||||
|
||||
// Add ourselves to the users list
|
||||
_networkUsers[_currentUser.id] = _currentUser;
|
||||
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error starting network services: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _stopNetworkServices() async {
|
||||
// Stop timers
|
||||
_heartbeatTimer?.cancel();
|
||||
_discoveryTimer?.cancel();
|
||||
|
||||
// Stop HTTP server
|
||||
await _httpServer?.close();
|
||||
_httpServer = null;
|
||||
|
||||
// Stop mDNS client
|
||||
_mdnsClient?.stop();
|
||||
_mdnsClient = null;
|
||||
|
||||
// Clear users
|
||||
_networkUsers.clear();
|
||||
}
|
||||
|
||||
Future<void> _startHttpServer() async {
|
||||
try {
|
||||
_httpServer = await HttpServer.bind(InternetAddress.anyIPv4, _discoveryPort);
|
||||
_httpServer!.listen((request) async {
|
||||
await _handleHttpRequest(request);
|
||||
});
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error starting HTTP server: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleHttpRequest(HttpRequest request) async {
|
||||
try {
|
||||
final response = request.response;
|
||||
response.headers.set('Content-Type', 'application/json');
|
||||
response.headers.set('Access-Control-Allow-Origin', '*');
|
||||
|
||||
if (request.method == 'GET' && request.uri.path == '/user') {
|
||||
// Return current user info
|
||||
response.write(jsonEncode(_currentUser.toJson()));
|
||||
} else if (request.method == 'POST' && request.uri.path == '/heartbeat') {
|
||||
// Handle heartbeat from other users
|
||||
final body = await utf8.decoder.bind(request).join();
|
||||
final userData = jsonDecode(body);
|
||||
final user = NetworkUser.fromJson(userData);
|
||||
|
||||
_networkUsers[user.id] = user;
|
||||
notifyListeners();
|
||||
|
||||
response.write(jsonEncode({'status': 'ok'}));
|
||||
} else {
|
||||
response.statusCode = 404;
|
||||
response.write(jsonEncode({'error': 'Not found'}));
|
||||
}
|
||||
|
||||
await response.close();
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error handling HTTP request: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _announceService() async {
|
||||
if (_mdnsClient == null || _localIpAddress.isEmpty) return;
|
||||
|
||||
try {
|
||||
// This would announce our service via mDNS
|
||||
// In a real implementation, you'd use proper mDNS announcements
|
||||
if (kDebugMode) {
|
||||
print('Announcing service on $_localIpAddress:$_discoveryPort');
|
||||
}
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error announcing service: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _startDiscovery() {
|
||||
_discoveryTimer = Timer.periodic(const Duration(seconds: 15), (_) async {
|
||||
await _discoverUsers();
|
||||
});
|
||||
|
||||
// Initial discovery
|
||||
_discoverUsers();
|
||||
}
|
||||
|
||||
Future<void> _discoverUsers() async {
|
||||
if (_networkSubnet.isEmpty) return;
|
||||
|
||||
// Scan local network for other SleepySound users
|
||||
final futures = <Future>[];
|
||||
|
||||
for (int i = 1; i <= 254; i++) {
|
||||
final ip = '$_networkSubnet.$i';
|
||||
if (ip != _localIpAddress) {
|
||||
futures.add(_tryConnectToUser(ip));
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for all connection attempts (with timeout)
|
||||
await Future.wait(futures).timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () => [],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _tryConnectToUser(String ip) async {
|
||||
try {
|
||||
final client = HttpClient();
|
||||
client.connectionTimeout = const Duration(seconds: 2);
|
||||
|
||||
final request = await client.getUrl(Uri.parse('http://$ip:$_discoveryPort/user'));
|
||||
final response = await request.close().timeout(const Duration(seconds: 2));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final body = await utf8.decoder.bind(response).join();
|
||||
final userData = jsonDecode(body);
|
||||
final user = NetworkUser.fromJson(userData);
|
||||
|
||||
if (user.id != _currentUser.id) {
|
||||
_networkUsers[user.id] = user;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
client.close();
|
||||
} catch (e) {
|
||||
// Ignore connection errors (expected for non-SleepySound devices)
|
||||
}
|
||||
}
|
||||
|
||||
void _startHeartbeat() {
|
||||
_heartbeatTimer = Timer.periodic(const Duration(seconds: _heartbeatInterval), (_) async {
|
||||
await _sendHeartbeat();
|
||||
_cleanupOfflineUsers();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _sendHeartbeat() async {
|
||||
final heartbeatData = jsonEncode(_currentUser.toJson());
|
||||
|
||||
// Send heartbeat to all known users
|
||||
final futures = _networkUsers.values
|
||||
.where((user) => user.id != _currentUser.id && user.isOnline)
|
||||
.map((user) => _sendHeartbeatToUser(user.ipAddress, heartbeatData));
|
||||
|
||||
await Future.wait(futures);
|
||||
}
|
||||
|
||||
Future<void> _sendHeartbeatToUser(String ip, String data) async {
|
||||
try {
|
||||
final client = HttpClient();
|
||||
client.connectionTimeout = const Duration(seconds: 2);
|
||||
|
||||
final request = await client.postUrl(Uri.parse('http://$ip:$_discoveryPort/heartbeat'));
|
||||
request.headers.set('Content-Type', 'application/json');
|
||||
request.write(data);
|
||||
|
||||
await request.close().timeout(const Duration(seconds: 2));
|
||||
client.close();
|
||||
} catch (e) {
|
||||
// Mark user as potentially offline
|
||||
final user = _networkUsers.values.firstWhere(
|
||||
(u) => u.ipAddress == ip,
|
||||
orElse: () => NetworkUser(id: '', name: '', ipAddress: '', joinedAt: DateTime.now()),
|
||||
);
|
||||
if (user.id.isNotEmpty) {
|
||||
user.isOnline = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _cleanupOfflineUsers() {
|
||||
final now = DateTime.now();
|
||||
final usersToRemove = <String>[];
|
||||
|
||||
for (final user in _networkUsers.values) {
|
||||
if (user.id != _currentUser.id) {
|
||||
final timeSinceLastSeen = now.difference(user.lastSeen).inSeconds;
|
||||
if (timeSinceLastSeen > _heartbeatInterval * 3) {
|
||||
user.isOnline = false;
|
||||
// Remove users that have been offline for more than 5 minutes
|
||||
if (timeSinceLastSeen > 300) {
|
||||
usersToRemove.add(user.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final userId in usersToRemove) {
|
||||
_networkUsers.remove(userId);
|
||||
}
|
||||
|
||||
if (usersToRemove.isNotEmpty) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// Public methods for UI interaction
|
||||
Future<void> refreshNetwork() async {
|
||||
await _checkConnectivity();
|
||||
if (_isConnectedToWifi) {
|
||||
await _discoverUsers();
|
||||
}
|
||||
}
|
||||
|
||||
void updateUserVotes(int votes) {
|
||||
_currentUser = NetworkUser(
|
||||
id: _currentUser.id,
|
||||
name: _currentUser.name,
|
||||
ipAddress: _currentUser.ipAddress,
|
||||
joinedAt: _currentUser.joinedAt,
|
||||
votes: votes,
|
||||
isOnline: true,
|
||||
);
|
||||
_networkUsers[_currentUser.id] = _currentUser;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
String getConnectionStatus() {
|
||||
if (!_isConnectedToWifi) {
|
||||
return 'Not connected to WiFi';
|
||||
}
|
||||
if (_currentNetworkName.isEmpty) {
|
||||
return 'Connected to WiFi';
|
||||
}
|
||||
return 'Connected to $_currentNetworkName';
|
||||
}
|
||||
|
||||
// Demo methods for testing
|
||||
void simulateNetworkConnection() {
|
||||
_isConnectedToWifi = true;
|
||||
_currentNetworkName = 'Demo WiFi Network';
|
||||
_currentNetworkSSID = '"Demo WiFi Network"';
|
||||
_localIpAddress = '192.168.1.100';
|
||||
_networkSubnet = '192.168.1';
|
||||
|
||||
// Update current user with new IP
|
||||
_currentUser = NetworkUser(
|
||||
id: _currentUser.id,
|
||||
name: _currentUser.name,
|
||||
ipAddress: _localIpAddress,
|
||||
joinedAt: _currentUser.joinedAt,
|
||||
votes: _currentUser.votes,
|
||||
);
|
||||
_networkUsers[_currentUser.id] = _currentUser;
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clearDemoUsers() {
|
||||
_networkUsers.clear();
|
||||
_networkUsers[_currentUser.id] = _currentUser;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void addDemoUser(String name, int votes) {
|
||||
final user = NetworkUser(
|
||||
id: 'demo_${DateTime.now().millisecondsSinceEpoch}_${name.hashCode}',
|
||||
name: name,
|
||||
ipAddress: '192.168.1.${101 + _networkUsers.length}',
|
||||
joinedAt: DateTime.now().subtract(Duration(minutes: (votes * 2))),
|
||||
votes: votes,
|
||||
isOnline: true,
|
||||
);
|
||||
|
||||
_networkUsers[user.id] = user;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_stopNetworkServices();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue