blob: 7427f6cf736b9766625e32f65fecce6c4aa58978 [file] [log] [blame]
// Copyright 2020 The Chromium 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 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import '../service/vm_service_wrapper.dart';
import 'analytics/analytics.dart' as ga;
import 'analytics/constants.dart' as gac;
import 'globals.dart';
import 'inspector_service.dart';
import 'primitives/auto_dispose.dart';
import 'primitives/utils.dart';
/// A controller for global application preferences.
class PreferencesController extends DisposableController
with AutoDisposeControllerMixin {
final ValueNotifier<bool> _darkModeTheme = ValueNotifier(true);
final ValueNotifier<bool> _vmDeveloperMode = ValueNotifier(false);
final ValueNotifier<bool> _denseMode = ValueNotifier(false);
ValueListenable<bool> get darkModeTheme => _darkModeTheme;
ValueListenable<bool> get vmDeveloperModeEnabled => _vmDeveloperMode;
ValueListenable<bool> get denseModeEnabled => _denseMode;
InspectorPreferencesController get inspector => _inspector;
final _inspector = InspectorPreferencesController();
MemoryPreferencesController get memory => _memory;
final _memory = MemoryPreferencesController();
CpuProfilerPreferencesController get cpuProfiler => _cpuProfiler;
final _cpuProfiler = CpuProfilerPreferencesController();
Future<void> init() async {
// Get the current values and listen for and write back changes.
String? value = await storage.getValue('ui.darkMode');
toggleDarkModeTheme(value == null || value == 'true');
addAutoDisposeListener(_darkModeTheme, () {
storage.setValue('ui.darkMode', '${_darkModeTheme.value}');
});
value = await storage.getValue('ui.vmDeveloperMode');
toggleVmDeveloperMode(value == 'true');
addAutoDisposeListener(_vmDeveloperMode, () {
storage.setValue('ui.vmDeveloperMode', '${_vmDeveloperMode.value}');
});
value = await storage.getValue('ui.denseMode');
toggleDenseMode(value == 'true');
addAutoDisposeListener(_denseMode, () {
storage.setValue('ui.denseMode', '${_denseMode.value}');
});
await inspector.init();
await memory.init();
await cpuProfiler.init();
setGlobal(PreferencesController, this);
}
@override
void dispose() {
inspector.dispose();
memory.dispose();
super.dispose();
}
/// Change the value for the dark mode setting.
void toggleDarkModeTheme(bool useDarkMode) {
_darkModeTheme.value = useDarkMode;
}
/// Change the value for the VM developer mode setting.
void toggleVmDeveloperMode(bool enableVmDeveloperMode) {
_vmDeveloperMode.value = enableVmDeveloperMode;
VmServiceWrapper.enablePrivateRpcs = enableVmDeveloperMode;
}
/// Change the value for the dense mode setting.
void toggleDenseMode(bool enableDenseMode) {
_denseMode.value = enableDenseMode;
}
}
class InspectorPreferencesController extends DisposableController
with AutoDisposeControllerMixin {
ValueListenable<bool> get hoverEvalModeEnabled => _hoverEvalMode;
ListValueNotifier<String> get customPubRootDirectories =>
_customPubRootDirectories;
ValueListenable<bool> get isRefreshingCustomPubRootDirectories =>
_customPubRootDirectoriesAreBusy;
InspectorService? get _inspectorService =>
serviceManager.inspectorService as InspectorService?;
final _hoverEvalMode = ValueNotifier<bool>(false);
final _customPubRootDirectories = ListValueNotifier<String>([]);
final _customPubRootDirectoriesAreBusy = ValueNotifier<bool>(false);
final _busyCounter = ValueNotifier<int>(0);
static const _hoverEvalModeStorageId = 'inspector.hoverEvalMode';
static const _customPubRootDirectoriesStoragePrefix =
'inspector.customPubRootDirectories';
String? _mainScriptDir;
Future<void> _updateMainScriptRef() async {
final rootLibUriString = await serviceManager.tryToDetectMainRootLib();
final rootLibUri = Uri.parse(rootLibUriString ?? '');
final directorySegments =
rootLibUri.pathSegments.sublist(0, rootLibUri.pathSegments.length - 1);
final rootLibDirectory = rootLibUri.replace(
pathSegments: directorySegments,
);
_mainScriptDir = rootLibDirectory.path;
}
Future<void> init() async {
await _initHoverEvalMode();
// TODO(jacobr): consider initializing this first as it is not blocking.
_initCustomPubRootDirectories();
}
Future<void> _initHoverEvalMode() async {
String? hoverEvalModeEnabledValue =
await storage.getValue(_hoverEvalModeStorageId);
// When embedded, default hoverEvalMode to off
hoverEvalModeEnabledValue ??= (!ideTheme.embed).toString();
setHoverEvalMode(hoverEvalModeEnabledValue == 'true');
addAutoDisposeListener(_hoverEvalMode, () {
storage.setValue(
_hoverEvalModeStorageId,
_hoverEvalMode.value.toString(),
);
});
}
void _initCustomPubRootDirectories() {
autoDisposeStreamSubscription(
serviceManager.onConnectionAvailable
.listen(_handleConnectionToNewService),
);
autoDisposeStreamSubscription(
serviceManager.onConnectionClosed.listen(_handleConnectionClosed),
);
addAutoDisposeListener(_busyCounter, () {
_customPubRootDirectoriesAreBusy.value = _busyCounter.value != 0;
});
addAutoDisposeListener(
serviceManager.isolateManager.mainIsolate,
() {
if (_mainScriptDir != null &&
serviceManager.isolateManager.mainIsolate.value != null) {
final debuggerState =
serviceManager.isolateManager.mainIsolateDebuggerState;
if (debuggerState?.isPaused.value == false) {
// the isolate is already unpaused, we can try to load
// the directories
unawaited(preferences.inspector.loadCustomPubRootDirectories());
} else {
late Function() pausedListener;
pausedListener = () {
if (debuggerState?.isPaused.value == false) {
unawaited(preferences.inspector.loadCustomPubRootDirectories());
debuggerState?.isPaused.removeListener(pausedListener);
}
};
// The isolate is still paused, listen for when it becomes unpaused.
addAutoDisposeListener(debuggerState?.isPaused, pausedListener);
}
}
},
);
}
void _handleConnectionClosed(Object? _) {
_mainScriptDir = null;
_customPubRootDirectories.clear();
}
Future<void> _handleConnectionToNewService(VmServiceWrapper _) async {
await _updateMainScriptRef();
_customPubRootDirectories.clear();
await loadCustomPubRootDirectories();
if (_customPubRootDirectories.value.isEmpty) {
// If there are no pub root directories set on the first connection
// then try inferring them.
await _customPubRootDirectoryBusyTracker(() async {
await _inspectorService?.inferPubRootDirectoryIfNeeded();
await loadCustomPubRootDirectories();
});
}
}
void _persistCustomPubRootDirectoriesToStorage() {
unawaited(
storage.setValue(
_customPubRootStorageId(),
jsonEncode(_customPubRootDirectories.value),
),
);
}
Future<void> addPubRootDirectories(
List<String> pubRootDirectories,
) async {
// TODO(https://github.com/flutter/devtools/issues/4380):
// Add validation to EditableList Input.
// Directories of just / will break the inspector tree local package checks.
pubRootDirectories.removeWhere(
(element) => RegExp('^[/\\s]*\$').firstMatch(element) != null,
);
if (!serviceManager.hasConnection) return;
await _customPubRootDirectoryBusyTracker(() async {
final inspectorService = _inspectorService;
if (inspectorService == null) return;
await inspectorService.addPubRootDirectories(pubRootDirectories);
await _refreshPubRootDirectoriesFromService();
});
}
Future<void> removePubRootDirectories(
List<String> pubRootDirectories,
) async {
if (!serviceManager.hasConnection) return;
await _customPubRootDirectoryBusyTracker(() async {
final localInspectorService = _inspectorService;
if (localInspectorService == null) return;
await localInspectorService.removePubRootDirectories(pubRootDirectories);
await _refreshPubRootDirectoriesFromService();
});
}
Future<void> _refreshPubRootDirectoriesFromService() async {
await _customPubRootDirectoryBusyTracker(() async {
final localInspectorService = _inspectorService;
if (localInspectorService == null) return;
final freshPubRootDirectories =
await localInspectorService.getPubRootDirectories();
if (freshPubRootDirectories != null) {
final newSet = Set<String>.of(freshPubRootDirectories);
final oldSet = Set<String>.of(_customPubRootDirectories.value);
final directoriesToAdd = newSet.difference(oldSet);
final directoriesToRemove = oldSet.difference(newSet);
_customPubRootDirectories.removeAll(directoriesToRemove);
_customPubRootDirectories.addAll(directoriesToAdd);
_persistCustomPubRootDirectoriesToStorage();
}
});
}
String _customPubRootStorageId() {
assert(_mainScriptDir != null);
final packageId = _mainScriptDir ?? '_fallback';
return '${_customPubRootDirectoriesStoragePrefix}_$packageId';
}
Future<void> loadCustomPubRootDirectories() async {
if (!serviceManager.hasConnection) return;
await _customPubRootDirectoryBusyTracker(() async {
final storedCustomPubRootDirectories =
await storage.getValue(_customPubRootStorageId());
if (storedCustomPubRootDirectories != null) {
await addPubRootDirectories(
List<String>.from(
jsonDecode(storedCustomPubRootDirectories),
),
);
}
await _refreshPubRootDirectoriesFromService();
});
}
Future<void> _customPubRootDirectoryBusyTracker(
Future<void> Function() callback,
) async {
try {
_busyCounter.value++;
await callback();
} finally {
_busyCounter.value--;
}
}
/// Change the value for the hover eval mode setting.
void setHoverEvalMode(bool enableHoverEvalMode) {
_hoverEvalMode.value = enableHoverEvalMode;
}
}
class MemoryPreferencesController extends DisposableController
with AutoDisposeControllerMixin {
final androidCollectionEnabled = ValueNotifier<bool>(false);
static const _androidCollectionEnabledStorageId =
'memory.androidCollectionEnabled';
final showChart = ValueNotifier<bool>(true);
static const _showChartStorageId = 'memory.showChart';
Future<void> init() async {
addAutoDisposeListener(
androidCollectionEnabled,
() {
storage.setValue(
_androidCollectionEnabledStorageId,
androidCollectionEnabled.value.toString(),
);
if (androidCollectionEnabled.value) {
ga.select(
gac.memory,
gac.MemoryEvent.chartAndroid,
);
}
},
);
androidCollectionEnabled.value =
await storage.getValue(_androidCollectionEnabledStorageId) == 'true';
addAutoDisposeListener(
showChart,
() {
storage.setValue(
_showChartStorageId,
showChart.value.toString(),
);
ga.select(
gac.memory,
showChart.value
? gac.MemoryEvent.showChart
: gac.MemoryEvent.hideChart,
);
},
);
showChart.value = await storage.getValue(_showChartStorageId) == 'true';
}
}
class CpuProfilerPreferencesController extends DisposableController
with AutoDisposeControllerMixin {
final displayTreeGuidelines = ValueNotifier<bool>(false);
static final _displayTreeGuidelinesId =
'${gac.cpuProfiler}.${gac.cpuProfileDisplayTreeGuidelines}';
Future<void> init() async {
addAutoDisposeListener(
displayTreeGuidelines,
() {
storage.setValue(
_displayTreeGuidelinesId,
displayTreeGuidelines.value.toString(),
);
ga.select(
gac.cpuProfiler,
gac.cpuProfileDisplayTreeGuidelines,
value: displayTreeGuidelines.value ? 1 : 0,
);
},
);
displayTreeGuidelines.value =
await storage.getValue(_displayTreeGuidelinesId) == 'true';
}
}