expose additional fields on Analytics
diff --git a/.analysis_options b/.analysis_options
index a10d4c5..545d5cc 100644
--- a/.analysis_options
+++ b/.analysis_options
@@ -1,2 +1,7 @@
 analyzer:
   strong-mode: true
+linter:
+  rules:
+    #- annotate_overrides
+    - empty_constructor_bodies
+    - empty_statements
diff --git a/changelog.md b/changelog.md
index a516440..2a74305 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,7 +1,11 @@
 # Changelog
 
-## 3.1.1
+## 3.2.0
+- expose the `Analytics.applicationName` and `Analytics.applicationVersion`
+  properties
+- make it easier for clients to extend the `AnalyticsIO` class
 
+## 3.1.1
 - make Analytics.clientId available immediately
 
 ## 3.1.0
diff --git a/lib/src/usage_impl.dart b/lib/src/usage_impl.dart
index cb92972..27cee22 100644
--- a/lib/src/usage_impl.dart
+++ b/lib/src/usage_impl.dart
@@ -60,10 +60,9 @@
   static const String _defaultAnalyticsUrl =
       'https://www.google-analytics.com/collect';
 
-  /**
-   * Tracking ID / Property ID.
-   */
   final String trackingId;
+  final String applicationName;
+  final String applicationVersion;
 
   final PersistentProperties properties;
   final PostHandler postHandler;
@@ -81,9 +80,7 @@
       new StreamController.broadcast(sync: true);
 
   AnalyticsImpl(this.trackingId, this.properties, this.postHandler,
-      {String applicationName,
-      String applicationVersion,
-      String analyticsUrl}) {
+      {this.applicationName, this.applicationVersion, String analyticsUrl}) {
     assert(trackingId != null);
 
     if (applicationName != null) setSessionValue('an', applicationName);
@@ -259,6 +256,10 @@
 
   dynamic operator [](String key);
   void operator []=(String key, dynamic value);
+
+  /// Re-read settings from the backing store. This may be a no-op on some
+  /// platforms.
+  void syncSettings();
 }
 
 /**
diff --git a/lib/src/usage_impl_html.dart b/lib/src/usage_impl_html.dart
index 7752fe5..6861db7 100644
--- a/lib/src/usage_impl_html.dart
+++ b/lib/src/usage_impl_html.dart
@@ -70,4 +70,6 @@
 
     window.localStorage[name] = JSON.encode(_map);
   }
+
+  void syncSettings() {}
 }
diff --git a/lib/src/usage_impl_io.dart b/lib/src/usage_impl_io.dart
index 57ac8ec..ad87e91 100644
--- a/lib/src/usage_impl_io.dart
+++ b/lib/src/usage_impl_io.dart
@@ -3,7 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'dart:async';
-import 'dart:convert' show JSON;
+import 'dart:convert' show JSON, JsonEncoder;
 import 'dart:io';
 
 import 'package:path/path.dart' as path;
@@ -60,7 +60,7 @@
   }
 }
 
-String _userHomeDir() {
+String userHomeDir() {
   String envKey = Platform.operatingSystem == 'windows' ? 'APPDATA' : 'HOME';
   String value = Platform.environment[envKey];
   return value == null ? '.' : value;
@@ -96,23 +96,28 @@
   }
 }
 
+JsonEncoder _jsonEncoder = new JsonEncoder.withIndent('  ');
+
 class IOPersistentProperties extends PersistentProperties {
   File _file;
   Map _map;
 
   IOPersistentProperties(String name, {String documentDirPath}) : super(name) {
     String fileName = '.${name.replaceAll(' ', '_')}';
-    documentDirPath ??= _userHomeDir();
+    documentDirPath ??= userHomeDir();
     _file = new File(path.join(documentDirPath, fileName));
-
-    try {
-      if (!_file.existsSync()) _file.createSync();
-      String contents = _file.readAsStringSync();
-      if (contents.isEmpty) contents = '{}';
-      _map = JSON.decode(contents);
-    } catch (_) {
-      _map = {};
+    if (!_file.existsSync()) {
+      _file.createSync();
     }
+    syncSettings();
+  }
+
+  IOPersistentProperties.fromFile(File file) : super(path.basename(file.path)) {
+    _file = file;
+    if (!_file.existsSync()) {
+      _file.createSync();
+    }
+    syncSettings();
   }
 
   dynamic operator [](String key) => _map[key];
@@ -128,9 +133,19 @@
     }
 
     try {
-      _file.writeAsStringSync(JSON.encode(_map) + '\n');
+      _file.writeAsStringSync(_jsonEncoder.convert(_map) + '\n');
     } catch (_) {}
   }
+
+  void syncSettings() {
+    try {
+      String contents = _file.readAsStringSync();
+      if (contents.isEmpty) contents = '{}';
+      _map = JSON.decode(contents);
+    } catch (_) {
+      _map = {};
+    }
+  }
 }
 
 /// Return the string for the platform's locale; return's `null` if the locale
diff --git a/lib/usage.dart b/lib/usage.dart
index dfdc54b..d60e372 100644
--- a/lib/usage.dart
+++ b/lib/usage.dart
@@ -47,6 +47,12 @@
    */
   String get trackingId;
 
+  /// The application name.
+  String get applicationName;
+
+  /// The application version.
+  String get applicationVersion;
+
   /**
    * Is this the first time the tool has run?
    */
@@ -213,6 +219,9 @@
  */
 class AnalyticsMock implements Analytics {
   String get trackingId => 'UA-0';
+  String get applicationName => 'mock-app';
+  String get applicationVersion => '1.0.0';
+
   final bool logCalls;
 
   /**
diff --git a/test/src/common.dart b/test/src/common.dart
index d18b155..4ce7b17 100644
--- a/test/src/common.dart
+++ b/test/src/common.dart
@@ -39,6 +39,8 @@
   void operator []=(String key, dynamic value) {
     props[key] = value;
   }
+
+  void syncSettings() {}
 }
 
 class MockPostHandler extends PostHandler {
diff --git a/test/usage_impl_test.dart b/test/usage_impl_test.dart
index 1952237..a4ca3ac 100644
--- a/test/usage_impl_test.dart
+++ b/test/usage_impl_test.dart
@@ -28,6 +28,21 @@
   });
 
   group('AnalyticsImpl', () {
+    test('trackingId', () {
+      AnalyticsImplMock mock = createMock();
+      expect(mock.trackingId, isNotNull);
+    });
+
+    test('applicationName', () {
+      AnalyticsImplMock mock = createMock();
+      expect(mock.applicationName, isNotNull);
+    });
+
+    test('applicationVersion', () {
+      AnalyticsImplMock mock = createMock();
+      expect(mock.applicationVersion, isNotNull);
+    });
+
     test('respects disabled', () {
       AnalyticsImplMock mock = createMock();
       mock.enabled = false;