[flutter_tool] Update analytics policy, send event on disable (#43217)

diff --git a/packages/flutter_tools/lib/runner.dart b/packages/flutter_tools/lib/runner.dart
index 9c4e9a9..a81493d 100644
--- a/packages/flutter_tools/lib/runner.dart
+++ b/packages/flutter_tools/lib/runner.dart
@@ -215,9 +215,8 @@
 }
 
 Future<int> _exit(int code) async {
-  if (flutterUsage.isFirstRun) {
-    flutterUsage.printWelcome();
-  }
+  // Prints the welcome message if needed.
+  flutterUsage.printWelcome();
 
   // Send any last analytics calls that are in progress without overly delaying
   // the tool's exit (we wait a maximum of 250ms).
diff --git a/packages/flutter_tools/lib/src/base/config.dart b/packages/flutter_tools/lib/src/base/config.dart
index e16ba3d..4526cef 100644
--- a/packages/flutter_tools/lib/src/base/config.dart
+++ b/packages/flutter_tools/lib/src/base/config.dart
@@ -5,11 +5,10 @@
 import '../convert.dart';
 import 'context.dart';
 import 'file_system.dart';
-import 'platform.dart';
 
 class Config {
   Config([File configFile]) {
-    _configFile = configFile ?? fs.file(fs.path.join(_userHomeDir(), '.flutter_settings'));
+    _configFile = configFile ?? fs.file(fs.path.join(userHomePath(), '.flutter_settings'));
     if (_configFile.existsSync()) {
       _values = json.decode(_configFile.readAsStringSync());
     }
@@ -44,8 +43,3 @@
     _configFile.writeAsStringSync(json);
   }
 }
-
-String _userHomeDir() {
-  final String envKey = platform.operatingSystem == 'windows' ? 'APPDATA' : 'HOME';
-  return platform.environment[envKey] ?? '.';
-}
diff --git a/packages/flutter_tools/lib/src/base/file_system.dart b/packages/flutter_tools/lib/src/base/file_system.dart
index 0e3d3fa..46f3456 100644
--- a/packages/flutter_tools/lib/src/base/file_system.dart
+++ b/packages/flutter_tools/lib/src/base/file_system.dart
@@ -185,3 +185,11 @@
   @override
   String toString() => 'File not found: $path';
 }
+
+/// Reads the process environment to find the current user's home directory.
+///
+/// If the searched environment variables are not set, '.' is returned instead.
+String userHomePath() {
+  final String envKey = platform.operatingSystem == 'windows' ? 'APPDATA' : 'HOME';
+  return platform.environment[envKey] ?? '.';
+}
diff --git a/packages/flutter_tools/lib/src/commands/config.dart b/packages/flutter_tools/lib/src/commands/config.dart
index c113603..46a73bd 100644
--- a/packages/flutter_tools/lib/src/commands/config.dart
+++ b/packages/flutter_tools/lib/src/commands/config.dart
@@ -117,6 +117,7 @@
     if (argResults.wasParsed('analytics')) {
       final bool value = argResults['analytics'];
       flutterUsage.enabled = value;
+      AnalyticsConfigEvent(enabled: value).send();
       printStatus('Analytics reporting ${value ? 'enabled' : 'disabled'}.');
     }
 
diff --git a/packages/flutter_tools/lib/src/commands/upgrade.dart b/packages/flutter_tools/lib/src/commands/upgrade.dart
index 71cc708..cf047e0 100644
--- a/packages/flutter_tools/lib/src/commands/upgrade.dart
+++ b/packages/flutter_tools/lib/src/commands/upgrade.dart
@@ -15,12 +15,14 @@
 import '../cache.dart';
 import '../dart/pub.dart';
 import '../globals.dart';
+import '../persistent_tool_state.dart';
 import '../runner/flutter_command.dart';
 import '../version.dart';
 import 'channel.dart';
 
 class UpgradeCommand extends FlutterCommand {
-  UpgradeCommand() {
+  UpgradeCommand([UpgradeCommandRunner commandRunner])
+    : _commandRunner = commandRunner ?? UpgradeCommandRunner() {
     argParser
       ..addFlag(
         'force',
@@ -32,10 +34,14 @@
         'continue',
         hide: true,
         negatable: false,
-        help: 'For the second half of the upgrade flow requiring the new version of Flutter. Should not be invoked manually, but re-entrantly by the standard upgrade command.',
+        help: 'For the second half of the upgrade flow requiring the new '
+              'version of Flutter. Should not be invoked manually, but '
+              're-entrantly by the standard upgrade command.',
       );
   }
 
+  final UpgradeCommandRunner _commandRunner;
+
   @override
   final String name = 'upgrade';
 
@@ -52,8 +58,7 @@
 
   @override
   Future<FlutterCommandResult> runCommand() async {
-    final UpgradeCommandRunner upgradeCommandRunner = UpgradeCommandRunner();
-    await upgradeCommandRunner.runCommand(
+    await _commandRunner.runCommand(
       argResults['force'],
       argResults['continue'],
       GitTagVersion.determine(),
@@ -141,9 +146,13 @@
   // This method should only be called if the upgrade command is invoked
   // re-entrantly with the `--continue` flag
   Future<void> runCommandSecondHalf(FlutterVersion flutterVersion) async {
+    // Make sure the welcome message re-display is delayed until the end.
+    persistentToolState.redisplayWelcomeMessage = false;
     await precacheArtifacts();
     await updatePackages(flutterVersion);
     await runDoctor();
+    // Force the welcome message to re-display following the upgrade.
+    persistentToolState.redisplayWelcomeMessage = true;
   }
 
   Future<bool> hasUncomittedChanges() async {
diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart
index e210afd..7a09986 100644
--- a/packages/flutter_tools/lib/src/context_runner.dart
+++ b/packages/flutter_tools/lib/src/context_runner.dart
@@ -48,6 +48,7 @@
 import 'macos/xcode.dart';
 import 'macos/xcode_validator.dart';
 import 'mdns_discovery.dart';
+import 'persistent_tool_state.dart';
 import 'reporting/reporting.dart';
 import 'run_hot.dart';
 import 'version.dart';
@@ -106,9 +107,10 @@
       MacOSWorkflow: () => const MacOSWorkflow(),
       MDnsObservatoryDiscovery: () => MDnsObservatoryDiscovery(),
       OperatingSystemUtils: () => OperatingSystemUtils(),
-      Pub: () => const Pub(),
+      PersistentToolState: () => PersistentToolState(),
       ProcessInfo: () => ProcessInfo(),
       ProcessUtils: () => ProcessUtils(),
+      Pub: () => const Pub(),
       Signals: () => Signals(),
       SimControl: () => SimControl(),
       Stdio: () => const Stdio(),
@@ -121,8 +123,8 @@
       WebWorkflow: () => const WebWorkflow(),
       WindowsWorkflow: () => const WindowsWorkflow(),
       Xcode: () => Xcode(),
-      XcodeValidator: () => const XcodeValidator(),
       XcodeProjectInterpreter: () => XcodeProjectInterpreter(),
+      XcodeValidator: () => const XcodeValidator(),
     },
   );
 }
diff --git a/packages/flutter_tools/lib/src/persistent_tool_state.dart b/packages/flutter_tools/lib/src/persistent_tool_state.dart
new file mode 100644
index 0000000..71ded3a
--- /dev/null
+++ b/packages/flutter_tools/lib/src/persistent_tool_state.dart
@@ -0,0 +1,41 @@
+// Copyright 2019 The Chromium 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 'base/config.dart';
+import 'base/context.dart';
+import 'base/file_system.dart';
+
+PersistentToolState get persistentToolState => PersistentToolState.instance;
+
+/// A class that represents global (non-project-specific) internal state that
+/// must persist across tool invocations.
+abstract class PersistentToolState {
+  factory PersistentToolState([File configFile]) =>
+    _DefaultPersistentToolState(configFile);
+
+  static PersistentToolState get instance => context.get<PersistentToolState>();
+
+  /// Whether the welcome message should be redisplayed.
+  ///
+  /// May give null if the value has not been set.
+  bool redisplayWelcomeMessage;
+}
+
+class _DefaultPersistentToolState implements PersistentToolState {
+  _DefaultPersistentToolState([File configFile]) :
+    _config = Config(configFile ?? fs.file(fs.path.join(userHomePath(), _kFileName)));
+
+  static const String _kFileName = '.flutter_tool_state';
+  static const String _kRedisplayWelcomeMessage = 'redisplay-welcome-message';
+
+  final Config _config;
+
+  @override
+  bool get redisplayWelcomeMessage => _config.getValue(_kRedisplayWelcomeMessage);
+
+  @override
+  set redisplayWelcomeMessage(bool value) {
+    _config.setValue(_kRedisplayWelcomeMessage, value);
+  }
+}
diff --git a/packages/flutter_tools/lib/src/reporting/events.dart b/packages/flutter_tools/lib/src/reporting/events.dart
index 4eac731..3ab033c 100644
--- a/packages/flutter_tools/lib/src/reporting/events.dart
+++ b/packages/flutter_tools/lib/src/reporting/events.dart
@@ -199,3 +199,15 @@
     }
   }
 }
