blob: 0023f92508fb5e3c15e0ea6f5228104d3405e168 [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: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 {
FlutterUsage({String settingsName = 'flutter'}) {
_analytics = AnalyticsIO('', settingsName, '');
}
late 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 (~/.flutter-devtools/.devtools).
class DevToolsUsage {
DevToolsUsage() {
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;
late IOPersistentProperties properties;
static const _surveyActionTaken = 'surveyActionTaken';
static const _surveyShownCount = 'surveyShownCount';
void reset() {
// TODO(kenz): remove this in Feb 2022. See
// https://github.com/flutter/devtools/issues/3264. The `firstRun` property
// has been replaced by `isFirstRun`. This is to force all users to answer
// the analytics dialog again. The 'enabled' property has been replaced by
// 'analyticsEnabled' for better naming.
properties.remove('firstRun');
properties.remove('enabled');
properties.remove('firstDevToolsRun');
properties['analyticsEnabled'] = false;
}
bool get isFirstRun {
// TODO(kenz): remove this in Feb 2022. See
// https://github.com/flutter/devtools/issues/3264.The `firstRun` property
// has been replaced by `isFirstRun`. This is to force all users to answer
// the analytics dialog again.
properties.remove('firstRun');
return properties['isFirstRun'] = properties['isFirstRun'] == null;
}
bool get analyticsEnabled {
// TODO(kenz): remove this in Feb 2022. See
// https://github.com/flutter/devtools/issues/3264. The `enabled` property
// has been replaced by `analyticsEnabled` for better naming.
if (properties['enabled'] != null) {
properties['analyticsEnabled'] = properties['enabled'];
properties.remove('enabled');
}
return properties['analyticsEnabled'] =
properties['analyticsEnabled'] == true;
}
set analyticsEnabled(bool value) {
properties['analyticsEnabled'] = value;
}
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,
};
}
/// The active survey in [properties], as a [_ActiveSurveyJson].
_ActiveSurveyJson get _activeSurveyFromProperties => _ActiveSurveyJson(
(properties[activeSurvey!] as Map).cast<String, Object?>(),
);
int get surveyShownCount {
assert(activeSurvey != null);
final prop = _activeSurveyFromProperties;
if (prop.surveyShownCount == null) {
rewriteActiveSurvey(prop.surveyActionTaken, 0);
}
return _activeSurveyFromProperties.surveyShownCount!;
}
void incrementSurveyShownCount() {
assert(activeSurvey != null);
surveyShownCount; // Ensure surveyShownCount has been initialized.
final prop = _activeSurveyFromProperties;
rewriteActiveSurvey(
prop.surveyActionTaken,
prop.surveyShownCount! + 1,
);
}
bool get surveyActionTaken {
return _activeSurveyFromProperties.surveyActionTaken;
}
set surveyActionTaken(bool value) {
rewriteActiveSurvey(
value,
_activeSurveyFromProperties.surveyShownCount!,
);
}
String get lastReleaseNotesVersion {
return (properties['lastReleaseNotesVersion'] ??= '') as String;
}
set lastReleaseNotesVersion(String value) {
properties['lastReleaseNotesVersion'] = value;
}
}
extension type _ActiveSurveyJson(Map<String, Object?> json) {
bool get surveyActionTaken => json[DevToolsUsage._surveyActionTaken] as bool;
int? get surveyShownCount => json[DevToolsUsage._surveyShownCount] as int?;
}
abstract class PersistentProperties {
PersistentProperties(this.name);
final String name;
// ignore: avoid-dynamic, dynamic by design.
dynamic operator [](String key);
// ignore: avoid-dynamic, dynamic by design.
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();
}
late File _file;
late Map<String, Object?> _map;
@override
// ignore: avoid-dynamic, necessary here.
dynamic operator [](String key) => _map[key];
@override
void operator []=(String key, Object? 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) as Map).cast<String, Object>();
} catch (_) {
_map = {};
}
}
void remove(String propertyName) {
_map.remove(propertyName);
}
}