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

This commit is contained in:
Jonah Williams 2021-01-05 18:44:04 -08:00 committed by GitHub
parent 5f4cf6596e
commit 7c618758bb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 174 additions and 71 deletions

View file

@ -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(

View file

@ -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];
}

View file

@ -5,9 +5,6 @@
part of reporting;
class DisabledUsage implements Usage {
@override
bool get isFirstRun => false;
@override
bool get suppressAnalytics => true;

View file

@ -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;
}

View file

@ -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';

View file

@ -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();
}
}
}

View file

@ -34,8 +34,6 @@ void main() {
mockAndroidSdk = MockAndroidSdk();
mockFlutterVersion = MockFlutterVersion();
mockUsage = MockUsage();
when(mockUsage.isFirstRun).thenReturn(false);
});
void verifyNoAnalytics() {

View file

@ -223,7 +223,6 @@ void main() {
setUp(() {
mockUsage = MockUsage();
when(mockUsage.isFirstRun).thenReturn(true);
});
testUsingContext('contains installed', () async {

View file

@ -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();

View file

@ -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',

View file

@ -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');

View file

@ -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(

View file

@ -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;
}

View file

@ -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))
);

View file

@ -259,9 +259,6 @@ class CrashingUsage implements Usage {
_sentException = exception;
}
@override
bool get isFirstRun => _impl.isFirstRun;
@override
bool get suppressAnalytics => _impl.suppressAnalytics;

View file

@ -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;

View file

@ -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();