+
+/// An event that reports on changes in the configuration of analytics.
+class AnalyticsConfigEvent extends UsageEvent {
+  AnalyticsConfigEvent({
+    /// Whether analytics reporting is being enabled (true) or disabled (false).
+    @required bool enabled,
+  }) : super(
+    'analytics',
+    'enabled',
+    label: enabled ? 'true' : 'false',
+  );
+}
diff --git a/packages/flutter_tools/lib/src/reporting/reporting.dart b/packages/flutter_tools/lib/src/reporting/reporting.dart
index 759f808..dd83b46 100644
--- a/packages/flutter_tools/lib/src/reporting/reporting.dart
+++ b/packages/flutter_tools/lib/src/reporting/reporting.dart
@@ -21,6 +21,7 @@
 import '../doctor.dart';
 import '../features.dart';
 import '../globals.dart';
+import '../persistent_tool_state.dart';
 import '../runner/flutter_command.dart';
 import '../version.dart';
 
diff --git a/packages/flutter_tools/lib/src/reporting/usage.dart b/packages/flutter_tools/lib/src/reporting/usage.dart
index 01695e4..e737439 100644
--- a/packages/flutter_tools/lib/src/reporting/usage.dart
+++ b/packages/flutter_tools/lib/src/reporting/usage.dart
@@ -323,35 +323,55 @@
     await _analytics.waitForLastPing(timeout: const Duration(milliseconds: 250));
   }
 
