| // Copyright 2021 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. |
| |
| // @dart=2.9 |
| |
| // TODO(bkonyi): remove once package:devtools_server_api is available |
| // See https://github.com/flutter/devtools/issues/2958. |
| |
| import 'dart:convert'; |
| import 'dart:io'; |
| |
| import 'package:path/path.dart' as path; |
| import 'package:usage/usage_io.dart'; |
| |
| import 'file_system.dart'; |
| |
| /// Access the file '~/.flutter'. |
| class FlutterUsage { |
| /// Create a new Usage instance; [versionOverride] and [configDirOverride] are |
| /// used for testing. |
| FlutterUsage({ |
| String settingsName = 'flutter', |
| String versionOverride, |
| String configDirOverride, |
| }) { |
| _analytics = AnalyticsIO('', settingsName, ''); |
| } |
| |
| Analytics _analytics; |
| |
| /// Does the .flutter store exist? |
| static bool get doesStoreExist { |
| return LocalFileSystem.flutterStoreExists(); |
| } |
| |
| bool get isFirstRun => _analytics.firstRun; |
| |
| bool get enabled => _analytics.enabled; |
| |
| set enabled(bool value) => _analytics.enabled = value; |
| |
| String get clientId => _analytics.clientId; |
| } |
| |
| // Access the DevTools on disk store (~/.devtools/.devtools). |
| class DevToolsUsage { |
| /// Create a new Usage instance; [versionOverride] and [configDirOverride] are |
| /// used for testing. |
| DevToolsUsage({ |
| String versionOverride, |
| String configDirOverride, |
| }) { |
| LocalFileSystem.maybeMoveLegacyDevToolsStore(); |
| properties = IOPersistentProperties( |
| storeName, |
| documentDirPath: LocalFileSystem.devToolsDir(), |
| ); |
| } |
| |
| static const storeName = '.devtools'; |
| |
| /// The activeSurvey is the property name of a top-level property |
| /// existing or created in the file ~/.devtools |
| /// If the property doesn't exist it is created with default survey values: |
| /// |
| /// properties[activeSurvey]['surveyActionTaken'] = false; |
| /// properties[activeSurvey]['surveyShownCount'] = 0; |
| /// |
| /// It is a requirement that the API apiSetActiveSurvey must be called before |
| /// calling any survey method on DevToolsUsage (addSurvey, rewriteActiveSurvey, |
| /// surveyShownCount, incrementSurveyShownCount, or surveyActionTaken). |
| String _activeSurvey; |
| |
| IOPersistentProperties properties; |
| |
| static const _surveyActionTaken = 'surveyActionTaken'; |
| static const _surveyShownCount = 'surveyShownCount'; |
| |
| void reset() { |
| properties.remove('firstRun'); |
| properties['enabled'] = false; |
| } |
| |
| bool get isFirstRun { |
| properties['firstRun'] = properties['firstRun'] == null; |
| return properties['firstRun']; |
| } |
| |
| bool get enabled { |
| if (properties['enabled'] == null) { |
| properties['enabled'] = false; |
| } |
| |
| return properties['enabled']; |
| } |
| |
| set enabled(bool value) { |
| properties['enabled'] = value; |
| return properties['enabled']; |
| } |
| |
| bool surveyNameExists(String surveyName) => properties[surveyName] != null; |
| |
| void _addSurvey(String surveyName) { |
| assert(activeSurvey != null); |
| assert(activeSurvey == surveyName); |
| rewriteActiveSurvey(false, 0); |
| } |
| |
| String get activeSurvey => _activeSurvey; |
| |
| set activeSurvey(String surveyName) { |
| assert(surveyName != null); |
| _activeSurvey = surveyName; |
| |
| if (!surveyNameExists(activeSurvey)) { |
| // Create the survey if property is non-existent in ~/.devtools |
| _addSurvey(activeSurvey); |
| } |
| } |
| |
| /// Need to rewrite the entire survey structure for property to be persisted. |
| void rewriteActiveSurvey(bool actionTaken, int shownCount) { |
| assert(activeSurvey != null); |
| properties[activeSurvey] = { |
| _surveyActionTaken: actionTaken, |
| _surveyShownCount: shownCount, |
| }; |
| } |
| |
| int get surveyShownCount { |
| assert(activeSurvey != null); |
| final prop = properties[activeSurvey]; |
| if (prop[_surveyShownCount] == null) { |
| rewriteActiveSurvey(prop[_surveyActionTaken], 0); |
| } |
| return properties[activeSurvey][_surveyShownCount]; |
| } |
| |
| void incrementSurveyShownCount() { |
| assert(activeSurvey != null); |
| surveyShownCount; // Ensure surveyShownCount has been initialized. |
| final prop = properties[activeSurvey]; |
| rewriteActiveSurvey(prop[_surveyActionTaken], prop[_surveyShownCount] + 1); |
| } |
| |
| bool get surveyActionTaken { |
| assert(activeSurvey != null); |
| return properties[activeSurvey][_surveyActionTaken] == true; |
| } |
| |
| set surveyActionTaken(bool value) { |
| assert(activeSurvey != null); |
| final prop = properties[activeSurvey]; |
| rewriteActiveSurvey(value, prop[_surveyShownCount]); |
| } |
| } |
| |
| abstract class PersistentProperties { |
| PersistentProperties(this.name); |
| |
| final String name; |
| |
| dynamic operator [](String key); |
| |
| void operator []=(String key, dynamic value); |
| |
| /// Re-read settings from the backing store. |
| /// |
| /// May be a no-op on some platforms. |
| void syncSettings(); |
| } |
| |
| const JsonEncoder _jsonEncoder = JsonEncoder.withIndent(' '); |
| |
| class IOPersistentProperties extends PersistentProperties { |
| IOPersistentProperties( |
| String name, { |
| String documentDirPath, |
| }) : super(name) { |
| final String fileName = name.replaceAll(' ', '_'); |
| documentDirPath ??= LocalFileSystem.devToolsDir(); |
| _file = File(path.join(documentDirPath, fileName)); |
| if (!_file.existsSync()) { |
| _file.createSync(recursive: true); |
| } |
| syncSettings(); |
| } |
| |
| IOPersistentProperties.fromFile(File file) : super(path.basename(file.path)) { |
| _file = file; |
| if (!_file.existsSync()) { |
| _file.createSync(recursive: true); |
| } |
| syncSettings(); |
| } |
| |
| File _file; |
| |
| Map _map; |
| |
| @override |
| dynamic operator [](String key) => _map[key]; |
| |
| @override |
| void operator []=(String key, dynamic value) { |
| if (value == null && !_map.containsKey(key)) return; |
| if (_map[key] == value) return; |
| |
| if (value == null) { |
| _map.remove(key); |
| } else { |
| _map[key] = value; |
| } |
| |
| try { |
| _file.writeAsStringSync(_jsonEncoder.convert(_map) + '\n'); |
| } catch (_) {} |
| } |
| |
| @override |
| void syncSettings() { |
| try { |
| String contents = _file.readAsStringSync(); |
| if (contents.isEmpty) contents = '{}'; |
| _map = jsonDecode(contents); |
| } catch (_) { |
| _map = {}; |
| } |
| } |
| |
| void remove(String propertyName) { |
| _map.remove(propertyName); |
| } |
| } |