Commit fca30031 authored by Nicolas Richard Walter Boeckh's avatar Nicolas Richard Walter Boeckh 💬

Merge branch 'ux-context-aware-and-last-prefs' into 'main'

Ux context aware and last prefs

See merge request !9
parents cf419147 84356333
This diff is collapsed.
include: package:pedantic/analysis_options.yaml
analyzer:
exclude:
- lib\localization\*
linter:
rules:
unnecessary_this: false
omit_local_variable_types: false
\ No newline at end of file
......@@ -38,7 +38,6 @@ android {
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.logair.logair_application"
minSdkVersion 19
targetSdkVersion 28
......
......@@ -9,6 +9,7 @@
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<application
android:name="io.flutter.app.FlutterApplication"
android:label="logair.io"
......
......@@ -4,4 +4,8 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
</manifest>
......@@ -23,7 +23,7 @@ class HomeController {
this._triggerOriginNavbar = true;
}
static final HomeController _singleton = new HomeController._internal();
static final HomeController _singleton = HomeController._internal();
/// The controller for the [CarouselSlider].
PageController _pageController;
......@@ -42,12 +42,14 @@ class HomeController {
/// Forces the [CircularBottomNavigation] to the correct position.
set navbar(int page) {
this._currentPage = page;
// Set to false to mitigate livelock.
this._triggerOriginNavbar = false;
// Guard
if (this._navigationController != null)
if (this._navigationController != null) {
this._navigationController.value = (this._currentPage + 1) % 3;
}
this._previousPage = this._currentPage;
// Set to true once risk of livelock passed.
......@@ -62,11 +64,12 @@ class HomeController {
this._currentPage = (page - 1) % 3;
// Make the carousel controller move forward (jump to +1 would limit UI because of lackluster cyclical handling).
if ((this._currentPage == 0 && this._previousPage == 2) || (this._currentPage == 1 && this._previousPage == 0) || (this._currentPage == 2 && this._previousPage == 1))
if ((this._currentPage == 0 && this._previousPage == 2) || (this._currentPage == 1 && this._previousPage == 0) || (this._currentPage == 2 && this._previousPage == 1)) {
this._pageController.nextPage(duration: Duration(milliseconds: 200), curve: Curves.linear);
// Make the carousel controller move backward (jump to +1 would limit UI because of lackluster cyclical handling).
else if ((this._currentPage == 2 && this._previousPage == 0) || (this._currentPage == 0 && this._previousPage == 1) || (this._currentPage == 1 && this._previousPage == 2))
this._pageController.previousPage(duration: Duration(milliseconds: 200), curve: Curves.linear);
// Make the carousel controller move backward (jump to +1 would limit UI because of lackluster cyclical handling).
} else if ((this._currentPage == 2 && this._previousPage == 0) || (this._currentPage == 0 && this._previousPage == 1) || (this._currentPage == 1 && this._previousPage == 2)) {
this._pageController.previousPage(duration: Duration(milliseconds: 200), curve: Curves.linear);
}
this._previousPage = this._currentPage;
}
......
......@@ -2,14 +2,15 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_map/plugin_api.dart';
import 'package:geolocator/geolocator.dart';
import 'package:latlong/latlong.dart';
import 'package:logair_application/logic/handlers/data_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';
import 'package:rxdart/rxdart.dart';
/// The [MapDisplayController] is a singleton that handles the mapping of information.
class MapDisplayController {
......@@ -23,7 +24,10 @@ class MapDisplayController {
LatLng _lastPos;
/// The [FlutterMap]'s zoom level, used to keep centering stable.
double _zoom;
double _zoom = 13;
/// Retrieves [FlutterMap]'s current zoom level.
double get zoom => _zoom;
/// The [FlutterMap]s [MapController], kept unique in order to avoid incessant reloading of resources.
MapController _controller;
......@@ -34,6 +38,10 @@ class MapDisplayController {
/// [Marker] representing the user's current [Position].
Marker _marker;
/// Null [BehaviorSubject] tracking when the [FlutterMap] should be redrawn.
BehaviorSubject shouldRedraw = BehaviorSubject();
Future<void> _rebuildMarkers() async {
List<DataPacket> packets = await MainDatabaseHandler().getLastPackets(200);
packets.forEach((DataPacket packet) {
......@@ -41,7 +49,7 @@ class MapDisplayController {
case PMSymbol.PM1:
this.addToList(this._currentPos, packet.pm1, 200);
break;
case PMSymbol.PM1:
case PMSymbol.PM2_5:
this.addToList(this._currentPos, packet.pm2_5, 200);
break;
case PMSymbol.PM4:
......@@ -58,17 +66,26 @@ class MapDisplayController {
List<CircleMarker> _pmMarkers = <CircleMarker>[];
/// [Singleton]() instantiation factory.
/// Singleton instantiation factory.
factory MapDisplayController() => _singleton;
final BehaviorSubject<Position> _positionObserver = PositionHandler().devicePosition;
/// @Getter for [_polylines].
List<Polyline> get polylines => this._polylines;
/// @Getter for [_marker].
Marker get marker => this._marker != null ? this._marker : _buildLocationMarker(LatLng(0, 0));
Marker get marker {
if (this._marker == null) {
LatLng position = this._positionObserver.value == null ? LatLng(0, 0) : LatLng(this._positionObserver.value.latitude, this._positionObserver.value.longitude);
this._marker = _buildLocationMarker(position);
}
return this._marker;
}
/// Object used to calculate the real-life distance of two [Position]s
final Distance _distance = new Distance();
final Distance _distance = Distance();
List<CircleMarker> get pmMarkers => this._pmMarkers;
......@@ -82,31 +99,35 @@ class MapDisplayController {
}
void addToList(LatLng position, double value, int maxPoints) {
if (this._pmMarkers.length > maxPoints)
if (this._pmMarkers.length > maxPoints) {
this._pmMarkers.removeRange(0, this._pmMarkers.length - 200);
}
if (value != null)
if (value != null) {
this._pmMarkers.add(_buildPMMarker(position, value));
}
}
/// This function is executed on first initialization of the [MapDisplayController]
// 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.
this._controller = new MapController();
this._controller = MapController();
/// This function is hooked to the [PositionHandler]s progress, ie. when a new [Position] is acquired (once per second), it is broadcasted across the app and can be listened to.
// TODO Fix this to allow external data (bounded map data)
PositionHandler().getCurrentOrLastPosition().listen((posChanged) {
if (posChanged == null)
if (posChanged == null) {
return;
}
/// If a [BluetoothDevice] is connected, then the [DataHandler] will have a [DataPacket] available.
DataPacket latest = null;
DataPacket latest;
/// 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.
this._lastPos = this._currentPos;
......@@ -115,41 +136,49 @@ class MapDisplayController {
this._currentPos = LatLng(posChanged.latitude, posChanged.longitude);
/// If the user has not acquired manual control, then center the map onto the newer position.
if (!this._manualMotion && this._controller != null && this._controller.ready)
if (!this._manualMotion && this._controller != null && this._controller.ready) {
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._currentPos != this._lastPos) {
this.shouldRedraw.value = null;
}
if ((this._lastPos != null && this._currentPos != null)) {// && this._distance.as(LengthUnit.Meter, this._lastPos, this._currentPos) >= 2)) {
if (this._polylines.length == 0) {
if ((this._lastPos != null && this._currentPos != null) && this._distance.as(LengthUnit.Meter, this._lastPos, this._currentPos) >= .5) {
if (this._polylines.isEmpty) {
this._polylines.add(
Polyline(
points: [this._lastPos, this._currentPos],
color: Colors.blue
)
);
} else
} else {
this._polylines.last.points.add(this._currentPos);
}
if (latest != null) {
switch (_pmSymbol) {
case PMSymbol.PM1:
if (latest.pm1 != null)
if (latest.pm1 != null) {
this.addToList(this._currentPos, latest.pm1, 200);
}
break;
case PMSymbol.PM2_5:
if (latest.pm2_5 != null)
if (latest.pm2_5 != null) {
this.addToList(this._currentPos, latest.pm2_5, 200);
}
break;
case PMSymbol.PM4:
if (latest.pm4 != null)
if (latest.pm4 != null) {
this.addToList(this._currentPos, latest.pm4, 200);
}
break;
case PMSymbol.PM10:
if (latest.pm10 != null)
if (latest.pm10 != null) {
this.addToList(this._currentPos, latest.pm10, 200);
}
break;
default:
break;
......@@ -158,19 +187,24 @@ class MapDisplayController {
}
});
new Timer.periodic(Duration(minutes: 1), (Timer t) {
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);
}
});
}
@override
MapDisplayController.dispose() {
shouldRedraw.close();
_positionObserver.close();
}
/// Internal static [Singleton]() reference.
static final MapDisplayController _singleton = new MapDisplayController._internal();
static final MapDisplayController _singleton = MapDisplayController._internal();
/// @Getter for [_currentPos]
LatLng currentPosition() => _currentPos != null ? _currentPos : LatLng(0, 0);
LatLng currentPosition() => _currentPos ?? LatLng(0, 0);
/// @Getter for [_controller]
MapController controller() => _controller;
......@@ -178,8 +212,9 @@ class MapDisplayController {
/// Function called whenever the user interacts with the [FlutterMap], setting the [FlutterMap]'s viewport under their control.
void setControllerChange(LatLng position, double zoom) {
/// Avoid triggering full user control when only the zoom value changes.
if (this._currentPos != null && this._distance.as(LengthUnit.Meter, position, this._currentPos) >= 10)
if (this._currentPos != null && this._distance.as(LengthUnit.Meter, position, this._currentPos) >= 10) {
this._manualMotion = true;
}
this._zoom = zoom;
}
......@@ -189,10 +224,11 @@ class MapDisplayController {
/// Enables the user to have a controlled zoom in, zoom out experience.
void setControllerZoom(bool zoomIn) {
/// Bound the zoomability.
if (zoomIn && this._zoom <= 19)
if (zoomIn && this._zoom <= 19) {
this._zoom += 0.5;
else if (!zoomIn && this._zoom >= 1)
} else if (!zoomIn && this._zoom >= 1) {
this._zoom -= 0.5;
}
this._controller.move(this.controller().center, this._zoom);
}
......
......@@ -12,7 +12,7 @@ class DataHeader {
_headerSet = false;
}
static final DataHeader _singleton = new DataHeader._internal();
static final DataHeader _singleton = DataHeader._internal();
/// Data contained within the Header packet
List<String> _headerData;
......@@ -64,7 +64,7 @@ class DataHeader {
Stream<String> deviceIdStream() async* {
while(true) {
yield this._deviceId;
yield this._deviceId ?? '';
await Future.delayed(Duration(seconds: 1));
}
}
......
......@@ -9,6 +9,7 @@ import 'package:logair_application/logic/handlers/preference_handler.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';
import 'package:logair_application/utils/utils.dart';
// TODO Attempt reacquiring device after 5 seconds inactivity.
......@@ -24,7 +25,7 @@ class BTLEHandler {
}
/// Internal static [Singleton]() reference.
static final BTLEHandler _singleton = new BTLEHandler._internal();
static final BTLEHandler _singleton = BTLEHandler._internal();
/// The referenced [BluetoothCharacteristic].
/// Should be singular, unless we plan to broadcast over channels.
......@@ -68,15 +69,17 @@ class BTLEHandler {
/// Makes the phone connect to the [BluetoothDevice] and initiate [BluetoothService] discovery.
Future<void> connect() async {
/// If the [BluetoothDevice] is connected to this handler.
if (this._device != null)
if (this._device != null) {
await this._asyncConnectionProcess();
}
}
/// Makes the smartphone disconnect from the [BluetoothDevice].
Future<dynamic> disconnect() => this._device?.let((it) async {
/// Remove notifications and references to the [BluetoothDevice] or [BluetoothCharacteristic]s.
if (_characteristic != null)
if (_characteristic != null) {
await _characteristic.setNotifyValue(false);
}
_characteristic = null;
_device = null;
_bluetoothConnectionStatus = BluetoothConnectionStatus.BTSTATUS_NOT_STREAMING;
......@@ -97,23 +100,22 @@ class BTLEHandler {
final String bluetoothLeCc254xServiceUUID = await PreferencesHandler().getPreferencesString(PreferenceKeys.BT__SERVICE_UUID.key);
final String bluetoothLeCc254xReadUUID = await PreferencesHandler().getPreferencesString(PreferenceKeys.BT__CHARACTERISTIC_UUID.key);
print('$bluetoothLeCc254xServiceUUID\n$bluetoothLeCc254xReadUUID');
/// Get the first round of [BluetoothService]s and apply filter to desired Service UUID.
services = await this._device.services.first;
services = services.where((s) => s.uuid.toString() == bluetoothLeCc254xServiceUUID).toList();
/// If the [BluetoothService] exists.
if (services.length > 0) {
if (services.isNotEmpty) {
/// Get the associated [BluetoothCharacteristic]s and apply filter to desired Characteristic UUID.
characteristics = services[0].characteristics;
characteristics = characteristics.where((c) => c.uuid.toString() == bluetoothLeCc254xReadUUID).toList();
/// If the [BluetoothCharacteristic] exists.
if (characteristics.length > 0) {
if (characteristics.isNotEmpty) {
/// Set the [BluetoothCharacteristic] to notify new data.
if (!characteristics[0].isNotifying)
if (!characteristics[0].isNotifying) {
await characteristics[0].setNotifyValue(true);
}
_characteristic = characteristics[0];
success = true;
}
......@@ -133,14 +135,14 @@ class BTLEHandler {
/// Apply a pipe transform to the values that are notified by the [BluetoothCharacteristic].
_characteristic.value.transform(
new StreamTransformer.fromHandlers(
StreamTransformer.fromHandlers(
handleData: (List<int> list, EventSink<List<int>> sink) =>
sink.add((list.isNotEmpty) ? list : []))
).listen((data) =>
/// Handoff the data to the [DataHandler].
DataHandler().addData(data)
);
print("RETURNS");
printDebug('BTHandler: Returned');
success = true;
} else {
_bluetoothConnectionStatus = BluetoothConnectionStatus.BTSTATUS_NOT_STREAMING;
......
......@@ -12,10 +12,11 @@ class BTWakeHandler {
static bool _wakeServiceActive = false;
bool switchState({bool start}) {
if (_wakeServiceActive || !start)
if (_wakeServiceActive || !start) {
_end();
else if (!_wakeServiceActive || start)
} else if (!_wakeServiceActive || start) {
_start();
}
return _wakeServiceActive;
}
......
......@@ -3,6 +3,7 @@ import 'package:logair_application/logic/data_header.dart';
import 'package:logair_application/logic/data_packet.dart';
import 'package:logair_application/logic/handlers/main_database_handler.dart';
import 'package:logair_application/logic/handlers/network_handler.dart';
import 'package:logair_application/utils/utils.dart';
class DataHandler {
factory DataHandler() => _singleton;
......@@ -13,13 +14,13 @@ class DataHandler {
static const HEADER_SIZE = 11;
static final DataHandler _singleton = new DataHandler._internal();
static final DataHandler _singleton = DataHandler._internal();
/// [List] containing all of the data received by this.
List<int> _data = [];
final List<int> _data = [];
/// [List] containing separate packet instances received by this.
List<DataPacket> _sortedData = [];
final List<DataPacket> _sortedData = [];
/// Allows the [BTLEHandler] to add acquired information to this, and effects
/// a primary triage.
......@@ -28,16 +29,18 @@ class DataHandler {
/// Get a start index for the data, either '{' (DataHeader) or '[' (DataPacket)
int index = _data.indexWhere((e) => e == 91 || e == 123);
if (index == -1)
if (index == -1) {
return;
}
/// Remove noise if exists
_data.removeRange(0, index);
/// Get the termination index '$'
int termIndex = _data.indexWhere((e) => e == 36);
if (termIndex == -1)
if (termIndex == -1) {
return;
}
/// If both exist, then extract the packet.
List<int> packet = _data.sublist(index, termIndex + 1);
......@@ -46,14 +49,13 @@ class DataHandler {
/// If a packet is ingrained within this packet (possibly because of BLE transmission issues),
/// this will attempt to fix it.
if (packet.sublist(1).contains((e) => e == 91 || e == 123)) {
print('True ${packet.sublist(1).indexWhere((e) => e == 91 || e == 123)}');
int startIndex = packet.sublist(1).indexWhere((e) => e == 91 || e == 123);
packet.removeRange(0, startIndex);
}
/// If it's a [DataHeader] instance.
if (packet[0] == 123) {
print('HEADER ${String.fromCharCodes(packet)}');
printDebug('DataHandler: HEADER ${String.fromCharCodes(packet)}');
bool wasSet = DataHeader().headerSet;
DataHeader().setHeader(packet);
/// Retroactively add all previous packets
......@@ -67,14 +69,15 @@ class DataHandler {
/// Or if it is a [DataPacket] instance.
} else if (packet[0] == 91) {
print('PACKET ${String.fromCharCodes(packet)}');
printDebug('DataHandler: PACKET ${String.fromCharCodes(packet)}');
DataPacket dataPacket = DataPacket(packet);
if (DataHeader().headerSet) {
dataPacket.deviceId = DataHeader().deviceId;
dataPacket.url = DataHeader().url;
MainDatabaseHandler().insertData(dataPacket);
} else
} else {
_sortedData.add(dataPacket);
}
}
}
......@@ -91,19 +94,20 @@ class DataHandler {
/// Remove the first element of the [DataPacket]s. Used for surgical removal.
@deprecated
void pop(int timestamp) {
if (_sortedData.isNotEmpty)
if (_sortedData.isNotEmpty) {
_sortedData.removeWhere((DataPacket d) => d.timestamp == timestamp);
}
}
/// Get the latest [DataPacket]
@deprecated
DataPacket getLatestData() => (_sortedData.length > 0) ? _sortedData.last : null;
DataPacket getLatestData() => (_sortedData.isNotEmpty) ? _sortedData.last : null;
/// Stream the latest confirmed [DataPacket] received by the device.
@deprecated
Stream<DataPacket> getDataStream() async* {
while(true) {
yield (_sortedData.length > 0) ? _sortedData.last : null;
yield (_sortedData.isNotEmpty) ? _sortedData.last : null;
await Future.delayed(Duration(seconds: 1));
}
}
......
......@@ -14,7 +14,7 @@ class LocationsOfInterestDatabaseHandler {
LocationsOfInterestDatabaseHandler._internal();
static final LocationsOfInterestDatabaseHandler _singleton = new LocationsOfInterestDatabaseHandler._internal();
static final LocationsOfInterestDatabaseHandler _singleton = LocationsOfInterestDatabaseHandler._internal();
/// Reference to the internalized [Database] within this instance.
Future<Database> _loidb;
......@@ -27,12 +27,13 @@ class LocationsOfInterestDatabaseHandler {
/// Retrieves the internal [Database] instance, and if not instantiated, instantiates it.
Future<Database> get _loiDatabase async {
if (_loidb == null)
if (_loidb == null) {
this._loidb = openDatabase(
join((await getDatabasesPath()), 'logair_loi.db'),
version: 0,
onCreate: (db, version) => db.execute(this._createLOIDBString),
);
}
return this._loidb;
}
......
import 'package:jiffy/jiffy.dart';
import 'package:logair_application/logic/data_packet.dart';
import 'package:logair_application/logic/handlers/preference_handler.dart';
import 'package:logair_application/utils/enums/preference_keys.dart';
import 'package:logair_application/utils/utils.dart';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
import 'package:tuple/tuple.dart';
......@@ -10,7 +14,7 @@ class MainDatabaseHandler {
MainDatabaseHandler._internal();
static final MainDatabaseHandler _singleton = new MainDatabaseHandler._internal();
static final MainDatabaseHandler _singleton = MainDatabaseHandler._internal();
/// Reference to the internalized [Database] within this instance.
Future<Database> _maindb;
......@@ -30,12 +34,23 @@ class MainDatabaseHandler {
/// Retrieves the internal [Database] instance, and if not instantiated, instantiates it.
Future<Database> get _mainDatabase async {
if (_maindb == null)
if (_maindb == null) {
this._maindb = openDatabase(
join((await getDatabasesPath()), 'logair_data.db'),
version: 3,
onCreate: (db, version) => db.execute(this._createMainDBString)
);
// Delete data older than 15 days at boot.
int maxDays = await PreferencesHandler().getPreferencesInt(PreferenceKeys.DB__MAX_AGE.key);
int timeStampLastStored = Jiffy().subtract(days: maxDays).millisecondsSinceEpoch;
await (await this._maindb).delete(
'data',
where: 'timestamp_nix < ?',
whereArgs: [timeStampLastStored]
);
}
return this._maindb;
}
......@@ -98,8 +113,7 @@ class MainDatabaseHandler {
final Database database = await this._mainDatabase;
int count = await database.rawUpdate('UPDATE data SET exported = 1 WHERE id IN (${ids.join(', ')})');
print('Exported $count');
printDebug('DBHandler: Exported $count to API');