-  @override
-  void printWelcome() {
-    // This gets called if it's the first run by the selected command, if any,
-    // and on exit, in case there was no command.
-    if (_printedWelcome) {
-      return;
-    }
-    _printedWelcome = true;
-
+  void _printWelcome() {
     printStatus('');
     printStatus('''
   ╔════════════════════════════════════════════════════════════════════════════╗
   ║                 Welcome to Flutter! - https://flutter.dev                  ║
   ║                                                                            ║
-  ║ The Flutter tool anonymously reports feature usage statistics and crash    ║
-  ║ reports to Google in order to help Google contribute improvements to       ║
-  ║ Flutter over time.                                                         ║
+  ║ The Flutter tool uses Google Analytics to anonymously report feature usage ║
+  ║ statistics and basic crash reports. This data is used to help improve      ║
+  ║ Flutter tools over time.                                                   ║
+  ║                                                                            ║
+  ║ Flutter tool analytics are not sent on the very first run. To disable      ║
+  ║ reporting, type 'flutter config --no-analytics'. To display the current    ║
+  ║ setting, type 'flutter config'. If you opt out of analytics, an opt-out    ║
+  ║ event will be sent, and then no further information will be sent by the    ║
+  ║ Flutter tool.                                                              ║
+  ║                                                                            ║
+  ║ By downloading the Flutter SDK, you agree to the Google Terms of Service.  ║
+  ║ Note: The Google Privacy Policy describes how data is handled in this      ║
+  ║ service.                                                                   ║
+  ║                                                                            ║
+  ║ Moreover, Flutter includes the Dart SDK, which may send usage metrics and  ║
+  ║ crash reports to Google.                                                   ║
   ║                                                                            ║
   ║ Read about data we send with crash reports:                                ║
   ║ https://github.com/flutter/flutter/wiki/Flutter-CLI-crash-reporting        ║
   ║                                                                            ║
   ║ See Google's privacy policy:                                               ║
   ║ https://www.google.com/intl/en/policies/privacy/                           ║
-  ║                                                                            ║
-  ║ Use "flutter config --no-analytics" to disable analytics and crash         ║
-  ║ reporting.                                                                 ║
   ╚════════════════════════════════════════════════════════════════════════════╝
   ''', emphasis: true);
   }
+
+  @override
+  void printWelcome() {
+    // Only print once per run.
+    if (_printedWelcome) {
+      return;
+    }
+    if (// Display the welcome message if this is the first run of the tool.
+        isFirstRun ||
+        // Display the welcome message if we are not on master, and if the
+        // persistent tool state instructs that we should.
+        (!FlutterVersion.instance.isMaster &&
+        (persistentToolState.redisplayWelcomeMessage ?? true))) {
+      _printWelcome();
+      _printedWelcome = true;
+      persistentToolState.redisplayWelcomeMessage = false;
+    }
+  }
 }
 
 // An Analytics mock that logs to file. Unimplemented methods goes to stdout.
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index 00087a1..09e89ab 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -447,9 +447,8 @@
       name: 'command',
       overrides: <Type, Generator>{FlutterCommand: () => this},
       body: () async {
-        if (flutterUsage.isFirstRun) {
-          flutterUsage.printWelcome();
-        }
+        // Prints the welcome message if needed.
+        flutterUsage.printWelcome();
         final String commandPath = await usagePath;
         _registerSignalHandlers(commandPath, startTime);
         FlutterCommandResult commandResult;
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/config_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/config_test.dart
index ead9e5a..d5b79b7 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/config_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/config_test.dart
@@ -14,6 +14,7 @@
 import 'package:flutter_tools/src/build_info.dart';
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/commands/config.dart';
+import 'package:flutter_tools/src/reporting/reporting.dart';
 import 'package:flutter_tools/src/version.dart';
 import 'package:mockito/mockito.dart';
 
@@ -24,6 +25,7 @@
   MockAndroidStudio mockAndroidStudio;
   MockAndroidSdk mockAndroidSdk;
   MockFlutterVersion mockFlutterVersion;
+  MockUsage mockUsage;
 
   setUpAll(() {
     Cache.disableLocking();
@@ -33,8 +35,31 @@
     mockAndroidStudio = MockAndroidStudio();
     mockAndroidSdk = MockAndroidSdk();
     mockFlutterVersion = MockFlutterVersion();
+    mockUsage = MockUsage();
+
+    when(mockUsage.isFirstRun).thenReturn(false);
   });
 
+  void verifyNoAnalytics() {
+    verifyNever(mockUsage.sendCommand(
+      any,
+      parameters: anyNamed('parameters'),
+    ));
+    verifyNever(mockUsage.sendEvent(
+      any,
+      any,
+      label: anyNamed('label'),
+      value: anyNamed('value'),
+      parameters: anyNamed('parameters'),
+    ));
+    verifyNever(mockUsage.sendTiming(
+      any,
+      any,
+      any,
+      label: anyNamed('label'),
+    ));
+  }
+
   group('config', () {
     testUsingContext('machine flag', () async {
       final BufferLogger logger = context.get<Logger>();
@@ -50,9 +75,11 @@
 
       expect(jsonObject.containsKey('android-sdk'), true);
       expect(jsonObject['android-sdk'], isNotNull);
+      verifyNoAnalytics();
     }, overrides: <Type, Generator>{
       AndroidStudio: () => mockAndroidStudio,
       AndroidSdk: () => mockAndroidSdk,
+      Usage: () => mockUsage,
     });
 
     testUsingContext('Can set build-dir', () async {
@@ -65,6 +92,9 @@
       ]);
 
       expect(getBuildDirectory(), 'foo');
+      verifyNoAnalytics();
+    }, overrides: <Type, Generator>{
+      Usage: () => mockUsage,
     });
 
     testUsingContext('throws error on absolute path to build-dir', () async {
@@ -75,6 +105,9 @@
         'config',
         '--build-dir=/foo',
       ]), throwsA(isInstanceOf<ToolExit>()));
