Unverified Commit 7c618758 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[flutter_tools] delegate first run message re-display to new class, only if changed (#73353)

parent 5f4cf659
......@@ -46,6 +46,7 @@ import 'macos/macos_workflow.dart';
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 @@ Future<T> runInContext<T>(
SystemClock: () => const SystemClock(),
Usage: () => Usage(
runningOnBot: runningOnBot,
firstRunMessenger: FirstRunMessenger(persistentToolState: globals.persistentToolState),
),
UserMessages: () => UserMessages(),
VisualStudioValidator: () => VisualStudioValidator(
......
......@@ -47,6 +47,9 @@ abstract class PersistentToolState {
/// 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 @@ class _DefaultPersistentToolState implements PersistentToolState {
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 @@ class _DefaultPersistentToolState implements PersistentToolState {
_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];
}
......
......@@ -5,9 +5,6 @@
part of reporting;
class DisabledUsage implements Usage {
@override
bool get isFirstRun => false;
@override
bool get suppressAnalytics => true;
......
// 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;
}
......@@ -35,6 +35,7 @@ import '../globals.dart' as globals;
import '../project.dart';
import '../runner/flutter_command.dart';
import '../version.dart';
import 'first_run.dart';
part 'crash_reporting.dart';
part 'disabled_usage.dart';
......
......@@ -79,13 +79,15 @@ abstract class Usage {
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 @@ abstract class Usage {
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 @@ class _DefaultUsage implements Usage {
String configDirOverride,
String logFile,
AnalyticsFactory analyticsIOFactory,
@required this.firstRunMessenger,
@required bool runningOnBot,
}) {
final FlutterVersion flutterVersion = globals.flutterVersion;
......@@ -202,7 +202,7 @@ class _DefaultUsage implements Usage {
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,17 +277,16 @@ class _DefaultUsage implements Usage {
_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;
......@@ -383,52 +382,19 @@ class _DefaultUsage implements Usage {
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();
}
}
}
......
......@@ -34,8 +34,6 @@ void main() {
mockAndroidSdk = MockAndroidSdk();
mockFlutterVersion = MockFlutterVersion();
mockUsage = MockUsage();
when(mockUsage.isFirstRun).thenReturn(false);
});
void verifyNoAnalytics() {
......
......@@ -223,7 +223,6 @@ void main() {
setUp(() {
mockUsage = MockUsage();
when(mockUsage.isFirstRun).thenReturn(true);
});
testUsingContext('contains installed', () async {
......
......@@ -207,12 +207,8 @@ void main() {
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();
......
......@@ -124,7 +124,6 @@ void main() {
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',
......
......@@ -106,8 +106,6 @@ void main() {
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');
......
......@@ -160,7 +160,6 @@ void main() {
memoryFileSystem = MemoryFileSystem.test();
mockStdio = MockStdio();
mockUsage = MockUsage();
when(mockUsage.isFirstRun).thenReturn(false);
mockClock = MockClock();
mockDoctor = MockDoctor();
when(mockClock.now()).thenAnswer(
......
// 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;
}
......@@ -40,7 +40,6 @@ void main() {
clock = MockClock();
mockProcessInfo = MockProcessInfo();
when(usage.isFirstRun).thenReturn(false);
when(clock.now()).thenAnswer(
(Invocation _) => DateTime.fromMillisecondsSinceEpoch(mockTimes.removeAt(0))
);
......
......@@ -259,9 +259,6 @@ class CrashingUsage implements Usage {
_sentException = exception;
}
@override
bool get isFirstRun => _impl.isFirstRun;
@override
bool get suppressAnalytics => _impl.suppressAnalytics;
......
......@@ -331,9 +331,6 @@ class FakeOperatingSystemUtils implements OperatingSystemUtils {
class MockIOSSimulatorUtils extends Mock implements IOSSimulatorUtils {}
class FakeUsage implements Usage {
@override
bool get isFirstRun => false;
@override
bool get suppressAnalytics => false;
......
......@@ -174,9 +174,6 @@ class NoOpUsage implements Usage {
return null;
}
@override
bool get isFirstRun => false;
@override
Stream<Map<String, Object>> get onSend => const Stream<Map<String, Object>>.empty();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment