blob: 3adb7f1ddb4bb17551589e91196d2f8164a7908a [file] [log] [blame]
// Copyright 2018 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.
// This file contain higher level utils, i.e. utils that depend on
// other libraries in this package.
// Utils, that do not have dependencies, should go to primitives/utils.dart.
import 'dart:async';
import 'dart:math';
import 'package:devtools_app_shared/service.dart';
import 'package:devtools_app_shared/ui.dart';
import 'package:devtools_app_shared/utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:logging/logging.dart';
import 'package:provider/provider.dart';
import 'package:vm_service/vm_service.dart';
import '../../devtools.dart' as devtools;
import 'common_widgets.dart';
import 'connected_app.dart';
import 'globals.dart';
import 'primitives/utils.dart';
import 'query_parameters.dart';
final _log = Logger('lib/src/shared/utils');
/// Logging to debug console only in debug runs.
void debugLogger(String message) {
assert(
() {
_log.info(message);
return true;
}(),
);
}
/// Whether DevTools is using a dark theme.
///
/// When DevTools is in embedded mode, we first check if the [ideTheme] has
/// specified a light or dark theme, and if it has we use this value. This is
/// safe to do because the user cannot access the dark theme DevTools setting
/// when in embedded mode, which is intentional so that the embedded DevTools
/// matches the theme of its surrounding window (the IDE).
///
/// When DevTools is not embedded, we use the user preference to determine
/// whether DevTools is using a light or dark theme.
///
/// This utility method should be used in favor of checking
/// [preferences.darkModeTheme.value] so that the embedded case is always
/// handled properly.
bool isDarkThemeEnabled() {
return isEmbedded() && ideTheme.ideSpecifiedTheme
? ideTheme.isDarkMode
: preferences.darkModeEnabled.value;
}
extension VmExtension on VM {
List<IsolateRef> isolatesForDevToolsMode() {
final vmDeveloperModeEnabled = preferences.vmDeveloperModeEnabled.value;
final vmIsolates = isolates ?? <IsolateRef>[];
return [
...vmIsolates,
if (vmDeveloperModeEnabled || vmIsolates.isEmpty)
...systemIsolates ?? <IsolateRef>[],
];
}
String get deviceDisplay {
return [
'$targetCPU',
if (architectureBits != null && architectureBits != -1)
'($architectureBits bit)',
operatingSystem,
].join(' ');
}
}
List<ConnectionDescription> generateDeviceDescription(
VM vm,
ConnectedApp connectedApp, {
bool includeVmServiceConnection = true,
}) {
var version = vm.version!;
// Convert '2.9.0-13.0.dev (dev) (Fri May ... +0200) on "macos_x64"' to
// '2.9.0-13.0.dev'.
if (version.contains(' ')) {
version = version.substring(0, version.indexOf(' '));
}
final flutterVersion = connectedApp.flutterVersionNow;
ConnectionDescription? vmServiceConnection;
if (includeVmServiceConnection &&
serviceConnection.serviceManager.service != null) {
final description = serviceConnection.serviceManager.serviceUri!;
vmServiceConnection = ConnectionDescription(
title: 'VM Service Connection',
description: description,
actions: [
CopyToClipboardControl(
dataProvider: () => description,
),
],
);
}
return [
ConnectionDescription(title: 'CPU / OS', description: vm.deviceDisplay),
ConnectionDescription(
title: 'Connected app type',
description: connectedApp.display,
),
if (vmServiceConnection != null) vmServiceConnection,
ConnectionDescription(title: 'Dart Version', description: version),
if (flutterVersion != null && !flutterVersion.unknown) ...{
ConnectionDescription(
title: 'Flutter Version',
description: '${flutterVersion.version} / ${flutterVersion.channel}',
),
ConnectionDescription(
title: 'Framework / Engine',
description: '${flutterVersion.frameworkRevision} / '
'${flutterVersion.engineRevision}',
),
},
];
}
/// This method should be public, because it is used by g3 specific code.
List<String> issueLinkDetails() {
final ide = DevToolsQueryParams.load().ide;
final issueDescriptionItems = [
'<-- Please describe your problem here. Be sure to include repro steps. -->',
'___', // This will create a separator in the rendered markdown.
'**DevTools version**: $devToolsVersion',
if (ide != null) '**IDE**: $ide',
];
final vm = serviceConnection.serviceManager.vm;
final connectedApp = serviceConnection.serviceManager.connectedApp;
if (vm != null && connectedApp != null) {
final descriptionEntries = generateDeviceDescription(
vm,
connectedApp,
includeVmServiceConnection: false,
);
final deviceDescription = descriptionEntries
.map((entry) => '${entry.title}: ${entry.description}');
issueDescriptionItems.addAll([
'**Connected Device**:',
...deviceDescription,
]);
}
return issueDescriptionItems;
}
typedef ProvidedControllerCallback<T> = void Function(T);
/// Mixin that provides a [controller] from package:provider for a State class.
///
/// [initController] must be called from [State.didChangeDependencies]. If
/// [initController] returns false, return early from [didChangeDependencies] to
/// avoid calling any initialization code that should only be called once for a
/// controller. See [initController] documentation below for more details.
mixin ProvidedControllerMixin<T, V extends StatefulWidget> on State<V> {
T get controller => _controller!;
T? _controller;
final _callWhenReady = <ProvidedControllerCallback>[];
/// Calls the provided [callback] once [_controller] has been initialized.
///
/// The [callback] will be called immediately if [_controller] has already
/// been initialized.
void callWhenControllerReady(ProvidedControllerCallback callback) {
if (_controller != null) {
callback(_controller!);
} else {
_callWhenReady.add(callback);
}
}
/// Initializes [_controller] from package:provider.
///
/// This method should be called in [didChangeDependencies]. Returns whether
/// or not a new controller was provided upon subsequent calls to
/// [initController].
///
/// This method will commonly be used to return early from
/// [didChangeDependencies] when initialization code should not be run again
/// if the provided controller has not changed.
///
/// E.g. `if (!initController()) return;`
bool initController() {
final newController = Provider.of<T>(context);
if (newController == _controller) return false;
final firstInitialization = _controller == null;
_controller = newController;
if (firstInitialization) {
for (final callback in _callWhenReady) {
callback(_controller!);
}
_callWhenReady.clear();
}
return true;
}
}
class ConnectionDescription {
ConnectionDescription({
required this.title,
required this.description,
this.actions = const <Widget>[],
});
final String title;
final String description;
final List<Widget> actions;
}
const _google3PathSegment = 'google3';
bool isGoogle3Path(List<String> pathParts) =>
pathParts.contains(_google3PathSegment);
List<String> stripGoogle3(List<String> pathParts) {
final google3Index = pathParts.lastIndexOf(_google3PathSegment);
if (google3Index != -1 && google3Index + 1 < pathParts.length) {
return pathParts.sublist(google3Index + 1);
}
return pathParts;
}
/// An extension on [KeyEvent] to make it simpler to determine if it is a key
/// down event.
extension IsKeyType on KeyEvent {
bool get isKeyDownOrRepeat => this is KeyDownEvent || this is KeyRepeatEvent;
}
/// A helper class for [Timer] functionality, where the callbacks are debounced.
class DebounceTimer {
/// A periodic timer that ensures [callback] is only called at most once
/// per [duration].
///
/// [callback] is triggered once immediately, and then every [duration] the
/// timer checks to see if the previous [callback] call has finished running.
/// If it has finished, then then next call to [callback] will begin.
DebounceTimer.periodic(
Duration duration,
Future<void> Function() callback,
) : _callback = callback {
// Start running the first call to the callback.
_runCallback();
// Start periodic timer so that the callback will be periodically triggered
// after the first callback.
_timer = Timer.periodic(duration, (_) => _runCallback());
}
void _runCallback() async {
// If the previous callback is still running, then don't trigger another
// callback. (debounce)
if (_isRunning) {
return;
}
try {
_isRunning = true;
await _callback();
} finally {
_isRunning = false;
}
}
late final Timer _timer;
final Future<void> Function() _callback;
bool _isRunning = false;
void cancel() {
_timer.cancel();
}
bool get isCancelled => !_timer.isActive;
void dispose() {
cancel();
}
}
Future<void> launchUrlWithErrorHandling(String url) async {
await launchUrl(
url,
onError: () => notificationService.push('Unable to open $url.'),
);
}
/// A worker that will run [callback] in groups of [chunkSize], when [doWork] is called.
///
/// [progressCallback] will be called with 0.0 progress when starting the work and any
/// time a chunk finishes running, with a value that represents the proportion of
/// indices that have been completed so far.
///
/// This class may be helpful when sets of work need to be done over a list, while
/// avoiding blocking the UI thread.
class InterruptableChunkWorker {
InterruptableChunkWorker({
int chunkSize = _defaultChunkSize,
required this.callback,
required this.progressCallback,
}) : _chunkSize = chunkSize;
static const _defaultChunkSize = 50;
final int _chunkSize;
int _workId = 0;
bool _disposed = false;
void Function(int) callback;
void Function(double progress) progressCallback;
/// Start doing the chunked work.
///
/// [callback] will be called on every index from 0...[length-1], inclusive,
/// in chunks of [_chunkSize]
///
/// If [doWork] is called again, then [callback] will no longer be called
/// on any remaining indices from previous [doWork] calls.
///
Future<bool> doWork(int length) {
final completer = Completer<bool>();
final localWorkId = ++_workId;
Future<void> doChunkWork(int chunkStartingIndex) async {
if (_disposed) {
return completer.complete(false);
}
if (chunkStartingIndex >= length) {
return completer.complete(true);
}
final chunkUpperIndexLimit = min(length, chunkStartingIndex + _chunkSize);
for (int indexIterator = chunkStartingIndex;
indexIterator < chunkUpperIndexLimit;
indexIterator++) {
// If our localWorkId is no longer active, then do not continue working
if (localWorkId != _workId) return completer.complete(false);
callback(indexIterator);
}
progressCallback(chunkUpperIndexLimit / length);
await delayToReleaseUiThread();
await doChunkWork(chunkStartingIndex + _chunkSize);
}
if (length <= 0) {
return Future.value(true);
}
progressCallback(0.0);
doChunkWork(0);
return completer.future;
}
void dispose() {
_disposed = true;
}
}
String get devToolsVersion => devtools.version;