Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions app/lib/features/connect/hotspot_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ library;

import 'dart:async';

import 'package:flutter/foundation.dart' show TargetPlatform, defaultTargetPlatform;
import 'package:flutter/material.dart';

import '../../app.dart';
Expand All @@ -28,6 +29,9 @@ class _HotspotScreenState extends State<HotspotScreen> {
late final Connection _c;
bool _pinTab = false;

/// Android names the network itself; a computer's screen needs a larger code.
bool get _phone => defaultTargetPlatform == TargetPlatform.android;

@override
void initState() {
super.initState();
Expand Down Expand Up @@ -203,7 +207,7 @@ class _HotspotScreenState extends State<HotspotScreen> {
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20)),
// A computer's screen shows it at about 1 mm a module at 230 (real test of
// 29 October 2026): the camera needs to be close, where it cannot focus.
child: SizedBox.square(dimension: _c.hotspot.canJoin ? 230 : 340, child: QrView(key: const Key('my-qr'), data: _c.qrData!)),
child: SizedBox.square(dimension: _phone ? 230 : 340, child: QrView(key: const Key('my-qr'), data: _c.qrData!)),
),
),
const SizedBox(height: 14),
Expand All @@ -219,7 +223,7 @@ class _HotspotScreenState extends State<HotspotScreen> {
_Field(label: l.networkLabel, value: h.ssid ?? '', valueKey: const Key('hotspot-ssid')),
_Field(label: l.passwordLabel, value: h.passphrase ?? '', valueKey: const Key('hotspot-password')),
const SizedBox(height: 4),
Text(_c.hotspot.canJoin ? l.chosenByAndroid : l.chosenByApp, style: TextStyle(color: c.muted, fontSize: 12)),
Text(_phone ? l.chosenByAndroid : l.chosenByApp, style: TextStyle(color: c.muted, fontSize: 12)),
],
),
),
Expand Down
11 changes: 6 additions & 5 deletions app/lib/platform/ble.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,10 @@ abstract interface class Ble {
Future<WifiBands> wifiBands();
}