+      verifyNoAnalytics();
+    }, overrides: <Type, Generator>{
+      Usage: () => mockUsage,
     });
 
     testUsingContext('allows setting and removing feature flags', () async {
@@ -115,9 +148,11 @@
       expect(Config.instance.getValue('enable-linux-desktop'), false);
       expect(Config.instance.getValue('enable-windows-desktop'), false);
       expect(Config.instance.getValue('enable-macos-desktop'), false);
+      verifyNoAnalytics();
     }, overrides: <Type, Generator>{
       AndroidStudio: () => mockAndroidStudio,
       AndroidSdk: () => mockAndroidSdk,
+      Usage: () => mockUsage,
     });
 
     testUsingContext('displays which config settings are available on stable', () async {
@@ -142,10 +177,86 @@
       expect(logger.statusText, contains('enable-linux-desktop: true (Unavailable)'));
       expect(logger.statusText, contains('enable-windows-desktop: true (Unavailable)'));
       expect(logger.statusText, contains('enable-macos-desktop: true (Unavailable)'));
+      verifyNoAnalytics();
     }, overrides: <Type, Generator>{
       AndroidStudio: () => mockAndroidStudio,
       AndroidSdk: () => mockAndroidSdk,
       FlutterVersion: () => mockFlutterVersion,
+      Usage: () => mockUsage,
+    });
+
+    testUsingContext('no-analytics flag flips usage flag and sends event', () async {
+      final ConfigCommand configCommand = ConfigCommand();
+      final CommandRunner<void> commandRunner = createTestCommandRunner(configCommand);
+
+      await commandRunner.run(<String>[
+        'config',
+        '--no-analytics',
+      ]);
+
+      expect(mockUsage.enabled, false);
+
+      // Verify that we only send the analytics disable event, and no other
+      // info.
+      verifyNever(mockUsage.sendCommand(
+        any,
+        parameters: anyNamed('parameters'),
+      ));
+      verifyNever(mockUsage.sendTiming(
+        any,
+        any,
+        any,
+        label: anyNamed('label'),
+      ));
+
+      expect(verify(mockUsage.sendEvent(
+        captureAny,
+        captureAny,
+        label: captureAnyNamed('label'),
+        value: anyNamed('value'),
+        parameters: anyNamed('parameters'),
+      )).captured,
+        <dynamic>['analytics', 'enabled', 'false'],
+      );
+    }, overrides: <Type, Generator>{
+      Usage: () => mockUsage,
+    });
+
+    testUsingContext('analytics flag flips usage flag and sends event', () async {
+      final ConfigCommand configCommand = ConfigCommand();
+      final CommandRunner<void> commandRunner = createTestCommandRunner(configCommand);
+
+      await commandRunner.run(<String>[
+        'config',
+        '--analytics',
+      ]);
+
+      expect(mockUsage.enabled, true);
+
+      // Verify that we only send the analytics disable event, and no other
+      // info.
+      verifyNever(mockUsage.sendCommand(
+        any,
+        parameters: anyNamed('parameters'),
+      ));
+      verifyNever(mockUsage.sendTiming(
+        any,
+        any,
+        any,
+        label: anyNamed('label'),
+      ));
+
+      expect(verify(mockUsage.sendEvent(
+        captureAny,
+        captureAny,
+        label: captureAnyNamed('label'),
+        value: anyNamed('value'),
+        parameters: anyNamed('parameters'),
+      )).captured,
+        <dynamic>['analytics', 'enabled', 'true'],
+      );
+    }, overrides: <Type, Generator>{
+      Usage: () => mockUsage,
     });
   });
 }
