diff --git a/packages/flutter_tools/lib/src/version.dart b/packages/flutter_tools/lib/src/version.dart index 7d1ef094ee..c08c44435e 100644 --- a/packages/flutter_tools/lib/src/version.dart +++ b/packages/flutter_tools/lib/src/version.dart @@ -206,12 +206,14 @@ class FlutterVersion { /// The amount of time we wait before pinging the server to check for the /// availability of a newer version of Flutter. @visibleForTesting - static const Duration kCheckAgeConsideredUpToDate = const Duration(days: 7); + static const Duration kCheckAgeConsideredUpToDate = const Duration(days: 3); /// We warn the user if the age of their Flutter installation is greater than /// this duration. + /// + /// This is set to 5 weeks because releases are currently around every 4 weeks. @visibleForTesting - static final Duration kVersionAgeConsideredUpToDate = kCheckAgeConsideredUpToDate * 4; + static const Duration kVersionAgeConsideredUpToDate = const Duration(days: 35); /// The amount of time we wait between issuing a warning. /// @@ -224,7 +226,7 @@ class FlutterVersion { /// /// This can be customized in tests to speed them up. @visibleForTesting - static Duration kPauseToLetUserReadTheMessage = const Duration(seconds: 2); + static Duration timeToPauseToLetUserReadTheMessage = const Duration(seconds: 2); /// Checks if the currently installed version of Flutter is up-to-date, and /// warns the user if it isn't. @@ -232,30 +234,49 @@ class FlutterVersion { /// This function must run while [Cache.lock] is acquired because it reads and /// writes shared cache files. Future checkFlutterVersionFreshness() async { + // Don't perform update checks if we're not on an official channel. + if (!officialChannels.contains(_channel)) { + return; + } + final DateTime localFrameworkCommitDate = DateTime.parse(frameworkCommitDate); final Duration frameworkAge = _clock.now().difference(localFrameworkCommitDate); final bool installationSeemsOutdated = frameworkAge > kVersionAgeConsideredUpToDate; - Future newerFrameworkVersionAvailable() async { - final DateTime latestFlutterCommitDate = await _getLatestAvailableFlutterVersion(); - - if (latestFlutterCommitDate == null) - return false; - - return latestFlutterCommitDate.isAfter(localFrameworkCommitDate); - } + // Get whether there's a newer version on the remote. This only goes + // to the server if we haven't checked recently so won't happen on every + // command. + final DateTime latestFlutterCommitDate = await _getLatestAvailableFlutterDate(); + final VersionCheckResult remoteVersionStatus = + latestFlutterCommitDate == null + ? VersionCheckResult.unknown + : latestFlutterCommitDate.isAfter(localFrameworkCommitDate) + ? VersionCheckResult.newVersionAvailable + : VersionCheckResult.versionIsCurrent; + // Do not load the stamp before the above server check as it may modify the stamp file. final VersionCheckStamp stamp = await VersionCheckStamp.load(); final DateTime lastTimeWarningWasPrinted = stamp.lastTimeWarningWasPrinted ?? _clock.agoBy(kMaxTimeSinceLastWarning * 2); final bool beenAWhileSinceWarningWasPrinted = _clock.now().difference(lastTimeWarningWasPrinted) > kMaxTimeSinceLastWarning; - if (beenAWhileSinceWarningWasPrinted && installationSeemsOutdated && await newerFrameworkVersionAvailable()) { - printStatus(versionOutOfDateMessage(frameworkAge), emphasis: true); + // We show a warning if either we know there is a new remote version, or we couldn't tell but the local + // version is outdated. + final bool canShowWarning = + remoteVersionStatus == VersionCheckResult.newVersionAvailable || + (remoteVersionStatus == VersionCheckResult.unknown && + installationSeemsOutdated); + + if (beenAWhileSinceWarningWasPrinted && canShowWarning) { + final String updateMessage = + remoteVersionStatus == VersionCheckResult.newVersionAvailable + ? newVersionAvailableMessage() + : versionOutOfDateMessage(frameworkAge); + printStatus(updateMessage, emphasis: true); await Future.wait(>[ stamp.store( newTimeWarningWasPrinted: _clock.now(), ), - new Future.delayed(kPauseToLetUserReadTheMessage), + new Future.delayed(timeToPauseToLetUserReadTheMessage), ]); } } @@ -275,6 +296,17 @@ class FlutterVersion { '''; } + @visibleForTesting + static String newVersionAvailableMessage() { + return ''' + ╔════════════════════════════════════════════════════════════════════════════╗ + ║ A new version of Flutter is available! ║ + ║ ║ + ║ To update to the latest version, run "flutter upgrade". ║ + ╚════════════════════════════════════════════════════════════════════════════╝ +'''; + } + /// Gets the release date of the latest available Flutter version. /// /// This method sends a server request if it's been more than @@ -282,7 +314,7 @@ class FlutterVersion { /// /// Returns null if the cached version is out-of-date or missing, and we are /// unable to reach the server to get the latest version. - Future _getLatestAvailableFlutterVersion() async { + Future _getLatestAvailableFlutterDate() async { Cache.checkLockAcquired(); final VersionCheckStamp versionCheckStamp = await VersionCheckStamp.load(); @@ -296,8 +328,7 @@ class FlutterVersion { // Cache is empty or it's been a while since the last server ping. Ping the server. try { - final String branch = officialChannels.contains(_channel) ? _channel : 'master'; - final DateTime remoteFrameworkCommitDate = DateTime.parse(await FlutterVersion.fetchRemoteFrameworkCommitDate(branch)); + final DateTime remoteFrameworkCommitDate = DateTime.parse(await FlutterVersion.fetchRemoteFrameworkCommitDate(_channel)); await versionCheckStamp.store( newTimeVersionWasChecked: _clock.now(), newKnownRemoteVersion: remoteFrameworkCommitDate, @@ -308,6 +339,11 @@ class FlutterVersion { // there's no Internet connectivity. Remote version check is best effort // only. We do not prevent the command from running when it fails. printTrace('Failed to check Flutter version in the remote repository: $error'); + // Still update the timestamp to avoid us hitting the server on every single + // command if for some reason we cannot connect (eg. we may be offline). + await versionCheckStamp.store( + newTimeVersionWasChecked: _clock.now(), + ); return null; } } @@ -355,8 +391,8 @@ class VersionCheckStamp { static VersionCheckStamp fromJson(Map jsonObject) { DateTime readDateTime(String property) { return jsonObject.containsKey(property) - ? DateTime.parse(jsonObject[property]) - : null; + ? DateTime.parse(jsonObject[property]) + : null; } return new VersionCheckStamp( @@ -513,3 +549,13 @@ class GitTagVersion { return '$x.$y.${z + 1}-pre.$commits'; } } + +enum VersionCheckResult { + /// Unable to check whether a new version is available, possibly due to + /// a connectivity issue. + unknown, + /// The current version is up to date. + versionIsCurrent, + /// A newer version is available. + newVersionAvailable, +} diff --git a/packages/flutter_tools/test/version_test.dart b/packages/flutter_tools/test/version_test.dart index 9770ef9784..61ec9f6c05 100644 --- a/packages/flutter_tools/test/version_test.dart +++ b/packages/flutter_tools/test/version_test.dart @@ -36,7 +36,7 @@ void main() { ['git', 'rev-parse', '--abbrev-ref', '--symbolic', '@{u}'], workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment'), - )).thenReturn(new ProcessResult(101, 0, 'channel', '')); + )).thenReturn(new ProcessResult(101, 0, 'master', '')); when(mockProcessManager.runSync( ['git', 'rev-parse', '--abbrev-ref', 'HEAD'], workingDirectory: anyNamed('workingDirectory'), @@ -62,11 +62,18 @@ void main() { group('$FlutterVersion', () { setUpAll(() { Cache.disableLocking(); - FlutterVersion.kPauseToLetUserReadTheMessage = Duration.zero; + FlutterVersion.timeToPauseToLetUserReadTheMessage = Duration.zero; }); testUsingContext('prints nothing when Flutter installation looks fresh', () async { - fakeData(mockProcessManager, mockCache, localCommitDate: _upToDateVersion); + fakeData( + mockProcessManager, + mockCache, + localCommitDate: _upToDateVersion, + // Server will be pinged because we haven't pinged within last x days + expectServerPing: true, + remoteCommitDate: _outOfDateVersion, + expectSetStamp: true); await FlutterVersion.instance.checkFlutterVersionFreshness(); _expectVersionMessage(''); }, overrides: { @@ -114,7 +121,7 @@ void main() { ); await version.checkFlutterVersionFreshness(); - _expectVersionMessage(FlutterVersion.versionOutOfDateMessage(_testClock.now().difference(_outOfDateVersion))); + _expectVersionMessage(FlutterVersion.newVersionAvailableMessage()); }, overrides: { FlutterVersion: () => new FlutterVersion(_testClock), ProcessManager: () => mockProcessManager, @@ -136,7 +143,7 @@ void main() { ); await version.checkFlutterVersionFreshness(); - _expectVersionMessage(FlutterVersion.versionOutOfDateMessage(_testClock.now().difference(_outOfDateVersion))); + _expectVersionMessage(FlutterVersion.newVersionAvailableMessage()); expect((await VersionCheckStamp.load()).lastTimeWarningWasPrinted, _testClock.now()); await version.checkFlutterVersionFreshness(); @@ -160,7 +167,7 @@ void main() { ); await version.checkFlutterVersionFreshness(); - _expectVersionMessage(FlutterVersion.versionOutOfDateMessage(_testClock.now().difference(_outOfDateVersion))); + _expectVersionMessage(FlutterVersion.newVersionAvailableMessage()); // Immediate subsequent check is not expected to ping the server. fakeData( @@ -194,14 +201,34 @@ void main() { ); await version.checkFlutterVersionFreshness(); - _expectVersionMessage(FlutterVersion.versionOutOfDateMessage(_testClock.now().difference(_outOfDateVersion))); + _expectVersionMessage(FlutterVersion.newVersionAvailableMessage()); }, overrides: { FlutterVersion: () => new FlutterVersion(_testClock), ProcessManager: () => mockProcessManager, Cache: () => mockCache, }); - testUsingContext('ignores network issues', () async { + testUsingContext('does not print warning when unable to connect to server if not out of date', () async { + final FlutterVersion version = FlutterVersion.instance; + + fakeData( + mockProcessManager, + mockCache, + localCommitDate: _upToDateVersion, + errorOnFetch: true, + expectServerPing: true, + expectSetStamp: true, + ); + + await version.checkFlutterVersionFreshness(); + _expectVersionMessage(''); + }, overrides: { + FlutterVersion: () => new FlutterVersion(_testClock), + ProcessManager: () => mockProcessManager, + Cache: () => mockCache, + }); + + testUsingContext('prints warning when unable to connect to server if really out of date', () async { final FlutterVersion version = FlutterVersion.instance; fakeData( @@ -210,10 +237,11 @@ void main() { localCommitDate: _outOfDateVersion, errorOnFetch: true, expectServerPing: true, + expectSetStamp: true ); await version.checkFlutterVersionFreshness(); - _expectVersionMessage(''); + _expectVersionMessage(FlutterVersion.versionOutOfDateMessage(_testClock.now().difference(_outOfDateVersion))); }, overrides: { FlutterVersion: () => new FlutterVersion(_testClock), ProcessManager: () => mockProcessManager,