diff --git a/.flutter-plugins-dependencies b/.flutter-plugins-dependencies index cf4364fb06a78d8bf21a3a465b597f9defd3bcc6..078d7af5290766659fc88126c85ac23b0ebcdf5f 100644 --- a/.flutter-plugins-dependencies +++ b/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"connectivity","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\connectivity-0.4.9+2\\\\","dependencies":[]},{"name":"flutter_blue","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\flutter_blue-0.7.2\\\\","dependencies":[]},{"name":"geolocator","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\geolocator-5.2.1\\\\","dependencies":["google_api_availability","location_permissions"]},{"name":"google_api_availability","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\google_api_availability-2.0.2\\\\","dependencies":[]},{"name":"location_permissions","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\location_permissions-2.0.4+1\\\\","dependencies":[]},{"name":"path_provider","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\path_provider-1.6.0\\\\","dependencies":[]},{"name":"permission_handler","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\permission_handler-4.2.0+hotfix.3\\\\","dependencies":[]},{"name":"shared_preferences","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\shared_preferences-0.5.7+2\\\\","dependencies":[]},{"name":"sqflite","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\sqflite-1.2.0\\\\","dependencies":[]}],"android":[{"name":"connectivity","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\connectivity-0.4.9+2\\\\","dependencies":[]},{"name":"flutter_blue","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\flutter_blue-0.7.2\\\\","dependencies":[]},{"name":"geolocator","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\geolocator-5.2.1\\\\","dependencies":["google_api_availability","location_permissions"]},{"name":"google_api_availability","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\google_api_availability-2.0.2\\\\","dependencies":[]},{"name":"location_permissions","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\location_permissions-2.0.4+1\\\\","dependencies":[]},{"name":"path_provider","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\path_provider-1.6.0\\\\","dependencies":[]},{"name":"permission_handler","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\permission_handler-4.2.0+hotfix.3\\\\","dependencies":[]},{"name":"shared_preferences","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\shared_preferences-0.5.7+2\\\\","dependencies":[]},{"name":"sqflite","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\sqflite-1.2.0\\\\","dependencies":[]}],"macos":[{"name":"connectivity_macos","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\connectivity_macos-0.1.0+4\\\\","dependencies":[]},{"name":"shared_preferences_macos","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\shared_preferences_macos-0.0.1+8\\\\","dependencies":[]},{"name":"sqflite","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\sqflite-1.2.0\\\\","dependencies":[]}],"linux":[],"windows":[],"web":[{"name":"connectivity_for_web","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\connectivity_for_web-0.3.1+2\\\\","dependencies":[]},{"name":"shared_preferences_web","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\shared_preferences_web-0.1.2+5\\\\","dependencies":[]}]},"dependencyGraph":[{"name":"connectivity","dependencies":["connectivity_macos","connectivity_for_web"]},{"name":"connectivity_for_web","dependencies":[]},{"name":"connectivity_macos","dependencies":[]},{"name":"flutter_blue","dependencies":[]},{"name":"geolocator","dependencies":["google_api_availability","location_permissions"]},{"name":"google_api_availability","dependencies":[]},{"name":"location_permissions","dependencies":[]},{"name":"path_provider","dependencies":[]},{"name":"permission_handler","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_macos","shared_preferences_web"]},{"name":"shared_preferences_macos","dependencies":[]},{"name":"shared_preferences_web","dependencies":[]},{"name":"sqflite","dependencies":[]}],"date_created":"2020-09-07 01:52:18.814120","version":"1.21.0-10.0.pre.193"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"app_settings","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\app_settings-4.0.3\\\\","dependencies":[]},{"name":"connectivity","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\connectivity-0.4.9+2\\\\","dependencies":[]},{"name":"flutter_blue","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\flutter_blue-0.7.2\\\\","dependencies":[]},{"name":"geolocator","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\geolocator-5.2.1\\\\","dependencies":["google_api_availability","location_permissions"]},{"name":"google_api_availability","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\google_api_availability-2.0.2\\\\","dependencies":[]},{"name":"location_permissions","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\location_permissions-2.0.4+1\\\\","dependencies":[]},{"name":"path_provider","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\path_provider-1.6.0\\\\","dependencies":[]},{"name":"permission_handler","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\permission_handler-4.2.0+hotfix.3\\\\","dependencies":[]},{"name":"shared_preferences","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\shared_preferences-0.5.7+2\\\\","dependencies":[]},{"name":"sqflite","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\sqflite-1.2.0\\\\","dependencies":[]}],"android":[{"name":"app_settings","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\app_settings-4.0.3\\\\","dependencies":[]},{"name":"connectivity","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\connectivity-0.4.9+2\\\\","dependencies":[]},{"name":"flutter_blue","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\flutter_blue-0.7.2\\\\","dependencies":[]},{"name":"geolocator","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\geolocator-5.2.1\\\\","dependencies":["google_api_availability","location_permissions"]},{"name":"google_api_availability","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\google_api_availability-2.0.2\\\\","dependencies":[]},{"name":"location_permissions","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\location_permissions-2.0.4+1\\\\","dependencies":[]},{"name":"path_provider","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\path_provider-1.6.0\\\\","dependencies":[]},{"name":"permission_handler","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\permission_handler-4.2.0+hotfix.3\\\\","dependencies":[]},{"name":"shared_preferences","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\shared_preferences-0.5.7+2\\\\","dependencies":[]},{"name":"sqflite","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\sqflite-1.2.0\\\\","dependencies":[]}],"macos":[{"name":"connectivity_macos","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\connectivity_macos-0.1.0+4\\\\","dependencies":[]},{"name":"shared_preferences_macos","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\shared_preferences_macos-0.0.1+8\\\\","dependencies":[]},{"name":"sqflite","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\sqflite-1.2.0\\\\","dependencies":[]}],"linux":[],"windows":[],"web":[{"name":"connectivity_for_web","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\connectivity_for_web-0.3.1+2\\\\","dependencies":[]},{"name":"shared_preferences_web","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\shared_preferences_web-0.1.2+5\\\\","dependencies":[]}]},"dependencyGraph":[{"name":"app_settings","dependencies":[]},{"name":"connectivity","dependencies":["connectivity_macos","connectivity_for_web"]},{"name":"connectivity_for_web","dependencies":[]},{"name":"connectivity_macos","dependencies":[]},{"name":"flutter_blue","dependencies":[]},{"name":"geolocator","dependencies":["google_api_availability","location_permissions"]},{"name":"google_api_availability","dependencies":[]},{"name":"location_permissions","dependencies":[]},{"name":"path_provider","dependencies":[]},{"name":"permission_handler","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_macos","shared_preferences_web"]},{"name":"shared_preferences_macos","dependencies":[]},{"name":"shared_preferences_web","dependencies":[]},{"name":"sqflite","dependencies":[]}],"date_created":"2020-09-14 03:13:52.288184","version":"1.21.0-10.0.pre.193"} \ No newline at end of file diff --git a/README.md b/README.md index bcdaf2b2d36cdf0b5067bb88d8d139bedce4f3d1..53abdc718fd4280e83518330eb647037be28e6c3 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,25 @@ flutter pub pub run intl_translation:extract_to_arb --output-dir=lib/localizatio flutter pub pub run intl_translation:generate_from_arb --output-dir=lib/localization/l10n --no-use-deferred-loading lib/localization/localization.dart lib/localization/l10n/intl_en.arb lib/localization/l10n/intl_fr.arb ``` +## Documentation + +You can generate the application documentation yourself using the following command : + +```bash +dartdoc +``` + +
+ In case of "dartdoc failed: Invalid argument(s): join(null, "bin", "cache", "dart-sdk")" + + Set the following environment variable: + + ```bash + FLUTTER_ROOT= + ``` + +
+ ## File Structure The project is divided in multiple components: diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index d13eaf3543c9da50099feecf13c32942ccc14830..baca04abeb6ff18531936d6ddff6c0c1588f24c2 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -6,6 +6,9 @@ additional functionality it is fine to subclass or reimplement FlutterApplication and put your custom class here. --> + + + { 'fr' ].contains(locale.languageCode); - /// Delegates the loading of [Locale] to an [async] task, which enables it to be done in the background. + /// Delegates the loading of [Locale] to an async task, which enables it to be done in the background. @override Future load(Locale locale) => AppLocalization.load(locale); diff --git a/lib/logic/controllers/home_controller.dart b/lib/logic/controllers/home_controller.dart index 0b40c399fe6418770ecfc66065d3448e926bb135..0e7269efa38b4d7d3372913731446656e4231c59 100644 --- a/lib/logic/controllers/home_controller.dart +++ b/lib/logic/controllers/home_controller.dart @@ -16,6 +16,7 @@ class HomeController { factory HomeController() => _singleton; + /// [HomeController] initializer that sets the default initial values. HomeController._internal() { this._currentPage = 0; this._previousPage = 0; diff --git a/lib/logic/controllers/map_display_controller.dart b/lib/logic/controllers/map_display_controller.dart index 60cece797b01a9dff49f7bf58be27113e0c110ba..432c792ba6c1ecf4fae7cb7b1267fe338ec0b234 100644 --- a/lib/logic/controllers/map_display_controller.dart +++ b/lib/logic/controllers/map_display_controller.dart @@ -4,14 +4,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/plugin_api.dart'; import 'package:latlong/latlong.dart'; import 'package:logair_application/logic/handlers/data_handler.dart'; -import 'package:logair_application/logic/handlers/database_handler.dart'; +import 'package:logair_application/logic/handlers/main_database_handler.dart'; import 'package:logair_application/logic/handlers/network_handler.dart'; import 'package:logair_application/logic/data_header.dart'; import 'package:logair_application/logic/data_packet.dart'; import 'package:logair_application/logic/handlers/position_handler.dart'; import 'package:logair_application/utils/enums/pm_symbol.dart'; -/// The [MapDisplayController] is a [Singleton] that handles the mapping of information. +/// The [MapDisplayController] is a singleton that handles the mapping of information. class MapDisplayController { /// Whether or not the user has assumed manual control of the [FlutterMap], bool _manualMotion = false; @@ -35,20 +35,20 @@ class MapDisplayController { Marker _marker; Future _rebuildMarkers() async { - List packets = await DatabaseHandler().getLastPackets(200); + List packets = await MainDatabaseHandler().getLastPackets(200); packets.forEach((DataPacket packet) { switch (_pmSymbol) { case PMSymbol.PM1: - this.addToList(this._currentPos, packet.pm1(), 200); + this.addToList(this._currentPos, packet.pm1, 200); break; case PMSymbol.PM1: - this.addToList(this._currentPos, packet.pm2_5(), 200); + this.addToList(this._currentPos, packet.pm2_5, 200); break; case PMSymbol.PM4: - this.addToList(this._currentPos, packet.pm4(), 200); + this.addToList(this._currentPos, packet.pm4, 200); break; case PMSymbol.PM10: - this.addToList(this._currentPos, packet.pm10(), 200); + this.addToList(this._currentPos, packet.pm10, 200); break; default: break; @@ -90,7 +90,7 @@ class MapDisplayController { } /// This function is executed on first initialization of the [MapDisplayController] - /// TODO: Throttle to 1 in 5 updates for efficiency ? + // TODO: Throttle to 1 in 5 updates for efficiency ? MapDisplayController._internal() { /// A new [MapController] is used as the global Controller. /// This avoids Garbage Collection rules and enables the [FlutterMap] to survive [State] changes, and other such events. @@ -103,10 +103,9 @@ class MapDisplayController { return; /// If a [BluetoothDevice] is connected, then the [DataHandler] will have a [DataPacket] available. - DataPacket latest = DataHandler().getLatestData(); - + DataPacket latest = null; /// If the [DataPacket] is older than 5 seconds we invalidate it. - if (latest != null && latest.timestamp() <= (DateTime.now().millisecondsSinceEpoch - 5000)) + if (latest != null && latest.timestamp <= (DateTime.now().millisecondsSinceEpoch - 5000)) latest = null; /// Keep track of the last [Position] to draw [Polyline]s. @@ -120,6 +119,7 @@ class MapDisplayController { this._controller.move(this._currentPos, this._zoom); /// Move the [Marker] to the current [Position]. + // TODO: Use StreamBuilder in Map. this._marker = this._buildLocationMarker(this._currentPos); if ((this._lastPos != null && this._currentPos != null)) {// && this._distance.as(LengthUnit.Meter, this._lastPos, this._currentPos) >= 2)) { @@ -136,20 +136,20 @@ class MapDisplayController { if (latest != null) { switch (_pmSymbol) { case PMSymbol.PM1: - if (latest.pm1() != null) - this.addToList(this._currentPos, latest.pm1(), 200); + if (latest.pm1 != null) + this.addToList(this._currentPos, latest.pm1, 200); break; case PMSymbol.PM2_5: - if (latest.pm2_5() != null) - this.addToList(this._currentPos, latest.pm2_5(), 200); + if (latest.pm2_5 != null) + this.addToList(this._currentPos, latest.pm2_5, 200); break; case PMSymbol.PM4: - if (latest.pm4() != null) - this.addToList(this._currentPos, latest.pm4(), 200); + if (latest.pm4 != null) + this.addToList(this._currentPos, latest.pm4, 200); break; case PMSymbol.PM10: - if (latest.pm10() != null) - this.addToList(this._currentPos, latest.pm10(), 200); + if (latest.pm10 != null) + this.addToList(this._currentPos, latest.pm10, 200); break; default: break; @@ -161,7 +161,7 @@ class MapDisplayController { new Timer.periodic(Duration(minutes: 1), (Timer t) { if (this._controller != null && this._controller.bounds != null) { //print('${this._controller.bounds.southWest}, ${this._controller.bounds.northEast}'); - NetworkHandler().getProximityPoints(DataHeader().deviceID, this._controller.bounds); + NetworkHandler().getProximityPoints(DataHeader().deviceId, this._controller.bounds); } }); } diff --git a/lib/logic/data_header.dart b/lib/logic/data_header.dart index 3d6cb9d3460198baef04a78cb5a5108b342ebf2d..31f71d6861d90cd50ea4ed6b3502f53f470d4135 100644 --- a/lib/logic/data_header.dart +++ b/lib/logic/data_header.dart @@ -19,19 +19,28 @@ class DataHeader { bool _headerSet; + String _deviceId; + + String _url; + void setHeader(List header) { this._headerData = String.fromCharCodes(header.sublist(1, header.length - 1)).split(','); + this._deviceId = this._headerData[0]; + this._url = this._headerData[1]; this._headerSet = true; } - void setHeaderFromExisting({String deviceId = '', String url = ''}) { - this._headerData = [deviceID, url]; + void setHeaderFromExisting({@required String deviceId, @required String url}) { + this._headerData = [deviceId, url]; + this._deviceId = deviceId; + this._url = url; this._headerSet = true; } /// Retrieve the deviceID from the header - String get deviceID => (this._headerData != null && this._headerData.length > 0) ? this._headerData[0] : ''; - String get url => (this._headerData != null && this._headerData.length > 0) ? this._headerData[1] : ''; + String get deviceId => (this._headerSet && this._deviceId != null) ? this._deviceId : ''; + String get url => (this._headerSet && this._url != null) ? this._url : ''; + /// @Getter for _headerSet; bool get headerSet => this._headerSet; @@ -46,7 +55,7 @@ class DataHeader { bool isEqual(List that) => ListEquality().equals(_headerData, that); // TODO - Map jsonify() => { 'device_id' : this._headerData[0], 'url': '000000000000' }; + Map jsonify() => { 'device_id' : this.deviceId, 'url': this.url }; void dispose() { this._headerSet = false; @@ -55,7 +64,7 @@ class DataHeader { Stream deviceIdStream() async* { while(true) { - yield this.deviceID; + yield this._deviceId; await Future.delayed(Duration(seconds: 1)); } } diff --git a/lib/logic/data_packet.dart b/lib/logic/data_packet.dart index 0b371fcb390cad15791702a3e9f0865c0291a7ac..e6c3312ecdcca29efbe5a5496dc3afd609ec23ed 100644 --- a/lib/logic/data_packet.dart +++ b/lib/logic/data_packet.dart @@ -11,6 +11,7 @@ class DataPacket { List _data; int _timestamp; String _deviceId; + String _url; /* Location variables */ double _latitude; @@ -31,26 +32,32 @@ class DataPacket { double _pm10; // TODO Firmware v2 + // ignore: unused_field String _extraData; - int timestamp() => _timestamp; - String deviceId() => _deviceId ?? ''; - double latitude() => _latitude; - double longitude() => _longitude; - int altitude() => _altitude; - int heading() => _heading; - double speed() => _speed; - double temperature() => _temperature; - double pressure() => _pressure; - double relativeHumidity() => _relativeHumidity; - double pm1() => _pm1; - double pm2_5() => _pm2_5; - double pm4() => _pm4; - double pm10() => _pm10; + int get timestamp => this._timestamp; + String get deviceId => this._deviceId ?? ''; + String get url => this._url ?? ''; + double get latitude => this._latitude; + double get longitude => this._longitude; + int get altitude => this._altitude; + int get heading => this._heading; + double get speed => this._speed; + double get temperature => this._temperature; + double get pressure => this._pressure; + double get relativeHumidity => this._relativeHumidity; + double get pm1 => this._pm1; + double get pm2_5 => this._pm2_5; + double get pm4 => this._pm4; + double get pm10 => this._pm10; + + set deviceId(String newId) => this._deviceId = newId; + set url(String newUrl) => this._url = newUrl; DataPacket(this._data) { _timestamp = DateTime.now().millisecondsSinceEpoch; List acquiredData = String.fromCharCodes(this._data.sublist(1, this._data.length - 1)).split(','); + this._latitude = (acquiredData[0] != '') ? parseDoubleWrapper(acquiredData[0]) : null; this._longitude = (acquiredData[1] != '') ? parseDoubleWrapper(acquiredData[1]) : null; this._altitude = (acquiredData[2] != '') ? int.tryParse(acquiredData[2]) : null; @@ -80,9 +87,15 @@ class DataPacket { DataPacket.fromData(this._timestamp, this._latitude, this._longitude, this._altitude, this._speed, this._heading, this._temperature, this._pressure, this._relativeHumidity, this._pm1, this._pm2_5, this._pm4, this._pm10, this._extraData); - // TODO Guard clause DataPacket.fromMappedData(Map data) { + List guardSet = ['device_id', 'url', 'timestamp_nix', 'latitude', 'longitude', 'altitude', 'speed', 'heading', 'temperature', 'pressure', 'relative_humidity', 'pm_1', 'pm_2_5', 'pm_4', 'pm_10', 'extra']; + + if(data.keys.toList().where((String key) => guardSet.contains(key)).length < guardSet.length) { + throw Exception('Mapped Data Error'); + } + this._deviceId = data['device_id']; + this._url = data['url']; this._timestamp = data['timestamp_nix']; this._latitude = data['latitude']; this._longitude = data['longitude']; @@ -90,8 +103,8 @@ class DataPacket { this._speed = data['speed']; this._heading = data['heading']; this._temperature = data['temperature']; - this._relativeHumidity = data['relative_humidity']; this._pressure = data['pressure']; + this._relativeHumidity = data['relative_humidity']; this._pm1 = data['pm_1']; this._pm2_5 = data['pm_2_5']; this._pm4 = data['pm_4']; @@ -117,6 +130,7 @@ class DataPacket { this._pm10 ]; + /// Retrieves the DB insertable reference to this packet. Map toMap() { return { 'timestamp_nix': this._timestamp, @@ -126,13 +140,16 @@ class DataPacket { 'speed': this._speed, 'heading': this._heading, 'temperature': this._temperature, - 'relative_humidity': this._temperature, + 'relative_humidity': this.relativeHumidity, 'pressure': this._pressure, 'pm_1': this._pm1, 'pm_2_5': this._pm2_5, 'pm_4': this._pm4, 'pm_10': this._pm10, - 'extra': '' + 'extra': '', + 'device_id': this._deviceId, + 'url': this._url, + }; } diff --git a/lib/logic/handlers/bluetooth_le_handler.dart b/lib/logic/handlers/bluetooth_le_handler.dart index 6a0b93213eb6af604e3b3fcd61d02a5ecd9f7757..cdbd29df7b482fd47f4d6c043999572b65c505a8 100644 --- a/lib/logic/handlers/bluetooth_le_handler.dart +++ b/lib/logic/handlers/bluetooth_le_handler.dart @@ -6,11 +6,12 @@ import 'package:flutter_blue/flutter_blue.dart'; import 'package:kotlin_flavor/scope_functions.dart'; import 'package:logair_application/logic/handlers/bluetooth_wake_handler.dart'; import 'package:logair_application/logic/handlers/preference_handler.dart'; -import 'package:logair_application/services/wake_service.dart'; import 'package:logair_application/utils/enums/bluetooth_connection_status.dart'; import 'package:logair_application/logic/handlers/data_handler.dart'; import 'package:logair_application/utils/enums/preference_keys.dart'; +// TODO Attempt reacquiring device after 5 seconds inactivity. + /// Serves as a wrapper class around [FlutterBlue], and helps integrate it into the application with extensions class BTLEHandler { /// [Singleton]() instantiation factory. @@ -60,7 +61,7 @@ class BTLEHandler { Stream getConnectionStatus() async* { while (true) { yield _bluetoothConnectionStatus; - await Future.delayed(Duration(seconds: 2)); + await Future.delayed(Duration(milliseconds: 250)); } } diff --git a/lib/logic/handlers/bluetooth_wake_handler.dart b/lib/logic/handlers/bluetooth_wake_handler.dart index 7c63b61b438c28f323cdbac64a8c97ab24e16619..28633c381656f0e1c90245749a71e31838f14f45 100644 --- a/lib/logic/handlers/bluetooth_wake_handler.dart +++ b/lib/logic/handlers/bluetooth_wake_handler.dart @@ -1,9 +1,7 @@ -import 'dart:async'; - import 'package:logair_application/services/wake_service.dart'; /// Handler that wraps the [WakeService] -/// TODO flutter_blue seems to do so already, needs testing and iOS solution +// TODO: Relative end / start -> Start and end with guard instead of switch. class BTWakeHandler { factory BTWakeHandler() => _singleton; diff --git a/lib/logic/handlers/data_handler.dart b/lib/logic/handlers/data_handler.dart index 7905fde6efcbf0a98f745df9064f602d5b029660..988bba3147186b5152c6771358508d2ed4940441 100644 --- a/lib/logic/handlers/data_handler.dart +++ b/lib/logic/handlers/data_handler.dart @@ -1,7 +1,7 @@ import 'package:logair_application/logic/handlers/bluetooth_le_handler.dart'; import 'package:logair_application/logic/data_header.dart'; import 'package:logair_application/logic/data_packet.dart'; -import 'package:logair_application/logic/handlers/database_handler.dart'; +import 'package:logair_application/logic/handlers/main_database_handler.dart'; import 'package:logair_application/logic/handlers/network_handler.dart'; class DataHandler { @@ -57,53 +57,56 @@ class DataHandler { bool wasSet = DataHeader().headerSet; DataHeader().setHeader(packet); /// Retroactively add all previous packets - if (!wasSet && DataHeader().headerSet) - _sortedData.forEach((packet) => DatabaseHandler().insertData(packet, DataHeader().deviceID, DataHeader().url)); + if (!wasSet && DataHeader().headerSet) { + _sortedData.forEach((dataPacket) { + dataPacket.deviceId = DataHeader().deviceId; + dataPacket.url = DataHeader().url; + MainDatabaseHandler().insertData(dataPacket); + }); + } /// Or if it is a [DataPacket] instance. } else if (packet[0] == 91) { - _sortedData.add(DataPacket(packet)); print('PACKET ${String.fromCharCodes(packet)}'); - if (DataHeader().headerSet) - DatabaseHandler().insertData(_sortedData.last, DataHeader().deviceID, DataHeader().url); + DataPacket dataPacket = DataPacket(packet); + if (DataHeader().headerSet) { + dataPacket.deviceId = DataHeader().deviceId; + dataPacket.url = DataHeader().url; + MainDatabaseHandler().insertData(dataPacket); + } else + _sortedData.add(dataPacket); } } - /// TODO Used for debugging, later - void printLatest() { - print(_sortedData.last); - } /// Get the entirety of the [DataPacket]s. + @deprecated List getData() => _sortedData; /// Remove the entirety of the acquired data. + @deprecated void clearData() { _sortedData.removeWhere((element) => true); } /// Remove the first element of the [DataPacket]s. Used for surgical removal. - void pop() { - if (_sortedData.isNotEmpty) - _sortedData.removeAt(0); + @deprecated + void pop(int timestamp) { + if (_sortedData.isNotEmpty) + _sortedData.removeWhere((DataPacket d) => d.timestamp == timestamp); } /// Get the latest [DataPacket] + @deprecated DataPacket getLatestData() => (_sortedData.length > 0) ? _sortedData.last : null; /// Stream the latest confirmed [DataPacket] received by the device. + @deprecated Stream getDataStream() async* { while(true) { yield (_sortedData.length > 0) ? _sortedData.last : null; await Future.delayed(Duration(seconds: 1)); } } - - Stream getMockDataStream() async* { - while(true) { - yield new DataPacket.fromData(1597957282000, 5, 42, 420, 3.0, 32, 24, 10123, 32, null, 5, null, 9, ""); - await Future.delayed(Duration(seconds: 1)); - } - } /// This destroys the elements it depends on to avoid duplication or wrong information void dispose() { diff --git a/lib/logic/handlers/locations_interest_database_handler.dart b/lib/logic/handlers/locations_interest_database_handler.dart new file mode 100644 index 0000000000000000000000000000000000000000..86bca84728e108a3ae8343428118ebf8c1fc5b06 --- /dev/null +++ b/lib/logic/handlers/locations_interest_database_handler.dart @@ -0,0 +1,88 @@ + +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; +import 'package:logair_application/logic/data_packet.dart'; +import 'package:logair_application/utils/helpers/location_of_interest.dart'; +import 'package:path/path.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:sqflite/sqflite.dart'; + +/// Handler that integrates a mysqli database into the application's workflow. +class LocationsOfInterestDatabaseHandler { + factory LocationsOfInterestDatabaseHandler() => _singleton; + + LocationsOfInterestDatabaseHandler._internal(); + + static final LocationsOfInterestDatabaseHandler _singleton = new LocationsOfInterestDatabaseHandler._internal(); + + /// Reference to the internalized [Database] within this instance. + Future _loidb; + + /// [Database] initialization string, to reduce repetition. + final String _createLOIDBString = '''CREATE TABLE loi( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT, latitude REAL, longitude REAL + )'''; + + /// Retrieves the internal [Database] instance, and if not instantiated, instantiates it. + Future get _loiDatabase async { + if (_loidb == null) + this._loidb = openDatabase( + join((await getDatabasesPath()), 'logair_loi.db'), + version: 0, + onCreate: (db, version) => db.execute(this._createLOIDBString), + ); + return this._loidb; + } + + /// Purges the [Database] of all internal data. + Future recreateDB() async { + final Database database = await _loiDatabase; + + await database.rawQuery('DROP TABLE IF EXISTS loi'); + await database.rawQuery(this._createLOIDBString); + this.onUpdate.value = 1; + } + + /// Inserts data into the [Database]. + Future insertData({@required String name, @required double latitude, @required double longitude}) async { + final Database database = await this._loiDatabase; + Map insert = { + 'name': name, + 'latitude': latitude, + 'longitude': longitude + }; + await database.insert( + 'loi', + insert, + conflictAlgorithm: ConflictAlgorithm.replace + ); + this.onUpdate.value = 1; + } + + /// Retrieves all of the [DataPacket]s inserted into the [Database]. + Future> get data async { + final Database database = await this._loiDatabase; + + final List> data = await database.query('loi'); + + return List.generate(data.length, (i) => LocationOfInterest( + id: data[i]['id'], + name: data[i]['name'], + latitude: data[i]['latitude'], + longitude: data[i]['longitude'] + )); + } + + /// Closes the [Database] instance. + Future close() async => (await this._loiDatabase).close(); + + /// Async capable variable used to trigger updates. + BehaviorSubject onUpdate = BehaviorSubject(); + + LocationsOfInterestDatabaseHandler.dispose() { + onUpdate.close(); + this.close(); + } +} \ No newline at end of file diff --git a/lib/logic/handlers/database_handler.dart b/lib/logic/handlers/main_database_handler.dart similarity index 63% rename from lib/logic/handlers/database_handler.dart rename to lib/logic/handlers/main_database_handler.dart index b8604b8ed4b68664777a814f433180d64e6d4e4f..1f288496426bfd493ab85d31807a69c5adf99db5 100644 --- a/lib/logic/handlers/database_handler.dart +++ b/lib/logic/handlers/main_database_handler.dart @@ -5,63 +5,52 @@ import 'package:sqflite/sqflite.dart'; import 'package:tuple/tuple.dart'; /// Handler that integrates a mysqli database into the application's workflow. -class DatabaseHandler { - factory DatabaseHandler() => _singleton; +class MainDatabaseHandler { + factory MainDatabaseHandler() => _singleton; - DatabaseHandler._internal(); + MainDatabaseHandler._internal(); - static final DatabaseHandler _singleton = new DatabaseHandler._internal(); + static final MainDatabaseHandler _singleton = new MainDatabaseHandler._internal(); /// Reference to the internalized [Database] within this instance. - Future _db; + Future _maindb; /// [Database] initialization string, to reduce repetition. - final String createString = '''CREATE TABLE data( + final String _createMainDBString = '''CREATE TABLE data( id INTEGER PRIMARY KEY AUTOINCREMENT, - device_id TEXT, - url TEXT, + device_id TEXT, url TEXT, timestamp_nix INTEGER, - latitude REAL, - longitude REAL, - altitude INTEGER, - speed REAL, - heading INTEGER, - temperature FLOAT, - relative_humidity FLOAT, - pressure FLOAT, - pm_1 FLOAT, - pm_2_5 FLOAT, - pm_4 FLOAT, - pm_10 FLOAT, + latitude REAL, longitude REAL, + altitude INTEGER, speed REAL, heading INTEGER, + temperature FLOAT, relative_humidity FLOAT, pressure FLOAT, + pm_1 FLOAT, pm_2_5 FLOAT, pm_4 FLOAT, pm_10 FLOAT, extra TEXT, exported TINYINT DEFAULT 0 )'''; /// Retrieves the internal [Database] instance, and if not instantiated, instantiates it. - Future get _database async { - if (_db == null) - this._db = openDatabase( + Future get _mainDatabase async { + if (_maindb == null) + this._maindb = openDatabase( join((await getDatabasesPath()), 'logair_data.db'), - version: 2, - onCreate: (db, version) => db.execute(createString) + version: 3, + onCreate: (db, version) => db.execute(this._createMainDBString) ); - return this._db; + return this._maindb; } - /// Purges the [Database] of all internal. + /// Purges the [Database] of all internal data. Future recreateDB() async { - final Database database = await _database; + final Database database = await _mainDatabase; await database.rawQuery('DROP TABLE IF EXISTS data'); - await database.rawQuery(createString); + await database.rawQuery(this._createMainDBString); } /// Inserts data into the [Database]. - Future insertData(DataPacket packet, String deviceId, String url) async { - final Database database = await _database; + Future insertData(DataPacket packet) async { + final Database database = await this._mainDatabase; Map insert = packet.toMap(); - insert['device_id'] = deviceId; - insert['url'] = url; await database.insert( 'data', insert, @@ -71,7 +60,7 @@ class DatabaseHandler { /// Retrieves all of the [DataPacket]s inserted into the [Database]. Future> get data async { - final Database database = await _database; + final Database database = await this._mainDatabase; final List> data = await database.query('data'); @@ -79,11 +68,11 @@ class DatabaseHandler { } /// Closes the [Database] instance. - Future close() async => (await this._database).close(); + Future close() async => (await this._mainDatabase).close(); /// Streams the amount of elements contained within the [Database]. Stream getDBSize() async* { - final Database database = await _database; + final Database database = await this._mainDatabase; while (true) { final List> data = await database.query('data', columns: ['COUNT(*) AS count']); yield data[0]['count'] ?? 0; @@ -93,7 +82,7 @@ class DatabaseHandler { /// Retrieves all of the unsent packets in the [DataPacket]. Future>> getUnsent(int limit) async { - final Database database = await _database; + final Database database = await this._mainDatabase; final List> data = await database.query('data', where: 'exported = ?', whereArgs: [0], @@ -106,7 +95,7 @@ class DatabaseHandler { /// This flags all of the internal packets that are given as [ids] as exported to the external servers. Future setExported(List ids) async { - final Database database = await _database; + final Database database = await this._mainDatabase; int count = await database.rawUpdate('UPDATE data SET exported = 1 WHERE id IN (${ids.join(', ')})'); @@ -115,7 +104,7 @@ class DatabaseHandler { /// Used to monitor the amount of unsent [DataPacket]s. Stream getUnsentPacketsLength() async* { - final Database database = await _database; + final Database database = await this._mainDatabase; while (true) { final List> data = await database.query('data', columns: ['COUNT(*) AS count'], @@ -129,7 +118,7 @@ class DatabaseHandler { /// Retrieves the latest 5 seconds of data every 5 seconds. Stream> getLatest() async* { - final Database database = await _database; + final Database database = await _mainDatabase; while (true) { final List> data = await database.rawQuery('SELECT MIN(timestamp_nix) AS start, AVG(pm_1) AS pm_1, AVG(pm_2_5) AS pm_2_5, AVG(pm_4) AS pm_4, AVG(pm_10) AS pm_10 FROM (SELECT * FROM data WHERE timestamp_nix >= ${DateTime.now().millisecondsSinceEpoch - 10000} ORDER BY id DESC LIMIT 5)'); yield data[0] ?? Map(); @@ -139,10 +128,26 @@ class DatabaseHandler { /// Retrieves the latest [maxResults] packets inserted into the [Database]. Future> getLastPackets(int maxResults) async { - final Database database = await _database; + final Database database = await this._mainDatabase; final List> data = await database.rawQuery('SELECT * FROM data ORDER BY id DESC LIMIT $maxResults;'); return List.generate(data.length, (i) => DataPacket.fromMappedData(data[i])); } + + /// Retrieves the latest packet inserted into the [Database]. + Stream getLatestData() async* { + final Database database = await _mainDatabase; + while (true) { + final List> data = await database.rawQuery('SELECT * FROM data ORDER BY timestamp_nix DESC LIMIT 1'); + DataPacket packet = (data.length > 0) ? DataPacket.fromMappedData(data[0]) : null; + + // Ignore packets older than 5 seconds. + if (packet != null && packet.timestamp + 5000 >= DateTime.now().millisecondsSinceEpoch) { + yield packet; + } else + yield null; + await Future.delayed(Duration(seconds: 1)); + } + } } \ No newline at end of file diff --git a/lib/logic/handlers/network_handler.dart b/lib/logic/handlers/network_handler.dart index 12a3447726083088f5346638e269ff1fe3e8787a..2c96ea5d4060ac843843e7ef0c50b240ab560a46 100644 --- a/lib/logic/handlers/network_handler.dart +++ b/lib/logic/handlers/network_handler.dart @@ -3,15 +3,18 @@ import 'dart:convert'; import 'dart:collection'; import 'package:connectivity/connectivity.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:http/http.dart' as http; import 'package:logair_application/logic/data_header.dart'; import 'package:logair_application/logic/data_packet.dart'; import 'package:logair_application/logic/handlers/data_handler.dart'; -import 'package:logair_application/logic/handlers/database_handler.dart'; +import 'package:logair_application/logic/handlers/main_database_handler.dart'; import 'package:logair_application/logic/handlers/preference_handler.dart'; import 'package:logair_application/utils/enums/preference_keys.dart'; +import 'package:logair_application/utils/helpers/location_of_interest.dart'; +import 'package:logair_application/utils/utils.dart'; import 'package:rxdart/rxdart.dart'; import 'package:tuple/tuple.dart'; @@ -19,9 +22,11 @@ import 'package:tuple/tuple.dart'; class NetworkHandler { factory NetworkHandler() => _singleton; + /// [NetworkHandler] initializer that sets up listeners on the [PreferencesHandler]. NetworkHandler._internal() { this._pushFrequency.listen((value) { if (value != this._oldPushFrequency) { + // Resets the timer if the value has changed. if (this._timer != null) { this._timer.cancel(); } @@ -32,10 +37,13 @@ class NetworkHandler { } }); - /// Define the interval between attempts to send the data to the server. (default: 1 minute). + /// Retrieve the interval between attempts to send the data to the server. (default: 1 minute). PreferencesHandler().getPreferencesInt(PreferenceKeys.NET__MAX_ITEMS_PER_PUSH.key).then((value) => this._pushAmount.value = value); + + /// Retrieve the interval between attempts to send the data to the server. (default: 1 minute). PreferencesHandler().getPreferencesInt(PreferenceKeys.NET__TIME_TO_PUSH.key).then((value) => this._pushFrequency.value = value); + /// Checks the [PreferencesHandler] every 20 seconds on Preference updates. new Timer.periodic(Duration(seconds: 20), (Timer t) async { this._pushAmount.value = await PreferencesHandler().getPreferencesInt(PreferenceKeys.NET__MAX_ITEMS_PER_PUSH.key); this._pushFrequency.value = await PreferencesHandler().getPreferencesInt(PreferenceKeys.NET__TIME_TO_PUSH.key); @@ -43,6 +51,7 @@ class NetworkHandler { } + /// Kills the timer various timers and [BehaviorSubject]s. @override NetworkHandler.dispose() { if (this._timer != null) { @@ -55,16 +64,28 @@ class NetworkHandler { static final NetworkHandler _singleton = new NetworkHandler._internal(); + /// The reference [DateTime] of the last time data was sent to LogAir's servers. DateTime _lastServerTransmission; - int a = 0; - + /// [BehaviorSubject] concerning the amount of data that should be pushed to the server per attempt. BehaviorSubject _pushAmount = BehaviorSubject(); + + /// [BehaviorSubject] concerning the frequency of attempts to push to the server. BehaviorSubject _pushFrequency = BehaviorSubject(); + + /// A reference to the old frequency in order to limit updates. int _oldPushFrequency = -1; + + /// The frequency of the updates. Duration _duration; - Timer _timer; + /// The [Timer] that triggers the attempt to push to the server. + Timer _timer; + + /// Proceeds to check whether an attempt to push to the server should be attempted. + /// + /// If the user set [PreferenceKeys.NET__USE_MOBILE_NET] is set to true and 3G/4G/5G or Wi-Fi is available, then it will attempt to send data to the server. + /// Else it will only attempt when Wi-Fi is available. Future checkNetworkAllowed() async { ConnectivityResult connectivity = await Connectivity().checkConnectivity(); return !(connectivity == ConnectivityResult.none || @@ -82,21 +103,21 @@ class NetworkHandler { /// The default endpoint. String url = 'https://api.logair.unige.ch/v1/service'; - if (await DatabaseHandler().getUnsentPacketsLength().first > 0) { - DataPacket source = (await DatabaseHandler().getUnsent(_pushAmount.value ?? PreferenceKeys.NET__MAX_ITEMS_PER_PUSH.defaultValue))[0].item2; - // TODO set url / clean up - DataHeader().setHeaderFromExisting(deviceId: source.deviceId(), url: ''); - } - /// Break conditions ([DataHeader] unset or no [DataPacket] available). - else if (!DataHeader().headerSet || DataHandler().getData().length == 0) + // Send data in a serialized manner (FIFO) + if (await MainDatabaseHandler().getUnsentPacketsLength().first > 0) { + DataPacket source = (await MainDatabaseHandler().getUnsent(_pushAmount.value ?? PreferenceKeys.NET__MAX_ITEMS_PER_PUSH.defaultValue))[0].item2; + DataHeader().setHeaderFromExisting(deviceId: source.deviceId, url: source.url); + } else { return; + } - /// Get the map representation of a [DataHeader]. + /// The map representation of a [DataHeader]. Map headerData = DataHeader().jsonify(); - List> databaseData = await DatabaseHandler().getUnsent(_pushAmount.value ?? PreferenceKeys.NET__MAX_ITEMS_PER_PUSH.defaultValue); + /// The indexed [DataPacket]s to be sent (user set or default value @see [PreferenceKeys.NET__MAX_ITEMS_PER_PUSH]). + List> databaseData = await MainDatabaseHandler().getUnsent(_pushAmount.value ?? PreferenceKeys.NET__MAX_ITEMS_PER_PUSH.defaultValue); - /// Get all the [DataPacket]s to be sent (maximum 100). + /// The [DataPacket]s to be sent. List packets = databaseData.map((x) => x.item2).toList(); /// Convert the [DataPacket] list to a [Map] of their [List] representations. @@ -104,7 +125,7 @@ class NetworkHandler { 'data' : LinkedHashSet>.from(packets.map((e) => e.jsonify()).toList()).toList() }; - /// Merge all of the [Map]s into 1. + /// Merge all of the [Map]s. Map postData = new Map(); postData.addAll(headerData); postData.addAll(packetList); @@ -117,7 +138,7 @@ class NetworkHandler { headers: { "accept": "application/json", "content-type": "application/json" }, body: json.encode(postData) ); - /// On error, the error should be ignored and not break the thread. + /// On error, the error should be ignored and not hang/break the thread. } catch (e) { print("${e.toString()} COULDN'T CONNECT"); response = null; @@ -125,10 +146,9 @@ class NetworkHandler { /// If the response is defined as an instance of [http.Response], check it's status code and remove all of the packets from the log. if (response != null && response.statusCode == 200) { - for (int i = 0; i < packets.length; i++) { - DataHandler().pop(); - } - await DatabaseHandler().setExported(databaseData.map((x) => x.item1).toList()); + /// If the transfer was successful () + + await MainDatabaseHandler().setExported(databaseData.map((x) => x.item1).toList()); print('RESPONSE ${response.statusCode}'); this._lastServerTransmission = DateTime.now(); } @@ -168,5 +188,62 @@ class NetworkHandler { } } - //TODO Generate a stream from a place for Place display + Future _getLocationOfInterestData({@required double latitude, @required double longitude}) async { + if (!(await checkNetworkAllowed())) return null; + + // TODO retrieve from preferences. + double focus = 0.0001; + int age = 60000; + + /// The default endpoint. + // TODO get focus from preferences + String url = 'https://api.logair.unige.ch/v1/geo/proximity?lat=$latitude&lng=$longitude&age=$age&focus=$focus'; + /// Attempt to send the data to the endpoint via HTTP POST request. + http.Response response; + try { + response = await http.get( + url, + headers: { "accept": "application/json", "content-type": "application/json" }, + ); + /// On error, the error should be ignored and not break the thread. + } catch (e) { + print("${e.toString()} COULDN'T CONNECT"); + response = null; + } + + return response; + } + + /// Retrieve the average [DataPacket] representation of data collected in a user-defined [LocationOfInterest]. + Stream generateLocationOfInterestStream({@required LocationOfInterest loi}) async* { + while (true) { + http.Response response; + try { + response = await _getLocationOfInterestData(latitude: loi.latitude, longitude: loi.longitude); + } catch (e) { + print("${e.toString()} COULDN'T CONNECT"); + yield null; + } + + DataPacket packet; + + if (response != null && response.statusCode == 200) { + Map responseBody = json.decode(response.body); + + packet = DataPacket.fromData( + 0, + loi.latitude, loi.longitude, 0, 0, 0, + castDoubleWrapper(responseBody['avg_temperature']), castDoubleWrapper(responseBody['avg_pressure']), castDoubleWrapper(responseBody['avg_relative_humidity']), + castDoubleWrapper(responseBody['avg_pm_1']), castDoubleWrapper(responseBody['avg_pm_2_5']), castDoubleWrapper(responseBody['avg_pm_4']), castDoubleWrapper(responseBody['avg_pm_10']), + '' + ); + + yield packet; + } else + yield null; + + // TODO: Preferences. + await Future.delayed(Duration(seconds: 60)); + } + } } \ No newline at end of file diff --git a/lib/logic/handlers/position_handler.dart b/lib/logic/handlers/position_handler.dart index 4413f69d2ad732d3940c94064491de876bb49510..de6b415a12607201812f600bc1b26158971da369 100644 --- a/lib/logic/handlers/position_handler.dart +++ b/lib/logic/handlers/position_handler.dart @@ -6,12 +6,14 @@ import 'package:permission_handler/permission_handler.dart'; /// Handler enabling the frequent polling of the devices [Position] class PositionHandler { - /// [Singleton]() instantiation factory. + /// [PositionHandler] instantiation factory. factory PositionHandler() => _singleton; - PositionHandler._internal(); + PositionHandler._internal() { + new Timer.periodic(Duration(seconds: 1), (timer) => this._timeSinceLastChange += 1); + } - /// Internal static [Singleton]() reference. + /// Internal static [PositionHandler] reference. static final PositionHandler _singleton = new PositionHandler._internal(); /// The last known [Position] of the device. @@ -26,16 +28,19 @@ class PositionHandler { /// The current [PermissionGroup] right's status. GeolocationStatus _geolocationStatus; + /// The time since the [Position] was last updated. int _timeSinceLastChange = 0; + // TODO: GPS.ACCURACY integrate /// Default location tweaking options. - /// TODO GPS.ACCURACY integrate LocationOptions locationOptions = LocationOptions(accuracy: LocationAccuracy.high, distanceFilter: 0, timeInterval: 1); /// Get whether the user has granted the location permissions. Stream getGeolocationStatus() async* { while (true) { - yield _geolocationStatus; + yield this._geolocationStatus; + this._timeSinceLastChange = 0; + await Future.delayed(Duration(seconds: 1)); } } @@ -84,4 +89,12 @@ class PositionHandler { await Future.delayed(Duration(milliseconds: 1000)); } } + + /// Retrieves the time since the [Position] was last acquired, in seconds, once per second. + Stream get timeSinceLastPositionAcquired async* { + while(true) { + yield this._timeSinceLastChange; + await Future.delayed(Duration(seconds: 1)); + } + } } \ No newline at end of file diff --git a/lib/logic/handlers/preference_handler.dart b/lib/logic/handlers/preference_handler.dart index f8f56c5e0ab865db8b7ba65e1b0bef51a60c72b2..747ebdace6a050969b6806ec4b3d08b352f276c0 100644 --- a/lib/logic/handlers/preference_handler.dart +++ b/lib/logic/handlers/preference_handler.dart @@ -1,3 +1,6 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_blue/flutter_blue.dart'; +import 'package:geolocator/geolocator.dart'; import 'package:logair_application/utils/enums/preference_keys.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -28,6 +31,7 @@ class PreferencesHandler { this._sharedPreferencesInternal = await SharedPreferences.getInstance(); /// Initial Setup + print('Setting up preferences'); PreferenceKeys.list.forEach((PreferenceKeys prefs) { Function setter; switch (prefs.type) { @@ -104,17 +108,25 @@ class PreferencesHandler { await setter(pref.key, pref.defaultValue); } - /// This is a generic function to reset all preferences to their default values. + /// Resets all preferences to their default values. Future resetAllPreferences() async { - PreferenceKeys.list.forEach( + PreferenceKeys.list.where((PreferenceKeys k) => k != PreferenceKeys.GEN__SHARE && k != PreferenceKeys.GEN__FIRST).forEach( (PreferenceKeys e) async => await resetPreference(e.key) ); } + /// Retrieves the [LocationAccuracy] value from the [PreferencesHandler]. Stream get currentDesiredGPSAccuracy async* { while (true) { yield await getPreferencesInt(PreferenceKeys.GPS__ACCURACY.key); await Future.delayed(Duration(milliseconds: 200)); } } + + void addToFrequentDevices({@required BluetoothDevice device}) async { + // TODO check if in fr devices, + // true => add to first place, shift others back. + // false => add to first, plonk last if len > 10. + + } } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 744d2830ed7eb79fdb45088efe8ea1bccb18dfc2..faaad8ae384a030c380653b7cef785b2fce7e358 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,8 @@ import 'package:flutter/material.dart'; import 'package:device_preview/device_preview.dart'; import 'package:flutter/services.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:logair_application/logic/handlers/locations_interest_database_handler.dart'; +import 'package:logair_application/logic/handlers/main_database_handler.dart'; import 'package:logair_application/logic/handlers/network_handler.dart'; import 'package:logair_application/logic/handlers/preference_handler.dart'; @@ -27,6 +29,9 @@ class LogAirApplication extends StatelessWidget { PreferencesHandler(); NetworkHandler(); + MainDatabaseHandler(); + LocationsOfInterestDatabaseHandler(); + SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, ]); diff --git a/lib/ui/components/body/bluetooth/bluetooth_selection.dart b/lib/ui/components/body/bluetooth/bluetooth_selection.dart new file mode 100644 index 0000000000000000000000000000000000000000..70f01dc8675a4401755903fb0afa9108534dd057 --- /dev/null +++ b/lib/ui/components/body/bluetooth/bluetooth_selection.dart @@ -0,0 +1,272 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_blue/flutter_blue.dart'; + +import 'package:logair_application/localization/localization.dart'; +import 'package:logair_application/logic/handlers/bluetooth_le_handler.dart'; +import 'package:logair_application/ui/components/body/bluetooth/bluetooth_unusable_dialog.dart'; +import 'package:logair_application/ui/components/body/bluetooth/bt_scan_tile.dart'; +import 'package:logair_application/utils/helpers/device.dart'; + +import 'package:rxdart/rxdart.dart'; + +class BluetoothDialog extends StatefulWidget { + BluetoothDialog({Key key}) : super(key: key); + + @override + _BluetoothDialogState createState() => _BluetoothDialogState(key: key); +} + +// TODO Doc +class _BluetoothDialogState extends State { + _BluetoothDialogState({Key key}); + + + BehaviorSubject _bluetoothUsable = BehaviorSubject(); + bool _bluetoothAvailable = false; + bool _bluetoothAvailabilityChecked = false; + bool _showingDialog = false; + + BluetoothState _state = BluetoothState.off; + + + List _frequentDevices; + GlobalKey _key = GlobalKey(); + + @override + void initState() { + super.initState(); + + if (!this._bluetoothAvailabilityChecked) { + print('Checking BTAvail'); + BTLEHandler().fblueInstance.isAvailable.then( + (bool value) => setState(() { + this._bluetoothAvailabilityChecked = true; + this._bluetoothAvailable = value; + this._bluetoothUsable.value = value && (_state != BluetoothState.off); + }) + ); + } + + + BTLEHandler().fblueInstance.state.first.then( + (newState) { + setState(() { + this._bluetoothUsable.value = this._bluetoothAvailable && (newState != BluetoothState.off); + _state = newState; + }); + } + ); + + BTLEHandler().fblueInstance.state.listen((BluetoothState newState) { + if (mounted && _state != newState) { + this._bluetoothUsable.value = this._bluetoothAvailable && (newState != BluetoothState.off); + setState(() { + _state = newState; + }); + } + }); + + } + + @override + void dispose() { + _bluetoothUsable.close(); + super.dispose(); + } + + Widget _buildSubZoneTitle({@required String text}) => + Container( + width: double.maxFinite, + alignment: Alignment.center, + padding: EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration( + color: Colors.blueAccent[400], + boxShadow: [ + BoxShadow( + offset: Offset(2, -2), + color: Colors.grey[100] + ) + ], + borderRadius: BorderRadius.only(bottomLeft: Radius.circular(5), bottomRight: Radius.circular(5)) + ), + child: Text( + text, + style: TextStyle( + fontSize: 20, + color: Colors.white + ), + ), + ); + + @override + Widget build(BuildContext context) { + this._bluetoothUsable.listen((bool value) { + if (!value && !_showingDialog) { + _showingDialog = true; + showBluetoothUnusableDialog(context: context, btAvailable: this._bluetoothAvailable); + } else if (value && _showingDialog) { + _showingDialog = false; + Navigator.of(context).pop(); + } + }); + + return SafeArea( + child: Scaffold( + key: _key, + appBar: AppBar( + title: Text( + AppLocalization.of(context).bluetoothScanMessage, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22 + ) + ), + actions: [ + IconButton( + padding: EdgeInsets.all(10), + icon: Icon(Icons.refresh), + onPressed: () async { + if (this._bluetoothUsable.value) { + if (await BTLEHandler().fblueInstance.isScanning.first) + await BTLEHandler().fblueInstance.stopScan(); + try { + await BTLEHandler().disconnect(); + await BTLEHandler().fblueInstance.startScan(timeout: Duration(seconds: 10)); + } catch (e) { } + } + } + ) + ], + ), + + body: RefreshIndicator( + + onRefresh: () async { + if (this._bluetoothUsable.value) { + if (await BTLEHandler().fblueInstance.isScanning.first) + await BTLEHandler().fblueInstance.stopScan(); + try { + await BTLEHandler().disconnect(); + await BTLEHandler().fblueInstance.startScan(timeout: Duration(seconds: 5)); + } catch (e) { } + } + }, + + child: Container( + height: double.maxFinite, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: EdgeInsets.all(0), + child: Container( + padding: EdgeInsets.all(0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + _buildSubZoneTitle(text: AppLocalization.of(context).bluetoothFrequent), + StreamBuilder>( + stream: BTLEHandler().fblueInstance.scanResults.transform( + new StreamTransformer.fromHandlers( + handleData: (List list, EventSink> sink) => + sink.add( + list.where( + (result) { + bool intermediate = false; + if (this._frequentDevices != null) { + intermediate = _frequentDevices.map((Device d) => d.address).contains(result.device.id.toString()); + } + return intermediate && result.device.name.length > 0; + } + ).toList() + ) + ) + ), + initialData: [], + builder: (context, snapshot) { + if (snapshot.data.length > 0) { + return Column( + children: snapshot.data.map((result) { + return BTLEScanTile( + result: result, + onTap: () { + BTLEHandler().setDevice(result.device); + BTLEHandler().connect(); + Navigator.of(context).pop(true); + } + ); + }).toList() + ); + } else { + return Container( + padding: EdgeInsets.symmetric(vertical: 5), + child: Text( + AppLocalization.of(context).none, + style: Theme.of(context).textTheme.subtitle1, + ) + ); + } + } + ), + Divider(indent: 20, endIndent: 20, color: Colors.grey[600], thickness: 1,), + _buildSubZoneTitle(text: AppLocalization.of(context).bluetoothAvailable), + + StreamBuilder>( + stream: BTLEHandler().fblueInstance.scanResults.transform( + new StreamTransformer.fromHandlers( + handleData: (List list, EventSink> sink) => + sink.add( + list.where( + (result) { + bool intermediate = true; + if (this._frequentDevices != null) { + intermediate = !_frequentDevices.map((Device d) => d.address).contains(result.device.id.toString()); + } + return intermediate && result.device.name.length > 0; + } + ).toList() + ) + ) + ), + initialData: [], + builder: (context, snapshot) { + if (snapshot.data.length > 0) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: snapshot.data.map((result) { + return BTLEScanTile( + result: result, + onTap: () { + BTLEHandler().setDevice(result.device); + BTLEHandler().connect(); + Navigator.of(context).pop(true); + } + ); + }).toList() + ); + } else { + return Container( + padding: EdgeInsets.symmetric(vertical: 5), + child: Text( + AppLocalization.of(context).none, + style: Theme.of(context).textTheme.subtitle1, + ) + ); + } + } + ), + ], + ), + ), + ), + ), + ), + ) + ); + } +} \ No newline at end of file diff --git a/lib/ui/components/body/bluetooth/bluetooth_unusable_dialog.dart b/lib/ui/components/body/bluetooth/bluetooth_unusable_dialog.dart new file mode 100644 index 0000000000000000000000000000000000000000..f6a0f2eb374c18490441d210aa0e7daa61f84dba --- /dev/null +++ b/lib/ui/components/body/bluetooth/bluetooth_unusable_dialog.dart @@ -0,0 +1,87 @@ +import 'dart:ui'; + +import 'package:app_settings/app_settings.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:logair_application/utils/decorations.dart'; + +Widget _buildDialogButton({@required String label, @required BoxDecoration decoration, @required EdgeInsets margin, @required Function action}) { + return GestureDetector( + onTap: () => action(), + child: Container( + padding: EdgeInsets.symmetric(vertical: 8, horizontal: 10), + margin: margin, + decoration: decoration, + child: Text( + label, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 18 + ), + ), + ), + ); +} + +dynamic showBluetoothUnusableDialog({@required BuildContext context, @required bool btAvailable}) { + return showDialog( + context: context, + barrierDismissible: false, + useSafeArea: true, + useRootNavigator: true, + barrierColor: Color.fromARGB(0x55, 0xAC, 0xAC, 0xAC), + builder: (context) { + return WillPopScope( + // ignore: missing_return + onWillPop: () {}, + child: AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(20.0)) + ), + title: Column( + children: [ + Icon( + Icons.bluetooth_disabled_outlined, + size: 36, + color: Colors.red, + ), + Divider(height: 10, thickness: 0,), + Text( + 'Bluetooth is not ' + ((btAvailable) ? 'enabled' : 'supported') + '\non this device.', + textAlign: TextAlign.center, + ), + ] + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: (btAvailable) ? _buildDialogButton( + label: 'Open Settings', + decoration: containerNeumorphicDecoration, + margin: EdgeInsets.only(right: 5), + action: () => AppSettings.openBluetoothSettings() + ) : (SizedBox()), + ), + Expanded( + child: _buildDialogButton( + label: 'Go Back', + decoration: errorContainerNeumorphicDecoration, + margin: EdgeInsets.only(left: 5), + action: () => Navigator.of(context).popUntil((Route route) => route.isFirst), + ), + ), + ] + ) + ], + ), + ), + + ); + } + ); +} \ No newline at end of file diff --git a/lib/ui/components/dialog/bluetooth_dialog/bt_scan_tile.dart b/lib/ui/components/body/bluetooth/bt_scan_tile.dart similarity index 71% rename from lib/ui/components/dialog/bluetooth_dialog/bt_scan_tile.dart rename to lib/ui/components/body/bluetooth/bt_scan_tile.dart index 434577f5270ba61a664ae9015ca1ab91bb3b04b4..d3c207c68184e9d03e4e1b4d76a336e77ea034eb 100644 --- a/lib/ui/components/dialog/bluetooth_dialog/bt_scan_tile.dart +++ b/lib/ui/components/body/bluetooth/bt_scan_tile.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_blue/flutter_blue.dart'; +import 'package:logair_application/utils/decorations.dart'; class BTLEScanTile extends StatelessWidget { /// The [ScanResult] on which the information is based. @@ -14,7 +15,12 @@ class BTLEScanTile extends StatelessWidget { @override Widget build(BuildContext context) { return ListTile( + contentPadding: EdgeInsets.symmetric(horizontal: 3, vertical: 0), + dense: true, + visualDensity: VisualDensity.compact, title: Container( + decoration: genericContainerNeumorphicDecoration, + padding: EdgeInsets.symmetric(vertical: 6, horizontal: 8), child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, @@ -23,12 +29,12 @@ class BTLEScanTile extends StatelessWidget { result.device.name, overflow: TextOverflow.fade, style: TextStyle( - fontSize: 18 + fontSize: 20 ), ), Text( result.device.id.toString(), - style: Theme.of(context).textTheme.caption, + style: Theme.of(context).textTheme.caption.copyWith(fontSize: 14), ), ], ), diff --git a/lib/ui/components/body/data/data_widget.dart b/lib/ui/components/body/data/data_widget.dart index ccaa616ee6083a9595f9834ffc0aa55c0e1ff9c4..c75040a48b3a9479adcd36a9dd7ace8acb224547 100644 --- a/lib/ui/components/body/data/data_widget.dart +++ b/lib/ui/components/body/data/data_widget.dart @@ -1,9 +1,9 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; -import 'package:intersperse/intersperse.dart'; -import 'package:logair_application/logic/handlers/data_handler.dart'; +import 'package:logair_application/logic/handlers/main_database_handler.dart'; +import 'package:logair_application/ui/components/body/data/locations_of_interest_tiles.dart'; import 'package:logair_application/ui/components/common/carousel_card.dart'; import 'package:logair_application/ui/components/body/data/graph/graph.dart'; import 'package:logair_application/ui/components/body/data/tiles/data_tile.dart'; @@ -15,31 +15,13 @@ class DataWidget extends StatelessWidget { Widget build(BuildContext context) { Widget widget = Center( child: Column( - children: [ - /*Container( - height: 45, - decoration: BoxDecoration( - color: Colors.blue[300], - borderRadius: BorderRadius.all(Radius.circular(5)), - ), - child: Row( - children: [ - Expanded( - child: Text( - 'Current', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22 - ), - ), - ), - ], - ), - ),*/ - + children: [ // Local data (Chart + Data points) Graph(), - DataTile(stream: DataHandler().getDataStream(), isDistant: false), + DataTile( + stream: MainDatabaseHandler().getLatestData(), + isDistant: false + ), Divider(indent: 20, endIndent: 20, color: Colors.black, height: 20, thickness: 3,), // Bar (Locations + Add new -> Pop to new route) Container( @@ -61,30 +43,14 @@ class DataWidget extends StatelessWidget { ), ), IconButton( + // Open other (Scaffold + Form) with nominatim etc + adds to DB. onPressed: () => {}, icon: Icon(Icons.add), ) ], ), ), - Column( - children: intersperse( - Divider(indent: 10, endIndent: 10, color: Colors.black, height: 5,), - [ - DataTile(stream: DataHandler().getMockDataStream(), locationName: 'Home'), - DataTile(stream: DataHandler().getMockDataStream(), locationName: 'Work'), - DataTile(stream: DataHandler().getMockDataStream(), locationName: 'C'), - DataTile(stream: DataHandler().getMockDataStream(), locationName: 'D'), - DataTile(stream: DataHandler().getMockDataStream(), locationName: 'E'), - DataTile(stream: DataHandler().getMockDataStream(), locationName: 'F'), - DataTile(stream: DataHandler().getMockDataStream(), locationName: 'G'), - DataTile(stream: DataHandler().getMockDataStream(), locationName: 'H'), - DataTile(stream: DataHandler().getMockDataStream(), locationName: 'I') - ] - ).toList() + [ - SizedBox(height: 30,) - ], - ), + LocationsOfInterestWidget() ], ) ); diff --git a/lib/ui/components/body/data/graph/graph.dart b/lib/ui/components/body/data/graph/graph.dart index d0e5c52e9968a2a2b940737ab4e003717ae652c5..1912513344405e9caafebf7511da81f33c38ef80 100644 --- a/lib/ui/components/body/data/graph/graph.dart +++ b/lib/ui/components/body/data/graph/graph.dart @@ -5,7 +5,7 @@ import 'package:logair_application/ui/components/body/data/graph/graph_colors.da import 'package:logair_application/ui/components/body/data/graph/graph_controller.dart'; import 'package:logair_application/ui/components/body/data/graph/indicator.dart'; -/// This graph displays the average quality of the air in 5 second increments. +/// This [Graph] displays the average quality of the air in 5 second increments. class Graph extends StatefulWidget { Graph({Key key}) : super(key: key); @@ -13,6 +13,7 @@ class Graph extends StatefulWidget { State createState() => _GraphState(); } +/// The [_GraphState] reacts to changes in time (additional data), and updates it's [List] accordingly to draw the line chart. class _GraphState extends State { /// The first registered timestamp. double start; @@ -23,9 +24,16 @@ class _GraphState extends State { /// The maximum y-axis coordinate. double yMax = 0; + /// The [FlSpot]s representing collected PM 1 values. List spotsPM1 = [FlSpot(0, 0)]; + + /// The [FlSpot]s representing collected PM 2.5 values. List spotsPM2_5 = [FlSpot(0, 0)]; + + /// The [FlSpot]s representing collected PM 4 values. List spotsPM4 = [FlSpot(0, 0)]; + + /// The [FlSpot]s representing collected PM 10 values. List spotsPM10 = [FlSpot(0, 0)]; _GraphState() { @@ -91,6 +99,7 @@ class _GraphState extends State { DateTime dtValue = DateTime.fromMillisecondsSinceEpoch( ((value * 1000) + (start ?? DateTime.now().millisecondsSinceEpoch)).round() ); + // If the timestamp is at a value that is a multiple of 20 seconds, draw it in the x-Axis axis titles. if (dtValue.second % 20 == 0 && dtValue.millisecondsSinceEpoch < DateTime.now().millisecondsSinceEpoch) return '${dtValue.hour.toString().padLeft(2, '0')}:${dtValue.minute.toString().padLeft(2, '0')}:${dtValue.second.toString().padLeft(2, '0')}'; return ''; @@ -124,6 +133,7 @@ class _GraphState extends State { maxX: (now ?? 0) + 10, maxY: (this.yMax ?? 0) + 10, minY: 0, + // StreamBuilder ? lineBarsData: [ _generateLine(spotsPM1, GraphColors.PM1.color), _generateLine(spotsPM2_5, GraphColors.PM2_5.color), @@ -134,6 +144,7 @@ class _GraphState extends State { show: true, leftTitle: AxisTitle( showTitle: true, + // TODO Internationalize titleText: 'PM density µg/m³', margin: 0, textStyle: TextStyle( @@ -143,6 +154,7 @@ class _GraphState extends State { ), bottomTitle: AxisTitle( showTitle: true, + // TODO Internationalize titleText: 'Time', margin: 0, textStyle: TextStyle( @@ -161,11 +173,11 @@ class _GraphState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.center, + // Draw Indicators with the same color / label data as the graph. children: GraphColors.list.map((GraphColors gc) => Indicator( text: gc.label, color: gc.color, - isSquare: true, ) ).toList() ), diff --git a/lib/ui/components/body/data/graph/graph_controller.dart b/lib/ui/components/body/data/graph/graph_controller.dart index bf6ec3fa53aff251a14936430b92a73bc943734e..75da192485e30f0a60eda0db4dffe6e7e5fc3b45 100644 --- a/lib/ui/components/body/data/graph/graph_controller.dart +++ b/lib/ui/components/body/data/graph/graph_controller.dart @@ -1,5 +1,5 @@ import 'package:fl_chart/fl_chart.dart'; -import 'package:logair_application/logic/handlers/database_handler.dart'; +import 'package:logair_application/logic/handlers/main_database_handler.dart'; import 'package:logair_application/ui/components/body/data/data_widget.dart'; /// This Controller type class serves to remove the data aspect of the [LineChart] from Flutter's build tree, @@ -12,7 +12,7 @@ class GraphController { GraphController._internal() { /// Every 5 seconds, averaged values are taken from the database and streamed. /// This listener appends the data to the storage. - DatabaseHandler().getLatest().listen((newData) { + MainDatabaseHandler().getLatest().listen((newData) { this.start = this.start ?? DateTime.now().millisecondsSinceEpoch * 1.0; this.now = (DateTime.now().millisecondsSinceEpoch - this.start) / 1000; // Get max y value to avoid out of bounding on y-axis diff --git a/lib/ui/components/body/data/graph/indicator.dart b/lib/ui/components/body/data/graph/indicator.dart index fec521b094995bbea646bc8a525373bec39c791d..6887c5ffbac3c401cbf822e586bf1ca29b23d84e 100644 --- a/lib/ui/components/body/data/graph/indicator.dart +++ b/lib/ui/components/body/data/graph/indicator.dart @@ -2,9 +2,11 @@ import 'package:flutter/material.dart'; /// Small label [Widget] that serves as Caption for the [Graph] class Indicator extends StatelessWidget { + /// The [Color] of the [Indicator]. final Color color; + + /// The label of the [Indicator]. final String text; - final bool isSquare; final double size; final Color textColor; @@ -12,7 +14,6 @@ class Indicator extends StatelessWidget { Key key, this.color, this.text, - this.isSquare, this.size = 16, this.textColor = const Color(0xff505050), }) : super(key: key); @@ -27,7 +28,7 @@ class Indicator extends StatelessWidget { width: size, height: size, decoration: BoxDecoration( - shape: isSquare ? BoxShape.rectangle : BoxShape.circle, + shape: BoxShape.rectangle, color: color, borderRadius: BorderRadius.circular(3) ), diff --git a/lib/ui/components/body/data/locations_of_interest_tiles.dart b/lib/ui/components/body/data/locations_of_interest_tiles.dart new file mode 100644 index 0000000000000000000000000000000000000000..e834ce04c0991c9688b9d52c8b93e5418b0d749c --- /dev/null +++ b/lib/ui/components/body/data/locations_of_interest_tiles.dart @@ -0,0 +1,63 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:intersperse/intersperse.dart'; +import 'package:logair_application/logic/handlers/locations_interest_database_handler.dart'; +import 'package:logair_application/logic/handlers/network_handler.dart'; +import 'package:logair_application/ui/components/body/data/tiles/data_tile.dart'; +import 'package:logair_application/utils/helpers/location_of_interest.dart'; + +class LocationsOfInterestWidget extends StatefulWidget { + + + @override + State createState() => _LocationsOfInterestWidgetState(); +} + +class _LocationsOfInterestWidgetState extends State { + _LocationsOfInterestWidgetState(); + + List _children = []; + + Future> _generateChildren() async { + List loi = await LocationsOfInterestDatabaseHandler().data; + + return []; + } + + @override + void initState() { + super.initState(); + // TODO Listeners. + + LocationsOfInterestDatabaseHandler().onUpdate.listen((_) async { + setState(() => { + + }); + }); + } + + @override + Widget build(BuildContext context) { + return Column( + children: intersperse( + Divider(indent: 10, endIndent: 10, color: Colors.black, height: 5,), + [ + DataTile( + stream: NetworkHandler().generateLocationOfInterestStream( + loi: LocationOfInterest( + id: 0, + latitude: 46.1764826, + longitude: 6.1400497, + name: 'FacLab' + ) + ), + isDistant: true, + locationName: 'FacLab', + ), + ] + ).toList() + [ + SizedBox(height: 30,) + ], + ); + } +} \ No newline at end of file diff --git a/lib/ui/components/body/data/tiles/data_tile.dart b/lib/ui/components/body/data/tiles/data_tile.dart index 77d82be2c5e3ca88f6e9fa476e85b361bec96477..e62bbd88518a9d72d461875871eef530625ae001 100644 --- a/lib/ui/components/body/data/tiles/data_tile.dart +++ b/lib/ui/components/body/data/tiles/data_tile.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:logair_application/logic/data_packet.dart'; import 'package:logair_application/utils/enums/pm_symbol.dart'; -//TODO Add weather ? +//TODO Add weather ? (Not feasible unless negotiate free API access) /// Widget that displays the data specific to a specific place, depending on the stream provided. /// Data displayed is PM1, PM2.5, PM4, PM10, Temperature, Pressure, Relative Humidity. /// @@ -62,90 +62,105 @@ class _DataTileState extends State { _DataTileState(this.stream, this.isDistant, this.locationName) { stream.listen((DataPacket newPacket) { // Checks that the packet contains actual data. - if (newPacket != null && this.mounted) + if (newPacket != null && this.mounted) { setState(() { - _pm1 = newPacket.pm1(); - _pm2_5 = newPacket.pm2_5(); - _pm4 = newPacket.pm4(); - _pm10 = newPacket.pm10(); - _temperature = newPacket.temperature(); - _pressure = newPacket.pressure(); - _relativeHumidity = newPacket.relativeHumidity(); + _pm1 = newPacket.pm1; + _pm2_5 = newPacket.pm2_5; + _pm4 = newPacket.pm4; + _pm10 = newPacket.pm10; + _temperature = newPacket.temperature; + _pressure = newPacket.pressure; + _relativeHumidity = newPacket.relativeHumidity; }); + } }); } @override Widget build(BuildContext context) { /// Builds an indicator for a specific PM value, with appropriate AQI color scheme - Widget _buildPMDataCell(PMSymbol pmSymbol, double value) => Expanded( - flex: 1, - child: Container( - padding: EdgeInsets.symmetric(vertical: 3), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5), - color: (value != -1 && value != null) ? pmSymbol.colorizeValue(value) : Colors.grey[200], - ), - child: Column( - children: [ - Text( - pmSymbol.key, - style: TextStyle( - fontSize: 19, - color: (value != -1 && value != null) ? Colors.black : Colors.grey[500] - ), - ), - SizedBox(height: 4), - Text( - (value != -1 && value != null) ? value.toString() : '', - style: TextStyle( - fontSize: 17, - fontWeight: FontWeight.bold + Widget _buildPMDataCell(PMSymbol pmSymbol, double value) { + String valueRepresentation = (value != -1 && value != null) ? value.toStringAsFixed(2) : ''; + + if (valueRepresentation.endsWith('.00')) + valueRepresentation = valueRepresentation.split('.')[0]; + + return Expanded( + flex: 1, + child: Container( + padding: EdgeInsets.symmetric(vertical: 3), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: (value != -1 && value != null) ? pmSymbol.colorizeValue(value) : Colors.grey[200], + ), + child: Column( + children: [ + Text( + pmSymbol.key, + style: TextStyle( + fontSize: 19, + color: (value != -1 && value != null) ? Colors.black : Colors.grey[500] + ), ), - ) - ] + SizedBox(height: 4), + Text( + valueRepresentation, + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.bold + ), + ) + ] + ) ) - ) - ); + ); + } /// Builds the headers indicator for environmental data cells - Widget _buildEnvironmentalDataCells(String title, String unit, double value) => Expanded( - flex: 1, - child: Container( - margin: EdgeInsets.all(2), - padding: EdgeInsets.symmetric(vertical: 3), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5), - color: Colors.grey[200], - ), - child: Column( - children: [ - Text( - title, - style: TextStyle( - fontSize: 20, - color: (value != -1 && value != null) ? Colors.black : Colors.grey[500] - ), - ), - Text( - unit, - style: TextStyle( - fontSize: 14, - color: (value != -1 && value != null) ? Colors.black : Colors.grey[500] + Widget _buildEnvironmentalDataCells({@required String title, @required String unit, @required double value}) { + String valueRepresentation = (value != -1 && value != null) ? value.toStringAsFixed(2) : ''; + + if (valueRepresentation.endsWith('.00')) + valueRepresentation = valueRepresentation.split('.')[0]; + + return Expanded( + flex: 1, + child: Container( + margin: EdgeInsets.all(2), + padding: EdgeInsets.symmetric(vertical: 3), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: Colors.grey[200], + ), + child: Column( + children: [ + Text( + title, + style: TextStyle( + fontSize: 20, + color: (value != -1 && value != null) ? Colors.black : Colors.grey[500] + ), ), - ), - SizedBox(height: 8), - Text( - (value != -1 && value != null) ? value.toString() : '', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold + Text( + unit, + style: TextStyle( + fontSize: 14, + color: (value != -1 && value != null) ? Colors.black : Colors.grey[500] + ), ), - ) - ] + SizedBox(height: 8), + Text( + valueRepresentation, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold + ), + ) + ] + ) ) - ) - ); + ); + } Widget tile = Container( margin: EdgeInsets.symmetric(horizontal: 3, vertical: 3), @@ -199,9 +214,9 @@ class _DataTileState extends State { mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.end, children: [ - _buildEnvironmentalDataCells('Temp.', '(C°)', _temperature), - _buildEnvironmentalDataCells('Pres.', '(hPa)', _pressure), - _buildEnvironmentalDataCells('Rel. Hum.', '(%)', _relativeHumidity), + _buildEnvironmentalDataCells(title: 'Temp.', unit: '(C°)', value: this._temperature), + _buildEnvironmentalDataCells(title: 'Pres.', unit: '(hPa)', value: this._pressure), + _buildEnvironmentalDataCells(title: 'Rel. Hum.', unit: '(%)', value: this._relativeHumidity), ], ), ] @@ -216,6 +231,7 @@ class _DataTileState extends State { child: InkWell( child: tile, splashColor: Color.fromARGB(0xAA, 0xFF, 0x00, 0x00), + // Delete location function (Remove from db) TODO onLongPress: () => print(this.locationName), ) ); diff --git a/lib/ui/components/body/home/initial_dialog.dart b/lib/ui/components/body/home/initial_dialog.dart new file mode 100644 index 0000000000000000000000000000000000000000..db5a760d13a5b6fadc7d9bdbfba2faefac285dd4 --- /dev/null +++ b/lib/ui/components/body/home/initial_dialog.dart @@ -0,0 +1,207 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:logair_application/logic/handlers/preference_handler.dart'; +import 'package:logair_application/ui/components/common/tristate_toggle.dart'; +import 'package:logair_application/utils/decorations.dart'; +import 'package:logair_application/utils/enums/preference_keys.dart'; +import 'package:rxdart/subjects.dart'; + +Widget _buildDialogButton({@required String label, @required BoxDecoration decoration, @required EdgeInsets margin, @required Function action}) { + return GestureDetector( + onTap: () => action(), + child: Container( + padding: EdgeInsets.symmetric(vertical: 8, horizontal: 10), + margin: margin, + decoration: decoration, + child: Text( + label, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 18 + ), + ), + ), + ); +} + +dynamic showInitialDialog({@required BuildContext context}) { + + BehaviorSubject _shareToggleObservable = BehaviorSubject(); + _shareToggleObservable.value = 0; + + // ignore: non_constant_identifier_names + BehaviorSubject _3gToggleObservable = BehaviorSubject(); + _3gToggleObservable.value = 0; + + + BehaviorSubject _displayError = BehaviorSubject(); + _displayError.value = false; + + BehaviorSubject _displayError1 = BehaviorSubject(); + _displayError1.value = false; + + BehaviorSubject _displayError2 = BehaviorSubject(); + _displayError2.value = false; + + _displayError1.stream.listen((bool value) => _displayError.value = value || _displayError2.value); + _displayError2.stream.listen((bool value) => _displayError.value = _displayError1.value || value); + + return showDialog( + context: context, + barrierDismissible: false, + useSafeArea: true, + useRootNavigator: true, + barrierColor: Color.fromARGB(0x55, 0xAC, 0xAC, 0xAC), + builder: (context) { + return WillPopScope( + // ignore: missing_return + onWillPop: () {}, + child: AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(20.0)) + ), + title: Text( + 'Welcome to LogAir.io', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headline4, + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'The following settings require your immediate attention :', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headline6.copyWith(fontSize: 16), + ), + Divider(thickness: 0, indent: 20, endIndent: 20, height: 5,), + StreamBuilder( + stream: _displayError.stream, + initialData: false, + builder: (context, snapshot) { + if (snapshot.data) { + return Text( + 'Both settings need to be set', + style: Theme.of(context).textTheme.subtitle1.copyWith(color: Colors.red) + ); + } else { + return SizedBox(); + } + }, + ), + Divider(color: Colors.grey[600], indent: 20, endIndent: 20, height: 20,), + StreamBuilder( + stream: _displayError1.stream, + initialData: false, + builder: (context, snapshot) { + return Container( + alignment: Alignment.center, + padding: EdgeInsets.symmetric(vertical: 5, horizontal: 5), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Colors.grey[300], + border: Border.all(color: (snapshot.data) ? Colors.red : Colors.transparent) + ), + child: Column( + children: [ + Text( + 'Share data with LogAir servers', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headline6.copyWith(fontSize: 16), + ), + Divider(thickness: 0, indent: 20, endIndent: 20, height: 10,), + Container( + padding: EdgeInsets.symmetric(vertical: 5, horizontal: 3), + child: TristateToggleWidget( + observable: _shareToggleObservable, + ) + ), + ], + ), + ); + }, + + ), + Divider(color: Colors.grey[600], indent: 20, endIndent: 20, height: 20,), + StreamBuilder( + stream: _displayError2.stream, + initialData: false, + builder: (context, snapshot) { + return Container( + alignment: Alignment.center, + padding: EdgeInsets.symmetric(vertical: 5, horizontal: 5), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Colors.grey[300], + border: Border.all(color: (snapshot.data) ? Colors.red : Colors.transparent) + ), + child: Column( + children: [ + Text( + 'Use 3G/4G/5G networks to send and receive application data', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headline6.copyWith(fontSize: 16), + ), + Divider(thickness: 0, indent: 20, endIndent: 20, height: 10,), + Container( + padding: EdgeInsets.symmetric(vertical: 5, horizontal: 3), + child: TristateToggleWidget( + observable: _3gToggleObservable, + ) + ), + ], + ), + ); + } + ), + Divider(height: 30,), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: SizedBox(), + ), + Expanded( + child: _buildDialogButton( + label: 'Confirm', + decoration: containerNeumorphicDecoration, + margin: EdgeInsets.only(left: 5), + action: () async { + int _wantsToShare = _shareToggleObservable.value; + int _wantsTo3g = _3gToggleObservable.value; + + _displayError1.value = _wantsToShare == 0; + _displayError2.value = _wantsTo3g == 0; + + if (_wantsToShare == 0 || _wantsTo3g == 0) { + return; + } + + await PreferencesHandler().setPreferencesBool(PreferenceKeys.GEN__SHARE.key, _wantsToShare == 1); + await PreferencesHandler().setPreferencesBool(PreferenceKeys.NET__USE_MOBILE_NET.key, _wantsTo3g == 1); + + Navigator.of(context).pop(true); + }, + ), + ), + ] + ) + ], + ), + ), + + ); + } + ).whenComplete(() async { + await PreferencesHandler().setPreferencesBool(PreferenceKeys.GEN__FIRST.key, false); + + _shareToggleObservable.close(); + _3gToggleObservable.close(); + _displayError.close(); + _displayError1.close(); + _displayError2.close(); + + Navigator.of(context).popUntil((Route route) => route.isFirst); + }); +} \ No newline at end of file diff --git a/lib/ui/components/body/map/map_widget.dart b/lib/ui/components/body/map/map_widget.dart index 6b187444c54f51524f3b8f84284680bcb11f3858..db8198e5994af1ef9fa0ac7f1685b6343a9c3abf 100644 --- a/lib/ui/components/body/map/map_widget.dart +++ b/lib/ui/components/body/map/map_widget.dart @@ -27,6 +27,7 @@ class MapContainer extends BaseWidget { PolylineLayerOptions( polylines: MapDisplayController().polylines, ), + // TODO: Make circle Behavior subject dependent. CircleLayerOptions( circles: MapDisplayController().pmMarkers, ), @@ -58,7 +59,7 @@ class MapContainer extends BaseWidget { children: [ BaseWidget( builder: (ctx, sizingInformation) => Container( - height: sizingInformation.unnotchedHeight() * 33/40 * 0.16, + height: sizingInformation.unnotchedHeight * 33/40 * 0.16, child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -131,7 +132,7 @@ class MapContainer extends BaseWidget { ), BaseWidget( builder: (ctx, sizingInformation) => Container( - height: sizingInformation.unnotchedHeight() * 33/40 * 0.05, + height: sizingInformation.unnotchedHeight * 33/40 * 0.05, child: Row( children: [ _buildMapLayerSelection('PM 1', () => MapDisplayController().setPMKeyIndex(PMSymbol.PM1)), @@ -147,7 +148,7 @@ class MapContainer extends BaseWidget { children: [ BaseWidget( builder: (ctx, sizingInformation) => Container( - height: sizingInformation.unnotchedHeight() * 33/40 * 0.75, + height: sizingInformation.unnotchedHeight * 33/40 * 0.75, child: Listener( behavior: HitTestBehavior.deferToChild, child: _buildMap() diff --git a/lib/ui/components/body/overview/overview_navigation_button.dart b/lib/ui/components/body/overview/overview_navigation_button.dart index 919909e4eed84d6331f80aa95b2b8f31c6d8f407..146daab17771e6728195a4d80e3e0f1416e24afd 100644 --- a/lib/ui/components/body/overview/overview_navigation_button.dart +++ b/lib/ui/components/body/overview/overview_navigation_button.dart @@ -3,9 +3,15 @@ import 'dart:ui'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +/// Neumorphic-esque button that brings the user to another [Page] when clicked. class OverviewNavigationButton extends StatelessWidget { + /// The leading [Icon] final IconData icon; + + /// The title of the button final String title; + + /// What should happen when the button is pressed. final Function action; OverviewNavigationButton({@required this.icon, @required this.title, @required this.action, Key key}) : super(key: key); diff --git a/lib/ui/components/body/overview/overview_widget.dart b/lib/ui/components/body/overview/overview_widget.dart index fc936b049406afeb6cf460250f6c4b9dbf9b6fde..f32cd7978aab539741f12e557bac78616269d8f0 100644 --- a/lib/ui/components/body/overview/overview_widget.dart +++ b/lib/ui/components/body/overview/overview_widget.dart @@ -5,15 +5,17 @@ import 'package:logair_application/ui/components/body/overview/overview_widgets/ import 'package:logair_application/ui/components/body/overview/overview_widgets/map_position_overview_widget.dart'; import 'package:logair_application/ui/components/body/overview/overview_widgets/network_overview_widget.dart'; import 'package:logair_application/ui/components/body/overview/overview_navigation_button.dart'; -import 'package:logair_application/ui/components/dialog/bluetooth_dialog/dialog.dart'; -import 'package:logair_application/ui/components/header/header.dart'; +import 'package:logair_application/ui/components/body/bluetooth/bluetooth_selection.dart'; +import 'package:logair_application/ui/components/footer/main_footer.dart'; import 'package:logair_application/ui/routes/preferences.dart'; import 'package:logair_application/ui/components/common/base_widget.dart'; +/// [Widget] displaying activity relevant information to the user to provide transparency on the metrics used. class OverviewWidget extends StatelessWidget { OverviewWidget({Key key}) : super(key: key); + /// Builds a [Container] (@see [child]) that holds various status indicators. Container _buildOverviewCell({@required Widget child}) => Container( width: double.infinity, height: double.infinity, @@ -91,7 +93,7 @@ class OverviewWidget extends StatelessWidget { height: double.infinity, child: Column( children: [ - Spacer(flex: 2), + Spacer(flex: 10), /// Access the app settings OverviewNavigationButton( icon: Icons.settings, @@ -99,12 +101,6 @@ class OverviewWidget extends StatelessWidget { action: () => Navigator.push(context, PopForwardRoute(page: SettingsView())) ), Spacer(flex: 1), - OverviewNavigationButton( - icon: Icons.bluetooth, - title: 'Bluetooth', - action: () => Navigator.push(context, PopForwardRoute(page: BluetoothDialog())) - ), - Spacer(flex: 1), OverviewNavigationButton( icon: Icons.book, title: 'Terms of Service', @@ -126,7 +122,7 @@ class OverviewWidget extends StatelessWidget { onLongPress: () { Navigator.of(context).pop(); }, - child: HeaderWidget(false) + child: MainFooter(), ), ), ), diff --git a/lib/ui/components/body/overview/overview_widgets/bt_overview_widget.dart b/lib/ui/components/body/overview/overview_widgets/bt_overview_widget.dart index cd1d625590eceb888ab5cd1c917e1c55daf54335..76415a0c0f60e1b1b39ffbc1a23b2004167c196f 100644 --- a/lib/ui/components/body/overview/overview_widgets/bt_overview_widget.dart +++ b/lib/ui/components/body/overview/overview_widgets/bt_overview_widget.dart @@ -2,111 +2,74 @@ import 'dart:ui'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_blue/flutter_blue.dart'; +import 'package:intersperse/intersperse.dart'; import 'package:logair_application/logic/data_header.dart'; -import 'package:logair_application/ui/components/body/overview/overview_widgets/decorations.dart'; -import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:logair_application/ui/components/body/overview/overview_widgets/common/overview_data_cell.dart'; +import 'package:logair_application/utils/decorations.dart'; +import 'package:logair_application/utils/utils.dart'; class BTOverviewWidget extends StatelessWidget { BTOverviewWidget({Key key}) : super(key: key); - @override - Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Container( - width: 50, - height: 50, - decoration: standardIconDecoration, - child: Icon( - Icons.bluetooth, + /// Builds a [Text] containing the device id. + StreamBuilder _buildDeviceIdWidget() => StreamBuilder( + stream: DataHeader().deviceIdStream(), + initialData: '', + builder: (context, snapshot) => Text( + snapshot.data != '' ? snapshot.data : 'Unknown', + style: TextStyle( + color: Colors.white + ) + ) + ); + + /// Builds a [Row] containing the [BluetoothDevice] battery level and associated [Icon] representation. + StreamBuilder _buildDeviceBatteryLevelWidget() => StreamBuilder( + stream: DataHeader().deviceBatteryLevel(), + initialData: -1, + builder: (context, snapshot) { + int l = snapshot.data; + IconData icon = getBatteryLevelIcon(level: l); + + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icon, color: Colors.white, - size: 30, ), - ), + Text((l >= 0) ? '$l%' : 'Unknown', style: TextStyle(color: Colors.white)) + ] + ); + } + ); - Container( - decoration: standardDecoration, - padding: EdgeInsets.symmetric(vertical: 3, horizontal: 10), - margin: EdgeInsets.symmetric(horizontal: 5), - alignment: Alignment.center, - child: Column( - children: [ - Text( - 'Connected to', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - fontSize: 16 - ) - ), - SizedBox(height: 5), - StreamBuilder( - stream: DataHeader().deviceIdStream(), - initialData: '', - builder: (context, snapshot) => Text( - snapshot.data != '' ? snapshot.data : 'Unknown', - style: TextStyle( - color: Colors.white - ) - ) - ) - ], + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + children: intersperse(SizedBox(height: 10), [ + Container( + width: 50, + height: 50, + decoration: iconContainerNeumorphicDecoration, + child: Icon( + Icons.bluetooth, + color: Colors.white, + size: 30, + ), ), - ), - - Container( - decoration: standardDecoration, - padding: EdgeInsets.symmetric(vertical: 3, horizontal: 10), - margin: EdgeInsets.symmetric(horizontal: 5), - alignment: Alignment.center, - child: Column( - children: [ - Text( - 'Device battery', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - fontSize: 16 - ) - ), - SizedBox(height: 5), - StreamBuilder( - stream: DataHeader().deviceBatteryLevel(), - initialData: -1, - builder: (context, snapshot) { - int l = snapshot.data; - IconData icon; - if (l < 0) icon = MdiIcons.batteryUnknown; - else if (0 <= l && l <= 5) icon = MdiIcons.batteryAlertVariant; - else if (5 < l && l < 20) icon = MdiIcons.battery10; - else if (20 <= l && l < 30) icon = MdiIcons.battery20; - else if (30 <= l && l < 40) icon = MdiIcons.battery30; - else if (40 <= l && l < 50) icon = MdiIcons.battery40; - else if (50 <= l && l < 60) icon = MdiIcons.battery50; - else if (60 <= l && l < 70) icon = MdiIcons.battery60; - else if (70 <= l && l < 80) icon = MdiIcons.battery70; - else if (80 <= l && l < 90) icon = MdiIcons.battery80; - else if (90 <= l) icon = MdiIcons.battery90; - else icon = MdiIcons.battery; - - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - icon, - color: Colors.white, - ), - Text((l >= 0) ? '$l%' : 'Unknown', style: TextStyle(color: Colors.white)) - ] - ); - } - ), - ] + OverviewDataCell( + label: 'Connected to', + builder: _buildDeviceIdWidget(), + ), + OverviewDataCell( + label: 'Device battery', + builder: _buildDeviceBatteryLevelWidget(), ), - ), - - ] + ]).toList() + ) ); } } \ No newline at end of file diff --git a/lib/ui/components/body/overview/overview_widgets/common/overview_data_cell.dart b/lib/ui/components/body/overview/overview_widgets/common/overview_data_cell.dart new file mode 100644 index 0000000000000000000000000000000000000000..51ce3752c6a46b5740f41ac91fdca2de8c446639 --- /dev/null +++ b/lib/ui/components/body/overview/overview_widgets/common/overview_data_cell.dart @@ -0,0 +1,36 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:logair_application/utils/decorations.dart'; + +/// Neumorphic cell with a text label preceding a [StreamBuilder] constructed [Widget] +class OverviewDataCell extends StatelessWidget { + /// The label of the cell. + final String label; + + /// A [StreamBuilder] constructed [Widget], defined higher in the tree. + final StreamBuilder builder; + + OverviewDataCell({@required this.label, @required this.builder, Key key}) : super(key: key); + + @override + Container build(BuildContext context) => Container( + decoration: containerNeumorphicDecoration, + padding: EdgeInsets.symmetric(vertical: 3, horizontal: 10), + margin: EdgeInsets.symmetric(horizontal: 5), + alignment: Alignment.center, + child: Column( + children: [ + Text( + this.label, + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 16 + ) + ), + SizedBox(height: 5), + this.builder + ], + ), + ); +} \ No newline at end of file diff --git a/lib/ui/components/body/overview/overview_widgets/db_overview_widget.dart b/lib/ui/components/body/overview/overview_widgets/db_overview_widget.dart index 64da7ccf7479422776e6f2f8eef75daa3e4c79cd..688520dd4c1f301d0d2db1b10ce676ea5dad6b4f 100644 --- a/lib/ui/components/body/overview/overview_widgets/db_overview_widget.dart +++ b/lib/ui/components/body/overview/overview_widgets/db_overview_widget.dart @@ -3,102 +3,66 @@ import 'dart:ui'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:fluttericon/linecons_icons.dart'; +import 'package:intersperse/intersperse.dart'; import 'package:intl/intl.dart'; -import 'package:logair_application/logic/handlers/database_handler.dart'; -import 'package:logair_application/ui/components/body/overview/overview_widgets/decorations.dart'; +import 'package:logair_application/logic/handlers/main_database_handler.dart'; +import 'package:logair_application/utils/decorations.dart'; +import 'package:logair_application/ui/components/body/overview/overview_widgets/common/overview_data_cell.dart'; class DBOverviewWidget extends StatelessWidget { DBOverviewWidget({Key key}) : super(key: key); - Stream mockStream() async* { - while(true) { - yield 1000000000000000; - await Future.delayed(Duration(seconds: 1)); - } - } + /// Builds a [Text] containing the number of elements stored in the internal database. + StreamBuilder _buildDBSizeWidget() => StreamBuilder( + stream: MainDatabaseHandler().getDBSize(), + initialData: 0, + builder: (BuildContext context, AsyncSnapshot snapshot) => Text( + '${NumberFormat.compact().format(snapshot.data)}', + style: TextStyle( + color: Colors.white, + fontSize: 20 + ) + ) + ); + + /// Builds a [Text] containing the number of unsent elements stored in the internal database. + StreamBuilder _buildDBUnsentQuantityWidget() => StreamBuilder( + stream: MainDatabaseHandler().getUnsentPacketsLength(), + initialData: 0, + builder: (BuildContext context, AsyncSnapshot snapshot) => Text( + '${NumberFormat.compact().format(snapshot.data)}', + style: TextStyle( + color: Colors.white, + fontSize: 20 + ) + ) + ); @override Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Container( - width: 50, - height: 50, - decoration: standardIconDecoration, - child: Icon( - Linecons.database, - color: Colors.white, - size: 30, + return SingleChildScrollView( + child: Column( + children: intersperse(SizedBox(height: 10), [ + Container( + width: 50, + height: 50, + decoration: iconContainerNeumorphicDecoration, + child: Icon( + Linecons.database, + color: Colors.white, + size: 30, + ), + ), + OverviewDataCell( + label: 'Packets stored', + builder: _buildDBSizeWidget(), + ), + OverviewDataCell( + label: 'Unsent packets', + builder: _buildDBUnsentQuantityWidget(), ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - decoration: standardDecoration, - padding: EdgeInsets.symmetric(vertical: 3, horizontal: 10), - alignment: Alignment.center, - child: Column( - children: [ - Text( - 'Packets stored', - style: TextStyle( - color: Colors.white, - fontSize: 16 - ) - ), - SizedBox(height: 5), - StreamBuilder( - stream: DatabaseHandler().getDBSize(), - initialData: 0, - builder: (BuildContext context, AsyncSnapshot snapshot) => Text( - '${NumberFormat.compact().format(snapshot.data)}', - style: TextStyle( - color: Colors.white, - fontSize: 20 - ) - ) - ) - ], - ), - ) - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - decoration: standardDecoration, - padding: EdgeInsets.symmetric(vertical: 3, horizontal: 10), - alignment: Alignment.center, - child: Column( - children: [ - Text( - 'Unsent packets', - style: TextStyle( - color: Colors.white, - fontSize: 16 - ) - ), - SizedBox(height: 5), - StreamBuilder( - stream: DatabaseHandler().getUnsentPacketsLength(), - initialData: 0, - builder: (BuildContext context, AsyncSnapshot snapshot) => Text( - '${NumberFormat.compact().format(snapshot.data)}', - style: TextStyle( - color: Colors.white, - fontSize: 20 - ) - ) - ) - ], - ), - ) - ], - ) - ], + ]).toList(), + ) ); } } \ No newline at end of file diff --git a/lib/ui/components/body/overview/overview_widgets/decorations.dart b/lib/ui/components/body/overview/overview_widgets/decorations.dart deleted file mode 100644 index 992fcc46738d1b7cbd9478aedbfe9e9aeef20763..0000000000000000000000000000000000000000 --- a/lib/ui/components/body/overview/overview_widgets/decorations.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; - -final BoxDecoration standardIconDecoration = BoxDecoration( - boxShadow: [ - BoxShadow( - offset: Offset(2, -2), - color: Colors.blue[200] - ) - ], - borderRadius: BorderRadius.circular(90), - border: Border.all( - color: Colors.blue[500] - ), - color: Colors.blue[600], -); - -final BoxDecoration standardDecoration = BoxDecoration( - boxShadow: [ - BoxShadow( - offset: Offset(2, -2), - color: Colors.blue[200] - ) - ], - borderRadius: BorderRadius.circular(10), - border: Border.all( - color: Colors.blue[500] - ), - color: Colors.blue[500], -); \ No newline at end of file diff --git a/lib/ui/components/body/overview/overview_widgets/map_position_overview_widget.dart b/lib/ui/components/body/overview/overview_widgets/map_position_overview_widget.dart index e2959d41f4f08b8e51948e85de53ff1e67300c6b..b37b4e64d0f41cbb971de8da297699c199026dc5 100644 --- a/lib/ui/components/body/overview/overview_widgets/map_position_overview_widget.dart +++ b/lib/ui/components/body/overview/overview_widgets/map_position_overview_widget.dart @@ -1,10 +1,77 @@ import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:intersperse/intersperse.dart'; +import 'package:intl/intl.dart'; +import 'package:logair_application/logic/handlers/position_handler.dart'; +import 'package:logair_application/logic/handlers/preference_handler.dart'; +import 'package:logair_application/ui/components/body/overview/overview_widgets/common/overview_data_cell.dart'; +import 'package:logair_application/utils/decorations.dart'; +/// [Widget] recapping the various GPS related statistics for quick lookup. class MapPositionOverviewWidget extends StatelessWidget { MapPositionOverviewWidget({Key key}) : super(key: key); + /// Builds a [Text] containing the time at which the position was last acquired. + StreamBuilder _buildPositionLastAcquiredWidget() => StreamBuilder( + stream: PositionHandler().timeSinceLastPositionAcquired, + initialData: 0, + builder: (BuildContext context, AsyncSnapshot snapshot) => Text( + '${NumberFormat.compact().format(snapshot.data)}s ago', + style: TextStyle( + color: Colors.white, + fontSize: 20 + ) + ) + ); + + /// Builds a [Text] containing the desired location accuracy. + StreamBuilder _buildLocationAccuracyWidget() => StreamBuilder( + stream: PreferencesHandler().currentDesiredGPSAccuracy, + initialData: -1, + builder: (BuildContext context, AsyncSnapshot snapshot) { + String text; + switch(snapshot.data) { + case 0: text = 'Low'; break; + case 1: text = 'Medium'; break; + case 2: text = 'High'; break; + case 3: text = 'Best'; break; + default: text = 'N/A'; break; + } + return Text( + text, + style: TextStyle( + color: Colors.white, + fontSize: 20 + ) + ); + } + ); + @override Widget build(BuildContext context) { - return Text('Map x GPS oversight'); + return SingleChildScrollView( + child: Column( + children: intersperse(SizedBox(height: 10), [ + Container( + width: 50, + height: 50, + decoration: iconContainerNeumorphicDecoration, + child: Icon( + Icons.location_on, + color: Colors.white, + size: 30, + ), + ), + OverviewDataCell( + label: 'Position last acquired', + builder: _buildPositionLastAcquiredWidget(), + ), + OverviewDataCell( + label: 'Location Accuracy', + builder: _buildLocationAccuracyWidget(), + ), + ]).toList() + ) + ); } } \ No newline at end of file diff --git a/lib/ui/components/body/overview/overview_widgets/network_overview_widget.dart b/lib/ui/components/body/overview/overview_widgets/network_overview_widget.dart index cf02596e958d6fe20d8b9d432046ee50e6772a36..739eb823d0c563cd4341ea38fcb2a615931acbb9 100644 --- a/lib/ui/components/body/overview/overview_widgets/network_overview_widget.dart +++ b/lib/ui/components/body/overview/overview_widgets/network_overview_widget.dart @@ -1,58 +1,131 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:intersperse/intersperse.dart'; import 'package:intl/intl.dart'; import 'package:logair_application/logic/handlers/network_handler.dart'; -import 'package:logair_application/ui/components/body/overview/overview_widgets/decorations.dart'; +import 'package:logair_application/ui/components/body/overview/overview_widgets/common/overview_data_cell.dart'; +import 'package:logair_application/utils/decorations.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:tuple/tuple.dart'; class NetworkOverviewWidget extends StatelessWidget { NetworkOverviewWidget({Key key}) : super(key: key); - @override - Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Container( - width: 50, - height: 50, - decoration: standardIconDecoration, - child: Icon( - Icons.network_check, - color: Colors.white, - size: 30, - ), - ), - Container( - decoration: standardDecoration, - padding: EdgeInsets.symmetric(vertical: 3, horizontal: 10), - margin: EdgeInsets.symmetric(horizontal: 5), - alignment: Alignment.center, - child: Column( - children: [ - Text( - 'Last connected to\nserver at:', + // Upload counter // Upload Rate (kb/s) // Download rate (kb/s) // Download counter + Stream> mockStream() async* { + while (true) { + yield Tuple4(1, 340, 130, 0); + await Future.delayed(Duration(milliseconds: 500)); + } + } + + /// Builds a [Text] containing the time at which the device last pushed data to the server. + StreamBuilder _buildLastConnectionWidget() { + return StreamBuilder( + stream: NetworkHandler().getLastServerConnection(), + initialData: null, + builder: (BuildContext context, AsyncSnapshot snapshot) => Text( + '${(snapshot.data != null) ? DateFormat('kk:mm:ss').format(snapshot.data) : 'Unknown'}', + style: TextStyle( + color: Colors.white, + fontSize: 20 + ) + ) + ); + } + + /// Builds a [Row] containing the upload and download speeds. + StreamBuilder> _buildConnectionUsageWidget() { + return StreamBuilder>( + stream: mockStream(), + initialData: Tuple4(0, 0, 0, 0), + builder: (BuildContext context, AsyncSnapshot snapshot) { + Tuple4 data = snapshot.data; + int uploadCounter = data.item1; + int uploadRate = data.item2; + int downloadRate = data.item3; + int downloadCounter = data.item4; + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Text( + '${NumberFormat.compact().format(uploadRate)}\nkb/s', textAlign: TextAlign.center, style: TextStyle( color: Colors.white, fontSize: 16 ) ), - SizedBox(height: 5), - StreamBuilder( - stream: NetworkHandler().getLastServerConnection(), - initialData: null, - builder: (BuildContext context, AsyncSnapshot snapshot) => Text( - '${(snapshot.data != null) ? DateFormat('kk:mm:ss').format(snapshot.data) : 'Unknown'}', - style: TextStyle( - color: Colors.white, - fontSize: 20 + ), + + Container( + width: 30, + height: 35, + child: Stack( + children: [ + Positioned( + top: 0, + child: Icon( + MdiIcons.arrowUpThick, + color: uploadCounter == 0 ? Colors.grey[100] : Colors.orange, + ), + ), + Positioned( + left: 4, + top: 12, + child: Icon( + MdiIcons.arrowDownThick, + color: downloadCounter == 0 ? Colors.grey[100] : Colors.green, + ), ) + ], + ), + ), + Expanded( + child: Text( + '${NumberFormat.compact().format(downloadRate)}\nkb/s', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 16 ) - ) - ], + ), + ), + ], + ); + } + ); + } + + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + children: intersperse(SizedBox(height: 10), [ + Container( + width: 50, + height: 50, + decoration: iconContainerNeumorphicDecoration, + child: Icon( + Icons.network_check, + color: Colors.white, + size: 30, + ), ), - ) - ] + OverviewDataCell( + label: 'Last connected to\nserver at:', + builder: _buildLastConnectionWidget() + ), + OverviewDataCell( + label: 'Network usage:', + builder: _buildConnectionUsageWidget() + ), + ]).toList() + ) ); } } \ No newline at end of file diff --git a/lib/ui/components/body/preferences/preferences_widget.dart b/lib/ui/components/body/preferences/preferences_widget.dart index bc765239db04ffc3e2527b5ba92a6b1834701838..a72242eddd9234a772ed06134d1051132eb7b891 100644 --- a/lib/ui/components/body/preferences/preferences_widget.dart +++ b/lib/ui/components/body/preferences/preferences_widget.dart @@ -6,7 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:fluttericon/linecons_icons.dart'; import 'package:intersperse/intersperse.dart'; -import 'package:logair_application/logic/handlers/database_handler.dart'; +import 'package:logair_application/logic/handlers/main_database_handler.dart'; import 'package:logair_application/logic/handlers/preference_handler.dart'; import 'package:logair_application/ui/components/body/preferences/tiles/action_tile.dart'; import 'package:logair_application/ui/components/body/preferences/tiles/location_selector.dart'; @@ -166,7 +166,7 @@ class _PreferencesWidgetState extends State { labelText: 'Delete local data', buttonText: 'Delete', warningText: 'Operation is final', - action: () => DatabaseHandler().recreateDB() + action: () => MainDatabaseHandler().recreateDB() ), TextInputTile( controller: this._dbMaxAgeController, @@ -336,7 +336,7 @@ class _PreferencesWidgetState extends State { ), padding: EdgeInsets.symmetric(vertical: 12), onPressed: () async { - print('FormState validate : ${_formKey.currentState.validate()}'); + //print('FormState validate : ${_formKey.currentState.validate()}'); if (_formKey.currentState.validate()) { await PreferencesHandler().setPreferencesBool(PreferenceKeys.GEN__SHARE.key, _generalShareController.value); diff --git a/lib/ui/components/common/dash_separator.dart b/lib/ui/components/common/dash_separator.dart deleted file mode 100644 index 2df91715ced2162b6de183ea70259e707c4f6bc5..0000000000000000000000000000000000000000 --- a/lib/ui/components/common/dash_separator.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; - -class DashSeparator extends StatelessWidget { - final double height; - final Color color; - - const DashSeparator({this.height = 1, this.color = Colors.black}); - - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (BuildContext ctx, BoxConstraints constraints) { - final double width = constraints.constrainWidth(); - final double dashWidth = 10; - final double dashHeight = this.height; - final int dashCount = (width / (2*dashWidth)).floor(); - - return Flex( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - direction: Axis.horizontal, - children: List.generate( - dashCount, - (_) => SizedBox( - width: dashWidth, - height: dashHeight, - child: DecoratedBox( - decoration: BoxDecoration( - color: color - ), - ), - ) - ), - ); - }, - ); - } -} \ No newline at end of file diff --git a/lib/ui/components/common/growing_text.dart b/lib/ui/components/common/growing_text.dart deleted file mode 100644 index aca6773dab67660f778fb94aa18d755ca656c0f6..0000000000000000000000000000000000000000 --- a/lib/ui/components/common/growing_text.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:flutter/widgets.dart'; - -class TextArea extends StatefulWidget { - TextArea({Key key}) : super(key: key); - - @override - _TextAreaState createState() => _TextAreaState(); -} - -class _TextAreaState extends State