[ermine] Add weather settings.
Currently hardcodes weather location to Mountain View and San
Francisco. Allow changing temperation unit.
Change-Id: I79b7af37bad368672f16c51615fb1742cc4a4aaf
diff --git a/session_shells/ermine/settings/BUILD.gn b/session_shells/ermine/settings/BUILD.gn
index 1d0b709..0dae919 100644
--- a/session_shells/ermine/settings/BUILD.gn
+++ b/session_shells/ermine/settings/BUILD.gn
@@ -13,6 +13,7 @@
"src/battery.dart",
"src/brightness.dart",
"src/memory.dart",
+ "src/weather.dart",
]
deps = [
diff --git a/session_shells/ermine/settings/lib/settings.dart b/session_shells/ermine/settings/lib/settings.dart
index 8ff6011..b5caca8 100644
--- a/session_shells/ermine/settings/lib/settings.dart
+++ b/session_shells/ermine/settings/lib/settings.dart
@@ -5,3 +5,4 @@
export 'src/battery.dart';
export 'src/brightness.dart';
export 'src/memory.dart';
+export 'src/weather.dart';
diff --git a/session_shells/ermine/settings/lib/src/battery.dart b/session_shells/ermine/settings/lib/src/battery.dart
index 31c99f9..82212f7 100644
--- a/session_shells/ermine/settings/lib/src/battery.dart
+++ b/session_shells/ermine/settings/lib/src/battery.dart
@@ -96,7 +96,7 @@
}) : _binding = binding ?? BatteryInfoWatcherBinding() {
monitor
..watch(_binding.wrap(_BatteryInfoWatcherImpl(this)))
- ..getBatteryInfo().then(updateBattery);
+ ..getBatteryInfo().then(_updateBattery);
}
void dispose() {
@@ -109,7 +109,7 @@
onChange?.call();
}
- void updateBattery(BatteryInfo info) {
+ void _updateBattery(BatteryInfo info) {
final chargeStatus = info.chargeStatus;
charging = chargeStatus == ChargeStatus.charging;
battery = info.levelPercent;
@@ -122,6 +122,6 @@
@override
Future<void> onChangeBatteryInfo(BatteryInfo info) async {
- batteryModel.updateBattery(info);
+ batteryModel._updateBattery(info);
}
}
diff --git a/session_shells/ermine/settings/lib/src/weather.dart b/session_shells/ermine/settings/lib/src/weather.dart
new file mode 100644
index 0000000..f5e7c39
--- /dev/null
+++ b/session_shells/ermine/settings/lib/src/weather.dart
@@ -0,0 +1,157 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:fidl_fuchsia_ui_remotewidgets/fidl_async.dart';
+import 'package:internationalization/strings.dart';
+import 'package:quickui/quickui.dart';
+
+const _weatherBaseUrl = 'https://api.weather.gov';
+
+/// Defines a [UiSpec] for displaying weather for locations.
+class Weather extends UiSpec {
+ // Localized strings.
+ static String get _title => Strings.weather;
+
+ static const changeUnitAction = 2;
+
+ // TODO(sanjayc): Replace hardcoded locations with user specified ones.
+ static final List<_Location> _locations = <_Location>[
+ _Location('Mountain View', '37.386051,-122.083855'),
+ _Location('San Francisco', '37.61961,-122.365579'),
+ ];
+
+ // Weather refresh duration.
+ static final _refreshDuration = Duration(minutes: 10);
+ Timer _timer;
+ bool tempInFahrenheit = true;
+
+ Weather() {
+ _timer = Timer.periodic(_refreshDuration, (_) => _onChange());
+ _onChange();
+ }
+
+ void _onChange() async {
+ try {
+ await _refresh();
+ spec = _specForWeather(tempInFahrenheit);
+ } on Exception catch (_) {
+ spec = null;
+ }
+ }
+
+ @override
+ void update(Value value) async {
+ if (value.$tag == ValueTag.button &&
+ value.button.action == changeUnitAction) {
+ tempInFahrenheit = !tempInFahrenheit;
+ spec = _specForWeather(tempInFahrenheit);
+ }
+ }
+
+ @override
+ void dispose() {
+ _timer.cancel();
+ }
+
+ static Spec _specForWeather(bool tempInFahrenheit) {
+ final locations =
+ _locations.where((location) => location.observation != null).toList();
+ if (locations.isEmpty) {
+ return null;
+ }
+ return Spec(groups: [
+ Group(title: _title, values: [
+ if (tempInFahrenheit)
+ Value.withButton(
+ ButtonValue(label: 'Use °C', action: changeUnitAction)),
+ if (!tempInFahrenheit)
+ Value.withButton(
+ ButtonValue(label: 'Use °F', action: changeUnitAction)),
+ Value.withGrid(GridValue(
+ columns: 2,
+ values: List<TextValue>.generate(locations.length * 2, (index) {
+ final location = locations[index ~/ 2];
+ final temp =
+ tempInFahrenheit ? location.fahrenheit : location.degrees;
+ final weather = '$temp ${location.observation}';
+ return TextValue(text: index.isEven ? location.name : weather);
+ }))),
+ ]),
+ ]);
+ }
+
+ static Future<void> _refresh() async {
+ await Future.forEach(_locations, (location) async {
+ // Load the station if it is not loaded yet.
+ if (location.station == null) {
+ await _loadStation(location);
+ }
+ // Now load the current weather data for the station.
+ await _loadCurrentCondition(location);
+ });
+ }
+
+ // Get the first weather station for [_Location] point using:
+ // https://www.weather.gov/documentation/services-web-api#/default/get_stations__stationId__observations_latest
+ static Future<void> _loadStation(_Location location) async {
+ // Get the
+ final stationsUrl = '$_weatherBaseUrl/points/${location.point}/stations';
+ var request = await HttpClient().getUrl(Uri.parse(stationsUrl));
+ var response = await request.close();
+ var result = await _readResponse(response);
+ var data = json.decode(result);
+ List features = data['features'];
+ if (features.isNotEmpty) {
+ location.station = features[0]['properties']['stationIdentifier'];
+ }
+ }
+
+ // Get the latest observation for weather station in [_Location] using:
+ // https://www.weather.gov/documentation/services-web-api#/default/get_points__point__stations
+ static Future<void> _loadCurrentCondition(_Location location) async {
+ final observationUrl =
+ '$_weatherBaseUrl/stations/${location.station}/observations/latest';
+ var request = await HttpClient().getUrl(Uri.parse(observationUrl));
+ var response = await request.close();
+ var result = await _readResponse(response);
+ var data = json.decode(result);
+ var properties = data['properties'];
+ location
+ ..observation = properties['textDescription']
+ ..tempInDegrees = properties['temperature']['value'].toDouble();
+ }
+
+ // Read the string response from the [HttpClientResponse].
+ static Future<String> _readResponse(HttpClientResponse response) {
+ var completer = Completer<String>();
+ var contents = StringBuffer();
+ response.transform(utf8.decoder).listen((data) {
+ contents.write(data);
+ }, onDone: () => completer.complete(contents.toString()));
+ return completer.future;
+ }
+}
+
+// Holds weather data for a location.
+class _Location {
+ final String name;
+ final String point;
+ String observation;
+ String station;
+ double tempInDegrees;
+
+ _Location(this.name, this.point, [this.observation]);
+
+ String get degrees => '${tempInDegrees.toInt()}°C';
+
+ String get fahrenheit => '${(tempInDegrees * 1.8 + 32).toInt()}°F';
+
+ @override
+ String toString() =>
+ 'name: $name station: $station observation: $observation temp: $tempInDegrees';
+}
diff --git a/session_shells/ermine/shell/lib/src/models/status_model.dart b/session_shells/ermine/shell/lib/src/models/status_model.dart
index 9aed8e1..9bdde9b 100644
--- a/session_shells/ermine/shell/lib/src/models/status_model.dart
+++ b/session_shells/ermine/shell/lib/src/models/status_model.dart
@@ -22,6 +22,7 @@
UiStream brightness;
UiStream memory;
UiStream battery;
+ UiStream weather;
final StartupContext startupContext;
final modular.PuppetMasterProxy puppetMaster;
final AdministratorProxy deviceManager;
@@ -30,6 +31,7 @@
brightness = UiStream(Brightness.fromStartupContext(startupContext));
memory = UiStream(Memory.fromStartupContext(startupContext));
battery = UiStream(Battery.fromStartupContext(startupContext));
+ weather = UiStream(Weather());
}
factory StatusModel.fromStartupContext(StartupContext startupContext) {
@@ -52,6 +54,7 @@
brightness.dispose();
memory.dispose();
battery.dispose();
+ weather.dispose();
}
/// Launch settings mod.
diff --git a/session_shells/ermine/shell/lib/src/widgets/status/status.dart b/session_shells/ermine/shell/lib/src/widgets/status/status.dart
index 444c3f1..30458ba 100644
--- a/session_shells/ermine/shell/lib/src/widgets/status/status.dart
+++ b/session_shells/ermine/shell/lib/src/widgets/status/status.dart
@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'dart:ui';
+
import 'package:fidl_fuchsia_ui_remotewidgets/fidl_async.dart';
import 'package:flutter/material.dart';
import 'package:internationalization/strings.dart';
@@ -42,6 +44,7 @@
_StatusEntry(model.brightness),
_StatusEntry(model.battery),
_StatusEntry(model.memory),
+ _StatusEntry(model.weather),
],
),
);
@@ -86,58 +89,25 @@
List<Widget> result = <Widget>[];
List<Widget> widgets = <Widget>[];
- Widget titleRow(String title, List<Widget> children) {
- return Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- textBaseline: TextBaseline.alphabetic,
- children: <Widget>[
- Container(
- width: kTitleWidth,
- child: Text(title.toUpperCase()),
- ),
- Expanded(
- child: Wrap(
- children: children,
- crossAxisAlignment: WrapCrossAlignment.center,
- alignment: WrapAlignment.end,
- spacing: kPadding,
- runSpacing: kPadding,
- ),
- ),
- ],
- );
- }
-
- Widget valueRow(List<Widget> children) {
- return Wrap(
- children: children,
- crossAxisAlignment: WrapCrossAlignment.center,
- alignment: WrapAlignment.end,
- spacing: kPadding,
- runSpacing: kPadding,
- );
- }
-
for (final group in spec.groups) {
for (final value in group.values) {
final widget = _buildFromValue(value, update);
- if (value is GridValue) {
+ if (value.$tag == ValueTag.grid) {
if (result.isEmpty) {
- result.add(titleRow(group.title, widgets.toList()));
+ result.add(_buildTitleRow(group.title, widgets.toList()));
} else {
- result.add(valueRow(widgets.toList()));
+ result.add(_buildValueRow(widgets.toList()));
}
widgets.clear();
- result.add(widget);
- } else {
- widgets.add(widget);
}
+ widgets.add(widget);
}
+
if (widgets.isNotEmpty) {
if (result.isEmpty) {
- result.add(titleRow(group.title, widgets.toList()));
+ result.add(_buildTitleRow(group.title, widgets.toList()));
} else {
- result.add(valueRow(widgets.toList()));
+ result.add(_buildValueRow(widgets.toList()));
}
}
}
@@ -164,14 +134,30 @@
}
if (value.$tag == ValueTag.grid) {
- return Padding(
- padding: EdgeInsets.symmetric(vertical: 12),
- child: GridView.count(
- shrinkWrap: true,
- childAspectRatio: 4,
- physics: NeverScrollableScrollPhysics(),
- crossAxisCount: value.grid.columns,
- children: value.grid.values.map((v) => Text(v.text)).toList(),
+ int columns = value.grid.columns;
+ int rows = value.grid.values.length ~/ columns;
+ return Container(
+ padding: EdgeInsets.only(left: 8),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: List<Widget>.generate(rows, (row) {
+ return Padding(
+ padding: EdgeInsets.symmetric(vertical: 4),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: List<Widget>.generate(value.grid.columns, (column) {
+ final index = row * value.grid.columns + column;
+ return Expanded(
+ flex: column == 0 ? 2 : 1,
+ child: Text(
+ value.grid.values[index].text,
+ textAlign: column == 0 ? TextAlign.start : TextAlign.end,
+ ),
+ );
+ }),
+ ),
+ );
+ }),
),
);
}
@@ -194,6 +180,49 @@
}
}
+Widget _buildValueRow(List<Widget> children) {
+ return Wrap(
+ children: children,
+ alignment: WrapAlignment.end,
+ spacing: kPadding,
+ runSpacing: kPadding,
+ );
+}
+
+Widget _buildTitleRow(String title, List<Widget> children) {
+ return Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ textBaseline: TextBaseline.alphabetic,
+ children: <Widget>[
+ Container(
+ width: kTitleWidth,
+ child: Text(title.toUpperCase()),
+ ),
+ Expanded(
+ child: _buildValueRow(children),
+ ),
+ ],
+ );
+}
+
+Widget _buildButton(String label, void Function() onTap) {
+ return GestureDetector(
+ onTap: onTap,
+ child: Container(
+ height: kItemHeight,
+ color: Colors.white,
+ padding: EdgeInsets.symmetric(vertical: 0, horizontal: 2),
+ child: Text(
+ label.toUpperCase(),
+ style: TextStyle(
+ color: Colors.black,
+ fontWeight: FontWeight.w400,
+ ),
+ ),
+ ),
+ );
+}
+
class _ManualStatusEntry extends StatelessWidget {
final StatusModel model;
@@ -216,21 +245,3 @@
);
}
}
-
-Widget _buildButton(String label, void Function() onTap) {
- return GestureDetector(
- onTap: onTap,
- child: Container(
- height: kItemHeight,
- color: Colors.white,
- padding: EdgeInsets.symmetric(vertical: 0, horizontal: 2),
- child: Text(
- label.toUpperCase(),
- style: TextStyle(
- color: Colors.black,
- fontWeight: FontWeight.w400,
- ),
- ),
- ),
- );
-}