[ermine] Add datetime and timezone quick settings.

- Fix weather setting to use hardcoded station for now.
- Update existing settings based on quickui.fidl changes in
  http://fxr/335169
- Refactor status.dart to separate widgets for quickui spec types.
- Add ability to display detail view for timezone setting.

Testing: Includes tests for datetime and timezone.

Change-Id: I0cf8b842acaf0da8e5305c207aa7ae9712035355
diff --git a/lib/quickui/test/quickui_test.dart b/lib/quickui/test/quickui_test.dart
index 9bf75ae..c84d97d 100644
--- a/lib/quickui/test/quickui_test.dart
+++ b/lib/quickui/test/quickui_test.dart
@@ -44,6 +44,7 @@
   @override
   void update(Value value) {
     spec = Spec(
+      title: '',
       groups: <Group>[
         Group(title: 'Bar', values: [value]),
       ],
@@ -54,6 +55,6 @@
   void dispose() {}
 
   static Spec _build() {
-    return Spec(groups: <Group>[Group(title: 'Foo', values: [])]);
+    return Spec(title: '', groups: <Group>[Group(title: 'Foo', values: [])]);
   }
 }
diff --git a/lib/quickui/test/uistream_test.dart b/lib/quickui/test/uistream_test.dart
index e20cc46..fe406cf 100644
--- a/lib/quickui/test/uistream_test.dart
+++ b/lib/quickui/test/uistream_test.dart
@@ -50,6 +50,7 @@
   @override
   void update(Value value) {
     spec = Spec(
+      title: '',
       groups: <Group>[
         Group(title: 'Bar', values: [value]),
       ],
@@ -60,6 +61,6 @@
   void dispose() {}
 
   static Spec _build() {
-    return Spec(groups: <Group>[Group(title: 'Foo', values: [])]);
+    return Spec(title: '', groups: <Group>[Group(title: 'Foo', values: [])]);
   }
 }
diff --git a/session_shells/ermine/internationalization/lib/strings.dart b/session_shells/ermine/internationalization/lib/strings.dart
index fa3d215..487168a 100644
--- a/session_shells/ermine/internationalization/lib/strings.dart
+++ b/session_shells/ermine/internationalization/lib/strings.dart
@@ -164,12 +164,30 @@
         desc: 'The short name for the "Weather" label',
       );
 
+  static String get unit => Intl.message(
+        'Unit',
+        name: 'unit',
+        desc: 'The short name for the unit of measurement',
+      );
+
   static String get date => Intl.message(
         'Date',
         name: 'date',
         desc: 'The short name for the "date" label',
       );
 
+  static String get dateTime => Intl.message(
+        'Date & Time',
+        name: 'dateTime',
+        desc: 'The short name for the "date & time" label',
+      );
+
+  static String get timezone => Intl.message(
+        'Timezone',
+        name: 'timezone',
+        desc: 'The short name for the "timezone" label',
+      );
+
   static String get network => Intl.message(
         'Network',
         name: 'network',
diff --git a/session_shells/ermine/settings/BUILD.gn b/session_shells/ermine/settings/BUILD.gn
index d88c8d0..3a49b1b 100644
--- a/session_shells/ermine/settings/BUILD.gn
+++ b/session_shells/ermine/settings/BUILD.gn
@@ -13,7 +13,9 @@
     "src/battery.dart",
     "src/bluetooth.dart",
     "src/brightness.dart",
+    "src/datetime.dart",
     "src/memory.dart",
+    "src/timezone.dart",
     "src/volume.dart",
     "src/weather.dart",
   ]
@@ -23,6 +25,7 @@
     "//sdk/fidl/fuchsia.media",
     "//sdk/fidl/fuchsia.memory",
     "//sdk/fidl/fuchsia.power",
+    "//sdk/fidl/fuchsia.timezone",
     "//sdk/fidl/fuchsia.ui.brightness",
     "//src/experiences/lib/quickui",
     "//src/experiences/session_shells/ermine/internationalization",
@@ -34,7 +37,9 @@
 flutter_test("ermine_settings_unittests") {
   sources = [
     "brightness_test.dart",
+    "datetime_test.dart",
     "memory_test.dart",
+    "timezone_test.dart",
   ]
 
   deps = [
diff --git a/session_shells/ermine/settings/lib/settings.dart b/session_shells/ermine/settings/lib/settings.dart
index 9fe40a8..6b9ddbe 100644
--- a/session_shells/ermine/settings/lib/settings.dart
+++ b/session_shells/ermine/settings/lib/settings.dart
@@ -5,6 +5,8 @@
 export 'src/battery.dart';
 export 'src/bluetooth.dart';
 export 'src/brightness.dart';
+export 'src/datetime.dart';
 export 'src/memory.dart';
+export 'src/timezone.dart';
 export 'src/volume.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 82212f7..2458e4c 100644
--- a/session_shells/ermine/settings/lib/src/battery.dart
+++ b/session_shells/ermine/settings/lib/src/battery.dart
@@ -51,14 +51,14 @@
       return null;
     }
     if (value == 100) {
-      return Spec(groups: [
+      return Spec(title: _title, groups: [
         Group(title: _title, values: [
           Value.withIcon(IconValue(codePoint: Icons.battery_full.codePoint)),
           Value.withText(TextValue(text: batteryText)),
         ]),
       ]);
     } else if (charging) {
-      return Spec(groups: [
+      return Spec(title: _title, groups: [
         Group(title: _title, values: [
           Value.withIcon(
               IconValue(codePoint: Icons.battery_charging_full.codePoint)),
@@ -66,14 +66,14 @@
         ]),
       ]);
     } else if (value <= 10) {
-      return Spec(groups: [
+      return Spec(title: _title, groups: [
         Group(title: _title, values: [
           Value.withIcon(IconValue(codePoint: Icons.battery_alert.codePoint)),
           Value.withText(TextValue(text: batteryText)),
         ]),
       ]);
     } else {
-      return Spec(groups: [
+      return Spec(title: _title, groups: [
         Group(title: _title, values: [
           Value.withText(TextValue(text: batteryText)),
         ]),
@@ -96,7 +96,8 @@
   }) : _binding = binding ?? BatteryInfoWatcherBinding() {
     monitor
       ..watch(_binding.wrap(_BatteryInfoWatcherImpl(this)))
-      ..getBatteryInfo().then(_updateBattery);
+      ..getBatteryInfo().then(_updateBattery)
+      ..ctrl.close();
   }
 
   void dispose() {
diff --git a/session_shells/ermine/settings/lib/src/bluetooth.dart b/session_shells/ermine/settings/lib/src/bluetooth.dart
index c9ab07e..dc9aa3c 100644
--- a/session_shells/ermine/settings/lib/src/bluetooth.dart
+++ b/session_shells/ermine/settings/lib/src/bluetooth.dart
@@ -45,7 +45,7 @@
   }
 
   static Spec _specForBluetooth(List<TextValue> values) {
-    return Spec(groups: [
+    return Spec(title: _title, groups: [
       Group(title: _title, values: [
         Value.withIcon(IconValue(codePoint: Icons.bluetooth.codePoint)),
         Value.withGrid(GridValue(columns: 1, values: values))
diff --git a/session_shells/ermine/settings/lib/src/brightness.dart b/session_shells/ermine/settings/lib/src/brightness.dart
index 234bd30..273eced 100644
--- a/session_shells/ermine/settings/lib/src/brightness.dart
+++ b/session_shells/ermine/settings/lib/src/brightness.dart
@@ -68,7 +68,7 @@
   }
 
   static Spec _specForAutoBrightness(double value) {
-    return Spec(groups: [
+    return Spec(title: _title, groups: [
       Group(title: _title, values: [
         Value.withIcon(IconValue(codePoint: Icons.brightness_auto.codePoint)),
         Value.withProgress(ProgressValue(value: value, action: progressAction)),
@@ -78,7 +78,7 @@
   }
 
   static Spec _specForManualBrightness(double value) {
-    return Spec(groups: [
+    return Spec(title: _title, groups: [
       Group(title: _title, values: [
         Value.withIcon(IconValue(codePoint: Icons.brightness_low.codePoint)),
         Value.withProgress(ProgressValue(value: value, action: progressAction)),
diff --git a/session_shells/ermine/settings/lib/src/datetime.dart b/session_shells/ermine/settings/lib/src/datetime.dart
new file mode 100644
index 0000000..cc3b47b
--- /dev/null
+++ b/session_shells/ermine/settings/lib/src/datetime.dart
@@ -0,0 +1,46 @@
+// 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 'package:fidl_fuchsia_ui_remotewidgets/fidl_async.dart';
+import 'package:intl/intl.dart';
+import 'package:internationalization/strings.dart';
+import 'package:quickui/quickui.dart';
+
+/// Defines a [UiSpec] for displaying date and time.
+class Datetime extends UiSpec {
+  // Localized strings.
+  static String get _title => Strings.dateTime;
+  static const Duration refreshDuration = Duration(seconds: 1);
+
+  // Action to change timezone.
+  static int changeAction = QuickAction.details.$value;
+
+  Timer _timer;
+
+  Datetime() {
+    _timer = Timer.periodic(refreshDuration, (_) => _onChange());
+    _onChange();
+  }
+
+  void _onChange() async {
+    spec = _specForDateTime();
+  }
+
+  @override
+  void update(Value value) async {}
+
+  @override
+  void dispose() {
+    _timer?.cancel();
+  }
+
+  static Spec _specForDateTime([int action = 0]) {
+    String dateTime = DateFormat().add_E().add_jm().format(DateTime.now());
+    return Spec(title: _title, groups: [
+      Group(title: _title, values: [Value.withText(TextValue(text: dateTime))]),
+    ]);
+  }
+}
diff --git a/session_shells/ermine/settings/lib/src/memory.dart b/session_shells/ermine/settings/lib/src/memory.dart
index 54a9533..4432266 100644
--- a/session_shells/ermine/settings/lib/src/memory.dart
+++ b/session_shells/ermine/settings/lib/src/memory.dart
@@ -48,7 +48,7 @@
   static Spec _specForMemory(double value, double used, double total) {
     String usedString = (used).toStringAsPrecision(3);
     String totalString = (total).toStringAsPrecision(3);
-    return Spec(groups: [
+    return Spec(title: _memory, groups: [
       Group(title: _memory, values: [
         Value.withProgress(ProgressValue(value: value)),
         Value.withText(TextValue(text: '${usedString}GB / ${totalString}GB')),
diff --git a/session_shells/ermine/settings/lib/src/timezone.dart b/session_shells/ermine/settings/lib/src/timezone.dart
new file mode 100644
index 0000000..be742b1
--- /dev/null
+++ b/session_shells/ermine/settings/lib/src/timezone.dart
@@ -0,0 +1,723 @@
+// 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 'package:flutter/material.dart';
+
+import 'package:fidl_fuchsia_timezone/fidl_async.dart';
+import 'package:fidl_fuchsia_ui_remotewidgets/fidl_async.dart';
+import 'package:fuchsia_logger/logger.dart';
+import 'package:fuchsia_services/services.dart' show StartupContext;
+import 'package:internationalization/strings.dart';
+import 'package:quickui/quickui.dart';
+
+/// Defines a [UiSpec] for displaying and changing timezone.
+class TimeZone extends UiSpec {
+  // Localized strings.
+  static String get _title => Strings.timezone;
+
+  // Action to change timezone.
+  static int changeAction = QuickAction.details.$value;
+
+  _TimeZoneModel model;
+
+  TimeZone({TimezoneProxy timezone, TimezoneWatcherBinding binding}) {
+    model = _TimeZoneModel(
+      timezone: timezone,
+      binding: binding,
+      onChange: _onChange,
+    );
+  }
+
+  factory TimeZone.fromStartupContext(StartupContext startupContext) {
+    // Connect to timeservice and update the system time.
+    final timeService = TimeServiceProxy();
+    startupContext.incoming.connectToService(timeService);
+    timeService.update(3 /* num_retries */).then((success) {
+      if (!success) {
+        log.warning('Failed to update system time from the network.');
+      }
+      timeService.ctrl.close();
+    });
+
+    // Connect to timezone service.
+    final timezoneService = TimezoneProxy();
+    startupContext.incoming.connectToService(timezoneService);
+
+    final timezone = TimeZone(timezone: timezoneService);
+    return timezone;
+  }
+
+  void _onChange() async {
+    spec = _specForTimeZone(model);
+  }
+
+  @override
+  void update(Value value) async {
+    if (value.$tag == ValueTag.button &&
+        value.button.action == QuickAction.cancel.$value) {
+      spec = _specForTimeZone(model);
+    } else if (value.$tag == ValueTag.text && value.text.action > 0) {
+      if (value.text.action == changeAction) {
+        spec = _specForTimeZone(model, changeAction);
+      } else {
+        final index = value.text.action ^ QuickAction.submit.$value;
+        model.timezoneId = _kTimeZones[index].zoneId;
+        spec = _specForTimeZone(model);
+      }
+    }
+  }
+
+  @override
+  void dispose() {
+    model.dispose();
+  }
+
+  static Spec _specForTimeZone(_TimeZoneModel model, [int action = 0]) {
+    if (action == 0 || action & QuickAction.cancel.$value > 0) {
+      return Spec(title: _title, groups: [
+        Group(title: _title, values: [
+          Value.withText(TextValue(
+            text: model.timezoneId,
+            action: changeAction,
+          )),
+        ]),
+      ]);
+    } else if (action == changeAction) {
+      final values = List<TextValue>.generate(
+          _kTimeZones.length,
+          (index) => TextValue(
+                text: _kTimeZones[index].zoneId,
+                action: QuickAction.submit.$value | index,
+              ));
+      return Spec(title: _title, groups: [
+        Group(title: 'Select Timezone', values: [
+          Value.withGrid(GridValue(
+            columns: 1,
+            values: values,
+          )),
+          Value.withButton(ButtonValue(
+            label: 'close',
+            action: QuickAction.cancel.$value,
+          )),
+        ]),
+      ]);
+    } else {
+      return null;
+    }
+  }
+}
+
+class _TimeZoneModel {
+  final TimezoneProxy timezone;
+  final TimezoneWatcherBinding _binding;
+  final VoidCallback onChange;
+
+  String _timezoneId;
+
+  _TimeZoneModel({this.timezone, TimezoneWatcherBinding binding, this.onChange})
+      : _binding = binding ?? TimezoneWatcherBinding() {
+    // Get current timezone and watch it for changes.
+    timezone
+      ..getTimezoneId().then((tz) {
+        _timezoneId = tz;
+        onChange();
+      })
+      ..watch(_binding.wrap(_TimezoneWatcherImpl(this)));
+  }
+
+  void dispose() {
+    timezone.ctrl.close();
+    _binding.close();
+  }
+
+  String get timezoneId => _timezoneId;
+  set timezoneId(String value) {
+    _timezoneId = value;
+    timezone.setTimezone(value);
+  }
+}
+
+class _TimezoneWatcherImpl extends TimezoneWatcher {
+  final _TimeZoneModel model;
+  _TimezoneWatcherImpl(this.model);
+  @override
+  Future<void> onTimezoneOffsetChange(String timezoneId) async {
+    model._timezoneId = timezoneId;
+    model.onChange();
+  }
+}
+
+class _Timezone {
+  /// The ICU standard zone ID.
+  final String zoneId;
+
+  const _Timezone({this.zoneId});
+}
+
+// Note: these timezones were generated from a script using ICU data.
+// These should ideally be loaded ad hoc or stored somewhere.
+const List<_Timezone> _kTimeZones = <_Timezone>[
+  _Timezone(zoneId: 'US/Eastern'),
+  _Timezone(zoneId: 'US/Pacific'),
+  _Timezone(zoneId: 'Europe/Paris'),
+  _Timezone(zoneId: 'Africa/Abidjan'),
+  _Timezone(zoneId: 'Africa/Accra'),
+  _Timezone(zoneId: 'Africa/Addis_Ababa'),
+  _Timezone(zoneId: 'Africa/Algiers'),
+  _Timezone(zoneId: 'Africa/Asmara'),
+  _Timezone(zoneId: 'Africa/Asmera'),
+  _Timezone(zoneId: 'Africa/Bamako'),
+  _Timezone(zoneId: 'Africa/Bangui'),
+  _Timezone(zoneId: 'Africa/Banjul'),
+  _Timezone(zoneId: 'Africa/Bissau'),
+  _Timezone(zoneId: 'Africa/Blantyre'),
+  _Timezone(zoneId: 'Africa/Brazzaville'),
+  _Timezone(zoneId: 'Africa/Bujumbura'),
+  _Timezone(zoneId: 'Africa/Cairo'),
+  _Timezone(zoneId: 'Africa/Casablanca'),
+  _Timezone(zoneId: 'Africa/Ceuta'),
+  _Timezone(zoneId: 'Africa/Conakry'),
+  _Timezone(zoneId: 'Africa/Dakar'),
+  _Timezone(zoneId: 'Africa/Dar_es_Salaam'),
+  _Timezone(zoneId: 'Africa/Djibouti'),
+  _Timezone(zoneId: 'Africa/Douala'),
+  _Timezone(zoneId: 'Africa/El_Aaiun'),
+  _Timezone(zoneId: 'Africa/Freetown'),
+  _Timezone(zoneId: 'Africa/Gaborone'),
+  _Timezone(zoneId: 'Africa/Harare'),
+  _Timezone(zoneId: 'Africa/Johannesburg'),
+  _Timezone(zoneId: 'Africa/Kampala'),
+  _Timezone(zoneId: 'Africa/Khartoum'),
+  _Timezone(zoneId: 'Africa/Kigali'),
+  _Timezone(zoneId: 'Africa/Kinshasa'),
+  _Timezone(zoneId: 'Africa/Lagos'),
+  _Timezone(zoneId: 'Africa/Libreville'),
+  _Timezone(zoneId: 'Africa/Lome'),
+  _Timezone(zoneId: 'Africa/Luanda'),
+  _Timezone(zoneId: 'Africa/Lubumbashi'),
+  _Timezone(zoneId: 'Africa/Lusaka'),
+  _Timezone(zoneId: 'Africa/Malabo'),
+  _Timezone(zoneId: 'Africa/Maputo'),
+  _Timezone(zoneId: 'Africa/Maseru'),
+  _Timezone(zoneId: 'Africa/Mbabane'),
+  _Timezone(zoneId: 'Africa/Mogadishu'),
+  _Timezone(zoneId: 'Africa/Monrovia'),
+  _Timezone(zoneId: 'Africa/Nairobi'),
+  _Timezone(zoneId: 'Africa/Ndjamena'),
+  _Timezone(zoneId: 'Africa/Niamey'),
+  _Timezone(zoneId: 'Africa/Nouakchott'),
+  _Timezone(zoneId: 'Africa/Ouagadougou'),
+  _Timezone(zoneId: 'Africa/Porto-Novo'),
+  _Timezone(zoneId: 'Africa/Sao_Tome'),
+  _Timezone(zoneId: 'Africa/Timbuktu'),
+  _Timezone(zoneId: 'Africa/Tripoli'),
+  _Timezone(zoneId: 'Africa/Tunis'),
+  _Timezone(zoneId: 'Africa/Windhoek'),
+  _Timezone(zoneId: 'America/Adak'),
+  _Timezone(zoneId: 'America/Anchorage'),
+  _Timezone(zoneId: 'America/Anguilla'),
+  _Timezone(zoneId: 'America/Antigua'),
+  _Timezone(zoneId: 'America/Araguaina'),
+  _Timezone(zoneId: 'America/Argentina/Buenos_Aires'),
+  _Timezone(zoneId: 'America/Argentina/Catamarca'),
+  _Timezone(zoneId: 'America/Argentina/ComodRivadavia'),
+  _Timezone(zoneId: 'America/Argentina/Cordoba'),
+  _Timezone(zoneId: 'America/Argentina/Jujuy'),
+  _Timezone(zoneId: 'America/Argentina/La_Rioja'),
+  _Timezone(zoneId: 'America/Argentina/Mendoza'),
+  _Timezone(zoneId: 'America/Argentina/Rio_Gallegos'),
+  _Timezone(zoneId: 'America/Argentina/San_Juan'),
+  _Timezone(zoneId: 'America/Argentina/Tucuman'),
+  _Timezone(zoneId: 'America/Argentina/Ushuaia'),
+  _Timezone(zoneId: 'America/Aruba'),
+  _Timezone(zoneId: 'America/Asuncion'),
+  _Timezone(zoneId: 'America/Atikokan'),
+  _Timezone(zoneId: 'America/Atka'),
+  _Timezone(zoneId: 'America/Bahia'),
+  _Timezone(zoneId: 'America/Barbados'),
+  _Timezone(zoneId: 'America/Belem'),
+  _Timezone(zoneId: 'America/Belize'),
+  _Timezone(zoneId: 'America/Blanc-Sablon'),
+  _Timezone(zoneId: 'America/Boa_Vista'),
+  _Timezone(zoneId: 'America/Bogota'),
+  _Timezone(zoneId: 'America/Boise'),
+  _Timezone(zoneId: 'America/Buenos_Aires'),
+  _Timezone(zoneId: 'America/Cambridge_Bay'),
+  _Timezone(zoneId: 'America/Campo_Grande'),
+  _Timezone(zoneId: 'America/Cancun'),
+  _Timezone(zoneId: 'America/Caracas'),
+  _Timezone(zoneId: 'America/Catamarca'),
+  _Timezone(zoneId: 'America/Cayenne'),
+  _Timezone(zoneId: 'America/Cayman'),
+  _Timezone(zoneId: 'America/Chicago'),
+  _Timezone(zoneId: 'America/Chihuahua'),
+  _Timezone(zoneId: 'America/Coral_Harbour'),
+  _Timezone(zoneId: 'America/Cordoba'),
+  _Timezone(zoneId: 'America/Costa_Rica'),
+  _Timezone(zoneId: 'America/Cuiaba'),
+  _Timezone(zoneId: 'America/Curacao'),
+  _Timezone(zoneId: 'America/Danmarkshavn'),
+  _Timezone(zoneId: 'America/Dawson'),
+  _Timezone(zoneId: 'America/Dawson_Creek'),
+  _Timezone(zoneId: 'America/Denver'),
+  _Timezone(zoneId: 'America/Detroit'),
+  _Timezone(zoneId: 'America/Dominica'),
+  _Timezone(zoneId: 'America/Edmonton'),
+  _Timezone(zoneId: 'America/Eirunepe'),
+  _Timezone(zoneId: 'America/El_Salvador'),
+  _Timezone(zoneId: 'America/Ensenada'),
+  _Timezone(zoneId: 'America/Fort_Wayne'),
+  _Timezone(zoneId: 'America/Fortaleza'),
+  _Timezone(zoneId: 'America/Glace_Bay'),
+  _Timezone(zoneId: 'America/Godthab'),
+  _Timezone(zoneId: 'America/Goose_Bay'),
+  _Timezone(zoneId: 'America/Grand_Turk'),
+  _Timezone(zoneId: 'America/Grenada'),
+  _Timezone(zoneId: 'America/Guadeloupe'),
+  _Timezone(zoneId: 'America/Guatemala'),
+  _Timezone(zoneId: 'America/Guayaquil'),
+  _Timezone(zoneId: 'America/Guyana'),
+  _Timezone(zoneId: 'America/Halifax'),
+  _Timezone(zoneId: 'America/Havana'),
+  _Timezone(zoneId: 'America/Hermosillo'),
+  _Timezone(zoneId: 'America/Indiana/Indianapolis'),
+  _Timezone(zoneId: 'America/Indiana/Knox'),
+  _Timezone(zoneId: 'America/Indiana/Marengo'),
+  _Timezone(zoneId: 'America/Indiana/Petersburg'),
+  _Timezone(zoneId: 'America/Indiana/Tell_City'),
+  _Timezone(zoneId: 'America/Indiana/Vevay'),
+  _Timezone(zoneId: 'America/Indiana/Vincennes'),
+  _Timezone(zoneId: 'America/Indiana/Winamac'),
+  _Timezone(zoneId: 'America/Indianapolis'),
+  _Timezone(zoneId: 'America/Inuvik'),
+  _Timezone(zoneId: 'America/Iqaluit'),
+  _Timezone(zoneId: 'America/Jamaica'),
+  _Timezone(zoneId: 'America/Jujuy'),
+  _Timezone(zoneId: 'America/Juneau'),
+  _Timezone(zoneId: 'America/Kentucky/Louisville'),
+  _Timezone(zoneId: 'America/Kentucky/Monticello'),
+  _Timezone(zoneId: 'America/Knox_IN'),
+  _Timezone(zoneId: 'America/La_Paz'),
+  _Timezone(zoneId: 'America/Lima'),
+  _Timezone(zoneId: 'America/Los_Angeles'),
+  _Timezone(zoneId: 'America/Louisville'),
+  _Timezone(zoneId: 'America/Maceio'),
+  _Timezone(zoneId: 'America/Managua'),
+  _Timezone(zoneId: 'America/Manaus'),
+  _Timezone(zoneId: 'America/Marigot'),
+  _Timezone(zoneId: 'America/Martinique'),
+  _Timezone(zoneId: 'America/Mazatlan'),
+  _Timezone(zoneId: 'America/Mendoza'),
+  _Timezone(zoneId: 'America/Menominee'),
+  _Timezone(zoneId: 'America/Merida'),
+  _Timezone(zoneId: 'America/Mexico_City'),
+  _Timezone(zoneId: 'America/Miquelon'),
+  _Timezone(zoneId: 'America/Moncton'),
+  _Timezone(zoneId: 'America/Monterrey'),
+  _Timezone(zoneId: 'America/Montevideo'),
+  _Timezone(zoneId: 'America/Montreal'),
+  _Timezone(zoneId: 'America/Montserrat'),
+  _Timezone(zoneId: 'America/Nassau'),
+  _Timezone(zoneId: 'America/New_York'),
+  _Timezone(zoneId: 'America/Nipigon'),
+  _Timezone(zoneId: 'America/Nome'),
+  _Timezone(zoneId: 'America/Noronha'),
+  _Timezone(zoneId: 'America/North_Dakota/Center'),
+  _Timezone(zoneId: 'America/North_Dakota/New_Salem'),
+  _Timezone(zoneId: 'America/Panama'),
+  _Timezone(zoneId: 'America/Pangnirtung'),
+  _Timezone(zoneId: 'America/Paramaribo'),
+  _Timezone(zoneId: 'America/Phoenix'),
+  _Timezone(zoneId: 'America/Port-au-Prince'),
+  _Timezone(zoneId: 'America/Port_of_Spain'),
+  _Timezone(zoneId: 'America/Porto_Acre'),
+  _Timezone(zoneId: 'America/Porto_Velho'),
+  _Timezone(zoneId: 'America/Puerto_Rico'),
+  _Timezone(zoneId: 'America/Rainy_River'),
+  _Timezone(zoneId: 'America/Rankin_Inlet'),
+  _Timezone(zoneId: 'America/Recife'),
+  _Timezone(zoneId: 'America/Regina'),
+  _Timezone(zoneId: 'America/Resolute'),
+  _Timezone(zoneId: 'America/Rio_Branco'),
+  _Timezone(zoneId: 'America/Rosario'),
+  _Timezone(zoneId: 'America/Santiago'),
+  _Timezone(zoneId: 'America/Santo_Domingo'),
+  _Timezone(zoneId: 'America/Sao_Paulo'),
+  _Timezone(zoneId: 'America/Scoresbysund'),
+  _Timezone(zoneId: 'America/Shiprock'),
+  _Timezone(zoneId: 'America/St_Barthelemy'),
+  _Timezone(zoneId: 'America/St_Johns'),
+  _Timezone(zoneId: 'America/St_Kitts'),
+  _Timezone(zoneId: 'America/St_Lucia'),
+  _Timezone(zoneId: 'America/St_Thomas'),
+  _Timezone(zoneId: 'America/St_Vincent'),
+  _Timezone(zoneId: 'America/Swift_Current'),
+  _Timezone(zoneId: 'America/Tegucigalpa'),
+  _Timezone(zoneId: 'America/Thule'),
+  _Timezone(zoneId: 'America/Thunder_Bay'),
+  _Timezone(zoneId: 'America/Tijuana'),
+  _Timezone(zoneId: 'America/Toronto'),
+  _Timezone(zoneId: 'America/Tortola'),
+  _Timezone(zoneId: 'America/Vancouver'),
+  _Timezone(zoneId: 'America/Virgin'),
+  _Timezone(zoneId: 'America/Whitehorse'),
+  _Timezone(zoneId: 'America/Winnipeg'),
+  _Timezone(zoneId: 'America/Yakutat'),
+  _Timezone(zoneId: 'America/Yellowknife'),
+  _Timezone(zoneId: 'Antarctica/Casey'),
+  _Timezone(zoneId: 'Antarctica/Davis'),
+  _Timezone(zoneId: 'Antarctica/DumontDUrville'),
+  _Timezone(zoneId: 'Antarctica/Mawson'),
+  _Timezone(zoneId: 'Antarctica/McMurdo'),
+  _Timezone(zoneId: 'Antarctica/Palmer'),
+  _Timezone(zoneId: 'Antarctica/Rothera'),
+  _Timezone(zoneId: 'Antarctica/South_Pole'),
+  _Timezone(zoneId: 'Antarctica/Syowa'),
+  _Timezone(zoneId: 'Antarctica/Vostok'),
+  _Timezone(zoneId: 'Arctic/Longyearbyen'),
+  _Timezone(zoneId: 'Asia/Aden'),
+  _Timezone(zoneId: 'Asia/Almaty'),
+  _Timezone(zoneId: 'Asia/Amman'),
+  _Timezone(zoneId: 'Asia/Anadyr'),
+  _Timezone(zoneId: 'Asia/Aqtau'),
+  _Timezone(zoneId: 'Asia/Aqtobe'),
+  _Timezone(zoneId: 'Asia/Ashgabat'),
+  _Timezone(zoneId: 'Asia/Ashkhabad'),
+  _Timezone(zoneId: 'Asia/Baghdad'),
+  _Timezone(zoneId: 'Asia/Bahrain'),
+  _Timezone(zoneId: 'Asia/Baku'),
+  _Timezone(zoneId: 'Asia/Bangkok'),
+  _Timezone(zoneId: 'Asia/Beirut'),
+  _Timezone(zoneId: 'Asia/Bishkek'),
+  _Timezone(zoneId: 'Asia/Brunei'),
+  _Timezone(zoneId: 'Asia/Calcutta'),
+  _Timezone(zoneId: 'Asia/Choibalsan'),
+  _Timezone(zoneId: 'Asia/Chongqing'),
+  _Timezone(zoneId: 'Asia/Chungking'),
+  _Timezone(zoneId: 'Asia/Colombo'),
+  _Timezone(zoneId: 'Asia/Dacca'),
+  _Timezone(zoneId: 'Asia/Damascus'),
+  _Timezone(zoneId: 'Asia/Dhaka'),
+  _Timezone(zoneId: 'Asia/Dili'),
+  _Timezone(zoneId: 'Asia/Dubai'),
+  _Timezone(zoneId: 'Asia/Dushanbe'),
+  _Timezone(zoneId: 'Asia/Gaza'),
+  _Timezone(zoneId: 'Asia/Harbin'),
+  _Timezone(zoneId: 'Asia/Hong_Kong'),
+  _Timezone(zoneId: 'Asia/Hovd'),
+  _Timezone(zoneId: 'Asia/Irkutsk'),
+  _Timezone(zoneId: 'Asia/Istanbul'),
+  _Timezone(zoneId: 'Asia/Jakarta'),
+  _Timezone(zoneId: 'Asia/Jayapura'),
+  _Timezone(zoneId: 'Asia/Jerusalem'),
+  _Timezone(zoneId: 'Asia/Kabul'),
+  _Timezone(zoneId: 'Asia/Kamchatka'),
+  _Timezone(zoneId: 'Asia/Karachi'),
+  _Timezone(zoneId: 'Asia/Kashgar'),
+  _Timezone(zoneId: 'Asia/Katmandu'),
+  _Timezone(zoneId: 'Asia/Krasnoyarsk'),
+  _Timezone(zoneId: 'Asia/Kuala_Lumpur'),
+  _Timezone(zoneId: 'Asia/Kuching'),
+  _Timezone(zoneId: 'Asia/Kuwait'),
+  _Timezone(zoneId: 'Asia/Macao'),
+  _Timezone(zoneId: 'Asia/Macau'),
+  _Timezone(zoneId: 'Asia/Magadan'),
+  _Timezone(zoneId: 'Asia/Makassar'),
+  _Timezone(zoneId: 'Asia/Manila'),
+  _Timezone(zoneId: 'Asia/Muscat'),
+  _Timezone(zoneId: 'Asia/Nicosia'),
+  _Timezone(zoneId: 'Asia/Novosibirsk'),
+  _Timezone(zoneId: 'Asia/Omsk'),
+  _Timezone(zoneId: 'Asia/Oral'),
+  _Timezone(zoneId: 'Asia/Phnom_Penh'),
+  _Timezone(zoneId: 'Asia/Pontianak'),
+  _Timezone(zoneId: 'Asia/Pyongyang'),
+  _Timezone(zoneId: 'Asia/Qatar'),
+  _Timezone(zoneId: 'Asia/Qyzylorda'),
+  _Timezone(zoneId: 'Asia/Rangoon'),
+  _Timezone(zoneId: 'Asia/Riyadh'),
+  _Timezone(zoneId: 'Asia/Riyadh87'),
+  _Timezone(zoneId: 'Asia/Riyadh88'),
+  _Timezone(zoneId: 'Asia/Riyadh89'),
+  _Timezone(zoneId: 'Asia/Saigon'),
+  _Timezone(zoneId: 'Asia/Sakhalin'),
+  _Timezone(zoneId: 'Asia/Samarkand'),
+  _Timezone(zoneId: 'Asia/Seoul'),
+  _Timezone(zoneId: 'Asia/Shanghai'),
+  _Timezone(zoneId: 'Asia/Singapore'),
+  _Timezone(zoneId: 'Asia/Taipei'),
+  _Timezone(zoneId: 'Asia/Tashkent'),
+  _Timezone(zoneId: 'Asia/Tbilisi'),
+  _Timezone(zoneId: 'Asia/Tehran'),
+  _Timezone(zoneId: 'Asia/Tel_Aviv'),
+  _Timezone(zoneId: 'Asia/Thimbu'),
+  _Timezone(zoneId: 'Asia/Thimphu'),
+  _Timezone(zoneId: 'Asia/Tokyo'),
+  _Timezone(zoneId: 'Asia/Ujung_Pandang'),
+  _Timezone(zoneId: 'Asia/Ulaanbaatar'),
+  _Timezone(zoneId: 'Asia/Ulan_Bator'),
+  _Timezone(zoneId: 'Asia/Urumqi'),
+  _Timezone(zoneId: 'Asia/Vientiane'),
+  _Timezone(zoneId: 'Asia/Vladivostok'),
+  _Timezone(zoneId: 'Asia/Yakutsk'),
+  _Timezone(zoneId: 'Asia/Yekaterinburg'),
+  _Timezone(zoneId: 'Asia/Yerevan'),
+  _Timezone(zoneId: 'Atlantic/Azores'),
+  _Timezone(zoneId: 'Atlantic/Bermuda'),
+  _Timezone(zoneId: 'Atlantic/Canary'),
+  _Timezone(zoneId: 'Atlantic/Cape_Verde'),
+  _Timezone(zoneId: 'Atlantic/Faeroe'),
+  _Timezone(zoneId: 'Atlantic/Faroe'),
+  _Timezone(zoneId: 'Atlantic/Jan_Mayen'),
+  _Timezone(zoneId: 'Atlantic/Madeira'),
+  _Timezone(zoneId: 'Atlantic/Reykjavik'),
+  _Timezone(zoneId: 'Atlantic/South_Georgia'),
+  _Timezone(zoneId: 'Atlantic/St_Helena'),
+  _Timezone(zoneId: 'Atlantic/Stanley'),
+  _Timezone(zoneId: 'Australia/ACT'),
+  _Timezone(zoneId: 'Australia/Adelaide'),
+  _Timezone(zoneId: 'Australia/Brisbane'),
+  _Timezone(zoneId: 'Australia/Broken_Hill'),
+  _Timezone(zoneId: 'Australia/Canberra'),
+  _Timezone(zoneId: 'Australia/Currie'),
+  _Timezone(zoneId: 'Australia/Darwin'),
+  _Timezone(zoneId: 'Australia/Eucla'),
+  _Timezone(zoneId: 'Australia/Hobart'),
+  _Timezone(zoneId: 'Australia/LHI'),
+  _Timezone(zoneId: 'Australia/Lindeman'),
+  _Timezone(zoneId: 'Australia/Lord_Howe'),
+  _Timezone(zoneId: 'Australia/Melbourne'),
+  _Timezone(zoneId: 'Australia/NSW'),
+  _Timezone(zoneId: 'Australia/North'),
+  _Timezone(zoneId: 'Australia/Perth'),
+  _Timezone(zoneId: 'Australia/Queensland'),
+  _Timezone(zoneId: 'Australia/South'),
+  _Timezone(zoneId: 'Australia/Sydney'),
+  _Timezone(zoneId: 'Australia/Tasmania'),
+  _Timezone(zoneId: 'Australia/Victoria'),
+  _Timezone(zoneId: 'Australia/West'),
+  _Timezone(zoneId: 'Australia/Yancowinna'),
+  _Timezone(zoneId: 'Brazil/Acre'),
+  _Timezone(zoneId: 'Brazil/DeNoronha'),
+  _Timezone(zoneId: 'Brazil/East'),
+  _Timezone(zoneId: 'Brazil/West'),
+  _Timezone(zoneId: 'CET'),
+  _Timezone(zoneId: 'CST6CDT'),
+  _Timezone(zoneId: 'Canada/Atlantic'),
+  _Timezone(zoneId: 'Canada/Central'),
+  _Timezone(zoneId: 'Canada/East-Saskatchewan'),
+  _Timezone(zoneId: 'Canada/Eastern'),
+  _Timezone(zoneId: 'Canada/Mountain'),
+  _Timezone(zoneId: 'Canada/Newfoundland'),
+  _Timezone(zoneId: 'Canada/Pacific'),
+  _Timezone(zoneId: 'Canada/Saskatchewan'),
+  _Timezone(zoneId: 'Canada/Yukon'),
+  _Timezone(zoneId: 'Chile/Continental'),
+  _Timezone(zoneId: 'Chile/EasterIsland'),
+  _Timezone(zoneId: 'Cuba'),
+  _Timezone(zoneId: 'EET'),
+  _Timezone(zoneId: 'EST'),
+  _Timezone(zoneId: 'EST5EDT'),
+  _Timezone(zoneId: 'Egypt'),
+  _Timezone(zoneId: 'Eire'),
+  _Timezone(zoneId: 'Etc/GMT'),
+  _Timezone(zoneId: 'Etc/GMT+0'),
+  _Timezone(zoneId: 'Etc/GMT+1'),
+  _Timezone(zoneId: 'Etc/GMT+10'),
+  _Timezone(zoneId: 'Etc/GMT+11'),
+  _Timezone(zoneId: 'Etc/GMT+12'),
+  _Timezone(zoneId: 'Etc/GMT+2'),
+  _Timezone(zoneId: 'Etc/GMT+3'),
+  _Timezone(zoneId: 'Etc/GMT+4'),
+  _Timezone(zoneId: 'Etc/GMT+5'),
+  _Timezone(zoneId: 'Etc/GMT+6'),
+  _Timezone(zoneId: 'Etc/GMT+7'),
+  _Timezone(zoneId: 'Etc/GMT+8'),
+  _Timezone(zoneId: 'Etc/GMT+9'),
+  _Timezone(zoneId: 'Etc/GMT-0'),
+  _Timezone(zoneId: 'Etc/GMT-1'),
+  _Timezone(zoneId: 'Etc/GMT-10'),
+  _Timezone(zoneId: 'Etc/GMT-11'),
+  _Timezone(zoneId: 'Etc/GMT-12'),
+  _Timezone(zoneId: 'Etc/GMT-13'),
+  _Timezone(zoneId: 'Etc/GMT-14'),
+  _Timezone(zoneId: 'Etc/GMT-2'),
+  _Timezone(zoneId: 'Etc/GMT-3'),
+  _Timezone(zoneId: 'Etc/GMT-4'),
+  _Timezone(zoneId: 'Etc/GMT-5'),
+  _Timezone(zoneId: 'Etc/GMT-6'),
+  _Timezone(zoneId: 'Etc/GMT-7'),
+  _Timezone(zoneId: 'Etc/GMT-8'),
+  _Timezone(zoneId: 'Etc/GMT-9'),
+  _Timezone(zoneId: 'Etc/GMT0'),
+  _Timezone(zoneId: 'Etc/Greenwich'),
+  _Timezone(zoneId: 'Etc/UCT'),
+  _Timezone(zoneId: 'Etc/UTC'),
+  _Timezone(zoneId: 'Etc/Universal'),
+  _Timezone(zoneId: 'Etc/Zulu'),
+  _Timezone(zoneId: 'Europe/Amsterdam'),
+  _Timezone(zoneId: 'Europe/Andorra'),
+  _Timezone(zoneId: 'Europe/Athens'),
+  _Timezone(zoneId: 'Europe/Belfast'),
+  _Timezone(zoneId: 'Europe/Belgrade'),
+  _Timezone(zoneId: 'Europe/Berlin'),
+  _Timezone(zoneId: 'Europe/Bratislava'),
+  _Timezone(zoneId: 'Europe/Brussels'),
+  _Timezone(zoneId: 'Europe/Bucharest'),
+  _Timezone(zoneId: 'Europe/Budapest'),
+  _Timezone(zoneId: 'Europe/Chisinau'),
+  _Timezone(zoneId: 'Europe/Copenhagen'),
+  _Timezone(zoneId: 'Europe/Dublin'),
+  _Timezone(zoneId: 'Europe/Gibraltar'),
+  _Timezone(zoneId: 'Europe/Guernsey'),
+  _Timezone(zoneId: 'Europe/Helsinki'),
+  _Timezone(zoneId: 'Europe/Isle_of_Man'),
+  _Timezone(zoneId: 'Europe/Istanbul'),
+  _Timezone(zoneId: 'Europe/Jersey'),
+  _Timezone(zoneId: 'Europe/Kaliningrad'),
+  _Timezone(zoneId: 'Europe/Kiev'),
+  _Timezone(zoneId: 'Europe/Lisbon'),
+  _Timezone(zoneId: 'Europe/Ljubljana'),
+  _Timezone(zoneId: 'Europe/London'),
+  _Timezone(zoneId: 'Europe/Luxembourg'),
+  _Timezone(zoneId: 'Europe/Madrid'),
+  _Timezone(zoneId: 'Europe/Malta'),
+  _Timezone(zoneId: 'Europe/Mariehamn'),
+  _Timezone(zoneId: 'Europe/Minsk'),
+  _Timezone(zoneId: 'Europe/Monaco'),
+  _Timezone(zoneId: 'Europe/Moscow'),
+  _Timezone(zoneId: 'Europe/Nicosia'),
+  _Timezone(zoneId: 'Europe/Oslo'),
+  _Timezone(zoneId: 'Europe/Podgorica'),
+  _Timezone(zoneId: 'Europe/Prague'),
+  _Timezone(zoneId: 'Europe/Riga'),
+  _Timezone(zoneId: 'Europe/Rome'),
+  _Timezone(zoneId: 'Europe/Samara'),
+  _Timezone(zoneId: 'Europe/San_Marino'),
+  _Timezone(zoneId: 'Europe/Sarajevo'),
+  _Timezone(zoneId: 'Europe/Simferopol'),
+  _Timezone(zoneId: 'Europe/Skopje'),
+  _Timezone(zoneId: 'Europe/Sofia'),
+  _Timezone(zoneId: 'Europe/Stockholm'),
+  _Timezone(zoneId: 'Europe/Tallinn'),
+  _Timezone(zoneId: 'Europe/Tirane'),
+  _Timezone(zoneId: 'Europe/Tiraspol'),
+  _Timezone(zoneId: 'Europe/Uzhgorod'),
+  _Timezone(zoneId: 'Europe/Vaduz'),
+  _Timezone(zoneId: 'Europe/Vatican'),
+  _Timezone(zoneId: 'Europe/Vienna'),
+  _Timezone(zoneId: 'Europe/Vilnius'),
+  _Timezone(zoneId: 'Europe/Volgograd'),
+  _Timezone(zoneId: 'Europe/Warsaw'),
+  _Timezone(zoneId: 'Europe/Zagreb'),
+  _Timezone(zoneId: 'Europe/Zaporozhye'),
+  _Timezone(zoneId: 'Europe/Zurich'),
+  _Timezone(zoneId: 'Factory'),
+  _Timezone(zoneId: 'GB'),
+  _Timezone(zoneId: 'GB-Eire'),
+  _Timezone(zoneId: 'GMT'),
+  _Timezone(zoneId: 'GMT+0'),
+  _Timezone(zoneId: 'GMT-0'),
+  _Timezone(zoneId: 'GMT0'),
+  _Timezone(zoneId: 'Greenwich'),
+  _Timezone(zoneId: 'HST'),
+  _Timezone(zoneId: 'Hongkong'),
+  _Timezone(zoneId: 'Iceland'),
+  _Timezone(zoneId: 'Indian/Antananarivo'),
+  _Timezone(zoneId: 'Indian/Chagos'),
+  _Timezone(zoneId: 'Indian/Christmas'),
+  _Timezone(zoneId: 'Indian/Cocos'),
+  _Timezone(zoneId: 'Indian/Comoro'),
+  _Timezone(zoneId: 'Indian/Kerguelen'),
+  _Timezone(zoneId: 'Indian/Mahe'),
+  _Timezone(zoneId: 'Indian/Maldives'),
+  _Timezone(zoneId: 'Indian/Mauritius'),
+  _Timezone(zoneId: 'Indian/Mayotte'),
+  _Timezone(zoneId: 'Indian/Reunion'),
+  _Timezone(zoneId: 'Iran'),
+  _Timezone(zoneId: 'Israel'),
+  _Timezone(zoneId: 'Jamaica'),
+  _Timezone(zoneId: 'Japan'),
+  _Timezone(zoneId: 'Kwajalein'),
+  _Timezone(zoneId: 'Libya'),
+  _Timezone(zoneId: 'MET'),
+  _Timezone(zoneId: 'MST'),
+  _Timezone(zoneId: 'MST7MDT'),
+  _Timezone(zoneId: 'Mexico/BajaNorte'),
+  _Timezone(zoneId: 'Mexico/BajaSur'),
+  _Timezone(zoneId: 'Mexico/General'),
+  _Timezone(zoneId: 'Mideast/Riyadh87'),
+  _Timezone(zoneId: 'Mideast/Riyadh88'),
+  _Timezone(zoneId: 'Mideast/Riyadh89'),
+  _Timezone(zoneId: 'NZ'),
+  _Timezone(zoneId: 'NZ-CHAT'),
+  _Timezone(zoneId: 'Navajo'),
+  _Timezone(zoneId: 'PRC'),
+  _Timezone(zoneId: 'PST8PDT'),
+  _Timezone(zoneId: 'Pacific/Apia'),
+  _Timezone(zoneId: 'Pacific/Auckland'),
+  _Timezone(zoneId: 'Pacific/Chatham'),
+  _Timezone(zoneId: 'Pacific/Easter'),
+  _Timezone(zoneId: 'Pacific/Efate'),
+  _Timezone(zoneId: 'Pacific/Enderbury'),
+  _Timezone(zoneId: 'Pacific/Fakaofo'),
+  _Timezone(zoneId: 'Pacific/Fiji'),
+  _Timezone(zoneId: 'Pacific/Funafuti'),
+  _Timezone(zoneId: 'Pacific/Galapagos'),
+  _Timezone(zoneId: 'Pacific/Gambier'),
+  _Timezone(zoneId: 'Pacific/Guadalcanal'),
+  _Timezone(zoneId: 'Pacific/Guam'),
+  _Timezone(zoneId: 'Pacific/Honolulu'),
+  _Timezone(zoneId: 'Pacific/Johnston'),
+  _Timezone(zoneId: 'Pacific/Kiritimati'),
+  _Timezone(zoneId: 'Pacific/Kosrae'),
+  _Timezone(zoneId: 'Pacific/Kwajalein'),
+  _Timezone(zoneId: 'Pacific/Majuro'),
+  _Timezone(zoneId: 'Pacific/Marquesas'),
+  _Timezone(zoneId: 'Pacific/Midway'),
+  _Timezone(zoneId: 'Pacific/Nauru'),
+  _Timezone(zoneId: 'Pacific/Niue'),
+  _Timezone(zoneId: 'Pacific/Norfolk'),
+  _Timezone(zoneId: 'Pacific/Noumea'),
+  _Timezone(zoneId: 'Pacific/Pago_Pago'),
+  _Timezone(zoneId: 'Pacific/Palau'),
+  _Timezone(zoneId: 'Pacific/Pitcairn'),
+  _Timezone(zoneId: 'Pacific/Ponape'),
+  _Timezone(zoneId: 'Pacific/Port_Moresby'),
+  _Timezone(zoneId: 'Pacific/Rarotonga'),
+  _Timezone(zoneId: 'Pacific/Saipan'),
+  _Timezone(zoneId: 'Pacific/Samoa'),
+  _Timezone(zoneId: 'Pacific/Tahiti'),
+  _Timezone(zoneId: 'Pacific/Tarawa'),
+  _Timezone(zoneId: 'Pacific/Tongatapu'),
+  _Timezone(zoneId: 'Pacific/Truk'),
+  _Timezone(zoneId: 'Pacific/Wake'),
+  _Timezone(zoneId: 'Pacific/Wallis'),
+  _Timezone(zoneId: 'Pacific/Yap'),
+  _Timezone(zoneId: 'Poland'),
+  _Timezone(zoneId: 'Portugal'),
+  _Timezone(zoneId: 'ROC'),
+  _Timezone(zoneId: 'ROK'),
+  _Timezone(zoneId: 'Singapore'),
+  _Timezone(zoneId: 'Turkey'),
+  _Timezone(zoneId: 'UCT'),
+  _Timezone(zoneId: 'US/Alaska'),
+  _Timezone(zoneId: 'US/Aleutian'),
+  _Timezone(zoneId: 'US/Arizona'),
+  _Timezone(zoneId: 'US/Central'),
+  _Timezone(zoneId: 'US/East-Indiana'),
+  _Timezone(zoneId: 'US/Hawaii'),
+  _Timezone(zoneId: 'US/Indiana-Starke'),
+  _Timezone(zoneId: 'US/Michigan'),
+  _Timezone(zoneId: 'US/Mountain'),
+  _Timezone(zoneId: 'US/Pacific'),
+  _Timezone(zoneId: 'US/Pacific-New'),
+  _Timezone(zoneId: 'US/Samoa'),
+  _Timezone(zoneId: 'UTC'),
+  _Timezone(zoneId: 'Universal'),
+  _Timezone(zoneId: 'W-SU'),
+  _Timezone(zoneId: 'WET'),
+  _Timezone(zoneId: 'Zulu'),
+];
diff --git a/session_shells/ermine/settings/lib/src/volume.dart b/session_shells/ermine/settings/lib/src/volume.dart
index 6d27563..d29ee64 100644
--- a/session_shells/ermine/settings/lib/src/volume.dart
+++ b/session_shells/ermine/settings/lib/src/volume.dart
@@ -59,7 +59,7 @@
 
   static Spec _specForVolume(double value) {
     String roundedVolume = (value * 100).round().toString();
-    return Spec(groups: [
+    return Spec(title: _title, groups: [
       Group(title: _title, values: [
         Value.withText(TextValue(text: roundedVolume)),
         Value.withProgress(
diff --git a/session_shells/ermine/settings/lib/src/weather.dart b/session_shells/ermine/settings/lib/src/weather.dart
index f5e7c39..9b26dff 100644
--- a/session_shells/ermine/settings/lib/src/weather.dart
+++ b/session_shells/ermine/settings/lib/src/weather.dart
@@ -21,8 +21,8 @@
 
   // 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'),
+    _Location('Mountain View', station: 'KSJC'),
+    _Location('San Francisco', station: 'KSFO'),
   ];
 
   // Weather refresh duration.
@@ -64,7 +64,7 @@
     if (locations.isEmpty) {
       return null;
     }
-    return Spec(groups: [
+    return Spec(title: _title, groups: [
       Group(title: _title, values: [
         if (tempInFahrenheit)
           Value.withButton(
@@ -78,7 +78,7 @@
               final location = locations[index ~/ 2];
               final temp =
                   tempInFahrenheit ? location.fahrenheit : location.degrees;
-              final weather = '$temp ${location.observation}';
+              final weather = '$temp / ${location.observation}';
               return TextValue(text: index.isEven ? location.name : weather);
             }))),
       ]),
@@ -86,29 +86,7 @@
   }
 
   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'];
-    }
+    await Future.forEach(_locations, _loadCurrentCondition);
   }
 
   // Get the latest observation for weather station in [_Location] using:
@@ -145,7 +123,7 @@
   String station;
   double tempInDegrees;
 
-  _Location(this.name, this.point, [this.observation]);
+  _Location(this.name, {this.point, this.station, this.observation});
 
   String get degrees => '${tempInDegrees.toInt()}°C';
 
diff --git a/session_shells/ermine/settings/test/datetime_test.dart b/session_shells/ermine/settings/test/datetime_test.dart
new file mode 100644
index 0000000..b1aa3b3
--- /dev/null
+++ b/session_shells/ermine/settings/test/datetime_test.dart
@@ -0,0 +1,23 @@
+// 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 'package:flutter_test/flutter_test.dart';
+import 'package:settings/settings.dart';
+
+void main() {
+  test('Datetime', () async {
+    final stopWatch = Stopwatch()..start();
+    final datetime = Datetime();
+    var spec = await datetime.getSpec();
+
+    expect(spec.title, isNotNull);
+    expect(spec.groups.first.values.first.text.text, isNotNull);
+
+    // Make sure the next update is received after [Datetime.refreshDuration].
+    spec = await datetime.getSpec();
+    stopWatch.stop();
+
+    expect(stopWatch.elapsed >= Datetime.refreshDuration, true);
+  });
+}
diff --git a/session_shells/ermine/settings/test/timezone_test.dart b/session_shells/ermine/settings/test/timezone_test.dart
new file mode 100644
index 0000000..8364e12
--- /dev/null
+++ b/session_shells/ermine/settings/test/timezone_test.dart
@@ -0,0 +1,46 @@
+// 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 'package:fidl_fuchsia_timezone/fidl_async.dart';
+import 'package:fidl_fuchsia_ui_remotewidgets/fidl_async.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/mockito.dart';
+import 'package:settings/settings.dart';
+
+void main() {
+  test('Timezone', () async {
+    final timezoneProxy = MockTimezoneProxy();
+    final binding = MockBinding();
+
+    when(timezoneProxy.getTimezoneId())
+        .thenAnswer((_) => Future<String>.value('Foo'));
+
+    TimeZone timezone = TimeZone(timezone: timezoneProxy, binding: binding);
+    final spec = await timezone.getSpec();
+    expect(spec.groups.first.values.first.text.text == 'Foo', true);
+  });
+
+  test('Change Timezone', () async {
+    final timezoneProxy = MockTimezoneProxy();
+    final binding = MockBinding();
+
+    when(timezoneProxy.getTimezoneId())
+        .thenAnswer((_) => Future<String>.value('Foo'));
+
+    TimeZone timezone = TimeZone(timezone: timezoneProxy, binding: binding);
+    await timezone.getSpec();
+
+    final spec = await timezone.getSpec(Value.withText(TextValue(
+      text: 'US/Eastern',
+      action: QuickAction.submit.$value,
+    )));
+
+    expect(spec.groups.first.values.first.text.text == 'US/Eastern', true);
+  });
+}
+
+// Mock classes.
+class MockTimezoneProxy extends Mock implements TimezoneProxy {}
+
+class MockBinding extends Mock implements TimezoneWatcherBinding {}
diff --git a/session_shells/ermine/shell/BUILD.gn b/session_shells/ermine/shell/BUILD.gn
index 6a56100..001acee 100644
--- a/session_shells/ermine/shell/BUILD.gn
+++ b/session_shells/ermine/shell/BUILD.gn
@@ -69,8 +69,12 @@
     "src/widgets/ask/ask_container.dart",
     "src/widgets/ask/ask_suggestion_list.dart",
     "src/widgets/ask/ask_text_field.dart",
+    "src/widgets/status/detail_status_entry.dart",
+    "src/widgets/status/spec_builder.dart",
     "src/widgets/status/status.dart",
+    "src/widgets/status/status_button.dart",
     "src/widgets/status/status_container.dart",
+    "src/widgets/status/status_entry.dart",
     "src/widgets/status/status_graph.dart",
     "src/widgets/status/status_progress.dart",
     "src/widgets/story/cluster.dart",
diff --git a/session_shells/ermine/shell/lib/src/models/app_model.dart b/session_shells/ermine/shell/lib/src/models/app_model.dart
index fe57a19..e7adecd 100644
--- a/session_shells/ermine/shell/lib/src/models/app_model.dart
+++ b/session_shells/ermine/shell/lib/src/models/app_model.dart
@@ -205,6 +205,7 @@
   /// Called when tapped behind Ask bar, quick settings, notifications or the
   /// Escape key was pressed.
   void onCancel() {
+    status.reset();
     askVisibility.value = false;
     statusVisibility.value = false;
     helpVisibility.value = false;
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 3e58666..8f11680 100644
--- a/session_shells/ermine/shell/lib/src/models/status_model.dart
+++ b/session_shells/ermine/shell/lib/src/models/status_model.dart
@@ -6,6 +6,7 @@
 
 import 'package:fidl_fuchsia_modular/fidl_async.dart' as modular;
 import 'package:fidl_fuchsia_device_manager/fidl_async.dart';
+import 'package:fidl_fuchsia_ui_remotewidgets/fidl_async.dart';
 import 'package:fuchsia_inspect/inspect.dart';
 import 'package:fuchsia_services/services.dart';
 import 'package:quickui/uistream.dart';
@@ -25,17 +26,22 @@
   UiStream weather;
   UiStream volume;
   UiStream bluetooth;
+  UiStream datetime;
+  UiStream timezone;
   final StartupContext startupContext;
   final modular.PuppetMasterProxy puppetMaster;
   final AdministratorProxy deviceManager;
+  final ValueNotifier<UiStream> detailNotifier = ValueNotifier<UiStream>(null);
 
   StatusModel({this.startupContext, this.puppetMaster, this.deviceManager}) {
+    datetime = UiStream(Datetime());
+    timezone = UiStream(TimeZone.fromStartupContext(startupContext));
     brightness = UiStream(Brightness.fromStartupContext(startupContext));
     memory = UiStream(Memory.fromStartupContext(startupContext));
     battery = UiStream(Battery.fromStartupContext(startupContext));
-    weather = UiStream(Weather());
     volume = UiStream(Volume.fromStartupContext(startupContext));
     bluetooth = UiStream(Bluetooth.fromStartupContext(startupContext));
+    weather = UiStream(Weather());
   }
 
   factory StatusModel.fromStartupContext(StartupContext startupContext) {
@@ -61,6 +67,19 @@
     weather.dispose();
     volume.dispose();
     battery.dispose();
+    datetime.dispose();
+    timezone.dispose();
+  }
+
+  UiStream get detailStream => detailNotifier.value;
+
+  void reset() {
+    // Send [QuickAction.cancel] to the detail stream if on detail view.
+    detailNotifier.value?.update(Value.withButton(ButtonValue(
+      label: '',
+      action: QuickAction.cancel.$value,
+    )));
+    detailNotifier.value = null;
   }
 
   /// Launch settings mod.
diff --git a/session_shells/ermine/shell/lib/src/widgets/status/detail_status_entry.dart b/session_shells/ermine/shell/lib/src/widgets/status/detail_status_entry.dart
new file mode 100644
index 0000000..818187b
--- /dev/null
+++ b/session_shells/ermine/shell/lib/src/widgets/status/detail_status_entry.dart
@@ -0,0 +1,84 @@
+// 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 'package:fidl_fuchsia_ui_remotewidgets/fidl_async.dart';
+import 'package:flutter/material.dart';
+
+import '../../models/status_model.dart';
+import '../../utils/styles.dart';
+import 'spec_builder.dart';
+
+/// Defines a widget that displays a status entry in the detail view of
+/// status shellement.
+class DetailStatusEntry extends StatelessWidget {
+  final StatusModel model;
+  final ValueChanged<Value> onChange;
+  final _lastSpec = ValueNotifier<Spec>(null);
+
+  DetailStatusEntry({this.model, this.onChange});
+
+  @override
+  Widget build(BuildContext context) {
+    return AnimatedBuilder(
+      animation: model.detailNotifier,
+      builder: (context, _) {
+        // Show Offstage if detail stream or last spec is not available.
+        if (_lastSpec.value == null && model.detailStream == null) {
+          return Offstage();
+        }
+
+        final uiStream = model.detailStream;
+        final spec = _lastSpec.value ?? uiStream.spec;
+        return Column(
+          crossAxisAlignment: CrossAxisAlignment.stretch,
+          children: <Widget>[
+            Container(
+              decoration: BoxDecoration(
+                border: Border(
+                  bottom: BorderSide(
+                    color: ErmineStyle.kOverlayBorderColor,
+                    width: ErmineStyle.kOverlayBorderWidth,
+                  ),
+                ),
+              ),
+              child: Row(
+                children: <Widget>[
+                  IconButton(
+                    icon: Icon(Icons.arrow_back),
+                    onPressed: () => onChange(Value.withButton(ButtonValue(
+                      label: '',
+                      action: QuickAction.cancel.$value,
+                    ))),
+                  ),
+                  Padding(padding: EdgeInsets.only(left: 8)),
+                  Expanded(
+                    child: Text(spec.title.toUpperCase()),
+                  ),
+                ],
+              ),
+            ),
+            Padding(padding: EdgeInsets.only(bottom: 8)),
+            Flexible(
+              child: SingleChildScrollView(
+                child: model.detailStream == null
+                    ? buildFromSpec(spec, onChange)
+                    : StreamBuilder<Spec>(
+                        stream: uiStream.stream,
+                        initialData: uiStream.spec,
+                        builder: (context, snapshot) {
+                          if (!snapshot.hasData) {
+                            return Offstage();
+                          }
+                          _lastSpec.value = snapshot.data;
+                          return buildFromSpec(_lastSpec.value, onChange);
+                        },
+                      ),
+              ),
+            ),
+          ],
+        );
+      },
+    );
+  }
+}
diff --git a/session_shells/ermine/shell/lib/src/widgets/status/spec_builder.dart b/session_shells/ermine/shell/lib/src/widgets/status/spec_builder.dart
new file mode 100644
index 0000000..53f5321
--- /dev/null
+++ b/session_shells/ermine/shell/lib/src/widgets/status/spec_builder.dart
@@ -0,0 +1,170 @@
+// 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 'package:fidl_fuchsia_ui_remotewidgets/fidl_async.dart';
+import 'package:flutter/material.dart';
+
+import 'status.dart';
+import 'status_button.dart';
+import 'status_graph.dart';
+import 'status_progress.dart';
+
+/// Returns a [Widget] built from the given [Spec].
+///
+/// The first row would include title [Text] and may be followed by widgets
+/// associated with the [Value] type. A [GridValue] widget is returned in its
+/// own row.
+Widget buildFromSpec(Spec spec, void Function(Value) update) {
+  // Split the values into lists separated by [GridValue].
+  List<Widget> result = <Widget>[];
+  for (final group in spec.groups) {
+    // Handle group with no values, but has a title.
+    if (group.values.isEmpty && group.title.isNotEmpty) {
+      result.add(_buildTitleRow(group.title, []));
+      continue;
+    }
+
+    // Split values in group by GridValue. Grid is on a row by itself.
+    final List<List<Value>> values = [];
+    for (final value in group.values) {
+      if (values.isEmpty ||
+          value.$tag == ValueTag.grid ||
+          values.last.last.$tag == ValueTag.grid) {
+        values.add([value]);
+      } else {
+        values.last.add(value);
+      }
+    }
+
+    for (final groupedValues in values) {
+      // Convert [Value] to [Widget]s.
+      final widgets =
+          groupedValues.map((value) => _buildFromValue(value, update)).toList();
+      // Create a title row for first set of values and value row for the rest.
+      if (groupedValues == values.first && group.title.isNotEmpty) {
+        // For grid, show title and grid in separate rows.
+        if (groupedValues.first.$tag == ValueTag.grid) {
+          result
+            ..add(_buildTitleRow(group.title, []))
+            ..add(_buildValueRow(widgets));
+        } else {
+          result.add(_buildTitleRow(group.title, widgets));
+        }
+      } else {
+        result.add(_buildValueRow(widgets));
+      }
+    }
+  }
+
+  return Container(
+    constraints: BoxConstraints(minHeight: kRowHeight),
+    child: Column(
+      crossAxisAlignment: CrossAxisAlignment.end,
+      children: result,
+    ),
+  );
+}
+
+Widget _buildFromValue(Value value, void Function(Value) update) {
+  if (value.$tag == ValueTag.button) {
+    return StatusButton(value.button.label, () => update(value));
+  }
+  if (value.$tag == ValueTag.text) {
+    final text = Text(value.text.text.toUpperCase());
+    return value.text.action > 0
+        ? GestureDetector(
+            onTap: () => update(value),
+            child: text,
+          )
+        : text;
+  }
+  if (value.$tag == ValueTag.progress) {
+    return SizedBox(
+      height: kItemHeight,
+      width: kProgressBarWidth,
+      child: ProgressBar(
+        value: value.progress.value,
+        onChange: (v) => update(Value.withProgress(
+            ProgressValue(value: v, action: value.progress.action))),
+      ),
+    );
+  }
+
+  if (value.$tag == ValueTag.grid) {
+    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.spaceEvenly,
+              children: List<Widget>.generate(value.grid.columns, (column) {
+                final index = row * value.grid.columns + column;
+                Value textValue = Value.withText(value.grid.values[index]);
+                final text = Text(
+                  textValue.text.text,
+                  textAlign: column == 0 ? TextAlign.start : TextAlign.end,
+                );
+                return Expanded(
+                  flex: column == 0 ? 3 : 2,
+                  child: textValue.text.action > 0
+                      ? GestureDetector(
+                          onTap: () => update(textValue),
+                          child: text,
+                        )
+                      : text,
+                );
+              }),
+            ),
+          );
+        }),
+      ),
+    );
+  }
+  if (value.$tag == ValueTag.icon) {
+    return GestureDetector(
+      child: Icon(
+        IconData(
+          value.icon.codePoint,
+          fontFamily: value.icon.fontFamily ?? 'MaterialIcons',
+        ),
+        size: kIconHeight,
+      ),
+      onTap: () => update(value),
+    );
+  }
+  if (value.$tag == ValueTag.graph) {
+    return QuickGraph(value: value.graph.value, step: value.graph.step);
+  }
+  return Offstage();
+}
+
+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(
+        padding: EdgeInsets.only(right: 32),
+        child: Text(title.toUpperCase()),
+      ),
+      Expanded(
+        child: _buildValueRow(children),
+      ),
+    ],
+  );
+}
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 35115f1..3144a3d 100644
--- a/session_shells/ermine/shell/lib/src/widgets/status/status.dart
+++ b/session_shells/ermine/shell/lib/src/widgets/status/status.dart
@@ -10,11 +10,12 @@
 import 'package:quickui/uistream.dart';
 
 import '../../models/status_model.dart';
-import 'status_graph.dart';
-import 'status_progress.dart';
+import '../../utils/styles.dart';
+import 'detail_status_entry.dart';
+import 'status_button.dart';
+import 'status_entry.dart';
 
 const kPadding = 12.0;
-const kTitleWidth = 100.0;
 const kRowHeight = 28.0;
 const kItemHeight = 16.0;
 const kIconHeight = 18.0;
@@ -36,195 +37,74 @@
 
   @override
   Widget build(BuildContext context) {
-    return SingleChildScrollView(
-      padding: EdgeInsets.all(kPadding),
-      child: Column(
-        children: <Widget>[
-          _ManualStatusEntry(model),
-          _StatusEntry(model.brightness),
-          _StatusEntry(model.volume),
-          _StatusEntry(model.battery),
-          _StatusEntry(model.memory),
-          _StatusEntry(model.weather),
-          _StatusEntry(model.bluetooth),
-        ],
-      ),
-    );
-  }
-}
+    // The [PageController] used to switch between main and detail views.
+    final pageController = PageController();
 
-class _StatusEntry extends StatelessWidget {
-  final UiStream uiStream;
+    // Returns the callback to handle [QuickAction] from buttons in spec
+    // available from the provided [uiStream].
+    ValueChanged<Value> _onChange(UiStream uiStream) {
+      return (Value value) {
+        // Check if a button with [QuickAction] was clicked.
+        final action = _actionFromValue(value);
+        if (action & QuickAction.details.$value > 0) {
+          model.detailNotifier.value = uiStream;
 
-  _StatusEntry(this.uiStream) {
-    uiStream.listen();
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return StreamBuilder<Spec>(
-      stream: uiStream.stream,
-      initialData: uiStream.spec,
-      builder: (context, snapshot) {
-        if (!snapshot.hasData) {
-          return Offstage();
-        }
-        final spec = snapshot.data;
-        final widgets = _buildFromSpec(spec, uiStream.update);
-        return Container(
-          constraints: BoxConstraints(minHeight: kRowHeight),
-          child: Column(
-            crossAxisAlignment: CrossAxisAlignment.end,
-            children: widgets,
-          ),
-        );
-      },
-    );
-  }
-
-// Returns a list of [Row] widgets from the given [Spec].
-// The first row would include title [Text] and may be followed by widgets
-// associated with the [Value] type. A [GridValue] widget is returned in its
-// own row.
-  List<Widget> _buildFromSpec(Spec spec, void Function(Value) update) {
-    // Split the values into lists separated by [GridValue].
-    List<Widget> result = <Widget>[];
-    List<Widget> widgets = <Widget>[];
-
-    for (final group in spec.groups) {
-      for (final value in group.values) {
-        final widget = _buildFromValue(value, update);
-        if (value.$tag == ValueTag.grid) {
-          if (result.isEmpty) {
-            result.add(_buildTitleRow(group.title, widgets.toList()));
-          } else {
-            result.add(_buildValueRow(widgets.toList()));
-          }
-          widgets.clear();
-        }
-        widgets.add(widget);
-      }
-
-      if (widgets.isNotEmpty) {
-        if (result.isEmpty) {
-          result.add(_buildTitleRow(group.title, widgets.toList()));
+          pageController.nextPage(
+            duration: ErmineStyle.kScreenAnimationDuration,
+            curve: ErmineStyle.kScreenAnimationCurve,
+          );
+          uiStream.update(value);
+        } else if (action & QuickAction.cancel.$value > 0 ||
+            action & QuickAction.submit.$value > 0) {
+          model.detailStream?.update(value);
+          pageController.previousPage(
+            duration: ErmineStyle.kScreenAnimationDuration,
+            curve: ErmineStyle.kScreenAnimationCurve,
+          );
+          model.detailNotifier.value = null;
         } else {
-          result.add(_buildValueRow(widgets.toList()));
+          uiStream?.update(value);
         }
-      }
-    }
-    return result;
-  }
-
-  Widget _buildFromValue(Value value, void Function(Value) update) {
-    if (value.$tag == ValueTag.button) {
-      return _buildButton(value.button.label, () => update(value));
-    }
-    if (value.$tag == ValueTag.text) {
-      return Text(value.text.text.toUpperCase());
-    }
-    if (value.$tag == ValueTag.progress) {
-      return SizedBox(
-        height: kItemHeight,
-        width: kProgressBarWidth,
-        child: ProgressBar(
-          value: value.progress.value,
-          onChange: (v) => update(Value.withProgress(
-              ProgressValue(value: v, action: value.progress.action))),
-        ),
-      );
+      };
     }
 
-    if (value.$tag == ValueTag.grid) {
-      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,
-                    ),
-                  );
-                }),
-              ),
-            );
-          }),
-        ),
-      );
-    }
-    if (value.$tag == ValueTag.icon) {
-      return GestureDetector(
-        child: Icon(
-          IconData(
-            value.icon.codePoint,
-            fontFamily: value.icon.fontFamily ?? 'MaterialIcons',
+    return PageView(
+      controller: pageController,
+      physics: NeverScrollableScrollPhysics(),
+      children: <Widget>[
+        SingleChildScrollView(
+          padding: EdgeInsets.all(kPadding),
+          child: Column(
+            children: <Widget>[
+              _ManualStatusEntry(model),
+              for (final uiStream in [
+                model.datetime,
+                model.timezone,
+                model.volume,
+                model.brightness,
+                model.battery,
+                model.memory,
+                model.bluetooth,
+                model.weather,
+              ])
+                StatusEntry(
+                  uiStream: uiStream,
+                  onChange: _onChange(uiStream),
+                  detailNotifier: model.detailNotifier,
+                  // getSpec: spec,
+                ),
+            ],
           ),
-          size: kIconHeight,
         ),
-        onTap: () => update(value),
-      );
-    }
-    if (value.$tag == ValueTag.graph) {
-      return QuickGraph(value: value.graph.value, step: value.graph.step);
-    }
-    return Offstage();
+        DetailStatusEntry(
+          model: model,
+          onChange: _onChange(null),
+        )
+      ],
+    );
   }
 }
 
-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;
 
@@ -237,13 +117,37 @@
       height: kRowHeight,
       child: Row(
         children: <Widget>[
-          _buildButton(Strings.restart, model.restartDevice),
+          StatusButton(Strings.restart, model.restartDevice),
           Padding(padding: EdgeInsets.only(right: kPadding)),
-          _buildButton(Strings.shutdown, model.shutdownDevice),
+          StatusButton(Strings.shutdown, model.shutdownDevice),
           Spacer(),
-          _buildButton(Strings.settings, model.launchSettings),
+          StatusButton(Strings.settings, model.launchSettings),
         ],
       ),
     );
   }
 }
+
+int _actionFromValue(Value value) {
+  switch (value.$tag) {
+    case ValueTag.button:
+      return value.button.action;
+    case ValueTag.number:
+      return value.number.action;
+    case ValueTag.text:
+      return value.text.action;
+    case ValueTag.progress:
+      return value.progress.action;
+    case ValueTag.input:
+      return value.input.action;
+    case ValueTag.icon:
+      return value.input.action;
+    case ValueTag.grid:
+      return 0;
+    case ValueTag.graph:
+      return value.graph.action;
+    case ValueTag.list:
+      return 0;
+  }
+  return 0;
+}
diff --git a/session_shells/ermine/shell/lib/src/widgets/status/status_button.dart b/session_shells/ermine/shell/lib/src/widgets/status/status_button.dart
new file mode 100644
index 0000000..8a6f50b
--- /dev/null
+++ b/session_shells/ermine/shell/lib/src/widgets/status/status_button.dart
@@ -0,0 +1,33 @@
+// 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 'package:flutter/material.dart';
+import 'status.dart';
+
+/// Defines a widget to render a Button for a status entry.
+class StatusButton extends StatelessWidget {
+  final String label;
+  final VoidCallback onTap;
+
+  const StatusButton(this.label, this.onTap);
+
+  @override
+  Widget build(BuildContext context) {
+    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,
+          ),
+        ),
+      ),
+    );
+  }
+}
diff --git a/session_shells/ermine/shell/lib/src/widgets/status/status_entry.dart b/session_shells/ermine/shell/lib/src/widgets/status/status_entry.dart
new file mode 100644
index 0000000..4738fe1
--- /dev/null
+++ b/session_shells/ermine/shell/lib/src/widgets/status/status_entry.dart
@@ -0,0 +1,45 @@
+// 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 'package:fidl_fuchsia_ui_remotewidgets/fidl_async.dart';
+import 'package:flutter/material.dart';
+import 'package:quickui/uistream.dart';
+
+import 'spec_builder.dart';
+
+/// Defines a widget to represent a status entry in the Status shellement.
+class StatusEntry extends StatelessWidget {
+  final UiStream uiStream;
+  final ValueChanged<Value> onChange;
+  final ValueNotifier<UiStream> detailNotifier;
+  final _lastSpec = ValueNotifier<Spec>(null);
+
+  StatusEntry({this.uiStream, this.onChange, this.detailNotifier}) {
+    uiStream.listen();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return AnimatedBuilder(
+      animation: detailNotifier,
+      builder: (context, child) {
+        // If a [DetailStatusEntry] is displaying for this stream, freeze the
+        // UI for this stream until its shown.
+        return detailNotifier.value == uiStream
+            ? buildFromSpec(_lastSpec.value, onChange)
+            : StreamBuilder<Spec>(
+                stream: uiStream.stream,
+                initialData: uiStream.spec,
+                builder: (context, snapshot) {
+                  if (!snapshot.hasData) {
+                    return Offstage();
+                  }
+                  _lastSpec.value = snapshot.data;
+                  return buildFromSpec(_lastSpec.value, onChange);
+                },
+              );
+      },
+    );
+  }
+}
diff --git a/session_shells/ermine/shell/meta/ermine.cmx b/session_shells/ermine/shell/meta/ermine.cmx
index a93b6e8..b3f234b 100644
--- a/session_shells/ermine/shell/meta/ermine.cmx
+++ b/session_shells/ermine/shell/meta/ermine.cmx
@@ -29,6 +29,8 @@
             "fuchsia.power.BatteryManager",
             "fuchsia.sys.Environment",
             "fuchsia.sys.Launcher",
+            "fuchsia.timezone.TimeService",
+            "fuchsia.timezone.Timezone",
             "fuchsia.ui.brightness.Control",
             "fuchsia.ui.input.ImeService",
             "fuchsia.ui.policy.Presenter",