[flutter_tools] delegate first run message re-display to new class, only if changed (#73353)
diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart
index f220d7b..02ca1a7 100644
--- a/packages/flutter_tools/lib/src/context_runner.dart
+++ b/packages/flutter_tools/lib/src/context_runner.dart
@@ -46,6 +46,7 @@
import 'macos/xcode.dart';
import 'mdns_discovery.dart';
import 'persistent_tool_state.dart';
+import 'reporting/first_run.dart';
import 'reporting/reporting.dart';
import 'resident_runner.dart';
import 'run_hot.dart';
@@ -278,6 +279,7 @@
SystemClock: () => const SystemClock(),
Usage: () => Usage(
runningOnBot: runningOnBot,
+ firstRunMessenger: FirstRunMessenger(persistentToolState: globals.persistentToolState),
),
UserMessages: () => UserMessages(),
VisualStudioValidator: () => VisualStudioValidator(
diff --git a/packages/flutter_tools/lib/src/persistent_tool_state.dart b/packages/flutter_tools/lib/src/persistent_tool_state.dart
index d596390..2fc1699 100644
--- a/packages/flutter_tools/lib/src/persistent_tool_state.dart
+++ b/packages/flutter_tools/lib/src/persistent_tool_state.dart
@@ -47,6 +47,9 @@
/// Update the last active version for a given [channel].
void updateLastActiveVersion(String fullGitHash, Channel channel);
+ /// Return the hash of the last active license terms.
+ String lastActiveLicenseTerms;
+
/// Whether this client was already determined to be or not be a bot.
bool isRunningOnBot;
}
@@ -82,6 +85,7 @@
Channel.stable: 'last-active-stable-version'
};
static const String _kBotKey = 'is-bot';
+ static const String _kLicenseHash = 'license-hash';
final Config _config;
@@ -109,6 +113,15 @@
_config.setValue(versionKey, fullGitHash);
}
+ @override
+ String get lastActiveLicenseTerms => _config.getValue(_kLicenseHash) as String;
+
+ @override
+ set lastActiveLicenseTerms(String value) {
+ assert(value != null);
+ _config.setValue(_kLicenseHash, value);
+ }
+
String _versionKeyFor(Channel channel) {
return _lastActiveVersionKeys[channel];
}
diff --git a/packages/flutter_tools/lib/src/reporting/disabled_usage.dart b/packages/flutter_tools/lib/src/reporting/disabled_usage.dart
index b908bea..b77303b 100644
--- a/packages/flutter_tools/lib/src/reporting/disabled_usage.dart
+++ b/packages/flutter_tools/lib/src/reporting/disabled_usage.dart
@@ -6,9 +6,6 @@
class DisabledUsage implements Usage {
@override
- bool get isFirstRun => false;
-
- @override
bool get suppressAnalytics => true;
@override
diff --git a/packages/flutter_tools/lib/src/reporting/first_run.dart b/packages/flutter_tools/lib/src/reporting/first_run.dart
new file mode 100644
index 0000000..49acb1e
--- /dev/null
+++ b/packages/flutter_tools/lib/src/reporting/first_run.dart
@@ -0,0 +1,79 @@
+// Copyright 2014 The Flutter 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:convert/convert.dart';
+import 'package:crypto/crypto.dart';
+import 'package:meta/meta.dart';
+
+import '../convert.dart';
+import '../persistent_tool_state.dart';
+
+/// This message is displayed on the first run of the Flutter tool, or anytime
+/// that the contents of this string change.
+const String _kFlutterFirstRunMessage = '''
+ ╔════════════════════════════════════════════════════════════════════════════╗
+ ║ Welcome to Flutter! - https://flutter.dev ║
+ ║ ║
+ ║ 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://flutter.dev/docs/reference/crash-reporting ║
+ ║ ║
+ ║ See Google's privacy policy: ║
+ ║ https://policies.google.com/privacy ║
+ ╚════════════════════════════════════════════════════════════════════════════╝
+''';
+
+/// The first run messenger determines whether the first run license terms
+/// need to be displayed.
+class FirstRunMessenger {
+ FirstRunMessenger({
+ @required PersistentToolState persistentToolState
+ }) : _persistentToolState = persistentToolState;
+
+ final PersistentToolState _persistentToolState;
+
+ /// Whether the license terms should be displayed.
+ ///
+ /// This is implemented by caching a hash of the previous license terms. This
+ /// does not update the cache hash value.
+ ///
+ /// The persistent tool state setting [PersistentToolState.redisplayWelcomeMessage]
+ /// can also be used to make this return false. This is primarily used to ensure
+ /// that the license terms are not printed during a `flutter upgrade`, until the
+ /// user manually runs the tool.
+ bool shouldDisplayLicenseTerms() {
+ if (_persistentToolState.redisplayWelcomeMessage == false) {
+ return false;
+ }
+ final String oldHash = _persistentToolState.lastActiveLicenseTerms;
+ return oldHash != _currentHash;
+ }
+
+ /// Update the cached license terms hash once the new terms have been displayed.
+ void confirmLicenseTermsDisplayed() {
+ _persistentToolState.lastActiveLicenseTerms = _currentHash;
+ }
+
+ /// The hash of the current license representation.
+ String get _currentHash => hex.encode(md5.convert(utf8.encode(licenseTerms)).bytes);
+
+ /// The current license terms.
+ String get licenseTerms => _kFlutterFirstRunMessage;
+}
diff --git a/packages/flutter_tools/lib/src/reporting/reporting.dart b/packages/flutter_tools/lib/src/reporting/reporting.dart
index 5185c68..7439a31 100644
--- a/packages/flutter_tools/lib/src/reporting/reporting.dart
+++ b/packages/flutter_tools/lib/src/reporting/reporting.dart
@@ -35,6 +35,7 @@
import '../project.dart';
import '../runner/flutter_command.dart';
import '../version.dart';
+import 'first_run.dart';
part 'crash_reporting.dart';
part 'disabled_usage.dart';
diff --git a/packages/flutter_tools/lib/src/reporting/usage.dart b/packages/flutter_tools/lib/src/reporting/usage.dart
index 1aacd1e..972f038 100644
--- a/packages/flutter_tools/lib/src/reporting/usage.dart
+++ b/packages/flutter_tools/lib/src/reporting/usage.dart
@@ -79,13 +79,15 @@
String configDirOverride,
String logFile,
AnalyticsFactory analyticsIOFactory,
+ FirstRunMessenger firstRunMessenger,
@required bool runningOnBot,
}) => _DefaultUsage(settingsName: settingsName,
versionOverride: versionOverride,
configDirOverride: configDirOverride,
logFile: logFile,
analyticsIOFactory: analyticsIOFactory,
- runningOnBot: runningOnBot);
+ runningOnBot: runningOnBot,
+ firstRunMessenger: firstRunMessenger);
factory Usage.test() => _DefaultUsage.test();
@@ -94,9 +96,6 @@
Map<CustomDimensions, Object> parameters,
}) => globals.flutterUsage.sendCommand(command, parameters: _useCdKeys(parameters));
- /// Whether this is the first run of the tool.
- bool get isFirstRun;
-
/// Whether analytics reporting should be suppressed.
bool get suppressAnalytics;
@@ -191,6 +190,7 @@
String configDirOverride,
String logFile,
AnalyticsFactory analyticsIOFactory,
+ @required this.firstRunMessenger,
@required bool runningOnBot,
}) {
final FlutterVersion flutterVersion = globals.flutterVersion;
@@ -202,7 +202,7 @@
analyticsIOFactory ??= _defaultAnalyticsIOFactory;
_clock = globals.systemClock;
- if (// To support testing, only allow other signals to supress analytics
+ if (// To support testing, only allow other signals to suppress analytics
// when analytics are not being shunted to a file.
!usingLogFile && (
// Ignore local user branches.
@@ -277,18 +277,17 @@
_DefaultUsage.test() :
_suppressAnalytics = false,
_analytics = AnalyticsMock(true),
+ firstRunMessenger = null,
_clock = SystemClock.fixed(DateTime(2020, 10, 8));
Analytics _analytics;
+ final FirstRunMessenger firstRunMessenger;
bool _printedWelcome = false;
bool _suppressAnalytics = false;
SystemClock _clock;
@override
- bool get isFirstRun => _analytics.firstRun;
-
- @override
bool get suppressAnalytics => _suppressAnalytics || _analytics.firstRun;
@override
@@ -383,52 +382,19 @@
await _analytics.waitForLastPing(timeout: const Duration(milliseconds: 250));
}
- void _printWelcome() {
- globals.printStatus('');
- globals.printStatus('''
- ╔════════════════════════════════════════════════════════════════════════════╗
- ║ Welcome to Flutter! - https://flutter.dev ║
- ║ ║
- ║ 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://flutter.dev/docs/reference/crash-reporting ║
- ║ ║
- ║ See Google's privacy policy: ║
- ║ https://policies.google.com/privacy ║
- ╚════════════════════════════════════════════════════════════════════════════╝
- ''', 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.
- (globals.persistentToolState.redisplayWelcomeMessage ?? true)) {
- _printWelcome();
+ // Display the welcome message if this is the first run of the tool or if
+ // the license terms have changed since it was last displayed.
+ if (firstRunMessenger != null && firstRunMessenger.shouldDisplayLicenseTerms() ?? true) {
+ globals.printStatus('');
+ globals.printStatus(firstRunMessenger.licenseTerms, emphasis: true);
_printedWelcome = true;
- globals.persistentToolState.redisplayWelcomeMessage = false;
+ firstRunMessenger.confirmLicenseTermsDisplayed();
}
}
}
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 9ab3d78..aa1299a 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/config_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/config_test.dart
@@ -34,8 +34,6 @@
mockAndroidSdk = MockAndroidSdk();
mockFlutterVersion = MockFlutterVersion();
mockUsage = MockUsage();
-
- when(mockUsage.isFirstRun).thenReturn(false);
});
void verifyNoAnalytics() {
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/doctor_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/doctor_test.dart
index 709d1e7..e9f4164 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/doctor_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/doctor_test.dart
@@ -223,7 +223,6 @@
setUp(() {
mockUsage = MockUsage();
- when(mockUsage.isFirstRun).thenReturn(true);
});
testUsingContext('contains installed', () async {
diff --git a/packages/flutter_tools/test/commands.shard/permeable/build_aar_test.dart b/packages/flutter_tools/test/commands.shard/permeable/build_aar_test.dart
index 1b49c3a..eb78656 100644
--- a/packages/flutter_tools/test/commands.shard/permeable/build_aar_test.dart
+++ b/packages/flutter_tools/test/commands.shard/permeable/build_aar_test.dart
@@ -207,12 +207,8 @@
ProcessManager mockProcessManager;
Directory tempDir;
AndroidSdk mockAndroidSdk;
- Usage mockUsage;
setUp(() {
- mockUsage = MockUsage();
- when(mockUsage.isFirstRun).thenReturn(true);
-
tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_packages_test.');
mockProcessManager = MockProcessManager();
diff --git a/packages/flutter_tools/test/commands.shard/permeable/build_apk_test.dart b/packages/flutter_tools/test/commands.shard/permeable/build_apk_test.dart
index 2750d67..421e1db 100644
--- a/packages/flutter_tools/test/commands.shard/permeable/build_apk_test.dart
+++ b/packages/flutter_tools/test/commands.shard/permeable/build_apk_test.dart
@@ -124,7 +124,6 @@
setUp(() {
mockUsage = MockUsage();
- when(mockUsage.isFirstRun).thenReturn(true);
tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_packages_test.');
gradlew = globals.fs.path.join(tempDir.path, 'flutter_project', 'android',
diff --git a/packages/flutter_tools/test/commands.shard/permeable/build_appbundle_test.dart b/packages/flutter_tools/test/commands.shard/permeable/build_appbundle_test.dart
index e11dd8d..decfc46 100644
--- a/packages/flutter_tools/test/commands.shard/permeable/build_appbundle_test.dart
+++ b/packages/flutter_tools/test/commands.shard/permeable/build_appbundle_test.dart
@@ -106,8 +106,6 @@
setUp(() {
mockUsage = MockUsage();
- when(mockUsage.isFirstRun).thenReturn(true);
-
tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_packages_test.');
gradlew = globals.fs.path.join(tempDir.path, 'flutter_project', 'android',
globals.platform.isWindows ? 'gradlew.bat' : 'gradlew');
diff --git a/packages/flutter_tools/test/general.shard/analytics_test.dart b/packages/flutter_tools/test/general.shard/analytics_test.dart
index 5551845..82cdbd0 100644
--- a/packages/flutter_tools/test/general.shard/analytics_test.dart
+++ b/packages/flutter_tools/test/general.shard/analytics_test.dart
@@ -160,7 +160,6 @@
memoryFileSystem = MemoryFileSystem.test();
mockStdio = MockStdio();
mockUsage = MockUsage();
- when(mockUsage.isFirstRun).thenReturn(false);
mockClock = MockClock();
mockDoctor = MockDoctor();
when(mockClock.now()).thenAnswer(
diff --git a/packages/flutter_tools/test/general.shard/reporting/first_run_test.dart b/packages/flutter_tools/test/general.shard/reporting/first_run_test.dart
new file mode 100644
index 0000000..4459410
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/reporting/first_run_test.dart
@@ -0,0 +1,66 @@
+// Copyright 2014 The Flutter 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:file/memory.dart';
+import 'package:flutter_tools/src/base/logger.dart';
+import 'package:flutter_tools/src/persistent_tool_state.dart';
+import 'package:flutter_tools/src/reporting/first_run.dart';
+
+import '../../src/common.dart';
+
+void main() {
+ testWithoutContext('FirstRunMessenger delegates to the first run message', () {
+ final FirstRunMessenger messenger = setUpFirstRunMessenger();
+
+ expect(messenger.licenseTerms, contains('Welcome to Flutter'));
+ });
+
+ testWithoutContext('FirstRunMessenger requires redisplay if it has never been run before', () {
+ final FirstRunMessenger messenger = setUpFirstRunMessenger();
+
+ expect(messenger.shouldDisplayLicenseTerms(), true);
+ expect(messenger.shouldDisplayLicenseTerms(), true);
+
+ // Once terms have been confirmed, then it will return false.
+ messenger.confirmLicenseTermsDisplayed();
+
+ expect(messenger.shouldDisplayLicenseTerms(), false);
+ });
+
+ testWithoutContext('FirstRunMessenger requires redisplay if the license terms have changed', () {
+ final TestFirstRunMessenger messenger = setUpFirstRunMessenger(test: true) as TestFirstRunMessenger;
+ messenger.confirmLicenseTermsDisplayed();
+
+ expect(messenger.shouldDisplayLicenseTerms(), false);
+
+ messenger.overrideLicenseTerms = 'This is a new license';
+
+ expect(messenger.shouldDisplayLicenseTerms(), true);
+ });
+
+ testWithoutContext('FirstRunMessenger does not require re-display if the persistent tool state disables it', () {
+ final FirstRunMessenger messenger = setUpFirstRunMessenger(redisplayWelcomeMessage: false);
+
+ expect(messenger.shouldDisplayLicenseTerms(), false);
+ });
+}
+
+FirstRunMessenger setUpFirstRunMessenger({bool redisplayWelcomeMessage, bool test = false }) {
+ final MemoryFileSystem fileSystem = MemoryFileSystem.test();
+ final PersistentToolState state = PersistentToolState.test(directory: fileSystem.currentDirectory, logger: BufferLogger.test())
+ ..redisplayWelcomeMessage = redisplayWelcomeMessage;
+ if (test) {
+ return TestFirstRunMessenger(state);
+ }
+ return FirstRunMessenger(persistentToolState: state);
+}
+
+class TestFirstRunMessenger extends FirstRunMessenger {
+ TestFirstRunMessenger(PersistentToolState persistentToolState) : super(persistentToolState: persistentToolState);
+
+ String overrideLicenseTerms;
+
+ @override
+ String get licenseTerms => overrideLicenseTerms ?? super.licenseTerms;
+}
diff --git a/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart b/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart
index 2eebf96..e5f46f7 100644
--- a/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart
+++ b/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart
@@ -40,7 +40,6 @@
clock = MockClock();
mockProcessInfo = MockProcessInfo();
- when(usage.isFirstRun).thenReturn(false);
when(clock.now()).thenAnswer(
(Invocation _) => DateTime.fromMillisecondsSinceEpoch(mockTimes.removeAt(0))
);
diff --git a/packages/flutter_tools/test/general.shard/runner/runner_test.dart b/packages/flutter_tools/test/general.shard/runner/runner_test.dart
index 8c6dfb1..cbb5328 100644
--- a/packages/flutter_tools/test/general.shard/runner/runner_test.dart
+++ b/packages/flutter_tools/test/general.shard/runner/runner_test.dart
@@ -260,9 +260,6 @@
}
@override
- bool get isFirstRun => _impl.isFirstRun;
-
- @override
bool get suppressAnalytics => _impl.suppressAnalytics;
@override
diff --git a/packages/flutter_tools/test/src/context.dart b/packages/flutter_tools/test/src/context.dart
index b1616fe..c86e182 100644
--- a/packages/flutter_tools/test/src/context.dart
+++ b/packages/flutter_tools/test/src/context.dart
@@ -332,9 +332,6 @@
class FakeUsage implements Usage {
@override
- bool get isFirstRun => false;
-
- @override
bool get suppressAnalytics => false;
@override
diff --git a/packages/flutter_tools/test/src/testbed.dart b/packages/flutter_tools/test/src/testbed.dart
index 94cd23e..b430777 100644
--- a/packages/flutter_tools/test/src/testbed.dart
+++ b/packages/flutter_tools/test/src/testbed.dart
@@ -175,9 +175,6 @@
}
@override
- bool get isFirstRun => false;
-
- @override
Stream<Map<String, Object>> get onSend => const Stream<Map<String, Object>>.empty();
@override