Commit 04f3929c authored by Nicolas Richard Walter Boeckh's avatar Nicolas Richard Walter Boeckh 💬

DB, Settings+, Graph

DB:

- DataHandler pumps to DB
- Network takes from DB and sets export flag
- DataPacket has overloaded constructors depending on reconstruction usage.

Settings :
  - Layout (ExpansionTile)
  - WIP Fix RenderFlex Error
  - MapSettings Removed, added to Settings.

Graph:
- Graph WIP (takes from DB).
- TODO State Management.

Bugs:
- BTLE reco/deco
- RenderFlex in Settings
- Values not updating in graph, state management.

- Found
parent c378db21
{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"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":"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":"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":"shared_preferences_web","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\shared_preferences_web-0.1.2+5\\\\","dependencies":[]}]},"dependencyGraph":[{"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-07-11 13:12:25.061947","version":"1.19.0-2.0.pre.49"}
\ No newline at end of file
{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"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":"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":"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":"shared_preferences_web","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\shared_preferences_web-0.1.2+5\\\\","dependencies":[]}]},"dependencyGraph":[{"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-07-13 03:41:01.914636","version":"1.19.0-2.0.pre.49"}
\ No newline at end of file
......@@ -29,8 +29,20 @@ class DataHeader {
/// @Getter for _headerSet;
bool get headerSet => this._headerSet;
Stream<bool> getWhenHeaderSet() async* {
while (true) {
yield this._headerSet;
await Future.delayed(Duration(seconds: 1));
}
}
bool isEqual(List<int> that) => ListEquality().equals(_headerData, that);
// TODO
Map<String, dynamic> jsonify() => { 'device_id' : this._headerData[0], 'url': '000000000000' };
void dispose() {
this._headerSet = false;
this._headerData = [];
}
}
\ No newline at end of file
......@@ -78,6 +78,24 @@ 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<String, dynamic> data) {
this._timestamp = data['timestamp_nix'];
this._latitude = data['latitude'];
this._longitude = data['longitude'];
this._altitude = data['altitude'];
this._speed = data['speed'];
this._heading = data['heading'];
this._temperature = data['temperature'];
this._relativeHumidity = data['relative_humidity'];
this._pressure = data['pressure'];
this._pm1 = data['pm_1'];
this._pm2_5 = data['pm_2_5'];
this._pm4 = data['pm_4'];
this._pm10 = data['pm_10'];
this._extraData = data['extra'];
}
String stringify() => '${this._latitude}, ${this._longitude}, ${this._temperature}, ${this._pressure}, ${this._relativeHumidity}, ${this._pm2_5}, ${this._pm10}';
List<dynamic> jsonify() => [
......
......@@ -80,7 +80,7 @@ class BTLEHandler {
_characteristic = null;
_device = null;
_bluetoothConnectionStatus = BluetoothConnectionStatus.BTSTATUS_NOT_STREAMING;
DataHandler().dispose();
return it.disconnect();
});
......
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';
class DataHandler {
factory DataHandler() => _singleton;
......@@ -8,7 +9,7 @@ class DataHandler {
DataHandler._internal();
static const HEADER_SIZE = 11;
static final DataHandler _singleton = new DataHandler._internal();
static final DataHandler _singleton = new DataHandler._internal();
/// [List] containing all of the data received by this.
List<int> _data = [];
......@@ -49,11 +50,18 @@ class DataHandler {
/// If it's a [DataHeader] instance.
if (packet[0] == 123) {
print('HEADER ${String.fromCharCodes(packet)}');
bool wasSet = DataHeader().headerSet;
DataHeader().setHeader(packet);
/// Retroactively add all previous packets
if (!wasSet && DataHeader().headerSet)
_sortedData.forEach((packet) => DatabaseHandler().insertData(packet, DataHeader().deviceID));
/// 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);
}
}
/// TODO Used for debugging, later
......@@ -89,11 +97,13 @@ class DataHandler {
}
}
@deprecated
/// Used to monitor the amount of unsent [DataPacket]s.
Stream<int> getUnsentPacketsLength() async* {
while (true) {
yield (_sortedData.length);
await Future.delayed(Duration(seconds: 1));
}
}
void dispose() {
DataHeader().dispose();
this._data.removeWhere((element) => true);
}
}
\ No newline at end of file
import 'package:logair_application/logic/data_header.dart';
import 'package:logair_application/logic/data_packet.dart';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
import 'package:tuple/tuple.dart';
class DatabaseHandler {
factory DatabaseHandler() => _singleton;
Future<Database> _db;
bool shouldCache;
Future<Database> get _database async {
if (_db == null)
this._db = openDatabase(
......@@ -66,11 +70,13 @@ class DatabaseHandler {
static final DatabaseHandler _singleton = new DatabaseHandler._internal();
Future<void> insertData(DataPacket packet, String device_id) async {
Future<void> insertData(DataPacket packet, String deviceId) async {
final Database database = await _database;
Map<String, dynamic> insert = packet.toMap();
insert['device_id'] = deviceId;
await database.insert(
'data',
packet.toMap(),
insert,
conflictAlgorithm: ConflictAlgorithm.replace
);
}
......@@ -79,28 +85,60 @@ class DatabaseHandler {
final Database database = await _database;
final List<Map<String, dynamic>> data = await database.query('data');
print(data);
return List.generate(data.length, (i) => DataPacket.fromData(
data[i]['timestamp_nix'],
data[i]['latitude'],
data[i]['longitude'],
data[i]['altitude'],
data[i]['speed'],
data[i]['heading'],
data[i]['temperature'],
data[i]['relative_humidity'],
data[i]['pressure'],
data[i]['pm_1'],
data[i]['pm_2_5'],
data[i]['pm_4'],
data[i]['pm_10'],
data[i]['extra'],
)
);
return List.generate(data.length, (i) => DataPacket.fromMappedData(data[i]));
}
Future<int> deleteAll() async => await (await this._database).delete('data');
Future<void> close() async => (await this._database).close();
Stream<int> getDBSize() async* {
final Database database = await _database;
while (true) {
final List<Map<String, dynamic>> data = await database.rawQuery('SELECT COUNT(*) AS count FROM data');
yield data[0]['count'] ?? 0;
await Future.delayed(Duration(seconds: 5));
}
}
Future<List<Tuple2<int, DataPacket>>> getUnsent(int limit) async {
final Database database = await _database;
final List<Map<String, dynamic>> data = await database.query('data',
where: 'exported = ?',
whereArgs: [0],
limit: limit,
orderBy: 'timestamp_nix ASC'
);
print(data);
return List.generate(data.length, (i) => Tuple2(data[i]['id'], DataPacket.fromMappedData(data[i])));
}
Future<void> setExported(List<int> ids) async {
final Database database = await _database;
print('UPDATE data SET exported = 1 WHERE id IN (${ids.join(', ')})');
int count = await database.rawUpdate('UPDATE data SET exported = 1 WHERE id IN (${ids.join(', ')})');
print('Exported $count');
}
/// Used to monitor the amount of unsent [DataPacket]s.
Stream<int> getUnsentPacketsLength() async* {
final Database database = await _database;
while (true) {
final List<Map<String, dynamic>> data = await database.rawQuery('SELECT COUNT(*) AS count FROM data WHERE exported = 0');
yield data[0]['count'] ?? 0;
await Future.delayed(Duration(seconds: 1));
}
}
Stream<Map<String, dynamic>> getLatest() async* {
final Database database = await _database;
while (true) {
final List<Map<String, dynamic>> 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 ORDER BY id DESC LIMIT 5)');
yield data[0] ?? Map();
await Future.delayed(Duration(seconds: 5));
}
}
}
\ No newline at end of file
......@@ -8,6 +8,8 @@ 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:tuple/tuple.dart';
/// Singleton data structure that contains services enabling the application to send data to a webserver.
/// TODO Make URL user configurable.
......@@ -15,6 +17,7 @@ class NetworkHandler {
factory NetworkHandler() => _singleton;
NetworkHandler._internal() {
// TODO Preferences
/// Define the interval between attempts to send the data to the server. (default: 1 minute).
const duration = const Duration(minutes: 1);
new Timer.periodic(duration, (Timer t) => this._sendDataToServer());
......@@ -26,6 +29,7 @@ class NetworkHandler {
/// Method to send acquired data packets by using a POST requests.
void _sendDataToServer() async {
// TODO Preferences
/// The default endpoint.
String url = 'https://api.logair.unige.ch/v1/service';
......@@ -36,8 +40,10 @@ class NetworkHandler {
/// Get the map representation of a [DataHeader].
Map<String, dynamic> headerData = DataHeader().jsonify();
List<Tuple2<int, DataPacket>> databaseData = await DatabaseHandler().getUnsent(100);
/// Get all the [DataPacket]s to be sent (maximum 100).
List<DataPacket> packets = DataHandler().getDataRange(100);
List<DataPacket> packets = databaseData.map((x) => x.item2).toList();
/// Convert the [DataPacket] list to a [Map] of their [List] representations.
Map<String, List<List<dynamic>>> packetList = {
......@@ -62,7 +68,8 @@ class NetworkHandler {
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());
print('RESPONSE ${response.statusCode}');
this._lastServerTransmission = DateTime.now();
}
......
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tuple/tuple.dart';
/// Handler enabling the storage and use of preferences.
class PreferencesHandler {
Map<String, Tuple2<dynamic, dynamic>> _defaultValues = {
'BT.USING_ADVANCED': Tuple2(bool, false),
'BT.FREQUENT': Tuple2(String, '[ ]'),
'BT.SERVICE_UUID': Tuple2(String, '0000ffe0-0000-1000-8000-00805f9b34fb'),
'BT.CHARACTERISTIC_UUID': Tuple2(String, '0000ffe1-0000-1000-8000-00805f9b34fb'),
'MAP.USING_ADVANCED': Tuple2(bool, false),
'MAP.PROX.MAX_PER_QUERY': Tuple2(int, 200),
'MAP.PROX.MAX_AGE_SECONDS': Tuple2(int, 200)
};
static final PreferencesHandler _singleton = new PreferencesHandler._internal();
SharedPreferences _sharedPreferences;
SharedPreferences _sharedPreferencesInternal;
PreferencesHandler._internal() {
SharedPreferences.getInstance().then((SharedPreferences prefs) {
this._sharedPreferences = prefs;
Future<SharedPreferences> get _sharedPreferences async {
if (_sharedPreferencesInternal == null) {
this._sharedPreferencesInternal = await SharedPreferences.getInstance();
/// Initial Setup
if (!prefs.containsKey('BT.USING_ADVANCED')) prefs.setBool('BT.USING_ADVANCED', false);
if (!prefs.containsKey('BT.USING_ADVANCED_DEFAULT')) prefs.setBool('BT.USING_ADVANCED_DEFAULT', false);
if (!prefs.containsKey('BT.FREQUENT')) prefs.setString('BT.FREQUENT', '[ ]');
if (!prefs.containsKey('BT.FREQUENT_DEFAULT')) prefs.setString('BT.FREQUENT_DEFAULT', '[ ]');
_defaultValues.forEach((key, value) {
Function setter;
switch (value.item1) {
case int:
setter = this._sharedPreferencesInternal.setInt;
break;
case bool:
setter = this._sharedPreferencesInternal.setBool;
break;
case String:
setter = this._sharedPreferencesInternal.setString;
break;
default:
throw ArgumentError('Unexpected property type');
}
if (!this._sharedPreferencesInternal.containsKey(key)) {
setter(key, value.item2);
setter(key + '_DEFAULT', value.item2);
}
});
print('Setup preferences \n ${List.generate(this._sharedPreferencesInternal.getKeys().length, (i) => this._sharedPreferencesInternal.getKeys().elementAt(i) + ' => ' + this._sharedPreferencesInternal.get(this._sharedPreferencesInternal.getKeys().elementAt(i)).toString()).join("\n")}');
}
if (!prefs.containsKey('BT.SERVICE_UUID')) prefs.setString('BT.SERVICE_UUID', '0000ffe0-0000-1000-8000-00805f9b34fb');
if (!prefs.containsKey('BT.SERVICE_UUID_DEFAULT')) prefs.setString('BT.SERVICE_UUID_DEFAULT', '0000ffe0-0000-1000-8000-00805f9b34fb');
return this._sharedPreferencesInternal;
}
if (!prefs.containsKey('BT.CHARACTERISTIC_UUID')) prefs.setString('BT.CHARACTERISTIC_UUID', '0000ffe1-0000-1000-8000-00805f9b34fb');
if (!prefs.containsKey('BT.CHARACTERISTIC_UUID_DEFAULT')) prefs.setString('BT.CHARACTERISTIC_UUID_DEFAULT', '0000ffe1-0000-1000-8000-00805f9b34fb');
PreferencesHandler._internal();
if (!prefs.containsKey('MAP.USING_ADVANCED')) prefs.setBool('MAP.USING_ADVANCED', false);
if (!prefs.containsKey('MAP.USING_ADVANCED_DEFAULT')) prefs.setBool('MAP.USING_ADVANCED_DEFAULT', false);
Future<int> getPreferencesInt(String key) async => (await this._sharedPreferences).getInt(key);
Future<String> getPreferencesString(String key) async => (await this._sharedPreferences).getString(key);
Future<bool> getPreferencesBool(String key) async => (await this._sharedPreferences).getBool(key);
if (!prefs.containsKey('MAP.PROX.MAX_PER_QUERY')) prefs.setInt('MAP.PROX.MAX_PER_QUERY', 200);
if (!prefs.containsKey('MAP.PROX.MAX_PER_QUERY_DEFAULT')) prefs.setInt('MAP.PROX.MAX_PER_QUERY_DEFAULT', 200);
Future<dynamic> getPreferences(String key) async => (await this._sharedPreferences).get(key);
if (!prefs.containsKey('MAP.PROX.MAX_AGE_SECONDS')) prefs.setInt('MAP.PROX.MAX_AGE_SECONDS', 200);
if (!prefs.containsKey('MAP.PROX.MAX_AGE_SECONDS_DEFAULT')) prefs.setInt('MAP.PROX.MAX_AGE_SECONDS_DEFAULT', 200);
Future<void> setPreferencesInt(String key, int value) async => (await this._sharedPreferences).setInt(key, value);
Future<void> setPreferencesString(String key, String value) async => (await this._sharedPreferences).setString(key, value);
Future<void> setPreferencesBool(String key, bool value) async => (await this._sharedPreferences).setBool(key, value);
Future<void> resetPreferences<T>(String key) async {
Function getter, setter;
switch (T) {
case int:
getter = getPreferencesInt;
setter = setPreferencesInt;
break;
case bool:
getter = getPreferencesBool;
setter = setPreferencesBool;
break;
case String:
getter = getPreferencesString;
setter = setPreferencesString;
}
print('Setup Prefs ${prefs.getKeys()}');
});
T value = await getter(key + '_DEFAULT');
await setter(key, value);
}
int getPreferencesInt(String key) => this._sharedPreferences.getInt(key);
String getPreferencesString(String key) => this._sharedPreferences.getString(key);
bool getPreferencesBool(String key) => this._sharedPreferences.getBool(key);
void setPreferencesInt(String key, int value) => this._sharedPreferences.setInt(key, value);
void setPreferencesString(String key, String value) => this._sharedPreferences.setString(key, value);
void setPreferencesBool(String key, bool value) => this._sharedPreferences.setBool(key, value);
void resetAllPreferences() async {
await (await this._sharedPreferences).setBool('BT.USING_ADVANCED', false);
await (await this._sharedPreferences).setString('BT.FREQUENT', '[ ]');
await (await this._sharedPreferences).setString('BT.SERVICE_UUID', '0000ffe0-0000-1000-8000-00805f9b34fb');
await (await this._sharedPreferences).setString('BT.CHARACTERISTIC_UUID', '0000ffe1-0000-1000-8000-00805f9b34fb');
await (await this._sharedPreferences).setBool('MAP.USING_ADVANCED', false);
await (await this._sharedPreferences).setInt('MAP.PROX.MAX_PER_QUERY', 200);
await (await this._sharedPreferences).setInt('MAP.PROX.MAX_AGE_SECONDS', 200);
}
factory PreferencesHandler() => _singleton;
}
\ No newline at end of file
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:logair_application/logic/handlers/preference_handler.dart';
/// This [Widget] is a [Form] that enables users to change settings.
/// It interacts with the [PreferencesHandler] to affect changes that will be maintained on app restart.
class SettingsForm extends StatefulWidget {
SettingsForm({Key key});
@override
State<StatefulWidget> createState() => _SettingsFormState(key: key);
}
class _SettingsFormState extends State<SettingsForm> {
_SettingsFormState({Key key}) {
PreferencesHandler().getPreferencesBool('BT.USING_ADVANCED').then((value) {
if (this._advancedBTOptions == null)
setState(() {
this._advancedBTOptions = value;
});
});
PreferencesHandler().getPreferencesBool('MAP.USING_ADVANCED').then((value) {
if (this._advancedMapOptions == null)
setState(() {
this._advancedMapOptions = value;
});
});
}
final _formKey = GlobalKey<FormState>(debugLabel: 'FORM_GENERAL');
bool _advancedBTOptions;
bool _advancedMapOptions;
final _bleServiceController = TextEditingController();
final _bleCharacteristicController = TextEditingController();
final _mapQueryLimitController = TextEditingController();
final _mapQueryAgeController = TextEditingController();
@override
void dispose() {
// Clean up the controller when the widget is disposed.
_bleServiceController.dispose();
_bleCharacteristicController.dispose();
_mapQueryLimitController.dispose();
_mapQueryAgeController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
/// The [Form] is comprised of multiple [ExpansionTile]s that represent settings subsections from general app management to more advanced settings.
return Form(
key: _formKey,
child: Column(
children: [
// TODO Fix renderFlex overflow
SingleChildScrollView(
child: Column(
children: [
// NETWORKING
ExpansionTile(
title: Text('Network'),
initiallyExpanded: true,
leading: Icon(Icons.network_check),
children: [
ListTile(title: Text('TODO: Max number of items per push (def 100)')),
ListTile(title: Text('TODO: Push Frequency')),
ListTile(title: Text('TODO: Push Destination')),
ListTile(title: Text('TODO: Push on 3G/4G')),
],
),
Divider(color: Colors.black,),
// CACHING
ExpansionTile(
title: Text('Local Data'),
initiallyExpanded: true,
leading: Icon(Icons.storage),
children: [
ListTile(title: Text('TODO: Delete local data'))
],
),
Divider(color: Colors.black,),
// BLUETOOTH
ExpansionTile(
title: Text('Bluetooth'),
subtitle: Text('(Advanced)'),
leading: Icon(Icons.bluetooth),
children: [
CheckboxListTile(
title: Text('Modify advanced bluetooth settings'),
value: _advancedBTOptions ?? false,
onChanged: (newValue) => setState(() {
_advancedBTOptions = newValue;
}),
controlAffinity: ListTileControlAffinity.leading,
),
Visibility(
visible: _advancedBTOptions ?? false,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 10),
child: Text(
'Changing these settings can break device data acquisition',
style: TextStyle(
color: Colors.red[600],
fontSize: 12
),
textAlign: TextAlign.center,
),
),
),
// TODO Service / Characteristic Explorer ?
_buildInputTile(_bleServiceController, 'BLE Service UUID', 'BT.SERVICE_UUID', _advancedBTOptions, TextInputType.text, <TextInputFormatter>[ ], RegExp('[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}')),
_buildInputTile(_bleCharacteristicController, 'BLE Characteristic UUID', 'BT.CHARACTERISTIC_UUID', _advancedBTOptions, TextInputType.text, <TextInputFormatter>[ ], RegExp('[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}')),
],
),
Divider(color: Colors.black,),
// MAP
ExpansionTile(
title: Text('Map'),
subtitle: Text('(Advanced)'),
leading: Icon(Icons.map),
children: [
CheckboxListTile(
title: Text('Modify advanced map settings'),
value: _advancedMapOptions ?? false,
onChanged: (newValue) {
Future.delayed(Duration.zero, () => {
if (newValue != _advancedMapOptions) setState(() {
_advancedMapOptions = newValue;
})
});
},
controlAffinity: ListTileControlAffinity.leading,
),
Visibility(
visible: _advancedMapOptions ?? false,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 10),
child: Text(
'Changing these settings can have serious performance impacts on your device and battery life',
style: TextStyle(
color: Colors.red[600],
fontSize: 12
),
textAlign: TextAlign.center,
),
),
),
_buildInputTile(_mapQueryLimitController, 'Maximum other points', 'MAP.PROX.MAX_PER_QUERY', _advancedMapOptions, TextInputType.number, <TextInputFormatter>[ WhitelistingTextInputFormatter.digitsOnly ], RegExp(r'\d+')),
_buildInputTile(_mapQueryAgeController, 'Maximum age of other points (seconds)', 'MAP.PROX.MAX_AGE_SECONDS', _advancedMapOptions, TextInputType.number, <TextInputFormatter>[ WhitelistingTextInputFormatter.digitsOnly ], RegExp(r'\d+')),
],
),
],
),
),
Spacer(flex: 1), // To force the row at the bottom of the screen
/// Action buttons (Confirm, Reset)
Row(
children: [
Expanded(
flex: 1,
child: RaisedButton(
onPressed: () {
print(_formKey.currentState.validate());
if (_formKey.currentState.validate()) {
PreferencesHandler().setPreferencesBool('BT.USING_ADVANCED', _advancedBTOptions);
PreferencesHandler().setPreferencesString('BT.SERVICE_UUID', _bleServiceController.text);
PreferencesHandler().setPreferencesString('BT.CHARACTERISTIC_UUID', _bleCharacteristicController.text);
PreferencesHandler().setPreferencesBool('MAP.USING_ADVANCED', _advancedMapOptions);
}
},
child: Text('Confirm Changes'),
),
),
Expanded(
flex: 1,
child: RaisedButton(
onPressed: () async {
PreferencesHandler().resetAllPreferences();
_formKey.currentState.reset();
},
child: Text('Reset values'),
),
),
],
)
],
)
);
}
/// This method builds a tile that enables a user to change the value of a setting.
/// TODO Individual field reset button key + '_DEFAULT'.
Widget _buildInputTile(TextEditingController controller, String labelText, String key, bool enabled, TextInputType inputType, List<TextInputFormatter> inputFormatters, RegExp validation) {
PreferencesHandler().getPreferences(key).then((value) => controller.text = value.toString());
return Container(
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: TextFormField(
controller: controller,
enabled: enabled,
decoration: InputDecoration(
labelText: labelText,
contentPadding: EdgeInsets.zero,
),
keyboardType: inputType,
inputFormatters: inputFormatters,
validator: (value) {
if (value.isEmpty) {