mirror of
https://github.com/flutter/flutter
synced 2024-10-13 03:32:55 +00:00
[flutter_tools] delegate first run message re-display to new class, only if changed (#73353)
This commit is contained in:
parent
5f4cf6596e
commit
7c618758bb
|
@ -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;
|
||||
|
||||
|
|
79
packages/flutter_tools/lib/src/reporting/first_run.dart
Normal file
79
packages/flutter_tools/lib/src/reporting/first_run.dart
Normal 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;
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
|
||||
|
|
Loading…
Reference in a new issue