From 46bb6f9895b5862a3036afb7bd22955287325611 Mon Sep 17 00:00:00 2001 From: kauzu Date: Sun, 26 Jun 2022 13:34:45 +0200 Subject: [PATCH 01/16] apk buildfix / update share_plus dependency --- openhaystack-mobile/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openhaystack-mobile/pubspec.yaml b/openhaystack-mobile/pubspec.yaml index 7caadf5..2b1779a 100644 --- a/openhaystack-mobile/pubspec.yaml +++ b/openhaystack-mobile/pubspec.yaml @@ -57,7 +57,7 @@ dependencies: # Sharing receive_sharing_intent: ^1.4.5 - share_plus: ^3.0.4 + share_plus: ^4.0.4 url_launcher: ^6.0.17 path_provider: ^2.0.8 maps_launcher: ^2.0.1 From 7d095322396d33aa088a28dec4dfa3092670f192 Mon Sep 17 00:00:00 2001 From: kauzu Date: Sun, 26 Jun 2022 13:40:56 +0200 Subject: [PATCH 02/16] apk buildfix / update pubspec --- openhaystack-mobile/pubspec.lock | 120 +++++++++++++++---------------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/openhaystack-mobile/pubspec.lock b/openhaystack-mobile/pubspec.lock index 1b7d8a6..237b3e7 100644 --- a/openhaystack-mobile/pubspec.lock +++ b/openhaystack-mobile/pubspec.lock @@ -7,14 +7,14 @@ packages: name: archive url: "https://pub.dartlang.org" source: hosted - version: "3.2.1" + version: "3.3.0" args: dependency: transitive description: name: args url: "https://pub.dartlang.org" source: hosted - version: "2.3.0" + version: "2.3.1" async: dependency: transitive description: @@ -56,35 +56,35 @@ packages: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0" + version: "1.16.0" convert: dependency: transitive description: name: convert url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.0.2" crypto: dependency: transitive description: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.0.2" fake_async: dependency: transitive description: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.0" ffi: dependency: transitive description: name: ffi url: "https://pub.dartlang.org" source: hosted - version: "1.1.2" + version: "1.2.1" file: dependency: transitive description: @@ -98,7 +98,7 @@ packages: name: file_picker url: "https://pub.dartlang.org" source: hosted - version: "4.4.0" + version: "4.6.1" flutter: dependency: "direct main" description: flutter @@ -117,7 +117,7 @@ packages: name: flutter_launcher_icons url: "https://pub.dartlang.org" source: hosted - version: "0.9.2" + version: "0.9.3" flutter_lints: dependency: "direct dev" description: @@ -138,7 +138,7 @@ packages: name: flutter_plugin_android_lifecycle url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.0.6" flutter_secure_storage: dependency: "direct main" description: @@ -187,7 +187,7 @@ packages: name: flutter_slidable url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.0" flutter_test: dependency: "direct dev" description: flutter @@ -204,7 +204,7 @@ packages: name: geocoding url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.4" geocoding_platform_interface: dependency: transitive description: @@ -225,14 +225,14 @@ packages: name: http_parser url: "https://pub.dartlang.org" source: hosted - version: "4.0.0" + version: "4.0.1" image: dependency: transitive description: name: image url: "https://pub.dartlang.org" source: hosted - version: "3.1.3" + version: "3.2.0" intl: dependency: transitive description: @@ -246,7 +246,7 @@ packages: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.3" + version: "0.6.4" latlong2: dependency: transitive description: @@ -274,7 +274,7 @@ packages: name: location url: "https://pub.dartlang.org" source: hosted - version: "4.3.0" + version: "4.4.0" location_platform_interface: dependency: transitive description: @@ -309,7 +309,7 @@ packages: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.3" + version: "0.1.4" meta: dependency: transitive description: @@ -330,7 +330,7 @@ packages: name: mime url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.2" nested: dependency: transitive description: @@ -344,63 +344,63 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" path_provider: dependency: "direct main" description: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "2.0.9" + version: "2.0.11" path_provider_android: dependency: transitive description: name: path_provider_android url: "https://pub.dartlang.org" source: hosted - version: "2.0.11" + version: "2.0.15" path_provider_ios: dependency: transitive description: name: path_provider_ios url: "https://pub.dartlang.org" source: hosted - version: "2.0.7" + version: "2.0.10" path_provider_linux: dependency: transitive description: name: path_provider_linux url: "https://pub.dartlang.org" source: hosted - version: "2.1.5" + version: "2.1.7" path_provider_macos: dependency: transitive description: name: path_provider_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.0.6" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.0.4" path_provider_windows: dependency: transitive description: name: path_provider_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.0.7" petitparser: dependency: transitive description: name: petitparser url: "https://pub.dartlang.org" source: hosted - version: "4.4.0" + version: "5.0.0" platform: dependency: transitive description: @@ -421,7 +421,7 @@ packages: name: pointycastle url: "https://pub.dartlang.org" source: hosted - version: "3.5.1" + version: "3.6.0" positioned_tap_detector_2: dependency: transitive description: @@ -449,14 +449,14 @@ packages: name: provider url: "https://pub.dartlang.org" source: hosted - version: "6.0.2" + version: "6.0.3" quiver: dependency: transitive description: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "3.0.1+1" + version: "3.1.0" receive_sharing_intent: dependency: "direct main" description: @@ -470,77 +470,77 @@ packages: name: share_plus url: "https://pub.dartlang.org" source: hosted - version: "3.1.0" + version: "4.0.9" share_plus_linux: dependency: transitive description: name: share_plus_linux url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "3.0.0" share_plus_macos: dependency: transitive description: name: share_plus_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "3.0.1" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "3.0.3" share_plus_web: dependency: transitive description: name: share_plus_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "3.0.1" share_plus_windows: dependency: transitive description: name: share_plus_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "3.0.1" shared_preferences: dependency: "direct main" description: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "2.0.13" + version: "2.0.15" shared_preferences_android: dependency: transitive description: name: shared_preferences_android url: "https://pub.dartlang.org" source: hosted - version: "2.0.10" + version: "2.0.12" shared_preferences_ios: dependency: transitive description: name: shared_preferences_ios url: "https://pub.dartlang.org" source: hosted - version: "2.0.9" + version: "2.1.1" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.0.4" shared_preferences_platform_interface: dependency: transitive description: @@ -554,14 +554,14 @@ packages: name: shared_preferences_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.0.4" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" sky_engine: dependency: transitive description: flutter @@ -573,7 +573,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" stack_trace: dependency: transitive description: @@ -608,7 +608,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.8" + version: "0.4.9" transparent_image: dependency: transitive description: @@ -629,7 +629,7 @@ packages: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.3.1" unicode: dependency: transitive description: @@ -643,70 +643,70 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "6.0.20" + version: "6.1.4" url_launcher_android: dependency: transitive description: name: url_launcher_android url: "https://pub.dartlang.org" source: hosted - version: "6.0.14" + version: "6.0.17" url_launcher_ios: dependency: transitive description: name: url_launcher_ios url: "https://pub.dartlang.org" source: hosted - version: "6.0.14" + version: "6.0.17" url_launcher_linux: dependency: transitive description: name: url_launcher_linux url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.1.0" url_launcher_web: dependency: transitive description: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.7" + version: "2.0.12" url_launcher_windows: dependency: transitive description: name: url_launcher_windows url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.2" win32: dependency: transitive description: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.3.6" + version: "2.6.1" wkt_parser: dependency: transitive description: @@ -727,14 +727,14 @@ packages: name: xml url: "https://pub.dartlang.org" source: hosted - version: "5.3.1" + version: "6.1.0" yaml: dependency: transitive description: name: yaml url: "https://pub.dartlang.org" source: hosted - version: "3.1.0" + version: "3.1.1" sdks: - dart: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + dart: ">=2.17.0 <3.0.0" + flutter: ">=3.0.0" From 05a634fd1e0b29f828aff845a65cdc101f931acf Mon Sep 17 00:00:00 2001 From: kauzu Date: Sun, 26 Jun 2022 13:42:23 +0200 Subject: [PATCH 03/16] reports_fetcher url add semilolon --- openhaystack-mobile/lib/findMy/reports_fetcher.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openhaystack-mobile/lib/findMy/reports_fetcher.dart b/openhaystack-mobile/lib/findMy/reports_fetcher.dart index 8bffff8..f0a7b10 100644 --- a/openhaystack-mobile/lib/findMy/reports_fetcher.dart +++ b/openhaystack-mobile/lib/findMy/reports_fetcher.dart @@ -3,7 +3,7 @@ import 'dart:convert'; import 'package:http/http.dart' as http; class ReportsFetcher { - static const _seemooEndpoint = "https://add-your-proxy-server-here/getLocationReports" + static const _seemooEndpoint = "https://add-your-proxy-server-here/getLocationReports"; /// Fetches the location reports corresponding to the given hashed advertisement /// key. From f2f9adcf280d40d36baaaa462aaba70d65e67e00 Mon Sep 17 00:00:00 2001 From: kauzu Date: Tue, 28 Jun 2022 23:38:01 +0200 Subject: [PATCH 04/16] Silence linux beacon --- Firmware/Linux_HCI/HCI.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Firmware/Linux_HCI/HCI.py b/Firmware/Linux_HCI/HCI.py index d38878f..888d075 100755 --- a/Firmware/Linux_HCI/HCI.py +++ b/Firmware/Linux_HCI/HCI.py @@ -32,7 +32,7 @@ def bytes_to_strarray(bytes_, with_prefix=False): def run_hci_cmd(cmd, hci="hci0", wait=1): cmd_ = ["hcitool", "-i", hci, "cmd"] cmd_ += cmd - print(cmd_) + #print(cmd_) subprocess.run(cmd_) if wait > 0: time.sleep(wait) @@ -46,9 +46,9 @@ def start_advertising(key, interval_ms=2000): adv[7:29] = key[6:28] adv[29] = key[0] >> 6 - print(f"key ({len(key):2}) {key.hex()}") - print(f"address ({len(addr):2}) {addr.hex()}") - print(f"payload ({len(adv):2}) {adv.hex()}") + #print(f"key ({len(key):2}) {key.hex()}") + #print(f"address ({len(addr):2}) {addr.hex()}") + #print(f"payload ({len(adv):2}) {adv.hex()}") # Set BLE address run_hci_cmd(["0x3f", "0x001"] + bytes_to_strarray(addr, with_prefix=True)[::-1]) From fb209e8930a19e91180a08fb113b71204c81e941 Mon Sep 17 00:00:00 2001 From: kauzu Date: Tue, 28 Jun 2022 23:55:03 +0200 Subject: [PATCH 05/16] Permanent advertising --- Firmware/Linux_HCI/HCI.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Firmware/Linux_HCI/HCI.py b/Firmware/Linux_HCI/HCI.py index 888d075..e50c62f 100755 --- a/Firmware/Linux_HCI/HCI.py +++ b/Firmware/Linux_HCI/HCI.py @@ -37,6 +37,10 @@ def run_hci_cmd(cmd, hci="hci0", wait=1): if wait > 0: time.sleep(wait) +def run_hciconf_leadv_3(hci="hci0"): + #EDIT: the above command makes the advertised service connectable. If you don't want to allow connections, change it to $ sudo hciconfig hci0 leadv 3 + cmd_ = ["hciconfig", hci, "leadv", "3"] + subprocess.run(cmd_) def start_advertising(key, interval_ms=2000): addr = bytearray(key[:6]) @@ -53,7 +57,7 @@ def start_advertising(key, interval_ms=2000): # Set BLE address run_hci_cmd(["0x3f", "0x001"] + bytes_to_strarray(addr, with_prefix=True)[::-1]) subprocess.run(["systemctl", "restart", "bluetooth"]) - time.sleep(1) + time.sleep(3) # Set BLE advertisement payload run_hci_cmd(["0x08", "0x0008"] + [format(len(adv), "x")] + bytes_to_strarray(adv)) @@ -70,6 +74,8 @@ def start_advertising(key, interval_ms=2000): # Start BLE advertising run_hci_cmd(["0x08", "0x000a"] + ["01"], wait=0) + run_hciconf_leadv_3() + def main(args): parser = argparse.ArgumentParser() From 482aad8d394c532dbc3d31393b9d9c8e06cb89f2 Mon Sep 17 00:00:00 2001 From: kauzu Date: Wed, 29 Jun 2022 17:34:56 +0200 Subject: [PATCH 06/16] remove isolate --- .../lib/findMy/decrypt_reports.dart | 31 ++++---- .../lib/findMy/find_my_controller.dart | 72 ++++++++++--------- 2 files changed, 56 insertions(+), 47 deletions(-) diff --git a/openhaystack-mobile/lib/findMy/decrypt_reports.dart b/openhaystack-mobile/lib/findMy/decrypt_reports.dart index f8fefa4..f1a0276 100644 --- a/openhaystack-mobile/lib/findMy/decrypt_reports.dart +++ b/openhaystack-mobile/lib/findMy/decrypt_reports.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'dart:isolate'; import 'dart:typed_data'; import 'package:pointycastle/export.dart'; @@ -19,9 +18,8 @@ class DecryptReports { _decodeTimeAndConfidence(payloadData, report); - final privateKey = ECPrivateKey( - pc_utils.decodeBigIntWithSign(1, key), - curveDomainParam); + final privateKey = + ECPrivateKey(pc_utils.decodeBigIntWithSign(1, key), curveDomainParam); final decodePoint = curveDomainParam.curve.decodePoint(ephemeralKeyBytes); final ephemeralPublicKey = ECPublicKey(decodePoint, curveDomainParam); @@ -36,9 +34,10 @@ class DecryptReports { } /// Decodes the unencrypted timestamp and confidence - static void _decodeTimeAndConfidence(Uint8List payloadData, FindMyReport report) { - final seenTimeStamp = payloadData.sublist(0, 4).buffer.asByteData() - .getInt32(0, Endian.big); + static void _decodeTimeAndConfidence( + Uint8List payloadData, FindMyReport report) { + final seenTimeStamp = + payloadData.sublist(0, 4).buffer.asByteData().getInt32(0, Endian.big); final timestamp = DateTime(2001).add(Duration(seconds: seenTimeStamp)); final confidence = payloadData.elementAt(4); report.timestamp = timestamp; @@ -47,11 +46,12 @@ class DecryptReports { /// Performs an Elliptic Curve Diffie-Hellman with the given keys. /// Returns the derived raw key data. - static Uint8List _ecdh(ECPublicKey ephemeralPublicKey, ECPrivateKey privateKey) { + static Uint8List _ecdh( + ECPublicKey ephemeralPublicKey, ECPrivateKey privateKey) { final sharedKey = ephemeralPublicKey.Q! * privateKey.d; - final sharedKeyBytes = pc_utils.encodeBigIntAsUnsigned( - sharedKey!.x!.toBigInteger()!); - print("Isolate:${Isolate.current.hashCode}: Shared Key (shared secret): ${base64Encode(sharedKeyBytes)}"); + final sharedKeyBytes = + pc_utils.encodeBigIntAsUnsigned(sharedKey!.x!.toBigInteger()!); + print("Shared Key (shared secret): ${base64Encode(sharedKeyBytes)}"); return sharedKeyBytes; } @@ -60,7 +60,6 @@ class DecryptReports { /// the resulting [FindMyLocationReport]. static FindMyLocationReport _decodePayload( Uint8List payload, FindMyReport report) { - final latitude = payload.buffer.asByteData(0, 4).getUint32(0, Endian.big); final longitude = payload.buffer.asByteData(4, 4).getUint32(0, Endian.big); final accuracy = payload.buffer.asByteData(8, 1).getUint8(0); @@ -80,8 +79,10 @@ class DecryptReports { final iv = symmetricKey.sublist(16, symmetricKey.length); final aesGcm = GCMBlockCipher(AESEngine()) - ..init(false, AEADParameters(KeyParameter(decryptionKey), - tag.lengthInBytes * 8, iv, tag)); + ..init( + false, + AEADParameters( + KeyParameter(decryptionKey), tag.lengthInBytes * 8, iv, tag)); final plainText = Uint8List(cipherText.length); var offset = 0; @@ -109,7 +110,7 @@ class DecryptReports { Uint8List out = Uint8List(shaDigest.digestSize); shaDigest.doFinal(out, 0); - print("Isolate:${Isolate.current.hashCode}: Derived key: ${base64Encode(out)}"); + print("Derived key: ${base64Encode(out)}"); return out; } } diff --git a/openhaystack-mobile/lib/findMy/find_my_controller.dart b/openhaystack-mobile/lib/findMy/find_my_controller.dart index 1b1abe4..13e03d9 100644 --- a/openhaystack-mobile/lib/findMy/find_my_controller.dart +++ b/openhaystack-mobile/lib/findMy/find_my_controller.dart @@ -1,6 +1,5 @@ import 'dart:collection'; import 'dart:convert'; -import 'dart:isolate'; import 'dart:typed_data'; import 'package:flutter/foundation.dart'; @@ -14,13 +13,14 @@ import 'package:openhaystack_mobile/findMy/reports_fetcher.dart'; class FindMyController { static const _storage = FlutterSecureStorage(); - static final ECCurve_secp224r1 _curveParams = ECCurve_secp224r1(); + static final ECCurve_secp224r1 _curveParams = ECCurve_secp224r1(); static HashMap _keyCache = HashMap(); /// Starts a new [Isolate], fetches and decrypts all location reports /// for the given [FindMyKeyPair]. /// Returns a list of [FindMyLocationReport]'s. - static Future> computeResults(FindMyKeyPair keyPair) async{ + static Future> computeResults( + FindMyKeyPair keyPair) async { await _loadPrivateKey(keyPair); return compute(_getListedReportResults, keyPair); } @@ -28,11 +28,14 @@ class FindMyController { /// Fetches and decrypts the location reports for the given /// [FindMyKeyPair] from apples FindMy Network. /// Returns a list of [FindMyLocationReport]. - static Future> _getListedReportResults(FindMyKeyPair keyPair) async{ + static Future> _getListedReportResults( + FindMyKeyPair keyPair) async { List results = []; - final jsonResults = await ReportsFetcher.fetchLocationReports(keyPair.getHashedAdvertisementKey()); + final jsonResults = await ReportsFetcher.fetchLocationReports( + keyPair.getHashedAdvertisementKey()); for (var result in jsonResults) { - results.add(await _decryptResult(result, keyPair, keyPair.privateKeyBase64!)); + results.add( + await _decryptResult(result, keyPair, keyPair.privateKeyBase64!)); } return results; } @@ -43,7 +46,8 @@ class FindMyController { String? privateKey; if (!_keyCache.containsKey(keyPair.hashedPublicKey)) { privateKey = await _storage.read(key: keyPair.hashedPublicKey); - final newKey = _keyCache.putIfAbsent(keyPair.hashedPublicKey, () => privateKey); + final newKey = + _keyCache.putIfAbsent(keyPair.hashedPublicKey, () => privateKey); assert(newKey == privateKey); } else { privateKey = _keyCache[keyPair.hashedPublicKey]; @@ -55,27 +59,29 @@ class FindMyController { static ECPublicKey _derivePublicKey(ECPrivateKey privateKey) { final pk = _curveParams.G * privateKey.d; final publicKey = ECPublicKey(pk, _curveParams); - print("Isolate:${Isolate.current.hashCode}: Point Data: ${base64Encode(publicKey.Q!.getEncoded(false))}"); + print("Point Data: ${base64Encode(publicKey.Q!.getEncoded(false))}"); return publicKey; } /// Decrypts the encrypted reports with the given [FindMyKeyPair] and private key. /// Returns the decrypted report as a [FindMyLocationReport]. - static Future _decryptResult(dynamic result, FindMyKeyPair keyPair, String privateKey) async { - assert (result["id"]! == keyPair.getHashedAdvertisementKey(), - "Returned FindMyReport hashed key != requested hashed key"); - - final unixTimestampInMillis = result["datePublished"]; - final datePublished = DateTime.fromMillisecondsSinceEpoch(unixTimestampInMillis); + static Future _decryptResult( + dynamic result, FindMyKeyPair keyPair, String privateKey) async { + assert(result["id"]! == keyPair.getHashedAdvertisementKey(), + "Returned FindMyReport hashed key != requested hashed key"); + + final unixTimestampInMillis = result["datePublished"]; + final datePublished = + DateTime.fromMillisecondsSinceEpoch(unixTimestampInMillis); FindMyReport report = FindMyReport( datePublished, base64Decode(result["payload"]), keyPair.getHashedAdvertisementKey(), result["statusCode"]); - FindMyLocationReport decryptedReport = await DecryptReports - .decryptReport(report, base64Decode(privateKey)); + FindMyLocationReport decryptedReport = + await DecryptReports.decryptReport(report, base64Decode(privateKey)); return decryptedReport; } @@ -86,10 +92,12 @@ class FindMyController { final privateKeyBase64 = await _storage.read(key: base64HashedPublicKey); ECPrivateKey privateKey = ECPrivateKey( - pc_utils.decodeBigIntWithSign(1, base64Decode(privateKeyBase64!)), _curveParams); + pc_utils.decodeBigIntWithSign(1, base64Decode(privateKeyBase64!)), + _curveParams); ECPublicKey publicKey = _derivePublicKey(privateKey); - return FindMyKeyPair(publicKey, base64HashedPublicKey, privateKey, DateTime.now(), -1); + return FindMyKeyPair( + publicKey, base64HashedPublicKey, privateKey, DateTime.now(), -1); } /// Imports a base64 encoded private key to the local [FlutterSecureStorage]. @@ -101,14 +109,11 @@ class FindMyController { final ECPublicKey publicKey = _derivePublicKey(privateKey); final hashedPublicKey = getHashedPublicKey(publicKey: publicKey); final keyPair = FindMyKeyPair( - publicKey, - hashedPublicKey, - privateKey, - DateTime.now(), - -1); - - await _storage.write(key: hashedPublicKey, value: keyPair.getBase64PrivateKey()); - + publicKey, hashedPublicKey, privateKey, DateTime.now(), -1); + + await _storage.write( + key: hashedPublicKey, value: keyPair.getBase64PrivateKey()); + return keyPair; } @@ -117,16 +122,18 @@ class FindMyController { static Future generateKeyPair() async { final ecCurve = ECCurve_secp224r1(); final secureRandom = SecureRandom('Fortuna') - ..seed(KeyParameter( - Platform.instance.platformEntropySource().getBytes(32))); + ..seed( + KeyParameter(Platform.instance.platformEntropySource().getBytes(32))); ECKeyGenerator keyGen = ECKeyGenerator() - ..init(ParametersWithRandom(ECKeyGeneratorParameters(ecCurve), secureRandom)); + ..init(ParametersWithRandom( + ECKeyGeneratorParameters(ecCurve), secureRandom)); final newKeyPair = keyGen.generateKeyPair(); final ECPublicKey publicKey = newKeyPair.publicKey as ECPublicKey; final ECPrivateKey privateKey = newKeyPair.privateKey as ECPrivateKey; final hashedKey = getHashedPublicKey(publicKey: publicKey); - final keyPair = FindMyKeyPair(publicKey, hashedKey, privateKey, DateTime.now(), -1); + final keyPair = + FindMyKeyPair(publicKey, hashedKey, privateKey, DateTime.now(), -1); await _storage.write(key: hashedKey, value: keyPair.getBase64PrivateKey()); return keyPair; @@ -135,7 +142,8 @@ class FindMyController { /// Returns hashed, base64 encoded public key for given [publicKeyBytes] /// or for an [ECPublicKey] object [publicKey], if [publicKeyBytes] equals null. /// Returns the base64 encoded hashed public key as a [String]. - static String getHashedPublicKey({Uint8List? publicKeyBytes, ECPublicKey? publicKey}) { + static String getHashedPublicKey( + {Uint8List? publicKeyBytes, ECPublicKey? publicKey}) { var pkBytes = publicKeyBytes ?? publicKey!.Q!.getEncoded(false); final shaDigest = SHA256Digest(); shaDigest.update(pkBytes, 0, pkBytes.lengthInBytes); @@ -143,4 +151,4 @@ class FindMyController { shaDigest.doFinal(out, 0); return base64Encode(out); } -} \ No newline at end of file +} From 955022fde610a2a30574e62be0f943ee2944016b Mon Sep 17 00:00:00 2001 From: kauzu Date: Wed, 29 Jun 2022 17:55:27 +0200 Subject: [PATCH 07/16] ReceiveSharingIntent only on Android/IOS --- openhaystack-mobile/lib/main.dart | 32 ++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/openhaystack-mobile/lib/main.dart b/openhaystack-mobile/lib/main.dart index 196275b..7f458b8 100644 --- a/openhaystack-mobile/lib/main.dart +++ b/openhaystack-mobile/lib/main.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; @@ -33,7 +34,7 @@ class MyApp extends StatelessWidget { primarySwatch: Colors.blue, ), darkTheme: ThemeData.dark(), - home: const AppLayout(), + home: const AppLayout(), ), ); } @@ -53,16 +54,20 @@ class _AppLayoutState extends State { initState() { super.initState(); - _intentDataStreamSubscription = ReceiveSharingIntent.getMediaStream() - .listen(handleFileSharingIntent, onError: print); - ReceiveSharingIntent.getInitialMedia() - .then(handleFileSharingIntent); + if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { + //Only supported on this platforms according to + //https://pub.dev/packages/receive_sharing_intent + _intentDataStreamSubscription = ReceiveSharingIntent.getMediaStream() + .listen(handleFileSharingIntent, onError: print); + ReceiveSharingIntent.getInitialMedia().then(handleFileSharingIntent); + } - var accessoryRegistry = Provider.of(context, listen: false); + var accessoryRegistry = + Provider.of(context, listen: false); accessoryRegistry.loadAccessories(); } - Future handleFileSharingIntent(List files) async { + Future handleFileSharingIntent(List files) async { // Received a sharing intent with a number of files. // Import the accessories for each device in sequence. // If no files are shared do nothing @@ -70,11 +75,13 @@ class _AppLayoutState extends State { if (file.type == SharedMediaType.FILE) { // On iOS the file:// prefix has to be stripped to access the file path String path = Platform.isIOS - ? Uri.decodeComponent(file.path.replaceFirst('file://', '')) - : file.path; - Navigator.push(context, MaterialPageRoute( - builder: (context) => ItemFileImport(filePath: path), - )); + ? Uri.decodeComponent(file.path.replaceFirst('file://', '')) + : file.path; + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ItemFileImport(filePath: path), + )); } } } @@ -92,7 +99,6 @@ class _AppLayoutState extends State { super.didChangeDependencies(); } - @override Widget build(BuildContext context) { bool isInitialized = context.watch().initialized; From b8139e6b762e7de3a77b0088d994731ab1539fd8 Mon Sep 17 00:00:00 2001 From: kauzu Date: Wed, 29 Jun 2022 20:13:44 +0200 Subject: [PATCH 08/16] Use web compatible crypto library --- .../lib/findMy/decrypt_reports.dart | 21 ++++++++++++++++--- openhaystack-mobile/pubspec.yaml | 3 ++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/openhaystack-mobile/lib/findMy/decrypt_reports.dart b/openhaystack-mobile/lib/findMy/decrypt_reports.dart index f1a0276..13a7acb 100644 --- a/openhaystack-mobile/lib/findMy/decrypt_reports.dart +++ b/openhaystack-mobile/lib/findMy/decrypt_reports.dart @@ -1,10 +1,13 @@ import 'dart:convert'; import 'dart:typed_data'; +import 'package:flutter/foundation.dart'; import 'package:pointycastle/export.dart'; import 'package:pointycastle/src/utils.dart' as pc_utils; import 'package:openhaystack_mobile/findMy/models.dart'; +import 'package:cryptography/cryptography.dart' as nice_crypto; + class DecryptReports { /// Decrypts a given [FindMyReport] with the given private key. static Future decryptReport( @@ -27,7 +30,7 @@ class DecryptReports { final Uint8List sharedKeyBytes = _ecdh(ephemeralPublicKey, privateKey); final Uint8List derivedKey = _kdf(sharedKeyBytes, ephemeralKeyBytes); - final decryptedPayload = _decryptPayload(encData, derivedKey, tag); + final decryptedPayload = await _decryptPayload(encData, derivedKey, tag); final locationReport = _decodePayload(decryptedPayload, report); return locationReport; @@ -73,10 +76,22 @@ class DecryptReports { /// Decrypts the given cipher text with the key data using an AES-GCM block cipher. /// Returns the decrypted raw data. - static Uint8List _decryptPayload( - Uint8List cipherText, Uint8List symmetricKey, Uint8List tag) { + static Future _decryptPayload( + Uint8List cipherText, Uint8List symmetricKey, Uint8List tag) async { final decryptionKey = symmetricKey.sublist(0, 16); final iv = symmetricKey.sublist(16, symmetricKey.length); + if (kIsWeb) { + nice_crypto.SecretKey secretKey = + new nice_crypto.SecretKey(decryptionKey); + + nice_crypto.SecretBox secretBox = new nice_crypto.SecretBox(cipherText, + nonce: iv, mac: nice_crypto.Mac(tag)); + + List decrypted = await nice_crypto.AesGcm.with128bits() + .decrypt(secretBox, secretKey: secretKey); + print(decrypted); + return Uint8List.fromList(decrypted); + } final aesGcm = GCMBlockCipher(AESEngine()) ..init( diff --git a/openhaystack-mobile/pubspec.yaml b/openhaystack-mobile/pubspec.yaml index 2b1779a..11671a0 100644 --- a/openhaystack-mobile/pubspec.yaml +++ b/openhaystack-mobile/pubspec.yaml @@ -40,7 +40,8 @@ dependencies: # Cryptography # latest version of pointy castle for crypto functions - pointycastle: ^3.4.0 + pointycastle: ^3.6.0 + cryptography: ^2.0.5 # State Management provider: ^6.0.1 From 3bdaf724f37ff30c14c27dc1401ad3ed79dc9194 Mon Sep 17 00:00:00 2001 From: kauzu Date: Wed, 29 Jun 2022 21:17:10 +0200 Subject: [PATCH 09/16] Some exception we can ignore i guess --- openhaystack-mobile/lib/findMy/decrypt_reports.dart | 2 +- openhaystack-mobile/lib/findMy/find_my_controller.dart | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/openhaystack-mobile/lib/findMy/decrypt_reports.dart b/openhaystack-mobile/lib/findMy/decrypt_reports.dart index 13a7acb..1ab0486 100644 --- a/openhaystack-mobile/lib/findMy/decrypt_reports.dart +++ b/openhaystack-mobile/lib/findMy/decrypt_reports.dart @@ -89,7 +89,7 @@ class DecryptReports { List decrypted = await nice_crypto.AesGcm.with128bits() .decrypt(secretBox, secretKey: secretKey); - print(decrypted); + return Uint8List.fromList(decrypted); } diff --git a/openhaystack-mobile/lib/findMy/find_my_controller.dart b/openhaystack-mobile/lib/findMy/find_my_controller.dart index 13e03d9..bc52e90 100644 --- a/openhaystack-mobile/lib/findMy/find_my_controller.dart +++ b/openhaystack-mobile/lib/findMy/find_my_controller.dart @@ -34,8 +34,12 @@ class FindMyController { final jsonResults = await ReportsFetcher.fetchLocationReports( keyPair.getHashedAdvertisementKey()); for (var result in jsonResults) { - results.add( - await _decryptResult(result, keyPair, keyPair.privateKeyBase64!)); + try { + results.add( + await _decryptResult(result, keyPair, keyPair.privateKeyBase64!)); + } catch (e) { + print(e); + } } return results; } From 6a8b78f7cb43bd0e6ff851f426b051fb7ac053de Mon Sep 17 00:00:00 2001 From: Andrea Baccega Date: Sun, 14 Aug 2022 00:37:51 +0200 Subject: [PATCH 10/16] fix desktop refresh indicator fix desktop "click on accessory" -> center map fix fitBounds when no data available (causes freeze on android first run) update deps (Attempt to make this working in web.) --- .../lib/accessory/accessory_list.dart | 11 ++++++++--- .../lib/dashboard/dashboard_desktop.dart | 15 +++++++++++++-- openhaystack-mobile/lib/map/map.dart | 13 ++++++++----- openhaystack-mobile/pubspec.lock | 2 +- openhaystack-mobile/pubspec.yaml | 2 +- 5 files changed, 31 insertions(+), 12 deletions(-) diff --git a/openhaystack-mobile/lib/accessory/accessory_list.dart b/openhaystack-mobile/lib/accessory/accessory_list.dart index 481025f..f5bc8e7 100644 --- a/openhaystack-mobile/lib/accessory/accessory_list.dart +++ b/openhaystack-mobile/lib/accessory/accessory_list.dart @@ -1,5 +1,6 @@ import 'dart:math'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; @@ -63,12 +64,16 @@ class _AccessoryListState extends State { return const NoAccessoriesPlaceholder(); } - // TODO: Refresh Indicator for desktop - // Use pull to refresh method return SlidableAutoCloseBehavior(child: RefreshIndicator( onRefresh: widget.loadLocationUpdates, - child: Scrollbar( + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith( + dragDevices: { + PointerDeviceKind.touch, + PointerDeviceKind.mouse, + }, + ), child: ListView( children: accessories.map((accessory) { // Calculate distance from users devices location diff --git a/openhaystack-mobile/lib/dashboard/dashboard_desktop.dart b/openhaystack-mobile/lib/dashboard/dashboard_desktop.dart index b7aa620..d25be2f 100644 --- a/openhaystack-mobile/lib/dashboard/dashboard_desktop.dart +++ b/openhaystack-mobile/lib/dashboard/dashboard_desktop.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:provider/provider.dart'; import 'package:openhaystack_mobile/accessory/accessory_list.dart'; import 'package:openhaystack_mobile/accessory/accessory_registry.dart'; @@ -6,6 +7,7 @@ import 'package:openhaystack_mobile/location/location_model.dart'; import 'package:openhaystack_mobile/map/map.dart'; import 'package:openhaystack_mobile/preferences/preferences_page.dart'; import 'package:openhaystack_mobile/preferences/user_preferences_model.dart'; +import 'package:latlong2/latlong.dart'; class DashboardDesktop extends StatefulWidget { @@ -20,7 +22,13 @@ class DashboardDesktop extends StatefulWidget { } class _DashboardDesktopState extends State { + final MapController _mapController = MapController(); + void _centerPoint(LatLng point) { + _mapController.fitBounds( + LatLngBounds(point), + ); + } @override void initState() { super.initState(); @@ -77,13 +85,16 @@ class _DashboardDesktopState extends State { Expanded( child: AccessoryList( loadLocationUpdates: loadLocationUpdates, + centerOnPoint: _centerPoint, ), ), ], ), ), - const Expanded( - child: AccessoryMap(), + Expanded( + child: AccessoryMap( + mapController: _mapController, + ), ), ], ), diff --git a/openhaystack-mobile/lib/map/map.dart b/openhaystack-mobile/lib/map/map.dart index 676286a..8322882 100644 --- a/openhaystack-mobile/lib/map/map.dart +++ b/openhaystack-mobile/lib/map/map.dart @@ -71,11 +71,14 @@ class _AccessoryMapState extends State { .where((accessory) => accessory.lastLocation != null) .map((accessory) => accessory.lastLocation!) .toList(); - _mapController.fitBounds( - LatLngBounds.fromPoints([...points, ...accessoryPoints]), - options: const FitBoundsOptions( - padding: EdgeInsets.all(25), - )); + points = [... points, ...accessoryPoints]; + if (points.isNotEmpty) { + _mapController.fitBounds( + LatLngBounds.fromPoints(points), + options: const FitBoundsOptions( + padding: EdgeInsets.all(25), + )); + } } @override diff --git a/openhaystack-mobile/pubspec.lock b/openhaystack-mobile/pubspec.lock index 1b7d8a6..1092174 100644 --- a/openhaystack-mobile/pubspec.lock +++ b/openhaystack-mobile/pubspec.lock @@ -421,7 +421,7 @@ packages: name: pointycastle url: "https://pub.dartlang.org" source: hosted - version: "3.5.1" + version: "3.6.1" positioned_tap_detector_2: dependency: transitive description: diff --git a/openhaystack-mobile/pubspec.yaml b/openhaystack-mobile/pubspec.yaml index 7caadf5..457fcb3 100644 --- a/openhaystack-mobile/pubspec.yaml +++ b/openhaystack-mobile/pubspec.yaml @@ -40,7 +40,7 @@ dependencies: # Cryptography # latest version of pointy castle for crypto functions - pointycastle: ^3.4.0 + pointycastle: ^3.6.1 # State Management provider: ^6.0.1 From 67a55501c290a8fe03a75c0430b87f5a8ca3aa57 Mon Sep 17 00:00:00 2001 From: Andrea Baccega Date: Sun, 14 Aug 2022 18:04:35 +0200 Subject: [PATCH 11/16] fix item management in desktop in options menu fix ability to copy on clipboard data and open json files in windows. added icon. --- .../lib/dashboard/dashboard_desktop.dart | 24 +++++++++- .../lib/item_management/item_export.dart | 42 ++++++++++++++---- .../item_management_desktop.dart | 25 +++++++++++ openhaystack-mobile/pubspec.lock | 21 ++++++--- openhaystack-mobile/pubspec.yaml | 3 +- openhaystack-mobile/windows/runner/main.cpp | 2 +- .../windows/runner/resources/app_icon.ico | Bin 33772 -> 138082 bytes 7 files changed, 97 insertions(+), 20 deletions(-) create mode 100644 openhaystack-mobile/lib/item_management/item_management_desktop.dart diff --git a/openhaystack-mobile/lib/dashboard/dashboard_desktop.dart b/openhaystack-mobile/lib/dashboard/dashboard_desktop.dart index d25be2f..27ba9c3 100644 --- a/openhaystack-mobile/lib/dashboard/dashboard_desktop.dart +++ b/openhaystack-mobile/lib/dashboard/dashboard_desktop.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; +import 'package:openhaystack_mobile/item_management/item_management_desktop.dart'; import 'package:provider/provider.dart'; import 'package:openhaystack_mobile/accessory/accessory_list.dart'; import 'package:openhaystack_mobile/accessory/accessory_registry.dart'; @@ -48,7 +49,21 @@ class _DashboardDesktopState extends State { /// Fetch locaiton updates for all accessories. Future loadLocationUpdates() async { var accessoryRegistry = Provider.of(context, listen: false); - await accessoryRegistry.loadLocationReports(); + try { + await accessoryRegistry.loadLocationReports(); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: Theme.of(context).colorScheme.error, + content: Text( + 'Could not find location reports. Try again later.', + style: TextStyle( + color: Theme.of(context).colorScheme.onError, + ), + ), + ), + ); + } } @override @@ -63,7 +78,12 @@ class _DashboardDesktopState extends State { AppBar( title: const Text('OpenHaystack'), leading: IconButton( - onPressed: () { /* reload */ }, + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const ItemManagementDesktop()), + ); + }, icon: const Icon(Icons.menu), ), actions: [ diff --git a/openhaystack-mobile/lib/item_management/item_export.dart b/openhaystack-mobile/lib/item_management/item_export.dart index e2a29f8..b8b42bc 100644 --- a/openhaystack-mobile/lib/item_management/item_export.dart +++ b/openhaystack-mobile/lib/item_management/item_export.dart @@ -3,12 +3,14 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; import 'package:openhaystack_mobile/accessory/accessory_dto.dart'; import 'package:openhaystack_mobile/accessory/accessory_model.dart'; import 'package:openhaystack_mobile/accessory/accessory_registry.dart'; import 'package:share_plus/share_plus.dart'; +import 'package:clipboard/clipboard.dart'; class ItemExportMenu extends StatelessWidget { /// The accessory to export from @@ -23,6 +25,22 @@ class ItemExportMenu extends StatelessWidget { required this.accessory, }) : super(key: key); + Future _share(String s, BuildContext context) async { + if (Platform.isWindows) { + await FlutterClipboard.copy(s); + ScaffoldMessenger.of(context).showSnackBar( + + const SnackBar( + content: Text('Copied in clipboard'), + + ), + ); + + return; + } + Share.share(s); + } + /// Shows the export options for the [accessory]. void showKeyExportSheet(BuildContext context, Accessory accessory) { showModalBottomSheet(context: context, builder: (BuildContext context) { @@ -58,7 +76,7 @@ class ItemExportMenu extends StatelessWidget { title: const Text('Export Hashed Advertisement Key (Base64)'), onTap: () async { var advertisementKey = await accessory.getHashedAdvertisementKey(); - Share.share(advertisementKey); + await _share(advertisementKey, context); Navigator.pop(context); }, ), @@ -66,7 +84,7 @@ class ItemExportMenu extends StatelessWidget { title: const Text('Export Advertisement Key (Base64)'), onTap: () async { var advertisementKey = await accessory.getAdvertisementKey(); - Share.share(advertisementKey); + await _share(advertisementKey, context); Navigator.pop(context); }, ), @@ -74,7 +92,7 @@ class ItemExportMenu extends StatelessWidget { title: const Text('Export Private Key (Base64)'), onTap: () async { var privateKey = await accessory.getPrivateKey(); - Share.share(privateKey); + await _share(privateKey, context); Navigator.pop(context); }, ), @@ -123,12 +141,18 @@ class ItemExportMenu extends StatelessWidget { JsonEncoder encoder = const JsonEncoder.withIndent(' '); // format output String encodedAccessories = encoder.convert(exportAccessories); await file.writeAsString(encodedAccessories); - // Share export file over os share dialog - Share.shareFiles( - [file.path], - mimeTypes: ['application/json'], - subject: filename, - ); + + if (Platform.isWindows) { + // on windows we can open the file. + launch('file://${file.path}'); + } else { + // Share export file over os share dialog + Share.shareFiles( + [file.path], + mimeTypes: ['application/json'], + subject: filename, + ); + } } /// Show an explanation how the different key types are used. diff --git a/openhaystack-mobile/lib/item_management/item_management_desktop.dart b/openhaystack-mobile/lib/item_management/item_management_desktop.dart new file mode 100644 index 0000000..7dc5861 --- /dev/null +++ b/openhaystack-mobile/lib/item_management/item_management_desktop.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:openhaystack_mobile/item_management/item_management.dart'; +import 'package:openhaystack_mobile/item_management/new_item_action.dart'; + +class ItemManagementDesktop extends StatefulWidget { + + /// Displays this preferences page with information about the app. + const ItemManagementDesktop({ Key? key }) : super(key: key); + + @override + _ItemManagementDesktopState createState() => _ItemManagementDesktopState(); +} + +class _ItemManagementDesktopState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Item Management'), + ), + body: const KeyManagement(), + floatingActionButton: const NewKeyAction(), + ); + } +} diff --git a/openhaystack-mobile/pubspec.lock b/openhaystack-mobile/pubspec.lock index 1092174..6c3c341 100644 --- a/openhaystack-mobile/pubspec.lock +++ b/openhaystack-mobile/pubspec.lock @@ -43,6 +43,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.1" + clipboard: + dependency: "direct main" + description: + name: clipboard + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.3" clock: dependency: transitive description: @@ -330,7 +337,7 @@ packages: name: mime url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.2" nested: dependency: transitive description: @@ -470,42 +477,42 @@ packages: name: share_plus url: "https://pub.dartlang.org" source: hosted - version: "3.1.0" + version: "4.0.10+1" share_plus_linux: dependency: transitive description: name: share_plus_linux url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "3.0.0" share_plus_macos: dependency: transitive description: name: share_plus_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "3.0.1" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "3.0.3" share_plus_web: dependency: transitive description: name: share_plus_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "3.0.0" share_plus_windows: dependency: transitive description: name: share_plus_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "3.0.0" shared_preferences: dependency: "direct main" description: diff --git a/openhaystack-mobile/pubspec.yaml b/openhaystack-mobile/pubspec.yaml index 457fcb3..7e08d1c 100644 --- a/openhaystack-mobile/pubspec.yaml +++ b/openhaystack-mobile/pubspec.yaml @@ -57,10 +57,11 @@ dependencies: # Sharing receive_sharing_intent: ^1.4.5 - share_plus: ^3.0.4 + share_plus: ^4.0.10+1 url_launcher: ^6.0.17 path_provider: ^2.0.8 maps_launcher: ^2.0.1 + clipboard: ^0.1.3 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. diff --git a/openhaystack-mobile/windows/runner/main.cpp b/openhaystack-mobile/windows/runner/main.cpp index 4926d71..c1681b9 100644 --- a/openhaystack-mobile/windows/runner/main.cpp +++ b/openhaystack-mobile/windows/runner/main.cpp @@ -27,7 +27,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); - if (!window.CreateAndShow(L"openhaystack_mobile", origin, size)) { + if (!window.CreateAndShow(L"OpenHaystack", origin, size)) { return EXIT_FAILURE; } window.SetQuitOnClose(true); diff --git a/openhaystack-mobile/windows/runner/resources/app_icon.ico b/openhaystack-mobile/windows/runner/resources/app_icon.ico index c04e20caf6370ebb9253ad831cc31de4a9c965f6..f707f5ee323644429a669f379e2c20e33803b95d 100644 GIT binary patch literal 138082 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