@@ -161,3 +272,8 @@
 }
 
 class MockFlutterVersion extends Mock implements FlutterVersion {}
+
+class MockUsage extends Mock implements Usage {
+  @override
+  bool enabled = true;
+}
diff --git a/packages/flutter_tools/test/commands.shard/permeable/upgrade_test.dart b/packages/flutter_tools/test/commands.shard/permeable/upgrade_test.dart
index 49cc3b7..d77d984 100644
--- a/packages/flutter_tools/test/commands.shard/permeable/upgrade_test.dart
+++ b/packages/flutter_tools/test/commands.shard/permeable/upgrade_test.dart
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'package:flutter_tools/runner.dart' as runner;
 import 'package:flutter_tools/src/base/common.dart';
 import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/base/io.dart';
@@ -9,6 +10,7 @@
 import 'package:flutter_tools/src/base/os.dart';
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/commands/upgrade.dart';
+import 'package:flutter_tools/src/persistent_tool_state.dart';
 import 'package:flutter_tools/src/runner/flutter_command.dart';
 import 'package:flutter_tools/src/version.dart';
 import 'package:mockito/mockito.dart';
@@ -17,6 +19,7 @@
 
 import '../../src/common.dart';
 import '../../src/context.dart';
+import '../../src/fake_process_manager.dart';
 import '../../src/mocks.dart';
 
 void main() {
@@ -184,6 +187,49 @@
       ProcessManager: () => processManager,
       Platform: () => fakePlatform,
     });
