blob: 07882781690987cc49ea41c35e2eb24822d3295e [file]
// Copyright 2022 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
import 'package:devtools_app_shared/service.dart';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
import 'globals.dart';
final _log = Logger('lib/src/shared/features_flags');
@visibleForTesting
bool get enableExperiments =>
_experimentsEnabledByEnvironment || _experimentsEnabledFromMain;
/// If true, features under construction will be enabled for release build.
///
/// By default, the constant is false.
/// To enable it, pass the compilation flag
/// `--dart-define=enable_experiments=true`.
///
/// To enable the flag in debug configuration of VSCode, add value:
/// "args": [
/// "--dart-define=enable_experiments=true"
/// ]
const _experimentsEnabledByEnvironment = bool.fromEnvironment(
'enable_experiments',
);
bool _experimentsEnabledFromMain = false;
void setEnableExperiments() {
_experimentsEnabledFromMain = true;
}
@visibleForTesting
bool get enableBeta => enableExperiments || !isExternalBuild;
/// A namespace for feature flags, which set the visibility of features under
/// active development.
///
/// When adding a new feature flag, the developer is responsible for adding it
/// to the [_booleanFlags] or [_flutterChannelFlags] map for debugging
/// purposes.
extension FeatureFlags on Never {
/// Flag to enable save/load for the Memory screen.
///
/// https://github.com/flutter/devtools/issues/8019
static final memorySaveLoad = BooleanFeatureFlag(
name: 'memorySaveLoad',
enabled: enableExperiments,
);
/// Flag to enable DevTools extensions.
///
/// TODO(https://github.com/flutter/devtools/issues/6443): remove this flag
/// once extension support is added in g3.
static final devToolsExtensions = BooleanFeatureFlag(
name: 'devToolsExtensions',
enabled: isExternalBuild,
);
/// Flag to enable debugging via DAP.
///
/// https://github.com/flutter/devtools/issues/6056
static final dapDebugging = BooleanFeatureFlag(
name: 'dapDebugging',
enabled: enableExperiments,
);
/// A set of all the boolean feature flags for debugging purposes.
///
/// When adding a new boolean flag, you are responsible for adding it to this
/// map as well.
static final _booleanFlags = <BooleanFeatureFlag>{
memorySaveLoad,
devToolsExtensions,
dapDebugging,
};
/// A set of all the Flutter channel feature flags for debugging purposes.
///
/// When adding a new Flutter channel flag, you are responsible for adding it
/// to this map as well.
static final _flutterChannelFlags = <FlutterChannelFeatureFlag>{};
/// A helper to print the status of all the feature flags.
static void debugPrintFeatureFlags({ConnectedApp? connectedApp}) {
for (final entry in _booleanFlags) {
_log.config(entry.toString());
}
for (final entry in _flutterChannelFlags) {
var logLine = entry.toString();
if (connectedApp != null) {
logLine += '(enabled: ${entry.isEnabled(connectedApp)})';
}
_log.config(logLine);
}
}
}
/// A simple feature flag that is enabled or disabled by a boolean value.
class BooleanFeatureFlag {
BooleanFeatureFlag({required this.name, required bool enabled})
: _enabled = enabled;
/// The name of the feature.
final String name;
bool _enabled;
/// Whether the feature is enabled.
bool get isEnabled => _enabled;
@override
String toString() => '$name: $isEnabled';
@visibleForTesting
void setEnabledForTests(bool enabled) {
_enabled = enabled;
}
}
/// A feature flag that is enabled based on the Flutter channel of the
/// connected application.
///
/// This flag will be enabled if the connected app's Flutter channel is less
/// than or equal to [flutterChannel]. For example, if [flutterChannel] is
/// [FlutterChannel.beta], this flag will be enabled for apps on the 'beta' and
/// 'dev' channels, but not for apps on the 'stable' channel.
///
/// TODO(https://github.com/flutter/devtools/issues/9439): Restrict features
/// based on the user's Dart version instead of Flutter version to allow for
/// shared experiments across Dart and Flutter.
class FlutterChannelFeatureFlag {
const FlutterChannelFeatureFlag({
required this.name,
required this.flutterChannel,
required bool enabledForDartApps,
required bool enabledForFlutterAppsFallback,
}) : _enabledForDartApps = enabledForDartApps,
_enabledForFlutterAppsFallback = enabledForFlutterAppsFallback;
/// The name of the feature.
final String name;
/// The maximum Flutter channel that this feature is enabled for.
final FlutterChannel flutterChannel;
/// Whether the feature is enabled when the connected app is a pure Dart app.
final bool _enabledForDartApps;
/// Whether the feature is enabled when the connected app is a Flutter app,
/// but we cannot determine the Flutter channel.
final bool _enabledForFlutterAppsFallback;
/// Returns whether the feature is enabled based on the [connectedApp]'s
/// Flutter version.
bool isEnabled(ConnectedApp connectedApp) {
final isFlutterApp = connectedApp.isFlutterAppNow ?? false;
if (!isFlutterApp) {
return _enabledForDartApps;
}
final flutterVersion = connectedApp.flutterVersionNow?.version;
if (flutterVersion == null) return _enabledForFlutterAppsFallback;
final currentChannel = FlutterVersion.identifyChannel(
flutterVersion,
channelStr: connectedApp.flutterVersionNow?.channel,
);
if (currentChannel == null) return _enabledForFlutterAppsFallback;
return currentChannel <= flutterChannel;
}
@override
String toString() => '$name: <=${flutterChannel.name}';
}