tB)M76b63bVj?k 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�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 From 556b21897df7b27e3e7c4d8cf7a6c3ae2d31cfa2 Mon Sep 17 00:00:00 2001 From: Andrea Baccega Date: Tue, 16 Aug 2022 14:52:43 +0000 Subject: [PATCH 12/16] merge + fix export on web. (copies to clipboard) --- .../lib/item_management/item_export.dart | 36 +++++++++++++------ openhaystack-mobile/lib/main.dart | 32 ++++++++++------- 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/openhaystack-mobile/lib/item_management/item_export.dart b/openhaystack-mobile/lib/item_management/item_export.dart index b8b42bc..1d46abf 100644 --- a/openhaystack-mobile/lib/item_management/item_export.dart +++ b/openhaystack-mobile/lib/item_management/item_export.dart @@ -17,8 +17,8 @@ class ItemExportMenu extends StatelessWidget { Accessory accessory; /// Displays a bottom sheet with export options. - /// - /// The accessory can be exported to a JSON file or the + /// + /// The accessory can be exported to a JSON file or the /// key parameters can be exported separately. ItemExportMenu({ Key? key, @@ -26,7 +26,7 @@ class ItemExportMenu extends StatelessWidget { }) : super(key: key); Future _share(String s, BuildContext context) async { - if (Platform.isWindows) { + if (kIsWeb || Platform.isWindows) { await FlutterClipboard.copy(s); ScaffoldMessenger.of(context).showSnackBar( @@ -61,14 +61,14 @@ class ItemExportMenu extends StatelessWidget { title: const Text('Export All Accessories (JSON)'), onTap: () async { var accessories = Provider.of(context, listen: false).accessories; - await _exportAccessoriesAsJSON(accessories); + await _exportAccessoriesAsJSON(accessories, context); Navigator.pop(context); }, ), ListTile( title: const Text('Export Accessory (JSON)'), onTap: () async { - await _exportAccessoriesAsJSON([accessory]); + await _exportAccessoriesAsJSON([accessory], context); Navigator.pop(context); }, ), @@ -103,13 +103,12 @@ class ItemExportMenu extends StatelessWidget { } /// Export the serialized [accessories] as a JSON file. - /// + /// /// The OpenHaystack export format is used for interoperability with /// the desktop app. - Future _exportAccessoriesAsJSON(List accessories) async { + Future _exportAccessoriesAsJSON(List accessories, BuildContext context) async { // Create temporary directory to store export file - Directory tempDir = await getTemporaryDirectory(); - String path = tempDir.path; + // Convert accessories to export format List exportAccessories = []; for (Accessory accessory in accessories) { @@ -135,11 +134,26 @@ class ItemExportMenu extends StatelessWidget { isActive: accessory.isActive, )); } + + JsonEncoder encoder = const JsonEncoder.withIndent(' '); // format output + String encodedAccessories = encoder.convert(exportAccessories); + if (kIsWeb) { + await FlutterClipboard.copy(encodedAccessories); + ScaffoldMessenger.of(context).showSnackBar( + + const SnackBar( + content: Text('Copied in clipboard'), + + ), + ); + return; + } + Directory tempDir = await getTemporaryDirectory(); + String path = tempDir.path; // Create file and write accessories as json const filename = 'accessories.json'; File file = File('$path/$filename'); - JsonEncoder encoder = const JsonEncoder.withIndent(' '); // format output - String encodedAccessories = encoder.convert(exportAccessories); + await file.writeAsString(encodedAccessories); if (Platform.isWindows) { diff --git a/openhaystack-mobile/lib/main.dart b/openhaystack-mobile/lib/main.dart index 196275b..7f458b8 100644 --- a/openhaystack-mobile/lib/main.dart +++ b/openhaystack-mobile/lib/main.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; @@ -33,7 +34,7 @@ class MyApp extends StatelessWidget { primarySwatch: Colors.blue, ), darkTheme: ThemeData.dark(), - home: const AppLayout(), + home: const AppLayout(), ), ); } @@ -53,16 +54,20 @@ class _AppLayoutState extends State { initState() { super.initState(); - _intentDataStreamSubscription = ReceiveSharingIntent.getMediaStream() - .listen(handleFileSharingIntent, onError: print); - ReceiveSharingIntent.getInitialMedia() - .then(handleFileSharingIntent); + if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { + //Only supported on this platforms according to + //https://pub.dev/packages/receive_sharing_intent + _intentDataStreamSubscription = ReceiveSharingIntent.getMediaStream() + .listen(handleFileSharingIntent, onError: print); + ReceiveSharingIntent.getInitialMedia().then(handleFileSharingIntent); + } - var accessoryRegistry = Provider.of(context, listen: false); + var accessoryRegistry = + Provider.of(context, listen: false); accessoryRegistry.loadAccessories(); } - Future handleFileSharingIntent(List files) async { + Future handleFileSharingIntent(List files) async { // Received a sharing intent with a number of files. // Import the accessories for each device in sequence. // If no files are shared do nothing @@ -70,11 +75,13 @@ class _AppLayoutState extends State { if (file.type == SharedMediaType.FILE) { // On iOS the file:// prefix has to be stripped to access the file path String path = Platform.isIOS - ? Uri.decodeComponent(file.path.replaceFirst('file://', '')) - : file.path; - Navigator.push(context, MaterialPageRoute( - builder: (context) => ItemFileImport(filePath: path), - )); + ? Uri.decodeComponent(file.path.replaceFirst('file://', '')) + : file.path; + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ItemFileImport(filePath: path), + )); } } } @@ -92,7 +99,6 @@ class _AppLayoutState extends State { super.didChangeDependencies(); } - @override Widget build(BuildContext context) { bool isInitialized = context.watch().initialized; From e5513078aee66d3c8f7756606f91649077486223 Mon Sep 17 00:00:00 2001 From: Andrea Baccega Date: Sat, 27 Aug 2022 10:44:43 +0200 Subject: [PATCH 13/16] Support for windows to support large number of beacons. See : https://github.com/mogol/flutter_secure_storage/issues/345 --- .../lib/accessory/accessory_registry.dart | 3 +- .../lib/accessory/accessory_storage.dart | 75 +++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 openhaystack-mobile/lib/accessory/accessory_storage.dart diff --git a/openhaystack-mobile/lib/accessory/accessory_registry.dart b/openhaystack-mobile/lib/accessory/accessory_registry.dart index 32c12d8..decb37a 100644 --- a/openhaystack-mobile/lib/accessory/accessory_registry.dart +++ b/openhaystack-mobile/lib/accessory/accessory_registry.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:openhaystack_mobile/accessory/accessory_model.dart'; import 'package:latlong2/latlong.dart'; +import 'package:openhaystack_mobile/accessory/accessory_storage.dart'; import 'package:openhaystack_mobile/findMy/find_my_controller.dart'; import 'package:openhaystack_mobile/findMy/models.dart'; @@ -12,7 +13,7 @@ const accessoryStorageKey = 'ACCESSORIES'; class AccessoryRegistry extends ChangeNotifier { - final _storage = const FlutterSecureStorage(); + final _storage = const AccessoryStorage(const FlutterSecureStorage()); final _findMyController = FindMyController(); List _accessories = []; bool loading = false; diff --git a/openhaystack-mobile/lib/accessory/accessory_storage.dart b/openhaystack-mobile/lib/accessory/accessory_storage.dart new file mode 100644 index 0000000..5b967af --- /dev/null +++ b/openhaystack-mobile/lib/accessory/accessory_storage.dart @@ -0,0 +1,75 @@ +import 'dart:io'; + +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +const CRED_MAX_CREDENTIAL_BLOB_SIZE = 5 * 512; + +class AccessoryStorage { + final FlutterSecureStorage flutterSecureStorage; + + const AccessoryStorage(this.flutterSecureStorage); + Future deleteAll() async { + await flutterSecureStorage.deleteAll(); + } + + Future delete({required String key}) async { + if (Platform.isWindows) { + final chunkedKey = '\$${key}_chunk_size'; + + final chunkSize = + int.parse(await flutterSecureStorage.read(key: chunkedKey) ?? '0'); + + if (chunkSize > 0) { + await Future.wait(List.generate(chunkSize, + (i) async => await flutterSecureStorage.delete(key: '${key}_${i + 1}'))); + } else { + await flutterSecureStorage.delete(key: key); + } + await flutterSecureStorage.delete(key:chunkedKey); + } else { + await flutterSecureStorage.delete(key: key); + } + } + + Future write({ + required String key, + required String? value, + }) async { + if (Platform.isWindows && + value != null && + value.length > CRED_MAX_CREDENTIAL_BLOB_SIZE) { + final exp = RegExp(r".{1,512}"); + final matches = exp.allMatches(value).toList(); + final chunkedKey = '\$${key}_chunk_size'; + await Future.wait(List.generate(matches.length + 1, (i) { + return i == 0 + ? flutterSecureStorage.write( + key: chunkedKey, value: matches.length.toString()) + : flutterSecureStorage.write( + key: '${key}_${i}', value: matches[i - 1].group(0)); + })); + } else { + await flutterSecureStorage.write(key: key, value: value); + } + } + + Future read({required String key}) async { + if (Platform.isWindows) { + // await this.delete(key: key); + final chunkedKey = '\$${key}_chunk_size'; + + final chunkSize = + int.parse(await flutterSecureStorage.read(key: chunkedKey) ?? '0'); + + if (chunkSize > 0) { + final chunks = await Future.wait(List.generate(chunkSize, + (i) async => await flutterSecureStorage.read(key: '${key}_${i+1}'))); + return chunks.join(); + } else { + return await flutterSecureStorage.read(key: key); + } + } else { + return await flutterSecureStorage.read(key: key); + } + } +} \ No newline at end of file From 9adc06c0cec28f89a9a6a9e8b7a72c23858957dd Mon Sep 17 00:00:00 2001 From: Andrea Baccega Date: Mon, 29 Aug 2022 23:41:34 +0200 Subject: [PATCH 14/16] fix issue when first location has invalid coordinates. --- openhaystack-mobile/lib/accessory/accessory_registry.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openhaystack-mobile/lib/accessory/accessory_registry.dart b/openhaystack-mobile/lib/accessory/accessory_registry.dart index decb37a..58a9e50 100644 --- a/openhaystack-mobile/lib/accessory/accessory_registry.dart +++ b/openhaystack-mobile/lib/accessory/accessory_registry.dart @@ -100,12 +100,12 @@ class AccessoryRegistry extends ChangeNotifier { var reportsForAccessories = await Future.wait(runningLocationRequests); for (var i = 0; i < currentAccessories.length; i++) { var accessory = currentAccessories.elementAt(i); - var reports = reportsForAccessories.elementAt(i); + var reports = reportsForAccessories.elementAt(i) + .where((report) => report.latitude.abs() <= 90 && report.longitude.abs() < 90 ); print("Found ${reports.length} reports for accessory '${accessory.name}'"); accessory.locationHistory = reports - .where((report) => report.latitude.abs() <= 90 && report.longitude.abs() < 90 ) .map((report) => Pair( LatLng(report.latitude, report.longitude), report.timestamp ?? report.published, From 923db582e83ed8ded0d897fff45fff3d8e985570 Mon Sep 17 00:00:00 2001 From: Andrea Baccega Date: Wed, 31 Aug 2022 15:04:51 +0200 Subject: [PATCH 15/16] added list of reports with map interaction and colors for start +end --- .../lib/history/accessory_history.dart | 256 +++++++++++------- 1 file changed, 162 insertions(+), 94 deletions(-) diff --git a/openhaystack-mobile/lib/history/accessory_history.dart b/openhaystack-mobile/lib/history/accessory_history.dart index 66de060..f1b744a 100644 --- a/openhaystack-mobile/lib/history/accessory_history.dart +++ b/openhaystack-mobile/lib/history/accessory_history.dart @@ -1,3 +1,4 @@ +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/plugin_api.dart'; import 'package:openhaystack_mobile/accessory/accessory_model.dart'; @@ -23,16 +24,26 @@ class AccessoryHistory extends StatefulWidget { class _AccessoryHistoryState extends State { final MapController _mapController = MapController(); + final ScrollController _scrollController = new ScrollController(); bool showPopup = false; Pair? popupEntry; double numberOfDays = 7; + int scrolledMarker = -1; @override void initState() { super.initState(); - + _scrollController.addListener(() { + setState(() { + var old = scrolledMarker; + scrolledMarker = _scrollController.offset.toInt() ~/ 24; + if (old != scrolledMarker) { + _mapController.move(widget.accessory.locationHistory.elementAt(scrolledMarker).a, _mapController.zoom); + } + }); + }); _mapController.onReady .then((_) { var historicLocations = widget.accessory.locationHistory @@ -42,6 +53,55 @@ class _AccessoryHistoryState extends State { }); } + List buildMarkers() { + List toRet = []; + var length = widget.accessory.locationHistory.length; + for (int i=1; i< length-1; i++) { + toRet.add(buildMarker(i)); + } + if (length > 0) { + toRet.add(buildMarker(0)); + } + if (length > 1) { + toRet.add(buildMarker(length-1)); + } + return toRet; + } + + Marker buildMarker(int index) { + var entry = widget.accessory.locationHistory.elementAt(index); + return Marker( + point: entry.a, + + builder: (ctx) => GestureDetector( + onTap: () { + setState(() { + showPopup = true; + popupEntry = entry; + }); + }, + child: Icon( + Icons.circle, + size: (widget.accessory.locationHistory.first == entry || widget.accessory.locationHistory.last == entry) ? 20: 10, + color: (() { + if (widget.accessory.locationHistory.first == entry) { + return Colors.amber; + } + if (widget.accessory.locationHistory.last == entry) { + return Colors.green; + } + if (index == scrolledMarker) { + return Colors.pink; + } + if (entry == popupEntry) { + return Colors.red; + } + return Theme.of(context).indicatorColor; + })().withOpacity(0.9), + ), + ), + ); + } @override Widget build(BuildContext context) { // Filter for the locations after the specified cutoff date (now - number of days) @@ -58,104 +118,112 @@ class _AccessoryHistoryState extends State { title: Text(widget.accessory.name), ), body: SafeArea( - child: Column( - children: [ - Flexible( - flex: 3, - fit: FlexFit.tight, - child: FlutterMap( - mapController: _mapController, - options: MapOptions( - center: LatLng(49.874739, 8.656280), - zoom: 13.0, - interactiveFlags: - InteractiveFlag.pinchZoom | InteractiveFlag.drag | - InteractiveFlag.doubleTapZoom | InteractiveFlag.flingAnimation | - InteractiveFlag.pinchMove, - onTap: (_, __) { - setState(() { - showPopup = false; - popupEntry = null; - }); - }, - ), - layers: [ - TileLayerOptions( - backgroundColor: Theme.of(context).colorScheme.surface, - tileBuilder: (context, child, tile) { - var isDark = (Theme.of(context).brightness == Brightness.dark); - return isDark ? ColorFiltered( - colorFilter: const ColorFilter.matrix([ - -1, 0, 0, 0, 255, - 0, -1, 0, 0, 255, - 0, 0, -1, 0, 255, - 0, 0, 0, 1, 0, - ]), - child: child, - ) : child; - }, - urlTemplate: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", - subdomains: ['a', 'b', 'c'], - attributionBuilder: (_) { - return const Text("© OpenStreetMap contributors"); + child: Scrollbar( + child: Column( + children: [ + Flexible( + flex: 3, + fit: FlexFit.tight, + child: FlutterMap( + mapController: _mapController, + options: MapOptions( + center: LatLng(49.874739, 8.656280), + zoom: 13.0, + maxZoom: 18.25, + interactiveFlags: + InteractiveFlag.pinchZoom | InteractiveFlag.drag | + InteractiveFlag.doubleTapZoom | InteractiveFlag.flingAnimation | + InteractiveFlag.pinchMove, + onTap: (_, __) { + setState(() { + showPopup = false; + popupEntry = null; + }); }, ), - // The line connecting the locations chronologically - PolylineLayerOptions( - polylines: [ - Polyline( - points: locationHistory.map((entry) => entry.a).toList(), - strokeWidth: 4, - color: Theme.of(context).colorScheme.primaryVariant, - ), - ], - ), - // The markers for the historic locaitons - MarkerLayerOptions( - markers: locationHistory.map((entry) => Marker( - point: entry.a, - builder: (ctx) => GestureDetector( - onTap: () { - setState(() { - showPopup = true; - popupEntry = entry; - }); - }, - child: Icon( - Icons.circle, - size: 15, - color: entry == popupEntry - ? Colors.red - : Theme.of(context).indicatorColor, + layers: [ + TileLayerOptions( + backgroundColor: Theme.of(context).colorScheme.surface, + tileBuilder: (context, child, tile) { + var isDark = (Theme.of(context).brightness == Brightness.dark); + return isDark ? ColorFiltered( + colorFilter: const ColorFilter.matrix([ + -1, 0, 0, 0, 255, + 0, -1, 0, 0, 255, + 0, 0, -1, 0, 255, + 0, 0, 0, 1, 0, + ]), + child: child, + ) : child; + }, + // urlTemplate: "https://mt0.google.com/vt/lyrs=m@221097413&x={x}&y={y}&z={z}", + urlTemplate: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", + subdomains: ['a', 'b', 'c'], + attributionBuilder: (_) { + return const Text("© OpenStreetMap contributors"); + }, + ), + // The line connecting the locations chronologically + PolylineLayerOptions( + polylines: [ + Polyline( + points: locationHistory.map((entry) => entry.a).toList(), + strokeWidth: 4, + color: Theme.of(context).colorScheme.primaryVariant, + isDotted: true ), - ), - )).toList(), - ), - // Displays the tooltip if active - MarkerLayerOptions( - markers: [ - if (showPopup) LocationPopup( - location: popupEntry!.a, - time: popupEntry!.b, - ), - ], - ), - ], + ], + ), + // The markers for the historic locaitons + MarkerLayerOptions( + markers: buildMarkers(), + ), + // Displays the tooltip if active + MarkerLayerOptions( + markers: [ + if (showPopup) LocationPopup( + location: popupEntry!.a, + time: popupEntry!.b, + ), + ], + ), + ], + ), ), - ), - Flexible( - flex: 1, - fit: FlexFit.tight, - child: DaysSelectionSlider( - numberOfDays: numberOfDays, - onChanged: (double newValue) { - setState(() { - numberOfDays = newValue; - }); - }, + Flexible( + flex: 1, + fit: FlexFit.tight, + child: DaysSelectionSlider( + numberOfDays: numberOfDays, + onChanged: (double newValue) { + setState(() { + numberOfDays = newValue; + }); + }, + ), ), - ), - ], + Flexible( + flex: 1, + fit: FlexFit.tight, + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith(dragDevices: { + PointerDeviceKind.touch, + PointerDeviceKind.mouse, + },), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 8.0), + child: ListView( + physics: const AlwaysScrollableScrollPhysics(), + padding: EdgeInsets.only(top:12), + controller: _scrollController, + scrollDirection: Axis.vertical, + children: locationHistory.map((l) => SizedBox(height: 24, child: Text(l.b.toString()))).toList(), + ), + ), + ) + ) + ], + ), ), ), ); From 0535d8b20905ac11e2faa4b5458596662c10e8bb Mon Sep 17 00:00:00 2001 From: Andrea Baccega Date: Wed, 31 Aug 2022 17:25:02 +0200 Subject: [PATCH 16/16] better performance when scrolling. Still sucks. --- .../lib/history/accessory_history.dart | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/openhaystack-mobile/lib/history/accessory_history.dart b/openhaystack-mobile/lib/history/accessory_history.dart index f1b744a..43173ac 100644 --- a/openhaystack-mobile/lib/history/accessory_history.dart +++ b/openhaystack-mobile/lib/history/accessory_history.dart @@ -25,7 +25,7 @@ class _AccessoryHistoryState extends State { final MapController _mapController = MapController(); final ScrollController _scrollController = new ScrollController(); - + List> locationHistory = []; bool showPopup = false; Pair? popupEntry; @@ -51,11 +51,23 @@ class _AccessoryHistoryState extends State { var bounds = LatLngBounds.fromPoints(historicLocations); _mapController.fitBounds(bounds); }); + buildLocationHistory(); } + void buildLocationHistory() { + var now = DateTime.now(); + locationHistory = widget.accessory.locationHistory + .where( + (element) => element.b.isAfter( + now.subtract(Duration(days: numberOfDays.round())), + ), + ).toList(); + + } List buildMarkers() { List toRet = []; - var length = widget.accessory.locationHistory.length; + + var length = locationHistory.length; for (int i=1; i< length-1; i++) { toRet.add(buildMarker(i)); } @@ -69,7 +81,7 @@ class _AccessoryHistoryState extends State { } Marker buildMarker(int index) { - var entry = widget.accessory.locationHistory.elementAt(index); + var entry = locationHistory.elementAt(index); return Marker( point: entry.a, @@ -82,12 +94,12 @@ class _AccessoryHistoryState extends State { }, child: Icon( Icons.circle, - size: (widget.accessory.locationHistory.first == entry || widget.accessory.locationHistory.last == entry) ? 20: 10, + size: (locationHistory.first == entry || locationHistory.last == entry) ? 20: 10, color: (() { - if (widget.accessory.locationHistory.first == entry) { + if (locationHistory.first == entry) { return Colors.amber; } - if (widget.accessory.locationHistory.last == entry) { + if (locationHistory.last == entry) { return Colors.green; } if (index == scrolledMarker) { @@ -105,13 +117,6 @@ class _AccessoryHistoryState extends State { @override Widget build(BuildContext context) { // Filter for the locations after the specified cutoff date (now - number of days) - var now = DateTime.now(); - List> locationHistory = widget.accessory.locationHistory - .where( - (element) => element.b.isAfter( - now.subtract(Duration(days: numberOfDays.round())), - ), - ).toList(); return Scaffold( appBar: AppBar( @@ -122,7 +127,7 @@ class _AccessoryHistoryState extends State { child: Column( children: [ Flexible( - flex: 3, + flex: 4, fit: FlexFit.tight, child: FlutterMap( mapController: _mapController, @@ -198,6 +203,7 @@ class _AccessoryHistoryState extends State { onChanged: (double newValue) { setState(() { numberOfDays = newValue; + buildLocationHistory(); }); }, ),