+
+    group('full command', () {
+      final FakeProcessManager fakeProcessManager = FakeProcessManager.list(<FakeCommand>[
+        const FakeCommand(command: <String>[
+          'git', 'describe', '--match', 'v*.*.*', '--first-parent', '--long', '--tags',
+        ]),
+      ]);
+
+      Directory tempDir;
+      File flutterToolState;
+
+      FlutterVersion mockFlutterVersion;
+
+      setUp(() {
+        Cache.disableLocking();
+        tempDir = fs.systemTempDirectory.createTempSync('flutter_upgrade_test.');
+        flutterToolState = tempDir.childFile('.flutter_tool_state');
+        mockFlutterVersion = MockFlutterVersion(isStable: true);
+      });
+
+      tearDown(() {
+        Cache.enableLocking();
+        tryToDelete(tempDir);
+      });
+
+      testUsingContext('upgrade continue prints welcome message', () async {
+        final UpgradeCommand upgradeCommand = UpgradeCommand(fakeCommandRunner);
+        await runner.run(
+          <String>[
+            'upgrade',
+            '--continue',
+          ],
+          <FlutterCommand>[
+            upgradeCommand,
+          ],
+        );
+        expect(testLogger.statusText, contains('Welcome to Flutter!'));
+      }, overrides: <Type, Generator>{
+        FlutterVersion: () => mockFlutterVersion,
+        ProcessManager: () => fakeProcessManager,
+        PersistentToolState: () => PersistentToolState(flutterToolState),
+      });
+    });
   });
 
   group('matchesGitLine', () {
@@ -267,7 +313,6 @@
   Future<void> runDoctor() async {}
 }
 
-class MockFlutterVersion extends Mock implements FlutterVersion {}
 class MockProcess extends Mock implements Process {}
 class MockProcessManager extends Mock implements ProcessManager {}
 class FakeProcessResult implements ProcessResult {
diff --git a/packages/flutter_tools/test/general.shard/persistent_tool_state_test.dart b/packages/flutter_tools/test/general.shard/persistent_tool_state_test.dart
new file mode 100644
index 0000000..5860db1
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/persistent_tool_state_test.dart
@@ -0,0 +1,31 @@
+// Copyright 2019 The Chromium 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_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/persistent_tool_state.dart';
+
+import '../src/common.dart';
+import '../src/testbed.dart';
+
+void main() {
+  Testbed testbed;
+
+  setUp(() {
+    testbed = Testbed();
+  });
+
+  test('state can be set and persists', () => testbed.run(() {
+    final File stateFile = fs.file('.flutter_tool_state');
+    final PersistentToolState state1 = PersistentToolState(stateFile);
+    expect(state1.redisplayWelcomeMessage, null);
+    state1.redisplayWelcomeMessage = true;
+    expect(stateFile.existsSync(), true);
+    expect(state1.redisplayWelcomeMessage, true);
+    state1.redisplayWelcomeMessage = false;
+    expect(state1.redisplayWelcomeMessage, false);
+
+    final PersistentToolState state2 = PersistentToolState(stateFile);
+    expect(state2.redisplayWelcomeMessage, false);
+  }));
+}
diff --git a/packages/flutter_tools/test/src/context.dart b/packages/flutter_tools/test/src/context.dart
index 7f1fdff..116df60 100644
--- a/packages/flutter_tools/test/src/context.dart
+++ b/packages/flutter_tools/test/src/context.dart
@@ -23,6 +23,7 @@
 import 'package:flutter_tools/src/ios/plist_parser.dart';
 import 'package:flutter_tools/src/ios/simulators.dart';
 import 'package:flutter_tools/src/ios/xcodeproj.dart';
+import 'package:flutter_tools/src/persistent_tool_state.dart';
 import 'package:flutter_tools/src/project.dart';
 import 'package:flutter_tools/src/reporting/reporting.dart';
 import 'package:flutter_tools/src/version.dart';
@@ -71,12 +72,18 @@
     }
   });
   Config buildConfig(FileSystem fs) {
-    configDir = fs.systemTempDirectory.createTempSync('flutter_config_dir_test.');
+    configDir ??= fs.systemTempDirectory.createTempSync('flutter_config_dir_test.');
     final File settingsFile = fs.file(
       fs.path.join(configDir.path, '.flutter_settings')
     );
     return Config(settingsFile);
   }
+  PersistentToolState buildPersistentToolState(FileSystem fs) {
+    configDir ??= fs.systemTempDirectory.createTempSync('flutter_config_dir_test.');
+    final File toolStateFile = fs.file(
+      fs.path.join(configDir.path, '.flutter_tool_state'));
+    return PersistentToolState(toolStateFile);
+  }
 
   test(description, () async {
     await runInContext<dynamic>(() {
@@ -96,6 +103,7 @@
           OutputPreferences: () => OutputPreferences.test(),
           Logger: () => BufferLogger(),
           OperatingSystemUtils: () => FakeOperatingSystemUtils(),
+          PersistentToolState: () => buildPersistentToolState(fs),
           SimControl: () => MockSimControl(),
           Usage: () => FakeUsage(),
           XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(),
diff --git a/packages/flutter_tools/test/src/testbed.dart b/packages/flutter_tools/test/src/testbed.dart
index c192e68..98126a8 100644
--- a/packages/flutter_tools/test/src/testbed.dart
+++ b/packages/flutter_tools/test/src/testbed.dart
@@ -64,7 +64,7 @@
 ///           });
 ///         })
 ///
-///         test('Can delete a file', () => testBed.run(() {
+///         test('Can delete a file', () => testbed.run(() {
 ///           expect(fs.file('foo').existsSync(), true);
 ///           fs.file('foo').deleteSync();
 ///           expect(fs.file('foo').existsSync(), false);