/// Android: android.bluetooth.le, through the app's Kotlin (Ble.kt).
class AndroidBle implements Ble {
AndroidBle({this.channel = const MethodChannel('dev.medxfer.app/ble')}) {
/// Android (android.bluetooth.le, the app's Kotlin: Ble.kt) and Windows (Windows.Devices.Bluetooth,
/// the runner's C++/WinRT: ble.cpp), by one channel.
class ChannelBle implements Ble {
ChannelBle({this.channel = const MethodChannel('dev.medxfer.app/ble')}) {
channel.setMethodCallHandler((call) async {
if (call.method != 'event') return;
final m = Map<Object?, Object?>.from(call.arguments as Map);
Expand All @@ -100,7 +101,7 @@ class AndroidBle implements Ble {
final _events = StreamController<BleEvent>.broadcast();

@override
bool get supported => Platform.isAndroid;
bool get supported => Platform.isAndroid || Platform.isWindows;

@override
Future<bool> enabled() async => await channel.invokeMethod<bool>('enabled') ?? false;
Expand Down Expand Up @@ -175,4 +176,4 @@ class NoBle implements Ble {
Future<WifiBands> wifiBands() async => (create: 0, join: 0);
}

Ble systemBle() => Platform.isAndroid ? AndroidBle() : const NoBle();
Ble systemBle() => Platform.isAndroid || Platform.isWindows ? ChannelBle() : const NoBle();
31 changes: 24 additions & 7 deletions app/lib/platform/hotspot.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
/// hotspot (WP-D.4 of the J6d plan). Windows and Linux (WP-C.2 of the J6c plan): a network
/// the computer creates at 2.4 GHz, its cards refusing 5 GHz on their own (probe WP-C.1);
/// Windows by Wi-Fi Direct with a name and passphrase of the app's (decision C4), Linux by
/// NetworkManager. A computer does not join another's network itself: the user does, in the
/// system's settings ([Hotspot.canJoin]).
/// NetworkManager. Joining another's network: Android and Windows (WP-D.3 of the J6d plan) do it
/// themselves; on Linux the user does, in the system's settings ([Hotspot.canJoin]).
library;

import 'dart:io';
Expand Down Expand Up @@ -97,8 +97,8 @@ abstract interface class Hotspot {
/// This device creates a network ([start]).
bool get supported;

/// This device joins another's network itself ([join]): Android. A desktop's user joins it
/// in the system's settings.
/// This device joins another's network itself ([join]): Android, Windows. Linux's user joins
/// it in the system's settings.
bool get canJoin;

/// The Wi-Fi this device is on is left while the network runs, and comes back at [stop]:
Expand Down Expand Up @@ -281,6 +281,8 @@ class NoHotspot implements Hotspot {
/// Windows serves the addresses on it, through its connection sharing, and accepts the phones.
/// When that sharing is taken by another card, it serves none: [DhcpServer] does, on the group's
/// address only, and Windows drops each phone after 2 minutes ([HotspotInfo.dropsClients]).
/// Another device's network is joined through Windows' Wi-Fi API (wifi_join.cpp), with a profile
/// of this user's that leaving deletes; the computer then goes back to its network.
class WindowsHotspot implements Hotspot {
WindowsHotspot({this.channel = const MethodChannel('dev.medxfer.app/hotspot'), AddressServer? addressServer})
: _addressServer = addressServer ?? _dhcpServer;
Expand All @@ -293,7 +295,7 @@ class WindowsHotspot implements Hotspot {
bool get supported => true;

@override
bool get canJoin => false;
bool get canJoin => true;

@override
bool get leavesWifi => false;
Expand Down Expand Up @@ -339,11 +341,26 @@ class WindowsHotspot implements Hotspot {
}

@override
Future<HotspotInfo> join(String ssid, String passphrase, {int security = 1, WifiBand? band}) => throw UnsupportedError('a computer joins in the system settings');
Future<HotspotInfo> join(String ssid, String passphrase, {int security = 1, WifiBand? band}) async {
final m = (await channel.invokeMapMethod<String, Object?>(
'join', {'ssid': ssid, 'passphrase': passphrase, 'security': security, if (band != null) 'band': band.code}))!;
final address = '${m['address']}';
final prefix = (m['prefix'] as num?)?.toInt() ?? 24;
return HotspotInfo(
ssid: ssid,
passphrase: passphrase,
security: security,
address: address,
interfaces: ['${m['name']}'],
net: NetInterface(name: '${m['name']}', index: (m['index'] as num?)?.toInt() ?? 0, addrs: ['$address/$prefix']),
band: WifiBand.of((m['frequency'] as num?)?.toInt() ?? 0),
);
}

@override
Future<void> leave() async {}
Future<void> leave() => channel.invokeMethod<void>('leave');

/// The core's sockets follow Windows' routes: the card that joined is the computer's Wi-Fi.
@override
Future<int> joinedNetwork() async => 0;

Expand Down
20 changes: 20 additions & 0 deletions app/test/platform/hotspot_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,26 @@ void main() {
expect(served.last, 'closed');
expect(calls, ['start', 'stop']);
});

test('WP-D.3: joins another device\'s network itself, its band said; leaving goes back', () async {
Object? asked;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, (call) async {
calls.add(call.method);
if (call.method == 'join') asked = call.arguments;
return switch (call.method) {
'join' => {'name': 'Wi-Fi', 'index': 12, 'address': '192.168.49.23', 'prefix': 24, 'frequency': 5745},
_ => null,
};
});
final h = WindowsHotspot(addressServer: (server, prefix) async => () {});
expect(h.canJoin, isTrue);
final info = await h.join('DIRECT-ab-Galaxy', 'K3PQ8Z2M', security: 1, band: WifiBand.ghz5);
expect(asked, {'ssid': 'DIRECT-ab-Galaxy', 'passphrase': 'K3PQ8Z2M', 'security': 1, 'band': '5'});
expect((info.address, info.band, info.net!.index), ('192.168.49.23', WifiBand.ghz5, 12));
expect(info.net!.addrs, ['192.168.49.23/24']);
await h.leave();
expect(calls, ['join', 'leave']);
});
});

group('WP-D.4: Android, the Wi-Fi Direct group at 5 GHz or the local hotspot', () {
Expand Down
4 changes: 4 additions & 0 deletions app/windows/runner/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ project(runner LANGUAGES CXX)
#
# Any new source files that you add to the application should be added here.
add_executable(${BINARY_NAME} WIN32
"ble.cpp"
"firewall.cpp"
"flutter_window.cpp"
"hotspot.cpp"
"main.cpp"
"utils.cpp"
"wifi_join.cpp"
"win32_window.cpp"
"wired.cpp"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
Expand Down Expand Up @@ -42,6 +44,8 @@ target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
target_link_libraries(${BINARY_NAME} PRIVATE "windowsapp.lib" "iphlpapi.lib" "ws2_32.lib")
# Windows' firewall and its networks (firewall.cpp): COM, BSTR and VARIANT.
target_link_libraries(${BINARY_NAME} PRIVATE "ole32.lib" "oleaut32.lib")
# Joining another device's network (wifi_join.cpp): Windows' Wi-Fi API.
target_link_libraries(${BINARY_NAME} PRIVATE "wlanapi.lib")
target_compile_features(${BINARY_NAME} PRIVATE cxx_std_20)
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")

Expand Down
Loading
Loading