blob: 6363a576ef427d71dc59d80995c64cec3889f198 [file] [log] [blame]
// Copyright 2021 The Fuchsia 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' show json;
import 'dart:io';
import 'dart:ui';
import 'package:ermine_utils/ermine_utils.dart';
import 'package:flutter/services.dart';
import 'package:fuchsia_logger/logger.dart';
import 'package:fuchsia_scenic_flutter/fuchsia_view.dart';
import 'package:fuchsia_services/services.dart';
import 'package:internationalization/strings.dart';
import 'package:mobx/mobx.dart';
import 'package:login/src/services/channel_service.dart';
import 'package:login/src/services/device_service.dart';
import 'package:login/src/services/privacy_consent_service.dart';
import 'package:login/src/services/shell_service.dart';
import 'package:login/src/services/ssh_keys_service.dart';
import 'package:login/src/states/oobe_state.dart';
/// Defines an implementation of [OobeState].
class OobeStateImpl with Disposable implements OobeState {
static const kDefaultConfigJson = '/config/data/startup_config.json';
static const kStartupConfigJson = '/data/startup_config.json';
final ComponentContext componentContext;
final ChannelService channelService;
final DeviceService deviceService;
final SshKeysService sshKeysService;
final ShellService shellService;
final PrivacyConsentService privacyConsentService;
OobeStateImpl({
required this.deviceService,
required this.shellService,
required this.channelService,
required this.sshKeysService,
required this.privacyConsentService,
}) : componentContext = ComponentContext.create(),
_localeStream = channelService.stream.asObservable() {
privacyPolicy = privacyConsentService.privacyPolicy;
channelService.onConnected = (connected) => runInAction(() async {
if (connected) {
channels
..clear()
..addAll(await channelService.channels);
_currentChannel.value = await channelService.currentChannel;
}
_updateChannelsAvailable.value = connected;
});
shellService.advertise(componentContext.outgoing);
componentContext.outgoing.serveFromStartupInfo();
// We cannot load MaterialIcons font file from pubspec.yaml. So load it
// explicitly.
File file = File('/pkg/data/MaterialIcons-Regular.otf');
if (file.existsSync()) {
FontLoader('MaterialIcons')
..addFont(() async {
final bytes = await file.readAsBytes();
return bytes.buffer.asByteData();
}())
..load();
}
}
@override
void dispose() {
super.dispose();
channelService.dispose();
privacyConsentService.dispose();
sshKeysService.dispose();
shellService.dispose();
}
@override
bool get launchOobe => _launchOobe.value;
set launchOobe(bool value) => runInAction(() => _launchOobe.value = value);
final Observable<bool> _launchOobe = Observable<bool>(() {
File config = File(kStartupConfigJson);
// If startup config does not exist, open the default config.
if (!config.existsSync()) {
config = File(kDefaultConfigJson);
}
// If default config is missing, log error and return defaults.
if (!config.existsSync()) {
log.severe('Missing startup and default configs. Skipping OOBE.');
return false;
}
final data = json.decode(config.readAsStringSync()) ?? {};
return data['launch_oobe'] == true;
}());
@override
bool get loginDone => _loginDone.value;
final _loginDone = true.asObservable();
@override
Locale? get locale => _localeStream.value;
final ObservableStream<Locale> _localeStream;
FuchsiaViewConnection? _ermineViewConnection;
@override
FuchsiaViewConnection get ermineViewConnection =>
_ermineViewConnection ??= shellService.launchErmineShell();
@override
OobeScreen get screen => _screen.value;
final Observable<OobeScreen> _screen = OobeScreen.channel.asObservable();
@override
bool get updateChannelsAvailable => _updateChannelsAvailable.value;
final Observable<bool> _updateChannelsAvailable = false.asObservable();
@override
final ObservableList<String> channels = ObservableList.of([]);
@override
String get currentChannel => _currentChannel.value;
final Observable<String> _currentChannel = ''.asObservable();
@override
final channelDescriptions = ChannelService.descriptions;
@override
SshScreen get sshScreen => _sshScreen.value;
final Observable<SshScreen> _sshScreen = SshScreen.add.asObservable();
@override
String get sshKeyTitle => _sshKeyTitle.value;
late final _sshKeyTitle = (() {
switch (sshScreen) {
case SshScreen.add:
return Strings.oobeSshKeysAddTitle;
case SshScreen.confirm:
return Strings.oobeSshKeysConfirmTitle;
case SshScreen.error:
return Strings.oobeSshKeysErrorTitle;
case SshScreen.exit:
return Strings.oobeSshKeysSuccessTitle;
}
}).asComputed();
@override
String get sshKeyDescription => _sshKeyDescription.value;
late final _sshKeyDescription = (() {
switch (sshScreen) {
case SshScreen.add:
return Strings.oobeSshKeysAddDesc;
case SshScreen.confirm:
return Strings.oobeSshKeysSelectionDesc(sshKeys.length);
case SshScreen.error:
return errorMessage;
case SshScreen.exit:
return Strings.oobeSshKeysSuccessDesc;
}
}).asComputed();
@override
SshImport get importMethod => _importMethod.value;
final Observable<SshImport> _importMethod = SshImport.github.asObservable();
@override
final List<String> sshKeys = ObservableList<String>();
@override
int get sshKeyIndex => _sshKeyIndex.value;
@override
set sshKeyIndex(int value) => runInAction(() => _sshKeyIndex.value = value);
final _sshKeyIndex = 0.asObservable();
@override
bool get privacyVisible => _privacyVisible.value;
final Observable<bool> _privacyVisible = false.asObservable();
@override
late final String privacyPolicy;
@override
void setCurrentChannel(String channel) => runInAction(() async {
await channelService.setCurrentChannel(channel);
_currentChannel.value = await channelService.currentChannel;
});
@override
void prevScreen() => runInAction(() {
if (screen.index > 0) {
_screen.value = OobeScreen.values[screen.index - 1];
}
});
@override
void nextScreen() => runInAction(() {
if (screen.index + 1 <= OobeScreen.done.index) {
_screen.value = OobeScreen.values[screen.index + 1];
}
});
@override
void agree() => runInAction(() {
privacyConsentService.setConsent(consent: true);
nextScreen();
});
@override
void disagree() => runInAction(() {
privacyConsentService.setConsent(consent: false);
nextScreen();
});
@override
void showPrivacy() => runInAction(() => _privacyVisible.value = true);
@override
void hidePrivacy() => runInAction(() => _privacyVisible.value = false);
@override
void sshImportMethod(SshImport? method) =>
runInAction(() => _importMethod.value = method!);
@override
void sshBackScreen() => runInAction(() {
if (sshScreen == SshScreen.add) {
prevScreen();
} else {
_sshScreen.value = SshScreen.add;
}
});
String errorMessage = '';
@override
void sshAdd(String userNameOrKey) => runInAction(() {
if (sshScreen == SshScreen.add) {
if (importMethod == SshImport.github) {
sshKeysService.fetchKeys(userNameOrKey).then((keys) {
if (keys.isEmpty) {
errorMessage =
Strings.oobeSshKeysGithubErrorDesc(userNameOrKey);
_sshScreen.value = SshScreen.error;
} else {
sshKeys
..clear()
..addAll(keys);
_sshScreen.value = SshScreen.confirm;
}
}).catchError((e) {
errorMessage = e.message;
_sshScreen.value = SshScreen.error;
});
} else {
// Add it manually.
_saveKey(userNameOrKey);
}
} else if (sshScreen == SshScreen.confirm) {
if (sshKeys.isNotEmpty == true) {
final selectedKey = sshKeys.elementAt(sshKeyIndex);
_saveKey(selectedKey);
}
}
});
void _saveKey(String key) {
sshKeysService
.addKey(key)
.then((_) => runInAction(() {
_sshScreen.value = SshScreen.exit;
}))
.catchError((e) {
errorMessage = Strings.oobeSshKeysFidlErrorDesc;
_sshScreen.value = SshScreen.error;
});
}
@override
void skip() => runInAction(() => _sshScreen.value = SshScreen.exit);
@override
void finish() => runInAction(() {
// Dismiss OOBE UX.
launchOobe = false;
// Mark login step as done.
_loginDone.value = true;
// Persistently record OOBE done.
File(kStartupConfigJson).writeAsStringSync('{"launch_oobe":false}');
// Clean up.
dispose();
});
@override
// TODO(http://fxb/81598): Implement create password functionality.
void setPassword(String password) => nextScreen();
@override
// TODO(http://fxb/81598): Implement login functionality.
void login(String password) => finish();
@override
void shutdown() => deviceService.shutdown();
}