[release] Snap to 06c59d58e7
Change-Id: I406ffade91cfa1b0ba0f0a2b9391036d3b761bb7
diff --git a/BUILD.gn b/BUILD.gn
index c1f6b09..e02ed53 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -16,24 +16,13 @@
group("tests") {
testonly = true
- deps = [
- ":dart_target_tests",
- ":dart_unittests",
- "session_shells/ermine/session:tests",
- ]
-}
-
-# Dart tests which can run on target
-group("dart_target_tests") {
- testonly = true
- deps = [ "bin:dart_target_tests" ]
+ deps = [ ":dart_unittests" ]
}
# Dart unit tests which can run on the host or target
group("dart_unittests") {
testonly = true
deps = [
- "bin:dart_unittests",
"examples:dart_unittests",
"lib:dart_unittests",
"session_shells:dart_unittests",
diff --git a/bin/BUILD.gn b/bin/BUILD.gn
index 50f0002..fba027a 100644
--- a/bin/BUILD.gn
+++ b/bin/BUILD.gn
@@ -3,27 +3,5 @@
# found in the LICENSE file.
group("bin") {
- deps = [
- "settings:settings",
- "simple_browser:simple-browser",
- ]
-}
-
-group("dart_target_tests") {
- testonly = true
-
- deps = [ "simple_browser/target_test:simple-browser-target-test" ]
-}
-
-group("dart_unittests") {
- testonly = true
-
- # TODO(fxb/41505): Temporarily disable flutter_tester tests on mac hosts.
- _flutter_tester_tests = []
- if (host_os != "mac") {
- _flutter_tester_tests +=
- [ "simple_browser:simple_browser_unittests($host_toolchain)" ]
- }
-
- deps = _flutter_tester_tests
+ deps = [ "settings:settings" ]
}
diff --git a/bin/simple_browser/BUILD.gn b/bin/simple_browser/BUILD.gn
deleted file mode 100644
index 47a1f9b..0000000
--- a/bin/simple_browser/BUILD.gn
+++ /dev/null
@@ -1,123 +0,0 @@
-# Copyright 2019 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("//build/components.gni")
-import("//build/dart/dart_library.gni")
-import("//build/fidl/fidl.gni")
-import("//build/flutter/flutter_component.gni")
-import("//build/flutter/test.gni")
-import("//build/testing/environments.gni")
-import("//build/testing/flutter_driver.gni")
-
-dart_library("lib") {
- package_name = "simple_browser"
- null_safe = true
-
- sources = [
- "app.dart",
- "create_web_context.dart",
- "main.dart",
- "src/blocs/tabs_bloc.dart",
- "src/blocs/webpage_bloc.dart",
- "src/models/app_model.dart",
- "src/models/tabs_action.dart",
- "src/models/webpage_action.dart",
- "src/services/simple_browser_navigation_event_listener.dart",
- "src/services/simple_browser_web_service.dart",
- "src/utils/browser_shortcuts.dart",
- "src/utils/sanitize_url.dart",
- "src/utils/tld_checker.dart",
- "src/utils/tlds_provider.dart",
- "src/utils/valid_tlds.dart",
- "src/widgets/error_page.dart",
- "src/widgets/history_buttons.dart",
- "src/widgets/navigation_bar.dart",
- "src/widgets/navigation_field.dart",
- "src/widgets/tabs_widget.dart",
- "test_main.dart",
- ]
-
- deps = [
- "//sdk/dart/fidl",
- "//sdk/dart/fuchsia_internationalization_flutter",
- "//sdk/dart/fuchsia_logger",
- "//sdk/dart/fuchsia_scenic",
- "//sdk/dart/fuchsia_scenic_flutter",
- "//sdk/dart/fuchsia_services",
- "//sdk/dart/zircon",
- "//sdk/fidl/fuchsia.intl",
- "//sdk/fidl/fuchsia.io",
- "//sdk/fidl/fuchsia.ui.shortcut",
- "//sdk/fidl/fuchsia.ui.views",
- "//sdk/fidl/fuchsia.web",
- "//src/experiences/bin/simple_browser_internationalization:internationalization",
- "//src/experiences/session_shells/ermine/keyboard_shortcuts",
- "//third_party/dart-pkg/git/flutter/packages/flutter",
- "//third_party/dart-pkg/git/flutter/packages/flutter_driver",
- "//third_party/dart-pkg/git/flutter/packages/flutter_localizations",
- "//third_party/dart-pkg/git/flutter/packages/flutter_test",
- "//third_party/dart-pkg/pub/html_unescape",
- "//third_party/dart-pkg/pub/http",
- "//third_party/dart-pkg/pub/intl",
- "//third_party/dart-pkg/pub/meta",
- ]
-}
-
-resource("keyboard-shortcuts") {
- sources = [ "config/keyboard_shortcuts.json" ]
- outputs = [ "data/keyboard_shortcuts.json" ]
-}
-
-resource("material-icons") {
- sources = [ "//prebuilt/third_party/dart/${host_platform}/bin/resources/devtools/assets/fonts/MaterialIcons-Regular.otf" ]
- outputs = [ "data/MaterialIcons-Regular.otf" ]
-}
-
-flutter_component("component") {
- if (flutter_driver_enabled) {
- main_dart = "test_main.dart"
- } else {
- main_dart = "main.dart"
- }
- component_name = "simple-browser"
- manifest = "meta/simple_browser.cmx"
- deps = [
- ":keyboard-shortcuts",
- ":lib",
- ":material-icons",
- ]
-}
-
-fuchsia_package("simple-browser") {
- deps = [ ":component" ]
-}
-
-# fx test simple_browser_unittests
-flutter_test("simple_browser_unittests") {
- null_safe = true
- sources = [
- "browser_shortcuts_test.dart",
- "sanitize_url_test.dart",
- "simple_browser_test.dart",
- "tabs_bloc_test.dart",
- "tld_checker_test.dart",
- "webpage_bloc_test.dart",
- "widgets/error_page_test.dart",
- "widgets/history_buttons_test.dart",
- "widgets/navigation_bar_test.dart",
- "widgets/navigation_field_test.dart",
- "widgets/tabs_widget_test.dart",
- ]
-
- deps = [
- ":lib",
- "//sdk/dart/fuchsia_logger",
- "//sdk/fidl/fuchsia.ui.shortcut",
- "//sdk/fidl/fuchsia.web",
- "//third_party/dart-pkg/git/flutter/packages/flutter",
- "//third_party/dart-pkg/git/flutter/packages/flutter_test",
- "//third_party/dart-pkg/pub/mockito",
- "//third_party/dart-pkg/pub/test",
- ]
-}
diff --git a/bin/simple_browser/OWNERS b/bin/simple_browser/OWNERS
deleted file mode 100644
index 9de9b92..0000000
--- a/bin/simple_browser/OWNERS
+++ /dev/null
@@ -1 +0,0 @@
-yhlee@google.com
diff --git a/bin/simple_browser/README.md b/bin/simple_browser/README.md
deleted file mode 100644
index a8d7b11..0000000
--- a/bin/simple_browser/README.md
+++ /dev/null
@@ -1,12 +0,0 @@
-# Browser
-
-Minimal traditional web browser with navigation and a text field for
-entering the URL.
-
-## Testing
-
-To run unit tests
-```
-fx set workstation.<BOARD> --with //src/experiences:dart_unittests
-fx run-host-tests simple_browser_unittests
-```
diff --git a/bin/simple_browser/config/keyboard_shortcuts.json b/bin/simple_browser/config/keyboard_shortcuts.json
deleted file mode 100644
index 7809396..0000000
--- a/bin/simple_browser/config/keyboard_shortcuts.json
+++ /dev/null
@@ -1,67 +0,0 @@
-{
- "closeTab": [
- {
- "char": "w",
- "chord": "Control + w",
- "description": "Close the current tab",
- "modifier": "control"
- }
- ],
- "focusField": [
- {
- "char": "l",
- "chord": "Control + l",
- "description": "Place the focus on the Address Bar",
- "modifier": "control",
- "trigger": "pressAndRelease"
- }
- ],
- "goBack": [
- {
- "char": "left",
- "chord": "Alt + <-",
- "description": "Open the previous page from your browsing history in the current tab",
- "modifier": "alt"
- }
- ],
- "goForward": [
- {
- "char": "right",
- "chord": "Alt + ->",
- "description": "Open the next page from your browsing history in the current tab",
- "modifier": "alt"
- }
- ],
- "newTab": [
- {
- "char": "t",
- "chord": "Control + t",
- "description": "Open a new tab",
- "modifier": "control"
- }
- ],
- "nextTab": [
- {
- "char": "tab",
- "chord": "Control + tab",
- "description": "Jump to the next open tab",
- "modifier": "control"
- }
- ],
- "previousTab": [
- {
- "char": "tab",
- "chord": "Control + Shift + tab",
- "description": "Jump to the previous open tab",
- "modifier": "control + shift"
- }
- ],
- "refresh": [
- {
- "char": "r",
- "chord": "Control + r",
- "description": "Reload the current page",
- "modifier": "control"
- }
- ]
-}
diff --git a/bin/simple_browser/lib/app.dart b/bin/simple_browser/lib/app.dart
deleted file mode 100644
index e111840..0000000
--- a/bin/simple_browser/lib/app.dart
+++ /dev/null
@@ -1,137 +0,0 @@
-// Copyright 2019 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:ui';
-
-import 'package:flutter/material.dart';
-import 'package:flutter_localizations/flutter_localizations.dart';
-import 'package:fuchsia_scenic_flutter/fuchsia_view.dart' show FuchsiaView;
-import 'package:internationalization/localizations_delegate.dart'
- as localizations;
-import 'package:internationalization/strings.dart';
-import 'package:internationalization/supported_locales.dart'
- as supported_locales;
-import 'package:intl/intl.dart';
-import 'src/blocs/webpage_bloc.dart';
-import 'src/models/app_model.dart';
-import 'src/widgets/error_page.dart';
-import 'src/widgets/navigation_bar.dart';
-import 'src/widgets/tabs_widget.dart';
-
-// TODO(fxb/45264): Replace these with colors from the central Ermine styles.
-const _kErmineColor100 = Color(0xFFE5E5E5);
-const _kErmineColor200 = Color(0xFFBDBDBD);
-const _kErmineColor300 = Color(0xFF282828);
-const _kErmineColor400 = Color(0xFF0C0C0C);
-
-const _kTextStyle = TextStyle(color: _kErmineColor400, fontSize: 14.0);
-
-class App extends StatelessWidget {
- final AppModel model;
-
- const App(this.model);
-
- @override
- Widget build(BuildContext context) {
- return FutureBuilder(
- future: model.localeStream.first,
- builder: (context, snapshot) => snapshot.hasData
- ? StreamBuilder<Locale>(
- stream: model.localeStream,
- initialData: snapshot.data as Locale,
- builder: (context, snapshot) {
- final locale = snapshot.data;
- Intl.defaultLocale = locale.toString();
- return MaterialApp(
- title: Strings.browser,
- theme: ThemeData(
- colorScheme: ColorScheme.light(
- background: _kErmineColor100,
- onBackground: _kErmineColor400,
- primary: _kErmineColor100,
- primaryVariant: _kErmineColor200,
- onPrimary: _kErmineColor400,
- secondary: _kErmineColor400,
- secondaryVariant: _kErmineColor300,
- onSecondary: _kErmineColor100,
- surface: _kErmineColor100,
- onSurface: _kErmineColor400,
- ),
- fontFamily: 'RobotoMono',
- textSelectionTheme: TextSelectionThemeData(
- selectionColor: _kErmineColor200,
- cursorColor: _kErmineColor400,
- selectionHandleColor: _kErmineColor400,
- ),
- textTheme: TextTheme(
- bodyText2: _kTextStyle,
- subtitle1: _kTextStyle,
- ),
- ),
- locale: locale,
- localizationsDelegates: [
- localizations.delegate(),
- GlobalMaterialLocalizations.delegate,
- GlobalWidgetsLocalizations.delegate,
- ],
- supportedLocales: supported_locales.locales,
- scrollBehavior: MaterialScrollBehavior().copyWith(
- dragDevices: {
- PointerDeviceKind.mouse,
- PointerDeviceKind.touch
- },
- ),
- home: Scaffold(
- body: Column(
- children: <Widget>[
- AnimatedBuilder(
- animation: model.tabsBloc.currentTabNotifier,
- builder: (_, __) => BrowserNavigationBar(
- bloc: model.tabsBloc.currentTab,
- newTab: model.newTab,
- fieldFocus: model.fieldFocus,
- ),
- ),
- TabsWidget(bloc: model.tabsBloc),
- Expanded(child: _buildContent()),
- ],
- ),
- ),
- );
- },
- )
- : Offstage(),
- );
- }
-
- Widget _buildContent() => AnimatedBuilder(
- animation: model.tabsBloc.currentTabNotifier,
- builder: (_, __) => model.tabsBloc.currentTab == null
- // hide if no tab selected
- ? _buildEmptyPage()
- : AnimatedBuilder(
- animation: model.tabsBloc.currentTab!.pageTypeNotifier,
- builder: (_, __) =>
- _buildPage(model.tabsBloc.currentTab!.pageType),
- ),
- );
-
- Widget _buildPage(PageType pageType) {
- switch (pageType) {
- case PageType.normal:
- return _buildNormalPage();
- case PageType.error:
- // show error for error state
- return _buildErrorPage();
- default:
- // hide if no content in tab
- return _buildEmptyPage();
- }
- }
-
- Widget _buildNormalPage() =>
- FuchsiaView(controller: model.tabsBloc.currentTab!.fuchsiaViewConnection);
- Widget _buildErrorPage() => ErrorPage();
- Widget _buildEmptyPage() => Container();
-}
diff --git a/bin/simple_browser/lib/create_web_context.dart b/bin/simple_browser/lib/create_web_context.dart
deleted file mode 100644
index 2154f9d..0000000
--- a/bin/simple_browser/lib/create_web_context.dart
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright 2019 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 'package:fidl/fidl.dart' show InterfaceHandle;
-import 'package:fidl_fuchsia_io/fidl_async.dart' as fidl_io;
-import 'package:fidl_fuchsia_web/fidl_async.dart' as web;
-import 'package:fuchsia_services/services.dart' show Incoming;
-import 'package:zircon/zircon.dart';
-
-/// Creates a web context for creating new web frames
-web.ContextProxy createWebContext() {
- final context = web.ContextProxy();
- final contextProvider = web.ContextProviderProxy();
- final contextProviderProxyRequest = contextProvider.ctrl.request();
- Incoming.fromSvcPath()
- ..connectToServiceByNameWithChannel(contextProvider.ctrl.$serviceName,
- contextProviderProxyRequest.passChannel())
- ..close();
- final channel = Channel.fromFile('/svc');
- final webFeatures = web.ContextFeatureFlags.network |
- web.ContextFeatureFlags.audio |
- web.ContextFeatureFlags.hardwareVideoDecoder |
- web.ContextFeatureFlags.keyboard |
- web.ContextFeatureFlags.vulkan;
- final web.CreateContextParams params = web.CreateContextParams(
- serviceDirectory: InterfaceHandle<fidl_io.Directory>(channel),
- features: webFeatures);
- contextProvider.create(params, context.ctrl.request());
- contextProvider.ctrl.close();
-
- return context;
-}
diff --git a/bin/simple_browser/lib/main.dart b/bin/simple_browser/lib/main.dart
deleted file mode 100644
index b48f479..0000000
--- a/bin/simple_browser/lib/main.dart
+++ /dev/null
@@ -1,91 +0,0 @@
-// Copyright 2019 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.
-
-// ignore_for_file: unused_import
-import 'dart:io';
-
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:fuchsia_logger/logger.dart';
-import 'package:fuchsia_scenic_flutter/fuchsia_view.dart';
-import 'package:fuchsia_services/services.dart';
-
-import 'app.dart';
-import 'create_web_context.dart';
-import 'src/blocs/tabs_bloc.dart';
-import 'src/blocs/webpage_bloc.dart';
-import 'src/models/app_model.dart';
-import 'src/models/tabs_action.dart';
-import 'src/models/webpage_action.dart';
-import 'src/services/simple_browser_web_service.dart';
-import 'src/utils/tld_checker.dart';
-
-final _handler = MethodChannel('flutter_driver/handler');
-
-void main() {
- setupLogger(name: 'simple_browser');
- final _context = createWebContext();
- TldChecker().prefetchTlds();
- ComponentContext.createAndServe();
-
- // Loads MaterialIcons-Regular.otf
- File file = File('/pkg/data/MaterialIcons-Regular.otf');
- if (file.existsSync()) {
- FontLoader('MaterialIcons')
- ..addFont(() async {
- return file.readAsBytesSync().buffer.asByteData();
- }())
- ..load();
- }
-
- // Binds |tabsBloc| here so that it can be referenced in the TabsBloc
- // constructor arguments.
- late TabsBloc tabsBloc;
-
- tabsBloc = TabsBloc(
- tabFactory: () {
- SimpleBrowserWebService webService = SimpleBrowserWebService(
- context: _context,
- popupHandler: (tab) => tabsBloc.request.add(
- AddTabAction(tab: tab),
- ),
- onLoaded: () => tabsBloc.currentTab!.request.add(SetFocusAction()),
- );
-
- // Enables the web console log only for debug.
- assert(() {
- webService.enableConsoleLog();
- return true;
- }());
-
- return WebPageBloc(
- webService: webService,
- );
- },
- disposeTab: (tab) {
- tab.dispose();
- },
- );
-
- tabsBloc.request.add(NewTabAction());
-
- final appModel = AppModel.fromStartupContext(
- tabsBloc: tabsBloc,
- );
-
- runApp(App(appModel));
-
- // Moves focus to the webview whenever the browser gets focused.
- FocusState.instance.stream().listen(appModel.onFocus);
-
- // This call is used only when flutter driver is enabled.
- if (TestDefaultBinaryMessengerBinding.instance != null) {
- _handler.setMockMethodCallHandler((call) async {
- final url = call.method;
- tabsBloc.currentTab!.request.add(NavigateToAction(url: url));
- log.info('Navigate to $url...');
- });
- }
-}
diff --git a/bin/simple_browser/lib/src/blocs/tabs_bloc.dart b/bin/simple_browser/lib/src/blocs/tabs_bloc.dart
deleted file mode 100644
index 7768780..0000000
--- a/bin/simple_browser/lib/src/blocs/tabs_bloc.dart
+++ /dev/null
@@ -1,132 +0,0 @@
-// Copyright 2019 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:async';
-import 'dart:collection';
-import 'package:flutter/foundation.dart';
-import '../blocs/webpage_bloc.dart';
-import '../models/tabs_action.dart';
-
-// Business logic for browser tabs.
-// Sinks:
-// TabsAction: a tabs action - open new tab, focus tab, etc.
-// Value Notifiers:
-// Tabs: the list of open tabs.
-// CurrentTab: the currently focused tab.
-class TabsBloc {
- final _tabsList = <WebPageBloc>[];
- final WebPageBloc Function() tabFactory;
- final void Function(WebPageBloc tab) disposeTab;
-
- // Value Notifiers
- final ValueNotifier<UnmodifiableListView<WebPageBloc>> _tabs =
- ValueNotifier<UnmodifiableListView<WebPageBloc>>(
- UnmodifiableListView(<WebPageBloc>[]));
- final ValueNotifier<WebPageBloc?> _currentTab =
- ValueNotifier<WebPageBloc?>(null);
-
- ChangeNotifier get tabsNotifier => _tabs;
- ChangeNotifier get currentTabNotifier => _currentTab;
-
- UnmodifiableListView<WebPageBloc> get tabs => _tabs.value;
- WebPageBloc? get currentTab => _currentTab.value;
- int? get currentTabIdx =>
- (currentTab != null) ? _tabsList.indexOf(currentTab!) : null;
- bool get isOnlyTab => _tabsList.length == 1;
-
- WebPageBloc? get previousTab {
- final idx = currentTabIdx;
- if (idx == null) {
- return null;
- }
- int prevIdx = idx - 1;
- prevIdx = (prevIdx < 0) ? (_tabsList.length - 1) : prevIdx;
- return _tabsList[prevIdx];
- }
-
- WebPageBloc? get nextTab {
- final idx = currentTabIdx;
- if (idx == null) {
- return null;
- }
- int nextIdx = idx + 1;
- nextIdx = (nextIdx > _tabsList.length - 1) ? 0 : nextIdx;
- return _tabsList[nextIdx];
- }
-
- // Sinks
- final _tabsActionController = StreamController<TabsAction>();
- Sink<TabsAction> get request => _tabsActionController.sink;
-
- TabsBloc({required this.tabFactory, required this.disposeTab}) {
- _tabsActionController.stream.listen(_onTabsActionChanged);
- }
-
- void dispose() {
- _tabsList.forEach(disposeTab);
- _tabsActionController.close();
- }
-
- void _onTabsActionChanged(TabsAction action) {
- switch (action.op) {
- case TabsActionType.newTab:
- final tab = tabFactory();
- _tabsList.add(tab);
- _tabs.value = UnmodifiableListView<WebPageBloc>(_tabsList);
- _currentTab.value = tab;
- break;
- case TabsActionType.focusTab:
- final focusTab = action as FocusTabAction;
- _currentTab.value = focusTab.tab;
- break;
- case TabsActionType.removeTab:
- if (tabs.isEmpty) {
- break;
- }
-
- final removeTab = action as RemoveTabAction;
- final tab = removeTab.tab;
-
- if (tabs.length == 1) {
- _currentTab.value = null;
- } else if (currentTab == removeTab.tab) {
- final indexOfRemoved = _tabsList.indexOf(tab);
- final indexOfNewTab =
- indexOfRemoved == 0 ? indexOfRemoved + 1 : indexOfRemoved - 1;
- _currentTab.value = _tabsList.elementAt(indexOfNewTab);
- }
-
- _tabsList.remove(tab);
- _tabs.value = UnmodifiableListView<WebPageBloc>(_tabsList);
- disposeTab(tab);
- break;
- case TabsActionType.addTab:
- final addTabAction = action as AddTabAction;
- _tabsList.add(addTabAction.tab);
- _tabs.value = UnmodifiableListView<WebPageBloc>(_tabsList);
- _currentTab.value = addTabAction.tab;
- break;
- case TabsActionType.rearrangeTabs:
- final rearrangeTabs = action as RearrangeTabsAction;
- final originalIndex = rearrangeTabs.originalIndex;
- final newIndex = rearrangeTabs.newIndex;
-
- final movingTab = _tabsList.elementAt(originalIndex);
- // the tab moves to the left.
- if (originalIndex > newIndex) {
- _tabsList
- ..removeAt(originalIndex)
- ..insert(newIndex, movingTab);
- }
- // the tab moves to the right.
- else {
- _tabsList
- ..insert(newIndex + 1, movingTab)
- ..removeAt(originalIndex);
- }
- _tabs.value = UnmodifiableListView<WebPageBloc>(_tabsList);
- break;
- }
- }
-}
diff --git a/bin/simple_browser/lib/src/blocs/webpage_bloc.dart b/bin/simple_browser/lib/src/blocs/webpage_bloc.dart
deleted file mode 100644
index 95712d5..0000000
--- a/bin/simple_browser/lib/src/blocs/webpage_bloc.dart
+++ /dev/null
@@ -1,106 +0,0 @@
-// Copyright 2019 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:async';
-import 'package:flutter/foundation.dart';
-import 'package:fuchsia_logger/logger.dart';
-import 'package:fuchsia_scenic_flutter/fuchsia_view.dart'
- show FuchsiaViewConnection;
-import '../models/webpage_action.dart';
-import '../services/simple_browser_web_service.dart';
-import '../utils/sanitize_url.dart';
-
-enum PageType { empty, normal, error }
-
-/// Business logic for the webpage.
-/// Sinks:
-/// WebPageAction: a browsing action - url request, prev/next page, etc.
-/// Value Notifiers:
-/// url: the current url.
-/// forwardState: bool indicating whether forward action is available.
-/// backState: bool indicating whether back action is available.
-/// isLoadedState: bool indicating whether main document has fully loaded.
-/// pageTitle: the current page title.
-/// pageType: the current type of the page; either normal, error, or empty.
-class WebPageBloc {
- final SimpleBrowserWebService webService;
-
- /// Used to present webpage in Flutter FuchsiaView
- FuchsiaViewConnection get fuchsiaViewConnection =>
- webService.fuchsiaViewConnection;
-
- ChangeNotifier get urlNotifier =>
- webService.navigationEventListener.urlNotifier;
- ChangeNotifier get forwardStateNotifier =>
- webService.navigationEventListener.forwardStateNotifier;
- ChangeNotifier get backStateNotifier =>
- webService.navigationEventListener.backStateNotifier;
- ChangeNotifier get isLoadedStateNotifier =>
- webService.navigationEventListener.isLoadedStateNotifier;
- ChangeNotifier get pageTitleNotifier =>
- webService.navigationEventListener.pageTitleNotifier;
- ChangeNotifier get pageTypeNotifier =>
- webService.navigationEventListener.pageTypeNotifier;
-
- String get url => webService.navigationEventListener.url;
- bool get forwardState => webService.navigationEventListener.forwardState;
- bool get backState => webService.navigationEventListener.backState;
- bool get isLoadedState => webService.navigationEventListener.isLoadedState;
- String? get pageTitle => webService.navigationEventListener.pageTitle;
- PageType get pageType => webService.navigationEventListener.pageType;
- // Sinks
- final _webPageActionController = StreamController<WebPageAction>();
- Sink<WebPageAction> get request => _webPageActionController.sink;
-
- // Constructors
-
- /// Creates a new [WebPageBloc] with a new page from [ContextProxy].
- ///
- /// A basic constructor for creating a brand-new tab.
- /// Can also be used for testing purposes and in this case,
- /// context parameter does not need to be set.
- WebPageBloc({
- required this.webService,
- String? homePage,
- }) {
- if (homePage != null) {
- _onWebPageActionChanged(NavigateToAction(url: homePage));
- }
-
- /// Begins handling action requests
- _webPageActionController.stream.listen(_onWebPageActionChanged);
- }
-
- void dispose() {
- webService.dispose();
- _webPageActionController.close();
- }
-
- Future<void> _onWebPageActionChanged(WebPageAction action) async {
- switch (action.op) {
- case WebPageActionType.navigateTo:
- final navigate = action as NavigateToAction;
- await webService.loadUrl(
- sanitizeUrl(navigate.url),
- );
- break;
- case WebPageActionType.goBack:
- await webService.goBack();
- break;
- case WebPageActionType.goForward:
- await webService.goForward();
- break;
- case WebPageActionType.refresh:
- await webService.refresh();
- break;
- case WebPageActionType.setFocus:
- try {
- await fuchsiaViewConnection.requestFocus();
- } on Exception catch (e) {
- log.warning('Failed to set focus on the current web view: $e');
- }
- break;
- }
- }
-}
diff --git a/bin/simple_browser/lib/src/models/app_model.dart b/bin/simple_browser/lib/src/models/app_model.dart
deleted file mode 100644
index d0ac5cf..0000000
--- a/bin/simple_browser/lib/src/models/app_model.dart
+++ /dev/null
@@ -1,123 +0,0 @@
-// Copyright 2019 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:async';
-import 'package:fidl_fuchsia_intl/fidl_async.dart';
-import 'package:flutter/material.dart';
-import 'package:fuchsia_internationalization_flutter/internationalization.dart';
-import 'package:fuchsia_scenic/views.dart';
-import 'package:fuchsia_services/services.dart';
-import 'package:keyboard_shortcuts/keyboard_shortcuts.dart';
-import '../blocs/tabs_bloc.dart';
-import '../models/tabs_action.dart';
-import '../models/webpage_action.dart';
-import '../utils/browser_shortcuts.dart';
-
-/// Model that handles the browser's tab and webpage states.
-///
-/// tabsBloc: A listener for the tab and web page states.
-/// localeStream: A getter for the current localization value.
-/// initialLocale: A getter for the initial localization value.
-/// keyboardShortcuts: A getter for the browser's [KeyboardShortcuts].
-class AppModel {
- final TabsBloc tabsBloc;
- Stream<Locale> _localeStream;
- late final FocusNode fieldFocus;
- late final KeyboardShortcuts? _keyboardShortcuts;
-
- AppModel({
- required this.tabsBloc,
- required Stream<Locale> localeStream,
- }) : _localeStream = localeStream {
- fieldFocus = FocusNode();
- _keyboardShortcuts =
- // TODO(https://fxbug.dev/71711): Figure out why `dart analyze`
- // complains about this.
- // ignore: argument_type_not_assignable
- BrowserShortcuts(tabsBloc: tabsBloc, actions: _shortcutActions())
- .activateShortcuts(ScenicContext.hostViewRef());
- }
-
- factory AppModel.fromStartupContext({required TabsBloc tabsBloc}) {
- final _intl = PropertyProviderProxy();
- Incoming.fromSvcPath()
- ..connectToService(_intl)
- ..close();
- final localStream = LocaleSource(_intl).stream().asBroadcastStream();
-
- return AppModel(
- tabsBloc: tabsBloc,
- localeStream: localStream,
- );
- }
-
- Stream<Locale> get localeStream => _localeStream;
-
- KeyboardShortcuts? get keyboardShortcuts => _keyboardShortcuts;
-
- void newTab() => tabsBloc.request.add(NewTabAction());
-
- Map<String, VoidCallback> _shortcutActions() {
- return {
- 'newTab': newTab,
- 'closeTab': _closeTab,
- 'goBack': _goBack,
- 'goForward': _goForward,
- 'refresh': _refresh,
- 'previousTab': _previousTab,
- 'nextTab': _nextTab,
- 'focusField': _focusField,
- };
- }
-
- void _closeTab() {
- if (tabsBloc.isOnlyTab) {
- return;
- }
- tabsBloc.request.add(RemoveTabAction(tab: tabsBloc.currentTab!));
- }
-
- void _goBack() {
- if (tabsBloc.currentTab!.backState) {
- tabsBloc.currentTab!.request.add(GoBackAction());
- return;
- }
- return;
- }
-
- void _goForward() {
- if (tabsBloc.currentTab!.forwardState) {
- tabsBloc.currentTab!.request.add(GoForwardAction());
- return;
- }
- return;
- }
-
- void _refresh() => tabsBloc.currentTab!.request.add(RefreshAction());
-
- void _previousTab() {
- if (tabsBloc.isOnlyTab || tabsBloc.previousTab == null) {
- return;
- }
- tabsBloc.request.add(FocusTabAction(tab: tabsBloc.previousTab!));
- }
-
- void _nextTab() {
- if (tabsBloc.isOnlyTab || tabsBloc.nextTab == null) {
- return;
- }
- tabsBloc.request.add(FocusTabAction(tab: tabsBloc.nextTab!));
- }
-
- void _focusField() => fieldFocus.requestFocus();
-
- // ignore: avoid_positional_boolean_parameters
- void onFocus(bool focused) {
- if (focused && tabsBloc.currentTab!.webService.isLoaded) {
- tabsBloc.currentTab!.request.add(SetFocusAction());
- }
- }
-
- // TODO: Activate/Deactivate the keyboardShortcuts depending on the browser's
- // focus state when its relevant support is ready (fxb/42185)
-}
diff --git a/bin/simple_browser/lib/src/models/tabs_action.dart b/bin/simple_browser/lib/src/models/tabs_action.dart
deleted file mode 100644
index 6643034..0000000
--- a/bin/simple_browser/lib/src/models/tabs_action.dart
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright 2019 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 '../blocs/webpage_bloc.dart';
-
-// Base class for actions handled by the tabs BLoC
-class TabsAction {
- final TabsActionType op;
- const TabsAction(this.op);
-}
-
-// Operations allowed for tab management
-enum TabsActionType { newTab, removeTab, focusTab, addTab, rearrangeTabs }
-
-// Instructs to add a new tab to tab list.
-class NewTabAction extends TabsAction {
- const NewTabAction() : super(TabsActionType.newTab);
-}
-
-// Instructs to remove a specific tab.
-class RemoveTabAction extends TabsAction {
- final WebPageBloc tab;
- const RemoveTabAction({required this.tab}) : super(TabsActionType.removeTab);
-}
-
-// Instructs to focus a specific tab.
-class FocusTabAction extends TabsAction {
- final WebPageBloc tab;
- const FocusTabAction({required this.tab}) : super(TabsActionType.focusTab);
-}
-
-// Instructs to add an existing tab to the tab list.
-class AddTabAction extends TabsAction {
- final WebPageBloc tab;
- const AddTabAction({required this.tab}) : super(TabsActionType.addTab);
-}
-
-class RearrangeTabsAction extends TabsAction {
- final int originalIndex;
- final int newIndex;
- const RearrangeTabsAction({
- required this.originalIndex,
- required this.newIndex,
- }) : super(TabsActionType.rearrangeTabs);
-}
diff --git a/bin/simple_browser/lib/src/models/webpage_action.dart b/bin/simple_browser/lib/src/models/webpage_action.dart
deleted file mode 100644
index 24e0a35..0000000
--- a/bin/simple_browser/lib/src/models/webpage_action.dart
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright 2019 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.
-
-// Base class for actions handled by the application's BLOC
-class WebPageAction {
- final WebPageActionType op;
- const WebPageAction(this.op);
-}
-
-// Operations allowed for browsing
-enum WebPageActionType { goForward, goBack, refresh, navigateTo, setFocus }
-
-// Instructs to go to the next page.
-class GoForwardAction extends WebPageAction {
- const GoForwardAction() : super(WebPageActionType.goForward);
-}
-
-// Instructs to go to the previous page.
-class GoBackAction extends WebPageAction {
- const GoBackAction() : super(WebPageActionType.goBack);
-}
-
-// Instructs to refresh the current page.
-class RefreshAction extends WebPageAction {
- const RefreshAction() : super(WebPageActionType.refresh);
-}
-
-// Instructs to navigate to some url.
-class NavigateToAction extends WebPageAction {
- final String url;
- NavigateToAction({required this.url}) : super(WebPageActionType.navigateTo);
-}
-
-// Instructs to set focus on webview.
-class SetFocusAction extends WebPageAction {
- const SetFocusAction() : super(WebPageActionType.setFocus);
-}
diff --git a/bin/simple_browser/lib/src/services/simple_browser_navigation_event_listener.dart b/bin/simple_browser/lib/src/services/simple_browser_navigation_event_listener.dart
deleted file mode 100644
index 83e3d3b..0000000
--- a/bin/simple_browser/lib/src/services/simple_browser_navigation_event_listener.dart
+++ /dev/null
@@ -1,77 +0,0 @@
-// Copyright 2019 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:async';
-import 'package:fidl_fuchsia_web/fidl_async.dart' as web;
-import 'package:flutter/foundation.dart';
-
-import 'package:fuchsia_logger/logger.dart';
-
-import '../blocs/webpage_bloc.dart';
-
-class SimpleBrowserNavigationEventListener extends web.NavigationEventListener {
- // Value Notifiers
- final _url = ValueNotifier<String>('');
- final _forwardState = ValueNotifier<bool>(false);
- final _backState = ValueNotifier<bool>(false);
- final _isLoadedState = ValueNotifier<bool>(true);
- final _pageTitle = ValueNotifier<String?>(null);
- final _pageType = ValueNotifier<PageType>(PageType.empty);
-
- ChangeNotifier get urlNotifier => _url;
- ChangeNotifier get forwardStateNotifier => _forwardState;
- ChangeNotifier get backStateNotifier => _backState;
- ChangeNotifier get isLoadedStateNotifier => _isLoadedState;
- ChangeNotifier get pageTitleNotifier => _pageTitle;
- ChangeNotifier get pageTypeNotifier => _pageType;
-
- String get url => _url.value;
- bool get forwardState => _forwardState.value;
- bool get backState => _backState.value;
- bool get isLoadedState => _isLoadedState.value;
- String? get pageTitle => _pageTitle.value;
- PageType get pageType => _pageType.value;
-
- SimpleBrowserNavigationEventListener();
-
- @override
- Future<Null> onNavigationStateChanged(web.NavigationState event) async {
- final url = event.url;
- if (url != null) {
- log.info('url loaded: $url');
- _url.value = url;
- }
- final canGoForward = event.canGoForward;
- if (canGoForward != null) {
- _forwardState.value = canGoForward;
- }
- final canGoBack = event.canGoBack;
- if (canGoBack != null) {
- _backState.value = canGoBack;
- }
- final isLoaded = event.isMainDocumentLoaded;
- if (isLoaded != null) {
- _isLoadedState.value = isLoaded;
- }
- final title = event.title;
- if (title != null) {
- _pageTitle.value = title;
- }
- final type = event.pageType;
- if (type != null) {
- _pageType.value = pageTypeForWebPageType(type);
- }
- }
-
- PageType pageTypeForWebPageType(web.PageType pageType) {
- switch (pageType) {
- case web.PageType.normal:
- return PageType.normal;
- case web.PageType.error:
- return PageType.error;
- default:
- return PageType.empty;
- }
- }
-}
diff --git a/bin/simple_browser/lib/src/services/simple_browser_web_service.dart b/bin/simple_browser/lib/src/services/simple_browser_web_service.dart
deleted file mode 100644
index 14d89c8..0000000
--- a/bin/simple_browser/lib/src/services/simple_browser_web_service.dart
+++ /dev/null
@@ -1,138 +0,0 @@
-// Copyright 2019 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:async';
-import 'package:fidl/fidl.dart' show InterfaceHandle;
-import 'package:fidl_fuchsia_ui_views/fidl_async.dart' as views;
-import 'package:fidl_fuchsia_web/fidl_async.dart' as web;
-import 'package:fuchsia_scenic/views.dart';
-import 'package:fuchsia_scenic_flutter/fuchsia_view.dart'
- show FuchsiaViewConnection;
-import 'package:zircon/zircon.dart';
-
-import '../blocs/webpage_bloc.dart';
-import 'simple_browser_navigation_event_listener.dart';
-
-class SimpleBrowserWebService {
- final web.FrameProxy _frame;
- final _navigationController = web.NavigationControllerProxy();
- final _navigationEventObserverBinding = web.NavigationEventListenerBinding();
- final _popupFrameCreationObserverBinding =
- web.PopupFrameCreationListenerBinding();
- final _simpleBrowserNavigationEventListener =
- SimpleBrowserNavigationEventListener();
-
- /// Used to present webpage in Flutter FuchsiaView
- late FuchsiaViewConnection _fuchsiaViewConnection;
- bool _rendered = false;
-
- FuchsiaViewConnection get fuchsiaViewConnection => _fuchsiaViewConnection;
- late views.ViewHolderToken _viewHolderToken;
- bool get isLoaded => _rendered;
- SimpleBrowserNavigationEventListener get navigationEventListener =>
- _simpleBrowserNavigationEventListener;
-
- factory SimpleBrowserWebService({
- required web.ContextProxy context,
- required void Function(WebPageBloc webPageBloc) popupHandler,
- void Function()? onLoaded,
- }) {
- final frame = web.FrameProxy();
- context.createFrame(frame.ctrl.request());
- return SimpleBrowserWebService.withFrame(
- frame: frame,
- popupHandler: popupHandler,
- onLoaded: onLoaded,
- );
- }
-
- SimpleBrowserWebService.withFrame({
- required web.FrameProxy frame,
- required void Function(WebPageBloc webPageBloc) popupHandler,
- void Function()? onLoaded,
- }) : _frame = frame {
- _frame
-
- /// Sets up listeners and attaches navigation controller.
- ..setNavigationEventListener(_navigationEventObserverBinding
- .wrap(_simpleBrowserNavigationEventListener))
- ..getNavigationController(_navigationController.ctrl.request())
- ..setPopupFrameCreationListener(
- _popupFrameCreationObserverBinding.wrap(
- _PopupListener(popupHandler, onLoaded: onLoaded),
- ),
- );
-
- /// Creates a token pair for the newly-created View.
- final tokenPair = ViewTokenPair();
- final viewRefPair = EventPairPair();
- assert(viewRefPair.status == ZX.OK);
-
- _viewHolderToken = tokenPair.viewHolderToken;
-
- final viewRef =
- views.ViewRef(reference: viewRefPair.first!.duplicate(ZX.RIGHTS_BASIC));
- final viewRefControl = views.ViewRefControl(
- reference: viewRefPair.second!
- .duplicate(ZX.DEFAULT_EVENTPAIR_RIGHTS & (~ZX.RIGHT_DUPLICATE)),
- );
- final viewRefInject =
- views.ViewRef(reference: viewRefPair.first!.duplicate(ZX.RIGHTS_BASIC));
-
- _frame.createViewWithViewRef(tokenPair.viewToken, viewRefControl, viewRef);
- _fuchsiaViewConnection = FuchsiaViewConnection(
- _viewHolderToken,
- viewRef: viewRefInject,
- onViewStateChanged: (_, state) {
- if (state == true && !_rendered) {
- onLoaded?.call();
- _rendered = true;
- }
- },
- );
- }
-
- Future<void> enableConsoleLog() =>
- _frame.setJavaScriptLogLevel(web.ConsoleLogLevel.debug);
-
- void dispose() {
- _navigationController.ctrl.close();
- _frame.ctrl.close();
- }
-
- Future<void> loadUrl(String url) => _navigationController.loadUrl(
- url,
- web.LoadUrlParams(
- type: web.LoadUrlReason.typed,
- wasUserActivated: true,
- ),
- );
- Future<void> goBack() => _navigationController.goBack();
- Future<void> goForward() => _navigationController.goForward();
- Future<void> refresh() =>
- _navigationController.reload(web.ReloadType.partialCache);
-}
-
-class _PopupListener extends web.PopupFrameCreationListener {
- final void Function(WebPageBloc webPageBloc) _handler;
- final void Function()? _onLoaded;
-
- _PopupListener(this._handler, {void Function()? onLoaded})
- : _onLoaded = onLoaded;
-
- @override
- Future<void> onPopupFrameCreated(
- InterfaceHandle<web.Frame> frame,
- web.PopupFrameCreationInfo info,
- ) async {
- final webService = SimpleBrowserWebService.withFrame(
- frame: web.FrameProxy()..ctrl.bind(frame),
- popupHandler: _handler,
- onLoaded: _onLoaded,
- );
- _handler(
- WebPageBloc(webService: webService),
- );
- }
-}
diff --git a/bin/simple_browser/lib/src/utils/browser_shortcuts.dart b/bin/simple_browser/lib/src/utils/browser_shortcuts.dart
deleted file mode 100644
index 25cea89..0000000
--- a/bin/simple_browser/lib/src/utils/browser_shortcuts.dart
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright 2019 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:io';
-
-import 'package:fidl_fuchsia_ui_shortcut/fidl_async.dart' as ui_shortcut
- show RegistryProxy;
-import 'package:fidl_fuchsia_ui_views/fidl_async.dart' show ViewRef;
-import 'package:flutter/material.dart';
-import 'package:fuchsia_logger/logger.dart';
-import 'package:fuchsia_services/services.dart';
-import 'package:keyboard_shortcuts/keyboard_shortcuts.dart';
-import 'package:simple_browser/src/blocs/tabs_bloc.dart';
-
-const path = '/pkg/data/keyboard_shortcuts.json';
-
-class BrowserShortcuts {
- final TabsBloc tabsBloc;
- late ui_shortcut.RegistryProxy registryProxy;
- final Map<String, VoidCallback> actions;
-
- factory BrowserShortcuts({
- required TabsBloc tabsBloc,
- required Map<String, VoidCallback> actions,
- ui_shortcut.RegistryProxy? shortcutRegistry,
- }) {
- if (shortcutRegistry == null) {
- return BrowserShortcuts._fromStartupContext(
- tabsBloc: tabsBloc,
- actions: actions,
- );
- }
- return BrowserShortcuts._afterStartupContext(
- tabsBloc: tabsBloc,
- actions: actions,
- );
- }
-
- BrowserShortcuts._fromStartupContext({
- required this.tabsBloc,
- required this.actions,
- }) {
- registryProxy = ui_shortcut.RegistryProxy();
- Incoming.fromSvcPath()
- ..connectToService(registryProxy)
- ..close();
- }
-
- BrowserShortcuts._afterStartupContext({
- required this.tabsBloc,
- required this.actions,
- });
-
- KeyboardShortcuts? activateShortcuts(ViewRef viewRef) {
- File file = File(path);
- file.readAsString().then((bindings) {
- return KeyboardShortcuts(
- registry: registryProxy,
- actions: actions,
- bindings: bindings,
- viewRef: viewRef,
- );
- }).catchError((err) {
- log.shout('$err: Failed to activate keyboard shortcuts.');
- });
- return null;
- }
-}
diff --git a/bin/simple_browser/lib/src/utils/sanitize_url.dart b/bin/simple_browser/lib/src/utils/sanitize_url.dart
deleted file mode 100644
index 364a038..0000000
--- a/bin/simple_browser/lib/src/utils/sanitize_url.dart
+++ /dev/null
@@ -1,56 +0,0 @@
-// Copyright 2019 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 'tld_checker.dart';
-
-String sanitizeUrl(String url) {
- // Checks if the input starts with a scheme.
- String scheme;
- try {
- scheme = Uri.parse(url).scheme;
- } on FormatException {
- return googleKeyword(url);
- }
-
- // Checks if the scheme is valid.
- // We currently only supports http, https and chrome.
- // localhost is included in the validSchemePattern since it is recognized
- // as a scheme by the dart Uri.parse method whene there is no other scheme.
- final validSchemePattern = RegExp(r'^(https?|chrome|localhost)$');
- if (scheme.isNotEmpty) {
- if (validSchemePattern.hasMatch(scheme)) {
- return url;
- }
- return googleKeyword(url);
- }
-
- // Adds a scheme to get a more accurate output from Uri.parse().host
- String schemedUrl = 'https://$url';
- String hostUrl = Uri.parse(schemedUrl).host;
-
- // Checks if the host url has a valid pattern.
- // Uri.parse().host does not check the validity.
- final validHostPattern = RegExp(r'([a-zA-Z0-9@_-]{1,256}[\.]{1,1})+[\w]+');
- if (validHostPattern.stringMatch(hostUrl) != hostUrl) {
- return googleKeyword(url);
- }
-
- // Checks if the URL has a valid TLD.
- String tld = hostUrl.split('.').last;
- if (TldChecker().isValid(tld)) {
- return schemedUrl;
- }
-
- // Checks if the URL is IPv4 address.
- final validIpv4Pattern = RegExp(
- r'\b((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3,3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$');
- if (validIpv4Pattern.stringMatch(hostUrl) == hostUrl) {
- return schemedUrl;
- }
-
- return googleKeyword(url);
-}
-
-String googleKeyword(String keyword) =>
- 'https://www.google.com/search?q=${Uri.encodeQueryComponent(keyword)}';
diff --git a/bin/simple_browser/lib/src/utils/tld_checker.dart b/bin/simple_browser/lib/src/utils/tld_checker.dart
deleted file mode 100644
index fcc9ff5..0000000
--- a/bin/simple_browser/lib/src/utils/tld_checker.dart
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright 2019 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 'package:fuchsia_logger/logger.dart';
-import 'package:meta/meta.dart';
-import 'tlds_provider.dart';
-import 'valid_tlds.dart';
-
-class TldChecker {
- late List<String> _validTlds;
-
- /// A flag that indicates if the valid TLD list is loaded or not.
- ///
- /// Its default value on the initilization is 'false'. and is set to 'true'
- /// once the [prefetchTlds()] is called, and never changes unless the browser
- /// is relaunched and this [TldChecker] is newly initiated.
- late bool _isIanaTldsLoaded;
-
- static final TldChecker _tldCheckerInstance = TldChecker._create();
- factory TldChecker() {
- return _tldCheckerInstance;
- }
-
- TldChecker._create() {
- _validTlds = kValidTlds;
- _isIanaTldsLoaded = false;
- log.info('A singleton TldChecker instance has been created.');
- }
-
- /// Fetches a valid TLD list from the IANA if it has not loaded yet.
- ///
- /// If a List<String> type parameter is given, it does not fetch the TLD list
- /// from the web and instead, just uses the parameter list as the valid TLD
- /// list. Therefore, this parameter should be given only for testing purposes.
- void prefetchTlds({List<String>? testTlds}) async {
- if (testTlds != null) {
- _validTlds = testTlds;
- } else {
- if (!_isIanaTldsLoaded) {
- _validTlds = await TldsProvider().fetchTldsList() ?? kValidTlds;
- _isIanaTldsLoaded = true;
- } else {
- log.warning(
- 'TLD List is already loaded. You do not need to fetch it again.');
- }
- }
- }
-
- bool isValid(String tld) => _validTlds.contains(tld.toUpperCase());
-
- @visibleForTesting
- List<String> get validTlds => _validTlds;
-}
diff --git a/bin/simple_browser/lib/src/utils/tlds_provider.dart b/bin/simple_browser/lib/src/utils/tlds_provider.dart
deleted file mode 100644
index d34af10..0000000
--- a/bin/simple_browser/lib/src/utils/tlds_provider.dart
+++ /dev/null
@@ -1,56 +0,0 @@
-// Copyright 2019 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:async';
-import 'package:fuchsia_logger/logger.dart';
-import 'package:http/http.dart' as http;
-
-class TldsProvider {
- String? _data;
-
- // Creates the TldsModel Once when the simple browser is initiated.
- Future<String?> loadIanaTldsList() async {
- try {
- final response = await http
- .get(Uri.parse('http://data.iana.org/TLD/tlds-alpha-by-domain.txt'));
-
- if (response.statusCode == 200) {
- log.info('Successfully loaded a TLD list from iana.org');
- return response.body;
- } else {
- log.warning('Failed to load a TLD list from iana.org '
- '(Bad response: ${response.statusCode})');
- return null;
- }
- // ignore: avoid_catches_without_on_clauses
- } catch (e) {
- log.severe('Failed to load a TLD list from iana.org ($e)');
- return null;
- }
- }
-
- Future<List<String>?> fetchTldsList() async {
- String? data;
-
- data = _data ?? await loadIanaTldsList();
-
- if (data == null) {
- return null;
- }
-
- List<String> tldsList = data.split('\n');
-
- // Removes all white spaces.
- for (int i = 0; i < tldsList.length; i++) {
- tldsList[i] = tldsList[i].replaceAll(RegExp(r'\s+'), '');
- }
-
- // Removes all comments and empty elements.
- tldsList.removeWhere((item) => item.isEmpty || item.startsWith('#'));
-
- return tldsList;
- }
-
- set data(String testData) => _data = testData;
-}
diff --git a/bin/simple_browser/lib/src/utils/valid_tlds.dart b/bin/simple_browser/lib/src/utils/valid_tlds.dart
deleted file mode 100644
index 30d39af..0000000
--- a/bin/simple_browser/lib/src/utils/valid_tlds.dart
+++ /dev/null
@@ -1,1533 +0,0 @@
-// Copyright 2019 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.
-
-const kValidTlds = <String>[
- 'AAA',
- 'AARP',
- 'ABARTH',
- 'ABB',
- 'ABBOTT',
- 'ABBVIE',
- 'ABC',
- 'ABLE',
- 'ABOGADO',
- 'ABUDHABI',
- 'AC',
- 'ACADEMY',
- 'ACCENTURE',
- 'ACCOUNTANT',
- 'ACCOUNTANTS',
- 'ACO',
- 'ACTOR',
- 'AD',
- 'ADAC',
- 'ADS',
- 'ADULT',
- 'AE',
- 'AEG',
- 'AERO',
- 'AETNA',
- 'AF',
- 'AFAMILYCOMPANY',
- 'AFL',
- 'AFRICA',
- 'AG',
- 'AGAKHAN',
- 'AGENCY',
- 'AI',
- 'AIG',
- 'AIGO',
- 'AIRBUS',
- 'AIRFORCE',
- 'AIRTEL',
- 'AKDN',
- 'AL',
- 'ALFAROMEO',
- 'ALIBABA',
- 'ALIPAY',
- 'ALLFINANZ',
- 'ALLSTATE',
- 'ALLY',
- 'ALSACE',
- 'ALSTOM',
- 'AM',
- 'AMERICANEXPRESS',
- 'AMERICANFAMILY',
- 'AMEX',
- 'AMFAM',
- 'AMICA',
- 'AMSTERDAM',
- 'ANALYTICS',
- 'ANDROID',
- 'ANQUAN',
- 'ANZ',
- 'AO',
- 'AOL',
- 'APARTMENTS',
- 'APP',
- 'APPLE',
- 'AQ',
- 'AQUARELLE',
- 'AR',
- 'ARAB',
- 'ARAMCO',
- 'ARCHI',
- 'ARMY',
- 'ARPA',
- 'ART',
- 'ARTE',
- 'AS',
- 'ASDA',
- 'ASIA',
- 'ASSOCIATES',
- 'AT',
- 'ATHLETA',
- 'ATTORNEY',
- 'AU',
- 'AUCTION',
- 'AUDI',
- 'AUDIBLE',
- 'AUDIO',
- 'AUSPOST',
- 'AUTHOR',
- 'AUTO',
- 'AUTOS',
- 'AVIANCA',
- 'AW',
- 'AWS',
- 'AX',
- 'AXA',
- 'AZ',
- 'AZURE',
- 'BA',
- 'BABY',
- 'BAIDU',
- 'BANAMEX',
- 'BANANAREPUBLIC',
- 'BAND',
- 'BANK',
- 'BAR',
- 'BARCELONA',
- 'BARCLAYCARD',
- 'BARCLAYS',
- 'BAREFOOT',
- 'BARGAINS',
- 'BASEBALL',
- 'BASKETBALL',
- 'BAUHAUS',
- 'BAYERN',
- 'BB',
- 'BBC',
- 'BBT',
- 'BBVA',
- 'BCG',
- 'BCN',
- 'BD',
- 'BE',
- 'BEATS',
- 'BEAUTY',
- 'BEER',
- 'BENTLEY',
- 'BERLIN',
- 'BEST',
- 'BESTBUY',
- 'BET',
- 'BF',
- 'BG',
- 'BH',
- 'BHARTI',
- 'BI',
- 'BIBLE',
- 'BID',
- 'BIKE',
- 'BING',
- 'BINGO',
- 'BIO',
- 'BIZ',
- 'BJ',
- 'BLACK',
- 'BLACKFRIDAY',
- 'BLOCKBUSTER',
- 'BLOG',
- 'BLOOMBERG',
- 'BLUE',
- 'BM',
- 'BMS',
- 'BMW',
- 'BN',
- 'BNPPARIBAS',
- 'BO',
- 'BOATS',
- 'BOEHRINGER',
- 'BOFA',
- 'BOM',
- 'BOND',
- 'BOO',
- 'BOOK',
- 'BOOKING',
- 'BOSCH',
- 'BOSTIK',
- 'BOSTON',
- 'BOT',
- 'BOUTIQUE',
- 'BOX',
- 'BR',
- 'BRADESCO',
- 'BRIDGESTONE',
- 'BROADWAY',
- 'BROKER',
- 'BROTHER',
- 'BRUSSELS',
- 'BS',
- 'BT',
- 'BUDAPEST',
- 'BUGATTI',
- 'BUILD',
- 'BUILDERS',
- 'BUSINESS',
- 'BUY',
- 'BUZZ',
- 'BV',
- 'BW',
- 'BY',
- 'BZ',
- 'BZH',
- 'CA',
- 'CAB',
- 'CAFE',
- 'CAL',
- 'CALL',
- 'CALVINKLEIN',
- 'CAM',
- 'CAMERA',
- 'CAMP',
- 'CANCERRESEARCH',
- 'CANON',
- 'CAPETOWN',
- 'CAPITAL',
- 'CAPITALONE',
- 'CAR',
- 'CARAVAN',
- 'CARDS',
- 'CARE',
- 'CAREER',
- 'CAREERS',
- 'CARS',
- 'CARTIER',
- 'CASA',
- 'CASE',
- 'CASEIH',
- 'CASH',
- 'CASINO',
- 'CAT',
- 'CATERING',
- 'CATHOLIC',
- 'CBA',
- 'CBN',
- 'CBRE',
- 'CBS',
- 'CC',
- 'CD',
- 'CEB',
- 'CENTER',
- 'CEO',
- 'CERN',
- 'CF',
- 'CFA',
- 'CFD',
- 'CG',
- 'CH',
- 'CHANEL',
- 'CHANNEL',
- 'CHARITY',
- 'CHASE',
- 'CHAT',
- 'CHEAP',
- 'CHINTAI',
- 'CHRISTMAS',
- 'CHROME',
- 'CHRYSLER',
- 'CHURCH',
- 'CI',
- 'CIPRIANI',
- 'CIRCLE',
- 'CISCO',
- 'CITADEL',
- 'CITI',
- 'CITIC',
- 'CITY',
- 'CITYEATS',
- 'CK',
- 'CL',
- 'CLAIMS',
- 'CLEANING',
- 'CLICK',
- 'CLINIC',
- 'CLINIQUE',
- 'CLOTHING',
- 'CLOUD',
- 'CLUB',
- 'CLUBMED',
- 'CM',
- 'CN',
- 'CO',
- 'COACH',
- 'CODES',
- 'COFFEE',
- 'COLLEGE',
- 'COLOGNE',
- 'COM',
- 'COMCAST',
- 'COMMBANK',
- 'COMMUNITY',
- 'COMPANY',
- 'COMPARE',
- 'COMPUTER',
- 'COMSEC',
- 'CONDOS',
- 'CONSTRUCTION',
- 'CONSULTING',
- 'CONTACT',
- 'CONTRACTORS',
- 'COOKING',
- 'COOKINGCHANNEL',
- 'COOL',
- 'COOP',
- 'CORSICA',
- 'COUNTRY',
- 'COUPON',
- 'COUPONS',
- 'COURSES',
- 'CPA',
- 'CR',
- 'CREDIT',
- 'CREDITCARD',
- 'CREDITUNION',
- 'CRICKET',
- 'CROWN',
- 'CRS',
- 'CRUISE',
- 'CRUISES',
- 'CSC',
- 'CU',
- 'CUISINELLA',
- 'CV',
- 'CW',
- 'CX',
- 'CY',
- 'CYMRU',
- 'CYOU',
- 'CZ',
- 'DABUR',
- 'DAD',
- 'DANCE',
- 'DATA',
- 'DATE',
- 'DATING',
- 'DATSUN',
- 'DAY',
- 'DCLK',
- 'DDS',
- 'DE',
- 'DEAL',
- 'DEALER',
- 'DEALS',
- 'DEGREE',
- 'DELIVERY',
- 'DELL',
- 'DELOITTE',
- 'DELTA',
- 'DEMOCRAT',
- 'DENTAL',
- 'DENTIST',
- 'DESI',
- 'DESIGN',
- 'DEV',
- 'DHL',
- 'DIAMONDS',
- 'DIET',
- 'DIGITAL',
- 'DIRECT',
- 'DIRECTORY',
- 'DISCOUNT',
- 'DISCOVER',
- 'DISH',
- 'DIY',
- 'DJ',
- 'DK',
- 'DM',
- 'DNP',
- 'DO',
- 'DOCS',
- 'DOCTOR',
- 'DODGE',
- 'DOG',
- 'DOMAINS',
- 'DOT',
- 'DOWNLOAD',
- 'DRIVE',
- 'DTV',
- 'DUBAI',
- 'DUCK',
- 'DUNLOP',
- 'DUPONT',
- 'DURBAN',
- 'DVAG',
- 'DVR',
- 'DZ',
- 'EARTH',
- 'EAT',
- 'EC',
- 'ECO',
- 'EDEKA',
- 'EDU',
- 'EDUCATION',
- 'EE',
- 'EG',
- 'EMAIL',
- 'EMERCK',
- 'ENERGY',
- 'ENGINEER',
- 'ENGINEERING',
- 'ENTERPRISES',
- 'EPSON',
- 'EQUIPMENT',
- 'ER',
- 'ERICSSON',
- 'ERNI',
- 'ES',
- 'ESQ',
- 'ESTATE',
- 'ESURANCE',
- 'ET',
- 'ETISALAT',
- 'EU',
- 'EUROVISION',
- 'EUS',
- 'EVENTS',
- 'EVERBANK',
- 'EXCHANGE',
- 'EXPERT',
- 'EXPOSED',
- 'EXPRESS',
- 'EXTRASPACE',
- 'FAGE',
- 'FAIL',
- 'FAIRWINDS',
- 'FAITH',
- 'FAMILY',
- 'FAN',
- 'FANS',
- 'FARM',
- 'FARMERS',
- 'FASHION',
- 'FAST',
- 'FEDEX',
- 'FEEDBACK',
- 'FERRARI',
- 'FERRERO',
- 'FI',
- 'FIAT',
- 'FIDELITY',
- 'FIDO',
- 'FILM',
- 'FINAL',
- 'FINANCE',
- 'FINANCIAL',
- 'FIRE',
- 'FIRESTONE',
- 'FIRMDALE',
- 'FISH',
- 'FISHING',
- 'FIT',
- 'FITNESS',
- 'FJ',
- 'FK',
- 'FLICKR',
- 'FLIGHTS',
- 'FLIR',
- 'FLORIST',
- 'FLOWERS',
- 'FLY',
- 'FM',
- 'FO',
- 'FOO',
- 'FOOD',
- 'FOODNETWORK',
- 'FOOTBALL',
- 'FORD',
- 'FOREX',
- 'FORSALE',
- 'FORUM',
- 'FOUNDATION',
- 'FOX',
- 'FR',
- 'FREE',
- 'FRESENIUS',
- 'FRL',
- 'FROGANS',
- 'FRONTDOOR',
- 'FRONTIER',
- 'FTR',
- 'FUJITSU',
- 'FUJIXEROX',
- 'FUN',
- 'FUND',
- 'FURNITURE',
- 'FUTBOL',
- 'FYI',
- 'GA',
- 'GAL',
- 'GALLERY',
- 'GALLO',
- 'GALLUP',
- 'GAME',
- 'GAMES',
- 'GAP',
- 'GARDEN',
- 'GAY',
- 'GB',
- 'GBIZ',
- 'GD',
- 'GDN',
- 'GE',
- 'GEA',
- 'GENT',
- 'GENTING',
- 'GEORGE',
- 'GF',
- 'GG',
- 'GGEE',
- 'GH',
- 'GI',
- 'GIFT',
- 'GIFTS',
- 'GIVES',
- 'GIVING',
- 'GL',
- 'GLADE',
- 'GLASS',
- 'GLE',
- 'GLOBAL',
- 'GLOBO',
- 'GM',
- 'GMAIL',
- 'GMBH',
- 'GMO',
- 'GMX',
- 'GN',
- 'GODADDY',
- 'GOLD',
- 'GOLDPOINT',
- 'GOLF',
- 'GOO',
- 'GOODYEAR',
- 'GOOG',
- 'GOOGLE',
- 'GOP',
- 'GOT',
- 'GOV',
- 'GP',
- 'GQ',
- 'GR',
- 'GRAINGER',
- 'GRAPHICS',
- 'GRATIS',
- 'GREEN',
- 'GRIPE',
- 'GROCERY',
- 'GROUP',
- 'GS',
- 'GT',
- 'GU',
- 'GUARDIAN',
- 'GUCCI',
- 'GUGE',
- 'GUIDE',
- 'GUITARS',
- 'GURU',
- 'GW',
- 'GY',
- 'HAIR',
- 'HAMBURG',
- 'HANGOUT',
- 'HAUS',
- 'HBO',
- 'HDFC',
- 'HDFCBANK',
- 'HEALTH',
- 'HEALTHCARE',
- 'HELP',
- 'HELSINKI',
- 'HERE',
- 'HERMES',
- 'HGTV',
- 'HIPHOP',
- 'HISAMITSU',
- 'HITACHI',
- 'HIV',
- 'HK',
- 'HKT',
- 'HM',
- 'HN',
- 'HOCKEY',
- 'HOLDINGS',
- 'HOLIDAY',
- 'HOMEDEPOT',
- 'HOMEGOODS',
- 'HOMES',
- 'HOMESENSE',
- 'HONDA',
- 'HORSE',
- 'HOSPITAL',
- 'HOST',
- 'HOSTING',
- 'HOT',
- 'HOTELES',
- 'HOTELS',
- 'HOTMAIL',
- 'HOUSE',
- 'HOW',
- 'HR',
- 'HSBC',
- 'HT',
- 'HU',
- 'HUGHES',
- 'HYATT',
- 'HYUNDAI',
- 'IBM',
- 'ICBC',
- 'ICE',
- 'ICU',
- 'ID',
- 'IE',
- 'IEEE',
- 'IFM',
- 'IKANO',
- 'IL',
- 'IM',
- 'IMAMAT',
- 'IMDB',
- 'IMMO',
- 'IMMOBILIEN',
- 'IN',
- 'INC',
- 'INDUSTRIES',
- 'INFINITI',
- 'INFO',
- 'ING',
- 'INK',
- 'INSTITUTE',
- 'INSURANCE',
- 'INSURE',
- 'INT',
- 'INTEL',
- 'INTERNATIONAL',
- 'INTUIT',
- 'INVESTMENTS',
- 'IO',
- 'IPIRANGA',
- 'IQ',
- 'IR',
- 'IRISH',
- 'IS',
- 'ISMAILI',
- 'IST',
- 'ISTANBUL',
- 'IT',
- 'ITAU',
- 'ITV',
- 'IVECO',
- 'JAGUAR',
- 'JAVA',
- 'JCB',
- 'JCP',
- 'JE',
- 'JEEP',
- 'JETZT',
- 'JEWELRY',
- 'JIO',
- 'JLL',
- 'JM',
- 'JMP',
- 'JNJ',
- 'JO',
- 'JOBS',
- 'JOBURG',
- 'JOT',
- 'JOY',
- 'JP',
- 'JPMORGAN',
- 'JPRS',
- 'JUEGOS',
- 'JUNIPER',
- 'KAUFEN',
- 'KDDI',
- 'KE',
- 'KERRYHOTELS',
- 'KERRYLOGISTICS',
- 'KERRYPROPERTIES',
- 'KFH',
- 'KG',
- 'KH',
- 'KI',
- 'KIA',
- 'KIM',
- 'KINDER',
- 'KINDLE',
- 'KITCHEN',
- 'KIWI',
- 'KM',
- 'KN',
- 'KOELN',
- 'KOMATSU',
- 'KOSHER',
- 'KP',
- 'KPMG',
- 'KPN',
- 'KR',
- 'KRD',
- 'KRED',
- 'KUOKGROUP',
- 'KW',
- 'KY',
- 'KYOTO',
- 'KZ',
- 'LA',
- 'LACAIXA',
- 'LADBROKES',
- 'LAMBORGHINI',
- 'LAMER',
- 'LANCASTER',
- 'LANCIA',
- 'LANCOME',
- 'LAND',
- 'LANDROVER',
- 'LANXESS',
- 'LASALLE',
- 'LAT',
- 'LATINO',
- 'LATROBE',
- 'LAW',
- 'LAWYER',
- 'LB',
- 'LC',
- 'LDS',
- 'LEASE',
- 'LECLERC',
- 'LEFRAK',
- 'LEGAL',
- 'LEGO',
- 'LEXUS',
- 'LGBT',
- 'LI',
- 'LIAISON',
- 'LIDL',
- 'LIFE',
- 'LIFEINSURANCE',
- 'LIFESTYLE',
- 'LIGHTING',
- 'LIKE',
- 'LILLY',
- 'LIMITED',
- 'LIMO',
- 'LINCOLN',
- 'LINDE',
- 'LINK',
- 'LIPSY',
- 'LIVE',
- 'LIVING',
- 'LIXIL',
- 'LK',
- 'LLC',
- 'LOAN',
- 'LOANS',
- 'LOCKER',
- 'LOCUS',
- 'LOFT',
- 'LOL',
- 'LONDON',
- 'LOTTE',
- 'LOTTO',
- 'LOVE',
- 'LPL',
- 'LPLFINANCIAL',
- 'LR',
- 'LS',
- 'LT',
- 'LTD',
- 'LTDA',
- 'LU',
- 'LUNDBECK',
- 'LUPIN',
- 'LUXE',
- 'LUXURY',
- 'LV',
- 'LY',
- 'MA',
- 'MACYS',
- 'MADRID',
- 'MAIF',
- 'MAISON',
- 'MAKEUP',
- 'MAN',
- 'MANAGEMENT',
- 'MANGO',
- 'MAP',
- 'MARKET',
- 'MARKETING',
- 'MARKETS',
- 'MARRIOTT',
- 'MARSHALLS',
- 'MASERATI',
- 'MATTEL',
- 'MBA',
- 'MC',
- 'MCKINSEY',
- 'MD',
- 'ME',
- 'MED',
- 'MEDIA',
- 'MEET',
- 'MELBOURNE',
- 'MEME',
- 'MEMORIAL',
- 'MEN',
- 'MENU',
- 'MERCKMSD',
- 'METLIFE',
- 'MG',
- 'MH',
- 'MIAMI',
- 'MICROSOFT',
- 'MIL',
- 'MINI',
- 'MINT',
- 'MIT',
- 'MITSUBISHI',
- 'MK',
- 'ML',
- 'MLB',
- 'MLS',
- 'MM',
- 'MMA',
- 'MN',
- 'MO',
- 'MOBI',
- 'MOBILE',
- 'MODA',
- 'MOE',
- 'MOI',
- 'MOM',
- 'MONASH',
- 'MONEY',
- 'MONSTER',
- 'MOPAR',
- 'MORMON',
- 'MORTGAGE',
- 'MOSCOW',
- 'MOTO',
- 'MOTORCYCLES',
- 'MOV',
- 'MOVIE',
- 'MOVISTAR',
- 'MP',
- 'MQ',
- 'MR',
- 'MS',
- 'MSD',
- 'MT',
- 'MTN',
- 'MTR',
- 'MU',
- 'MUSEUM',
- 'MUTUAL',
- 'MV',
- 'MW',
- 'MX',
- 'MY',
- 'MZ',
- 'NA',
- 'NAB',
- 'NADEX',
- 'NAGOYA',
- 'NAME',
- 'NATIONWIDE',
- 'NATURA',
- 'NAVY',
- 'NBA',
- 'NC',
- 'NE',
- 'NEC',
- 'NET',
- 'NETBANK',
- 'NETFLIX',
- 'NETWORK',
- 'NEUSTAR',
- 'NEW',
- 'NEWHOLLAND',
- 'NEWS',
- 'NEXT',
- 'NEXTDIRECT',
- 'NEXUS',
- 'NF',
- 'NFL',
- 'NG',
- 'NGO',
- 'NHK',
- 'NI',
- 'NICO',
- 'NIKE',
- 'NIKON',
- 'NINJA',
- 'NISSAN',
- 'NISSAY',
- 'NL',
- 'NO',
- 'NOKIA',
- 'NORTHWESTERNMUTUAL',
- 'NORTON',
- 'NOW',
- 'NOWRUZ',
- 'NOWTV',
- 'NP',
- 'NR',
- 'NRA',
- 'NRW',
- 'NTT',
- 'NU',
- 'NYC',
- 'NZ',
- 'OBI',
- 'OBSERVER',
- 'OFF',
- 'OFFICE',
- 'OKINAWA',
- 'OLAYAN',
- 'OLAYANGROUP',
- 'OLDNAVY',
- 'OLLO',
- 'OM',
- 'OMEGA',
- 'ONE',
- 'ONG',
- 'ONL',
- 'ONLINE',
- 'ONYOURSIDE',
- 'OOO',
- 'OPEN',
- 'ORACLE',
- 'ORANGE',
- 'ORG',
- 'ORGANIC',
- 'ORIGINS',
- 'OSAKA',
- 'OTSUKA',
- 'OTT',
- 'OVH',
- 'PA',
- 'PAGE',
- 'PANASONIC',
- 'PARIS',
- 'PARS',
- 'PARTNERS',
- 'PARTS',
- 'PARTY',
- 'PASSAGENS',
- 'PAY',
- 'PCCW',
- 'PE',
- 'PET',
- 'PF',
- 'PFIZER',
- 'PG',
- 'PH',
- 'PHARMACY',
- 'PHD',
- 'PHILIPS',
- 'PHONE',
- 'PHOTO',
- 'PHOTOGRAPHY',
- 'PHOTOS',
- 'PHYSIO',
- 'PIAGET',
- 'PICS',
- 'PICTET',
- 'PICTURES',
- 'PID',
- 'PIN',
- 'PING',
- 'PINK',
- 'PIONEER',
- 'PIZZA',
- 'PK',
- 'PL',
- 'PLACE',
- 'PLAY',
- 'PLAYSTATION',
- 'PLUMBING',
- 'PLUS',
- 'PM',
- 'PN',
- 'PNC',
- 'POHL',
- 'POKER',
- 'POLITIE',
- 'PORN',
- 'POST',
- 'PR',
- 'PRAMERICA',
- 'PRAXI',
- 'PRESS',
- 'PRIME',
- 'PRO',
- 'PROD',
- 'PRODUCTIONS',
- 'PROF',
- 'PROGRESSIVE',
- 'PROMO',
- 'PROPERTIES',
- 'PROPERTY',
- 'PROTECTION',
- 'PRU',
- 'PRUDENTIAL',
- 'PS',
- 'PT',
- 'PUB',
- 'PW',
- 'PWC',
- 'PY',
- 'QA',
- 'QPON',
- 'QUEBEC',
- 'QUEST',
- 'QVC',
- 'RACING',
- 'RADIO',
- 'RAID',
- 'RE',
- 'READ',
- 'REALESTATE',
- 'REALTOR',
- 'REALTY',
- 'RECIPES',
- 'RED',
- 'REDSTONE',
- 'REDUMBRELLA',
- 'REHAB',
- 'REISE',
- 'REISEN',
- 'REIT',
- 'RELIANCE',
- 'REN',
- 'RENT',
- 'RENTALS',
- 'REPAIR',
- 'REPORT',
- 'REPUBLICAN',
- 'REST',
- 'RESTAURANT',
- 'REVIEW',
- 'REVIEWS',
- 'REXROTH',
- 'RICH',
- 'RICHARDLI',
- 'RICOH',
- 'RIGHTATHOME',
- 'RIL',
- 'RIO',
- 'RIP',
- 'RMIT',
- 'RO',
- 'ROCHER',
- 'ROCKS',
- 'RODEO',
- 'ROGERS',
- 'ROOM',
- 'RS',
- 'RSVP',
- 'RU',
- 'RUGBY',
- 'RUHR',
- 'RUN',
- 'RW',
- 'RWE',
- 'RYUKYU',
- 'SA',
- 'SAARLAND',
- 'SAFE',
- 'SAFETY',
- 'SAKURA',
- 'SALE',
- 'SALON',
- 'SAMSCLUB',
- 'SAMSUNG',
- 'SANDVIK',
- 'SANDVIKCOROMANT',
- 'SANOFI',
- 'SAP',
- 'SARL',
- 'SAS',
- 'SAVE',
- 'SAXO',
- 'SB',
- 'SBI',
- 'SBS',
- 'SC',
- 'SCA',
- 'SCB',
- 'SCHAEFFLER',
- 'SCHMIDT',
- 'SCHOLARSHIPS',
- 'SCHOOL',
- 'SCHULE',
- 'SCHWARZ',
- 'SCIENCE',
- 'SCJOHNSON',
- 'SCOR',
- 'SCOT',
- 'SD',
- 'SE',
- 'SEARCH',
- 'SEAT',
- 'SECURE',
- 'SECURITY',
- 'SEEK',
- 'SELECT',
- 'SENER',
- 'SERVICES',
- 'SES',
- 'SEVEN',
- 'SEW',
- 'SEX',
- 'SEXY',
- 'SFR',
- 'SG',
- 'SH',
- 'SHANGRILA',
- 'SHARP',
- 'SHAW',
- 'SHELL',
- 'SHIA',
- 'SHIKSHA',
- 'SHOES',
- 'SHOP',
- 'SHOPPING',
- 'SHOUJI',
- 'SHOW',
- 'SHOWTIME',
- 'SHRIRAM',
- 'SI',
- 'SILK',
- 'SINA',
- 'SINGLES',
- 'SITE',
- 'SJ',
- 'SK',
- 'SKI',
- 'SKIN',
- 'SKY',
- 'SKYPE',
- 'SL',
- 'SLING',
- 'SM',
- 'SMART',
- 'SMILE',
- 'SN',
- 'SNCF',
- 'SO',
- 'SOCCER',
- 'SOCIAL',
- 'SOFTBANK',
- 'SOFTWARE',
- 'SOHU',
- 'SOLAR',
- 'SOLUTIONS',
- 'SONG',
- 'SONY',
- 'SOY',
- 'SPACE',
- 'SPORT',
- 'SPOT',
- 'SPREADBETTING',
- 'SR',
- 'SRL',
- 'SRT',
- 'SS',
- 'ST',
- 'STADA',
- 'STAPLES',
- 'STAR',
- 'STATEBANK',
- 'STATEFARM',
- 'STC',
- 'STCGROUP',
- 'STOCKHOLM',
- 'STORAGE',
- 'STORE',
- 'STREAM',
- 'STUDIO',
- 'STUDY',
- 'STYLE',
- 'SU',
- 'SUCKS',
- 'SUPPLIES',
- 'SUPPLY',
- 'SUPPORT',
- 'SURF',
- 'SURGERY',
- 'SUZUKI',
- 'SV',
- 'SWATCH',
- 'SWIFTCOVER',
- 'SWISS',
- 'SX',
- 'SY',
- 'SYDNEY',
- 'SYMANTEC',
- 'SYSTEMS',
- 'SZ',
- 'TAB',
- 'TAIPEI',
- 'TALK',
- 'TAOBAO',
- 'TARGET',
- 'TATAMOTORS',
- 'TATAR',
- 'TATTOO',
- 'TAX',
- 'TAXI',
- 'TC',
- 'TCI',
- 'TD',
- 'TDK',
- 'TEAM',
- 'TECH',
- 'TECHNOLOGY',
- 'TEL',
- 'TELEFONICA',
- 'TEMASEK',
- 'TENNIS',
- 'TEVA',
- 'TF',
- 'TG',
- 'TH',
- 'THD',
- 'THEATER',
- 'THEATRE',
- 'TIAA',
- 'TICKETS',
- 'TIENDA',
- 'TIFFANY',
- 'TIPS',
- 'TIRES',
- 'TIROL',
- 'TJ',
- 'TJMAXX',
- 'TJX',
- 'TK',
- 'TKMAXX',
- 'TL',
- 'TM',
- 'TMALL',
- 'TN',
- 'TO',
- 'TODAY',
- 'TOKYO',
- 'TOOLS',
- 'TOP',
- 'TORAY',
- 'TOSHIBA',
- 'TOTAL',
- 'TOURS',
- 'TOWN',
- 'TOYOTA',
- 'TOYS',
- 'TR',
- 'TRADE',
- 'TRADING',
- 'TRAINING',
- 'TRAVEL',
- 'TRAVELCHANNEL',
- 'TRAVELERS',
- 'TRAVELERSINSURANCE',
- 'TRUST',
- 'TRV',
- 'TT',
- 'TUBE',
- 'TUI',
- 'TUNES',
- 'TUSHU',
- 'TV',
- 'TVS',
- 'TW',
- 'TZ',
- 'UA',
- 'UBANK',
- 'UBS',
- 'UCONNECT',
- 'UG',
- 'UK',
- 'UNICOM',
- 'UNIVERSITY',
- 'UNO',
- 'UOL',
- 'UPS',
- 'US',
- 'UY',
- 'UZ',
- 'VA',
- 'VACATIONS',
- 'VANA',
- 'VANGUARD',
- 'VC',
- 'VE',
- 'VEGAS',
- 'VENTURES',
- 'VERISIGN',
- 'VERSICHERUNG',
- 'VET',
- 'VG',
- 'VI',
- 'VIAJES',
- 'VIDEO',
- 'VIG',
- 'VIKING',
- 'VILLAS',
- 'VIN',
- 'VIP',
- 'VIRGIN',
- 'VISA',
- 'VISION',
- 'VISTAPRINT',
- 'VIVA',
- 'VIVO',
- 'VLAANDEREN',
- 'VN',
- 'VODKA',
- 'VOLKSWAGEN',
- 'VOLVO',
- 'VOTE',
- 'VOTING',
- 'VOTO',
- 'VOYAGE',
- 'VU',
- 'VUELOS',
- 'WALES',
- 'WALMART',
- 'WALTER',
- 'WANG',
- 'WANGGOU',
- 'WARMAN',
- 'WATCH',
- 'WATCHES',
- 'WEATHER',
- 'WEATHERCHANNEL',
- 'WEBCAM',
- 'WEBER',
- 'WEBSITE',
- 'WED',
- 'WEDDING',
- 'WEIBO',
- 'WEIR',
- 'WF',
- 'WHOSWHO',
- 'WIEN',
- 'WIKI',
- 'WILLIAMHILL',
- 'WIN',
- 'WINDOWS',
- 'WINE',
- 'WINNERS',
- 'WME',
- 'WOLTERSKLUWER',
- 'WOODSIDE',
- 'WORK',
- 'WORKS',
- 'WORLD',
- 'WOW',
- 'WS',
- 'WTC',
- 'WTF',
- 'XBOX',
- 'XEROX',
- 'XFINITY',
- 'XIHUAN',
- 'XIN',
- 'XN--11B4C3D',
- 'XN--1CK2E1B',
- 'XN--1QQW23A',
- 'XN--2SCRJ9C',
- 'XN--30RR7Y',
- 'XN--3BST00M',
- 'XN--3DS443G',
- 'XN--3E0B707E',
- 'XN--3HCRJ9C',
- 'XN--3OQ18VL8PN36A',
- 'XN--3PXU8K',
- 'XN--42C2D9A',
- 'XN--45BR5CYL',
- 'XN--45BRJ9C',
- 'XN--45Q11C',
- 'XN--4GBRIM',
- 'XN--54B7FTA0CC',
- 'XN--55QW42G',
- 'XN--55QX5D',
- 'XN--5SU34J936BGSG',
- 'XN--5TZM5G',
- 'XN--6FRZ82G',
- 'XN--6QQ986B3XL',
- 'XN--80ADXHKS',
- 'XN--80AO21A',
- 'XN--80AQECDR1A',
- 'XN--80ASEHDB',
- 'XN--80ASWG',
- 'XN--8Y0A063A',
- 'XN--90A3AC',
- 'XN--90AE',
- 'XN--90AIS',
- 'XN--9DBQ2A',
- 'XN--9ET52U',
- 'XN--9KRT00A',
- 'XN--B4W605FERD',
- 'XN--BCK1B9A5DRE4C',
- 'XN--C1AVG',
- 'XN--C2BR7G',
- 'XN--CCK2B3B',
- 'XN--CG4BKI',
- 'XN--CLCHC0EA0B2G2A9GCD',
- 'XN--CZR694B',
- 'XN--CZRS0T',
- 'XN--CZRU2D',
- 'XN--D1ACJ3B',
- 'XN--D1ALF',
- 'XN--E1A4C',
- 'XN--ECKVDTC9D',
- 'XN--EFVY88H',
- 'XN--ESTV75G',
- 'XN--FCT429K',
- 'XN--FHBEI',
- 'XN--FIQ228C5HS',
- 'XN--FIQ64B',
- 'XN--FIQS8S',
- 'XN--FIQZ9S',
- 'XN--FJQ720A',
- 'XN--FLW351E',
- 'XN--FPCRJ9C3D',
- 'XN--FZC2C9E2C',
- 'XN--FZYS8D69UVGM',
- 'XN--G2XX48C',
- 'XN--GCKR3F0F',
- 'XN--GECRJ9C',
- 'XN--GK3AT1E',
- 'XN--H2BREG3EVE',
- 'XN--H2BRJ9C',
- 'XN--H2BRJ9C8C',
- 'XN--HXT814E',
- 'XN--I1B6B1A6A2E',
- 'XN--IMR513N',
- 'XN--IO0A7I',
- 'XN--J1AEF',
- 'XN--J1AMH',
- 'XN--J6W193G',
- 'XN--JLQ61U9W7B',
- 'XN--JVR189M',
- 'XN--KCRX77D1X4A',
- 'XN--KPRW13D',
- 'XN--KPRY57D',
- 'XN--KPU716F',
- 'XN--KPUT3I',
- 'XN--L1ACC',
- 'XN--LGBBAT1AD8J',
- 'XN--MGB9AWBF',
- 'XN--MGBA3A3EJT',
- 'XN--MGBA3A4F16A',
- 'XN--MGBA7C0BBN0A',
- 'XN--MGBAAKC7DVF',
- 'XN--MGBAAM7A8H',
- 'XN--MGBAB2BD',
- 'XN--MGBAH1A3HJKRD',
- 'XN--MGBAI9AZGQP6J',
- 'XN--MGBAYH7GPA',
- 'XN--MGBBH1A',
- 'XN--MGBBH1A71E',
- 'XN--MGBC0A9AZCG',
- 'XN--MGBCA7DZDO',
- 'XN--MGBERP4A5D4AR',
- 'XN--MGBGU82A',
- 'XN--MGBI4ECEXP',
- 'XN--MGBPL2FH',
- 'XN--MGBT3DHD',
- 'XN--MGBTX2B',
- 'XN--MGBX4CD0AB',
- 'XN--MIX891F',
- 'XN--MK1BU44C',
- 'XN--MXTQ1M',
- 'XN--NGBC5AZD',
- 'XN--NGBE9E0A',
- 'XN--NGBRX',
- 'XN--NODE',
- 'XN--NQV7F',
- 'XN--NQV7FS00EMA',
- 'XN--NYQY26A',
- 'XN--O3CW4H',
- 'XN--OGBPF8FL',
- 'XN--OTU796D',
- 'XN--P1ACF',
- 'XN--P1AI',
- 'XN--PBT977C',
- 'XN--PGBS0DH',
- 'XN--PSSY2U',
- 'XN--Q9JYB4C',
- 'XN--QCKA1PMC',
- 'XN--QXA6A',
- 'XN--QXAM',
- 'XN--RHQV96G',
- 'XN--ROVU88B',
- 'XN--RVC1E0AM3E',
- 'XN--S9BRJ9C',
- 'XN--SES554G',
- 'XN--T60B56A',
- 'XN--TCKWE',
- 'XN--TIQ49XQYJ',
- 'XN--UNUP4Y',
- 'XN--VERMGENSBERATER-CTB',
- 'XN--VERMGENSBERATUNG-PWB',
- 'XN--VHQUV',
- 'XN--VUQ861B',
- 'XN--W4R85EL8FHU5DNRA',
- 'XN--W4RS40L',
- 'XN--WGBH1C',
- 'XN--WGBL6A',
- 'XN--XHQ521B',
- 'XN--XKC2AL3HYE2A',
- 'XN--XKC2DL3A5EE0H',
- 'XN--Y9A3AQ',
- 'XN--YFRO4I67O',
- 'XN--YGBI2AMMX',
- 'XN--ZFR164B',
- 'XXX',
- 'XYZ',
- 'YACHTS',
- 'YAHOO',
- 'YAMAXUN',
- 'YANDEX',
- 'YE',
- 'YODOBASHI',
- 'YOGA',
- 'YOKOHAMA',
- 'YOU',
- 'YOUTUBE',
- 'YT',
- 'YUN',
- 'ZA',
- 'ZAPPOS',
- 'ZARA',
- 'ZERO',
- 'ZIP',
- 'ZM',
- 'ZONE',
- 'ZUERICH',
- 'ZW',
-];
diff --git a/bin/simple_browser/lib/src/widgets/error_page.dart b/bin/simple_browser/lib/src/widgets/error_page.dart
deleted file mode 100644
index c9c39a9..0000000
--- a/bin/simple_browser/lib/src/widgets/error_page.dart
+++ /dev/null
@@ -1,238 +0,0 @@
-// Copyright 2019 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:async' show Timer;
-import 'dart:math' show Random;
-import 'dart:ui' show lerpDouble;
-
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart' show RawKeyDownEvent, RawKeyEvent;
-
-const _kStartLength = 5;
-const _kMaxCrumbs = 20;
-const _kCrumbChanceToAppear = 0.05;
-const _kKeyLabelToDirection = <String, Coord>{
- 'w': Coord.up,
- 'a': Coord.left,
- 's': Coord.down,
- 'd': Coord.right,
-};
-const _kInitialCoords = <Coord>[
- Coord(-2, 0),
- Coord(-1, 0),
- Coord(0, 0),
- Coord(1, 0),
- Coord(2, 0),
-];
-
-class ErrorPage extends StatefulWidget {
- @override
- _ErrorPageState createState() => _ErrorPageState();
-}
-
-class _ErrorPageState extends State<ErrorPage> {
- final _focusNode = FocusNode();
- final _direction = ValueNotifier<Coord?>(null);
- final _coords = <Coord>[];
- final _crumbCoords = <Coord>[];
- int _length = _kStartLength;
- Timer? _timer;
- Size? _screenSize;
- Offset? _screenCenter;
- bool _lost = false;
- final _random = Random();
- double rnd() => _random.nextDouble();
-
- @override
- void initState() {
- super.initState();
- if (WidgetsBinding.instance != null) {
- WidgetsBinding.instance!.addPostFrameCallback(
- (_) => FocusScope.of(context).requestFocus(_focusNode));
- }
- _reset();
- }
-
- void _reset() {
- _coords
- ..clear()
- ..addAll(_kInitialCoords);
- _crumbCoords.clear();
- _length = _kStartLength;
- _lost = false;
- }
-
- void _startTimer() {
- _timer?.cancel();
- _timer = Timer.periodic(Duration(milliseconds: 250), _onTimer);
- }
-
- @override
- void dispose() {
- _timer?.cancel();
- _focusNode.dispose();
- super.dispose();
- }
-
- @override
- Widget build(BuildContext context) => RawKeyboardListener(
- focusNode: FocusNode(),
- onKey: _onKey,
- child: GestureDetector(
- child: Container(
- color: Colors.transparent,
- child: LayoutBuilder(builder: (context, constraints) {
- _screenSize = constraints.biggest;
- _screenCenter = _screenSize!.center(Offset.zero);
- return Stack(
- children: [
- ..._coords.asMap().map(_buildBody).values.toList(),
- ..._crumbCoords.map(_buildCrumb).toList(),
- Opacity(
- opacity: 0,
- child: TextField(
- focusNode: _focusNode,
- autofocus: true,
- ),
- ),
- ],
- );
- }),
- ),
- onTap: () => FocusScope.of(context).requestFocus(_focusNode),
- ),
- );
-
- void _onKey(RawKeyEvent value) {
- if (value.runtimeType == RawKeyDownEvent) {
- if (_lost) {
- _reset();
- }
-
- Coord? newDirection = _kKeyLabelToDirection[value.logicalKey.keyLabel];
- if (newDirection != null) {
- if (_coords[_coords.length - 2] - _coords.last != newDirection) {
- _direction.value = newDirection;
- _onTimer(null);
- _startTimer();
- }
- }
- }
- }
-
- String _textForIndex(int index) {
- if (index == 0) {
- return 'E';
- } else if (index == _coords.length - 2) {
- return 'O';
- } else {
- return 'R';
- }
- }
-
- Widget _buildCrumb(Coord coord) =>
- _buildSquare(squaresToScreen(coord), 'R', false);
-
- MapEntry<int, Widget> _buildBody(int index, Coord coord) => MapEntry(
- index, _buildSquare(squaresToScreen(coord), _textForIndex(index), true));
-
- Widget _buildSquare(Offset offset, String string, bool invert) => Positioned(
- left: offset.dx,
- top: offset.dy,
- child: Container(
- width: 16,
- height: 16,
- color: invert ? Colors.black : null,
- child: Text(
- string,
- textAlign: TextAlign.center,
- style: invert ? TextStyle(color: Colors.white) : null,
- ),
- ),
- );
-
- Coord screenToSquares(Offset screen) => Coord(
- ((screen.dx - _screenCenter!.dx) / 16).floor(),
- ((screen.dy - _screenCenter!.dy) / 16).floor());
- Offset squaresToScreen(Coord squares) =>
- Offset((squares.x - 0.5), (squares.y - 0.5)) * 16 + _screenCenter!;
-
- void _addCrumb() {
- Coord newCrumb = screenToSquares(Offset(
- lerpDouble(0, _screenSize!.width, rnd())!,
- lerpDouble(0, _screenSize!.height, rnd())!,
- ));
-
- // add if coordinate is currently free
- if (!_coords.contains(newCrumb) && !_crumbCoords.contains(newCrumb)) {
- _crumbCoords.add(newCrumb);
- }
- }
-
- void _onTimer(Timer? timer) {
- if (_direction.value == null) {
- return;
- }
-
- Coord newCoord = _coords.last + _direction.value!;
-
- // lost: eating own tail
- if (_coords.contains(newCoord)) {
- _lost = true;
- _timer?.cancel();
- return;
- }
-
- // lost: leaving the screen
- if (!(Offset.zero & _screenSize!).contains(squaresToScreen(newCoord))) {
- _lost = true;
- _timer?.cancel();
- return;
- }
-
- setState(() {
- _coords.add(newCoord);
-
- if (_coords.length > _length) {
- _coords.removeAt(0);
- }
-
- // yum
- if (_crumbCoords.remove(newCoord)) {
- _length++;
- }
-
- // more food
- if (_crumbCoords.length < _kMaxCrumbs && rnd() < _kCrumbChanceToAppear) {
- _addCrumb();
- }
- });
- }
-}
-
-class Coord {
- const Coord(this.x, this.y);
- final int x;
- final int y;
-
- static const Coord up = Coord(0, -1);
- static const Coord left = Coord(-1, 0);
- static const Coord down = Coord(0, 1);
- static const Coord right = Coord(1, 0);
-
- Coord operator -(Coord other) => Coord(x - other.x, y - other.y);
- Coord operator +(Coord other) => Coord(x + other.x, y + other.y);
-
- @override
- bool operator ==(dynamic other) {
- if (other is! Coord) {
- return false;
- }
- final Coord typedOther = other;
- return x == typedOther.x && y == typedOther.y;
- }
-
- @override
- int get hashCode => hashValues(x, y);
-}
diff --git a/bin/simple_browser/lib/src/widgets/history_buttons.dart b/bin/simple_browser/lib/src/widgets/history_buttons.dart
deleted file mode 100644
index 72f1240..0000000
--- a/bin/simple_browser/lib/src/widgets/history_buttons.dart
+++ /dev/null
@@ -1,80 +0,0 @@
-// Copyright 2019 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 'package:flutter/material.dart';
-import 'package:internationalization/strings.dart';
-
-import '../blocs/webpage_bloc.dart';
-import '../models/webpage_action.dart';
-
-const _kEnabledOpacity = 1.0;
-const _kDisabledOpacity = 0.54;
-const _kPadding = EdgeInsets.symmetric(horizontal: 4.0);
-
-class HistoryButtons extends StatelessWidget {
- const HistoryButtons({required this.bloc});
-
- final WebPageBloc bloc;
-
- @override
- Widget build(BuildContext context) {
- return Row(
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: <Widget>[
- AnimatedBuilder(
- animation: bloc.backStateNotifier,
- builder: (_, __) => _HistoryButton(
- title: Strings.back.toUpperCase(),
- valueKey: 'back',
- onTap: () => bloc.request.add(GoBackAction()),
- isEnabled: bloc.backState)),
- SizedBox(width: 8.0),
- AnimatedBuilder(
- animation: bloc.forwardStateNotifier,
- builder: (_, __) => _HistoryButton(
- title: Strings.forward.toUpperCase(),
- valueKey: 'forward',
- onTap: () => bloc.request.add(GoForwardAction()),
- isEnabled: bloc.forwardState)),
- SizedBox(width: 8.0),
- AnimatedBuilder(
- animation: bloc.urlNotifier,
- builder: (_, __) => _HistoryButton(
- title: Strings.refresh.toUpperCase(),
- valueKey: 'refresh',
- onTap: () => bloc.request.add(RefreshAction()),
- isEnabled: bloc.pageType == PageType.normal)),
- ],
- );
- }
-}
-
-class _HistoryButton extends StatelessWidget {
- const _HistoryButton({
- required this.title,
- required this.valueKey,
- required this.onTap,
- required this.isEnabled,
- });
-
- final String title;
- final String valueKey;
- final VoidCallback onTap;
- final bool isEnabled;
-
- @override
- Widget build(BuildContext context) => GestureDetector(
- key: ValueKey(valueKey),
- onTap: isEnabled ? onTap : null,
- child: Padding(
- padding: _kPadding,
- child: Center(
- child: Opacity(
- opacity: isEnabled ? _kEnabledOpacity : _kDisabledOpacity,
- child: Text(title),
- ),
- ),
- ),
- );
-}
diff --git a/bin/simple_browser/lib/src/widgets/navigation_bar.dart b/bin/simple_browser/lib/src/widgets/navigation_bar.dart
deleted file mode 100644
index af2ef9e..0000000
--- a/bin/simple_browser/lib/src/widgets/navigation_bar.dart
+++ /dev/null
@@ -1,147 +0,0 @@
-// Copyright 2019 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 'package:flutter/material.dart';
-import '../blocs/webpage_bloc.dart';
-import 'history_buttons.dart';
-import 'navigation_field.dart';
-
-// TODO(fxb/45264): Make the common factors as part of Ermine central styles.
-const _kNavbarHeight = 24.0;
-const _kIconSize = 14.0;
-
-enum _LayoutId { historyButtons, url, addTabButton }
-
-class BrowserNavigationBar extends StatelessWidget {
- final WebPageBloc? bloc;
- final VoidCallback newTab;
- final FocusNode _fieldFocus;
-
- const BrowserNavigationBar({
- required this.bloc,
- required this.newTab,
- required FocusNode fieldFocus,
- }) : _fieldFocus = fieldFocus;
-
- @override
- Widget build(BuildContext context) => Material(
- child: SizedBox(
- height: _kNavbarHeight,
- child: Stack(
- children: <Widget>[
- Positioned.fill(child: _buildWidgets(context)),
- if (bloc != null)
- Align(
- alignment: Alignment.bottomCenter,
- child: _buildLoadingIndicator(),
- ),
- ],
- ),
- ),
- );
-
- Widget _buildLoadingIndicator() {
- return AnimatedBuilder(
- animation: bloc!.isLoadedStateNotifier,
- builder: (context, snapshot) => bloc!.isLoadedState
- ? Offstage()
- : SizedBox(
- width: double.infinity,
- height: 4.0,
- child: LinearProgressIndicator(
- color: Theme.of(context).colorScheme.secondary,
- backgroundColor: Colors.transparent,
- ),
- ),
- );
- }
-
- Widget _buildWidgets(BuildContext context) {
- return CustomMultiChildLayout(
- delegate: _LayoutDelegate(),
- children: [
- LayoutId(
- id: _LayoutId.historyButtons,
- child: bloc != null ? HistoryButtons(bloc: bloc!) : Container(),
- ),
- LayoutId(
- id: _LayoutId.url,
- child: bloc != null
- ? NavigationField(bloc: bloc!, focus: _fieldFocus)
- : Container(),
- ),
- LayoutId(
- id: _LayoutId.addTabButton,
- child: _buildNewTabButton(context),
- ),
- ],
- );
- }
-
- Widget _buildNewTabButton(BuildContext context) {
- return GestureDetector(
- onTap: newTab,
- child: Align(
- alignment: Alignment.centerRight,
- child: AspectRatio(
- aspectRatio: 1.0,
- child: Padding(
- padding: const EdgeInsets.all(1.0),
- child: Container(
- color: Theme.of(context).colorScheme.secondary,
- alignment: Alignment.center,
- child: Icon(
- Icons.add,
- key: Key('new_tab'),
- color: Theme.of(context).colorScheme.primary,
- size: _kIconSize,
- ),
- ),
- ),
- ),
- ),
- );
- }
-}
-
-class _LayoutDelegate extends MultiChildLayoutDelegate {
- @override
- void performLayout(Size size) {
- final historyButtonsSize = layoutChild(
- _LayoutId.historyButtons,
- BoxConstraints.tightFor(height: size.height),
- );
- positionChild(_LayoutId.historyButtons, Offset.zero);
- final newTabButtonSize = layoutChild(
- _LayoutId.addTabButton,
- BoxConstraints(
- minHeight: size.height,
- maxHeight: size.height,
- minWidth: historyButtonsSize.width,
- ),
- );
- positionChild(
- _LayoutId.addTabButton,
- size.topRight(-newTabButtonSize.topRight(Offset.zero)),
- );
-
- final urlSize = layoutChild(
- _LayoutId.url,
- BoxConstraints.tightFor(
- width: (size.width - historyButtonsSize.width - newTabButtonSize.width)
- .clamp(0.0, size.width),
- ),
- );
- positionChild(
- _LayoutId.url,
- Offset(
- historyButtonsSize.width,
- (size.height - urlSize.height) * 0.5,
- ),
- );
- }
-
- @override
- bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => false;
-}
diff --git a/bin/simple_browser/lib/src/widgets/navigation_field.dart b/bin/simple_browser/lib/src/widgets/navigation_field.dart
deleted file mode 100644
index 07bd38f..0000000
--- a/bin/simple_browser/lib/src/widgets/navigation_field.dart
+++ /dev/null
@@ -1,100 +0,0 @@
-// Copyright 2019 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 'package:flutter/material.dart';
-import 'package:internationalization/strings.dart';
-
-import '../blocs/webpage_bloc.dart';
-import '../models/webpage_action.dart';
-
-class NavigationField extends StatefulWidget {
- const NavigationField({required this.bloc, required this.focus});
- final WebPageBloc bloc;
- final FocusNode focus;
-
- @override
- _NavigationFieldState createState() => _NavigationFieldState();
-}
-
-class _NavigationFieldState extends State<NavigationField> {
- final _controller = TextEditingController();
-
- @override
- void initState() {
- super.initState();
- widget.focus.addListener(_onFocusChange);
- _setupBloc(null, widget);
- }
-
- @override
- void dispose() {
- _setupBloc(widget, null);
- _controller.dispose();
- widget.focus
- ..removeListener(_onFocusChange)
- ..dispose();
- super.dispose();
- }
-
- @override
- void didUpdateWidget(NavigationField oldWidget) {
- super.didUpdateWidget(oldWidget);
- _setupBloc(oldWidget, widget);
- _updateFocus();
- }
-
- void _setupBloc(NavigationField? oldWidget, NavigationField? newWidget) {
- if (oldWidget?.bloc != newWidget?.bloc) {
- oldWidget?.bloc.urlNotifier.removeListener(_onUrlChanged);
- widget.bloc.urlNotifier.addListener(_onUrlChanged);
- if (newWidget != null) {
- _controller.text = newWidget.bloc.url;
- }
- }
- }
-
- void _updateFocus() {
- if (_controller.text.isEmpty) {
- FocusScope.of(context).requestFocus(widget.focus);
- } else {
- widget.focus.unfocus();
- }
- }
-
- void _onFocusChange() {
- if (widget.focus.hasFocus) {
- _controller.selection =
- TextSelection(baseOffset: 0, extentOffset: _controller.text.length);
- }
- }
-
- void _onUrlChanged() {
- _controller.text = widget.bloc.url;
- _updateFocus();
- }
-
- @override
- Widget build(BuildContext context) => TextField(
- focusNode: widget.focus,
- autofocus: _controller.text.isEmpty,
- controller: _controller,
- cursorWidth: 8,
- cursorRadius: Radius.zero,
- cursorColor: Colors.black,
- enableInteractiveSelection: true,
- textAlign: TextAlign.center,
- keyboardType: TextInputType.url,
- decoration: InputDecoration(
- contentPadding: EdgeInsets.zero,
- // In general, do not use space characters to move graphical elements
- // around, or you are going to have a bad time. :)
- hintText: ' ${Strings.search.toUpperCase()}',
- border: InputBorder.none,
- isDense: true,
- ),
- onSubmitted: (value) =>
- widget.bloc.request.add(NavigateToAction(url: value)),
- textInputAction: TextInputAction.go,
- );
-}
diff --git a/bin/simple_browser/lib/src/widgets/tabs_widget.dart b/bin/simple_browser/lib/src/widgets/tabs_widget.dart
deleted file mode 100644
index 43c950e..0000000
--- a/bin/simple_browser/lib/src/widgets/tabs_widget.dart
+++ /dev/null
@@ -1,733 +0,0 @@
-// Copyright 2019 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 'package:flutter/material.dart';
-import 'package:internationalization/strings.dart';
-import '../blocs/tabs_bloc.dart';
-import '../blocs/webpage_bloc.dart';
-import '../models/tabs_action.dart';
-
-// TODO(fxb/45264): Make the common factors as part of Ermine central styles.
-const _kTabBarHeight = 24.0;
-const _kMinTabWidth = 120.0;
-const _kBorderWidth = 1.0;
-const _kTabPadding = EdgeInsets.symmetric(horizontal: _kTabBarHeight);
-const _kScrollToMargin = _kMinTabWidth / 3;
-const _kScrollAnimationDuration = 300;
-const _kAutoScrollOffset = 5.0;
-const _kIconSize = 14.0;
-
-enum _ScrollDirection { left, right }
-
-@visibleForTesting
-double get kTabBarHeight => _kTabBarHeight;
-
-@visibleForTesting
-double get kMinTabWidth => _kMinTabWidth;
-
-@visibleForTesting
-double get kBorderWidth => _kBorderWidth;
-
-@visibleForTesting
-double get kScrollToMargin => _kScrollToMargin;
-
-/// The list of currently opened tabs in the browser.
-///
-/// Builds different widget trees for the tab list depending on the selected tab
-/// and the number of tabs.
-/// Handles tab rearrangement and list scroll events, and is also involved in drawing
-/// tab borders as they are affected by the tab rearrangement action.
-class TabsWidget extends StatefulWidget {
- final TabsBloc bloc;
- const TabsWidget({required this.bloc});
-
- @override
- _TabsWidgetState createState() => _TabsWidgetState();
-}
-
-class _TabsWidgetState extends State<TabsWidget>
- with TickerProviderStateMixin<TabsWidget> {
- double _tabListWidth = 0.0;
- double _tabWidth = 0.0;
- double _dragCursorOffset = 0.0;
- final _currentTabX = ValueNotifier<double>(0.0);
-
- int _ghostIndex = 0;
- int _dragStartIndex = 0;
- late bool _isDragging;
- late bool _isAnimating;
-
- static const Duration _reorderAnimationDuration = Duration(milliseconds: 200);
- late AnimationController _ghostController;
- late AnimationController _leftNewGhostController;
- late AnimationController _rightNewGhostController;
-
- final _scrollController = ScrollController();
- final _leftScrollButton = _ScrollButton(_ScrollDirection.left);
- final _rightScrollButton = _ScrollButton(_ScrollDirection.right);
-
- late ThemeData _browserTheme;
-
- @override
- void initState() {
- super.initState();
- _isDragging = false;
- _isAnimating = false;
-
- _ghostController =
- AnimationController(vsync: this, duration: _reorderAnimationDuration);
- _leftNewGhostController =
- AnimationController(vsync: this, duration: _reorderAnimationDuration);
- _rightNewGhostController =
- AnimationController(vsync: this, duration: _reorderAnimationDuration);
-
- _ghostController.value = 1.0;
- _leftNewGhostController.value = 0.0;
- _rightNewGhostController.value = 0.0;
-
- _currentTabX.addListener(_onCurrentTabXChanged);
- _leftNewGhostController.addStatusListener(_onLeftNewGhostControllerChanged);
- _rightNewGhostController
- .addStatusListener(_onRightNewGhostControllerChanged);
- _setupBloc(null, widget);
- }
-
- @override
- void dispose() {
- _setupBloc(widget, null);
- _ghostController.dispose();
- _leftNewGhostController
- ..removeStatusListener(_onLeftNewGhostControllerChanged)
- ..dispose();
- _rightNewGhostController
- ..removeStatusListener(_onRightNewGhostControllerChanged)
- ..dispose();
- _scrollController.dispose();
- super.dispose();
- }
-
- @override
- void didUpdateWidget(TabsWidget oldWidget) {
- super.didUpdateWidget(oldWidget);
- _setupBloc(oldWidget, widget);
- }
-
- void _setupBloc(TabsWidget? oldWidget, TabsWidget? newWidget) {
- if (oldWidget?.bloc != newWidget?.bloc) {
- oldWidget?.bloc.currentTabNotifier.removeListener(_onCurrentTabChanged);
- widget.bloc.currentTabNotifier.addListener(_onCurrentTabChanged);
- oldWidget?.bloc.tabsNotifier.removeListener(_onTabsChanged);
- widget.bloc.tabsNotifier.addListener(_onTabsChanged);
- }
- }
-
- @override
- Widget build(BuildContext context) {
- _tabListWidth = MediaQuery.of(context).size.width;
- _browserTheme = Theme.of(context);
-
- return AnimatedBuilder(
- animation: Listenable.merge(
- [widget.bloc.tabsNotifier, widget.bloc.currentTabNotifier]),
- builder: (_, __) {
- if (widget.bloc.tabs.length > 1) {
- _setVariableTabWidth();
- return Container(
- height: _kTabBarHeight,
- decoration: BoxDecoration(
- color: _browserTheme.colorScheme.primary,
- border: Border(
- top: BorderSide(
- color: _browserTheme.colorScheme.secondary,
- width: _kBorderWidth,
- ),
- bottom: BorderSide(
- color: _browserTheme.colorScheme.secondary,
- width: _kBorderWidth,
- ),
- ),
- ),
- child: LayoutBuilder(
- builder: (context, constraints) => _tabWidth > _kMinTabWidth
- ? _buildTabStacks()
- : _buildScrollableTabListWithButtons(),
- ),
- );
- }
- return Offstage();
- });
- }
-
- void _setVariableTabWidth() {
- _tabWidth = (_tabListWidth / widget.bloc.tabs.length)
- .clamp(_kMinTabWidth, _tabListWidth / 2);
- if (!_isDragging) {
- _currentTabX.value = _tabWidth * widget.bloc.currentTabIdx!;
- }
- }
-
- // BUILDERS
-
- Widget _buildScrollableTabListWithButtons() => Row(
- children: <Widget>[
- _buildScrollButton(_leftScrollButton),
- Expanded(child: _buildScrollableTabList()),
- _buildScrollButton(_rightScrollButton),
- ],
- );
-
- Widget _buildScrollableTabList() => NotificationListener<ScrollNotification>(
- onNotification: (scrollNotification) {
- if (scrollNotification is ScrollEndNotification) {
- _onScrollEnd(scrollNotification.metrics);
- }
- return true;
- },
- child: SingleChildScrollView(
- controller: _scrollController,
- scrollDirection: Axis.horizontal,
- physics: NeverScrollableScrollPhysics(),
- child: _buildTabStacks(),
- ),
- );
-
- Widget _buildScrollButton(_ScrollButton button) => GestureDetector(
- onTap: () => _onScrollButtonTap(button),
- child: Container(
- width: _kTabBarHeight,
- height: _kTabBarHeight,
- color: _browserTheme.colorScheme.secondaryVariant,
- child: Center(
- child: AnimatedBuilder(
- animation: button.isEnabled,
- builder: (_, __) => Icon(
- button.icon,
- color: button.isEnabled.value
- ? _browserTheme.colorScheme.primary
- : _browserTheme.colorScheme.primary.withOpacity(0.2),
- size: _kIconSize,
- ),
- ),
- ),
- ),
- );
-
- Widget _buildTabStacks() => Stack(
- children: <Widget>[
- Row(
- children: List.generate(widget.bloc.tabs.length, (index) {
- return _wrapWithGestureDetector(
- index,
- _buildUnselectedTab(index),
- );
- }),
- ),
- AnimatedBuilder(
- animation: _currentTabX,
- builder: (_, __) => Positioned(
- left: _currentTabX.value,
- child: _wrapWithGestureDetector(
- widget.bloc.currentTabIdx!,
- _buildTabWithBorder(widget.bloc.currentTabIdx!),
- ),
- ),
- ),
- ],
- );
-
- // Makes a tab to be rearrangeable and selectable.
- Widget _wrapWithGestureDetector(int index, Widget child) => GestureDetector(
- onHorizontalDragStart: (DragStartDetails details) =>
- _onDragStart(index, details),
- onHorizontalDragUpdate: _onDragUpdate,
- onHorizontalDragEnd: _onDragEnd,
- child: child,
- );
-
- Widget _buildUnselectedTab(int index) {
- final spacing = Container(
- width: _tabWidth,
- height: _kTabBarHeight,
- decoration: BoxDecoration(
- border: _buildBorder(index != 0),
- ),
- );
-
- if (index == _ghostIndex) {
- return _buildGhostTab(_ghostController, spacing);
- }
-
- int actualIndex = index;
-
- // Shifts the tabs located between the moving tab's original and current positions
- // to the right if the it is moving to the left.
- if (_ghostIndex < widget.bloc.currentTabIdx!) {
- //
- if (index > _ghostIndex && index <= widget.bloc.currentTabIdx!) {
- actualIndex = index - 1;
- }
- }
- // Shifts the tabs located between the moving tab's original and current positions
- // to the left if it is moving to the right.
- else if (_ghostIndex > widget.bloc.currentTabIdx!) {
- if (index < _ghostIndex && index >= widget.bloc.currentTabIdx!) {
- actualIndex = index + 1;
- }
- }
-
- final child = _buildTabWithBorder(actualIndex, renderingIndex: index);
-
- // Inserts a potential empty space to the left of the tab which is currently left
- // to the moving tab.
- if (index == _ghostIndex - 1) {
- return Row(
- children: [
- _buildGhostTab(_leftNewGhostController, spacing),
- child,
- ],
- );
- }
- // Inserts a potential empty space to the right of the tab which is currently right
- // to the moving tab.
- else if (index == _ghostIndex + 1) {
- return Row(
- children: [
- child,
- _buildGhostTab(_rightNewGhostController, spacing),
- ],
- );
- }
-
- return child;
- }
-
- Widget _buildTabWithBorder(int index, {int? renderingIndex}) {
- renderingIndex ??= index;
-
- return Container(
- key: Key('tab'),
- width: _tabWidth,
- height: _kTabBarHeight,
- decoration: BoxDecoration(
- color: (index == widget.bloc.currentTabIdx)
- ? _browserTheme.colorScheme.secondary
- : _browserTheme.colorScheme.primary,
- border: _buildBorder(index != widget.bloc.currentTabIdx &&
- !((!_isAnimating) && renderingIndex == 0) &&
- !(_isAnimating &&
- (renderingIndex != _ghostIndex - 1 && renderingIndex == 0))),
- ),
- child: _buildTab(widget.bloc.tabs[index]),
- );
- }
-
- // Creates an empty space for the selected tab on the unselected tab widget list.
- Widget _buildGhostTab(
- AnimationController animationController, Widget child) =>
- SizeTransition(
- sizeFactor: animationController,
- axis: Axis.horizontal,
- axisAlignment: -1.0,
- child: child,
- );
-
- Border _buildBorder(bool hasBorder) => Border(
- left: BorderSide(
- color: hasBorder
- ? _browserTheme.colorScheme.secondary
- : Colors.transparent,
- width: _kBorderWidth,
- ),
- );
-
- Widget _buildTab(WebPageBloc tab) => _TabWidget(
- bloc: tab,
- selected: tab == widget.bloc.currentTab,
- onSelect: () => widget.bloc.request.add(FocusTabAction(tab: tab)),
- onClose: () => widget.bloc.request.add(RemoveTabAction(tab: tab)),
- );
-
- // EVENT HANDLERS
-
- void _onTabsChanged() {
- _syncGhost();
- }
-
- void _onCurrentTabChanged() {
- _syncGhost();
-
- if (_scrollController.hasClients) {
- final viewportWidth = _scrollController.position.viewportDimension;
-
- final offsetForLeftEdge = _currentTabX.value - _kScrollToMargin;
- final offsetForRightEdge =
- _currentTabX.value - viewportWidth + _kMinTabWidth + _kScrollToMargin;
-
- double? newOffset;
-
- if (_scrollController.offset > offsetForLeftEdge) {
- newOffset = offsetForLeftEdge;
- } else if (_scrollController.offset < offsetForRightEdge) {
- newOffset = offsetForRightEdge;
- }
-
- if (newOffset != null) {
- _scrollController.animateTo(
- newOffset,
- duration: Duration(milliseconds: _kScrollAnimationDuration),
- curve: Curves.ease,
- );
- }
- }
- }
-
- void _syncGhost() {
- final currentIdx = widget.bloc.currentTabIdx;
- if (currentIdx == null) {
- return;
- }
- _ghostIndex = currentIdx;
- _currentTabX.value = _tabWidth * _ghostIndex;
- }
-
- // Remembers the current values of properties such as the dragging target tab's
- // position and the scroll controller's offset when dragging starts.
- void _onDragStart(int index, DragStartDetails details) {
- if (index != widget.bloc.currentTabIdx) {
- widget.bloc.request.add(FocusTabAction(tab: widget.bloc.tabs[index]));
- }
- _isDragging = true;
- _dragStartIndex = index;
- _ghostIndex = index;
- double scrollOffset = 0.0;
- if (_scrollController.hasClients) {
- scrollOffset = _scrollController.offset;
- }
-
- // The distance between the mouse cursor and the moving tab's left edge
- // on the X-axis.
- _dragCursorOffset = (_tabWidth * _dragStartIndex - scrollOffset) -
- details.globalPosition.dx;
- }
-
- // Updates the moving tab's position as the mouse cursor moves.
- void _onDragUpdate(DragUpdateDetails details) {
- double scrollOffset = 0.0;
- double dragXMax = _tabListWidth - _tabWidth;
-
- if (_scrollController.hasClients) {
- scrollOffset = _scrollController.offset;
- dragXMax = (_kMinTabWidth * widget.bloc.tabs.length) - _kMinTabWidth;
- }
- double newX = details.globalPosition.dx + _dragCursorOffset + scrollOffset;
- _currentTabX.value = newX.clamp(0.0, dragXMax);
-
- if (_scrollController.hasClients) {
- if (_didHitLeftEdge()) {
- _autoScrollTo(_ScrollDirection.left);
- } else if (_didHitRightEdge()) {
- _autoScrollTo(_ScrollDirection.right);
- }
- }
- }
-
- void _onDragEnd(DragEndDetails details) {
- _isDragging = false;
-
- // Rearranges the selected tab to the currently empty space only when
- // there is no unfinished animations.
- if (!_isAnimating) {
- _completeRearrangement();
- }
- }
-
- void _onLeftNewGhostControllerChanged(AnimationStatus status) {
- if (status == AnimationStatus.completed) {
- _isAnimating = false;
- --_ghostIndex;
- _ghostController.value = 1.0;
- _leftNewGhostController.value = 0.0;
-
- _onAnimationInterrupted();
- }
- }
-
- void _onCurrentTabXChanged() {
- if (_isDragging) {
- // Sees if the moving tab is overlapping more than half of its neighbor tab,
- // then shift the overlapped tab to the left/right accordingly.
- // Does not check while a shifting animation is happening.
- if (!_isAnimating) {
- if (_isOverlappingLeftTabHalf()) {
- _shiftLeftToRight();
- }
- if (_isOverlappingRightTabHalf()) {
- _shiftRightToLeft();
- }
- }
- }
- }
-
- void _onRightNewGhostControllerChanged(AnimationStatus status) {
- if (status == AnimationStatus.completed) {
- _isAnimating = false;
- ++_ghostIndex;
- _ghostController.value = 1.0;
- _rightNewGhostController.value = 0.0;
-
- _onAnimationInterrupted();
- }
- }
-
- // Checks if the DragEnd event occurs before a spacing;s size transition animation
- // finishes, and if so, rearranges the selected tab to the currently empty space.
- void _onAnimationInterrupted() {
- if (_isDragging == false) {
- _completeRearrangement();
- }
- }
-
- // Changes the order of the tabs in the TabsBloc.
- void _completeRearrangement() {
- if (_ghostIndex != _dragStartIndex) {
- widget.bloc.request.add(RearrangeTabsAction(
- originalIndex: _dragStartIndex,
- newIndex: _ghostIndex,
- ));
- }
- _currentTabX.value = _tabWidth * _ghostIndex;
- }
-
- void _onScrollButtonTap(_ScrollButton button) {
- if (!button.isEnabled.value) {
- return;
- }
-
- final currentOffset = _scrollController.offset;
- final newOffset = (_tabListWidth / 2) * button.directionFactor;
-
- _scrollController.animateTo(
- currentOffset + newOffset,
- duration: Duration(milliseconds: _kScrollAnimationDuration),
- curve: Curves.ease,
- );
- }
-
- void _onScrollEnd(ScrollMetrics metrics) {
- if (_canScrollTo(_ScrollDirection.left)) {
- _leftScrollButton.enable();
- } else {
- _leftScrollButton.disable();
- }
-
- if (_canScrollTo(_ScrollDirection.right)) {
- _rightScrollButton.enable();
- } else {
- _rightScrollButton.disable();
- }
- }
-
- // CHECKERS
-
- bool _didHitLeftEdge() {
- final scrollOffset = _scrollController.offset;
-
- return (_currentTabX.value < scrollOffset);
- }
-
- bool _didHitRightEdge() {
- final scrollOffset = _scrollController.offset;
- final listViewportWidth = _scrollController.position.viewportDimension;
-
- return (_currentTabX.value + _kMinTabWidth >
- scrollOffset + listViewportWidth);
- }
-
- bool _isOverlappingLeftTabHalf() {
- if (_ghostIndex < 1) {
- return false;
- }
-
- double leftTabCenterX = _tabWidth * (_ghostIndex - 1) + (_tabWidth / 2);
- if (_currentTabX.value < leftTabCenterX) {
- return true;
- }
- return false;
- }
-
- bool _isOverlappingRightTabHalf() {
- if (_ghostIndex > widget.bloc.tabs.length - 2) {
- return false;
- }
- double rightTabCenterX = _tabWidth * (_ghostIndex + 1) + (_tabWidth / 2);
- if (_currentTabX.value + _tabWidth > rightTabCenterX) {
- return true;
- }
- return false;
- }
-
- bool _canScrollTo(_ScrollDirection direction) {
- switch (direction) {
- case _ScrollDirection.left:
- if (_scrollController.offset <= 0.0) {
- return false;
- }
- return true;
- case _ScrollDirection.right:
- if (_scrollController.offset >=
- (_kMinTabWidth * widget.bloc.tabs.length -
- _scrollController.position.viewportDimension)) {
- return false;
- }
- return true;
- default:
- return true;
- }
- }
-
- // ANIMATORS
-
- void _autoScrollTo(_ScrollDirection direction) {
- final currentOffset = _scrollController.offset;
- final addOnOffset = (direction == _ScrollDirection.left)
- ? _kAutoScrollOffset * -1
- : _kAutoScrollOffset;
-
- final offsetMax = (_tabWidth * widget.bloc.tabs.length) -
- _scrollController.position.viewportDimension;
-
- final newOffset = (currentOffset + addOnOffset).clamp(0.0, offsetMax);
-
- final tabXMax = (_tabWidth * widget.bloc.tabs.length) - _tabWidth;
- _scrollController.jumpTo(newOffset);
- _currentTabX.value = (_currentTabX.value + addOnOffset).clamp(0.0, tabXMax);
- }
-
- void _shiftLeftToRight() {
- _isAnimating = true;
- _ghostController.reverse(from: 1.0);
- _leftNewGhostController.forward(from: 0.0);
- }
-
- void _shiftRightToLeft() {
- _isAnimating = true;
- _ghostController.reverse(from: 1.0);
- _rightNewGhostController.forward(from: 0.0);
- }
-}
-
-/// An individual tab.
-///
-/// Shows the title of its webpage and displays a'close' button when it is selected or
-/// a mouse cursor hovers over it. Also, handles the closing tab event when the close
-/// button is tapped on.
-class _TabWidget extends StatefulWidget {
- const _TabWidget(
- {required this.bloc,
- required this.selected,
- required this.onSelect,
- required this.onClose});
- final WebPageBloc bloc;
- final bool selected;
- final VoidCallback onSelect;
- final VoidCallback onClose;
-
- @override
- _TabWidgetState createState() => _TabWidgetState();
-}
-
-class _TabWidgetState extends State<_TabWidget> {
- final _hovering = ValueNotifier<bool>(false);
- @override
- Widget build(BuildContext context) {
- final baseTheme = Theme.of(context);
- return MouseRegion(
- onEnter: (_) {
- _hovering.value = true;
- },
- onExit: (_) {
- WidgetsBinding.instance?.addPostFrameCallback((_) {
- _hovering.value = false;
- });
- },
- child: DefaultTextStyle(
- style: baseTheme.textTheme.bodyText2!.copyWith(
- color: widget.selected
- ? baseTheme.colorScheme.primary
- : baseTheme.colorScheme.secondary,
- ),
- child: Stack(
- children: <Widget>[
- Center(
- child: Padding(
- padding: _kTabPadding,
- child: AnimatedBuilder(
- animation: widget.bloc.pageTitleNotifier,
- builder: (_, __) => Text(
- widget.bloc.pageTitle ?? Strings.newtab.toUpperCase(),
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- ),
- ),
- ),
- ),
- Positioned(
- right: 0,
- child: AnimatedBuilder(
- animation: _hovering,
- builder: (_, child) => Offstage(
- offstage: !(widget.selected || _hovering.value),
- child: child,
- ),
- child: Padding(
- padding: EdgeInsets.all(4),
- child: GestureDetector(
- key: ValueKey('tab_close'),
- onTap: widget.onClose,
- child: Container(
- color: Colors.transparent,
- alignment: Alignment.center,
- child: Icon(
- Icons.clear,
- color: widget.selected
- ? baseTheme.colorScheme.primary
- : baseTheme.colorScheme.secondary,
- size: _kIconSize,
- ),
- ),
- ),
- ),
- ),
- ),
- ],
- ),
- ),
- );
- }
-}
-
-class _ScrollButton {
- final _ScrollDirection type;
- late IconData icon;
- final ValueNotifier<bool> isEnabled = ValueNotifier<bool>(true);
- double directionFactor = 0.0;
-
- _ScrollButton(this.type)
- : assert(
- type == _ScrollDirection.left || type == _ScrollDirection.right) {
- switch (type) {
- case _ScrollDirection.left:
- icon = Icons.keyboard_arrow_left;
- directionFactor = -1.0;
- break;
- default:
- icon = Icons.keyboard_arrow_right;
- directionFactor = 1.0;
- break;
- }
- }
-
- void disable() => isEnabled.value = false;
- void enable() => isEnabled.value = true;
-}
diff --git a/bin/simple_browser/lib/test_main.dart b/bin/simple_browser/lib/test_main.dart
deleted file mode 100644
index 934097e..0000000
--- a/bin/simple_browser/lib/test_main.dart
+++ /dev/null
@@ -1,22 +0,0 @@
-// 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 'package:flutter/services.dart';
-import 'package:flutter_driver/driver_extension.dart';
-
-import 'main.dart' as entrypoint;
-
-void main() {
- final handler = OptionalMethodChannel('flutter_driver/handler');
- enableFlutterDriverExtension(
- enableTextEntryEmulation: false,
- handler: (String? data) async {
- if (data != null) {
- final result = await handler.invokeMethod(data);
- return result.toString();
- }
- return '';
- });
- entrypoint.main();
-}
diff --git a/bin/simple_browser/meta/simple_browser.cmx b/bin/simple_browser/meta/simple_browser.cmx
deleted file mode 100644
index 00e6a95..0000000
--- a/bin/simple_browser/meta/simple_browser.cmx
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- "include": [
- "//src/chromium/web_engine/meta/shards/web_engine_base.shard.cmx",
- "//src/chromium/web_engine/meta/shards/web_engine_feature_hardware_video_decoder.shard.cmx",
- "//src/chromium/web_engine/meta/shards/web_engine_feature_network.shard.cmx",
- "//src/chromium/web_engine/meta/shards/web_engine_view.shard.cmx",
- "//src/lib/vulkan/application-container.shard.cmx"
- ],
- "program": {
- "data": "data/simple-browser"
- },
- "sandbox": {
- "services": [
- "fuchsia.cobalt.LoggerFactory",
- "fuchsia.intl.PropertyProvider",
- "fuchsia.logger.LogSink",
- "fuchsia.media.Audio",
- "fuchsia.media.ProfileProvider",
- "fuchsia.media.SessionAudioConsumerFactory",
- "fuchsia.sys.Environment",
- "fuchsia.sys.Launcher",
- "fuchsia.ui.input.ImeService",
- "fuchsia.ui.input.ImeVisibilityService",
- "fuchsia.ui.policy.Presenter",
- "fuchsia.ui.shortcut.Registry",
- "fuchsia.web.ContextProvider"
- ]
- }
-}
\ No newline at end of file
diff --git a/bin/simple_browser/pubspec.yaml b/bin/simple_browser/pubspec.yaml
deleted file mode 100644
index 8db4a58..0000000
--- a/bin/simple_browser/pubspec.yaml
+++ /dev/null
@@ -1,6 +0,0 @@
-# Copyright 2019 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.
-
-name: simple_browser
-description: A simple browser for Fuchsia
\ No newline at end of file
diff --git a/bin/simple_browser/target_test/BUILD.gn b/bin/simple_browser/target_test/BUILD.gn
deleted file mode 100644
index d9f0551..0000000
--- a/bin/simple_browser/target_test/BUILD.gn
+++ /dev/null
@@ -1,26 +0,0 @@
-# Copyright 2020 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("//build/components.gni")
-import("//build/flutter/flutter_test_component.gni")
-
-flutter_test_component("target-test-component") {
- component_name = "simple-browser-target-test"
- manifest = "meta/simple_browser_target_test.cmx"
- sources = [ "simple_browser_target_test.dart" ]
-
- deps = [
- "//src/experiences/bin/simple_browser:lib",
- "//third_party/dart-pkg/git/flutter/packages/flutter_test",
- "//third_party/dart-pkg/pub/test",
- ]
-}
-
-# fx test simple-browser-target-test
-fuchsia_test_package("simple-browser-target-test") {
- test_components = [ ":target-test-component" ]
- test_specs = {
- environments = basic_envs
- }
-}
diff --git a/bin/simple_browser/target_test/meta/simple_browser_target_test.cmx b/bin/simple_browser/target_test/meta/simple_browser_target_test.cmx
deleted file mode 100644
index 55515bd..0000000
--- a/bin/simple_browser/target_test/meta/simple_browser_target_test.cmx
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "include": [
- "//src/lib/vulkan/application-container.shard.cmx"
- ],
- "program": {
- "data": "data/simple-browser-target-test"
- },
- "sandbox": {
- "services": [
- "fuchsia.accessibility.semantics.SemanticsManager",
- "fuchsia.cobalt.LoggerFactory",
- "fuchsia.device.NameProvider",
- "fuchsia.fonts.Provider",
- "fuchsia.intl.PropertyProvider",
- "fuchsia.logger.LogSink",
- "fuchsia.media.Audio",
- "fuchsia.media.SessionAudioConsumerFactory",
- "fuchsia.mediacodec.CodecFactory",
- "fuchsia.memorypressure.Provider",
- "fuchsia.net.interfaces.State",
- "fuchsia.netstack.Netstack",
- "fuchsia.posix.socket.Provider",
- "fuchsia.process.Launcher",
- "fuchsia.sys.Environment",
- "fuchsia.sys.Launcher",
- "fuchsia.ui.input.ImeService",
- "fuchsia.ui.input.ImeVisibilityService",
- "fuchsia.ui.policy.Presenter",
- "fuchsia.ui.scenic.Scenic",
- "fuchsia.ui.shortcut.Registry",
- "fuchsia.web.ContextProvider"
- ]
- }
-}
diff --git a/bin/simple_browser/target_test/pubspec.yaml b/bin/simple_browser/target_test/pubspec.yaml
deleted file mode 100644
index 23ed432..0000000
--- a/bin/simple_browser/target_test/pubspec.yaml
+++ /dev/null
@@ -1,12 +0,0 @@
-# Copyright 2019 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.
-
-name: simple_browser
-description: A simple browser for Fuchsia
-
-flutter:
- fonts:
- - family: MaterialIcons
- fonts:
- - asset: fonts/MaterialIcons-Regular.otf
diff --git a/bin/simple_browser/target_test/test/simple_browser_target_test.dart b/bin/simple_browser/target_test/test/simple_browser_target_test.dart
deleted file mode 100644
index f43c614..0000000
--- a/bin/simple_browser/target_test/test/simple_browser_target_test.dart
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright 2019 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.
-
-// TODO(https://fxbug.dev/84961): Fix null safety and remove this language version.
-// @dart=2.9
-
-import 'package:test/test.dart';
-
-void main() {
- test('test stub', () {
- expect(true, true);
- });
-}
diff --git a/bin/simple_browser/test/browser_shortcuts_test.dart b/bin/simple_browser/test/browser_shortcuts_test.dart
deleted file mode 100644
index eefffa1..0000000
--- a/bin/simple_browser/test/browser_shortcuts_test.dart
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright 2019 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 'package:fidl_fuchsia_ui_shortcut/fidl_async.dart' as ui_shortcut
- show RegistryProxy;
-import 'package:flutter/material.dart';
-import 'package:fuchsia_logger/logger.dart';
-import 'package:mockito/mockito.dart';
-// ignore_for_file: implementation_imports
-import 'package:simple_browser/src/blocs/tabs_bloc.dart';
-import 'package:simple_browser/src/utils/browser_shortcuts.dart';
-import 'package:test/test.dart';
-
-void main() {
- late MockRegistryProxy mockRegistryProxy;
- late MockTabsBloc mockTabsBloc;
- const List<String> defaultKeys = [
- 'newTab',
- 'closeTab',
- 'goBack',
- 'goForward',
- 'refresh',
- 'previousTab',
- 'nextTab'
- ];
-
- setupLogger(name: 'browser_shortcuts_test');
-
- setUp(() {
- mockRegistryProxy = MockRegistryProxy();
- mockTabsBloc = MockTabsBloc();
- });
-
- test('Should apply the given action map to BrowserShortcuts', () {
- Map<String, VoidCallback> testActions = {
- 'action1': () {},
- 'action2': () {},
- 'action3': () {},
- };
- BrowserShortcuts bs = BrowserShortcuts(
- tabsBloc: mockTabsBloc, //tabsBloc,
- shortcutRegistry: mockRegistryProxy,
- actions: testActions,
- );
-
- expect(bs.actions.length, 3, reason: '''Expected 3 shortcuts,
- but actually ${bs.actions.length} shortcuts.''');
- testActions.forEach((key, value) {
- expect(bs.actions.containsKey(key), true,
- reason: 'Expected to have $key, but does not.');
- });
- for (String key in defaultKeys) {
- expect(bs.actions.containsKey(key), false,
- reason: 'Expected not to have $key, but does.');
- }
- });
-}
-
-class MockRegistryProxy extends Mock implements ui_shortcut.RegistryProxy {}
-
-class MockTabsBloc extends Mock implements TabsBloc {}
diff --git a/bin/simple_browser/test/sanitize_url_test.dart b/bin/simple_browser/test/sanitize_url_test.dart
deleted file mode 100644
index 90210c6..0000000
--- a/bin/simple_browser/test/sanitize_url_test.dart
+++ /dev/null
@@ -1,79 +0,0 @@
-// Copyright 2019 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 'package:flutter_test/flutter_test.dart';
-import 'package:fuchsia_logger/logger.dart';
-
-// ignore_for_file: implementation_imports
-import 'package:simple_browser/src/utils/sanitize_url.dart';
-
-void main() {
- setupLogger(name: 'sanitize_url_test');
-
- void urlSanitizationTest(String testUrl, String expectedResult) {
- String testResult = sanitizeUrl(testUrl);
-
- expect(testResult, expectedResult,
- reason: 'expected "$expectedResult" but actually "$testResult".');
- }
-
- test('URL Sanitization: URLs with supported schemes.', () {
- // With a URL that causes Format Exception to Dart's Uri.parse().
- urlSanitizationTest('http ://wrongformat',
- 'https://www.google.com/search?q=http+%3A%2F%2Fwrongformat');
-
- // With a supported scheme(https) and a valid TLD(com).
- urlSanitizationTest('https://google.com', 'https://google.com');
-
- // With a supported scheme(http) and a valid TLD(org).
- urlSanitizationTest('http://wikipedia.org', 'http://wikipedia.org');
-
- // With a supported scheme(chrome) and a valid URL.
- urlSanitizationTest('chrome://gpu', 'chrome://gpu');
-
- // With a supported scheme(https) and an invalid TLD.
- urlSanitizationTest('https://google.cooom', 'https://google.cooom');
-
- // localhost with a port number
- urlSanitizationTest('localhost:8000', 'localhost:8000');
- });
-
- test('URL Sanitization: URLs without schemes.', () {
- const String googleSearchUrl = 'https://www.google.com/search?q=';
-
- // With a valid host pattern and a valid TLD.
- urlSanitizationTest('flutter.dev', 'https://flutter.dev');
-
- // With a valid host pattern and a valid TLD and a path
- urlSanitizationTest('flutter.dev/clock', 'https://flutter.dev/clock');
-
- // With an invalid host pattern and a valid TLD.
- urlSanitizationTest(
- 'flu#%*tter.dev', '${googleSearchUrl}flu%23%25%2Atter.dev');
-
- // With a valid host pattern and an invalid TLD.
- urlSanitizationTest('google.cooom', '${googleSearchUrl}google.cooom');
-
- // With a keyword.
- urlSanitizationTest('fuchsia', '${googleSearchUrl}fuchsia');
-
- // With a valid ip address.
- urlSanitizationTest('255.111.18.1', 'https://255.111.18.1');
-
- // With a valid ip address and a path
- urlSanitizationTest('200.102.11.9/hello', 'https://200.102.11.9/hello');
-
- // With an invalid ip address (Each number must be a value between 0 - 255).
- urlSanitizationTest('955.111.18.1', '${googleSearchUrl}955.111.18.1');
-
- // With an invalid ip address (The address must consist of 4 numbers).
- urlSanitizationTest('255.111.18', '${googleSearchUrl}255.111.18');
-
- // With an invalid ip address (Spaced not allowed).
- urlSanitizationTest('255.111.18. 1', '${googleSearchUrl}255.111.18.+1');
-
- // With an invalid ip address (Non-digit characters except '.' not allowed).
- urlSanitizationTest('255.111.18.*1', '${googleSearchUrl}255.111.18.%2A1');
- });
-}
diff --git a/bin/simple_browser/test/simple_browser_test.dart b/bin/simple_browser/test/simple_browser_test.dart
deleted file mode 100644
index 2e02177..0000000
--- a/bin/simple_browser/test/simple_browser_test.dart
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright 2019 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 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:fuchsia_logger/logger.dart';
-import 'package:mockito/mockito.dart';
-
-// ignore_for_file: implementation_imports
-import 'package:simple_browser/app.dart';
-import 'package:simple_browser/src/blocs/tabs_bloc.dart';
-import 'package:simple_browser/src/blocs/webpage_bloc.dart';
-import 'package:simple_browser/src/models/app_model.dart';
-
-void main() {
- setupLogger(name: 'simple_browser_test');
-
- Stream<Locale> lstream;
- TabsBloc tabsBloc;
-
- testWidgets('localized text is displayed in the widgets',
- (WidgetTester tester) async {
- lstream = Stream.fromIterable(
- [Locale.fromSubtags(languageCode: 'sr', countryCode: 'RS')])
- .asBroadcastStream();
-
- tabsBloc = TabsBloc(
- // TODO(https://fxbug.dev/71711): Figure out why `dart analyze` complains
- // about this.
- tabFactory: () => MockWebPageBloc(), // ignore: unnecessary_lambdas
- disposeTab: (tab) {
- tab.dispose();
- },
- );
-
- final model = MockAppModel();
-
- when(model.tabsBloc).thenAnswer((_) => tabsBloc);
- when(model.localeStream).thenAnswer((_) => lstream);
-
- final app = App(model);
-
- await tester.pumpWidget(app);
- await tester.pumpAndSettle();
-
- expect(
- find.byWidgetPredicate(
- (Widget widget) => widget is Title && widget.title == 'Прегледач',
- description: 'A widget with a localized title was displayed'),
- findsOneWidget);
- });
-}
-
-class MockAppModel extends Mock implements AppModel {}
-
-class MockWebPageBloc extends Mock implements WebPageBloc {}
diff --git a/bin/simple_browser/test/tabs_bloc_test.dart b/bin/simple_browser/test/tabs_bloc_test.dart
deleted file mode 100644
index 713e9d8..0000000
--- a/bin/simple_browser/test/tabs_bloc_test.dart
+++ /dev/null
@@ -1,532 +0,0 @@
-// Copyright 2019 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:async';
-import 'package:flutter/foundation.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:fuchsia_logger/logger.dart';
-import 'package:mockito/mockito.dart';
-
-// ignore_for_file: implementation_imports
-import 'package:simple_browser/src/blocs/tabs_bloc.dart';
-import 'package:simple_browser/src/blocs/webpage_bloc.dart';
-import 'package:simple_browser/src/models/tabs_action.dart';
-
-void main() {
- setupLogger(name: 'tabs_bloc_test');
-
- group('newTab', () {
- test('Add 1 tab.', () async {
- // Creates one tab.
- final tb = await _creatNTabs(1);
-
- // Sees if there is only one tab.
- int actual = tb.tabs.length;
- int expected = 1;
- expect(
- actual,
- expected,
- reason: _expectNTabsReason(
- actual.toString(),
- expected.toString(),
- ),
- );
- });
-
- test('Add 2 tabs.', () async {
- // Creates two tabs.
- final tb = await _creatNTabs(2);
-
- // Sees if there are two tabs.
- int actual = tb.tabs.length;
- int expected = 2;
- expect(
- actual,
- expected,
- reason: _expectNTabsReason(
- actual.toString(),
- expected.toString(),
- ),
- );
- });
- });
-
- group('focusTab', () {
- test('Add 3 tabs, focus on the first tab and then the second tab.',
- () async {
- // Test Environment Set Up:
- // Creates three tabs.
- final tb = await _creatNTabs(3);
-
- // Makes sure that the last tab is currently focused.
- int actual = tb.currentTabIdx!;
- int expected = 2;
- expect(
- actual,
- expected,
- reason: _expectFocusedTabReason(
- actual.toString(),
- expected.toString(),
- ),
- );
-
- // Changes the focus to the first tab.
- await _focusTab(tb, tb.tabs[0]);
-
- // Sees if the index of the newly focused tab is 0.
- actual = tb.currentTabIdx!;
- expected = 0;
- expect(
- actual,
- expected,
- reason: _expectFocusedTabReason(
- actual.toString(),
- expected.toString(),
- ),
- );
-
- // Changes the focus to the second tab.
- await _focusTab(tb, tb.tabs[1]);
-
- // Sees if the index of the newly focused tab is 1.
- actual = tb.currentTabIdx!;
- expected = 1;
- expect(
- actual,
- expected,
- reason: _expectFocusedTabReason(
- actual.toString(),
- expected.toString(),
- ),
- );
- });
- });
-
- group('removeTab', () {
- test('Add 1 tab and remove it.', () async {
- // Test Environment Set Up:
- // Creates one tab.
- final tb = await _creatNTabs(1);
-
- // Makes sure that there is only one tab.
- int actual = tb.tabs.length;
- int expected = 1;
- expect(
- actual,
- expected,
- reason: _expectNTabsReason(
- actual.toString(),
- expected.toString(),
- ),
- );
-
- // Closes the tab.
- await _closeTab(tb, tb.currentTab!);
-
- // Sees if there is no tabs anyumore.
- actual = tb.tabs.length;
- expected = 0;
- expect(
- actual,
- expected,
- reason: _expectNTabsReason(
- actual.toString(),
- expected.toString(),
- ),
- );
-
- // Sees if the currentTab indicates null.
- expect(
- tb.currentTab,
- null,
- reason: _expectFocusedTabReason(
- (tb.currentTabIdx).toString(),
- 'null',
- ),
- );
- });
-
- test(
- 'Add 3 tabs, and remove the currently focused tab, which is the last tab.',
- () async {
- // Test Environment Set Up:
- // Creates three tabs and focus on the first tab.
- final tb = await _creatNTabs(3);
-
- // Makes sure that the last tab is currently focused.
- int actualFocusedTabIdx = tb.currentTabIdx!;
- int expectedFocusedTabIdx = 2;
- expect(
- actualFocusedTabIdx,
- expectedFocusedTabIdx,
- reason: _expectFocusedTabReason(
- actualFocusedTabIdx.toString(),
- expectedFocusedTabIdx.toString(),
- ),
- );
-
- // Saves the tab previous to the currently focused tab. (the second tab)
- final expectedTab = tb.tabs[1];
-
- // Closes the currently focused tab. (the last tab)
- await _closeTab(tb, tb.currentTab!);
-
- // Sees if the number of the remaining tabs is 2.
- int actualNumTabs = tb.tabs.length;
- int expectedNumTabs = 2;
- expect(
- actualNumTabs,
- expectedNumTabs,
- reason: _expectNTabsReason(
- actualNumTabs.toString(),
- expectedNumTabs.toString(),
- ),
- );
-
- // Sees if the newly focused tab is the tab previous to the removed tab.
- expect(tb.currentTab, expectedTab,
- reason:
- '''The currently focused tab is expected to be the previous one to the removed one,
- but is actually not.''');
-
- // Sees if the index of the newly focused tab is still 1.
- actualFocusedTabIdx = tb.currentTabIdx!;
- expectedFocusedTabIdx = 1;
- expect(
- actualFocusedTabIdx,
- expectedFocusedTabIdx,
- reason: _expectFocusedTabReason(
- actualFocusedTabIdx.toString(),
- expectedFocusedTabIdx.toString(),
- ),
- );
- });
-
- test('Add 3 tabs, focus on the first tab, and remove it.', () async {
- // Test Environment Set Up:
- // Creates three tabs and focus on the first tab.
- final tb = await _creatNTabs(3);
- await _focusTab(tb, tb.tabs[0]);
-
- // Makes sure that the first tab is currently focused.
- int actualFocusedTabIdx = tb.currentTabIdx!;
- int expectedFocusedTabIdx = 0;
- expect(
- actualFocusedTabIdx,
- expectedFocusedTabIdx,
- reason: _expectFocusedTabReason(
- actualFocusedTabIdx.toString(),
- expectedFocusedTabIdx.toString(),
- ),
- );
-
- // Saves the tab next to the currently focused tab. (the second tab)
- final expectedTab = tb.tabs[1];
-
- // Closes the currently focused tab. (the first tab)
- await _closeTab(tb, tb.currentTab!);
-
- // Sees if the number of the remaining tabs is 2.
- int actualNumTabs = tb.tabs.length;
- int expectedNumTabs = 2;
- expect(
- actualNumTabs,
- expectedNumTabs,
- reason: _expectNTabsReason(
- actualNumTabs.toString(),
- expectedNumTabs.toString(),
- ),
- );
-
- // Sees if the newly focused tab is the tab next to the closed tab.
- expect(tb.currentTab, expectedTab,
- reason:
- '''The currently focused tab is expected to be the next one to the removed one,
- but is actually not.''');
-
- // Sees if the index of the newly focused tab has been shifted to 0.
- actualFocusedTabIdx = tb.currentTabIdx!;
- expectedFocusedTabIdx = 0;
- expect(
- actualFocusedTabIdx,
- expectedFocusedTabIdx,
- reason: _expectFocusedTabReason(
- actualFocusedTabIdx.toString(),
- expectedFocusedTabIdx.toString(),
- ),
- );
- });
-
- test('Add 3 tabs and remove the first one.', () async {
- // Test Environment Set Up:
- // Creates three tabs.
- final tb = await _creatNTabs(3);
-
- // Makes sure that the last tab is currently focused.
- int actualFocusedTabIdx = tb.currentTabIdx!;
- int expectedFocusedTabIdx = 2;
- expect(
- actualFocusedTabIdx,
- expectedFocusedTabIdx,
- reason: _expectFocusedTabReason(
- actualFocusedTabIdx.toString(),
- expectedFocusedTabIdx.toString(),
- ),
- );
-
- // Saves the currently focused tab. (the last tab)
- final expectedTab = tb.currentTab;
-
- // Closes the first tab.
- await _closeTab(tb, tb.tabs[0]);
-
- // Sees if the number of the remaining tabs is 2.
- int actualNumTabs = tb.tabs.length;
- int expectedNumTabs = 2;
- expect(
- actualNumTabs,
- expectedNumTabs,
- reason: _expectNTabsReason(
- actualNumTabs.toString(),
- expectedNumTabs.toString(),
- ),
- );
-
- // Sees if the currently focused tab is still the same one.
- expect(tb.currentTab, expectedTab,
- reason:
- '''The focused tab is expected to be the same before and after closing the second tab,
- but is actually different.''');
-
- // Sees if the index of the currently focused tab has been shifted to 1.
- actualFocusedTabIdx = tb.currentTabIdx!;
- expectedFocusedTabIdx = 1;
- expect(
- actualFocusedTabIdx,
- expectedFocusedTabIdx,
- reason: _expectFocusedTabReason(
- actualFocusedTabIdx.toString(),
- expectedFocusedTabIdx.toString(),
- ),
- );
- });
-
- test('Add 5 tabs and move a tab to the left and right.', () async {
- // Test Environment Set Up:
- // Creates five tabs.
- final tb = await _creatNTabs(5);
-
- // Makes sure that the last tab is currently focused.
- int actualFocusedTabIdx = tb.currentTabIdx!;
- int expectedFocusedTabIdx = 4;
- expect(
- actualFocusedTabIdx,
- expectedFocusedTabIdx,
- reason: _expectFocusedTabReason(
- actualFocusedTabIdx.toString(),
- expectedFocusedTabIdx.toString(),
- ),
- );
-
- // Saves the current tabs.
- WebPageBloc tab0BeforeMove = tb.tabs[0];
- WebPageBloc tab1BeforeMove = tb.tabs[1];
- WebPageBloc tab2BeforeMove = tb.tabs[2];
- WebPageBloc tab3BeforeMove = tb.tabs[3];
- WebPageBloc tab4BeforeMove = tb.tabs[4];
-
- // Moves the currently focused tab (index 4) to the 3rd tab(index 2) position.
- int indexToMove = 4;
- int indexToBe = 2;
- await _rearrangeTabs(tb, indexToMove, indexToBe);
-
- // Sees if the index of the currently focused tab is now 2.
- actualFocusedTabIdx = tb.currentTabIdx!;
- expectedFocusedTabIdx = indexToBe;
- expect(
- actualFocusedTabIdx,
- expectedFocusedTabIdx,
- reason:
- '''Expected the currently focused tab index to be $expectedFocusedTabIdx,
- but actually, is $actualFocusedTabIdx.''',
- );
-
- // Sees if all tabs have been rarranged correctly.
- expect(tb.tabs[0], tab0BeforeMove,
- reason: 'Expected that the 1st tab used to be the 1st.');
- expect(tb.tabs[1], tab1BeforeMove,
- reason: 'Expected that the 2nd tab used to be the 2nd.');
- expect(tb.tabs[2], tab4BeforeMove,
- reason: 'Expected that the 3rd tab used to be the 5th.');
- expect(tb.tabs[3], tab2BeforeMove,
- reason: 'Expected that the 4th tab used to be the 3rd.');
-
- expect(tb.tabs[4], tab3BeforeMove,
- reason: 'Expected that the 5th tab used to the 4th.');
-
- // Saves the rearranged tabs.
- tab0BeforeMove = tb.tabs[0];
- tab1BeforeMove = tb.tabs[1];
- tab2BeforeMove = tb.tabs[2];
- tab3BeforeMove = tb.tabs[3];
- tab4BeforeMove = tb.tabs[4];
-
- // Moves the 2nd tab(index 1) to the 4th tab(index 3) position.
- indexToMove = 1;
- indexToBe = 3;
- await _rearrangeTabs(tb, indexToMove, indexToBe);
-
- // Sees if the currently focused tab has been shifted to the right and now has
- // 3 as its index.
- actualFocusedTabIdx = tb.currentTabIdx!;
- expectedFocusedTabIdx = 1;
- expect(
- actualFocusedTabIdx,
- expectedFocusedTabIdx,
- reason: '''Expected that the currently focused tab has been moved to
- index $expectedFocusedTabIdx, but actually, its index is $actualFocusedTabIdx.''',
- );
-
- // Sees if all tabs have been rarranged correctly.
- expect(tb.tabs[0], tab0BeforeMove,
- reason: 'Expected that the 1st tab used to be the 1st.');
- expect(tb.tabs[1], tab2BeforeMove,
- reason: 'Expected that the 2nd tab used to be the 3rd.');
- expect(tb.tabs[2], tab3BeforeMove,
- reason: 'Expected that the 3rd tab used to be the 4th.');
- expect(tb.tabs[3], tab1BeforeMove,
- reason: 'Expected that the 4th tab used to be the 2nd.');
-
- expect(tb.tabs[4], tab4BeforeMove,
- reason: 'Expected that the 5th tab used to the 5th.');
- });
- });
-
- group('isOnlyTab, previousTab, and nextTab getters', () {
- test(
- '''isOnlyTab getter should return true when there is only one tab in the TabsBloc,
- and return false when another tab is added.''', () async {
- // Creates one tab.
- final tb = await _creatNTabs(1);
-
- // Sees if isOnlyTab getter returns true.
- expect(
- tb.isOnlyTab,
- true,
- reason:
- 'isOnlyTab is expected to be true, but is actually ${tb.isOnlyTab.toString()}.',
- );
-
- // Creates one more tab.
- await _newTab(tb);
-
- // Sees if isOnlyTab getter returns false.
- expect(
- tb.isOnlyTab,
- false,
- reason:
- 'isOnlyTab is expected to be false, but is actually ${tb.isOnlyTab.toString()}.',
- );
- });
-
- test(
- '''previousTab getter should return the previous tab to the currently focused tab,
- and the nextTab getter should return the next tab to the currently focused tab.''',
- () async {
- // Creates two tabs.
- final tb = await _creatNTabs(2);
-
- // Sees if the previousTab getter returns the first tab.
- expect(
- tb.previousTab,
- tb.tabs.first,
- reason: '''previousTab is expected to be the first tab,
- but is actually ${tb.tabs.indexOf(tb.previousTab)}th tab.''',
- );
-
- // Changes the focus to the first tab.
- await _focusTab(tb, tb.tabs[0]);
-
- // Sees if the nextTab getter returns the second(last) tab.
- expect(
- tb.nextTab,
- tb.tabs.last,
- reason: '''nextTab is expected to be the last tab,
- but is actually ${tb.tabs.indexOf(tb.nextTab)}th tab.''',
- );
- });
- });
-}
-
-Future<TabsBloc> _creatNTabs(int n) async {
- TabsBloc tb = TabsBloc(
- // TODO(https://fxbug.dev/71711): Figure out why `dart analyze` complains
- // about this.
- tabFactory: () => MockWebPageBloc(), // ignore: unnecessary_lambdas
- disposeTab: (tab) => tab.dispose(),
- );
-
- for (int i = 0; i < n; i++) {
- await _newTab(tb);
- }
- return tb;
-}
-
-String _expectNTabsReason(String actual, String expected) =>
- 'TabsBloc is expected to have $expected tabs, but actually has $actual tabs.';
-
-String _expectFocusedTabReason(String actual, String expected) =>
- 'The index of the currently focused tab is expected to be $expected, but is actually $actual.';
-
-/// awaits for a single callback from a [Listenable]
-Future _awaitListenable(Listenable listenable) {
- final c = Completer();
- late VoidCallback l;
- l = () {
- c.complete();
- listenable.removeListener(l);
- };
- listenable.addListener(l);
- return c.future;
-}
-
-/// sends an [TabsAction] to [TabsBloc] and awaits for a callback in a [Listenable]
-Future _addActionAndAwait(
- TabsBloc tb,
- Listenable listenable,
- TabsAction action,
-) async {
- tb.request.add(action);
- await _awaitListenable(listenable);
-}
-
-/// adds a new tab to a [TabsBloc] and awaits completion with [TabsBloc.tabs]
-Future _newTab(TabsBloc tb) => _addActionAndAwait(
- tb,
- tb.tabsNotifier,
- NewTabAction(),
- );
-
-/// sets focus to a tab in [TabsBloc] and awaits completion with [TabsBloc.currentTab]
-Future _focusTab(TabsBloc tb, WebPageBloc tab) => _addActionAndAwait(
- tb,
- tb.currentTabNotifier,
- FocusTabAction(tab: tab),
- );
-
-Future _closeTab(TabsBloc tb, WebPageBloc tab) => _addActionAndAwait(
- tb,
- tb.tabsNotifier,
- RemoveTabAction(tab: tab),
- );
-
-Future _rearrangeTabs(TabsBloc tb, int startIndex, int endIndex) =>
- _addActionAndAwait(
- tb,
- tb.tabsNotifier,
- RearrangeTabsAction(originalIndex: startIndex, newIndex: endIndex),
- );
-
-class MockWebPageBloc extends Mock implements WebPageBloc {}
diff --git a/bin/simple_browser/test/tld_checker_test.dart b/bin/simple_browser/test/tld_checker_test.dart
deleted file mode 100644
index f5c36d4..0000000
--- a/bin/simple_browser/test/tld_checker_test.dart
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright 2019 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 'package:fuchsia_logger/logger.dart';
-// ignore_for_file: implementation_imports
-import 'package:simple_browser/src/utils/tld_checker.dart';
-import 'package:simple_browser/src/utils/tlds_provider.dart';
-import 'package:test/test.dart';
-
-void main() {
- setupLogger(name: 'tld_checker_test');
- test('TLD Validity check test with local TLD list.', () {
- expect(TldChecker().isValid('dev'), true, reason: '"dev" should be valid.');
- expect(TldChecker().isValid('asdf'), false,
- reason: '"asdf" should be invalid.');
- });
-
- test('Fetch test with two valid TLDs and one comment line.', () async {
- String testData = '''
- # version 2019111500, Last Updated Fri Nov 15 07:07:01 2019 UTC
- AAA
- BBB
- ''';
- TldsProvider provider = TldsProvider()..data = testData;
- List<String>? testTlds = await provider.fetchTldsList();
- TldChecker().prefetchTlds(testTlds: testTlds);
-
- expect(TldChecker().validTlds.length, 2,
- reason: 'The length of valid TLD list should be two.');
- expect(TldChecker().isValid('aaa'), true, reason: '"aaa" should be valid.');
- expect(TldChecker().isValid('bbb'), true, reason: '"bbb should be valid.');
- });
-
- test('Fetch test with one valid TLDs and multiple comment lines.', () async {
- String testData = '''
- # version 2019111500, Last Updated Fri Nov 15 07:07:01 2019 UTC
- # Lorem ipsum dolor sit amet, consectetur adipiscing elit.
- # Donec sed libero at lacus blandit scelerisque.
- # Aliquam at felis ac orci facilisis tincidunt.
- # Nullam eu justo faucibus, pretium urna et, pretium magna.
- # Duis faucibus elit id felis sollicitudin, quis fermentum neque convallis.
- QQQ
- ''';
-
- TldsProvider provider = TldsProvider()..data = testData;
- List<String>? testTlds = await provider.fetchTldsList();
- TldChecker().prefetchTlds(testTlds: testTlds);
-
- expect(TldChecker().validTlds.length, 1,
- reason: 'The length of valid TLD list should be one.');
- expect(TldChecker().isValid('qqq'), true, reason: '"qqq" should be valid.');
- });
-}
diff --git a/bin/simple_browser/test/webpage_bloc_test.dart b/bin/simple_browser/test/webpage_bloc_test.dart
deleted file mode 100644
index 163f72b..0000000
--- a/bin/simple_browser/test/webpage_bloc_test.dart
+++ /dev/null
@@ -1,237 +0,0 @@
-// Copyright 2019 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 'package:fidl_fuchsia_web/fidl_async.dart' as web;
-import 'package:fuchsia_logger/logger.dart';
-import 'package:mockito/mockito.dart';
-// ignore_for_file: implementation_imports
-import 'package:simple_browser/src/blocs/webpage_bloc.dart';
-import 'package:simple_browser/src/models/webpage_action.dart';
-import 'package:simple_browser/src/services/simple_browser_navigation_event_listener.dart';
-import 'package:simple_browser/src/services/simple_browser_web_service.dart';
-import 'package:test/test.dart';
-
-void main() {
- setupLogger(name: 'webpage_bloc_test');
-
- late MockSimpleBrowserWebService mockSimpleBrowserWebService;
- late MockNavigationState mockNavigationState;
- late WebPageBloc webPageBloc;
- late SimpleBrowserNavigationEventListener
- simpleBrowserNavigationEventListener;
-
- setUp(() {
- mockSimpleBrowserWebService = MockSimpleBrowserWebService();
- mockNavigationState = MockNavigationState();
- simpleBrowserNavigationEventListener =
- SimpleBrowserNavigationEventListener();
- when(mockSimpleBrowserWebService.navigationEventListener)
- .thenReturn(simpleBrowserNavigationEventListener);
- webPageBloc = WebPageBloc(
- webService: mockSimpleBrowserWebService,
- );
- });
-
- /// Tests if relavent getters in [WebPageBloc] are properly getting updated values
- /// in [SimpleBrowserNavigationEventListener] when [NavigationState] is changed.
- group('onNavigationStateChanged', () {
- test('''WebPageBloc.url should be updated
- when NavigationState.url changed.''', () {
- // url should be an empty string by default.
- expect(webPageBloc.url, '',
- reason: '''The initial value of url is expected to be a blank,
- but is actually ${webPageBloc.url}.''');
-
- // When NavigationState.url is changed to 'https://www.flutter.dev'.
- String testUrl = 'https://www.flutter.dev';
- when(mockNavigationState.url).thenReturn(testUrl);
- simpleBrowserNavigationEventListener
- .onNavigationStateChanged(mockNavigationState);
- expect(webPageBloc.url, testUrl,
- reason: '''url value is expected to be updated to $testUrl,
- but is actually ${webPageBloc.url}.''');
- });
-
- test('''WebPageBloc.forwardState should be updated
- when NavigationState.canGoForward changed.''', () {
- // forwardState should be false by default.
- expect(webPageBloc.forwardState, false,
- reason: '''The initial value of forwardState is expected to be false,
- but is actually ${webPageBloc.forwardState}.''');
-
- // when NavigationState.canGoForward is changed to true.
- when(mockNavigationState.canGoForward).thenReturn(true);
- simpleBrowserNavigationEventListener
- .onNavigationStateChanged(mockNavigationState);
- expect(webPageBloc.forwardState, true,
- reason: '''forwardState is expected to be updated to true,
- but is actually ${webPageBloc.forwardState}.''');
- });
-
- test('''WebPageBloc.backState should be updated
- when NavigationState.canGoBack changed.''', () {
- // backState should be false by default.
- expect(webPageBloc.backState, false,
- reason: '''The initial value of backState is expected to be false,
- but is actually ${webPageBloc.backState}.''');
-
- // when NavigationState.canGoBack is changed to true.
- when(mockNavigationState.canGoBack).thenReturn(true);
- simpleBrowserNavigationEventListener
- .onNavigationStateChanged(mockNavigationState);
- expect(webPageBloc.backState, true,
- reason: '''backState is expected to be updated to true,
- but is actually ${webPageBloc.backState}.''');
- });
-
- test('''WebPageBloc.isLoadedState should be updated
- when NavigationState.isMainDocumentLoaded changed.''', () {
- // isLoadedState should be true by default.
- expect(webPageBloc.isLoadedState, true,
- reason: '''The initial value of isLoadedState is expected to be true,
- but is actually ${webPageBloc.isLoadedState}.''');
-
- // when NavigationState.isMainDocumentLoaded is changed to false.
- when(mockNavigationState.isMainDocumentLoaded).thenReturn(false);
- simpleBrowserNavigationEventListener
- .onNavigationStateChanged(mockNavigationState);
- expect(webPageBloc.isLoadedState, false,
- reason: '''isLoadedState is expected to be updated to false,
- but is actually ${webPageBloc.isLoadedState}.''');
- });
-
- test('''WebPageBloc.pageTitle should be updated
- when NavigationState.title changed.''', () {
- // pageTitle has no default value.
-
- // when NavigationState.title is changed to 'test'.
- String testTitle = 'test';
- when(mockNavigationState.title).thenReturn(testTitle);
- simpleBrowserNavigationEventListener
- .onNavigationStateChanged(mockNavigationState);
- expect(webPageBloc.pageTitle, testTitle,
- reason: '''pageTitle is expected to be updated to $testTitle,
- but is actually ${webPageBloc.pageTitle}.''');
- });
-
- test('''WebPageBloc.pageType should be updated
- when NavigationState.pageType changed.''', () {
- // pageType should be PageType.empty by default.
- expect(webPageBloc.pageType, PageType.empty,
- reason:
- '''The initial value of pageType is expected to be PageType.empty,
- but is actually ${webPageBloc.pageType.toString()}.''');
-
- // when NavigationState.pageType is changed to web.PageType.normal.
- web.PageType testPageType = web.PageType.normal;
- when(mockNavigationState.pageType).thenReturn(testPageType);
- simpleBrowserNavigationEventListener
- .onNavigationStateChanged(mockNavigationState);
- expect(webPageBloc.pageType, PageType.normal,
- reason: '''pageType is expected to be updated to PageType.normal,
- but is actually ${webPageBloc.pageType.toString()}.''');
-
- // when NavigationState.pageType is changed to web.PageType.error.
- testPageType = web.PageType.error;
- when(mockNavigationState.pageType).thenReturn(testPageType);
- simpleBrowserNavigationEventListener
- .onNavigationStateChanged(mockNavigationState);
- expect(webPageBloc.pageType, PageType.error,
- reason: '''pageType is expected to be updated to PageType.error,
- but is actually ${webPageBloc.pageType.toString()}.''');
- });
- });
-
- /// Tests [WebPageBloc]'s [StreamController] and its callback [_onActionChanged].
- ///
- /// Verify if relavent methods in [SimpleBrowserWebService] are called,
- /// and irrelavent ones are not called through the callback when a [WebPageAction]
- /// is added to the bloc.
- group('Handling actions', () {
- test('''
- Should call NavigationControllerProxy.loadUrl() with the given url
- when NavigateToAction is added to the webPageBloc with a normal url.
- ''', () async {
- String testUrl = 'https://www.google.com';
-
- webPageBloc.request.add(NavigateToAction(url: testUrl));
-
- await untilCalled(mockSimpleBrowserWebService.loadUrl(any));
- verify(webPageBloc.webService.loadUrl(testUrl)).called(1);
- verifyNever(webPageBloc.webService.goBack());
- verifyNever(webPageBloc.webService.goForward());
- verifyNever(webPageBloc.webService.refresh());
- });
-
- test('''
- Should call NavigationControllerProxy.loadUrl() with the given url
- when NavigateToAction is added to the webPageBloc with a search query url.
- ''', () async {
- String testUrl = 'https://www.google.com/search?q=cat';
-
- webPageBloc.request.add(NavigateToAction(url: testUrl));
-
- await untilCalled(mockSimpleBrowserWebService.loadUrl(any));
- verify(webPageBloc.webService.loadUrl(testUrl)).called(1);
- verifyNever(webPageBloc.webService.goBack());
- verifyNever(webPageBloc.webService.goForward());
- verifyNever(webPageBloc.webService.refresh());
- });
-
- test('''
- Should call NavigationControllerProxy.goBack()
- when GoBackAction is added to the webPageBloc.
- ''', () async {
- webPageBloc.request.add(GoBackAction());
-
- await untilCalled(webPageBloc.webService.goBack());
- verify(webPageBloc.webService.goBack()).called(1);
- verifyNever(mockSimpleBrowserWebService.loadUrl(any));
- verifyNever(webPageBloc.webService.goForward());
- verifyNever(webPageBloc.webService.refresh());
- });
-
- test('''
- Should call NavigationControllerProxy.goForward()
- when GoBackAction is added to the webPageBloc.
- ''', () async {
- webPageBloc.request.add(GoForwardAction());
-
- await untilCalled(webPageBloc.webService.goForward());
- verify(webPageBloc.webService.goForward()).called(1);
- verifyNever(mockSimpleBrowserWebService.loadUrl(any));
- verifyNever(webPageBloc.webService.goBack());
- verifyNever(webPageBloc.webService.refresh());
- });
-
- test('''
- Should call NavigationControllerProxy.reload()
- when RefreshAction is added to the webPageBloc.
- ''', () async {
- webPageBloc.request.add(RefreshAction());
-
- await untilCalled(webPageBloc.webService.refresh());
- verify(webPageBloc.webService.refresh()).called(1);
- verifyNever(mockSimpleBrowserWebService.loadUrl(any));
- verifyNever(webPageBloc.webService.goBack());
- verifyNever(webPageBloc.webService.goForward());
- });
- });
-}
-
-class MockSimpleBrowserWebService extends Mock
- implements SimpleBrowserWebService {
- @override
- Future<void> loadUrl(String? url) =>
- super.noSuchMethod(Invocation.method(#loadUrl, [url]));
-}
-
-class MockNavigationState extends Mock implements web.NavigationState {
- @override
- bool operator ==(dynamic other) =>
- super.noSuchMethod(Invocation.setter(#==, other));
-
- @override
- int get hashCode => super.noSuchMethod(Invocation.getter(#hashcode));
-}
diff --git a/bin/simple_browser/test/widgets/error_page_test.dart b/bin/simple_browser/test/widgets/error_page_test.dart
deleted file mode 100644
index 8f998bb..0000000
--- a/bin/simple_browser/test/widgets/error_page_test.dart
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright 2019 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 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:fuchsia_logger/logger.dart';
-
-// ignore_for_file: implementation_imports
-import 'package:simple_browser/src/widgets/error_page.dart';
-
-void main() {
- setupLogger(name: 'error_page_test');
-
- late double bodyWidth;
- late double bodyHeight;
-
- setUpAll(() {
- bodyWidth = 800.0;
- bodyHeight = 600.0;
- });
-
- testWidgets('There should be 5 text widgets: E,R,R,O,R.',
- (WidgetTester tester) async {
- await _setUpErrorPage(tester, bodyWidth, bodyHeight);
-
- // Sees if there are one ‘E’, one ‘O’ and three ‘R’ texts.
- expect(find.text('E'), findsOneWidget,
- reason: 'Expected an E on the error page.');
- expect(find.text('O'), findsOneWidget,
- reason: 'Expected an O on the error page.');
- expect(find.text('R'), findsNWidgets(3),
- reason: 'Expected three Rs on the error page.');
- });
-
- testWidgets('There should be 5 Positioned widgets in the intended order.',
- (WidgetTester tester) async {
- await _setUpErrorPage(tester, bodyWidth, bodyHeight);
-
- // Sees if there are 5 Positioned widgets
- expect(find.byType(Positioned), findsNWidgets(5),
- reason: 'Expected 5 Positioned widgets on the error page.');
-
- // Sees if all those Positioned widgets are positioned on the intended locations.
-
- // Verifies the left offsets of the widgets.
- final e = find.text('E');
- final r = find.text('R');
- final o = find.text('O');
-
- // Sees if each character is displayed in the correct order.
- _expectAToBeFollowedByB(tester, e, r.at(0));
- _expectAToBeFollowedByB(tester, r.at(0), r.at(1));
- _expectAToBeFollowedByB(tester, r.at(1), o);
- _expectAToBeFollowedByB(tester, o, r.at(2));
- });
-}
-
-Future<void> _setUpErrorPage(
- WidgetTester tester, double width, double height) async {
- await tester.pumpWidget(MaterialApp(
- home: Scaffold(
- body: Container(
- width: width,
- height: height,
- child: ErrorPage(),
- ),
- ),
- ));
-}
-
-void _expectAToBeFollowedByB(WidgetTester tester, Finder a, Finder b) {
- expect(tester.getTopLeft(a).dx < tester.getTopLeft(b).dx, true,
- reason: 'Expected $a to be followed by $b when an error page created.');
-}
diff --git a/bin/simple_browser/test/widgets/history_buttons_test.dart b/bin/simple_browser/test/widgets/history_buttons_test.dart
deleted file mode 100644
index bab00c6..0000000
--- a/bin/simple_browser/test/widgets/history_buttons_test.dart
+++ /dev/null
@@ -1,164 +0,0 @@
-// Copyright 2019 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 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:fuchsia_logger/logger.dart';
-import 'package:mockito/mockito.dart';
-
-// ignore_for_file: implementation_imports
-import 'package:simple_browser/src/blocs/webpage_bloc.dart';
-import 'package:simple_browser/src/services/simple_browser_navigation_event_listener.dart';
-import 'package:simple_browser/src/services/simple_browser_web_service.dart';
-import 'package:simple_browser/src/widgets/history_buttons.dart';
-
-enum ButtonType {
- back,
- forward,
- refresh,
-}
-
-void main() {
- setupLogger(name: 'history_buttons_test');
-
- late WebPageBloc webPageBloc;
- late MockSimpleBrowserWebService mockWebService;
- late MockSimpleBrowserNavigationEventListener mockEventListener;
-
- ValueNotifier backStateNotifier = ValueNotifier<bool>(false);
- ValueNotifier forwardStateNotifier = ValueNotifier<bool>(false);
- ValueNotifier urlNotifier = ValueNotifier<String>('');
- ValueNotifier pageTypeNotifier = ValueNotifier<PageType>(PageType.empty);
-
- setUpAll(() {
- mockEventListener = MockSimpleBrowserNavigationEventListener();
- when(mockEventListener.backStateNotifier)
- .thenAnswer((_) => backStateNotifier);
- when(mockEventListener.forwardStateNotifier)
- .thenAnswer((_) => forwardStateNotifier);
- when(mockEventListener.urlNotifier).thenAnswer((_) => urlNotifier);
-
- when(mockEventListener.backState)
- .thenAnswer((_) => backStateNotifier.value);
- when(mockEventListener.forwardState)
- .thenAnswer((_) => forwardStateNotifier.value);
- when(mockEventListener.pageType).thenAnswer((_) => pageTypeNotifier.value);
-
- mockWebService = MockSimpleBrowserWebService();
- when(mockWebService.navigationEventListener)
- .thenAnswer((_) => mockEventListener);
- webPageBloc = WebPageBloc(
- webService: mockWebService,
- );
- });
-
- testWidgets('There should be 3 text widgets: BCK, FWD, and RFRSH.',
- (WidgetTester tester) async {
- await _setUpHistoryButtons(tester, webPageBloc);
-
- // Sees if there are a ‘BCK’, a 'FWD' and a 'RFRSH' texts.
- expect(find.text('BCK'), findsOneWidget);
- expect(find.text('FWD'), findsOneWidget);
- expect(find.text('RFRSH'), findsOneWidget);
- });
-
- group('Buttons are all disabled', () {
- testWidgets('A disalbed button should not work when tapped.',
- (WidgetTester tester) async {
- await _setUpHistoryButtons(tester, webPageBloc);
-
- final historyButtons = _findHistoryButtons();
-
- // Taps the back button and sees whether it works or not.
- await _tapHistoryButton(tester, historyButtons, ButtonType.back);
- _verifyAllNeverWork(webPageBloc);
-
- // Taps the forward button and sees whether it works or not.
- await _tapHistoryButton(tester, historyButtons, ButtonType.forward);
- _verifyAllNeverWork(webPageBloc);
-
- // Taps the refresh button and sees whether it works or not.
- await _tapHistoryButton(tester, historyButtons, ButtonType.refresh);
- _verifyAllNeverWork(webPageBloc);
- });
- });
-
- group('Buttons are all enabled', () {
- String testUrl;
-
- // Set-ups for enabling the history buttons.
- setUp(() {
- testUrl = 'https://www.google.com';
- backStateNotifier.value = true;
- forwardStateNotifier.value = true;
- urlNotifier.value = testUrl;
- pageTypeNotifier.value = PageType.normal;
- });
-
- testWidgets('An enabled button should work when tapped.',
- (WidgetTester tester) async {
- await _setUpHistoryButtons(tester, webPageBloc);
-
- final historyButtons = _findHistoryButtons();
-
- // Taps the back button and sees whether it works or not.
- await _tapHistoryButton(tester, historyButtons, ButtonType.back);
- verify(webPageBloc.webService.goBack());
- verifyNever(webPageBloc.webService.goForward());
- verifyNever(webPageBloc.webService.refresh());
-
- // Taps the forward button and sees whether it works or not.
- await _tapHistoryButton(tester, historyButtons, ButtonType.forward);
- verifyNever(webPageBloc.webService.goBack());
- verify(webPageBloc.webService.goForward());
- verifyNever(webPageBloc.webService.refresh());
-
- // Taps the refresh button and sees whether it works or not.
- await _tapHistoryButton(tester, historyButtons, ButtonType.refresh);
- verifyNever(webPageBloc.webService.goBack());
- verifyNever(webPageBloc.webService.goForward());
- verify(webPageBloc.webService.refresh());
- });
- });
-}
-
-Future<void> _setUpHistoryButtons(
- WidgetTester tester,
- WebPageBloc bloc,
-) async {
- await tester.pumpWidget(
- MaterialApp(
- home: Scaffold(
- body: HistoryButtons(
- bloc: bloc,
- ),
- ),
- ),
- );
- await tester.pumpAndSettle();
-
- expect(_findHistoryButtons(), findsNWidgets(3),
- reason: 'Expected 3 history buttons on the HistoryButtons widget.');
-}
-
-Finder _findHistoryButtons() => find.byType(GestureDetector);
-
-Future<void> _tapHistoryButton(
- WidgetTester tester, Finder buttons, ButtonType target) async {
- int index = target.index;
-
- await tester.tap(buttons.at(index));
- await tester.pumpAndSettle();
-}
-
-void _verifyAllNeverWork(WebPageBloc bloc) {
- verifyNever(bloc.webService.goBack());
- verifyNever(bloc.webService.goForward());
- verifyNever(bloc.webService.refresh());
-}
-
-class MockSimpleBrowserNavigationEventListener extends Mock
- implements SimpleBrowserNavigationEventListener {}
-
-class MockSimpleBrowserWebService extends Mock
- implements SimpleBrowserWebService {}
diff --git a/bin/simple_browser/test/widgets/navigation_bar_test.dart b/bin/simple_browser/test/widgets/navigation_bar_test.dart
deleted file mode 100644
index 6aeb048..0000000
--- a/bin/simple_browser/test/widgets/navigation_bar_test.dart
+++ /dev/null
@@ -1,170 +0,0 @@
-// Copyright 2019 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 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:fuchsia_logger/logger.dart';
-import 'package:mockito/mockito.dart';
-
-// ignore_for_file: implementation_imports
-import 'package:simple_browser/src/blocs/tabs_bloc.dart';
-import 'package:simple_browser/src/blocs/webpage_bloc.dart';
-import 'package:simple_browser/src/models/tabs_action.dart';
-import 'package:simple_browser/src/services/simple_browser_navigation_event_listener.dart';
-import 'package:simple_browser/src/services/simple_browser_web_service.dart';
-import 'package:simple_browser/src/widgets/history_buttons.dart';
-import 'package:simple_browser/src/widgets/navigation_bar.dart';
-import 'package:simple_browser/src/widgets/navigation_field.dart';
-
-void main() {
- setupLogger(name: 'navigation_bar_test');
-
- WebPageBloc? webPageBloc;
- MockSimpleBrowserWebService mockWebService;
- MockSimpleBrowserNavigationEventListener mockEventListener;
-
- ValueNotifier backStateNotifier = ValueNotifier<bool>(false);
- ValueNotifier forwardStateNotifier = ValueNotifier<bool>(false);
- ValueNotifier urlNotifier = ValueNotifier<String>('');
- ValueNotifier pageTypeNotifier = ValueNotifier<PageType>(PageType.empty);
- ValueNotifier isLoadedStateNotifier = ValueNotifier<bool>(true);
-
- group('The bloc is null.', () {
- testWidgets('Should create two empty containers and one + button.',
- (WidgetTester tester) async {
- await _setUpNavigationBar(tester, webPageBloc, () {});
-
- const reasonSuffix =
- 'when the NavigationBar widget has a null webPageBloc.';
-
- expect(find.byType(Container), findsNWidgets(3),
- reason: 'Expected three containers $reasonSuffix');
-
- // Sees if there are two empty Containers.
- expect(
- find.byWidgetPredicate(
- (Widget widget) => widget is Container && widget.child == null,
- description: 'Empty containers.',
- ),
- findsNWidgets(2),
- reason: 'Expected two of those containers were empty $reasonSuffix');
-
- // Sees if there are one + button.
- expect(_findNewTabButton(), findsOneWidget,
- reason:
- 'Expected one of those containers was a + button $reasonSuffix');
- });
- });
-
- group('The bloc is not null.', () {
- setUp(() {
- mockEventListener = MockSimpleBrowserNavigationEventListener();
- when(mockEventListener.backStateNotifier)
- .thenAnswer((_) => backStateNotifier);
- when(mockEventListener.forwardStateNotifier)
- .thenAnswer((_) => forwardStateNotifier);
- when(mockEventListener.urlNotifier).thenAnswer((_) => urlNotifier);
- when(mockEventListener.isLoadedStateNotifier)
- .thenAnswer((_) => isLoadedStateNotifier);
-
- when(mockEventListener.backState)
- .thenAnswer((_) => backStateNotifier.value);
- when(mockEventListener.forwardState)
- .thenAnswer((_) => forwardStateNotifier.value);
- when(mockEventListener.pageType)
- .thenAnswer((_) => pageTypeNotifier.value);
- when(mockEventListener.isLoadedState)
- .thenAnswer((_) => isLoadedStateNotifier.value);
-
- mockWebService = MockSimpleBrowserWebService();
- when(mockWebService.navigationEventListener)
- .thenAnswer((_) => mockEventListener);
- webPageBloc = WebPageBloc(
- webService: mockWebService,
- );
- });
-
- testWidgets(
- 'Should create one HistoryButtons, one URL field and one + button.',
- (WidgetTester tester) async {
- await _setUpNavigationBar(tester, webPageBloc, () {});
-
- const reasonSuffix =
- 'when the NavigationBar widget has a non-null webPageBloc.';
-
- expect(find.byType(HistoryButtons), findsOneWidget,
- reason: 'Expected a HistoryButtons widget $reasonSuffix');
- expect(find.byType(NavigationField), findsOneWidget,
- reason: 'Expected a NavigationField widget $reasonSuffix');
- expect(_findNewTabButton(), findsOneWidget,
- reason: 'Expected a + button $reasonSuffix');
- });
-
- testWidgets('''Should show a progress bar when the page has not been loaded,
- and should not show it anymore when the page loading is complete.''',
- (WidgetTester tester) async {
- await _setUpNavigationBar(tester, webPageBloc, () {});
- isLoadedStateNotifier.value = false;
- await tester.pump();
- expect(find.byType(LinearProgressIndicator), findsOneWidget,
- reason: 'Expected a progress bar when loading has not finished.');
-
- isLoadedStateNotifier.value = true;
- await tester.pumpAndSettle();
- expect(find.byType(LinearProgressIndicator), findsNothing,
- reason: 'Expected no progress bars when loading has completed.');
- });
-
- testWidgets('Should call the newTab callback when the + button is tapped.',
- (WidgetTester tester) async {
- TabsBloc tb = TabsBloc(
- // TODO(https://fxbug.dev/71711): Figure out why `dart analyze`
- // complains about this.
- tabFactory: () => MockWebPageBloc(), // ignore: unnecessary_lambdas
- disposeTab: (tab) => tab.dispose(),
- );
-
- await _setUpNavigationBar(
- tester, webPageBloc, () => tb.request.add(NewTabAction()));
-
- expect(tb.tabs.length, 0,
- reason:
- 'Expected no tabs in the tabsBloc when none has been added to it.');
-
- final newTabBtn = _findNewTabButton();
- expect(newTabBtn, findsOneWidget,
- reason: 'Expected one + button on the NavigationBar.');
- await tester.tap(newTabBtn);
- await tester.pumpAndSettle();
- expect(tb.tabs.length, 1,
- reason:
- 'Expected a tab in the tabsBloc when the + button was tapped.');
- });
- });
-}
-
-Future<void> _setUpNavigationBar(
- WidgetTester tester, WebPageBloc? bloc, VoidCallback callback) async {
- await tester.pumpWidget(
- MaterialApp(
- home: Scaffold(
- body: BrowserNavigationBar(
- bloc: bloc,
- newTab: callback,
- fieldFocus: FocusNode(),
- ),
- ),
- ),
- );
-}
-
-Finder _findNewTabButton() => find.byKey(Key('new_tab'));
-
-class MockSimpleBrowserNavigationEventListener extends Mock
- implements SimpleBrowserNavigationEventListener {}
-
-class MockSimpleBrowserWebService extends Mock
- implements SimpleBrowserWebService {}
-
-class MockWebPageBloc extends Mock implements WebPageBloc {}
diff --git a/bin/simple_browser/test/widgets/navigation_field_test.dart b/bin/simple_browser/test/widgets/navigation_field_test.dart
deleted file mode 100644
index 7f32cae..0000000
--- a/bin/simple_browser/test/widgets/navigation_field_test.dart
+++ /dev/null
@@ -1,117 +0,0 @@
-// Copyright 2019 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 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:fuchsia_logger/logger.dart';
-import 'package:mockito/mockito.dart';
-
-// ignore_for_file: implementation_imports
-import 'package:simple_browser/src/blocs/webpage_bloc.dart';
-import 'package:simple_browser/src/services/simple_browser_navigation_event_listener.dart';
-import 'package:simple_browser/src/services/simple_browser_web_service.dart';
-import 'package:simple_browser/src/widgets/navigation_field.dart';
-
-void main() {
- setupLogger(name: 'navigation_field_test');
-
- late SimpleBrowserWebService mockWebService;
- late SimpleBrowserNavigationEventListener mockEventListener;
- late WebPageBloc webPageBloc;
-
- setUpAll(() {
- mockWebService = MockSimpleBrowserWebService();
- mockEventListener = MockSimpleBrowserNavigationEventListener();
- webPageBloc = WebPageBloc(webService: mockWebService);
- });
-
- group('Default textfield (without URL)', () {
- ValueNotifier urlNotifier = ValueNotifier<String>('');
-
- setUp(() {
- when(mockEventListener.urlNotifier).thenAnswer((_) => urlNotifier);
- when(mockEventListener.url).thenAnswer((_) => urlNotifier.value);
- when(mockWebService.navigationEventListener)
- .thenAnswer((_) => mockEventListener);
- });
-
- const whenSuffix = 'when created it empty.';
- testWidgets('Should focus on the textfield $whenSuffix',
- (WidgetTester tester) async {
- await _setUpNavigationField(tester, webPageBloc);
-
- final textField = _findTextField();
- expect(tester.widget<TextField>(textField).autofocus, true,
- reason:
- 'Expected the TextField to be focused by default $whenSuffix');
- });
-
- testWidgets('Should call the callback when a valid url is entered.',
- (WidgetTester tester) async {
- await _setUpNavigationField(tester, webPageBloc);
-
- String testUrl = 'https://www.google.com';
- final textField = _findTextField();
-
- // Enters the testUrl to the text field and submit it.
- await tester.enterText(textField, testUrl);
- await tester.testTextInput.receiveAction(TextInputAction.go);
- await tester.pump();
-
- // Sees if the corresponding callback is called.
- verify(webPageBloc.webService.loadUrl(testUrl)).called(1);
- });
- });
-
- group('Textfield with a URL', () {
- ValueNotifier urlNotifier = ValueNotifier<String>('');
-
- setUp(() {
- when(mockEventListener.urlNotifier).thenAnswer((_) => urlNotifier);
- when(mockEventListener.url).thenAnswer((_) => urlNotifier.value);
- when(mockWebService.navigationEventListener)
- .thenAnswer((_) => mockEventListener);
- });
-
- const whenSuffix = 'when created it with a url.';
-
- testWidgets('Should not focus on the TextField $whenSuffix',
- (WidgetTester tester) async {
- urlNotifier.value = 'https://www.google.com';
-
- await _setUpNavigationField(tester, webPageBloc);
-
- final textField = _findTextField();
- expect(tester.widget<TextField>(textField).autofocus, false,
- reason: 'Expected the textfield not focused by default $whenSuffix');
- });
- });
-}
-
-Future<void> _setUpNavigationField(
- WidgetTester tester, WebPageBloc bloc) async {
- await tester.pumpWidget(
- MaterialApp(
- home: Scaffold(
- body: NavigationField(
- bloc: bloc,
- focus: FocusNode(),
- ),
- ),
- ),
- );
-}
-
-Finder _findTextField() {
- final textField = find.byType(TextField);
- expect(textField, findsOneWidget,
- reason: 'Expected a TextField on the NavigationField widget.');
-
- return textField;
-}
-
-class MockSimpleBrowserNavigationEventListener extends Mock
- implements SimpleBrowserNavigationEventListener {}
-
-class MockSimpleBrowserWebService extends Mock
- implements SimpleBrowserWebService {}
diff --git a/bin/simple_browser/test/widgets/tabs_widget_test.dart b/bin/simple_browser/test/widgets/tabs_widget_test.dart
deleted file mode 100644
index abe20f0..0000000
--- a/bin/simple_browser/test/widgets/tabs_widget_test.dart
+++ /dev/null
@@ -1,633 +0,0 @@
-// Copyright 2019 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:ui';
-
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:fuchsia_logger/logger.dart';
-import 'package:mockito/mockito.dart';
-
-// ignore_for_file: implementation_imports
-import 'package:simple_browser/src/blocs/tabs_bloc.dart';
-import 'package:simple_browser/src/blocs/webpage_bloc.dart';
-import 'package:simple_browser/src/models/tabs_action.dart';
-import 'package:simple_browser/src/services/simple_browser_navigation_event_listener.dart';
-import 'package:simple_browser/src/services/simple_browser_web_service.dart';
-import 'package:simple_browser/src/widgets/tabs_widget.dart';
-
-const _emptyTitle = 'NEW TAB';
-const screenWidth = 800.0;
-
-void main() {
- setupLogger(name: 'tabs_widget_test');
-
- late TabsBloc tabsBloc;
- late SimpleBrowserWebService mockWebService;
- late SimpleBrowserNavigationEventListener mockListener;
- ValueNotifier titleNotifier = ValueNotifier<String>('');
-
- setUp(() {
- mockWebService = MockSimpleBrowserWebService();
- mockListener = MockSimpleBrowserNavigationEventListener();
-
- when(mockWebService.navigationEventListener)
- .thenAnswer((_) => mockListener);
- when(mockListener.pageTitleNotifier).thenAnswer((_) => titleNotifier);
-
- tabsBloc = TabsBloc(
- tabFactory: () => WebPageBloc(
- webService: mockWebService,
- ),
- disposeTab: (tab) => tab.dispose(),
- );
- });
-
- testWidgets('Should create tab widgets when more than two tabs added.',
- (WidgetTester tester) async {
- // Initial state: Tabsbloc does not have any tabs.
- expect(tabsBloc.tabs.length, 0);
- await _setUpTabsWidget(tester, tabsBloc);
-
- // Sees if there is no tab widgets on the screen.
- expect(_findNewTabWidgets(), findsNothing,
- reason: 'Expected no tab widgets when no tabs added.');
-
- // Adds one tab and sees if there is a tab in the tabsBloc, but still no tab widgets
- // on the screen.
- await _addNTabsToTabsBloc(tester, tabsBloc, 1);
- expect(_findNewTabWidgets(), findsNothing,
- reason: 'Expected no tab widgets when only 1 tab added.');
-
- // Adds one more tab and sees if there are two tabs in the tabsBloc.
- await _addNTabsToTabsBloc(tester, tabsBloc, 1);
-
- // See if the tab widgets have the default title.
- expect(_findNewTabWidgets(), findsNWidgets(2),
- reason: '''Expected 2 tab widgets with the title, $_emptyTitle,
- when 2 tabs added.''');
- });
-
- testWidgets(
- 'Should create tab widgets with a custom title when one is given.',
- (WidgetTester tester) async {
- // Gives a custom title, 'TAB_WIDGET_TEST'.
- String expectedTitle = 'TAB_WIDGET_TEST';
- when(mockListener.pageTitle).thenReturn(expectedTitle);
-
- await _setUpTabsWidget(tester, tabsBloc);
-
- // Adds two tabs since tab widgets are only created when there are more than one tab.
- await _addNTabsToTabsBloc(tester, tabsBloc, 2);
-
- // Sees if there are two tab widgets with the given title.
- expect(find.text(expectedTitle), findsNWidgets(2),
- reason: '''Expected 2 tab widgets with the title $expectedTitle
- when 2 new tabs with it have been added.''');
- });
-
- testWidgets('''Should give the minimum width to the tab widgets
- when the total sum of the tab widths is larger than the browser width.''',
- (WidgetTester tester) async {
- await _setUpTabsWidget(tester, tabsBloc);
-
- // Adds seven tabs.
- await _addNTabsToTabsBloc(tester, tabsBloc, 7);
-
- // Sees if there are seven SizedBox widgets with the minimum width for the tab widgets.
- // A tabsBloc should be wrapped by a SizedBox widget and have the minimum width
- // when the total widths of the currently displayed tabs is larger than the browser width.
- expect(_findMinTabWidgets(), findsNWidgets(7));
- });
-
- testWidgets('Should change the focus to it when an unfocused tab is tapped.',
- (WidgetTester tester) async {
- await _setUpTabsWidget(tester, tabsBloc);
-
- await _addNTabsToTabsBloc(tester, tabsBloc, 3);
-
- _verifyFocusedTabIndex(tabsBloc, 2);
-
- final tabs = _findNewTabWidgets();
- await tester.tap(tabs.at(0));
- await tester.pumpAndSettle();
-
- _verifyFocusedTabIndex(tabsBloc, 0);
- });
-
- testWidgets(
- 'Should show close buttons on the focused tab and a hovered tab if any.',
- (WidgetTester tester) async {
- await _setUpTabsWidget(tester, tabsBloc);
- await _addNTabsToTabsBloc(tester, tabsBloc, 3);
- final tabs = _findNewTabWidgets();
- expect(tabs, findsNWidgets(3),
- reason: 'Expected to find 3 tab widgets when 3 tabs added.');
-
- // Sees if there is only one tab that has a close button on it.
- expect(_findClose(), findsOneWidget,
- reason: 'Expected to find 1 close button when hovering no tabs.');
-
- // Configures a gesture.
- final TestGesture gesture =
- await tester.createGesture(kind: PointerDeviceKind.mouse);
- addTearDown(gesture.removePointer);
-
- // Mouse Enters onto the first tab.
- final Offset firstTabCenter = tester.getCenter(tabs.at(0));
- await gesture.moveTo(firstTabCenter);
- await tester.pumpAndSettle();
-
- // Sees if there are two tabs that have a close button on it.
- expect(_findClose(), findsNWidgets(2),
- reason: 'Expected to find 2 close buttons when hovering a tab.');
-
- // Mouse is out from the first tab and moves to the currently focused tab.
- final Offset focusedTabCenter =
- tester.getCenter(tabs.at(tabsBloc.currentTabIdx!));
- await gesture.moveTo(focusedTabCenter);
- await tester.pumpAndSettle();
-
- // Sees if there is only one tab that has a close button on it.
- expect(_findClose(), findsOneWidget,
- reason:
- 'Expected to find 1 close button when hovering the focused tab.');
- });
-
- testWidgets('Should close the tab when its close button is tapped.',
- (WidgetTester tester) async {
- await _setUpTabsWidget(tester, tabsBloc);
- await _addNTabsToTabsBloc(tester, tabsBloc, 4);
- expect(_findNewTabWidgets(), findsNWidgets(4),
- reason: 'Expected to find 4 tab widgets when 4 tabs added.');
- _verifyFocusedTabIndex(tabsBloc, 3);
-
- Finder closeButtons = _findClose();
- expect(closeButtons, findsOneWidget,
- reason: 'Expected to find 1 close button when hovering no tabs.');
-
- await tester.tap(closeButtons);
- await tester.pumpAndSettle();
- expect(tabsBloc.tabs.length, 3,
- reason: 'Expected 3 tabs in tabsBloc after tapped the close.');
- expect(_findNewTabWidgets(), findsNWidgets(3),
- reason: 'Expected to find 3 tab widgets after tapped the close.');
- _verifyFocusedTabIndex(tabsBloc, 2);
- });
-
- testWidgets(
- 'Should rearrange the tabs when a tab is dragged to another position',
- (WidgetTester tester) async {
- await _setUpTabsWidget(tester, tabsBloc);
- await _addNTabsToTabsBloc(tester, tabsBloc, 5);
- final tabs = _findNewTabWidgets();
- expect(tabs, findsNWidgets(5),
- reason: 'Expected to find 5 tab widgets when 5 tabs added.');
- _verifyFocusedTabIndex(tabsBloc, 4);
-
- WebPageBloc originalTab0 = tabsBloc.tabs[0];
- WebPageBloc originalTab1 = tabsBloc.tabs[1];
- WebPageBloc originalTab2 = tabsBloc.tabs[2];
- WebPageBloc originalTab3 = tabsBloc.tabs[3];
- WebPageBloc originalTab4 = tabsBloc.tabs[4];
-
- // Drags the last tab 161.0 to the left.
- await tester.drag(tabs.at(4), Offset(-161.0, 0.0));
- await tester.pumpAndSettle();
-
- // Sees if the tab just moved is focused.
- expect(tabsBloc.currentTabIdx, 3,
- reason: '''Expected the index of the focused tab to be rearranged to 3,
- but actually has been rearranged to ${tabsBloc.currentTabIdx}.''');
-
- // Sees if all tabs have been rarranged correctly.
- expect(tabsBloc.tabs[0], originalTab0,
- reason: 'Expected that the 1st tab used to be the 1st.');
- expect(tabsBloc.tabs[1], originalTab1,
- reason: 'Expected that the 2nd tab used to be the 2nd.');
- expect(tabsBloc.tabs[2], originalTab2,
- reason: 'Expected that the 3rd tab used to be the 3rd.');
- expect(tabsBloc.tabs[3], originalTab4,
- reason: 'Expected that the 4th tab used to be the 5th.');
-
- expect(tabsBloc.tabs[4], originalTab3,
- reason: 'Expected that the 5th tab used to the 4th.');
-
- // Drags the 2nd tab 170.0 to the right.
- await tester.drag(tabs.at(1), Offset(161.0, 0.0));
- await tester.pumpAndSettle();
-
- // Sees if the tab just moved is focused.
- _verifyFocusedTabIndex(tabsBloc, 2);
-
- // Sees if all tabs have been rarranged correctly.
- expect(tabsBloc.tabs[0], originalTab0,
- reason: 'Expected that the 1st tab used to be the 1st.');
- expect(tabsBloc.tabs[1], originalTab2,
- reason: 'Expected that the 2nd tab used to be the 3rd.');
- expect(tabsBloc.tabs[2], originalTab1,
- reason: 'Expected that the 3rd tab used to be the 2nd.');
- expect(tabsBloc.tabs[3], originalTab4,
- reason: 'Expected that the 4th tab used to be the 5th.');
- expect(tabsBloc.tabs[4], originalTab3,
- reason: 'Expected that the 5th tab used to the 4th.');
- });
-
- group('Scrollable tab list', () {
- final rightScrollMargin =
- screenWidth - kTabBarHeight - kScrollToMargin - kMinTabWidth;
- final rightMinMargin = screenWidth - kTabBarHeight - kMinTabWidth;
- final leftScrollMargin = kScrollToMargin + kTabBarHeight;
- final leftMinMargin = kTabBarHeight;
-
- testWidgets('The tab widget list should scroll on a scroll button tapped.',
- (WidgetTester tester) async {
- await _setUpTabsWidget(tester, tabsBloc);
-
- // Creates 8 tabs.
- await _addNTabsToTabsBloc(tester, tabsBloc, 10);
-
- // The expected display of the initial tab list (*currently focused tab):
- // |-0-||-1-||-2-||-3-||-4-||-5-||-6-||-7-||-8-||-9*-|
- // |- SCREEN -|
-
- final tabs = _findMinTabWidgets();
-
- // Sees if there are 8 tab widgets created.
- expect(tabs, findsNWidgets(10),
- reason: 'Expected to find 10 tabs, but actually $tabs were found.');
-
- // Sees if the viewport of the list is on the right.
- _verifyFocusedTabIndex(tabsBloc, 9);
- _verifyFocusedTabMargin(tester, tabs, rightMinMargin);
- _verifyPartlyOffscreenFromLeft(tester, tabs.at(3));
-
- final leftScrollButton = find.byIcon(Icons.keyboard_arrow_left);
- expect(leftScrollButton, findsOneWidget);
-
- final rightScrollButton = find.byIcon(Icons.keyboard_arrow_right);
- expect(rightScrollButton, findsOneWidget);
-
- // Taps on the left scroll button.
- await tester.tap(leftScrollButton);
- await tester.pumpAndSettle();
-
- // The expected display of the tab list (*currently focused tab):
- // |-0-||-1-||-2-||-3-||-4-||-5-||-6-||-7-||-8-||-9*-|
- // |- SCREEN -|
-
- // Sees if the list has been scrolled to the left.
- _verifyPartlyOffscreenFromLeft(tester, tabs.first);
- _verifyEntirelyOffscreenFromRight(tester, tabs.last);
- _verifyPartlyOffscreenFromRight(tester, tabs.at(6));
-
- await tester.tap(leftScrollButton);
- await tester.pumpAndSettle();
-
- // The expected display of the tab list (*currently focused tab):
- // |-0-||-1-||-2-||-3-||-4-||-5-||-6-||-7-||-8-||-9*-|
- // |- SCREEN -|
-
- // Sees if the list has been scrolled to the left end.
- final firstTabLeftX = tester.getTopLeft(tabs.first).dx;
- expect(firstTabLeftX, leftMinMargin,
- reason: '''Expected the scroll list have been scroll to its far left,
- but actually it has not.''');
-
- // Taps on the right scroll button.
- await tester.tap(rightScrollButton);
- await tester.pumpAndSettle();
-
- // The expected display of the tab list (*currently focused tab):
- // |-0-||-1-||-2-||-3-||-4-||-5-||-6-||-7-||-8-||-9*-|
- // |- SCREEN -|
-
- // Sees if the list has been scrolled to the right.
- _verifyPartlyOffscreenFromLeft(tester, tabs.at(3));
- _verifyPartlyOffscreenFromRight(tester, tabs.last);
-
- await tester.tap(rightScrollButton);
- await tester.pumpAndSettle();
-
- // The expected display of the tab list (*currently focused tab):
- // |-0-||-1-||-2-||-3-||-4-||-5-||-6-||-7-||-8-||-9*-|
- // |- SCREEN -|
-
- // Sees if the list has been scrolled to the right end.
- _verifyFocusedTabMargin(tester, tabs, rightMinMargin);
- _verifyPartlyOffscreenFromLeft(tester, tabs.at(3));
- });
-
- testWidgets(
- 'The tab list should scroll to the left when the moving tab hits its left edge.',
- (WidgetTester tester) async {
- await _setUpTabsWidget(tester, tabsBloc);
-
- const numTabs = 8;
-
- // Creates 8 tabs.
- await _addNTabsToTabsBloc(tester, tabsBloc, numTabs);
-
- // The expected display of the initial tab list (*currently focused tab):
- // |- 0 -| |- 1 -| |- 2 -| |- 3 -| |- 4 -| |- 5 -| |- 6 -| |- 7* -|
- // |- SCREEN -|
-
- final tabs = _findMinTabWidgets();
- _verifyFocusedTabIndex(tabsBloc, numTabs - 1);
-
- // Saves the original tab positions.
- final originalXs = <double>[];
- for (int i = 0; i < numTabs; i++) {
- originalXs.add(tester.getTopLeft(tabs.at(i)).dx);
- }
-
- // Drags the 3rd tab 35.0 to the left to hit the left edge of the list.
- const tabIndexToDrag = 2;
- await tester.drag(tabs.at(tabIndexToDrag), Offset(-35.0, 0.0));
- await tester.pumpAndSettle();
-
- // The expected display of the tab list (*currently focused tab):
- // |- 0 -| |- 1 -| |- 2* -| |- 3 -| |- 4 -| |- 5 -| |- 6 -| |- 7 -|
- // |- SCREEN -|
-
- _verifyFocusedTabIndex(tabsBloc, tabIndexToDrag);
- final afterDragXs = <double>[];
-
- // Saves the new tab positions to a list in the actual tab order.
- for (int i = 0; i < tabIndexToDrag; i++) {
- afterDragXs.add(tester.getTopLeft(tabs.at(i)).dx);
- }
- afterDragXs.add(tester.getTopLeft(tabs.last).dx);
- for (int i = tabIndexToDrag; i < numTabs - 1; i++) {
- afterDragXs.add(tester.getTopLeft(tabs.at(i)).dx);
- }
-
- // Sees if the list has auto-scrolled to the left by comparing
- // the tab positions before and after the 3rd tab hit the left edge.
- for (int i = 0; i < numTabs; i++) {
- expect(afterDragXs[i] > originalXs[i], true);
- }
- });
-
- testWidgets(
- 'The tab list should scroll to the right when the moving tab hits its right edge.',
- (WidgetTester tester) async {
- await _setUpTabsWidget(tester, tabsBloc);
-
- const numTabs = 8;
-
- // Creates 8 tabs.
- await _addNTabsToTabsBloc(tester, tabsBloc, numTabs);
-
- // The expected display of the initial tab list (*currently focused tab):
- // |- 0 -| |- 1 -| |- 2 -| |- 3 -| |- 4 -| |- 5 -| |- 6 -| |- 7* -|
- // |- SCREEN -|
-
- final tabs = _findMinTabWidgets();
- _verifyFocusedTabIndex(tabsBloc, numTabs - 1);
-
- _verifyPartlyOffscreenFromLeft(tester, tabs.at(1));
-
- final leftScrollButton = find.byIcon(Icons.keyboard_arrow_left);
- expect(leftScrollButton, findsOneWidget);
-
- // Taps on the left scroll button.
- await tester.tap(leftScrollButton);
- await tester.pumpAndSettle();
-
- // The expected display of the tab list (*currently focused tab):
- // |- 0 -| |- 1 -| |- 2* -| |- 3 -| |- 4 -| |- 5 -| |- 6 -| |- 7 -|
- // |- SCREEN -|
-
- _verifyPartlyOffscreenFromRight(tester, tabs.at(6));
- _verifyEntirelyOffscreenFromRight(tester, tabs.last);
-
- // Saves the original tab positions.
- final originalXs = <double>[];
- for (int i = 0; i < numTabs; i++) {
- originalXs.add(tester.getTopLeft(tabs.at(i)).dx);
- }
-
- // Drags the 6th tab 35.0 to the right to hit the right edge of the list.
- const tabIndexToDrag = 5;
- await tester.drag(tabs.at(tabIndexToDrag), Offset(35.0, 0.0));
- await tester.pumpAndSettle();
-
- // The expected display of the tab list (*currently focused tab):
- // |- 0 -| |- 1 -| |- 2 -| |- 3 -| |- 4 -| |- 5* -| |- 6 -| |- 7 -|
- // |- SCREEN -|
-
- _verifyFocusedTabIndex(tabsBloc, tabIndexToDrag);
-
- final afterDragXs = <double>[];
-
- // Saves the new tab positions to a list in the actual tab order.
- for (int i = 0; i < tabIndexToDrag; i++) {
- afterDragXs.add(tester.getTopLeft(tabs.at(i)).dx);
- }
- afterDragXs.add(tester.getTopLeft(tabs.last).dx);
- for (int i = tabIndexToDrag; i < numTabs - 1; i++) {
- afterDragXs.add(tester.getTopLeft(tabs.at(i)).dx);
- }
-
- // Sees if the list has auto-scrolled to the right by comparing
- // the tab positions before and after the 6th tab hit the right edge.
- for (int i = 0; i < numTabs; i++) {
- expect(afterDragXs[i] < originalXs[i], true);
- }
- });
-
- testWidgets(
- 'The tab widget list should scroll if needed depending on the offset of the focused tab.',
- (WidgetTester tester) async {
- await _setUpTabsWidget(tester, tabsBloc);
-
- // Creates 8 tabs.
- await _addNTabsToTabsBloc(tester, tabsBloc, 8);
-
- // The expected display of the initial tab list (*currently focused tab):
- // |- 0 -| |- 1 -| |- 2 -| |- 3 -| |- 4 -| |- 5 -| |- 6 -| |- 7* -|
- // |- SCREEN -|
-
- final tabs = _findMinTabWidgets();
-
- // Sees if there are 8 tab widgets created.
- expect(tabs, findsNWidgets(8),
- reason: 'Expected to find 8 tabs, but actually $tabs were found.');
-
- // Sees if the expected tab is currently focused and its at the desired
- // position.
- _verifyFocusedTabIndex(tabsBloc, 7);
- _verifyFocusedTabMargin(tester, tabs, rightMinMargin);
-
- // Sees if certain tabs are partly/entire offscreen.
- _verifyEntirelyOffscreenFromLeft(tester, tabs.first);
- _verifyPartlyOffscreenFromLeft(tester, tabs.at(1));
-
- // Finds the fifth tab and checks its current position.
- final fifthTab = tabs.at(4);
- final expectedFifthTabLeftX = tester.getTopLeft(fifthTab).dx;
-
- // Taps on the fifth tab to change the focus.
- await tester.tap(fifthTab);
- await tester.pumpAndSettle();
-
- // The expected display of the tab list (*currently focused tab):
- // |- 0 -| |- 1 -| |- 2 -| |- 3 -| |- 4* -| |- 5 -| |- 6 -| |- 7 -|
- // |- SCREEN -|
-
- _verifyFocusedTabIndex(tabsBloc, 4);
- _verifyFocusedTabMargin(tester, tabs, expectedFifthTabLeftX);
-
- // Focuses on the second tab by directly adding the FocusTabAction to the
- // tabsBloc since tester.tap() does not work on the widget whose center
- // is offscreen.
- tabsBloc.request.add(FocusTabAction(tab: tabsBloc.tabs[1]));
- await tester.pumpAndSettle();
-
- // The expected display of the tab list (*currently focused tab):
- // |- 0 -| |- 1* -| |- 2 -| |- 3 -| |- 4 -| |- 5 -| |- 6 -| |- 7 -|
- // |- SCREEN -|
-
- _verifyFocusedTabIndex(tabsBloc, 1);
- _verifyFocusedTabMargin(tester, tabs, leftScrollMargin);
-
- _verifyPartlyOffscreenFromLeft(tester, tabs.first);
- _verifyPartlyOffscreenFromRight(tester, tabs.at(5));
- _verifyEntirelyOffscreenFromRight(tester, tabs.at(6));
-
- // Focuses on the first tab.
- tabsBloc.request.add(FocusTabAction(tab: tabsBloc.tabs[0]));
- await tester.pumpAndSettle();
-
- // The expected display of the tab list (*currently focused tab):
- // |- 0* -| |- 1 -| |- 2 -| |- 3 -| |- 4 -| |- 5 -| |- 6 -| |- 7 -|
- // |- SCREEN -|
-
- _verifyFocusedTabIndex(tabsBloc, 0);
- _verifyFocusedTabMargin(tester, tabs, leftMinMargin);
-
- _verifyPartlyOffscreenFromRight(tester, tabs.at(5));
- _verifyEntirelyOffscreenFromRight(tester, tabs.at(6));
-
- // Focuses on the seventh tab.
- tabsBloc.request.add(FocusTabAction(tab: tabsBloc.tabs[6]));
- await tester.pumpAndSettle();
-
- // The expected display of the tab list (*currently focused tab):
- // |- 0 -| |- 1 -| |- 2 -| |- 3 -| |- 4 -| |- 5 -| |- 6* -| |- 7 -|
- // |- SCREEN -|
-
- _verifyFocusedTabIndex(tabsBloc, 6);
- _verifyFocusedTabMargin(tester, tabs, rightScrollMargin);
-
- _verifyEntirelyOffscreenFromLeft(tester, tabs.first);
- _verifyPartlyOffscreenFromLeft(tester, tabs.at(1));
- _verifyPartlyOffscreenFromRight(tester, tabs.at(6));
- });
- });
-}
-
-Future<void> _setUpTabsWidget(WidgetTester tester, TabsBloc tabsBloc) async {
- await tester.pumpWidget(MaterialApp(
- home: Scaffold(
- body: Container(
- width: screenWidth,
- child: TabsWidget(
- bloc: tabsBloc,
- ),
- ),
- ),
- ));
-
- await tester.pumpAndSettle();
-}
-
-Future<void> _addNTabsToTabsBloc(
- WidgetTester tester, TabsBloc tabsBloc, int n) async {
- int currentNumTabs = tabsBloc.tabs.length;
- for (int i = 0; i < n; i++) {
- tabsBloc.request.add(NewTabAction());
- await tester.pumpAndSettle();
- }
- expect(tabsBloc.tabs.length, currentNumTabs + n);
-}
-
-Finder _findMinTabWidgets() => find.byWidgetPredicate((Widget widget) {
- if (widget is Container && widget.key == Key('tab')) {
- BoxConstraints width = widget.constraints!.widthConstraints();
- return (width.minWidth == width.maxWidth) &&
- (width.minWidth == kMinTabWidth);
- }
- return false;
- });
-
-Finder _findNewTabWidgets() => find.text(_emptyTitle);
-
-Finder _findClose() => find.byIcon(Icons.clear);
-
-// Verifies if the currently focused tab is the expected tab.
-void _verifyFocusedTabIndex(TabsBloc tb, int expectedIndex) {
- expect(tb.currentTabIdx, expectedIndex,
- reason: '''Expected the index of the currently focused tab to be
- $expectedIndex, but actually is ${tb.currentTabIdx}''');
-}
-
-// Verifies if the currently focused tab has the expected left margin.
-void _verifyFocusedTabMargin(
- WidgetTester tester, Finder tabs, double expectedMargin) {
- // The currently focused tab always become the last member of the finder
- // since it is rendered on the top of the other tabs.
- final actualMargin = tester.getTopLeft(tabs.last).dx;
- expect(actualMargin, expectedMargin,
- reason: '''Expected the left margin to the currently focused tab to be
- $expectedMargin, but is actually $actualMargin.''');
-}
-
-void _verifyPartlyOffscreenFromLeft(WidgetTester tester, Finder tab) {
- final leftBorderX = kTabBarHeight;
- final tabLeftX = tester.getTopLeft(tab).dx;
- final tabRightX = tester.getTopRight(tab).dx;
- expect(tabLeftX < leftBorderX, true,
- reason: '''Expected this tab's left edge to be offscreen,
- but actually is onscreen.''');
- expect(tabRightX >= leftBorderX, true,
- reason: '''Expected this tab's right edge to be onscreen,
- but actually is offscreen.''');
-}
-
-void _verifyEntirelyOffscreenFromLeft(WidgetTester tester, Finder tab) {
- final leftBorderX = kTabBarHeight;
- final tabRightX = tester.getTopRight(tab).dx;
- expect(tabRightX < leftBorderX, true,
- reason: '''Expected this tab's right edge to be offscreen,
- but actually is onscreen.''');
-}
-
-void _verifyPartlyOffscreenFromRight(WidgetTester tester, Finder tab) {
- final rightBorderX = screenWidth - kTabBarHeight;
- final tabLeftX = tester.getTopLeft(tab).dx;
- final tabRightX = tester.getTopRight(tab).dx;
- expect(tabLeftX < rightBorderX, true,
- reason: '''Expected this tab's left edge to be onscreen,
- but actually is offscreen.''');
- expect(tabRightX >= rightBorderX, true,
- reason: '''Expected this tab's right edge to be offscreen,
- but actually is onscreen.''');
-}
-
-void _verifyEntirelyOffscreenFromRight(WidgetTester tester, Finder tab) {
- final rightBorderX = screenWidth - kTabBarHeight;
- final tabLeftX = tester.getTopLeft(tab).dx;
- expect(tabLeftX > rightBorderX, true,
- reason: '''Expected this tab's left edge to be offscreen,
- but actually is onscreen.''');
-}
-
-class MockSimpleBrowserNavigationEventListener extends Mock
- implements SimpleBrowserNavigationEventListener {}
-
-class MockSimpleBrowserWebService extends Mock
- implements SimpleBrowserWebService {}
-
-class MockWebPageBloc extends Mock implements WebPageBloc {}
diff --git a/bin/simple_browser_internationalization/BUILD.gn b/bin/simple_browser_internationalization/BUILD.gn
deleted file mode 100644
index 20f985d..0000000
--- a/bin/simple_browser_internationalization/BUILD.gn
+++ /dev/null
@@ -1,36 +0,0 @@
-# Copyright 2019 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("//build/dart/dart_library.gni")
-
-dart_library("internationalization") {
- package_name = "internationalization"
- null_safe = true
-
- # Disabling analysis for the time being, this is mostly generated code to begin
- # with, and there are issues like this one that make things harder than necessary:
- # https://github.com/dart-lang/sdk/issues/38598.
- disable_analysis = true
-
- sources = [
- "localization/messages_all.dart",
- "localization/messages_ar-XB.dart",
- "localization/messages_en-XA.dart",
- "localization/messages_en-XC.dart",
- "localization/messages_nl.dart",
- "localization/messages_sr-Latn.dart",
- "localization/messages_sr.dart",
- "localizations_delegate.dart",
- "strings.dart",
- "supported_locales.dart",
- ]
-
- deps = [
- "//sdk/dart/fuchsia_internationalization_flutter",
- "//sdk/dart/fuchsia_logger",
- "//third_party/dart-pkg/git/flutter/packages/flutter",
- "//third_party/dart-pkg/git/flutter/packages/flutter_localizations",
- "//third_party/dart-pkg/pub/intl",
- ]
-}
diff --git a/bin/simple_browser_internationalization/README.md b/bin/simple_browser_internationalization/README.md
deleted file mode 100644
index f30004e..0000000
--- a/bin/simple_browser_internationalization/README.md
+++ /dev/null
@@ -1,42 +0,0 @@
-# `simple_browser` internationalization and localization
-
-This Dart package contains the support for `simple_browser` localization and
-internationalization.
-
-At its current state the support is in its very early phase. There is a lot of
-manual scaffolding that could be replaced by auto-generating the code of
-interest. Doing so will be the topic of followup work.
-
-# How to use this package
-
-This package is purpose-built for the `simple_browser`. The central part is
-the file `lib/strings.dart` which by design contains all the strings that the
-simple browser needs to vary based on the system locale.
-
-Add a function to this file whenever you need to introduce a new text message.
-
-## Translating
-
-The translation process amounts to making ARB files for each of the supported
-locales of interest and providing the translations for the messages and
-strings. The basic tool for doing so would be a simple text editor. While
-there also exists a wealth of tools that help software translators, it is out
-of scope of this file to go into specific details. So the choice of the
-translation environment is left to the developer and translator.
-
-## Generating Dart runtime format for the translations
-
-Once translated you can use the file `./scripts/run_generate_from_arb.sh` from
-`fuchsia_internationalization` library to generate the Dart code that wires up
-the translation. You will need to rebuild the entire project to take the new
-translations in.
-
-# Further reading
-
-Internationalization and localization are topic unto themselves, and it is out
-of scope of this file to go into all the details. Please see the section
-on [Internationalizing Flutter apps][1] for many more details that are directly
-relevant to Dart and Flutter app localization.
-
-[1]: https://flutter.dev/docs/development/accessibility-and-localization/internationalization
-[arb]: https://github.com/google/app-resource-bundle
diff --git a/bin/simple_browser_internationalization/lib/localization/messages_all.dart b/bin/simple_browser_internationalization/lib/localization/messages_all.dart
deleted file mode 100755
index 4bfe09a..0000000
--- a/bin/simple_browser_internationalization/lib/localization/messages_all.dart
+++ /dev/null
@@ -1,140 +0,0 @@
-// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
-// This is a library that looks up messages for specific locales by
-// delegating to the appropriate library.
-
-// Ignore issues from commonly used lints in this file.
-// ignore_for_file:implementation_imports, file_names, unnecessary_new
-// ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering
-// ignore_for_file:argument_type_not_assignable, invalid_assignment
-// ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases
-// ignore_for_file:comment_references
-
-import 'dart:async';
-
-import 'package:intl/intl.dart';
-import 'package:intl/message_lookup_by_library.dart';
-import 'package:intl/src/intl_helpers.dart';
-
-import 'messages_ar-XB.dart' deferred as messages_ar_xb;
-import 'messages_en-XA.dart' deferred as messages_en_xa;
-import 'messages_en-XC.dart' deferred as messages_en_xc;
-import 'messages_nl.dart' deferred as messages_nl;
-import 'messages_sr-Latn.dart' deferred as messages_sr_latn;
-import 'messages_sr.dart' deferred as messages_sr;
-
-typedef Future<dynamic> LibraryLoader();
-Map<String, LibraryLoader> _deferredLibraries = {
- 'ar_XB': messages_ar_xb.loadLibrary,
- 'en_XA': messages_en_xa.loadLibrary,
- 'en_XC': messages_en_xc.loadLibrary,
- 'nl': messages_nl.loadLibrary,
- 'sr_Latn': messages_sr_latn.loadLibrary,
- 'sr': messages_sr.loadLibrary,
-};
-
-MessageLookupByLibrary? _findExact(String localeName) {
- switch (localeName) {
- case 'ar_XB':
- return messages_ar_xb.messages;
- case 'en_XA':
- return messages_en_xa.messages;
- case 'en_XC':
- return messages_en_xc.messages;
- case 'nl':
- return messages_nl.messages;
- case 'sr_Latn':
- return messages_sr_latn.messages;
- case 'sr':
- return messages_sr.messages;
- default:
- return null;
- }
-}
-
-/// User programs should call this before using [localeName] for messages.
-Future<bool> initializeMessages(String localeName) async {
- var availableLocale = Intl.verifiedLocale(
- localeName, (locale) => _deferredLibraries[locale] != null,
- onFailure: (_) => null);
- if (availableLocale == null) {
- return new Future.value(false);
- }
- var lib = _deferredLibraries[availableLocale];
- await (lib == null ? new Future.value(false) : lib());
- initializeInternalMessageLookup(() => new CompositeMessageLookup());
- messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor);
- return new Future.value(true);
-}
-
-bool _messagesExistFor(String locale) {
- try {
- return _findExact(locale) != null;
- } catch (e) {
- return false;
- }
-}
-
-MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) {
- var actualLocale =
- Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null);
- if (actualLocale == null) return null;
- return _findExact(actualLocale);
-}
-
-/// Turn the JSON template into a string.
-///
-/// We expect one of the following forms for the template.
-/// * null -> null
-/// * String s -> s
-/// * int n -> '${args[n]}'
-/// * List list, one of
-/// * ['Intl.plural', int howMany, (templates for zero, one, ...)]
-/// * ['Intl.gender', String gender, (templates for female, male, other)]
-/// * ['Intl.select', String choice, { 'case' : template, ...} ]
-/// * ['text alternating with ', 0 , ' indexes in the argument list']
-String? evaluateJsonTemplate(dynamic input, List<dynamic> args) {
- if (input == null) return null;
- if (input is String) return input;
- if (input is int) {
- return "${args[input]}";
- }
-
- List<dynamic> template = input;
- var messageName = template.first;
- if (messageName == "Intl.plural") {
- var howMany = args[template[1]];
- return evaluateJsonTemplate(
- Intl.pluralLogic(howMany,
- zero: template[2],
- one: template[3],
- two: template[4],
- few: template[5],
- many: template[6],
- other: template[7]),
- args);
- }
- if (messageName == "Intl.gender") {
- var gender = args[template[1]];
- return evaluateJsonTemplate(
- Intl.genderLogic(gender,
- female: template[2], male: template[3], other: template[4]),
- args);
- }
- if (messageName == "Intl.select") {
- var select = args[template[1]];
- var choices = template[2];
- return evaluateJsonTemplate(Intl.selectLogic(select, choices), args);
- }
-
- // If we get this far, then we are a basic interpolation, just strings and
- // ints.
- var output = StringBuffer();
- for (var entry in template) {
- if (entry is int) {
- output.write("${args[entry]}");
- } else {
- output.write("$entry");
- }
- }
- return output.toString();
-}
diff --git a/bin/simple_browser_internationalization/lib/localization/messages_ar-XB.dart b/bin/simple_browser_internationalization/lib/localization/messages_ar-XB.dart
deleted file mode 100755
index 9e206d0..0000000
--- a/bin/simple_browser_internationalization/lib/localization/messages_ar-XB.dart
+++ /dev/null
@@ -1,33 +0,0 @@
-// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
-// This is a library that provides messages for a ar_XB locale. All the
-// messages from the main program should be duplicated here with the same
-// function name.
-
-// Ignore issues from commonly used lints in this file.
-// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
-// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
-// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
-// ignore_for_file:unused_import, file_names
-
-import 'package:intl/intl.dart';
-import 'package:intl/message_lookup_by_library.dart';
-import 'dart:convert';
-import 'messages_all.dart' show evaluateJsonTemplate;
-
-final messages = new MessageLookup();
-
-typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
-
-class MessageLookup extends MessageLookupByLibrary {
- String get localeName => 'ar_XB';
-
- String? evaluateMessage(translation, List<dynamic> args) {
- return evaluateJsonTemplate(translation, args);
- }
-
- var _messages;
- get messages => _messages ??=
- const JsonDecoder().convert(messageText) as Map<String, dynamic>;
- static final messageText = r'''
-{"back":"Bck","browser":"Browser","forward":"Fwd","refresh":"Rfrsh","search":"Search"}''';
-}
diff --git a/bin/simple_browser_internationalization/lib/localization/messages_en-XA.dart b/bin/simple_browser_internationalization/lib/localization/messages_en-XA.dart
deleted file mode 100755
index 78335bc..0000000
--- a/bin/simple_browser_internationalization/lib/localization/messages_en-XA.dart
+++ /dev/null
@@ -1,33 +0,0 @@
-// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
-// This is a library that provides messages for a en_XA locale. All the
-// messages from the main program should be duplicated here with the same
-// function name.
-
-// Ignore issues from commonly used lints in this file.
-// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
-// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
-// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
-// ignore_for_file:unused_import, file_names
-
-import 'package:intl/intl.dart';
-import 'package:intl/message_lookup_by_library.dart';
-import 'dart:convert';
-import 'messages_all.dart' show evaluateJsonTemplate;
-
-final messages = new MessageLookup();
-
-typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
-
-class MessageLookup extends MessageLookupByLibrary {
- String get localeName => 'en_XA';
-
- String? evaluateMessage(translation, List<dynamic> args) {
- return evaluateJsonTemplate(translation, args);
- }
-
- var _messages;
- get messages => _messages ??=
- const JsonDecoder().convert(messageText) as Map<String, dynamic>;
- static final messageText = r'''
-{"back":"[Бçķ one]","browser":"[Бŕöŵšéŕ one]","forward":"[Fŵð one]","refresh":"[Ŕƒŕšĥ one]","search":"[Šéåŕçĥ one]"}''';
-}
diff --git a/bin/simple_browser_internationalization/lib/localization/messages_en-XC.dart b/bin/simple_browser_internationalization/lib/localization/messages_en-XC.dart
deleted file mode 100755
index c9e9d98..0000000
--- a/bin/simple_browser_internationalization/lib/localization/messages_en-XC.dart
+++ /dev/null
@@ -1,33 +0,0 @@
-// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
-// This is a library that provides messages for a en_XC locale. All the
-// messages from the main program should be duplicated here with the same
-// function name.
-
-// Ignore issues from commonly used lints in this file.
-// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
-// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
-// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
-// ignore_for_file:unused_import, file_names
-
-import 'package:intl/intl.dart';
-import 'package:intl/message_lookup_by_library.dart';
-import 'dart:convert';
-import 'messages_all.dart' show evaluateJsonTemplate;
-
-final messages = new MessageLookup();
-
-typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
-
-class MessageLookup extends MessageLookupByLibrary {
- String get localeName => 'en_XC';
-
- String? evaluateMessage(translation, List<dynamic> args) {
- return evaluateJsonTemplate(translation, args);
- }
-
- var _messages;
- get messages => _messages ??=
- const JsonDecoder().convert(messageText) as Map<String, dynamic>;
- static final messageText = r'''
-{"back":"Bck","browser":"Browser","forward":"Fwd","refresh":"Rfrsh","search":"Search"}''';
-}
diff --git a/bin/simple_browser_internationalization/lib/localization/messages_nl.dart b/bin/simple_browser_internationalization/lib/localization/messages_nl.dart
deleted file mode 100755
index 263f49b..0000000
--- a/bin/simple_browser_internationalization/lib/localization/messages_nl.dart
+++ /dev/null
@@ -1,33 +0,0 @@
-// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
-// This is a library that provides messages for a nl locale. All the
-// messages from the main program should be duplicated here with the same
-// function name.
-
-// Ignore issues from commonly used lints in this file.
-// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
-// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
-// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
-// ignore_for_file:unused_import, file_names
-
-import 'package:intl/intl.dart';
-import 'package:intl/message_lookup_by_library.dart';
-import 'dart:convert';
-import 'messages_all.dart' show evaluateJsonTemplate;
-
-final messages = new MessageLookup();
-
-typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
-
-class MessageLookup extends MessageLookupByLibrary {
- String get localeName => 'nl';
-
- String? evaluateMessage(translation, List<dynamic> args) {
- return evaluateJsonTemplate(translation, args);
- }
-
- var _messages;
- get messages => _messages ??=
- const JsonDecoder().convert(messageText) as Map<String, dynamic>;
- static final messageText = r'''
-{}''';
-}
diff --git a/bin/simple_browser_internationalization/lib/localization/messages_sr-Latn.dart b/bin/simple_browser_internationalization/lib/localization/messages_sr-Latn.dart
deleted file mode 100755
index 3c6c699..0000000
--- a/bin/simple_browser_internationalization/lib/localization/messages_sr-Latn.dart
+++ /dev/null
@@ -1,33 +0,0 @@
-// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
-// This is a library that provides messages for a sr_Latn locale. All the
-// messages from the main program should be duplicated here with the same
-// function name.
-
-// Ignore issues from commonly used lints in this file.
-// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
-// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
-// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
-// ignore_for_file:unused_import, file_names
-
-import 'package:intl/intl.dart';
-import 'package:intl/message_lookup_by_library.dart';
-import 'dart:convert';
-import 'messages_all.dart' show evaluateJsonTemplate;
-
-final messages = new MessageLookup();
-
-typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
-
-class MessageLookup extends MessageLookupByLibrary {
- String get localeName => 'sr_Latn';
-
- String? evaluateMessage(translation, List<dynamic> args) {
- return evaluateJsonTemplate(translation, args);
- }
-
- var _messages;
- get messages => _messages ??=
- const JsonDecoder().convert(messageText) as Map<String, dynamic>;
- static final messageText = r'''
-{"back":"Nzd","browser":"Pregledač","forward":"Npr","refresh":"Osvž","search":"Pretražite"}''';
-}
diff --git a/bin/simple_browser_internationalization/lib/localization/messages_sr.dart b/bin/simple_browser_internationalization/lib/localization/messages_sr.dart
deleted file mode 100755
index b4fdf94..0000000
--- a/bin/simple_browser_internationalization/lib/localization/messages_sr.dart
+++ /dev/null
@@ -1,33 +0,0 @@
-// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
-// This is a library that provides messages for a sr locale. All the
-// messages from the main program should be duplicated here with the same
-// function name.
-
-// Ignore issues from commonly used lints in this file.
-// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
-// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
-// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
-// ignore_for_file:unused_import, file_names
-
-import 'package:intl/intl.dart';
-import 'package:intl/message_lookup_by_library.dart';
-import 'dart:convert';
-import 'messages_all.dart' show evaluateJsonTemplate;
-
-final messages = new MessageLookup();
-
-typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
-
-class MessageLookup extends MessageLookupByLibrary {
- String get localeName => 'sr';
-
- String? evaluateMessage(translation, List<dynamic> args) {
- return evaluateJsonTemplate(translation, args);
- }
-
- var _messages;
- get messages => _messages ??=
- const JsonDecoder().convert(messageText) as Map<String, dynamic>;
- static final messageText = r'''
-{"back":"Нзд","browser":"Прегледач","forward":"Нпр","refresh":"Освж","search":"Претражите"}''';
-}
diff --git a/bin/simple_browser_internationalization/lib/localizations_delegate.dart b/bin/simple_browser_internationalization/lib/localizations_delegate.dart
deleted file mode 100644
index 2f3ef5c..0000000
--- a/bin/simple_browser_internationalization/lib/localizations_delegate.dart
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright 2019 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.
-
-// Should the localizations delegate be generated instead?
-
-import 'dart:async';
-import 'dart:ui' show Locale;
-
-import 'package:flutter/material.dart';
-import 'package:fuchsia_logger/logger.dart';
-import 'package:intl/intl.dart';
-
-import 'localization/messages_all.dart' as messages_all;
-import 'supported_locales.dart' as supported_locales;
-
-LocalizationsDelegate<void> delegate() => _LocalizationsDelegate();
-
-class _LocalizationsDelegate extends LocalizationsDelegate<void> {
- static Future<void> loadLocale(Locale locale) async {
- final String name =
- (locale.countryCode == null || locale.countryCode?.isEmpty == true)
- ? locale.languageCode
- : locale.toString();
- final String localeName = Intl.canonicalizedLocale(name);
- await messages_all.initializeMessages(localeName);
- log.info(
- '_LocalizationsDelegate: current default locale: ${Intl.defaultLocale}');
- }
-
- const _LocalizationsDelegate();
-
- @override
- Future<void> load(Locale locale) => loadLocale(locale);
-
- // For the time being, never reload.
- @override
- bool shouldReload(_LocalizationsDelegate __) => false;
-
- @override
- bool isSupported(Locale locale) {
- bool supported = supported_locales.locales.contains(locale);
- log.finer(
- '_LocalizationsDelegate: locale: ${locale.toString()}; isSupported: ${supported.toString()}');
- return supported;
- }
-}
diff --git a/bin/simple_browser_internationalization/lib/strings.dart b/bin/simple_browser_internationalization/lib/strings.dart
deleted file mode 100644
index 6e206ad..0000000
--- a/bin/simple_browser_internationalization/lib/strings.dart
+++ /dev/null
@@ -1,53 +0,0 @@
-// Copyright 2019 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.
-
-// This file is an L10N developer experience study fragment.
-// Let's see how it works when you offload all strings to a file.
-
-// The Intl.message texts have been offloaded to a separate file so as to
-// minimize the amount of code needed to be exported for localization.
-// While the individual static String get messages have been left in their original form,
-// the case-ness of the text is part of the presentation so should probably be
-// handled in the view code.
-
-import 'package:intl/intl.dart';
-
-/// Provides access to localized strings used in Ermine code.
-class Strings {
- static final Strings instance = Strings._internal();
- factory Strings() => instance;
- Strings._internal();
-
- static String get back => Intl.message(
- 'Bck',
- name: 'back',
- desc: 'A very short label meaning "Go back (to previous web page)"',
- );
- static String get forward => Intl.message(
- 'Fwd',
- name: 'forward',
- desc: 'A very short label meaning "Go forward (to the next web page)"',
- );
- static String get refresh => Intl.message(
- 'Rfrsh',
- name: 'refresh',
- desc: 'A very short label meaning "Refresh the web page"',
- );
- static String get search => Intl.message(
- 'Search',
- name: 'search',
- desc:
- 'A regular length string label appearing in the browser search bar',
- );
- static String get browser => Intl.message(
- 'Browser',
- name: 'browser',
- desc: 'As in: web browser',
- );
- static String get newtab => Intl.message(
- 'New Tab',
- name: 'newtab',
- desc: 'A default title for a newly created empty tab.',
- );
-}
diff --git a/bin/simple_browser_internationalization/lib/supported_locales.dart b/bin/simple_browser_internationalization/lib/supported_locales.dart
deleted file mode 100644
index 84d978f..0000000
--- a/bin/simple_browser_internationalization/lib/supported_locales.dart
+++ /dev/null
@@ -1,10 +0,0 @@
-// Copyright 2019 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:ui' show Locale;
-
-List<Locale> locales = [
- const Locale.fromSubtags(languageCode: 'sr'),
- const Locale.fromSubtags(languageCode: 'nl'),
-];
diff --git a/bin/simple_browser_internationalization/pubspec.yaml b/bin/simple_browser_internationalization/pubspec.yaml
deleted file mode 100644
index 4c61966..0000000
--- a/bin/simple_browser_internationalization/pubspec.yaml
+++ /dev/null
@@ -1,12 +0,0 @@
-# Copyright 2019 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.
-name: internationalization
-description: The internationalization library for Ermine.
-
-flutter:
-
-dependencies:
- # Needed for the ARB extraction scripts until this is fully automated.
- intl_translation: ^0.17.2
-
diff --git a/bin/simple_browser_internationalization/resources/intl_messages.arb b/bin/simple_browser_internationalization/resources/intl_messages.arb
deleted file mode 100644
index 9556f92..0000000
--- a/bin/simple_browser_internationalization/resources/intl_messages.arb
+++ /dev/null
@@ -1,33 +0,0 @@
-{
- "@@last_modified": "2019-11-11T17:42:01.328995",
- "back": "Bck",
- "@back": {
- "description": "A very short label meaning \"Go back (to previous web page)\"",
- "type": "text",
- "placeholders": {}
- },
- "forward": "Fwd",
- "@forward": {
- "description": "A very short label meaning \"Go forward (to the next web page)\"",
- "type": "text",
- "placeholders": {}
- },
- "refresh": "Rfrsh",
- "@refresh": {
- "description": "A very short label meaning \"Refresh the web page\"",
- "type": "text",
- "placeholders": {}
- },
- "search": "Search",
- "@search": {
- "description": "A regular length string label appearing in the browser search bar",
- "type": "text",
- "placeholders": {}
- },
- "browser": "Browser",
- "@browser": {
- "description": "As in: web browser",
- "type": "text",
- "placeholders": {}
- }
-}
diff --git a/bin/simple_browser_internationalization/resources/intl_nl.arb b/bin/simple_browser_internationalization/resources/intl_nl.arb
deleted file mode 100644
index 9728252..0000000
--- a/bin/simple_browser_internationalization/resources/intl_nl.arb
+++ /dev/null
@@ -1,33 +0,0 @@
-{
- "@@last_modified": "2019-11-06T17:31:05.454814",
- "back": "Trg",
- "@back": {
- "description": "A very short label meaning \"Go back (to previous web page)\"",
- "type": "text",
- "placeholders": {}
- },
- "forward": "Vor",
- "@forward": {
- "description": "A very short label meaning \"Go forward (to the next web page)\"",
- "type": "text",
- "placeholders": {}
- },
- "refresh": "Vrvrs",
- "@refresh": {
- "description": "A very short label meaning \"Refresh the web page\"",
- "type": "text",
- "placeholders": {}
- },
- "search": "Zoek",
- "@search": {
- "description": "A regular length string label appearing in the browser search bar",
- "type": "text",
- "placeholders": {}
- },
- "browser": "Browser",
- "@browser": {
- "description": "As in: web browser",
- "type": "text",
- "placeholders": {}
- }
-}
diff --git a/bin/simple_browser_internationalization/resources/intl_sr.arb b/bin/simple_browser_internationalization/resources/intl_sr.arb
deleted file mode 100644
index 7aca5cd..0000000
--- a/bin/simple_browser_internationalization/resources/intl_sr.arb
+++ /dev/null
@@ -1,33 +0,0 @@
-{
- "@@last_modified": "2019-11-06T17:31:05.454814",
- "back": "Наз",
- "@back": {
- "description": "A very short label meaning \"Go back (to previous web page)\"",
- "type": "text",
- "placeholders": {}
- },
- "forward": "Нпр",
- "@forward": {
- "description": "A very short label meaning \"Go forward (to the next web page)\"",
- "type": "text",
- "placeholders": {}
- },
- "refresh": "Освж",
- "@refresh": {
- "description": "A very short label meaning \"Refresh the web page\"",
- "type": "text",
- "placeholders": {}
- },
- "search": "Претрага",
- "@search": {
- "description": "A regular length string label appearing in the browser search bar",
- "type": "text",
- "placeholders": {}
- },
- "browser": "Прегледач",
- "@browser": {
- "description": "As in: web browser",
- "type": "text",
- "placeholders": {}
- }
-}
diff --git a/bin/simple_browser_internationalization/scripts/run_extract_to_arb.sh b/bin/simple_browser_internationalization/scripts/run_extract_to_arb.sh
deleted file mode 100755
index d847f84..0000000
--- a/bin/simple_browser_internationalization/scripts/run_extract_to_arb.sh
+++ /dev/null
@@ -1,16 +0,0 @@
-#!/bin/bash
-# Copyright 2019 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.
-
-set -x
-
-$FUCHSIA_DIR/third_party/dart-pkg/git/flutter/bin/flutter \
- packages pub get intl_translation:extract_to_arb
-
-$FUCHSIA_DIR/third_party/dart-pkg/git/flutter/bin/flutter \
- packages pub run intl_translation:extract_to_arb \
- --output-dir=resources lib/*strings.dart
-
-rm .packages pubspec.lock
-rm -fr .dart_tool
diff --git a/bin/simple_browser_internationalization/scripts/run_generate_from_arb.sh b/bin/simple_browser_internationalization/scripts/run_generate_from_arb.sh
deleted file mode 100755
index 93f7b81..0000000
--- a/bin/simple_browser_internationalization/scripts/run_generate_from_arb.sh
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/bin/bash
-# Copyright 2019 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.
-set -x
-
-$FUCHSIA_DIR/third_party/dart-pkg/git/flutter/bin/flutter \
- packages pub get intl_translation:generate_from_arb
-
-$FUCHSIA_DIR/third_party/dart-pkg/git/flutter/bin/flutter \
- packages pub run intl_translation:generate_from_arb \
- --output-dir=lib/localization \
- lib/*strings.dart \
- resources/*.arb
-
-rm .packages pubspec.lock
-rm -fr .dart_tool
diff --git a/examples/hello_experiences/BUILD.gn b/examples/hello_experiences/BUILD.gn
index ee4f4c1..c869f36 100644
--- a/examples/hello_experiences/BUILD.gn
+++ b/examples/hello_experiences/BUILD.gn
@@ -18,7 +18,7 @@
flutter_component("component") {
component_name = "hello-experiences"
- manifest = "meta/hello-experiences.cmx"
+ manifest = "meta/hello-experiences.cml"
deps = [ ":lib" ]
}
diff --git a/examples/hello_experiences/meta/hello-experiences.cml b/examples/hello_experiences/meta/hello-experiences.cml
new file mode 100644
index 0000000..3802e53
--- /dev/null
+++ b/examples/hello_experiences/meta/hello-experiences.cml
@@ -0,0 +1,21 @@
+{
+ include: [
+ // Enable system logging.
+ "syslog/client.shard.cml",
+ ],
+ program: {
+ data: "data/hello-experiences"
+ },
+ capabilities: [
+ {
+ protocol: [ "fuchsia.ui.app.ViewProvider" ],
+ },
+ ],
+ expose: [
+ {
+ protocol: [ "fuchsia.ui.app.ViewProvider" ],
+ from: "self",
+ to: "parent"
+ },
+ ],
+}
diff --git a/examples/hello_experiences/meta/hello-experiences.cmx b/examples/hello_experiences/meta/hello-experiences.cmx
deleted file mode 100644
index 21df53e..0000000
--- a/examples/hello_experiences/meta/hello-experiences.cmx
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "program": {
- "data": "data/hello-experiences"
- },
- "sandbox": {
- "services": [
- "fuchsia.cobalt.LoggerFactory",
- "fuchsia.fonts.Provider",
- "fuchsia.logger.LogSink",
- "fuchsia.modular.ComponentContext",
- "fuchsia.modular.ModuleContext",
- "fuchsia.netstack.Netstack",
- "fuchsia.posix.socket.Provider",
- "fuchsia.process.Launcher",
- "fuchsia.sys.Environment",
- "fuchsia.sys.Launcher",
- "fuchsia.ui.input.ImeService",
- "fuchsia.ui.input.ImeVisibilityService",
- "fuchsia.ui.policy.Presenter",
- "fuchsia.ui.scenic.Scenic"
- ]
- }
-}
diff --git a/examples/localized_flutter/localized_flutter_app/lib/localized_mod_localizations_delegate.dart b/examples/localized_flutter/localized_flutter_app/lib/localized_mod_localizations_delegate.dart
index 10ed432..8bf3943 100644
--- a/examples/localized_flutter/localized_flutter_app/lib/localized_mod_localizations_delegate.dart
+++ b/examples/localized_flutter/localized_flutter_app/lib/localized_mod_localizations_delegate.dart
@@ -48,6 +48,6 @@
// Delegate containing all app-level messages
LocalizedModLocalizationsDelegate(),
// Flutter-provided delegates for Flutter UI messages
- GlobalMaterialLocalizations.delegate,
+ ...GlobalMaterialLocalizations.delegates,
GlobalWidgetsLocalizations.delegate,
];
diff --git a/lib/BUILD.gn b/lib/BUILD.gn
index 0657544..a876772 100644
--- a/lib/BUILD.gn
+++ b/lib/BUILD.gn
@@ -6,7 +6,6 @@
deps = [
"ermine_localhost:ermine_localhost",
"ermine_ui:ermine_ui",
- "quickui:quickui",
]
}
@@ -18,7 +17,6 @@
_flutter_tester_tests += [
"ermine_localhost:ermine_localhost_unittests($host_toolchain)",
"ermine_ui:ermine_ui_unittests($host_toolchain)",
- "quickui:quickui_unittests($host_toolchain)",
]
}
diff --git a/lib/quickui/BUILD.gn b/lib/quickui/BUILD.gn
deleted file mode 100644
index 22dba1a..0000000
--- a/lib/quickui/BUILD.gn
+++ /dev/null
@@ -1,33 +0,0 @@
-# Copyright 2019 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("//build/dart/dart_library.gni")
-import("//build/dart/test.gni")
-
-dart_library("quickui") {
- package_name = "quickui"
- null_safe = true
-
- sources = [
- "quickui.dart",
- "uistream.dart",
- ]
-
- deps = [ "//sdk/fidl/fuchsia.ui.remotewidgets" ]
-}
-
-dart_test("quickui_unittests") {
- null_safe = true
- sources = [
- "quickui_test.dart",
- "uistream_test.dart",
- ]
-
- deps = [
- ":quickui",
- "//sdk/fidl/fuchsia.ui.remotewidgets",
- "//third_party/dart-pkg/pub/mockito",
- "//third_party/dart-pkg/pub/test",
- ]
-}
diff --git a/lib/quickui/README.md b/lib/quickui/README.md
deleted file mode 100644
index cddc438..0000000
--- a/lib/quickui/README.md
+++ /dev/null
@@ -1 +0,0 @@
-A library to build user interfaces for quick settings and interruptions.
\ No newline at end of file
diff --git a/lib/quickui/lib/quickui.dart b/lib/quickui/lib/quickui.dart
deleted file mode 100644
index adf8d8c..0000000
--- a/lib/quickui/lib/quickui.dart
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright 2019 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:async';
-import 'package:fidl_fuchsia_ui_remotewidgets/fidl_async.dart';
-
-/// Defines an interface for building UI based on [QuickUi] protocol.
-///
-/// This class should be extended and the [update] method should be provided.
-/// The [Spec] for the UI can be returned by setting the [spec] accessor. This
-/// can be called any time to update the UI. If the user of this class provides
-/// a [Value] at the time of requesting the UI, it is passed along in the
-/// [update] method. If this result in a change of the UI, it can be returned
-/// by calling the setter [spec].
-abstract class UiSpec extends QuickUi {
- // The [Completer] that holds the future for an outstanding [getSpec] request.
- Completer<Spec> _completer = Completer<Spec>();
-
- // Constructor.
- UiSpec([Spec? spec]) {
- if (spec != null) {
- _completer.complete(spec);
- }
- }
-
- /// Defines a 'null' spec, used to signal the QuickUI client to hide this
- /// service's UI.
- static final Spec nullSpec = Spec();
-
- /// Completes any outstanding Get for [Spec].
- set spec(Spec value) {
- if (_completer.isCompleted) {
- _completer = Completer<Spec>();
- }
- _completer.complete(value);
- }
-
- /// Overridden by derived classes.
- void update(Value value);
-
- /// Overridden by derived classes.
- void dispose();
-
- @override
- Future<Spec> getSpec([Value? value]) async {
- if (value != null) {
- update(value);
- }
-
- final future = _completer.future;
- if (_completer.isCompleted) {
- _completer = Completer<Spec>();
- }
- return future;
- }
-}
diff --git a/lib/quickui/lib/uistream.dart b/lib/quickui/lib/uistream.dart
deleted file mode 100644
index 064eb36..0000000
--- a/lib/quickui/lib/uistream.dart
+++ /dev/null
@@ -1,53 +0,0 @@
-// Copyright 2019 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:async';
-import 'package:fidl_fuchsia_ui_remotewidgets/fidl_async.dart';
-
-/// Defines a class that provides a [Stream] of UI [Spec].
-///
-/// The [stream] accessor provides the stream of [Spec] objects. The stream is
-/// started by calling [listen]. Call [dispose] to stop listening.
-/// Method [update] allows requesting a UI [Spec] by sending a [Value] as part
-/// of the request to the underlying [QuickUi] proxy.
-class UiStream {
- final QuickUi _ui;
- Spec? _spec;
- StreamSubscription<Spec>? _subscription;
- final _controller = StreamController<Spec>.broadcast();
-
- /// Constructor.
- UiStream(QuickUi ui) : _ui = ui;
-
- /// Returns the [Stream] over [Spec] objects.
- Stream<Spec> get stream => _controller.stream.asBroadcastStream();
-
- /// Returns the last [Spec] returned from QuickUi server.
- Spec? get spec => _spec;
-
- /// Defines a 'null' spec, used by the service to signal the client to hide
- /// its UI.
- static final Spec nullSpec = Spec(title: null, groups: null);
-
- /// Start listening to the [QuickUi] server.
- void listen() {
- update();
- }
-
- /// Send a [Value] to QuickUi server to request a new [Spec].
- void update([Value? value]) {
- _subscription?.cancel();
- _subscription = _ui.getSpec(value).asStream().listen((spec) {
- // Cache the spec until the next returned from the server.
- _spec = spec;
- _controller.add(spec);
- listen();
- });
- }
-
- /// Stop listening to the [QuickUi] server.
- void dispose() {
- _subscription?.cancel();
- }
-}
diff --git a/lib/quickui/test/quickui_test.dart b/lib/quickui/test/quickui_test.dart
deleted file mode 100644
index fbb8dee..0000000
--- a/lib/quickui/test/quickui_test.dart
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright 2019 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 'package:fidl_fuchsia_ui_remotewidgets/fidl_async.dart';
-import 'package:quickui/quickui.dart';
-import 'package:test/test.dart';
-
-void main() {
- test('Create UiSpec', () async {
- final ui = TestUi();
- final spec = await ui.getSpec(null);
-
- final group = spec.groups?.first;
- expect(group?.title, 'Foo');
- expect(group?.values?.length, 0);
- });
-
- test('Update UiSpec', () async {
- final ui = TestUi();
- Spec spec = await ui.getSpec(null);
-
- Group? group = spec.groups?.first;
- expect(group?.title, 'Foo');
- expect(group?.values?.length, 0);
-
- spec = await ui.getSpec(Value.withNumber(NumberValue(
- value: Number.withIntValue(5),
- action: 1,
- )));
-
- group = spec.groups?.first;
- expect(group?.title, 'Bar');
- expect(group?.values?.length, 1);
- expect(group?.values?.first.$tag, ValueTag.number);
- expect(group?.values?.first.number?.action, 1);
- expect(group?.values?.first.number?.value.intValue, 5);
- });
-}
-
-class TestUi extends UiSpec {
- TestUi() : super(_build());
-
- @override
- void update(Value value) {
- spec = Spec(
- title: '',
- groups: <Group>[
- Group(title: 'Bar', values: [value]),
- ],
- );
- }
-
- @override
- void dispose() {}
-
- //ignore: prefer_constructors_over_static_methods
- static Spec _build() {
- return Spec(title: '', groups: <Group>[Group(title: 'Foo', values: [])]);
- }
-}
diff --git a/lib/quickui/test/uistream_test.dart b/lib/quickui/test/uistream_test.dart
deleted file mode 100644
index 920ef2c..0000000
--- a/lib/quickui/test/uistream_test.dart
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright 2019 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 'package:fidl_fuchsia_ui_remotewidgets/fidl_async.dart';
-import 'package:quickui/quickui.dart';
-import 'package:quickui/uistream.dart';
-import 'package:test/test.dart';
-
-void main() {
- test('Create UiStream', () async {
- final ui = TestUi();
- final uiStream = UiStream(ui)..listen();
- final spec = await uiStream.stream.first;
-
- final group = spec.groups?.first;
- expect(group?.title, 'Foo');
- expect(group?.values?.length, 0);
- });
-
- test('Update UiStream', () async {
- final ui = TestUi();
- final uiStream = UiStream(ui)..listen();
- final stream = uiStream.stream;
- Spec spec = await stream.first;
-
- Group? group = spec.groups?.first;
- expect(group?.title, 'Foo');
- expect(group?.values?.length, 0);
-
- ui.update(Value.withNumber(NumberValue(
- value: Number.withIntValue(5),
- action: 1,
- )));
-
- spec = await stream.skip(1).first;
-
- group = spec.groups?.first;
- expect(group?.title, 'Bar');
- expect(group?.values?.length, 1);
- expect(group?.values?.first.$tag, ValueTag.number);
- expect(group?.values?.first.number?.action, 1);
- expect(group?.values?.first.number?.value.intValue, 5);
- });
-}
-
-class TestUi extends UiSpec {
- TestUi() : super(_build());
-
- @override
- void update(Value value) {
- spec = Spec(
- title: '',
- groups: <Group>[
- Group(title: 'Bar', values: [value]),
- ],
- );
- }
-
- @override
- void dispose() {}
-
- //ignore: prefer_constructors_over_static_methods
- static Spec _build() {
- return Spec(title: '', groups: <Group>[Group(title: 'Foo', values: [])]);
- }
-}
diff --git a/session_shells/BUILD.gn b/session_shells/BUILD.gn
index 9f318ee..d84d965 100644
--- a/session_shells/BUILD.gn
+++ b/session_shells/BUILD.gn
@@ -14,13 +14,10 @@
_flutter_tester_tests = []
if (host_os != "mac") {
_flutter_tester_tests += [
- "ermine/settings:ermine_settings_unittests($host_toolchain)",
"ermine/shell:ermine_unittests($host_toolchain)",
"ermine/keyboard_shortcuts:keyboard_shortcuts_unittests($host_toolchain)",
]
}
- deps =
- [ "//src/experiences/lib/quickui:quickui_unittests($host_toolchain)" ] +
- _flutter_tester_tests
+ deps = _flutter_tester_tests
}
diff --git a/session_shells/ermine/BUILD.gn b/session_shells/ermine/BUILD.gn
index c13223b..818a2f2 100644
--- a/session_shells/ermine/BUILD.gn
+++ b/session_shells/ermine/BUILD.gn
@@ -7,8 +7,7 @@
group("ermine") {
public_deps = [
":ermine-pkg",
- "session:workstation_routing",
- "session:workstation_session",
+ "session",
]
}
@@ -18,6 +17,7 @@
deps = [
"login:component",
+ "memfs:component",
"shell:component",
]
}
diff --git a/session_shells/ermine/internationalization/lib/strings.dart b/session_shells/ermine/internationalization/lib/strings.dart
index 8fb0b83..dbf920d 100644
--- a/session_shells/ermine/internationalization/lib/strings.dart
+++ b/session_shells/ermine/internationalization/lib/strings.dart
@@ -472,6 +472,18 @@
desc: 'The error text when password does not meet requirements',
);
+ static String get accountPasswordFailedAuthentication => Intl.message(
+ 'The password you entered was incorrect.',
+ name: 'accountPasswordFailedAuthentication',
+ desc: 'The error text when password does fails authentication',
+ );
+
+ static String get accountPartitionNotFound => Intl.message(
+ 'Account partition not found. Please re-pave the device.',
+ name: 'accountPartitionNotFound',
+ desc: 'The error text when account partition is not found',
+ );
+
static String get accountPasswordMismatch => Intl.message(
'The passwords do not match.',
name: 'accountPasswordMismatch',
@@ -967,6 +979,11 @@
name: 'increase volume',
desc: 'Keyboard shortcut description to increase sound volume.',
);
+ static String get logoutKeyboardShortcut => Intl.message(
+ 'Logout',
+ name: 'logout',
+ desc: 'Keyboard shortcut description to logout.',
+ );
static String get update => Intl.message(
'Update',
name: 'update',
@@ -977,6 +994,26 @@
name: 'continue',
desc: 'The label for "Continue" text field.',
);
+ static String get confirmLogoutAlertTitle => Intl.message(
+ 'Are you sure you want to log out?',
+ name: 'confirmLogoutAlertTitle',
+ desc: 'The alert dialog title to confirm logout.',
+ );
+ static String get confirmToSaveWorkAlertBody => Intl.message(
+ 'This will close all running applications and you could lose unsaved work.',
+ name: 'confirmToSaveWorkAlertBody',
+ desc: 'The alert dialog body to continue logout/restart/shutdown.',
+ );
+ static String get confirmRestartAlertTitle => Intl.message(
+ 'Are you sure you want restart?',
+ name: 'confirmRestartAlertTitle',
+ desc: 'The alert dialog title to confirm restart.',
+ );
+ static String get confirmShutdownAlertTitle => Intl.message(
+ 'Are you sure you want shutdown?',
+ name: 'confirmShutdownAlertTitle',
+ desc: 'The alert dialog title to confirm shutdown.',
+ );
static String get channelUpdateAlertTitle => Intl.message(
'System will reboot',
name: 'system will reboot',
@@ -1084,16 +1121,46 @@
);
static String get factoryDataReset => Intl.message(
- 'Factory Data Reset',
+ 'Erase all user data and settings and reset password',
name: 'factoryDataReset',
desc: 'The label of button to factory data reset a device.',
);
+
+ static String get factoryDataResetPrompt => Intl.message(
+ 'Are you sure to erase all user data and settings and reset the password?',
+ name: 'factoryDataReset',
+ desc: 'The label of button to factory data reset a device.',
+ );
+
+ static String get eraseAndReset => Intl.message(
+ 'Erase & Reset',
+ name: 'eraseAndReset',
+ desc: 'The label of button confirm factory data reset.',
+ );
+
static String get forget => Intl.message(
'Forget',
name: 'forget',
desc: 'The label for "Forget" text field.',
);
+ static String connectToNetwork(String network) => Intl.message(
+ 'Connect to $network',
+ name: 'Connect to network',
+ desc: 'The prompt text to connected to selected network.',
+ args: [network],
+ );
+ static String get connected => Intl.message(
+ 'Connected',
+ name: 'connected',
+ desc: 'The label for "Connected" text field.',
+ );
+ static String get incorrectPassword => Intl.message(
+ 'Incorrect Password',
+ name: 'incorrect password',
+ desc: 'The label for "Incorrect Password" text field.',
+ );
+
/// Lookup message given it's name.
static String? lookup(String name) {
final _messages = <String, String>{
diff --git a/session_shells/ermine/login/BUILD.gn b/session_shells/ermine/login/BUILD.gn
index 9ad4e07..ec75318 100644
--- a/session_shells/ermine/login/BUILD.gn
+++ b/session_shells/ermine/login/BUILD.gn
@@ -6,9 +6,11 @@
import("//build/dart/dart_library.gni")
import("//build/fidl/fidl.gni")
import("//build/flutter/flutter_component.gni")
+import("//build/testing/flutter_driver.gni")
declare_args() {
- # Whether or not to launch OOBE workflow on startup.
+ # TODO(http://fxb/85576): Whether or not to launch OOBE workflow on startup.
+ # This feature is still WIP but you can turn it on at your own risk.
start_oobe = false
}
@@ -16,9 +18,13 @@
package_name = "login"
null_safe = true
- entrypoints = [ "main.dart" ]
+ entrypoints = [
+ "main.dart",
+ "test_main.dart",
+ ]
services = [
+ "src/services/auth_service.dart",
"src/services/channel_service.dart",
"src/services/device_service.dart",
"src/services/privacy_consent_service.dart",
@@ -48,26 +54,35 @@
deps = [
"//sdk/dart/fidl",
+ "//sdk/dart/fuchsia_inspect",
"//sdk/dart/fuchsia_internationalization_flutter",
"//sdk/dart/fuchsia_logger",
"//sdk/dart/fuchsia_scenic_flutter",
"//sdk/dart/fuchsia_services",
+ "//sdk/dart/fuchsia_vfs",
"//sdk/dart/zircon",
- "//sdk/fidl/fuchsia.device.manager",
+ "//sdk/fidl/fuchsia.component",
+ "//sdk/fidl/fuchsia.component.decl",
"//sdk/fidl/fuchsia.element",
"//sdk/fidl/fuchsia.feedback",
+ "//sdk/fidl/fuchsia.hardware.power.statecontrol",
+ "//sdk/fidl/fuchsia.identity.account",
"//sdk/fidl/fuchsia.intl",
+ "//sdk/fidl/fuchsia.io",
"//sdk/fidl/fuchsia.mem",
+ "//sdk/fidl/fuchsia.recovery",
"//sdk/fidl/fuchsia.settings",
"//sdk/fidl/fuchsia.ssh",
"//sdk/fidl/fuchsia.sys",
"//sdk/fidl/fuchsia.ui.app",
"//sdk/fidl/fuchsia.ui.focus",
+ "//sdk/fidl/fuchsia.ui.scenic",
"//sdk/fidl/fuchsia.ui.views",
"//sdk/fidl/fuchsia.update.channelcontrol",
"//src/experiences/session_shells/ermine/internationalization",
"//src/experiences/session_shells/ermine/utils:ermine_utils",
"//third_party/dart-pkg/git/flutter/packages/flutter",
+ "//third_party/dart-pkg/git/flutter/packages/flutter_driver",
"//third_party/dart-pkg/git/flutter/packages/flutter_localizations",
"//third_party/dart-pkg/git/flutter/packages/flutter_test",
"//third_party/dart-pkg/pub/async",
@@ -78,9 +93,16 @@
}
flutter_component("component") {
- main_dart = "main.dart"
+ if (flutter_driver_enabled) {
+ main_dart = "test_main.dart"
+ } else {
+ main_dart = "main.dart"
+ }
+
component_name = "login"
- manifest = "meta/login.cmx"
+
+ manifest = "meta/login.cml"
+
deps = [
":images",
":lib",
@@ -94,13 +116,6 @@
}
}
-# fuchsia_package("login") {
-# deps = [
-# ":component",
-# ":images",
-# ]
-# }
-
resource("resources") {
sources = [
"config/privacy_policy.txt",
@@ -115,7 +130,9 @@
config_data("default_config") {
for_pkg = "ermine"
- sources = [ "config/default_config.json" ]
+ sources = [
+ "//src/experiences/session_shells/ermine/login/config/default_config.json",
+ ]
outputs = [ "startup_config.json" ]
}
@@ -123,7 +140,9 @@
config_data("oobe_config") {
for_pkg = "ermine"
- sources = [ "config/oobe_config.json" ]
+ sources = [
+ "//src/experiences/session_shells/ermine/login/config/oobe_config.json",
+ ]
outputs = [ "startup_config.json" ]
}
diff --git a/session_shells/ermine/login/lib/main.dart b/session_shells/ermine/login/lib/main.dart
index 9636aa6..5d3a7d5 100644
--- a/session_shells/ermine/login/lib/main.dart
+++ b/session_shells/ermine/login/lib/main.dart
@@ -16,7 +16,13 @@
setupLogger(name: 'login');
final oobe = OobeState.fromEnv();
final app = Observer(builder: (_) {
- return oobe.loginDone ? ErmineApp(oobe) : OobeApp(oobe);
+ return !oobe.ready
+ ? Offstage()
+ : oobe.loginDone
+ ? ErmineApp(oobe)
+ : oobe.launchOobe
+ ? OobeApp(oobe)
+ : Offstage();
});
runApp(app);
});
diff --git a/session_shells/ermine/login/lib/src/services/auth_service.dart b/session_shells/ermine/login/lib/src/services/auth_service.dart
new file mode 100644
index 0000000..cb2309d
--- /dev/null
+++ b/session_shells/ermine/login/lib/src/services/auth_service.dart
@@ -0,0 +1,216 @@
+// 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 'package:fidl/fidl.dart';
+import 'package:ermine_utils/ermine_utils.dart';
+import 'package:fidl_fuchsia_identity_account/fidl_async.dart';
+import 'package:fidl_fuchsia_io/fidl_async.dart';
+import 'package:fidl_fuchsia_recovery/fidl_async.dart';
+import 'package:fuchsia_logger/logger.dart';
+import 'package:fuchsia_services/services.dart';
+import 'package:fuchsia_vfs/vfs.dart';
+import 'package:internationalization/strings.dart';
+import 'package:mobx/mobx.dart';
+import 'package:zircon/zircon.dart';
+
+const kDeprecatedAccountName = 'created_by_user';
+const kSystemPickedAccountName = 'picked_by_system';
+const kUserPickedAccountName = 'picked_by_user';
+const kAccountDataDirectory = 'account_data';
+const kAccountCacheDirectory = 'account_cache';
+const kCacheSubdirectory = 'cache/';
+
+enum AuthMode { automatic, manual }
+
+/// Defines a service that performs authentication tasks like:
+/// - create an account with password
+/// - login to an account with password
+/// - logout from an account
+///
+/// Note:
+/// - It always picks the first account for login and logout.
+/// - Creating an account, when an account already exists, is an undefined
+/// behavior. The client of the service should ensure to not call account
+/// creation in this case.
+class AuthService {
+ late final PseudoDir hostedDirectories;
+
+ final _accountManager = AccountManagerProxy();
+ AccountProxy? _account;
+ final _accountIds = <int>[];
+ final _ready = false.asObservable();
+
+ AuthService() {
+ Incoming.fromSvcPath().connectToService(_accountManager);
+ }
+
+ void dispose() {
+ _accountManager.ctrl.close();
+ }
+
+ /// Load existing accounts from [AccountManager].
+ void loadAccounts(AuthMode currentAuthMode) async {
+ try {
+ final ids = (await _accountManager.getAccountIds()).toList();
+
+ // TODO(http://fxb/85576): Remove once login and OOBE are mandatory.
+ // Remove any accounts created with a deprecated name or from an auth
+ // mode that does not match the current build configuration.
+ final tempIds = <int>[]..addAll(ids);
+ for (var id in tempIds) {
+ final metadata = await _accountManager.getAccountMetadata(id);
+
+ if (metadata.name != null &&
+ _shouldRemoveAccountWithName(metadata.name!, currentAuthMode)) {
+ try {
+ await _accountManager.removeAccount(id, true);
+ ids.remove(id);
+ log.info('Removed account: $id with name: ${metadata.name}');
+ // ignore: avoid_catches_without_on_clauses
+ } catch (e) {
+ // We can only log and continue.
+ log.shout('Failed during deprecated account removal: $e');
+ }
+ }
+ }
+ _accountIds.addAll(ids);
+ runInAction(() => _ready.value = true);
+ if (ids.length > 1) {
+ log.shout(
+ 'Multiple (${ids.length}) accounts found, will use the first.');
+ }
+ // ignore: avoid_catches_without_on_clauses
+ } catch (e) {
+ log.shout('Failed during deprecated account removal: $e');
+ }
+ }
+
+ bool _shouldRemoveAccountWithName(String name, AuthMode currentAuthMode) {
+ if (name == kDeprecatedAccountName) {
+ return true;
+ }
+ if (currentAuthMode == AuthMode.automatic) {
+ // Current auth is automatic, remove account with user picked name.
+ return name == kUserPickedAccountName;
+ } else {
+ // Current auth is manual, remove account with system picked name.
+ return name == kSystemPickedAccountName;
+ }
+ }
+
+ /// Calls [FactoryReset] service to factory data reset the device.
+ void factoryReset() {
+ final proxy = FactoryResetProxy();
+ Incoming.fromSvcPath().connectToService(proxy);
+ proxy
+ .reset()
+ .then((status) => log.info('Requested factory reset.'))
+ .catchError((e) => log.shout('Failed to factory reset device: $e'));
+ }
+
+ /// Returns [true] after [_accountManager.getAccountIds()] completes.
+ bool get ready => _ready.value;
+
+ /// Returns [true] if no accounts exists on device.
+ bool get hasAccount {
+ assert(ready, 'Called before list of accounts could be retrieved.');
+ return _accountIds.isNotEmpty;
+ }
+
+ String errorFromException(Object e) {
+ if (e is MethodException) {
+ switch (e.value as Error) {
+ case Error.failedAuthentication:
+ return Strings.accountPasswordFailedAuthentication;
+ case Error.notFound:
+ return Strings.accountPartitionNotFound;
+ }
+ }
+ return e.toString();
+ }
+
+ /// Creates an account with password and sets up the account data directory.
+ Future<void> createAccountWithPassword(String password) async {
+ assert(_account == null, 'An account already exists.');
+ if (_account != null && _account!.ctrl.isBound) {
+ // ignore: unawaited_futures
+ _account!.lock().catchError((_) {});
+ _account!.ctrl.close();
+ }
+
+ final metadata = AccountMetadata(
+ name: password.isEmpty
+ ? kSystemPickedAccountName
+ : kUserPickedAccountName);
+ _account = AccountProxy();
+ await _accountManager.deprecatedProvisionNewAccount(
+ password,
+ metadata,
+ _account!.ctrl.request(),
+ );
+ final ids = await _accountManager.getAccountIds();
+ _accountIds
+ ..clear()
+ ..addAll(ids);
+ log.info('Account creation succeeded.');
+
+ await _publishAccountDirectory(_account!);
+ }
+
+ /// Logs in to the first account with [password] and sets up the account data
+ /// directory.
+ Future<void> loginWithPassword(String password) async {
+ assert(_accountIds.isNotEmpty, 'No account exist to login to.');
+ if (_account != null && _account!.ctrl.isBound) {
+ // ignore: unawaited_futures
+ _account!.lock().catchError((_) {});
+ _account!.ctrl.close();
+ }
+
+ _account = AccountProxy();
+ await _accountManager.deprecatedGetAccount(
+ _accountIds.first,
+ password,
+ _account!.ctrl.request(),
+ );
+ log.info('Login to first account on device succeeded.');
+
+ await _publishAccountDirectory(_account!);
+ }
+
+ /// Logs out of an account by locking it.
+ Future<void> logout() async {
+ assert(_account != null, 'No account exists to logout from.');
+ return _account!.lock();
+ }
+
+ Future<void> _publishAccountDirectory(Account account) async {
+ // Get the data directory for the account.
+ log.info('Getting data directory for account.');
+ final dataDirChannel = ChannelPair();
+ await account.getDataDirectory(InterfaceRequest(dataDirChannel.second));
+
+ // Open or create a subdirectory for the cache storage capability.
+ log.info('Opening cache directory for account.');
+ final dataDir = RemoteDir(dataDirChannel.first!);
+ final cacheDirChannel = ChannelPair();
+ dataDir.open(
+ openRightReadable |
+ openRightWritable |
+ openFlagCreate |
+ openFlagDirectory,
+ 0,
+ kCacheSubdirectory,
+ InterfaceRequest(cacheDirChannel.second));
+
+ // Host both directories.
+ hostedDirectories
+ ..removeNode(kAccountDataDirectory)
+ ..addNode(kAccountDataDirectory, dataDir)
+ ..removeNode(kAccountCacheDirectory)
+ ..addNode(kAccountCacheDirectory, RemoteDir(cacheDirChannel.first!));
+
+ log.info('Data and cache directories for account published.');
+ }
+}
diff --git a/session_shells/ermine/login/lib/src/services/device_service.dart b/session_shells/ermine/login/lib/src/services/device_service.dart
index 80ab868..b4ba9a7 100644
--- a/session_shells/ermine/login/lib/src/services/device_service.dart
+++ b/session_shells/ermine/login/lib/src/services/device_service.dart
@@ -2,13 +2,18 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-import 'package:fidl_fuchsia_device_manager/fidl_async.dart';
+import 'package:fidl_fuchsia_hardware_power_statecontrol/fidl_async.dart';
+import 'package:fuchsia_inspect/inspect.dart';
import 'package:fuchsia_services/services.dart';
/// Defines a service to handle device-specific operations like shutdown and
/// factory data reset.
class DeviceService {
- final _deviceManager = AdministratorProxy();
+ /// Callback to service [Inspect] requests from the system.
+ late final void Function(Node) onInspect;
+
+ final _deviceManager = AdminProxy();
+ final _inspect = Inspect();
DeviceService() {
Incoming.fromSvcPath().connectToService(_deviceManager);
@@ -18,5 +23,11 @@
_deviceManager.ctrl.close();
}
- void shutdown() => _deviceManager.suspend(suspendFlagPoweroff);
+ void serve(ComponentContext componentContext) {
+ _inspect
+ ..serve(componentContext.outgoing)
+ ..onDemand('login', onInspect);
+ }
+
+ void shutdown() => _deviceManager.poweroff();
}
diff --git a/session_shells/ermine/login/lib/src/services/shell_service.dart b/session_shells/ermine/login/lib/src/services/shell_service.dart
index 84e3b9e..51f673e 100644
--- a/session_shells/ermine/login/lib/src/services/shell_service.dart
+++ b/session_shells/ermine/login/lib/src/services/shell_service.dart
@@ -4,76 +4,108 @@
import 'dart:async';
-import 'package:fidl_fuchsia_element/fidl_async.dart';
-import 'package:fidl_fuchsia_sys/fidl_async.dart';
+import 'package:ermine_utils/ermine_utils.dart';
+import 'package:fidl_fuchsia_component/fidl_async.dart';
+import 'package:fidl_fuchsia_component_decl/fidl_async.dart';
+import 'package:fidl_fuchsia_io/fidl_async.dart';
import 'package:fidl_fuchsia_ui_app/fidl_async.dart';
+import 'package:fidl_fuchsia_ui_scenic/fidl_async.dart';
import 'package:fidl_fuchsia_ui_views/fidl_async.dart' hide FocusState;
+import 'package:fuchsia_logger/logger.dart';
import 'package:fuchsia_scenic_flutter/fuchsia_view.dart';
import 'package:fuchsia_services/services.dart';
+import 'package:flutter/material.dart';
+import 'package:mobx/mobx.dart';
import 'package:zircon/zircon.dart';
/// Defines a service to launch and support Ermine user shell.
class ShellService {
- final _graphicalPresenter = GraphicalPresenterProxy();
- late final FuchsiaViewConnection _fuchsiaViewConnection;
- bool _focusRequested = false;
late final StreamSubscription<bool> _focusSubscription;
+ late final VoidCallback onShellReady;
+ late final VoidCallback onShellExit;
+ late final bool _useFlatland;
+ _ErmineViewConnection? _ermine;
- void advertise(Outgoing outgoing) {
- outgoing.addPublicService((request) {
- GraphicalPresenterBinding().bind(_graphicalPresenter, request);
- }, GraphicalPresenter.$serviceName);
+ ShellService() {
+ ScenicProxy scenic = ScenicProxy();
+ Incoming.fromSvcPath().connectToService(scenic);
+ scenic.usesFlatland().then((scenicUsesFlatland) {
+ _useFlatland = scenicUsesFlatland;
+ runInAction(() => {_ready.value = true});
+ });
+ WidgetsFlutterBinding.ensureInitialized();
+ _focusSubscription = FocusState.instance.stream().listen(_onFocusChanged);
}
+ /// Returns [true] after call to [Scenic.usesFlatland] completes.
+ bool get ready => _ready.value;
+ final _ready = false.asObservable();
+
void dispose() {
_focusSubscription.cancel();
- _graphicalPresenter.ctrl.close();
}
/// Launch Ermine shell and return [FuchsiaViewConnection].
FuchsiaViewConnection launchErmineShell() {
- _focusSubscription = FocusState.instance.stream().listen(_onFocusChanged);
-
- final incoming = Incoming();
- final componentController = ComponentControllerProxy();
- final elementManager = ManagerProxy();
-
- final launcher = LauncherProxy();
- Incoming.fromSvcPath()
- ..connectToService(elementManager)
- ..connectToService(launcher)
- ..close();
-
- final binding = ServiceProviderBinding();
- final provider = ServiceProviderImpl()
- ..addServiceForName(
- (request) => ManagerBinding().bind(elementManager, request),
- Manager.$serviceName,
- );
-
- launcher.createComponent(
- LaunchInfo(
- url: 'fuchsia-pkg://fuchsia.com/ermine#meta/ermine.cmx',
- directoryRequest: incoming.request().passChannel(),
- additionalServices: ServiceList(
- names: [Manager.$serviceName],
- provider: binding.wrap(provider),
- ),
- ),
- componentController.ctrl.request(),
+ assert(_ermine == null, 'Instance of ermine shell already exists.');
+ _ermine = _ErmineViewConnection(
+ useFlatland: _useFlatland,
+ onReady: onShellReady,
+ onExit: onShellExit,
);
- launcher.ctrl.close();
+ return _ermine!.fuchsiaViewConnection;
+ }
- ViewProviderProxy viewProvider = ViewProviderProxy();
- incoming
- ..connectToService(viewProvider)
- ..connectToService(_graphicalPresenter);
+ void disposeErmineShell() {
+ _ermine = null;
+ }
- // TODO(fxbug.dev/86450): Flip this to use Flatland instead of legacy Scenic
- // Gfx API.
- const useFlatland = false;
+ // Transfer focus to Ermine shell whenever login shell receives focus.
+ void _onFocusChanged(bool focused) {
+ if (focused) {
+ _ermine?.setFocus();
+ }
+ }
+}
- // ignore: dead_code
+class _ErmineViewConnection {
+ final bool useFlatland;
+ final VoidCallback onReady;
+ final VoidCallback onExit;
+ late final FuchsiaViewConnection fuchsiaViewConnection;
+ bool _focusRequested = false;
+
+ _ErmineViewConnection({
+ required this.useFlatland,
+ required this.onReady,
+ required this.onExit,
+ }) {
+ // Connect to the Realm.
+ final realm = RealmProxy();
+ Incoming.fromSvcPath().connectToService(realm);
+
+ // Get the ermine shell's exposed /svc directory.
+ final exposedDir = DirectoryProxy();
+ realm.openExposedDir(
+ ChildRef(name: 'ermine_shell'), exposedDir.ctrl.request());
+
+ // Get the ermine shell's view provider.
+ final viewProvider = ViewProviderProxy();
+ Incoming.withDirectory(exposedDir).connectToService(viewProvider);
+ viewProvider.ctrl.whenClosed.then((_) => onExit());
+
+ fuchsiaViewConnection = _launch(viewProvider);
+ }
+
+ void setFocus() {
+ if (_focusRequested) {
+ fuchsiaViewConnection.requestFocus().catchError((e) {
+ log.shout(e);
+ });
+ }
+ }
+
+ FuchsiaViewConnection _launch(ViewProvider viewProvider) {
if (useFlatland) {
final viewTokens = ChannelPair();
assert(viewTokens.status == ZX.OK);
@@ -84,57 +116,42 @@
final createViewArgs =
CreateView2Args(viewCreationToken: viewCreationToken);
viewProvider.createView2(createViewArgs);
- viewProvider.ctrl.close();
- // TODO(fxbug.dev/86649): We should let the child send us the one they
- // minted for Flatland. Once that is available, we can call requestFocus()
- // on onViewStateChanged.
- return _fuchsiaViewConnection = FuchsiaViewConnection.flatland(
+ return FuchsiaViewConnection.flatland(
viewportCreationToken,
- onViewStateChanged: (_, state) {
- // Wait until ermine shell has rendered before focusing it.
- if (state == true && !_focusRequested) {
- _focusRequested = true;
- }
- },
+ onViewStateChanged: _onViewStateChanged,
+ );
+ } else {
+ final viewTokens = EventPairPair();
+ assert(viewTokens.status == ZX.OK);
+ final viewHolderToken = ViewHolderToken(value: viewTokens.first!);
+ final viewToken = ViewToken(value: viewTokens.second!);
+
+ final viewRefPair = EventPairPair();
+ final viewRef =
+ ViewRef(reference: viewRefPair.first!.duplicate(ZX.RIGHTS_BASIC));
+ final viewRefControl = ViewRefControl(
+ reference: viewRefPair.second!
+ .duplicate(ZX.DEFAULT_EVENTPAIR_RIGHTS & (~ZX.RIGHT_DUPLICATE)));
+ final viewRefInject =
+ ViewRef(reference: viewRefPair.first!.duplicate(ZX.RIGHTS_BASIC));
+
+ viewProvider.createViewWithViewRef(
+ viewToken.value, viewRefControl, viewRef);
+
+ return FuchsiaViewConnection(
+ viewHolderToken,
+ viewRef: viewRefInject,
+ onViewStateChanged: _onViewStateChanged,
);
}
-
- final viewTokens = EventPairPair();
- assert(viewTokens.status == ZX.OK);
- final viewHolderToken = ViewHolderToken(value: viewTokens.first!);
- final viewToken = ViewToken(value: viewTokens.second!);
-
- final viewRefPair = EventPairPair();
- final viewRef =
- ViewRef(reference: viewRefPair.first!.duplicate(ZX.RIGHTS_BASIC));
- final viewRefControl = ViewRefControl(
- reference: viewRefPair.second!
- .duplicate(ZX.DEFAULT_EVENTPAIR_RIGHTS & (~ZX.RIGHT_DUPLICATE)));
- final viewRefInject =
- ViewRef(reference: viewRefPair.first!.duplicate(ZX.RIGHTS_BASIC));
-
- viewProvider.createViewWithViewRef(
- viewToken.value, viewRefControl, viewRef);
- viewProvider.ctrl.close();
-
- return _fuchsiaViewConnection = FuchsiaViewConnection(
- viewHolderToken,
- viewRef: viewRefInject,
- onViewStateChanged: (_, state) {
- // Wait until ermine shell has rendered before focusing it.
- if (state == true && !_focusRequested) {
- _focusRequested = true;
- _fuchsiaViewConnection.requestFocus();
- }
- },
- );
}
- void _onFocusChanged(bool focused) {
- if (_focusRequested && focused) {
- // ignore: unawaited_futures
- _fuchsiaViewConnection.requestFocus();
+ void _onViewStateChanged(FuchsiaViewController _, bool? state) {
+ if (state == true && !_focusRequested) {
+ _focusRequested = true;
+ setFocus();
+ onReady();
}
}
}
diff --git a/session_shells/ermine/login/lib/src/states/oobe_state.dart b/session_shells/ermine/login/lib/src/states/oobe_state.dart
index ebf21ac..af062b0 100644
--- a/session_shells/ermine/login/lib/src/states/oobe_state.dart
+++ b/session_shells/ermine/login/lib/src/states/oobe_state.dart
@@ -5,6 +5,7 @@
import 'dart:ui';
import 'package:fuchsia_scenic_flutter/fuchsia_view.dart';
+import 'package:login/src/services/auth_service.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';
@@ -15,7 +16,7 @@
/// The oobe screens the user navigates through.
// TODO(fxbug.dev/73407): Skip data sharing screen until privacy policy is
// finalized.
-enum OobeScreen { channel, /* dataSharing, */ sshKeys, password, done }
+enum OobeScreen { /* channel, dataSharing, sshKeys,*/ password, done }
/// The screens the user navigates through to add SSH keys.
enum SshScreen { add, confirm, error, exit }
@@ -39,7 +40,11 @@
abstract int sshKeyIndex;
bool get privacyVisible;
bool get launchOobe;
+ bool get ready;
+ bool get hasAccount;
bool get loginDone;
+ bool get wait;
+ String get authError;
FuchsiaViewConnection get ermineViewConnection;
String get privacyPolicy;
@@ -59,9 +64,12 @@
void skip();
void finish();
void shutdown();
+ void factoryReset();
+ void resetAuthError();
factory OobeState.fromEnv() {
return OobeStateImpl(
+ authService: AuthService(),
deviceService: DeviceService(),
shellService: ShellService(),
channelService: ChannelService(),
diff --git a/session_shells/ermine/login/lib/src/states/oobe_state_impl.dart b/session_shells/ermine/login/lib/src/states/oobe_state_impl.dart
index 6363a57..f8b3be5 100644
--- a/session_shells/ermine/login/lib/src/states/oobe_state_impl.dart
+++ b/session_shells/ermine/login/lib/src/states/oobe_state_impl.dart
@@ -8,24 +8,30 @@
import 'package:ermine_utils/ermine_utils.dart';
import 'package:flutter/services.dart';
+import 'package:fuchsia_inspect/inspect.dart';
import 'package:fuchsia_logger/logger.dart';
import 'package:fuchsia_scenic_flutter/fuchsia_view.dart';
import 'package:fuchsia_services/services.dart';
+import 'package:fuchsia_vfs/vfs.dart';
import 'package:internationalization/strings.dart';
-import 'package:mobx/mobx.dart';
+import 'package:login/src/services/auth_service.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';
+import 'package:mobx/mobx.dart';
+
+const kHostedDirectories = 'hosted_directories';
/// Defines an implementation of [OobeState].
class OobeStateImpl with Disposable implements OobeState {
- static const kDefaultConfigJson = '/config/data/startup_config.json';
+ static const kDefaultConfigJson = '/config/data/ermine/startup_config.json';
static const kStartupConfigJson = '/data/startup_config.json';
final ComponentContext componentContext;
+ final AuthService authService;
final ChannelService channelService;
final DeviceService deviceService;
final SshKeysService sshKeysService;
@@ -33,6 +39,7 @@
final PrivacyConsentService privacyConsentService;
OobeStateImpl({
+ required this.authService,
required this.deviceService,
required this.shellService,
required this.channelService,
@@ -41,6 +48,24 @@
}) : componentContext = ComponentContext.create(),
_localeStream = channelService.stream.asObservable() {
privacyPolicy = privacyConsentService.privacyPolicy;
+ shellService
+ ..onShellReady = _onErmineShellReady
+ ..onShellExit = _onErmineShellExit;
+ deviceService
+ ..onInspect = _onInspect
+ ..serve(componentContext);
+
+ // Create a directory that will host the account_data directory. This is needed
+ // because the root directories are expected to exist at the time of serving.
+ final hostedDirectories = PseudoDir();
+ componentContext.outgoing
+ .rootDir()
+ .addNode(kHostedDirectories, hostedDirectories);
+
+ authService
+ ..hostedDirectories = hostedDirectories
+ ..loadAccounts(launchOobe ? AuthMode.manual : AuthMode.automatic);
+ componentContext.outgoing.serveFromStartupInfo();
channelService.onConnected = (connected) => runInAction(() async {
if (connected) {
@@ -51,8 +76,6 @@
}
_updateChannelsAvailable.value = connected;
});
- shellService.advertise(componentContext.outgoing);
- componentContext.outgoing.serveFromStartupInfo();
// We cannot load MaterialIcons font file from pubspec.yaml. So load it
// explicitly.
@@ -65,11 +88,22 @@
}())
..load();
}
+
+ // TODO(http://fxb/85576): Remove once login and OOBE are mandatory.
+ // If we are skipping OOBE, authenticate using empty password.
+ reactions.add(reaction((_) => ready, (ready) {
+ if (!launchOobe) {
+ _performNullLogin().then((_) {
+ runInAction(() => _loginDone.value = true);
+ });
+ }
+ }));
}
@override
void dispose() {
super.dispose();
+ authService.dispose();
channelService.dispose();
privacyConsentService.dispose();
sshKeysService.dispose();
@@ -78,13 +112,8 @@
@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);
- }
+ late final _launchOobe = Observable<bool>(() {
+ File 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.');
@@ -95,21 +124,34 @@
}());
@override
+ bool get ready => _ready.value;
+ late final _ready = (() {
+ return shellService.ready && authService.ready;
+ }).asComputed();
+
+ @override
+ bool get hasAccount {
+ // This should be called only after startup services are ready.
+ assert(ready, 'Startup services are not initialized.');
+ return authService.hasAccount;
+ }
+
+ @override
bool get loginDone => _loginDone.value;
- final _loginDone = true.asObservable();
+ final _loginDone = false.asObservable();
@override
Locale? get locale => _localeStream.value;
final ObservableStream<Locale> _localeStream;
- FuchsiaViewConnection? _ermineViewConnection;
+ final _ermineViewConnection = Observable<FuchsiaViewConnection?>(null);
@override
FuchsiaViewConnection get ermineViewConnection =>
- _ermineViewConnection ??= shellService.launchErmineShell();
+ _ermineViewConnection.value ??= shellService.launchErmineShell();
@override
OobeScreen get screen => _screen.value;
- final Observable<OobeScreen> _screen = OobeScreen.channel.asObservable();
+ final Observable<OobeScreen> _screen = OobeScreen.password.asObservable();
@override
bool get updateChannelsAvailable => _updateChannelsAvailable.value;
@@ -180,6 +222,14 @@
late final String privacyPolicy;
@override
+ String get authError => _authError.value;
+ final _authError = ''.asObservable();
+
+ @override
+ bool get wait => _wait.value;
+ final _wait = false.asObservable();
+
+ @override
void setCurrentChannel(String channel) => runInAction(() async {
await channelService.setCurrentChannel(channel);
_currentChannel.value = await channelService.currentChannel;
@@ -279,27 +329,120 @@
@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();
+ void setPassword(String password) async {
+ try {
+ runInAction(() {
+ _authError.value = '';
+ _wait.value = true;
+ });
+ await authService.createAccountWithPassword(password);
+ runInAction(() => _wait.value = false);
+ nextScreen();
+ // ignore: avoid_catches_without_on_clauses
+ } catch (e) {
+ log.shout('Caught exception during account creation: $e');
+ runInAction(() {
+ _wait.value = false;
+ _authError.value = authService.errorFromException(e);
+ });
+ }
+ }
@override
- // TODO(http://fxb/81598): Implement login functionality.
- void login(String password) => finish();
+ void resetAuthError() => runInAction(() => _authError.value = '');
+
+ @override
+ void login(String password) async {
+ try {
+ runInAction(() {
+ _authError.value = '';
+ _wait.value = true;
+ });
+ await authService.loginWithPassword(password);
+ runInAction(() => _wait.value = false);
+ finish();
+ // ignore: avoid_catches_without_on_clauses
+ } catch (e) {
+ log.shout('Caught exception during login: $e');
+ runInAction(() {
+ _wait.value = false;
+ _authError.value = authService.errorFromException(e);
+ });
+ }
+ }
@override
void shutdown() => deviceService.shutdown();
+
+ @override
+ void factoryReset() => authService.factoryReset();
+
+ bool _ermineReady = false;
+ void _onErmineShellReady() {
+ _ermineReady = true;
+ }
+
+ void _onErmineShellExit() {
+ _ermineReady = false;
+
+ runInAction(() => _loginDone.value = false);
+
+ // Define a local method to run after logout below.
+ void postLogout() {
+ // If OOBE is disabled, perform empty password re-login.
+ if (!launchOobe) {
+ _performNullLogin().then((_) {
+ runInAction(() {
+ shellService.disposeErmineShell();
+ _ermineViewConnection.value = null;
+ _loginDone.value = true;
+ });
+ });
+ } else {
+ // Display login screen again.
+ runInAction(() {
+ shellService.disposeErmineShell();
+ _ermineViewConnection.value = null;
+ });
+ }
+ }
+
+ // Logout and call [postLogout] on both success and error case.
+ authService.logout().then((_) => postLogout()).catchError((e) {
+ log.shout('Caught exception during logout: $e');
+ postLogout();
+ });
+ }
+
+ // TODO(http://fxb/85576): Remove once login and OOBE are mandatory.
+ // If we are skipping OOBE, authenticate using empty password.
+ Future<void> _performNullLogin() async {
+ log.info('Skipped OOBE, authenticating with empty password.');
+ try {
+ authService.hasAccount
+ ? await authService.loginWithPassword('')
+ : await authService.createAccountWithPassword('');
+ // ignore: avoid_catches_without_on_clauses
+ } catch (e) {
+ log.shout('Account found: ${authService.hasAccount}.'
+ ' Caught exception during authentication: $e');
+ }
+ }
+
+ void _onInspect(Node node) {
+ node.boolProperty('ready')!.setValue(ready);
+ node.boolProperty('launchOOBE')!.setValue(launchOobe);
+ node.boolProperty('ermineReady')!.setValue(_ermineReady);
+ node.boolProperty('authenticated')!.setValue(loginDone);
+ if (hasAccount) {
+ node.stringProperty('screen')!.setValue('login');
+ } else {
+ node.stringProperty('screen')!.setValue(screen.name);
+ }
+ }
}
diff --git a/session_shells/ermine/login/lib/src/widgets/app.dart b/session_shells/ermine/login/lib/src/widgets/app.dart
index 23c976e..1ca4d5f 100644
--- a/session_shells/ermine/login/lib/src/widgets/app.dart
+++ b/session_shells/ermine/login/lib/src/widgets/app.dart
@@ -14,9 +14,7 @@
as supported_locales;
import 'package:intl/intl.dart';
import 'package:login/src/states/oobe_state.dart';
-import 'package:login/src/widgets/ermine.dart';
-// TODO(http://fxb/81598): Uncomment once login is ready.
-// import 'package:login/src/widgets/login.dart';
+import 'package:login/src/widgets/login.dart';
import 'package:login/src/widgets/oobe.dart';
class OobeApp extends StatelessWidget {
@@ -38,11 +36,10 @@
locale: locale,
localizationsDelegates: [
localizations.delegate(),
- GlobalMaterialLocalizations.delegate,
+ ...GlobalMaterialLocalizations.delegates,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: supported_locales.locales,
- shortcuts: FuchsiaKeyboard.defaultShortcuts,
scrollBehavior: MaterialScrollBehavior().copyWith(
dragDevices: {PointerDeviceKind.mouse, PointerDeviceKind.touch},
),
@@ -52,11 +49,9 @@
return Material(
type: MaterialType.canvas,
child: Observer(builder: (_) {
- return WidgetFactory.create(() => oobe.launchOobe
- ? Oobe(oobe, onFinish: oobe.finish)
- // TODO(http://fxb/81598): Uncomment once login is ready.
- // : Login(oobe));
- : ErmineApp(oobe));
+ return oobe.hasAccount
+ ? WidgetFactory.create(() => Login(oobe))
+ : WidgetFactory.create(() => Oobe(oobe));
}),
);
}),
diff --git a/session_shells/ermine/login/lib/src/widgets/ermine.dart b/session_shells/ermine/login/lib/src/widgets/ermine.dart
index badb8d2..7b209ef 100644
--- a/session_shells/ermine/login/lib/src/widgets/ermine.dart
+++ b/session_shells/ermine/login/lib/src/widgets/ermine.dart
@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flutter/material.dart';
+import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:fuchsia_scenic_flutter/fuchsia_view.dart';
import 'package:login/src/states/oobe_state.dart';
@@ -14,6 +15,8 @@
@override
Widget build(BuildContext context) {
- return FuchsiaView(controller: oobe.ermineViewConnection);
+ return Observer(builder: (_) {
+ return FuchsiaView(controller: oobe.ermineViewConnection);
+ });
}
}
diff --git a/session_shells/ermine/login/lib/src/widgets/login.dart b/session_shells/ermine/login/lib/src/widgets/login.dart
index ec297fa..04a827f 100644
--- a/session_shells/ermine/login/lib/src/widgets/login.dart
+++ b/session_shells/ermine/login/lib/src/widgets/login.dart
@@ -7,6 +7,7 @@
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:internationalization/strings.dart';
import 'package:login/src/states/oobe_state.dart';
+import 'package:mobx/mobx.dart';
/// Width of the password field widget.
const double kOobeBodyFieldWidth = 492;
@@ -18,6 +19,7 @@
final _formState = GlobalKey<FormState>();
final _passwordController = TextEditingController();
final _showPassword = false.asObservable();
+ final _focusNode = FocusNode();
Login(this.oobe);
@@ -25,167 +27,161 @@
Widget build(BuildContext context) {
return Material(
color: Color.fromARGB(0xff, 0x0c, 0x0c, 0x0c),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- // Header: Fuchsia logo and welcome.
- SizedBox(
- height: 200,
- child: Row(
- mainAxisAlignment: MainAxisAlignment.center,
- crossAxisAlignment: CrossAxisAlignment.end,
- children: [
- // Fuchsia logo.
- Image(
- image: AssetImage('images/Fuchsia-logo-2x.png'),
- color: Theme.of(context).colorScheme.primary,
- width: 24,
- height: 24,
- ),
- SizedBox(width: 16),
- // Welcome text.
- Text(
- Strings.fuchsiaWelcome,
- style: Theme.of(context).textTheme.headline6,
- ),
- ],
- ),
- ),
-
- // Body: Oobe screens.
- Expanded(
- child: Padding(
- padding: EdgeInsets.all(16),
- child: FocusScope(
- child: Observer(builder: (context) {
- return Column(
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- // Password.
- Expanded(
- child: Form(
- key: _formState,
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- // Title.
- Text(
- Strings.login,
- style: Theme.of(context).textTheme.headline3,
+ child: Center(
+ child: FocusScope(
+ child: Observer(builder: (context) {
+ return Form(
+ onChanged: oobe.resetAuthError,
+ key: _formState,
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Title.
+ Padding(
+ padding: EdgeInsets.only(left: 16),
+ child: Text(
+ Strings.login,
+ style: Theme.of(context).textTheme.headline3,
+ ),
+ ),
+ SizedBox(height: 36),
+ // Password.
+ SizedBox(
+ width: kOobeBodyFieldWidth,
+ height: 92,
+ child: Padding(
+ padding: EdgeInsets.only(left: 16),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Expanded(
+ child: TextFormField(
+ key: ValueKey('password'),
+ focusNode: _focusNode,
+ autofocus: true,
+ autovalidateMode:
+ AutovalidateMode.onUserInteraction,
+ controller: _passwordController,
+ obscureText: !_showPassword.value,
+ decoration: InputDecoration(
+ border: OutlineInputBorder(),
+ labelText: Strings.passwordHint,
+ errorText: oobe.authError.isNotEmpty
+ ? oobe.authError
+ : null,
),
- SizedBox(height: 40),
- // Password.
- SizedBox(
- width: kOobeBodyFieldWidth,
- child: TextFormField(
- autofocus: true,
- controller: _passwordController,
- obscureText: !_showPassword.value,
- decoration: InputDecoration(
- border: OutlineInputBorder(),
- labelText: Strings.passwordHint,
+ validator: (value) {
+ if (value == null || value.isEmpty) {
+ return Strings.accountPasswordInvalid;
+ }
+ if (oobe.authError.isNotEmpty) {
+ Focus.of(context).requestFocus(_focusNode);
+ }
+ return null;
+ },
+ onFieldSubmitted: (_) {
+ if (_validate()) {
+ oobe.login(_passwordController.text);
+ }
+ },
+ ),
+ ),
+ SizedBox(width: 16),
+ Container(
+ width: 56,
+ height: 56,
+ color: oobe.wait
+ ? Theme.of(context).disabledColor
+ : Colors.white,
+ child: oobe.wait
+ ? Center(child: CircularProgressIndicator())
+ : ElevatedButton(
+ key: ValueKey('login'),
+ child: Icon(Icons.arrow_forward),
+ onPressed: () => _validate() && !oobe.wait
+ ? oobe.login(_passwordController.text)
+ : null,
),
- validator: (value) {
- // TODO(http://fxb/81598): Uncomment once
- // login functionality is ready.
- // if (value == null ||
- // value.isEmpty ||
- // value.length < passwordLength) {
- // return Strings.accountPasswordInvalid;
- // }
- return null;
- },
- onFieldSubmitted: (_) {
- if (_validate()) {
- oobe.login(_passwordController.text);
- }
- },
- ),
- ),
- SizedBox(height: 40),
+ ),
+ ],
+ ),
+ ),
+ ),
- // Show password checkbox.
- SizedBox(
- width: kOobeBodyFieldWidth,
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- Checkbox(
- onChanged: (value) =>
- _showPassword.value = value == true,
- value: _showPassword.value,
- ),
- SizedBox(height: 40),
- Text(Strings.showPassword)
- ],
- ),
- ),
- SizedBox(height: 40),
+ // Show password checkbox.
+ SizedBox(
+ width: kOobeBodyFieldWidth,
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Checkbox(
+ onChanged: (value) => runInAction(
+ () => _showPassword.value = value == true),
+ value: _showPassword.value,
+ ),
+ SizedBox(height: 40),
+ Text(Strings.showPassword)
+ ],
+ ),
+ ),
+ SizedBox(height: 144),
- // Factory reset.
- SizedBox(
- width: kOobeBodyFieldWidth,
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- TextButton(
- onPressed: () {},
- child: Text(
- Strings.factoryDataReset,
- style: TextStyle(
- decoration: TextDecoration.underline,
- ),
- ),
- ),
- SizedBox(width: 10),
- IconButton(
- icon: Icon(Icons.help_outline),
- // TODO(http://fxb/81598): Implement as a
- // tooltip.
- onPressed: () {},
- ),
- ],
- ),
- )
- ],
+ Container(
+ alignment: Alignment.centerLeft,
+ padding: EdgeInsets.only(left: 16),
+ width: kOobeBodyFieldWidth,
+ child: TextButton(
+ style: TextButton.styleFrom(padding: EdgeInsets.zero),
+ onPressed: () => _confirmFactoryReset(context),
+ child: Container(
+ padding: EdgeInsets.only(bottom: 1),
+ decoration: BoxDecoration(
+ border: Border(
+ bottom: BorderSide(
+ color: Colors.white,
+ width: 1,
+ ),
),
),
+ child: Text(Strings.factoryDataReset),
),
-
- // Buttons.
- Container(
- alignment: Alignment.center,
- padding: EdgeInsets.all(24),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- // Shutdown button.
- OutlinedButton(
- onPressed: oobe.shutdown,
- child: Text(Strings.shutdown.toUpperCase()),
- ),
- SizedBox(width: 24),
- // Login button.
- ElevatedButton(
- onPressed: () => _validate()
- ? oobe.login(_passwordController.text)
- : null,
- child: Text(Strings.login.toUpperCase()),
- ),
- ],
- ),
- ),
- ],
- );
- }),
+ ),
+ ),
+ ],
),
- ),
- ),
- ],
+ );
+ }),
+ ),
),
);
}
bool _validate() => _formState.currentState?.validate() ?? false;
+
+ void _confirmFactoryReset(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: SizedBox(
+ width: 672,
+ child: Text(Strings.factoryDataResetPrompt),
+ ),
+ actions: [
+ OutlinedButton(
+ child: Text(Strings.cancel.toUpperCase()),
+ onPressed: () => Navigator.pop(context, false),
+ ),
+ ElevatedButton(
+ child: Text(Strings.eraseAndReset.toUpperCase()),
+ onPressed: () => Navigator.pop(context, true),
+ ),
+ ],
+ ),
+ ).then((erase) {
+ if (erase) {
+ oobe.factoryReset();
+ }
+ });
+ }
}
diff --git a/session_shells/ermine/login/lib/src/widgets/oobe.dart b/session_shells/ermine/login/lib/src/widgets/oobe.dart
index 76fc4bc..794aa58 100644
--- a/session_shells/ermine/login/lib/src/widgets/oobe.dart
+++ b/session_shells/ermine/login/lib/src/widgets/oobe.dart
@@ -7,19 +7,18 @@
import 'package:internationalization/strings.dart';
import 'package:login/src/states/oobe_state.dart';
import 'package:login/src/widgets/password.dart';
-import 'package:login/src/widgets/channels.dart';
import 'package:login/src/widgets/ready.dart';
// TODO(fxbug.dev/73407): Skip data sharing screen until privacy policy is
// finalized.
+// import 'package:login/src/widgets/channels.dart';
// import 'package:login/src/widgets/data_sharing.dart';
-import 'package:login/src/widgets/ssh_keys.dart';
+// import 'package:login/src/widgets/ssh_keys.dart';
/// Defines a widget that handles the OOBE flow.
class Oobe extends StatelessWidget {
final OobeState oobe;
- final VoidCallback onFinish;
- const Oobe(this.oobe, {required this.onFinish});
+ const Oobe(this.oobe);
@override
Widget build(BuildContext context) {
@@ -56,14 +55,14 @@
Expanded(
child: Observer(builder: (context) {
switch (oobe.screen) {
- case OobeScreen.channel:
- return Channels(oobe);
// TODO(fxbug.dev/73407): Skip data sharing screen until privacy
// policy is finalized.
+ // case OobeScreen.channel:
+ // return Channels(oobe);
// case OobeScreen.dataSharing:
// return DataSharing(oobe);
- case OobeScreen.sshKeys:
- return SshKeys(oobe);
+ // case OobeScreen.sshKeys:
+ // return SshKeys(oobe);
case OobeScreen.password:
return Password(oobe);
case OobeScreen.done:
@@ -79,7 +78,7 @@
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
- for (var index = OobeScreen.channel.index;
+ for (var index = OobeScreen.password.index;
index <= OobeScreen.done.index;
index++)
Padding(
diff --git a/session_shells/ermine/login/lib/src/widgets/password.dart b/session_shells/ermine/login/lib/src/widgets/password.dart
index 7798674..fbab7e8 100644
--- a/session_shells/ermine/login/lib/src/widgets/password.dart
+++ b/session_shells/ermine/login/lib/src/widgets/password.dart
@@ -9,6 +9,7 @@
import 'package:login/src/states/oobe_state.dart';
import 'package:login/src/widgets/header.dart';
import 'package:login/src/widgets/login.dart';
+import 'package:mobx/mobx.dart';
/// Defines a widget to create account password.
class Password extends StatelessWidget {
@@ -49,7 +50,9 @@
SizedBox(
width: kOobeBodyFieldWidth,
child: TextFormField(
+ key: ValueKey('password1'),
autofocus: true,
+ autovalidateMode: AutovalidateMode.onUserInteraction,
controller: _passwordController,
obscureText: !_showPassword.value,
decoration: InputDecoration(
@@ -57,13 +60,11 @@
labelText: Strings.passwordHint,
),
validator: (value) {
- // TODO(http://fxb/81598): Uncomment once
- // login functionality is ready.
- // if (value == null ||
- // value.isEmpty ||
- // value.length < passwordLength) {
- // return Strings.accountPasswordInvalid;
- // }
+ if (value == null ||
+ value.isEmpty ||
+ value.length < kPasswordLength) {
+ return Strings.accountPasswordInvalid;
+ }
return null;
},
),
@@ -73,6 +74,8 @@
SizedBox(
width: kOobeBodyFieldWidth,
child: TextFormField(
+ key: ValueKey('password2'),
+ autovalidateMode: AutovalidateMode.onUserInteraction,
controller: _confirmPasswordController,
obscureText: !_showPassword.value,
decoration: InputDecoration(
@@ -80,16 +83,9 @@
labelText: Strings.confirmPasswordHint,
),
validator: (value) {
- // TODO(http://fxb/81598): Uncomment once
- // login functionality is ready.
- // if (value == null ||
- // value.isEmpty ||
- // value.length < passwordLength) {
- // return Strings.accountPasswordInvalid;
- // }
- // if (value != _passwordController.text) {
- // return Strings.accountPasswordMismatch;
- // }
+ if (value != _passwordController.text) {
+ return Strings.accountPasswordMismatch;
+ }
return null;
},
onFieldSubmitted: (value) =>
@@ -104,15 +100,30 @@
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Checkbox(
- onChanged: (value) =>
- _showPassword.value = value == true,
+ onChanged: (value) => runInAction(
+ () => _showPassword.value = value == true),
value: _showPassword.value,
),
SizedBox(height: 40),
Text(Strings.showPassword)
],
),
- )
+ ),
+ SizedBox(height: 40),
+ // Show spinning indicator if waiting or api errors,
+ // if any.
+ SizedBox(
+ height: 40,
+ width: kOobeBodyFieldWidth,
+ child: oobe.wait
+ ? Center(child: CircularProgressIndicator())
+ : oobe.authError.isNotEmpty
+ ? Text(
+ oobe.authError,
+ style: TextStyle(color: Colors.red),
+ )
+ : Offstage(),
+ ),
],
),
),
@@ -127,13 +138,14 @@
children: [
// Back button.
OutlinedButton(
- onPressed: oobe.prevScreen,
+ onPressed: oobe.wait ? null : oobe.prevScreen,
child: Text(Strings.back.toUpperCase()),
),
SizedBox(width: 24),
// Set password button.
ElevatedButton(
- onPressed: () => _validate()
+ key: ValueKey('setPassword'),
+ onPressed: () => _validate() && !oobe.wait
? oobe.setPassword(_confirmPasswordController.text)
: null,
child: Text(Strings.setPassword.toUpperCase()),
diff --git a/session_shells/ermine/login/lib/src/widgets/ready.dart b/session_shells/ermine/login/lib/src/widgets/ready.dart
index 4a2134a..7519f1f 100644
--- a/session_shells/ermine/login/lib/src/widgets/ready.dart
+++ b/session_shells/ermine/login/lib/src/widgets/ready.dart
@@ -3,7 +3,6 @@
// found in the LICENSE file.
import 'package:flutter/material.dart';
-import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:internationalization/strings.dart';
import 'package:login/src/states/oobe_state.dart';
@@ -18,55 +17,54 @@
return Padding(
padding: EdgeInsets.all(16),
child: FocusScope(
- child: Observer(builder: (context) {
- return Column(
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- // Title.
- Text(
- Strings.passwordIsSet,
- textAlign: TextAlign.center,
- style: Theme.of(context).textTheme.headline3,
- ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ // Title.
+ Text(
+ Strings.passwordIsSet,
+ textAlign: TextAlign.center,
+ style: Theme.of(context).textTheme.headline3,
+ ),
- // Description.
- Container(
- alignment: Alignment.center,
- padding: EdgeInsets.all(24),
- child: SizedBox(
- width: 600,
- child: Text(
- Strings.readyToUse,
- textAlign: TextAlign.center,
- style: Theme.of(context)
- .textTheme
- .bodyText1!
- .copyWith(height: 1.55),
+ // Description.
+ Container(
+ alignment: Alignment.center,
+ padding: EdgeInsets.all(24),
+ child: SizedBox(
+ width: 600,
+ child: Text(
+ Strings.readyToUse,
+ textAlign: TextAlign.center,
+ style: Theme.of(context)
+ .textTheme
+ .bodyText1!
+ .copyWith(height: 1.55),
+ ),
+ ),
+ ),
+
+ // Empty.
+ Expanded(child: Container()),
+
+ // Start workstation button.
+ Container(
+ alignment: Alignment.center,
+ padding: EdgeInsets.all(24),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ OutlinedButton(
+ key: ValueKey('startWorkstation'),
+ autofocus: true,
+ onPressed: oobe.finish,
+ child: Text(Strings.startWorkstation.toUpperCase()),
),
- ),
+ ],
),
-
- // Empty.
- Expanded(child: Container()),
-
- // Start workstation button.
- Container(
- alignment: Alignment.center,
- padding: EdgeInsets.all(24),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- OutlinedButton(
- autofocus: true,
- onPressed: oobe.finish,
- child: Text(Strings.startWorkstation.toUpperCase()),
- ),
- ],
- ),
- ),
- ],
- );
- }),
+ ),
+ ],
+ ),
),
);
}
diff --git a/session_shells/ermine/login/lib/test_main.dart b/session_shells/ermine/login/lib/test_main.dart
new file mode 100644
index 0000000..c4797e4
--- /dev/null
+++ b/session_shells/ermine/login/lib/test_main.dart
@@ -0,0 +1,14 @@
+// Copyright 2022 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 'package:flutter_driver/driver_extension.dart';
+
+import 'main.dart' as entrypoint;
+
+Future<void> main() async {
+ // Instrument flutter driver handler to invoke shortcut actions.
+ enableFlutterDriverExtension(enableTextEntryEmulation: false);
+
+ return entrypoint.main();
+}
diff --git a/session_shells/ermine/login/meta/login.cml b/session_shells/ermine/login/meta/login.cml
new file mode 100644
index 0000000..274d3e2
--- /dev/null
+++ b/session_shells/ermine/login/meta/login.cml
@@ -0,0 +1,169 @@
+{
+ include: [
+ "//sdk/lib/inspect/client.shard.cml",
+
+ // Enable system logging.
+ "syslog/client.shard.cml",
+ ],
+ program: {
+ args: [ "--expose_dirs=hosted_directories" ],
+ data: "data/login",
+ },
+ children: [
+ {
+ name: "ermine_shell",
+ url: "fuchsia-pkg://fuchsia.com/ermine#meta/ermine.cm",
+ startup: "lazy",
+ },
+ ],
+ capabilities: [
+ {
+ protocol: [ "fuchsia.ui.app.ViewProvider" ],
+ },
+ {
+ directory: "account_data_dir",
+ rights: [ "rw*" ],
+ path: "/hosted_directories/account_data",
+ },
+ {
+ directory: "account_cache_dir",
+ rights: [ "rw*" ],
+ path: "/hosted_directories/account_cache",
+ },
+ {
+ storage: "account",
+ from: "self",
+ backing_dir: "account_data_dir",
+ storage_id: "static_instance_id_or_moniker",
+ },
+ {
+ storage: "account_cache",
+ from: "self",
+ backing_dir: "account_cache_dir",
+ storage_id: "static_instance_id_or_moniker",
+ },
+ ],
+ use: [
+ {
+ protocol: "fuchsia.component.Realm",
+ from: "framework",
+ },
+ {
+ protocol: [
+ "fuchsia.accessibility.semantics.SemanticsManager",
+ "fuchsia.cobalt.LoggerFactory",
+ "fuchsia.fonts.Provider",
+ "fuchsia.hardware.power.statecontrol.Admin",
+ "fuchsia.identity.account.AccountManager",
+ "fuchsia.intl.PropertyProvider",
+ "fuchsia.recovery.FactoryReset",
+ "fuchsia.settings.Intl",
+ "fuchsia.settings.Privacy",
+ "fuchsia.ssh.AuthorizedKeys",
+ "fuchsia.ui.scenic.Scenic",
+ "fuchsia.update.channelcontrol.ChannelControl",
+ ],
+ },
+ {
+ directory: "config-data",
+ from: "parent",
+ rights: [ "r*" ],
+ path: "/config/data",
+ },
+ ],
+ offer: [
+ {
+ protocol: [
+ "fuchsia.accessibility.semantics.SemanticsManager",
+ "fuchsia.buildinfo.Provider",
+ "fuchsia.cobalt.LoggerFactory",
+ "fuchsia.element.Manager",
+ "fuchsia.feedback.CrashReporter",
+ "fuchsia.fonts.Provider",
+ "fuchsia.hardware.power.statecontrol.Admin",
+ "fuchsia.intl.PropertyProvider",
+ "fuchsia.logger.LogSink",
+ "fuchsia.media.Audio",
+ "fuchsia.media.AudioCore",
+ "fuchsia.media.AudioDeviceEnumerator",
+ "fuchsia.media.ProfileProvider",
+ "fuchsia.memory.Monitor",
+ "fuchsia.memorypressure.Provider",
+ "fuchsia.net.interfaces.State",
+ "fuchsia.net.name.Lookup",
+ "fuchsia.posix.socket.Provider",
+ "fuchsia.power.battery.BatteryManager",
+ "fuchsia.process.Launcher",
+ "fuchsia.settings.Intl",
+ "fuchsia.settings.Keyboard",
+ "fuchsia.settings.Privacy",
+ "fuchsia.ssh.AuthorizedKeys",
+ "fuchsia.sys.Launcher",
+ "fuchsia.sysmem.Allocator",
+ "fuchsia.tracing.provider.Registry",
+ "fuchsia.ui.activity.Provider",
+ "fuchsia.ui.activity.Tracker",
+ "fuchsia.ui.brightness.Control",
+ "fuchsia.ui.composition.Allocator",
+ "fuchsia.ui.composition.Flatland",
+ "fuchsia.ui.focus.FocusChainListenerRegistry",
+ "fuchsia.ui.input.ImeService",
+ "fuchsia.ui.input.InputDeviceRegistry",
+ "fuchsia.ui.input.PointerCaptureListenerRegistry",
+ "fuchsia.ui.input3.Keyboard",
+ "fuchsia.ui.scenic.Scenic",
+ "fuchsia.ui.shortcut.Registry",
+ "fuchsia.update.channelcontrol.ChannelControl",
+ "fuchsia.update.Manager",
+ "fuchsia.vulkan.loader.Loader",
+ "fuchsia.wlan.common",
+ "fuchsia.wlan.policy",
+ "fuchsia.wlan.policy.ClientProvider",
+ ],
+ from: "parent",
+ to: [ "#ermine_shell" ],
+ },
+ {
+ directory: "config-data",
+ from: "parent",
+ to: "#ermine_shell",
+ },
+ {
+ directory: "root-ssl-certificates",
+ from: "parent",
+ to: [ "#ermine_shell" ],
+ },
+ {
+ storage: "account",
+ from: "self",
+ to: "#ermine_shell",
+ },
+ {
+ // TODO(fxbug.dev/89628): This cache does not currently have any
+ // process deleting files when it gets full, meaning all clients
+ // need to place constraints on their usage. This is part of a wider
+ // question on cache policy management discussed in fxb/89628.
+ storage: "account_cache",
+ from: "self",
+ to: "#ermine_shell",
+ },
+ {
+ storage: "tmp",
+ from: "parent",
+ to: "#ermine_shell",
+ },
+ ],
+ expose: [
+ {
+ protocol: [ "fuchsia.ui.app.ViewProvider" ],
+ from: "self",
+ },
+ {
+ protocol: [
+ "fuchsia.element.GraphicalPresenter",
+ "fuchsia.element.Manager",
+ ],
+ from: "#ermine_shell",
+ },
+ ],
+}
diff --git a/session_shells/ermine/login/meta/login.cmx b/session_shells/ermine/login/meta/login.cmx
deleted file mode 100644
index d6fdc3c..0000000
--- a/session_shells/ermine/login/meta/login.cmx
+++ /dev/null
@@ -1,32 +0,0 @@
-{
- "program": {
- "data": "data/login"
- },
- "sandbox": {
- "features": [
- "config-data",
- "isolated-persistent-storage"
- ],
- "services": [
- "fuchsia.accessibility.semantics.SemanticsManager",
- "fuchsia.cobalt.LoggerFactory",
- "fuchsia.device.manager.Administrator",
- "fuchsia.element.Manager",
- "fuchsia.fonts.Provider",
- "fuchsia.intl.PropertyProvider",
- "fuchsia.logger.LogSink",
- "fuchsia.settings.Intl",
- "fuchsia.settings.Privacy",
- "fuchsia.ssh.AuthorizedKeys",
- "fuchsia.sys.Environment",
- "fuchsia.sys.Launcher",
- "fuchsia.ui.focus.FocusChainListenerRegistry",
- "fuchsia.ui.input.ImeService",
- "fuchsia.ui.input.InputDeviceRegistry",
- "fuchsia.ui.input3.Keyboard",
- "fuchsia.ui.scenic.Scenic",
- "fuchsia.ui.views.ViewRefInstalled",
- "fuchsia.update.channelcontrol.ChannelControl"
- ]
- }
-}
\ No newline at end of file
diff --git a/session_shells/ermine/memfs/BUILD.gn b/session_shells/ermine/memfs/BUILD.gn
new file mode 100644
index 0000000..1f56eb7
--- /dev/null
+++ b/session_shells/ermine/memfs/BUILD.gn
@@ -0,0 +1,14 @@
+# Copyright 2022 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("//build/components.gni")
+
+fuchsia_component("component") {
+ component_name = "memfs"
+ deps = [ "//src/sys/component_manager/tests/memfs" ]
+ manifest = "meta/memfs.cml"
+}
+
+fuchsia_package("memfs") {
+ deps = [ ":component" ]
+}
diff --git a/session_shells/ermine/memfs/README.md b/session_shells/ermine/memfs/README.md
new file mode 100644
index 0000000..c8800ce
--- /dev/null
+++ b/session_shells/ermine/memfs/README.md
@@ -0,0 +1,7 @@
+## memfs
+
+This component exposes a directory capability, `memfs`, backed by the
+in-memory [MemFS filesystem](https://cs.opensource.google/fuchsia/fuchsia/+/main:src/storage/memfs/README.md).
+
+The implementation lives in [//src/sys/component_manager/tests/memfs](https://cs.opensource.google/fuchsia/fuchsia/+/main:src/sys/component_manager/tests/memfs)
+
diff --git a/session_shells/ermine/memfs/meta/memfs.cml b/session_shells/ermine/memfs/meta/memfs.cml
new file mode 100644
index 0000000..6952ea4
--- /dev/null
+++ b/session_shells/ermine/memfs/meta/memfs.cml
@@ -0,0 +1,23 @@
+{
+ include: [
+ "syslog/client.shard.cml",
+ "syslog/elf_stdio.shard.cml",
+ ],
+ program: {
+ runner: "elf",
+ binary: "bin/memfs",
+ },
+ capabilities: [
+ {
+ directory: "memfs",
+ rights: [ "rw*" ],
+ path: "/svc/fuchsia.io.Directory",
+ },
+ ],
+ expose: [
+ {
+ directory: "memfs",
+ from: "self",
+ },
+ ],
+}
diff --git a/session_shells/ermine/session/BUILD.gn b/session_shells/ermine/session/BUILD.gn
index d4abeab..06b1f5a 100644
--- a/session_shells/ermine/session/BUILD.gn
+++ b/session_shells/ermine/session/BUILD.gn
@@ -3,76 +3,69 @@
# found in the LICENSE file.
import("//build/components.gni")
-import("//build/rust/rustc_binary.gni")
-import("//src/session/build/session_config.gni")
+import("//build/dart/dart_component.gni")
+import("//build/dart/dart_library.gni")
+import("//src/session/build/session_manager.gni")
-group("tests") {
- testonly = true
- deps = [ ":workstation_session_tests" ]
-}
-
-rustc_binary("bin") {
- output_name = "workstation_session"
- with_unit_tests = true
- edition = "2018"
-
+dart_library("lib") {
+ package_name = "workstation_session"
+ source_dir = "lib"
+ null_safe = true
+ sources = [ "main.dart" ]
deps = [
- "//sdk/fidl/fuchsia.component:fuchsia.component-rustc",
- "//sdk/fidl/fuchsia.element:fuchsia.element-rustc",
- "//sdk/fidl/fuchsia.identity.account:fuchsia.identity.account-rustc",
- "//sdk/fidl/fuchsia.input.report:fuchsia.input.report-rustc",
- "//sdk/fidl/fuchsia.io:fuchsia.io-rustc",
- "//sdk/fidl/fuchsia.session.scene:fuchsia.session.scene-rustc",
- "//sdk/fidl/fuchsia.sys:fuchsia.sys-rustc",
- "//sdk/fidl/fuchsia.sys2:fuchsia.sys2-rustc",
- "//sdk/fidl/fuchsia.ui.app:fuchsia.ui.app-rustc",
- "//sdk/fidl/fuchsia.ui.gfx:fuchsia.ui.gfx-rustc",
- "//sdk/fidl/fuchsia.ui.input:fuchsia.ui.input-rustc",
- "//sdk/fidl/fuchsia.ui.scenic:fuchsia.ui.scenic-rustc",
- "//sdk/fidl/fuchsia.ui.shortcut:fuchsia.ui.shortcut-rustc",
- "//sdk/fidl/fuchsia.ui.views:fuchsia.ui.views-rustc",
- "//src/lib/fidl/rust/fidl",
- "//src/lib/fuchsia-async",
- "//src/lib/fuchsia-component",
- "//src/lib/syslog/rust:syslog",
- "//src/lib/ui/fuchsia-scenic",
- "//src/lib/ui/input-synthesis",
- "//src/lib/zircon/rust:fuchsia-zircon",
- "//src/session/lib/element_management",
- "//src/ui/lib/input_pipeline",
- "//third_party/rust_crates:anyhow",
- "//third_party/rust_crates:async-trait",
- "//third_party/rust_crates:futures",
- "//third_party/rust_crates:log",
- "//third_party/rust_crates:matches",
- "//third_party/rust_crates:rand",
- "//third_party/rust_crates:uuid",
+ "//sdk/dart/fidl",
+ "//sdk/dart/fuchsia",
+ "//sdk/dart/fuchsia_logger",
+ "//sdk/dart/fuchsia_services",
+ "//sdk/dart/fuchsia_vfs",
+ "//sdk/dart/zircon",
+ "//sdk/fidl/fuchsia.component",
+ "//sdk/fidl/fuchsia.component.decl",
+ "//sdk/fidl/fuchsia.io",
+ "//sdk/fidl/fuchsia.session.scene",
+ "//sdk/fidl/fuchsia.ui.app",
+ "//sdk/fidl/fuchsia.ui.focus",
+ "//sdk/fidl/fuchsia.ui.input",
+ "//sdk/fidl/fuchsia.ui.keyboard.focus",
+ "//sdk/fidl/fuchsia.ui.shortcut",
+ "//sdk/fidl/fuchsia.ui.views",
]
-
- test_deps = []
-
- sources = [ "src/main.rs" ]
}
-fuchsia_package_with_single_component("workstation_routing") {
- manifest = "meta/workstation_routing.cml"
-}
-
-fuchsia_package_with_single_component("workstation_session") {
+dart_component("session_component") {
+ component_name = "workstation_session"
manifest = "meta/workstation_session.cml"
- deps = [ ":bin" ]
+ deps = [ ":lib" ]
}
-fuchsia_unittest_package("workstation_session_tests") {
- test_specs = {
- log_settings = {
- max_severity = "ERROR"
- }
+fuchsia_component("workstation_routing") {
+ if (!dart_default_build_cfg.is_aot && !dart_default_build_cfg.is_product) {
+ manifest = "meta/workstation_routing_jit.cml"
+ } else if (!dart_default_build_cfg.is_aot &&
+ dart_default_build_cfg.is_product) {
+ manifest = "meta/workstation_routing_jit_product.cml"
+ } else if (dart_default_build_cfg.is_aot &&
+ !dart_default_build_cfg.is_product) {
+ manifest = "meta/workstation_routing_aot.cml"
+ } else if (dart_default_build_cfg.is_aot &&
+ dart_default_build_cfg.is_product) {
+ manifest = "meta/workstation_routing_aot_product.cml"
}
- manifest = "meta/workstation_session_bin_test.cml"
- deps = [ ":bin_test" ]
}
-session_config("session_config") {
- config = "//src/experiences/session_shells/ermine/session/session_config.json"
+fuchsia_package("workstation_session_pkg") {
+ package_name = "workstation_session"
+ deps = [
+ ":session_component",
+ ":workstation_routing",
+ ]
+}
+
+session_manager_package("session_manager") {
+ config =
+ "//src/experiences/session_shells/ermine/session/session_config.json5"
+}
+
+group("session") {
+ public_deps = [ ":workstation_session_pkg" ]
}
diff --git a/session_shells/ermine/session/lib/main.dart b/session_shells/ermine/session/lib/main.dart
new file mode 100644
index 0000000..43a05ec
--- /dev/null
+++ b/session_shells/ermine/session/lib/main.dart
@@ -0,0 +1,97 @@
+// 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 'package:fidl_fuchsia_component/fidl_async.dart';
+import 'package:fidl_fuchsia_component_decl/fidl_async.dart';
+import 'package:fidl_fuchsia_io/fidl_async.dart';
+import 'package:fidl_fuchsia_session_scene/fidl_async.dart';
+import 'package:fidl_fuchsia_ui_app/fidl_async.dart';
+import 'package:fidl_fuchsia_ui_focus/fidl_async.dart';
+import 'package:fidl_fuchsia_ui_keyboard_focus/fidl_async.dart';
+import 'package:fidl_fuchsia_ui_shortcut/fidl_async.dart' as shortcut;
+import 'package:fidl_fuchsia_ui_views/fidl_async.dart';
+import 'package:fuchsia_logger/logger.dart';
+import 'package:fuchsia_services/services.dart';
+import 'package:zircon/zircon.dart';
+
+const kLoginShellName = 'login_shell';
+const kAccountName = 'created_by_session';
+const kAccountPassword = '';
+const kAccountDirectory = 'account_data';
+
+void main(List args) async {
+ setupLogger(name: 'workstation_session');
+ log.info('Setting up workstation session');
+
+ try {
+ // Connect to the Realm.
+ final realm = RealmProxy();
+ Incoming.fromSvcPath().connectToService(realm);
+
+ // Get the login shell's exposed /svc directory.
+ final exposedDir = DirectoryProxy();
+ await realm.openExposedDir(
+ ChildRef(name: kLoginShellName), exposedDir.ctrl.request());
+
+ // Get the login shell's view provider.
+ final viewProvider = ViewProviderProxy();
+ Incoming.withDirectory(exposedDir).connectToService(viewProvider);
+
+ // Set the login shell's view as the root view.
+ final sceneManager = ManagerProxy();
+ Incoming.fromSvcPath().connectToService(sceneManager);
+ final viewRef = await sceneManager.setRootView(viewProvider.ctrl.unbind());
+
+ // Wait for the view to be attached to the scene.
+ final viewRefInstalled = ViewRefInstalledProxy();
+ Incoming.fromSvcPath().connectToService(viewRefInstalled);
+ await viewRefInstalled.watch(viewRef.duplicate());
+
+ // Set focus on the root view.
+ await sceneManager.requestFocus(viewRef.duplicate());
+
+ // Hook up focus chain to IME and shortcut manager.
+ final focusChainRegistry = FocusChainListenerRegistryProxy();
+ Incoming.fromSvcPath().connectToService(focusChainRegistry);
+ await focusChainRegistry
+ .register(FocusChainListenerBinding().wrap(_FocusChainListener()));
+ // ignore: avoid_catches_without_on_clauses
+ } catch (e) {
+ log.severe('Caught exception during workstation session setup: $e');
+ }
+}
+
+/// Listens to focus chain updates and forwards them to [shortcut.Manager] and
+/// IME [Controller].
+class _FocusChainListener extends FocusChainListener {
+ final _ime = ControllerProxy();
+ final _shortcutManager = shortcut.ManagerProxy();
+
+ _FocusChainListener() {
+ Incoming.fromSvcPath().connectToService(_ime);
+ Incoming.fromSvcPath().connectToService(_shortcutManager);
+ }
+
+ @override
+ Future<void> onFocusChange(FocusChain focusChain) async {
+ final chain = focusChain.focusChain;
+ if (chain == null || chain.isEmpty) {
+ return;
+ }
+
+ try {
+ final viewRef = chain.last.duplicate();
+ await _ime.notify(viewRef);
+ await _shortcutManager.handleFocusChange(focusChain);
+ // ignore: avoid_catches_without_on_clauses
+ } catch (e) {
+ log.severe('Caught exception updating focus chain: $e');
+ }
+ }
+}
+
+extension _ViewRefDuplicator on ViewRef {
+ ViewRef duplicate() =>
+ ViewRef(reference: reference.duplicate(ZX.RIGHT_SAME_RIGHTS));
+}
diff --git a/session_shells/ermine/session/meta/workstation_routing.cml b/session_shells/ermine/session/meta/workstation_routing.cml
deleted file mode 100644
index 15d14ed..0000000
--- a/session_shells/ermine/session/meta/workstation_routing.cml
+++ /dev/null
@@ -1,214 +0,0 @@
-{
- include: [ "syslog/client.shard.cml" ],
- children: [
- {
- name: "workstation_session",
- url: "fuchsia-pkg://fuchsia.com/workstation_session#meta/workstation_session.cm",
- startup: "eager",
- },
- {
- name: "scene_manager",
- url: "fuchsia-pkg://fuchsia.com/scene_manager#meta/scene_manager.cm",
- startup: "eager",
- },
- {
- name: "element_manager",
- url: "fuchsia-pkg://fuchsia.com/element_manager#meta/element_manager.cm",
- environment: "#elements_env",
- },
- {
- name: "dart_jit_runner",
- url: "fuchsia-pkg://fuchsia.com/dart_jit_runner#meta/dart_jit_runner.cm",
- startup: "lazy",
- },
- {
- name: "dart_aot_runner",
- url: "fuchsia-pkg://fuchsia.com/dart_aot_runner#meta/dart_aot_runner.cm",
- startup: "lazy",
- },
- {
- name: "flutter_jit_runner",
- url: "fuchsia-pkg://fuchsia.com/flutter_jit_runner#meta/flutter_jit_runner.cm",
- startup: "lazy",
- },
- {
- name: "flutter_aot_runner",
- url: "fuchsia-pkg://fuchsia.com/flutter_aot_runner#meta/flutter_aot_runner.cm",
- startup: "lazy",
- },
- ],
- capabilities: [
- {
- protocol: [
- "fuchsia.element.Manager",
- "fuchsia.input.injection.InputDeviceRegistry",
- "fuchsia.ui.accessibility.view.Registry",
- ],
- },
- ],
- offer: [
- {
- protocol: [
- "fuchsia.logger.LogSink",
- "fuchsia.ui.composition.Flatland",
- "fuchsia.ui.composition.FlatlandDisplay",
- "fuchsia.ui.focus.FocusChainListenerRegistry",
- "fuchsia.ui.input.ImeService",
- "fuchsia.ui.input3.Keyboard",
- "fuchsia.ui.input3.KeyEventInjector",
- "fuchsia.ui.keyboard.focus.Controller",
- "fuchsia.ui.pointerinjector.Registry",
- "fuchsia.ui.scenic.Scenic",
- "fuchsia.ui.shortcut.Manager",
- "fuchsia.ui.views.ViewRefInstalled",
- ],
- from: "parent",
- to: [ "#scene_manager" ],
- },
- {
- directory: "dev-input-report",
- from: "parent",
- to: [ "#scene_manager" ],
- },
- {
- protocol: [
- "fuchsia.deprecatedtimezone.Timezone",
- "fuchsia.feedback.CrashReporter",
- "fuchsia.identity.account.AccountManager",
- "fuchsia.intl.PropertyProvider",
- "fuchsia.logger.LogSink",
- "fuchsia.posix.socket.Provider",
- "fuchsia.sys.Launcher",
- "fuchsia.tracing.provider.Registry",
- "fuchsia.ui.focus.FocusChainListenerRegistry",
- "fuchsia.ui.input.ImeService",
- "fuchsia.ui.keyboard.focus.Controller",
- "fuchsia.ui.shortcut.Manager",
- "fuchsia.ui.views.ViewRefInstalled",
- ],
- from: "parent",
- to: [ "#workstation_session" ],
- },
- {
- protocol: [ "fuchsia.session.scene.Manager" ],
- from: "#scene_manager",
- to: [ "#workstation_session" ],
- },
- {
- protocol: [
- "fuchsia.logger.LogSink",
- "fuchsia.sys.Launcher",
- "fuchsia.ui.scenic.Scenic",
- ],
- from: "parent",
- to: [ "#element_manager" ],
- },
- {
- protocol: [
- "fuchsia.deprecatedtimezone.Timezone",
- "fuchsia.feedback.CrashReporter",
- "fuchsia.intl.PropertyProvider",
- "fuchsia.logger.LogSink",
- "fuchsia.posix.socket.Provider",
- "fuchsia.tracing.provider.Registry",
- ],
- from: "parent",
- to: [
- "#dart_aot_runner",
- "#dart_jit_runner",
- "#flutter_aot_runner",
- "#flutter_jit_runner",
- ],
- },
- {
- protocol: [
- "fuchsia.fonts.Provider",
- "fuchsia.sysmem.Allocator",
- "fuchsia.ui.composition.Flatland",
- "fuchsia.ui.scenic.Scenic",
- "fuchsia.vulkan.loader.Loader",
- ],
- from: "parent",
- to: [
- "#flutter_aot_runner",
- "#flutter_jit_runner",
- ],
- },
- {
- directory: "config-data",
- from: "parent",
- to: [
- "#dart_aot_runner",
- "#dart_jit_runner",
- "#flutter_aot_runner",
- "#flutter_jit_runner",
- ],
- },
-
- // The session manually proxies protocols between #element_manager and the shell.
- // TODO(fxbug.dev/83819) This will be unnecessary once the shell is a V2 component.
- {
- protocol: [ "fuchsia.element.Manager" ],
- from: "#element_manager",
- to: [ "#workstation_session" ],
- },
-
- // The session manually proxies protocols between #element_manager and the shell.
- // TODO(fxbug.dev/83819) This will be unnecessary once the shell is a V2 component.
- {
- protocol: [ "fuchsia.element.GraphicalPresenter" ],
- from: "#workstation_session",
- to: [ "#element_manager" ],
-
- // A circular dependency exists because protocols are routed in both directions between
- // #workstation_session and #element_manager; a weak reference is uses to "break" this
- // cycle. NOTE: this isn't just because the shell is a V1 component; when it becomes a
- // V2 component, the circular dependency will still exist, except it will be between
- // #element_manager and the shell, since the session will no longer need to manually
- // proxy protocols between them.
- dependency: 'weak',
- },
- ],
- expose: [
- {
- protocol: [ "fuchsia.element.Manager" ],
- from: "#element_manager",
- },
- {
- protocol: [
- "fuchsia.input.injection.InputDeviceRegistry",
- "fuchsia.input.keymap.Configuration",
- "fuchsia.ui.accessibility.view.Registry",
- ],
- from: "#scene_manager",
- },
- {
- protocol: "fuchsia.component.Binder",
- from: "framework",
- },
- ],
- environments: [
- {
- name: "elements_env",
- extends: "realm",
- runners: [
- {
- runner: "dart_jit_runner",
- from: "#dart_jit_runner",
- },
- {
- runner: "dart_aot_runner",
- from: "#dart_aot_runner",
- },
- {
- runner: "flutter_jit_runner",
- from: "#flutter_jit_runner",
- },
- {
- runner: "flutter_aot_runner",
- from: "#flutter_aot_runner",
- },
- ],
- },
- ],
-}
diff --git a/session_shells/ermine/session/meta/workstation_routing_aot.cml b/session_shells/ermine/session/meta/workstation_routing_aot.cml
new file mode 100644
index 0000000..95ebe38
--- /dev/null
+++ b/session_shells/ermine/session/meta/workstation_routing_aot.cml
@@ -0,0 +1,73 @@
+{
+ include: [ "//src/experiences/session_shells/ermine/session/meta/workstation_routing_common.shard.cml" ],
+ children: [
+ {
+ name: "dart_aot_runner",
+ url: "fuchsia-pkg://fuchsia.com/dart_aot_runner#meta/dart_aot_runner.cm",
+ startup: "lazy",
+ },
+ {
+ name: "flutter_aot_runner",
+ url: "fuchsia-pkg://fuchsia.com/flutter_aot_runner#meta/flutter_aot_runner.cm",
+ startup: "lazy",
+ },
+ ],
+ offer: [
+ {
+ protocol: [
+ "fuchsia.feedback.CrashReporter",
+ "fuchsia.intl.PropertyProvider",
+ "fuchsia.logger.LogSink",
+ "fuchsia.posix.socket.Provider",
+ "fuchsia.tracing.provider.Registry",
+ ],
+ from: "parent",
+ to: [
+ "#dart_aot_runner",
+ "#flutter_aot_runner",
+ ],
+ },
+ {
+ protocol: [
+ "fuchsia.accessibility.semantics.SemanticsManager",
+ "fuchsia.fonts.Provider",
+ "fuchsia.memorypressure.Provider",
+ "fuchsia.settings.Intl",
+ "fuchsia.sysmem.Allocator",
+ "fuchsia.ui.composition.Allocator",
+ "fuchsia.ui.composition.Flatland",
+ "fuchsia.ui.input.ImeService",
+ "fuchsia.ui.input3.Keyboard",
+ "fuchsia.ui.scenic.Scenic",
+ "fuchsia.vulkan.loader.Loader",
+ ],
+ from: "parent",
+ to: [ "#flutter_aot_runner" ],
+ },
+ {
+ directory: "config-data",
+ from: "parent",
+ to: [
+ "#dart_aot_runner",
+ "#flutter_aot_runner",
+ "#workstation_session",
+ ],
+ },
+ ],
+ environments: [
+ {
+ name: "workstation_session_env",
+ extends: "realm",
+ runners: [
+ {
+ runner: "dart_aot_runner",
+ from: "#dart_aot_runner",
+ },
+ {
+ runner: "flutter_aot_runner",
+ from: "#flutter_aot_runner",
+ },
+ ],
+ },
+ ],
+}
diff --git a/session_shells/ermine/session/meta/workstation_routing_aot_product.cml b/session_shells/ermine/session/meta/workstation_routing_aot_product.cml
new file mode 100644
index 0000000..6b9c059
--- /dev/null
+++ b/session_shells/ermine/session/meta/workstation_routing_aot_product.cml
@@ -0,0 +1,73 @@
+{
+ include: [ "//src/experiences/session_shells/ermine/session/meta/workstation_routing_common.shard.cml" ],
+ children: [
+ {
+ name: "dart_aot_product_runner",
+ url: "fuchsia-pkg://fuchsia.com/dart_aot_product_runner#meta/dart_aot_product_runner.cm",
+ startup: "lazy",
+ },
+ {
+ name: "flutter_aot_product_runner",
+ url: "fuchsia-pkg://fuchsia.com/flutter_aot_product_runner#meta/flutter_aot_product_runner.cm",
+ startup: "lazy",
+ },
+ ],
+ offer: [
+ {
+ protocol: [
+ "fuchsia.feedback.CrashReporter",
+ "fuchsia.intl.PropertyProvider",
+ "fuchsia.logger.LogSink",
+ "fuchsia.posix.socket.Provider",
+ "fuchsia.tracing.provider.Registry",
+ ],
+ from: "parent",
+ to: [
+ "#dart_aot_product_runner",
+ "#flutter_aot_product_runner",
+ ],
+ },
+ {
+ protocol: [
+ "fuchsia.accessibility.semantics.SemanticsManager",
+ "fuchsia.fonts.Provider",
+ "fuchsia.memorypressure.Provider",
+ "fuchsia.settings.Intl",
+ "fuchsia.sysmem.Allocator",
+ "fuchsia.ui.composition.Allocator",
+ "fuchsia.ui.composition.Flatland",
+ "fuchsia.ui.input.ImeService",
+ "fuchsia.ui.input3.Keyboard",
+ "fuchsia.ui.scenic.Scenic",
+ "fuchsia.vulkan.loader.Loader",
+ ],
+ from: "parent",
+ to: [ "#flutter_aot_product_runner" ],
+ },
+ {
+ directory: "config-data",
+ from: "parent",
+ to: [
+ "#dart_aot_product_runner",
+ "#flutter_aot_product_runner",
+ "#workstation_session",
+ ],
+ },
+ ],
+ environments: [
+ {
+ name: "workstation_session_env",
+ extends: "realm",
+ runners: [
+ {
+ runner: "dart_aot_product_runner",
+ from: "#dart_aot_product_runner",
+ },
+ {
+ runner: "flutter_aot_product_runner",
+ from: "#flutter_aot_product_runner",
+ },
+ ],
+ },
+ ],
+}
diff --git a/session_shells/ermine/session/meta/workstation_routing_common.shard.cml b/session_shells/ermine/session/meta/workstation_routing_common.shard.cml
new file mode 100644
index 0000000..0c974a4
--- /dev/null
+++ b/session_shells/ermine/session/meta/workstation_routing_common.shard.cml
@@ -0,0 +1,107 @@
+{
+ include: [ "syslog/client.shard.cml" ],
+ children: [
+ {
+ name: "workstation_session",
+ url: "fuchsia-pkg://fuchsia.com/workstation_session#meta/workstation_session.cm",
+ startup: "eager",
+ environment: "#workstation_session_env",
+ },
+ ],
+ offer: [
+ {
+ protocol: [
+ "fuchsia.accessibility.semantics.SemanticsManager",
+ "fuchsia.buildinfo.Provider",
+ "fuchsia.feedback.CrashReporter",
+ "fuchsia.fonts.Provider",
+ "fuchsia.hardware.power.statecontrol.Admin",
+ "fuchsia.identity.account.AccountManager",
+ "fuchsia.intl.PropertyProvider",
+ "fuchsia.logger.LogSink",
+ "fuchsia.media.AudioCore",
+ "fuchsia.media.AudioDeviceEnumerator",
+ "fuchsia.media.ProfileProvider",
+ "fuchsia.memory.Monitor",
+ "fuchsia.memorypressure.Provider",
+ "fuchsia.net.interfaces.State",
+ "fuchsia.net.name.Lookup",
+ "fuchsia.posix.socket.Provider",
+ "fuchsia.power.battery.BatteryManager",
+ "fuchsia.process.Launcher",
+ "fuchsia.recovery.FactoryReset",
+ "fuchsia.session.scene.Manager",
+ "fuchsia.settings.Intl",
+ "fuchsia.settings.Keyboard",
+ "fuchsia.settings.Privacy",
+ "fuchsia.ssh.AuthorizedKeys",
+ "fuchsia.sys.Launcher",
+ "fuchsia.sysmem.Allocator",
+ "fuchsia.tracing.provider.Registry",
+ "fuchsia.ui.activity.Provider",
+ "fuchsia.ui.activity.Tracker",
+ "fuchsia.ui.brightness.Control",
+ "fuchsia.ui.composition.Allocator",
+ "fuchsia.ui.composition.Flatland",
+ "fuchsia.ui.focus.FocusChainListenerRegistry",
+ "fuchsia.ui.input.ImeService",
+ "fuchsia.ui.input.PointerCaptureListenerRegistry",
+ "fuchsia.ui.input3.Keyboard",
+ "fuchsia.ui.keyboard.focus.Controller",
+ "fuchsia.ui.scenic.Scenic",
+ "fuchsia.ui.shortcut.Manager",
+ "fuchsia.ui.shortcut.Registry",
+ "fuchsia.ui.views.ViewRefInstalled",
+ "fuchsia.update.channelcontrol.ChannelControl",
+ "fuchsia.update.Manager",
+ "fuchsia.vulkan.loader.Loader",
+ "fuchsia.wlan.policy.ClientProvider",
+ ],
+ from: "parent",
+ to: [ "#workstation_session" ],
+ },
+ {
+ // Protocols used by element_manager
+ protocol: [
+ "fuchsia.logger.LogSink",
+ "fuchsia.media.Audio",
+ "fuchsia.sys.Launcher",
+ "fuchsia.sysmem.Allocator",
+ "fuchsia.tracing.provider.Registry",
+ "fuchsia.ui.composition.Allocator",
+ "fuchsia.ui.composition.Flatland",
+ "fuchsia.ui.input3.Keyboard",
+ "fuchsia.ui.scenic.Scenic",
+ ],
+ from: "parent",
+ to: [ "#workstation_session" ],
+ },
+ {
+ directory: "root-ssl-certificates",
+ from: "parent",
+ to: [ "#workstation_session" ],
+ },
+ {
+ storage: [
+ "cache",
+ "data",
+ "tmp",
+ ],
+ from: "parent",
+ to: "#workstation_session",
+ },
+ ],
+ expose: [
+ {
+ protocol: "fuchsia.component.Binder",
+ from: "framework",
+ },
+ {
+ protocol: [
+ "fuchsia.element.GraphicalPresenter",
+ "fuchsia.element.Manager",
+ ],
+ from: "#workstation_session",
+ },
+ ],
+}
diff --git a/session_shells/ermine/session/meta/workstation_routing_jit.cml b/session_shells/ermine/session/meta/workstation_routing_jit.cml
new file mode 100644
index 0000000..5b4dfa6
--- /dev/null
+++ b/session_shells/ermine/session/meta/workstation_routing_jit.cml
@@ -0,0 +1,73 @@
+{
+ include: [ "//src/experiences/session_shells/ermine/session/meta/workstation_routing_common.shard.cml" ],
+ children: [
+ {
+ name: "dart_jit_runner",
+ url: "fuchsia-pkg://fuchsia.com/dart_jit_runner#meta/dart_jit_runner.cm",
+ startup: "lazy",
+ },
+ {
+ name: "flutter_jit_runner",
+ url: "fuchsia-pkg://fuchsia.com/flutter_jit_runner#meta/flutter_jit_runner.cm",
+ startup: "lazy",
+ },
+ ],
+ offer: [
+ {
+ protocol: [
+ "fuchsia.feedback.CrashReporter",
+ "fuchsia.intl.PropertyProvider",
+ "fuchsia.logger.LogSink",
+ "fuchsia.posix.socket.Provider",
+ "fuchsia.tracing.provider.Registry",
+ ],
+ from: "parent",
+ to: [
+ "#dart_jit_runner",
+ "#flutter_jit_runner",
+ ],
+ },
+ {
+ protocol: [
+ "fuchsia.accessibility.semantics.SemanticsManager",
+ "fuchsia.fonts.Provider",
+ "fuchsia.memorypressure.Provider",
+ "fuchsia.settings.Intl",
+ "fuchsia.sysmem.Allocator",
+ "fuchsia.ui.composition.Allocator",
+ "fuchsia.ui.composition.Flatland",
+ "fuchsia.ui.input.ImeService",
+ "fuchsia.ui.input3.Keyboard",
+ "fuchsia.ui.scenic.Scenic",
+ "fuchsia.vulkan.loader.Loader",
+ ],
+ from: "parent",
+ to: [ "#flutter_jit_runner" ],
+ },
+ {
+ directory: "config-data",
+ from: "parent",
+ to: [
+ "#dart_jit_runner",
+ "#flutter_jit_runner",
+ "#workstation_session",
+ ],
+ },
+ ],
+ environments: [
+ {
+ name: "workstation_session_env",
+ extends: "realm",
+ runners: [
+ {
+ runner: "dart_jit_runner",
+ from: "#dart_jit_runner",
+ },
+ {
+ runner: "flutter_jit_runner",
+ from: "#flutter_jit_runner",
+ },
+ ],
+ },
+ ],
+}
diff --git a/session_shells/ermine/session/meta/workstation_routing_jit_product.cml b/session_shells/ermine/session/meta/workstation_routing_jit_product.cml
new file mode 100644
index 0000000..270b075
--- /dev/null
+++ b/session_shells/ermine/session/meta/workstation_routing_jit_product.cml
@@ -0,0 +1,73 @@
+{
+ include: [ "//src/experiences/session_shells/ermine/session/meta/workstation_routing_common.shard.cml" ],
+ children: [
+ {
+ name: "dart_jit_product_runner",
+ url: "fuchsia-pkg://fuchsia.com/dart_jit_product_runner#meta/dart_jit_product_runner.cm",
+ startup: "lazy",
+ },
+ {
+ name: "flutter_jit_product_runner",
+ url: "fuchsia-pkg://fuchsia.com/flutter_jit_product_runner#meta/flutter_jit_product_runner.cm",
+ startup: "lazy",
+ },
+ ],
+ offer: [
+ {
+ protocol: [
+ "fuchsia.feedback.CrashReporter",
+ "fuchsia.intl.PropertyProvider",
+ "fuchsia.logger.LogSink",
+ "fuchsia.posix.socket.Provider",
+ "fuchsia.tracing.provider.Registry",
+ ],
+ from: "parent",
+ to: [
+ "#dart_jit_product_runner",
+ "#flutter_jit_product_runner",
+ ],
+ },
+ {
+ protocol: [
+ "fuchsia.accessibility.semantics.SemanticsManager",
+ "fuchsia.fonts.Provider",
+ "fuchsia.memorypressure.Provider",
+ "fuchsia.settings.Intl",
+ "fuchsia.sysmem.Allocator",
+ "fuchsia.ui.composition.Allocator",
+ "fuchsia.ui.composition.Flatland",
+ "fuchsia.ui.input.ImeService",
+ "fuchsia.ui.input3.Keyboard",
+ "fuchsia.ui.scenic.Scenic",
+ "fuchsia.vulkan.loader.Loader",
+ ],
+ from: "parent",
+ to: [ "#flutter_jit_product_runner" ],
+ },
+ {
+ directory: "config-data",
+ from: "parent",
+ to: [
+ "#dart_jit_product_runner",
+ "#flutter_jit_product_runner",
+ "#workstation_session",
+ ],
+ },
+ ],
+ environments: [
+ {
+ name: "workstation_session_env",
+ extends: "realm",
+ runners: [
+ {
+ runner: "dart_jit_product_runner",
+ from: "#dart_jit_product_runner",
+ },
+ {
+ runner: "flutter_jit_product_runner",
+ from: "#flutter_jit_product_runner",
+ },
+ ],
+ },
+ ],
+}
diff --git a/session_shells/ermine/session/meta/workstation_session.cml b/session_shells/ermine/session/meta/workstation_session.cml
index b47d446..bc4bb3c 100644
--- a/session_shells/ermine/session/meta/workstation_session.cml
+++ b/session_shells/ermine/session/meta/workstation_session.cml
@@ -4,49 +4,121 @@
"syslog/client.shard.cml",
],
program: {
- runner: "elf",
- binary: "bin/workstation_session",
+ data: "data/workstation_session",
},
- capabilities: [
+ children: [
{
- protocol: [ "fuchsia.element.GraphicalPresenter" ],
- },
- {
- directory: "account_dir",
- rights: [ "rw*" ],
- path: "/account_data",
- },
- {
- storage: "account",
- from: "self",
- backing_dir: "account_dir",
- storage_id: "static_instance_id_or_moniker",
+ name: "login_shell",
+ url: "fuchsia-pkg://fuchsia.com/ermine#meta/login.cm",
+ startup: "eager",
},
],
use: [
{
+ protocol: "fuchsia.component.Realm",
+ from: "framework",
+ },
+ {
protocol: [
- "fuchsia.deprecatedtimezone.Timezone",
- "fuchsia.element.Manager",
- "fuchsia.feedback.CrashReporter",
- "fuchsia.identity.account.AccountManager",
- "fuchsia.intl.PropertyProvider",
- "fuchsia.posix.socket.Provider",
"fuchsia.session.scene.Manager",
- "fuchsia.sys.Launcher",
- "fuchsia.tracing.provider.Registry",
"fuchsia.ui.focus.FocusChainListenerRegistry",
- "fuchsia.ui.input.ImeService",
"fuchsia.ui.keyboard.focus.Controller",
"fuchsia.ui.shortcut.Manager",
"fuchsia.ui.views.ViewRefInstalled",
],
},
+ {
+ directory: "config-data",
+ from: "parent",
+ rights: [ "r*" ],
+ path: "/config/data",
+ },
+ ],
+ offer: [
+ {
+ protocol: [
+ "fuchsia.accessibility.semantics.SemanticsManager",
+ "fuchsia.buildinfo.Provider",
+ "fuchsia.element.Manager",
+ "fuchsia.feedback.CrashReporter",
+ "fuchsia.fonts.Provider",
+ "fuchsia.hardware.power.statecontrol.Admin",
+ "fuchsia.identity.account.AccountManager",
+ "fuchsia.intl.PropertyProvider",
+ "fuchsia.logger.LogSink",
+ "fuchsia.media.Audio",
+ "fuchsia.media.AudioCore",
+ "fuchsia.media.AudioDeviceEnumerator",
+ "fuchsia.media.ProfileProvider",
+ "fuchsia.memory.Monitor",
+ "fuchsia.memorypressure.Provider",
+ "fuchsia.net.interfaces.State",
+ "fuchsia.net.name.Lookup",
+ "fuchsia.posix.socket.Provider",
+ "fuchsia.power.battery.BatteryManager",
+ "fuchsia.process.Launcher",
+ "fuchsia.recovery.FactoryReset",
+ "fuchsia.settings.Intl",
+ "fuchsia.settings.Keyboard",
+ "fuchsia.settings.Privacy",
+ "fuchsia.ssh.AuthorizedKeys",
+ "fuchsia.sys.Launcher",
+ "fuchsia.sysmem.Allocator",
+ "fuchsia.tracing.provider.Registry",
+ "fuchsia.ui.activity.Provider",
+ "fuchsia.ui.activity.Tracker",
+ "fuchsia.ui.brightness.Control",
+ "fuchsia.ui.composition.Allocator",
+ "fuchsia.ui.composition.Flatland",
+ "fuchsia.ui.focus.FocusChainListenerRegistry",
+ "fuchsia.ui.input.ImeService",
+ "fuchsia.ui.input.PointerCaptureListenerRegistry",
+ "fuchsia.ui.input3.Keyboard",
+ "fuchsia.ui.keyboard.focus.Controller",
+ "fuchsia.ui.scenic.Scenic",
+ "fuchsia.ui.shortcut.Registry",
+ "fuchsia.ui.views.ViewRefInstalled",
+ "fuchsia.update.channelcontrol.ChannelControl",
+ "fuchsia.update.Manager",
+ "fuchsia.vulkan.loader.Loader",
+ "fuchsia.wlan.policy.ClientProvider",
+ ],
+ from: "parent",
+ to: [ "#login_shell" ],
+ },
+ {
+ directory: "config-data",
+ from: "parent",
+ to: "#login_shell",
+ },
+ {
+ directory: "root-ssl-certificates",
+ from: "parent",
+ to: [ "#login_shell" ],
+ },
+ {
+ storage: [
+ "cache",
+ "tmp",
+ ],
+ from: "parent",
+ to: "#login_shell",
+ },
+
+ // Note: The "data" storage capability used to store
+ // device data is not passed to login_shell, components
+ // inside the session should use the "account" storage
+ // capability intended for storaging account data. The
+ // account storage capability is encrypted using the
+ // account's authentication factors.
],
expose: [
{
- protocol: [ "fuchsia.element.GraphicalPresenter" ],
- from: "self",
+ protocol: [
+ "fuchsia.element.GraphicalPresenter",
+ "fuchsia.element.Manager",
+ ],
+ from: "#login_shell",
},
],
}
diff --git a/session_shells/ermine/session/meta/workstation_session_bin_test.cml b/session_shells/ermine/session/meta/workstation_session_bin_test.cml
deleted file mode 100644
index d738945..0000000
--- a/session_shells/ermine/session/meta/workstation_session_bin_test.cml
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- include: [
- "//sdk/lib/inspect/client.shard.cml",
- "syslog/client.shard.cml",
- ],
- program: {
- runner: "rust_test_runner",
- binary: "bin/workstation_session_bin_test",
- },
- capabilities: [
- { protocol: "fuchsia.test.Suite" },
- ],
- use: [
- {
- protocol: "fuchsia.sys2.Realm",
- from: "framework",
- },
- ],
- expose: [
- {
- protocol: "fuchsia.test.Suite",
- from: "self",
- },
- ],
-}
diff --git a/lib/quickui/pubspec.yaml b/session_shells/ermine/session/pubspec.yaml
similarity index 86%
rename from lib/quickui/pubspec.yaml
rename to session_shells/ermine/session/pubspec.yaml
index dd36530..74d24fc 100644
--- a/lib/quickui/pubspec.yaml
+++ b/session_shells/ermine/session/pubspec.yaml
@@ -2,4 +2,4 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
-name: quickui
\ No newline at end of file
+name: workstation_session
\ No newline at end of file
diff --git a/session_shells/ermine/session/session_config.json b/session_shells/ermine/session/session_config.json
deleted file mode 100644
index bd854ba..0000000
--- a/session_shells/ermine/session/session_config.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "session_url": "fuchsia-pkg://fuchsia.com/workstation_routing#meta/workstation_routing.cm"
-}
diff --git a/session_shells/ermine/session/session_config.json5 b/session_shells/ermine/session/session_config.json5
new file mode 100644
index 0000000..9403d69
--- /dev/null
+++ b/session_shells/ermine/session/session_config.json5
@@ -0,0 +1,3 @@
+{
+ session_url: "fuchsia-pkg://fuchsia.com/workstation_session#meta/workstation_routing.cm"
+}
diff --git a/session_shells/ermine/session/src/main.rs b/session_shells/ermine/session/src/main.rs
deleted file mode 100644
index b8f00ca..0000000
--- a/session_shells/ermine/session/src/main.rs
+++ /dev/null
@@ -1,297 +0,0 @@
-// Copyright 2019 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.
-
-// This declaration is required to support the `select!`.
-#![recursion_limit = "256"]
-
-use {
- anyhow::{anyhow, Context as _, Error},
- fidl::endpoints::{create_endpoints, ClientEnd, DiscoverableProtocolMarker, Proxy},
- fidl_fuchsia_element::{
- GraphicalPresenterMarker, GraphicalPresenterProxy, GraphicalPresenterRequest,
- GraphicalPresenterRequestStream, ManagerMarker as ElementManagerMarker,
- ManagerProxy as ElementManagerProxy, ManagerRequest as ElementManagerRequest,
- ManagerRequestStream as ElementManagerRequestStream,
- },
- fidl_fuchsia_identity_account::{AccountManagerMarker, AccountMetadata, AccountProxy},
- fidl_fuchsia_io::DirectoryProxy,
- fidl_fuchsia_session_scene::ManagerMarker as SceneManagerMarker,
- fidl_fuchsia_sys::LauncherMarker,
- fidl_fuchsia_ui_app::ViewProviderMarker,
- fidl_fuchsia_ui_views as ui_views,
- fidl_fuchsia_ui_views::ViewRefInstalledMarker,
- fuchsia_async as fasync,
- fuchsia_component::{
- client::{connect_to_protocol, launch_with_options, App, LaunchOptions},
- server::ServiceFs,
- },
- fuchsia_zircon as zx,
- futures::{try_join, StreamExt, TryStreamExt},
- log::{error, info, warn},
- std::fs,
- std::rc::Rc,
- std::sync::{Arc, Weak},
-};
-
-enum ExposedServices {
- ElementManager(ElementManagerRequestStream),
- GraphicalPresenter(GraphicalPresenterRequestStream),
-}
-
-/// The maximum number of open requests to this component.
-///
-/// Currently we have this value set low because the only service we are serving
-/// is the ElementManager service and we don't expect many connections to it at
-/// any given time.
-const NUM_CONCURRENT_REQUESTS: usize = 5;
-
-/// A hardcoded password to send on the AccountManager interface.
-const EMPTY_PASSWORD: &str = "";
-
-/// A hardcoded name to set on all accounts we create via AccountManager.
-const ACCOUNT_NAME: &str = "created_by_session";
-
-async fn launch_ermine() -> Result<(App, zx::Channel), Error> {
- let launcher = connect_to_protocol::<LauncherMarker>()?;
-
- let (client_chan, server_chan) = zx::Channel::create().unwrap();
-
- let mut launch_options = LaunchOptions::new();
- launch_options.set_additional_services(
- vec![ElementManagerMarker::PROTOCOL_NAME.to_string()],
- client_chan,
- );
-
- // Check if shell is overridden. Otherwise start ermine's login shell.
- let shell_url = match fs::read_to_string("/config/data/shell") {
- Ok(url) => url,
- Err(_) => "fuchsia-pkg://fuchsia.com/ermine#meta/login.cmx".to_string(),
- };
-
- let app = launch_with_options(&launcher, shell_url, None, launch_options)?;
-
- Ok((app, server_chan))
-}
-
-async fn expose_services(
- graphical_presenter: GraphicalPresenterProxy,
- element_manager: ElementManagerProxy,
- ermine_services_server_end: zx::Channel,
- account_dir: Option<DirectoryProxy>,
-) -> Result<(), Error> {
- let mut fs = ServiceFs::new();
-
- // Add services for component outgoing directory.
- fs.dir("svc").add_fidl_service(ExposedServices::GraphicalPresenter);
- fs.take_and_serve_directory_handle()?;
-
- // Add the account directory if we were able to acquire it.
- if let Some(proxy) = account_dir {
- fs.add_remote("account_data", proxy);
- }
-
- // Add services served to Ermine over `ermine_services_server_end`.
- fs.add_fidl_service_at(ElementManagerMarker::PROTOCOL_NAME, ExposedServices::ElementManager);
- fs.serve_connection(ermine_services_server_end).unwrap();
-
- let graphical_presenter = Rc::new(graphical_presenter);
- let element_manager = Rc::new(element_manager);
-
- fs.for_each_concurrent(NUM_CONCURRENT_REQUESTS, |service_request: ExposedServices| {
- // It's a bit unforunate to clone both of these for each service request, since each service
- // requires only one of the two. However, as long as we have an "async move" block, we must
- // clone the refs before they are moved into it.
- let graphical_presenter = graphical_presenter.clone();
- let element_manager = element_manager.clone();
-
- async move {
- match service_request {
- ExposedServices::ElementManager(request_stream) => {
- run_proxy_element_manager_service(element_manager, request_stream)
- .await
- .unwrap_or_else(|e| error!("Failure in element manager proxy: {}", e));
- }
- ExposedServices::GraphicalPresenter(request_stream) => {
- run_proxy_graphical_presenter_service(graphical_presenter, request_stream)
- .await
- .unwrap_or_else(|e| error!("Failure in graphical presenter proxy: {}", e));
- }
- }
- }
- })
- .await;
-
- Ok(())
-}
-
-async fn run_proxy_element_manager_service(
- element_manager: Rc<ElementManagerProxy>,
- mut request_stream: ElementManagerRequestStream,
-) -> Result<(), Error> {
- while let Some(request) =
- request_stream.try_next().await.context("Failed to obtain next request from stream")?
- {
- match request {
- ElementManagerRequest::ProposeElement { spec, controller, responder } => {
- // TODO(fxbug.dev/47079): handle error
- let mut result = element_manager
- .propose_element(spec, controller)
- .await
- .context("Failed to forward proxied request")?;
- let _ = responder.send(&mut result);
- }
- }
- }
- Ok(())
-}
-
-async fn run_proxy_graphical_presenter_service(
- graphical_presenter: Rc<GraphicalPresenterProxy>,
- mut request_stream: GraphicalPresenterRequestStream,
-) -> Result<(), Error> {
- while let Some(request) =
- request_stream.try_next().await.context("Failed to obtain next request from stream")?
- {
- match request {
- GraphicalPresenterRequest::PresentView {
- view_spec,
- annotation_controller,
- view_controller_request,
- responder,
- } => {
- // TODO(fxbug.dev/47079): handle error
- let mut result = graphical_presenter
- .present_view(view_spec, annotation_controller, view_controller_request)
- .await
- .context("Failed to forward proxied request")?;
- let _ = responder.send(&mut result);
- }
- }
- }
- Ok(())
-}
-
-async fn set_view_focus(
- weak_focuser: Weak<fidl_fuchsia_session_scene::ManagerProxy>,
- mut view_ref: ui_views::ViewRef,
-) -> Result<(), Error> {
- // [ViewRef]'s are one-shot use only. Duplicate it for use in request_focus below.
- let mut viewref_dup = fuchsia_scenic::duplicate_view_ref(&view_ref)?;
-
- // Wait for the view_ref to signal its ready to be focused.
- let view_ref_installed = connect_to_protocol::<ViewRefInstalledMarker>()
- .context("Could not connect to ViewRefInstalledMarker")?;
- let watch_result = view_ref_installed.watch(&mut view_ref).await;
- match watch_result {
- // Handle fidl::Errors.
- Err(e) => Err(anyhow::format_err!("Failed with err: {}", e)),
- // Handle ui_views::ViewRefInstalledError.
- Ok(Err(value)) => Err(anyhow::format_err!("Failed with err: {:?}", value)),
- Ok(_) => {
- // Now set focus on the view_ref.
- if let Some(focuser) = weak_focuser.upgrade() {
- let focus_result = focuser.request_focus(&mut viewref_dup).await?;
- match focus_result {
- Ok(()) => Ok(()),
- Err(e) => Err(anyhow::format_err!("Failed with err: {:?}", e)),
- }
- } else {
- Err(anyhow::format_err!("Failed to acquire Focuser"))
- }
- }
- }
-}
-
-/// Use the AccountManager API (with the supplied password) to either get the only existing account
-/// or create a new account then acquire a data directory for that account.
-async fn get_account_directory(password: &str) -> Result<DirectoryProxy, Error> {
- let account_manager = Arc::new(connect_to_protocol::<AccountManagerMarker>().unwrap());
- info!("Connected to AccountManager");
-
- let account_ids = account_manager.get_account_ids().await?;
- let maybe_account_id = match account_ids.len() {
- 0 => None,
- 1 => Some(account_ids[0]),
- count => {
- return Err(anyhow!("Multiple ({}) accounts found, cannot get data directory", count));
- }
- };
-
- let (account_client_end, account_server_end) = create_endpoints()?;
- let account_metadata =
- AccountMetadata { name: Some(ACCOUNT_NAME.to_string()), ..AccountMetadata::EMPTY };
-
- match maybe_account_id {
- None => {
- info!("Creating a new account through AccountManager");
- account_manager
- .deprecated_provision_new_account(password, account_metadata, account_server_end)
- .await?
- .map_err(|err| anyhow!("Error provisioning new account: {:?}", err))?;
- }
- Some(account_id) => {
- info!("Getting existing account with ID {}", account_id);
- account_manager
- .deprecated_get_account(account_id, password, account_server_end)
- .await?
- .map_err(|err| anyhow!("Error getting account: {:?}", err))?;
- }
- }
-
- let account: AccountProxy = account_client_end.into_proxy()?;
- let (directory_client_end, directory_server_end) = create_endpoints()?;
- info!("Getting directory on account");
- account
- .get_data_directory(directory_server_end)
- .await?
- .map_err(|err| anyhow!("Error getting data directory: {:?}", err))?;
- Ok(directory_client_end.into_proxy()?)
-}
-
-#[fasync::run_singlethreaded]
-async fn main() -> Result<(), Error> {
- fuchsia_syslog::init_with_tags(&["workstation_session"]).expect("Failed to initialize logger.");
-
- let (app, ermine_services_server_end) = launch_ermine().await?;
- let view_provider = app.connect_to_protocol::<ViewProviderMarker>()?;
-
- // Attempt to retrieve a data directory for the account from AccountManager. If this fails
- // just continue without the directory.
- let maybe_account_dir = match get_account_directory(EMPTY_PASSWORD).await {
- Ok(dir) => {
- info!("Successfully acquired an account directory");
- Some(dir)
- }
- Err(err) => {
- warn!("Error getting account directory: {:?}", err);
- None
- }
- };
-
- let scene_manager = Arc::new(connect_to_protocol::<SceneManagerMarker>().unwrap());
-
- let shell_view_provider: ClientEnd<ViewProviderMarker> = view_provider
- .into_channel()
- .expect("no other users of the wrapped channel")
- .into_zx_channel()
- .into();
- let view_ref = scene_manager.set_root_view(shell_view_provider.into()).await.unwrap();
-
- let set_focus_fut = set_view_focus(Arc::downgrade(&scene_manager), view_ref);
- let focus_fut = input_pipeline::focus_listening::handle_focus_changes();
-
- let graphical_presenter = app.connect_to_protocol::<GraphicalPresenterMarker>()?;
- let element_manager = connect_to_protocol::<ElementManagerMarker>()?;
- let services_fut = expose_services(
- graphical_presenter,
- element_manager,
- ermine_services_server_end,
- maybe_account_dir,
- );
-
- //TODO(fxbug.dev/47080) monitor the futures to see if they complete in an error.
- let _ = try_join!(focus_fut, set_focus_fut, services_fut);
-
- Ok(())
-}
diff --git a/session_shells/ermine/settings/BUILD.gn b/session_shells/ermine/settings/BUILD.gn
deleted file mode 100644
index 1ecfdc8..0000000
--- a/session_shells/ermine/settings/BUILD.gn
+++ /dev/null
@@ -1,73 +0,0 @@
-# Copyright 2019 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("//build/dart/dart_library.gni")
-import("//build/flutter/test.gni")
-
-dart_library("settings") {
- package_name = "settings"
- null_safe = true
-
- sources = [
- "settings.dart",
- "src/battery.dart",
- "src/brightness.dart",
- "src/channel.dart",
- "src/datetime.dart",
- "src/memory.dart",
- "src/system_information.dart",
- "src/timezone.dart",
- "src/volume.dart",
- ]
-
- deps = [
- "//sdk/dart/fuchsia_logger",
- "//sdk/dart/fuchsia_services",
- "//sdk/fidl/fuchsia.intl",
- "//sdk/fidl/fuchsia.media",
- "//sdk/fidl/fuchsia.memory",
- "//sdk/fidl/fuchsia.power",
- "//sdk/fidl/fuchsia.session",
- "//sdk/fidl/fuchsia.settings",
- "//sdk/fidl/fuchsia.ui.brightness",
- "//sdk/fidl/fuchsia.ui.remotewidgets",
- "//sdk/fidl/fuchsia.update.channelcontrol",
- "//src/experiences/lib/quickui",
- "//src/experiences/session_shells/ermine/internationalization",
- "//third_party/dart-pkg/git/flutter/packages/flutter",
- "//third_party/dart-pkg/pub/async",
- "//third_party/dart-pkg/pub/intl",
- ]
-}
-
-flutter_test("ermine_settings_unittests") {
- null_safe = true
- sources = [
- "battery_test.dart",
- "brightness_test.dart",
- "channel_test.dart",
- "datetime_test.dart",
- "memory_test.dart",
- "system_information_test.dart",
- "timezone_test.dart",
- "volume_test.dart",
- ]
-
- deps = [
- ":settings",
- "//sdk/dart/fidl",
- "//sdk/fidl/fuchsia.intl",
- "//sdk/fidl/fuchsia.media",
- "//sdk/fidl/fuchsia.memory",
- "//sdk/fidl/fuchsia.power",
- "//sdk/fidl/fuchsia.settings",
- "//sdk/fidl/fuchsia.ui.brightness",
- "//sdk/fidl/fuchsia.ui.remotewidgets",
- "//sdk/fidl/fuchsia.update.channelcontrol",
- "//src/experiences/lib/quickui",
- "//src/experiences/session_shells/ermine/internationalization",
- "//third_party/dart-pkg/git/flutter/packages/flutter_test",
- "//third_party/dart-pkg/pub/mockito",
- ]
-}
diff --git a/session_shells/ermine/settings/lib/settings.dart b/session_shells/ermine/settings/lib/settings.dart
deleted file mode 100644
index 38a7ccf..0000000
--- a/session_shells/ermine/settings/lib/settings.dart
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright 2019 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.
-
-export 'src/battery.dart';
-export 'src/brightness.dart';
-export 'src/channel.dart';
-export 'src/datetime.dart';
-export 'src/memory.dart';
-export 'src/system_information.dart';
-export 'src/timezone.dart';
-export 'src/volume.dart';
diff --git a/session_shells/ermine/settings/lib/src/battery.dart b/session_shells/ermine/settings/lib/src/battery.dart
deleted file mode 100644
index 7defda5..0000000
--- a/session_shells/ermine/settings/lib/src/battery.dart
+++ /dev/null
@@ -1,154 +0,0 @@
-// Copyright 2019 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:async';
-
-import 'package:fidl_fuchsia_power/fidl_async.dart';
-import 'package:fidl_fuchsia_ui_remotewidgets/fidl_async.dart';
-import 'package:flutter/material.dart';
-import 'package:fuchsia_services/services.dart' show Incoming;
-import 'package:internationalization/strings.dart';
-import 'package:quickui/quickui.dart';
-
-// ignore_for_file: prefer_constructors_over_static_methods
-
-/// Defines a [UiSpec] for visualizing battery.
-class Battery extends UiSpec {
- // Localized strings.
- static String get _title => Strings.battery;
-
- late BatteryModel model;
-
- Battery({
- required BatteryManagerProxy monitor,
- BatteryInfoWatcherBinding? binding,
- }) {
- model = BatteryModel(
- monitor: monitor,
- binding: binding,
- onChange: _onChange,
- );
- }
-
- factory Battery.withSvcPath() {
- final batteryManager = BatteryManagerProxy();
- Incoming.fromSvcPath().connectToService(batteryManager);
- return Battery(monitor: batteryManager);
- }
-
- void _onChange() {
- spec = _specForBattery(model.battery, model.charging);
- }
-
- @override
- void update(Value value) async {}
-
- @override
- void dispose() {
- model.dispose();
- }
-
- static Spec _specForBattery(double value, bool charging) {
- if (value.isNaN) {
- // Send nullSpec to hide battery settings.
- return UiSpec.nullSpec;
- }
- final batteryText = '${value.toStringAsFixed(0)}%';
- if (value == 100) {
- return Spec(title: _title, groups: [
- Group(
- title: _title,
- icon: IconValue(codePoint: Icons.battery_full.codePoint),
- values: [Value.withText(TextValue(text: batteryText))],
- ),
- ]);
- } else if (charging) {
- return Spec(title: _title, groups: [
- Group(
- title: _title,
- icon: IconValue(codePoint: Icons.battery_charging_full.codePoint),
- values: [Value.withText(TextValue(text: batteryText))],
- ),
- ]);
- } else if (value <= 10) {
- return Spec(title: _title, groups: [
- Group(
- title: _title,
- icon: IconValue(codePoint: Icons.battery_alert.codePoint),
- values: [Value.withText(TextValue(text: batteryText))],
- ),
- ]);
- } else {
- return Spec(title: _title, groups: [
- Group(
- title: _title,
- icon: IconValue(codePoint: Icons.battery_std.codePoint),
- values: [Value.withText(TextValue(text: batteryText))],
- ),
- ]);
- }
- }
-}
-
-class BatteryModel {
- final VoidCallback onChange;
- final BatteryInfoWatcherBinding _binding;
- final BatteryManagerProxy _monitor;
-
- late double _battery;
- late bool charging;
-
- BatteryModel({
- required this.onChange,
- required BatteryManagerProxy monitor,
- BatteryInfoWatcherBinding? binding,
- }) : _binding = binding ?? BatteryInfoWatcherBinding(),
- _monitor = monitor {
- // Note that watcher will receive callback immediately with
- // current battery info, so no need to make additional calls
- // to get initial state.
- _monitor.watch(_binding.wrap(_BatteryInfoWatcherImpl(this)));
- }
-
- void dispose() {
- _monitor.ctrl.close();
- _binding.close();
- }
-
- double get battery => _battery;
- set battery(double value) {
- _battery = value;
- onChange();
- }
-
- void _updateBattery(BatteryInfo info) {
- // BatteryStatus.ok indicates that the battery is present and
- // in a known state (so we can show battery info).
- // Alternate states include:
- // BatteryStatus.unknown - not yet initialized
- // (waiting for information from the system)
- // BatteryStatus.notAvailable = battery present, but possibly disabled
- // BatteryStatus.notPresent = batteries not included
- if (info.status == BatteryStatus.ok) {
- final chargeStatus = info.chargeStatus;
- charging = chargeStatus == ChargeStatus.charging;
- battery = info.levelPercent!;
- } else if (info.status == BatteryStatus.notPresent) {
- // upon receiving report of status 'notPresent' it is safe to close the
- // connection and stop listening for battery status updates.
- _monitor.ctrl.close();
- _binding.close();
- }
- }
-}
-
-class _BatteryInfoWatcherImpl extends BatteryInfoWatcher {
- final BatteryModel batteryModel;
- _BatteryInfoWatcherImpl(this.batteryModel);
-
- @override
- Future<void> onChangeBatteryInfo(BatteryInfo info) async {
- batteryModel._updateBattery(info);
- }
-}
diff --git a/session_shells/ermine/settings/lib/src/brightness.dart b/session_shells/ermine/settings/lib/src/brightness.dart
deleted file mode 100644
index 4a03668..0000000
--- a/session_shells/ermine/settings/lib/src/brightness.dart
+++ /dev/null
@@ -1,151 +0,0 @@
-// Copyright 2019 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:async';
-
-import 'package:fidl_fuchsia_ui_brightness/fidl_async.dart';
-import 'package:fidl_fuchsia_ui_remotewidgets/fidl_async.dart';
-import 'package:flutter/material.dart';
-import 'package:fuchsia_services/services.dart' show Incoming;
-import 'package:internationalization/strings.dart';
-import 'package:quickui/quickui.dart';
-
-// ignore_for_file: prefer_constructors_over_static_methods
-
-/// Defines a [UiSpec] for controlling screen brightness.
-///
-/// * If the system does not have hardware for brightness control, no [Spec] is
-/// generated.
-/// * If brightness is set to AUTO, show slider followed by AUTO label:
-/// ----------------------
-/// {A} |//////////// | AUTO
-/// ----------------------
-/// * If brighness is MANUAL, show slider followed by AUTO button:
-/// ----------------------
-/// {b} |//////////// | {B} {AUTO}
-/// ----------------------
-/// * {A} - Auto icon, {b} Brightness min icon, {B} Brightness max icon.
-class Brightness extends UiSpec {
- static const progressAction = 1;
- static const autoAction = 2;
-
- // Localized strings.
- static String get _title => Strings.brightness;
- static String get _auto => Strings.auto;
-
- late _BrightnessModel _model;
-
- Brightness(ControlProxy control) {
- _model = _BrightnessModel(control: control, onChange: _onChange);
- }
-
- factory Brightness.withSvcPath() {
- final control = ControlProxy();
- Incoming.fromSvcPath().connectToService(control);
- return Brightness(control);
- }
-
- void _onChange() {
- spec = _model.auto
- ? _specForAutoBrightness(_model.brightness)
- : _specForManualBrightness(_model.brightness);
- }
-
- @override
- void update(Value value) async {
- if (value.$tag == ValueTag.progress &&
- value.progress!.action == progressAction) {
- _model.brightness = value.progress!.value;
- } else if (value.$tag == ValueTag.button &&
- value.button!.action == autoAction) {
- _model.auto = true;
- }
- }
-
- @override
- void dispose() {
- _model.dispose();
- }
-
- static Spec _specForAutoBrightness(double value) {
- return Spec(title: _title, groups: [
- Group(
- title: _title,
- icon: IconValue(codePoint: Icons.brightness_auto.codePoint),
- values: [
- Value.withProgress(
- ProgressValue(value: value, action: progressAction)),
- Value.withText(TextValue(text: _auto)),
- ],
- ),
- ]);
- }
-
- static Spec _specForManualBrightness(double value) {
- return Spec(title: _title, groups: [
- Group(
- title: _title,
- icon: IconValue(codePoint: Icons.brightness_5.codePoint),
- values: [
- Value.withIcon(
- IconValue(codePoint: Icons.brightness_low.codePoint)),
- Value.withProgress(
- ProgressValue(value: value, action: progressAction)),
- Value.withIcon(
- IconValue(codePoint: Icons.brightness_high.codePoint)),
- Value.withButton(ButtonValue(label: _auto, action: autoAction)),
- ]),
- ]);
- }
-}
-
-class _BrightnessModel {
- final ControlProxy control;
- final VoidCallback onChange;
-
- late bool _auto;
- late bool _enabled = false;
- late double _brightness;
- late StreamSubscription _brightnessSubscription;
-
- _BrightnessModel({required this.control, required this.onChange}) {
- control.watchAutoBrightness().then((auto) {
- _enabled = true;
- _auto = auto;
- _listen();
- });
- }
-
- void dispose() {
- control.ctrl.close();
- _brightnessSubscription.cancel();
- }
-
- bool get enabled => _enabled;
-
- bool get auto => _auto;
- set auto(bool value) {
- _auto = value;
- control.setAutoBrightness();
- onChange();
- }
-
- double get brightness => _brightness;
- set brightness(double value) {
- _brightness = value;
- _auto = false;
- control.setManualBrightness(value);
- onChange();
- }
-
- void _listen() {
- _brightnessSubscription =
- control.watchCurrentBrightness().asStream().listen((brightness) {
- _brightnessSubscription.cancel();
- _brightness = brightness;
- onChange();
- _listen();
- });
- }
-}
diff --git a/session_shells/ermine/settings/lib/src/channel.dart b/session_shells/ermine/settings/lib/src/channel.dart
deleted file mode 100644
index 0f86b83..0000000
--- a/session_shells/ermine/settings/lib/src/channel.dart
+++ /dev/null
@@ -1,142 +0,0 @@
-// 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 'package:fidl_fuchsia_ui_remotewidgets/fidl_async.dart';
-import 'package:fidl_fuchsia_update_channelcontrol/fidl_async.dart';
-import 'package:flutter/material.dart';
-import 'package:fuchsia_services/services.dart' show Incoming;
-import 'package:internationalization/strings.dart';
-import 'package:quickui/quickui.dart';
-
-/// Defines a [UiSpec] for displaying channel.
-class Channel extends UiSpec {
- // Localized strings.
- static String get _title => Strings.channel;
-
- // Icon for channel title.
- static IconValue get _icon =>
- IconValue(codePoint: Icons.cloud_outlined.codePoint);
-
- // Action to change channel.
- static int changeAction = QuickAction.details.$value;
-
- late _ChannelModel model;
-
- Channel(ChannelControlProxy control) {
- model = _ChannelModel(control: control, onChange: _onChange);
- }
-
- factory Channel.withSvcPath() {
- final control = ChannelControlProxy();
- Incoming.fromSvcPath().connectToService(control);
- return Channel(control);
- }
-
- void _onChange() async {
- spec = await _specForChannel(model);
- }
-
- @override
- void update(Value value) async {
- if (value.$tag == ValueTag.button &&
- value.button!.action == QuickAction.cancel.$value) {
- spec = await _specForChannel(model);
- } else if (value.$tag == ValueTag.text && value.text!.action > 0) {
- if (value.text!.action == changeAction) {
- spec = await _specForChannel(model, changeAction);
- } else {
- final index = value.text!.action ^ QuickAction.submit.$value;
- model.channel = model.channels[index];
- spec = await _specForChannel(model);
- }
- }
- }
-
- @override
- void dispose() {
- model.dispose();
- }
-
- Future<Spec> _specForChannel(_ChannelModel model, [int action = 0]) async {
- if (action == 0 || action & QuickAction.cancel.$value > 0) {
- return Spec(title: _title, groups: [
- Group(title: _title, icon: _icon, values: [
- Value.withText(TextValue(
- text: model.channel,
- action: changeAction,
- )),
- Value.withIcon(IconValue(
- codePoint: Icons.arrow_right.codePoint,
- action: changeAction,
- )),
- ]),
- ]);
- } else if (action == changeAction) {
- var channels = model.channels;
- final values = List<TextValue>.generate(
- channels.length,
- (index) => TextValue(
- text: channels[index],
- action: QuickAction.submit.$value | index,
- ));
- return Spec(title: _title, groups: [
- Group(title: 'Select Channel', values: [
- Value.withGrid(GridValue(
- columns: 1,
- values: values,
- )),
- Value.withButton(ButtonValue(
- label: 'close',
- action: QuickAction.cancel.$value,
- )),
- ]),
- ]);
- } else {
- return Spec(title: _title, groups: [
- Group(title: _title, values: [
- Value.withText(TextValue(text: 'loading...')),
- ]),
- ]);
- }
- }
-}
-
-class _ChannelModel {
- final ChannelControlProxy control;
- final VoidCallback onChange;
-
- late String _channel;
- late List<String> _channels;
-
- _ChannelModel({required this.control, required this.onChange}) {
- loadCurrentChannel();
- loadTargetChannels();
- }
-
- void dispose() {
- control.ctrl.close();
- }
-
- String get channel => _channel;
- set channel(String name) {
- _channel = name;
- control.setTarget(name);
- onChange();
- }
-
- List<String> get channels => _channels;
-
- void loadCurrentChannel() {
- control.getTarget().then((name) {
- _channel = name;
- onChange();
- });
- }
-
- void loadTargetChannels() {
- control.getTargetList().then((channels) {
- _channels = channels;
- });
- }
-}
diff --git a/session_shells/ermine/settings/lib/src/datetime.dart b/session_shells/ermine/settings/lib/src/datetime.dart
deleted file mode 100644
index 94be277..0000000
--- a/session_shells/ermine/settings/lib/src/datetime.dart
+++ /dev/null
@@ -1,56 +0,0 @@
-// Copyright 2019 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:async';
-
-import 'package:fidl_fuchsia_ui_remotewidgets/fidl_async.dart';
-import 'package:flutter/material.dart';
-import 'package:internationalization/strings.dart';
-import 'package:intl/intl.dart';
-import 'package:quickui/quickui.dart';
-
-// ignore_for_file: prefer_constructors_over_static_methods
-
-/// Defines a [UiSpec] for displaying date and time.
-class Datetime extends UiSpec {
- // Localized strings.
- static String get _title => Strings.dateTime;
-
- // Icon for datetime title.
- static IconValue get _icon =>
- IconValue(codePoint: Icons.access_time.codePoint);
- static const Duration refreshDuration = Duration(seconds: 1);
-
- // Action to change timezone.
- static int changeAction = QuickAction.details.$value;
-
- late Timer _timer;
-
- Datetime() {
- _timer = Timer.periodic(refreshDuration, (_) => _onChange());
- _onChange();
- }
-
- void _onChange() async {
- spec = _specForDateTime();
- }
-
- @override
- void update(Value value) async {}
-
- @override
- void dispose() {
- _timer.cancel();
- }
-
- static Spec _specForDateTime() {
- String dateTime = DateFormat.E().add_yMd().add_jm().format(DateTime.now());
- return Spec(title: _title, groups: [
- Group(
- title: _title,
- icon: _icon,
- values: [Value.withText(TextValue(text: dateTime))]),
- ]);
- }
-}
diff --git a/session_shells/ermine/settings/lib/src/memory.dart b/session_shells/ermine/settings/lib/src/memory.dart
deleted file mode 100644
index a08ef05..0000000
--- a/session_shells/ermine/settings/lib/src/memory.dart
+++ /dev/null
@@ -1,108 +0,0 @@
-// Copyright 2019 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:async';
-import 'dart:math';
-
-import 'package:fidl_fuchsia_memory/fidl_async.dart';
-import 'package:fidl_fuchsia_ui_remotewidgets/fidl_async.dart';
-import 'package:flutter/material.dart';
-import 'package:fuchsia_services/services.dart' show Incoming;
-import 'package:internationalization/strings.dart';
-import 'package:quickui/quickui.dart';
-
-// ignore_for_file: prefer_constructors_over_static_methods
-
-/// Defines a [UiSpec] for visualizing memory.
-class Memory extends UiSpec {
- static String get _memory => Strings.memory;
-
- // Icon for memory title.
- static IconValue get _icon =>
- IconValue(codePoint: Icons.memory_outlined.codePoint);
-
- late MemoryModel model;
-
- Memory({required Monitor monitor, WatcherBinding? binding}) {
- model = MemoryModel(
- monitor: monitor,
- binding: binding,
- onChange: _onChange,
- );
- }
-
- factory Memory.withSvcPath() {
- final monitor = MonitorProxy();
- Incoming.fromSvcPath().connectToService(monitor);
- return Memory(monitor: monitor);
- }
-
- void _onChange() {
- spec = _specForMemory(model.memory, model.memUsed, model.memTotal);
- }
-
- @override
- void update(Value value) async {}
-
- @override
- void dispose() {
- model.dispose();
- }
-
- static Spec _specForMemory(double value, double used, double total) {
- String usedString = (used).toStringAsPrecision(3);
- String totalString = (total).toStringAsPrecision(3);
- return Spec(title: _memory, groups: [
- Group(title: _memory, icon: _icon, values: [
- Value.withText(TextValue(text: '${usedString}GB / ${totalString}GB')),
- ]),
- ]);
- }
-}
-
-class MemoryModel {
- final VoidCallback onChange;
- final WatcherBinding _binding;
- late double memUsed;
- late double memTotal;
- late double _memory;
-
- MemoryModel({
- required this.onChange,
- required Monitor monitor,
- WatcherBinding? binding,
- }) : _binding = binding ?? WatcherBinding() {
- monitor.watch(_binding.wrap(_MonitorWatcherImpl(this)));
- }
-
- void dispose() {
- _binding.close();
- }
-
- double get memory => _memory;
- set memory(double value) {
- _memory = value;
- onChange();
- }
-
- void updateMem(Stats stats) {
- memUsed = _bytesToGB(stats.totalBytes - stats.freeBytes);
- memTotal = _bytesToGB(stats.totalBytes);
- memory = memUsed / memTotal;
- }
-
- double _bytesToGB(int bytes) {
- return (bytes / pow(1024, 3));
- }
-}
-
-class _MonitorWatcherImpl extends Watcher {
- final MemoryModel memoryModel;
- _MonitorWatcherImpl(this.memoryModel);
-
- @override
- Future<void> onChange(Stats stats) async {
- memoryModel.updateMem(stats);
- }
-}
diff --git a/session_shells/ermine/settings/lib/src/system_information.dart b/session_shells/ermine/settings/lib/src/system_information.dart
deleted file mode 100644
index e0fc6e5..0000000
--- a/session_shells/ermine/settings/lib/src/system_information.dart
+++ /dev/null
@@ -1,159 +0,0 @@
-// 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:async';
-
-import 'package:fidl_fuchsia_memory/fidl_async.dart';
-import 'package:fidl_fuchsia_session/fidl_async.dart' as session;
-import 'package:fidl_fuchsia_ui_remotewidgets/fidl_async.dart';
-import 'package:flutter/material.dart';
-import 'package:fuchsia_logger/logger.dart';
-import 'package:fuchsia_services/services.dart' show Incoming;
-import 'package:internationalization/strings.dart';
-import 'package:quickui/quickui.dart';
-
-import 'memory.dart' as memory;
-
-const licenseUrl =
- 'fuchsia-pkg://fuchsia.com/license_settings#meta/license_settings.cmx';
-const feedbackUrl =
- 'fuchsia-pkg://fuchsia.com/feedback_settings#meta/feedback_settings.cmx';
-
-// ignore_for_file: prefer_constructors_over_static_methods
-
-/// Defines a [UiSpec] for displaying system information.
-class SystemInformation extends UiSpec {
- // Localized strings.
- static String get _title => Strings.systemInformation;
- static String get _memory => Strings.memory;
- static String get _view => Strings.view;
- static String get _loading => Strings.loading;
- static String get _feedback => Strings.feedback;
- static String get _openSource => Strings.openSource;
- static String get _license => Strings.license;
-
- // Icon for system information title.
- static IconValue get _icon =>
- IconValue(codePoint: Icons.info_outlined.codePoint);
-
- // Action to launch license.
- static int changeAction = QuickAction.details.$value;
-
- // Memory model and variables
- late memory.MemoryModel memoryModel;
- late String usedMemory;
- late String totalMemory;
-
- SystemInformation({required Monitor monitor, WatcherBinding? binding}) {
- memoryModel = memory.MemoryModel(
- monitor: monitor, binding: binding, onChange: _onChangeMemory);
- _onChange();
- }
-
- factory SystemInformation.withSvcPath() {
- final monitor = MonitorProxy();
- Incoming.fromSvcPath().connectToService(monitor);
- return SystemInformation(monitor: monitor);
- }
-
- void _onChange() async {
- spec = await _specForSystemInformation();
- }
-
- void _onChangeMemory() async {
- usedMemory = (memoryModel.memUsed).toStringAsPrecision(3);
- totalMemory = (memoryModel.memTotal).toStringAsPrecision(3);
- }
-
- @override
- void update(Value value) async {
- if (value.$tag == ValueTag.button &&
- value.button!.action == QuickAction.cancel.$value) {
- spec = await _specForSystemInformation();
- } else if (value.$tag == ValueTag.text && value.text!.action > 0) {
- if (value.text!.action == changeAction) {
- spec = await _specForSystemInformation(changeAction);
- } else {
- final index = value.text!.action ^ QuickAction.submit.$value;
- if (index == 1) {
- await launchUrl(feedbackUrl);
- }
- if (index == 2) {
- await launchUrl(licenseUrl);
- }
- spec = await _specForSystemInformation();
- }
- }
- }
-
- @override
- void dispose() {
- memoryModel.dispose();
- }
-
- Future<void> launchUrl(String url) async {
- final proxy = session.ElementManagerProxy();
- final elementController = session.ElementControllerProxy();
-
- final incoming = Incoming.fromSvcPath()..connectToService(proxy);
-
- final spec = session.ElementSpec(componentUrl: url);
-
- await proxy
- .proposeElement(spec, elementController.ctrl.request())
- .catchError((err) {
- log.shout('$err: Failed to propose element <$url>');
- });
-
- proxy.ctrl.close();
- await incoming.close();
- }
-
- Future<Spec> _specForSystemInformation([int action = 0]) async {
- if (action == 0 || action & QuickAction.cancel.$value > 0) {
- return Spec(title: _title, groups: [
- Group(title: _title, icon: _icon, values: [
- Value.withText(TextValue(
- text: _view,
- action: changeAction,
- )),
- Value.withIcon(IconValue(
- codePoint: Icons.arrow_right.codePoint,
- action: changeAction,
- )),
- ]),
- ]);
- } else if (action == changeAction) {
- return Spec(title: _title, groups: [
- Group(title: '', values: [
- Value.withGrid(GridValue(
- columns: 2,
- values: [
- TextValue(text: '${_memory.toUpperCase()}'),
- TextValue(text: '${usedMemory}GB / ${totalMemory}GB'),
- TextValue(text: '${_feedback.toUpperCase()}'),
- TextValue(
- text: '${_view.toUpperCase()}',
- action: QuickAction.submit.$value | 1,
- ),
- TextValue(
- text: '${_openSource.toUpperCase()} ${_license.toUpperCase()}',
- ),
- TextValue(
- text: '${_view.toUpperCase()}',
- action: QuickAction.submit.$value | 2,
- )
- ],
- )),
- ]),
- ]);
- } else {
- return Spec(title: _title, groups: [
- Group(title: _title, values: [
- Value.withText(TextValue(text: '$_loading...')),
- ]),
- ]);
- }
- }
-}
diff --git a/session_shells/ermine/settings/lib/src/timezone.dart b/session_shells/ermine/settings/lib/src/timezone.dart
deleted file mode 100644
index 32bfabf..0000000
--- a/session_shells/ermine/settings/lib/src/timezone.dart
+++ /dev/null
@@ -1,183 +0,0 @@
-// Copyright 2019 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:io';
-
-import 'package:async/async.dart';
-import 'package:fidl_fuchsia_intl/fidl_async.dart';
-import 'package:fidl_fuchsia_settings/fidl_async.dart';
-import 'package:fidl_fuchsia_ui_remotewidgets/fidl_async.dart';
-import 'package:flutter/material.dart';
-import 'package:fuchsia_services/services.dart' show Incoming;
-import 'package:internationalization/strings.dart';
-import 'package:quickui/quickui.dart';
-
-/// Defines a [UiSpec] for displaying and changing timezone.
-class TimeZone extends UiSpec {
- // Localized strings.
- static String get _title => Strings.timezone;
-
- // Icon for timezone title.
- static IconValue get _icon =>
- IconValue(codePoint: Icons.access_time.codePoint);
-
- // Action to change timezone.
- static int changeAction = QuickAction.details.$value;
-
- late _TimeZoneModel model;
- Future<List<TimeZoneInfo>> Function() timeZonesProvider;
-
- TimeZone({
- required IntlProxy intlSettingsService,
- required this.timeZonesProvider,
- }) {
- model = _TimeZoneModel(
- intlSettingsService: intlSettingsService,
- onChange: _onChange,
- );
- }
-
- factory TimeZone.withSvcPath() {
- final intlSettingsService = IntlProxy();
- Incoming.fromSvcPath().connectToService(intlSettingsService);
-
- final timeZonesLoader = _TimeZonesLoader();
-
- final timezone = TimeZone(
- intlSettingsService: intlSettingsService,
- timeZonesProvider: timeZonesLoader.getList);
- return timezone;
- }
-
- void _onChange() async {
- spec = await _specForTimeZone(model);
- }
-
- @override
- void update(Value value) async {
- if (value.$tag == ValueTag.button &&
- value.button!.action == QuickAction.cancel.$value) {
- spec = await _specForTimeZone(model);
- } else if (value.$tag == ValueTag.text && value.text!.action > 0) {
- if (value.text!.action == changeAction) {
- spec = await _specForTimeZone(model, changeAction);
- } else {
- final index = value.text!.action ^ QuickAction.submit.$value;
- model.timeZoneId = (await timeZonesProvider())[index].zoneId;
- spec = await _specForTimeZone(model);
- }
- }
- }
-
- @override
- void dispose() {
- model.dispose();
- }
-
- Future<Spec> _specForTimeZone(_TimeZoneModel model, [int action = 0]) async {
- if (action == 0 || action & QuickAction.cancel.$value > 0) {
- return Spec(title: _title, groups: [
- Group(title: _title, icon: _icon, values: [
- Value.withText(TextValue(
- text: model.timeZoneId!,
- action: changeAction,
- )),
- Value.withIcon(IconValue(
- codePoint: Icons.arrow_right.codePoint,
- action: changeAction,
- )),
- ]),
- ]);
- } else if (action == changeAction) {
- var timeZones = await timeZonesProvider();
- final values = List<TextValue>.generate(
- timeZones.length,
- (index) => TextValue(
- text: timeZones[index].zoneId,
- action: QuickAction.submit.$value | index,
- ));
- return Spec(title: _title, groups: [
- Group(title: 'Select Timezone', values: [
- Value.withGrid(GridValue(
- columns: 1,
- values: values,
- )),
- Value.withButton(ButtonValue(
- label: 'close',
- action: QuickAction.cancel.$value,
- )),
- ]),
- ]);
- } else {
- return Spec(title: _title, groups: [
- Group(title: _title, values: [
- Value.withText(TextValue(
- text: model.timeZoneId!,
- action: changeAction,
- )),
- ]),
- ]);
- }
- }
-}
-
-class _TimeZoneModel {
- IntlProxy intlSettingsService;
- final VoidCallback onChange;
-
- IntlSettings? _intlSettings;
-
- _TimeZoneModel({required this.intlSettingsService, required this.onChange}) {
- // Get current timezone and watch it for changes.
- intlSettingsService.watch().then(_onIntlSettingsChange);
- }
-
- Future<void> _onIntlSettingsChange(IntlSettings intlSettings) async {
- bool timeZoneChanged = (_intlSettings == null) ||
- (_intlSettings?.timeZoneId?.id != intlSettings.timeZoneId?.id);
- _intlSettings = intlSettings;
- if (timeZoneChanged) {
- onChange();
- }
- // Use the FIDL "hanging get" pattern to request the next update.
- await intlSettingsService.watch().then(_onIntlSettingsChange);
- }
-
- void dispose() {
- intlSettingsService.ctrl.close();
- }
-
- String? get timeZoneId =>
- _intlSettings == null ? null : _intlSettings?.timeZoneId?.id;
- set timeZoneId(String? value) {
- final IntlSettings newIntlSettings = IntlSettings(
- locales: _intlSettings?.locales,
- temperatureUnit: _intlSettings?.temperatureUnit,
- timeZoneId: TimeZoneId(id: value!));
- intlSettingsService.set(newIntlSettings);
- }
-}
-
-// Information needed to render a time zone list entry.
-class TimeZoneInfo {
- /// The ICU standard zone ID.
- final String zoneId;
-
- const TimeZoneInfo({required this.zoneId});
-}
-
-// Loads and caches a list of time zones from the Ermine package.
-class _TimeZonesLoader {
- final _memoizer = AsyncMemoizer<List<TimeZoneInfo>>();
-
- Future<List<TimeZoneInfo>> getList() async => _memoizer.runOnce(_loadList);
-
- Future<List<TimeZoneInfo>> _loadList() async {
- var file = File('/pkg/data/tz_ids.txt');
- List<TimeZoneInfo> timeZones = (await file.readAsLines())
- .map((id) => TimeZoneInfo(zoneId: id))
- .toList();
- return timeZones;
- }
-}
diff --git a/session_shells/ermine/settings/lib/src/volume.dart b/session_shells/ermine/settings/lib/src/volume.dart
deleted file mode 100644
index 07a4189..0000000
--- a/session_shells/ermine/settings/lib/src/volume.dart
+++ /dev/null
@@ -1,135 +0,0 @@
-// Copyright 2019 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:async';
-
-import 'package:fidl_fuchsia_media/fidl_async.dart' as vol;
-import 'package:fidl_fuchsia_ui_remotewidgets/fidl_async.dart';
-import 'package:flutter/material.dart';
-import 'package:fuchsia_services/services.dart' show Incoming;
-import 'package:internationalization/strings.dart';
-import 'package:quickui/quickui.dart';
-
-// ignore_for_file: prefer_constructors_over_static_methods
-
-/// Defines a [UiSpec] for controlling device volume.
-class Volume extends UiSpec {
- static const minVolumeAction = 1;
- static const maxVolumeAction = 2;
- static const changeVolumeAction = 3;
-
- // Localized strings.
- static String get _title => Strings.volume;
- static String get _min => Strings.min;
- static String get _max => Strings.max;
-
- // Icon for volume title.
- static IconValue get _icon =>
- IconValue(codePoint: Icons.volume_up_outlined.codePoint);
-
- late _VolumeModel model;
-
- Volume(vol.AudioCoreProxy control) {
- model = _VolumeModel(control: control, onChange: _onChange);
- }
-
- factory Volume.withSvcPath() {
- final control = vol.AudioCoreProxy();
- Incoming.fromSvcPath().connectToService(control);
- return Volume(control);
- }
-
- void _onChange() {
- spec = _specForVolume(model.volume);
- }
-
- @override
- void update(Value value) async {
- if (value.$tag == ValueTag.button) {
- if (value.button!.action == minVolumeAction) {
- model.volume = 0;
- } else if (value.button!.action == maxVolumeAction) {
- model.volume = 1;
- }
- } else if (value.$tag == ValueTag.progress) {
- model.volume = value.progress!.value;
- }
- }
-
- @override
- void dispose() {
- model.dispose();
- }
-
- static Spec _specForVolume(double value) {
- String roundedVolume = (value * 100).round().toString();
- return Spec(title: _title, groups: [
- Group(title: _title, icon: _icon, values: [
- Value.withText(TextValue(text: roundedVolume)),
- Value.withProgress(
- ProgressValue(value: value, action: changeVolumeAction)),
- Value.withButton(ButtonValue(label: _min, action: minVolumeAction)),
- Value.withButton(ButtonValue(label: _max, action: maxVolumeAction)),
- ]),
- ]);
- }
-}
-
-class _VolumeModel {
- static const double _minLevelGainDb = -45.0;
- static const double _maxLevelGainDb = 0.0;
-
- final vol.AudioCoreProxy control;
- final VoidCallback onChange;
-
- late double _volume;
- late StreamSubscription _volumeSubscription;
-
- _VolumeModel({required this.control, required this.onChange}) {
- _volumeSubscription = control.systemGainMuteChanged.listen((response) {
- volume = gainToLevel(response.gainDb);
- });
- }
-
- void dispose() {
- _volumeSubscription.cancel();
- }
-
- double get volume => _volume;
- set volume(double value) {
- _volume = value;
- control.setSystemGain(levelToGain(value));
- if (_volume == 0) {
- control.setSystemMute(true);
- } else {
- control.setSystemMute(false);
- }
- onChange();
- }
-
- /// Converts a gain in db to an audio 'level' in the range 0.0 to 1.0
- /// inclusive.
- double gainToLevel(double gainDb) {
- if (gainDb <= _minLevelGainDb) {
- return 0.0;
- } else if (gainDb >= _maxLevelGainDb) {
- return 1.0;
- } else {
- //double ratio = gainDb / -_minLevelGainDb;
- return 1.0 - gainDb / _minLevelGainDb;
- }
- }
-
- /// Converts an audio 'level' in the range 0.0 to 1.0 inclusive to a gain in
- /// db.
- double levelToGain(double level) {
- if (level <= 0.0) {
- return _minLevelGainDb;
- } else if (level >= 1.0) {
- return _maxLevelGainDb;
- } else {
- return (1.0 - level) * _minLevelGainDb;
- }
- }
-}
diff --git a/session_shells/ermine/settings/pubspec.yaml b/session_shells/ermine/settings/pubspec.yaml
deleted file mode 100644
index 7c3a8e8..0000000
--- a/session_shells/ermine/settings/pubspec.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-# Copyright 2020 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.
-
-name: settings
\ No newline at end of file
diff --git a/session_shells/ermine/settings/test/battery_test.dart b/session_shells/ermine/settings/test/battery_test.dart
deleted file mode 100644
index 31a57c1..0000000
--- a/session_shells/ermine/settings/test/battery_test.dart
+++ /dev/null
@@ -1,209 +0,0 @@
-// Copyright 2019 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 'package:fidl/fidl.dart';
-import 'package:fidl_fuchsia_power/fidl_async.dart';
-import 'package:fidl_fuchsia_ui_remotewidgets/fidl_async.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:mockito/mockito.dart';
-import 'package:settings/settings.dart';
-
-void main() {
- test('Battery', () async {
- final monitorProxy = MockMonitorProxy();
- final binding = MockBinding();
-
- Battery battery = Battery(monitor: monitorProxy, binding: binding);
-
- final BatteryInfoWatcher watcher =
- verify(binding.wrap(captureAny)).captured.single;
- await watcher.onChangeBatteryInfo(
- _buildStats(5, BatteryStatus.ok, ChargeStatus.charging));
-
- final spec = await battery.getSpec();
-
- TextValue? text = spec.groups?.first.values
- ?.where((v) => v.$tag == ValueTag.text)
- .first
- .text;
- expect(spec.groups?.first.title, isNotNull);
- expect(spec.groups?.first.values?.isEmpty, false);
- expect(text?.text, '5%');
-
- // Confirm battery icon present in title
- expect(spec.groups?.first.icon, isNotNull);
- });
-
- test('Change Battery Level', () async {
- final monitorProxy = MockMonitorProxy();
- final binding = MockBinding();
-
- Battery battery = Battery(monitor: monitorProxy, binding: binding);
-
- final BatteryInfoWatcher watcher =
- verify(binding.wrap(captureAny)).captured.single;
- await watcher.onChangeBatteryInfo(
- _buildStats(5, BatteryStatus.ok, ChargeStatus.charging));
-
- final spec = await battery.getSpec();
-
- TextValue? text = spec.groups?.first.values
- ?.where((v) => v.$tag == ValueTag.text)
- .first
- .text;
- expect(spec.groups?.first.title, isNotNull);
- expect(spec.groups?.first.values?.isEmpty, false);
- expect(text?.text, '5%');
-
- // Change battery level
- await watcher.onChangeBatteryInfo(
- _buildStats(6, BatteryStatus.ok, ChargeStatus.notCharging));
-
- final updatedSpec = await battery.getSpec();
-
- text = updatedSpec.groups?.first.values
- ?.where((v) => v.$tag == ValueTag.text)
- .first
- .text;
- expect(spec.groups?.first.title, isNotNull);
- expect(spec.groups?.first.values?.isEmpty, false);
- expect(text?.text, '6%');
-
- // Confirm battery icon present in title
- expect(spec.groups?.first.icon, isNotNull);
- });
-
- test('Change Charging Status', () async {
- final monitorProxy = MockMonitorProxy();
- final binding = MockBinding();
-
- Battery battery = Battery(monitor: monitorProxy, binding: binding);
-
- final BatteryInfoWatcher watcher =
- verify(binding.wrap(captureAny)).captured.single;
- await watcher.onChangeBatteryInfo(
- _buildStats(50, BatteryStatus.ok, ChargeStatus.notCharging));
-
- Spec spec = await battery.getSpec();
-
- TextValue? text = spec.groups?.first.values
- ?.where((v) => v.$tag == ValueTag.text)
- .first
- .text;
- expect(spec.groups?.first.title, isNotNull);
- expect(spec.groups?.first.values?.isEmpty, false);
- expect(text?.text, '50%');
-
- // Change charging status
- await watcher.onChangeBatteryInfo(
- _buildStats(51, BatteryStatus.ok, ChargeStatus.charging));
-
- spec = await battery.getSpec();
- text = spec.groups?.first.values
- ?.where((v) => v.$tag == ValueTag.text)
- .first
- .text;
- expect(spec.groups?.first.title, isNotNull);
- expect(spec.groups?.first.values?.isEmpty, false);
- expect(text?.text, '51%');
-
- // Confirm battery icon present in title
- expect(spec.groups?.first.icon, isNotNull);
- });
-
- test('Low Battery Warning', () async {
- final monitorProxy = MockMonitorProxy();
- final binding = MockBinding();
-
- Battery battery = Battery(monitor: monitorProxy, binding: binding);
-
- final BatteryInfoWatcher watcher =
- verify(binding.wrap(captureAny)).captured.single;
- await watcher.onChangeBatteryInfo(
- _buildStats(50, BatteryStatus.ok, ChargeStatus.notCharging));
-
- Spec spec = await battery.getSpec();
-
- TextValue? text = spec.groups?.first.values
- ?.where((v) => v.$tag == ValueTag.text)
- .first
- .text;
- expect(spec.groups?.first.title, isNotNull);
- expect(spec.groups?.first.values?.isEmpty, false);
- expect(text?.text, '50%');
-
- // Change charge to <10% (low status)
- await watcher.onChangeBatteryInfo(
- _buildStats(9, BatteryStatus.ok, ChargeStatus.notCharging));
-
- spec = await battery.getSpec();
- text = spec.groups?.first.values
- ?.where((v) => v.$tag == ValueTag.text)
- .first
- .text;
- expect(spec.groups?.first.title, isNotNull);
- expect(spec.groups?.first.values?.isEmpty, false);
- expect(text?.text, '9%');
-
- // Confirm battery icon present in title
- expect(spec.groups?.first.icon, isNotNull);
- });
-
- test('Battery Full', () async {
- final monitorProxy = MockMonitorProxy();
- final binding = MockBinding();
-
- Battery battery = Battery(monitor: monitorProxy, binding: binding);
-
- final BatteryInfoWatcher watcher =
- verify(binding.wrap(captureAny)).captured.single;
- await watcher.onChangeBatteryInfo(
- _buildStats(50, BatteryStatus.ok, ChargeStatus.notCharging));
-
- Spec spec = await battery.getSpec();
-
- TextValue? text = spec.groups?.first.values
- ?.where((v) => v.$tag == ValueTag.text)
- .first
- .text;
- expect(spec.groups?.first.title, isNotNull);
- expect(spec.groups?.first.values?.isEmpty, false);
- expect(text?.text, '50%');
-
- // Change charge to 100% (full)
- await watcher.onChangeBatteryInfo(
- _buildStats(100, BatteryStatus.ok, ChargeStatus.notCharging));
-
- spec = await battery.getSpec();
- text = spec.groups?.first.values
- ?.where((v) => v.$tag == ValueTag.text)
- .first
- .text;
- expect(spec.groups?.first.title, isNotNull);
- expect(spec.groups?.first.values?.isEmpty, false);
- expect(text?.text, '100%');
-
- // Confirm battery icon present in title
- expect(spec.groups?.first.icon, isNotNull);
- });
-}
-
-BatteryInfo _buildStats(
- double power, BatteryStatus status, ChargeStatus charge) {
- // ignore: missing_required_param
- return BatteryInfo(
- levelPercent: power,
- status: status,
- chargeStatus: charge,
- );
-}
-
-// Mock classes.
-class MockMonitorProxy extends Mock implements BatteryManagerProxy {}
-
-class MockBinding extends Mock implements BatteryInfoWatcherBinding {
- @override
- InterfaceHandle<BatteryInfoWatcher> wrap(BatteryInfoWatcher? impl) =>
- super.noSuchMethod(Invocation.method(#wrap, [impl]));
-}
diff --git a/session_shells/ermine/settings/test/brightness_test.dart b/session_shells/ermine/settings/test/brightness_test.dart
deleted file mode 100644
index 6146bd1..0000000
--- a/session_shells/ermine/settings/test/brightness_test.dart
+++ /dev/null
@@ -1,110 +0,0 @@
-// Copyright 2019 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 'package:fidl/fidl.dart';
-import 'package:fidl_fuchsia_ui_brightness/fidl_async.dart';
-import 'package:fidl_fuchsia_ui_remotewidgets/fidl_async.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:mockito/mockito.dart';
-import 'package:settings/settings.dart';
-
-void main() {
- late MockControl control;
- late MockProxyController mockProxy;
-
- setUp(() async {
- control = MockControl();
- mockProxy = MockProxyController();
- when(control.ctrl).thenReturn(mockProxy);
- });
-
- tearDown(() async {
- verify(mockProxy.close()).called(1);
- });
-
- test('Brightness', () async {
- when(control.watchAutoBrightness()).thenAnswer((_) => Future.value(false));
- when(control.watchCurrentBrightness()).thenAnswer((_) => Future.value(0.8));
- final brightness = Brightness(control);
-
- // Should receive brightness spec.
- Spec spec = await brightness.getSpec();
- expect(spec.groups?.first.title, isNotNull);
- expect(spec.groups?.first.values?.isEmpty, false);
-
- ProgressValue? progress = spec.groups?.first.values
- ?.where((v) => v.$tag == ValueTag.progress)
- .first
- .progress;
- expect(progress, isNotNull);
- expect(progress?.value, 0.8);
-
- brightness.dispose();
- });
-
- test('Change Brightness', () async {
- when(control.watchAutoBrightness()).thenAnswer((_) => Future.value(true));
- when(control.watchCurrentBrightness()).thenAnswer((_) => Future.value(0.5));
-
- // Should receive brightness spec.
- final brightness = Brightness(control);
- Spec spec = await brightness.getSpec();
-
- // Now change the brightness.
- brightness.update(Value.withProgress(ProgressValue(
- value: 0.3,
- action: Brightness.progressAction,
- )));
-
- verify(control.setManualBrightness(0.3));
-
- // Should follow immediately by brightness spec.
- spec = await brightness.getSpec();
- expect(spec.groups?.first.title, isNotNull);
- expect(spec.groups?.first.values?.isEmpty, false);
-
- ProgressValue? progress = spec.groups?.first.values
- ?.where((v) => v.$tag == ValueTag.progress)
- .first
- .progress;
- expect(progress, isNotNull);
- expect(progress?.value, 0.3);
-
- brightness.dispose();
- });
-
- test('Set auto Brightness', () async {
- when(control.watchAutoBrightness()).thenAnswer((_) => Future.value(false));
- when(control.watchCurrentBrightness()).thenAnswer((_) => Future.value(0.5));
-
- // Should receive brightness spec.
- final brightness = Brightness(control);
- Spec spec = await brightness.getSpec();
-
- // Now set brightness to auto.
- brightness.update(Value.withButton(ButtonValue(
- label: 'label',
- action: Brightness.autoAction,
- )));
-
- verify(control.setAutoBrightness());
-
- // Should follow immediately by brightness spec.
- spec = await brightness.getSpec();
- expect(spec.groups?.first.title, isNotNull);
- expect(spec.groups?.first.values?.isEmpty, false);
-
- // Should be MISSING a button to set auto brightness.
- final hasButton =
- spec.groups?.first.values?.any((v) => v.$tag == ValueTag.button);
- expect(hasButton, isFalse);
-
- brightness.dispose();
- });
-}
-
-class MockControl extends Mock implements ControlProxy {}
-
-class MockProxyController extends Mock
- implements AsyncProxyController<Control> {}
diff --git a/session_shells/ermine/settings/test/channel_test.dart b/session_shells/ermine/settings/test/channel_test.dart
deleted file mode 100644
index 392ddb1..0000000
--- a/session_shells/ermine/settings/test/channel_test.dart
+++ /dev/null
@@ -1,54 +0,0 @@
-// 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 'package:fidl_fuchsia_ui_remotewidgets/fidl_async.dart';
-import 'package:fidl_fuchsia_update_channelcontrol/fidl_async.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:mockito/mockito.dart';
-import 'package:settings/settings.dart';
-
-const List<String> channels = [
- 'channelA',
- 'channelB',
-];
-
-void main() {
- late MockControl control;
-
- setUp(() async {
- control = MockControl();
- });
-
- test('Default Channel Spec', () async {
- when(control.getTarget()).thenAnswer((_) => Future.value(channels[0]));
- when(control.getTargetList()).thenAnswer((_) => Future.value(channels));
-
- final channel = Channel(control);
- Spec spec = await channel.getSpec();
-
- expect(spec.title, 'Channel');
- expect(spec.groups?.first.values?.first.text?.text, channels[0]);
- });
-
- test('Change Channel', () async {
- when(control.getTarget()).thenAnswer((_) => Future.value(channels[0]));
- when(control.getTargetList()).thenAnswer((_) => Future.value(channels));
-
- final channel = Channel(control);
- Spec specA = await channel.getSpec();
-
- expect(specA.title, 'Channel');
- expect(specA.groups?.first.values?.first.text?.text, channels[0]);
-
- // Change channel
- channel.model.channel = channels[1];
-
- // Wait one event cycle for the change
- await channel.getSpec();
- Spec specB = await channel.getSpec();
- expect(specB.groups?.first.values?.first.text?.text, channels[1]);
- });
-}
-
-class MockControl extends Mock implements ChannelControlProxy {}
diff --git a/session_shells/ermine/settings/test/datetime_test.dart b/session_shells/ermine/settings/test/datetime_test.dart
deleted file mode 100644
index 376ac37..0000000
--- a/session_shells/ermine/settings/test/datetime_test.dart
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright 2019 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 'package:flutter_test/flutter_test.dart';
-import 'package:settings/settings.dart';
-
-void main() {
- test('Datetime', () async {
- final stopWatch = Stopwatch()..start();
- final datetime = Datetime();
- var spec = await datetime.getSpec();
-
- expect(spec.title, isNotNull);
- expect(spec.groups?.first.values?.first.text?.text, isNotNull);
-
- // Make sure the next update is received after [Datetime.refreshDuration].
- spec = await datetime.getSpec();
- stopWatch.stop();
-
- expect(stopWatch.elapsed >= Datetime.refreshDuration, true);
- });
-}
diff --git a/session_shells/ermine/settings/test/memory_test.dart b/session_shells/ermine/settings/test/memory_test.dart
deleted file mode 100644
index 6dcb69b..0000000
--- a/session_shells/ermine/settings/test/memory_test.dart
+++ /dev/null
@@ -1,149 +0,0 @@
-// Copyright 2019 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:math';
-
-import 'package:fidl/fidl.dart';
-import 'package:fidl_fuchsia_memory/fidl_async.dart' as mem;
-import 'package:fidl_fuchsia_ui_remotewidgets/fidl_async.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:mockito/mockito.dart';
-import 'package:settings/settings.dart';
-
-void main() {
- test('Memory', () async {
- final monitorProxy = MockMonitorProxy();
- final binding = MockBinding();
- Memory memory = Memory(monitor: monitorProxy, binding: binding);
-
- final mem.Watcher watcher =
- verify(binding.wrap(captureAny)).captured.single;
- await watcher.onChange(_buildStats(0.5));
-
- // Should receive memory spec
- Spec spec = await memory.getSpec();
- expect(spec.groups?.first.title, isNotNull);
- expect(spec.groups?.first.values?.isEmpty, false);
-
- // Confirm text value is correct
- TextValue? text = spec.groups?.first.values
- ?.where((v) => v.$tag == ValueTag.text)
- .first
- .text;
- expect(text?.text, '0.500GB / 1.00GB');
-
- memory.dispose();
- });
-
- test('Change Memory', () async {
- final monitorProxy = MockMonitorProxy();
- final binding = MockBinding();
- Memory memory = Memory(monitor: monitorProxy, binding: binding);
-
- final mem.Watcher watcher =
- verify(binding.wrap(captureAny)).captured.single;
- await watcher.onChange(_buildStats(0.5));
-
- // Should receive memory spec
- Spec spec = await memory.getSpec();
- expect(spec.groups?.first.title, isNotNull);
- expect(spec.groups?.first.values?.isEmpty, false);
-
- // Confirm text value is correct
- TextValue? text = spec.groups?.first.values
- ?.where((v) => v.$tag == ValueTag.text)
- .first
- .text;
- expect(text?.text, '0.500GB / 1.00GB');
-
- // Update memory usage.
- await watcher.onChange(_buildStats(0.7));
-
- spec = await memory.getSpec();
- expect(spec.groups?.first.title, isNotNull);
- expect(spec.groups?.first.values?.isEmpty, false);
-
- // Confirm text value is correct
- text = spec.groups?.first.values
- ?.where((v) => v.$tag == ValueTag.text)
- .first
- .text;
- expect(text?.text, '0.300GB / 1.00GB');
-
- memory.dispose();
- });
-
- test('Min Memory', () async {
- final monitorProxy = MockMonitorProxy();
- final binding = MockBinding();
- Memory memory = Memory(monitor: monitorProxy, binding: binding);
-
- final mem.Watcher watcher =
- verify(binding.wrap(captureAny)).captured.single;
- await watcher.onChange(_buildStats(1));
-
- // Should receive memory spec
- Spec spec = await memory.getSpec();
- expect(spec.groups?.first.title, isNotNull);
- expect(spec.groups?.first.values?.isEmpty, false);
-
- // Confirm text value is correct
- TextValue? text = spec.groups?.first.values
- ?.where((v) => v.$tag == ValueTag.text)
- .first
- .text;
- expect(text?.text, '0.00GB / 1.00GB');
-
- memory.dispose();
- });
-
- test('Max Memory', () async {
- final monitorProxy = MockMonitorProxy();
- final binding = MockBinding();
- Memory memory = Memory(monitor: monitorProxy, binding: binding);
-
- final mem.Watcher watcher =
- verify(binding.wrap(captureAny)).captured.single;
- await watcher.onChange(_buildStats(0));
-
- // Should receive memory spec
- Spec spec = await memory.getSpec();
- expect(spec.groups?.first.title, isNotNull);
- expect(spec.groups?.first.values?.isEmpty, false);
-
- // Confirm text value is correct
- TextValue? text = spec.groups?.first.values
- ?.where((v) => v.$tag == ValueTag.text)
- .first
- .text;
- expect(text?.text, '1.00GB / 1.00GB');
-
- memory.dispose();
- });
-}
-
-int get gB => pow(1024, 3).toInt();
-
-mem.Stats _buildStats(double bytes) {
- return mem.Stats(
- totalBytes: 1 * gB,
- freeBytes: (bytes * gB).toInt(),
- wiredBytes: 0,
- totalHeapBytes: 0,
- freeHeapBytes: 0,
- vmoBytes: 0,
- mmuOverheadBytes: 0,
- ipcBytes: 0,
- otherBytes: 0,
- );
-}
-
-// Mock classes.
-class MockMonitorProxy extends Mock implements mem.MonitorProxy {}
-
-class MockBinding extends Mock implements mem.WatcherBinding {
- @override
- InterfaceHandle<mem.Watcher> wrap(mem.Watcher? impl) =>
- super.noSuchMethod(Invocation.method(#wrap, [impl]));
-}
diff --git a/session_shells/ermine/settings/test/system_information_test.dart b/session_shells/ermine/settings/test/system_information_test.dart
deleted file mode 100644
index 73a3d05..0000000
--- a/session_shells/ermine/settings/test/system_information_test.dart
+++ /dev/null
@@ -1,34 +0,0 @@
-// 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 'package:fidl/fidl.dart';
-import 'package:fidl_fuchsia_memory/fidl_async.dart' as mem;
-import 'package:flutter_test/flutter_test.dart';
-import 'package:mockito/mockito.dart';
-import 'package:settings/settings.dart';
-
-void main() {
- test('System Information', () async {
- final monitorProxy = MockMonitorProxy();
- final binding = MockBinding();
- final sysInfo = SystemInformation(monitor: monitorProxy, binding: binding);
- var spec = await sysInfo.getSpec();
-
- // Should receive system information spec.
- expect(spec.title, isNotNull);
- expect(spec.groups?.first.values?.first.text?.text, isNotNull);
- expect(spec.groups?.first.values?.first.text?.text, 'View');
-
- sysInfo.dispose();
- });
-}
-
-// Mock classes.
-class MockMonitorProxy extends Mock implements mem.MonitorProxy {}
-
-class MockBinding extends Mock implements mem.WatcherBinding {
- @override
- InterfaceHandle<mem.Watcher> wrap(mem.Watcher? impl) =>
- super.noSuchMethod(Invocation.method(#wrap, [impl]));
-}
diff --git a/session_shells/ermine/settings/test/timezone_test.dart b/session_shells/ermine/settings/test/timezone_test.dart
deleted file mode 100644
index 48d7aed..0000000
--- a/session_shells/ermine/settings/test/timezone_test.dart
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright 2019 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 'package:fidl/fidl.dart';
-import 'package:fidl_fuchsia_intl/fidl_async.dart' as fintl;
-import 'package:fidl_fuchsia_settings/fidl_async.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:mockito/mockito.dart';
-import 'package:settings/settings.dart';
-
-const List<TimeZoneInfo> timeZones = [
- TimeZoneInfo(zoneId: 'Test/A'),
- TimeZoneInfo(zoneId: 'Test/B'),
-];
-
-void main() {
- test('Change Timezone', () async {
- var response = 'tz1';
-
- final intlSettingsProxy = MockIntlProxy();
- final intlSettingsProxyController = MockIntlProxyController();
-
- when(intlSettingsProxy.ctrl).thenReturn(intlSettingsProxyController);
- when(intlSettingsProxy.watch())
- .thenAnswer((_) => Future<IntlSettings>.value(IntlSettings(
- timeZoneId: fintl.TimeZoneId(id: response),
- )));
-
- TimeZone timeZone = TimeZone(
- intlSettingsService: intlSettingsProxy,
- timeZonesProvider: () => Future.value(timeZones));
- final specA = await timeZone.getSpec();
- expect(specA.groups?.first.values?.first.text?.text, response);
-
- response = 'tz2';
- // Wait one event cycle for the change.
- await timeZone.getSpec();
- final specB = await timeZone.getSpec();
- expect(specB.groups?.first.values?.first.text?.text, response);
-
- timeZone.dispose();
- });
-}
-
-// Mock classes.
-class MockIntlProxy extends Mock implements IntlProxy {}
-
-class MockIntlProxyController extends Mock
- implements AsyncProxyController<Intl> {}
diff --git a/session_shells/ermine/settings/test/volume_test.dart b/session_shells/ermine/settings/test/volume_test.dart
deleted file mode 100644
index 4d78a44..0000000
--- a/session_shells/ermine/settings/test/volume_test.dart
+++ /dev/null
@@ -1,231 +0,0 @@
-// Copyright 2019 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:async';
-
-import 'package:fidl_fuchsia_media/fidl_async.dart' as vol;
-import 'package:fidl_fuchsia_ui_remotewidgets/fidl_async.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:mockito/mockito.dart';
-import 'package:settings/settings.dart';
-
-void main() {
- test('Volume', () async {
- final control = MockControl();
- when(control.systemGainMuteChanged)
- .thenAnswer((response) => _buildStats(-9, false));
-
- Volume volume = Volume(control);
-
- // Should receive volume spec.
- Spec spec = await volume.getSpec();
- expect(spec.groups?.first.title, isNotNull);
- expect(spec.groups?.first.values?.isEmpty, false);
-
- // Confirm progress value is correct
- ProgressValue? progress = spec.groups?.first.values
- ?.where((v) => v.$tag == ValueTag.progress)
- .first
- .progress;
- expect(progress, isNotNull);
- expect(progress?.value, 0.8);
-
- // Confirm text displayed is correct
- TextValue? text = spec.groups?.first.values
- ?.where((v) => v.$tag == ValueTag.text)
- .first
- .text;
- expect(text?.text, '80');
-
- // Confirm min & max buttons are present
- Iterable? hasButtons =
- spec.groups?.first.values?.where((v) => v.$tag == ValueTag.button);
- expect(hasButtons?.length, 2);
-
- volume.dispose();
- });
-
- test('Change Volume', () async {
- final control = MockControl();
- when(control.systemGainMuteChanged)
- .thenAnswer((_) => _buildStats(-9, false));
-
- Volume volume = Volume(control);
-
- // Should receive volume spec.
- Spec spec = await volume.getSpec();
- expect(spec.groups?.first.title, isNotNull);
- expect(spec.groups?.first.values?.isEmpty, false);
-
- // Confirm progress value is correct
- ProgressValue? progress = spec.groups?.first.values
- ?.where((v) => v.$tag == ValueTag.progress)
- .first
- .progress;
- expect(progress, isNotNull);
- expect(progress?.value, 0.8);
-
- // Confirm text displayed is correct
- TextValue? text = spec.groups?.first.values
- ?.where((v) => v.$tag == ValueTag.text)
- .first
- .text;
- expect(text?.text, '80');
-
- // Change volume level
- volume.model.volume = .9;
-
- // Should receive volume spec.
- spec = await volume.getSpec();
- expect(spec.groups?.first.title, isNotNull);
- expect(spec.groups?.first.values?.isEmpty, false);
-
- progress = spec.groups?.first.values
- ?.where((v) => v.$tag == ValueTag.progress)
- .first
- .progress;
- expect(progress, isNotNull);
- expect(progress?.value, 0.9);
-
- // Confirm text displayed is correct
- text = spec.groups?.first.values
- ?.where((v) => v.$tag == ValueTag.text)
- .first
- .text;
- expect(text?.text, '90');
-
- // Confirm min & max buttons are present
- Iterable? hasButtons =
- spec.groups?.first.values?.where((v) => v.$tag == ValueTag.button);
- expect(hasButtons?.length, 2);
-
- volume.dispose();
- });
-
- test('Max Volume', () async {
- final control = MockControl();
- when(control.systemGainMuteChanged)
- .thenAnswer((_) => _buildStats(0, false));
-
- Volume volume = Volume(control);
-
- // Should receive volume spec.
- Spec spec = await volume.getSpec();
- expect(spec.groups?.first.title, isNotNull);
- expect(spec.groups?.first.values?.isEmpty, false);
-
- // Confirm progress value is correct
- ProgressValue? progress = spec.groups?.first.values
- ?.where((v) => v.$tag == ValueTag.progress)
- .first
- .progress;
- expect(progress, isNotNull);
- expect(progress?.value, 1);
-
- // Confirm text displayed is correct
- TextValue? text = spec.groups?.first.values
- ?.where((v) => v.$tag == ValueTag.text)
- .first
- .text;
- expect(text?.text, '100');
-
- // Change volume level above max accepted dB
- volume.model.volume = volume.model.gainToLevel(10);
-
- // Should receive volume spec.
- spec = await volume.getSpec();
- expect(spec.groups?.first.title, isNotNull);
- expect(spec.groups?.first.values?.isEmpty, false);
-
- // Confirm volume is still at max
- progress = spec.groups?.first.values
- ?.where((v) => v.$tag == ValueTag.progress)
- .first
- .progress;
- expect(progress, isNotNull);
- expect(progress?.value, 1);
-
- // Confirm text displayed is correct
- text = spec.groups?.first.values
- ?.where((v) => v.$tag == ValueTag.text)
- .first
- .text;
- expect(text?.text, '100');
-
- // Confirm min & max buttons are present
- Iterable? hasButtons =
- spec.groups?.first.values?.where((v) => v.$tag == ValueTag.button);
- expect(hasButtons?.length, 2);
-
- volume.dispose();
- });
-
- test('Min Volume', () async {
- final control = MockControl();
- when(control.systemGainMuteChanged)
- .thenAnswer((_) => _buildStats(-45, false));
-
- Volume volume = Volume(control);
-
- // Should receive volume spec.
- Spec spec = await volume.getSpec();
- expect(spec.groups?.first.title, isNotNull);
- expect(spec.groups?.first.values?.isEmpty, false);
-
- // Confirm progress value is correct
- ProgressValue? progress = spec.groups?.first.values
- ?.where((v) => v.$tag == ValueTag.progress)
- .first
- .progress;
- expect(progress, isNotNull);
- expect(progress?.value, 0);
-
- // Confirm text displayed is correct
- TextValue? text = spec.groups?.first.values
- ?.where((v) => v.$tag == ValueTag.text)
- .first
- .text;
- expect(text?.text, '0');
-
- // Change volume level below min accepted dB
- volume.model.volume = volume.model.gainToLevel(-55);
-
- // Should receive volume spec.
- spec = await volume.getSpec();
- expect(spec.groups?.first.title, isNotNull);
- expect(spec.groups?.first.values?.isEmpty, false);
-
- // Confirm volume is still at min
- progress = spec.groups?.first.values
- ?.where((v) => v.$tag == ValueTag.progress)
- .first
- .progress;
- expect(progress, isNotNull);
- expect(progress?.value, 0);
-
- // Confirm text displayed is correct
- text = spec.groups?.first.values
- ?.where((v) => v.$tag == ValueTag.text)
- .first
- .text;
- expect(text?.text, '0');
-
- // Confirm min & max buttons are present
- Iterable? hasButtons =
- spec.groups?.first.values?.where((v) => v.$tag == ValueTag.button);
- expect(hasButtons?.length, 2);
-
- volume.dispose();
- });
-}
-
-Stream<vol.AudioCore$SystemGainMuteChanged$Response> _buildStats(
- double gainDB, bool muted) {
- return Stream.value(
- vol.AudioCore$SystemGainMuteChanged$Response(gainDB, muted));
-}
-
-class MockControl extends Mock implements vol.AudioCoreProxy {}
-
-class MockReponse extends Mock implements vol.AudioCore {}
diff --git a/session_shells/ermine/shell/BUILD.gn b/session_shells/ermine/shell/BUILD.gn
index f2003d4..cd4bac3 100644
--- a/session_shells/ermine/shell/BUILD.gn
+++ b/session_shells/ermine/shell/BUILD.gn
@@ -16,7 +16,7 @@
ermine_app_entries = "config/app_launch_entries.json"
# Whether or not to launch screensaver.
- ermine_start_screensaver = true
+ ermine_start_screensaver = false
}
dart_library("lib") {
@@ -66,8 +66,7 @@
"src/widgets/app_view.dart",
"src/widgets/dialogs/dialog.dart",
"src/widgets/dialogs/dialogs.dart",
- "src/widgets/dialogs/text_only_dialog.dart",
- "src/widgets/dialogs/textfield_dialog.dart",
+ "src/widgets/dialogs/password_prompt.dart",
"src/widgets/launch_button.dart",
"src/widgets/overlays.dart",
"src/widgets/quick_settings.dart",
@@ -87,6 +86,7 @@
deps = [
"//sdk/dart/fidl",
+ "//sdk/dart/fuchsia",
"//sdk/dart/fuchsia_inspect",
"//sdk/dart/fuchsia_internationalization_flutter",
"//sdk/dart/fuchsia_logger",
@@ -95,17 +95,16 @@
"//sdk/dart/fuchsia_services",
"//sdk/dart/zircon",
"//sdk/fidl/fuchsia.buildinfo",
- "//sdk/fidl/fuchsia.device.manager",
"//sdk/fidl/fuchsia.element",
+ "//sdk/fidl/fuchsia.hardware.power.statecontrol",
"//sdk/fidl/fuchsia.intl",
"//sdk/fidl/fuchsia.media",
"//sdk/fidl/fuchsia.media.audio",
"//sdk/fidl/fuchsia.memory",
"//sdk/fidl/fuchsia.net.interfaces",
- "//sdk/fidl/fuchsia.power",
+ "//sdk/fidl/fuchsia.power.battery",
"//sdk/fidl/fuchsia.settings",
"//sdk/fidl/fuchsia.ssh",
- "//sdk/fidl/fuchsia.sys",
"//sdk/fidl/fuchsia.ui.activity",
"//sdk/fidl/fuchsia.ui.app",
"//sdk/fidl/fuchsia.ui.brightness",
@@ -120,7 +119,6 @@
"//sdk/fidl/fuchsia.wlan.policy",
"//src/experiences/session_shells/ermine/internationalization",
"//src/experiences/session_shells/ermine/keyboard_shortcuts",
- "//src/experiences/session_shells/ermine/settings",
"//src/experiences/session_shells/ermine/utils:ermine_utils",
"//third_party/dart-pkg/git/flutter/packages/flutter",
"//third_party/dart-pkg/git/flutter/packages/flutter_driver",
@@ -143,7 +141,7 @@
main_dart = "main.dart"
}
component_name = "ermine"
- manifest = "meta/ermine.cmx"
+ manifest = "meta/ermine.cml"
deps = [
":app_launch_entries_resource",
":lib",
diff --git a/session_shells/ermine/shell/config/app_launch_entries.json b/session_shells/ermine/shell/config/app_launch_entries.json
index fee33be..66e69d6 100644
--- a/session_shells/ermine/shell/config/app_launch_entries.json
+++ b/session_shells/ermine/shell/config/app_launch_entries.json
@@ -1,8 +1,9 @@
[
{
- "icon": "images/SimpleBrowser-icon-2x.png",
- "title": "Simple Browser",
- "url": "fuchsia-pkg://fuchsia.com/simple-browser#meta/simple-browser.cmx"
+ "element_manager_name": "fuchsia.element.Manager-chrome",
+ "icon": "images/Chromium-icon-2x.png",
+ "title": "Chromium",
+ "url": "fuchsia-pkg://fuchsia.com/chrome#meta/chrome.cm"
},
{
"icon": "images/SpinningSquare-icon-2x.png",
@@ -14,4 +15,4 @@
"title": "Terminal",
"url": "fuchsia-pkg://fuchsia.com/terminal#meta/terminal.cmx"
}
-]
+]
\ No newline at end of file
diff --git a/session_shells/ermine/shell/config/app_launch_entries_with_chromium.json b/session_shells/ermine/shell/config/app_launch_entries_with_chromium.json
deleted file mode 100644
index 55d8f17..0000000
--- a/session_shells/ermine/shell/config/app_launch_entries_with_chromium.json
+++ /dev/null
@@ -1,17 +0,0 @@
-[
- {
- "icon": "images/Chromium-icon-2x.png",
- "title": "Chromium",
- "url": "fuchsia-pkg://fuchsia.com/chrome#meta/chrome_v1.cmx"
- },
- {
- "icon": "images/SpinningSquare-icon-2x.png",
- "title": "Spinning Square",
- "url": "fuchsia-pkg://fuchsia.com/spinning-square-rs#meta/spinning-square-rs.cmx"
- },
- {
- "icon": "images/Terminal-icon-2x.png",
- "title": "Terminal",
- "url": "fuchsia-pkg://fuchsia.com/terminal#meta/terminal.cmx"
- }
-]
diff --git a/session_shells/ermine/shell/config/keyboard_shortcuts.json b/session_shells/ermine/shell/config/keyboard_shortcuts.json
index f0b5648..8096e93 100644
--- a/session_shells/ermine/shell/config/keyboard_shortcuts.json
+++ b/session_shells/ermine/shell/config/keyboard_shortcuts.json
@@ -181,5 +181,15 @@
"exclusive": false,
"localizedDescription": "increaseVolumeKeyboardShortcut"
}
+ ],
+ "logout": [
+ {
+ "char": "q",
+ "chord": "Shift + Ctrl + q",
+ "description": "Logout",
+ "exclusive": true,
+ "modifier": "shift + control",
+ "localizedDescription": "logoutKeyboardShortcut"
+ }
]
}
\ No newline at end of file
diff --git a/session_shells/ermine/shell/lib/src/services/focus_service.dart b/session_shells/ermine/shell/lib/src/services/focus_service.dart
index af7b04e..6a9d728 100644
--- a/session_shells/ermine/shell/lib/src/services/focus_service.dart
+++ b/session_shells/ermine/shell/lib/src/services/focus_service.dart
@@ -22,21 +22,42 @@
late final ValueChanged<ViewHandle> onFocusMoved;
final _focusChainListenerBinding = FocusChainListenerBinding();
+ late final StreamSubscription<bool> _focusSubscription;
// Holds the currently focused child view. Null, if shell has focus.
ViewState? focusedChildView;
+ // Temporary variable to guard against FocusChain overwriting the values
+ // set by _onHostFocusChanged(). Note that Flatland doesn't work with
+ // FocusChain.
+ // TODO(fxbug.dev/93446): Remove this along with FocusChain subscription after
+ // enabling Flatland by default.
+ bool flatlandHasMovedFocusToChild = false;
+
FocusService(ViewRef viewRef) : hostView = ViewHandle(viewRef) {
final registryProxy = FocusChainListenerRegistryProxy();
Incoming.fromSvcPath().connectToService(registryProxy);
registryProxy.register(_focusChainListenerBinding.wrap(this));
registryProxy.ctrl.close();
+ _focusSubscription =
+ FocusState.instance.stream().listen(_onHostFocusChanged);
}
void dispose() {
+ _focusSubscription.cancel();
_focusChainListenerBinding.close(0);
}
+ void _onHostFocusChanged(bool focused) {
+ if (focused) {
+ onFocusMoved(hostView);
+ flatlandHasMovedFocusToChild = false;
+ } else if (focusedChildView != null) {
+ onFocusMoved(focusedChildView!.view);
+ flatlandHasMovedFocusToChild = true;
+ }
+ }
+
void setFocusOnHostView() {
focusedChildView?.cancelSetFocus();
focusedChildView = null;
@@ -84,6 +105,8 @@
final index = chain.lastIndexOf(hostView);
final childView = chain[index + 1];
- onFocusMoved(childView);
+ if (!flatlandHasMovedFocusToChild) {
+ onFocusMoved(childView);
+ }
}
}
diff --git a/session_shells/ermine/shell/lib/src/services/launch_service.dart b/session_shells/ermine/shell/lib/src/services/launch_service.dart
index 5b184c5..8ae1cf4 100644
--- a/session_shells/ermine/shell/lib/src/services/launch_service.dart
+++ b/session_shells/ermine/shell/lib/src/services/launch_service.dart
@@ -18,11 +18,18 @@
class LaunchService {
late final ControllerClosedCallback onControllerClosed;
- Future<ControllerProxy> launch(String title, String url) async {
+ Future<ControllerProxy> launch(String title, String url,
+ {String? alternateServiceName}) async {
final elementController = ControllerProxy();
final proxy = ManagerProxy();
- final incoming = Incoming.fromSvcPath()..connectToService(proxy);
+ final incoming = Incoming.fromSvcPath();
+ if (alternateServiceName != null) {
+ incoming.connectToServiceByNameWithChannel(
+ alternateServiceName, proxy.ctrl.request().passChannel());
+ } else {
+ incoming.connectToService(proxy);
+ }
final id = '${DateTime.now().millisecondsSinceEpoch}';
final annotations = [
diff --git a/session_shells/ermine/shell/lib/src/services/preferences_service.dart b/session_shells/ermine/shell/lib/src/services/preferences_service.dart
index 2958986..3b4e732 100644
--- a/session_shells/ermine/shell/lib/src/services/preferences_service.dart
+++ b/session_shells/ermine/shell/lib/src/services/preferences_service.dart
@@ -6,6 +6,7 @@
import 'dart:io';
import 'package:ermine_utils/ermine_utils.dart';
+import 'package:fuchsia_logger/logger.dart';
import 'package:mobx/mobx.dart';
/// Defines a service that allows reading and storing application data.
@@ -64,12 +65,15 @@
file = File(kPreferencesJson);
if (file.existsSync()) {
result.addAll(parsePreferences(file.readAsStringSync()));
+ log.info('Read settings from previous session');
+ } else {
+ log.info('Failed to read settings from previous session');
}
return result;
}
static void _writePreferences(Map<String, dynamic> data) {
- File(kPreferencesJson).writeAsStringSync(json.encode(data));
+ File(kPreferencesJson).writeAsStringSync(json.encode(data), flush: true);
}
}
diff --git a/session_shells/ermine/shell/lib/src/services/presenter_service.dart b/session_shells/ermine/shell/lib/src/services/presenter_service.dart
index b2d74ac..fb83488 100644
--- a/session_shells/ermine/shell/lib/src/services/presenter_service.dart
+++ b/session_shells/ermine/shell/lib/src/services/presenter_service.dart
@@ -13,6 +13,7 @@
import 'package:fidl/fidl.dart';
import 'package:fidl_fuchsia_element/fidl_async.dart';
import 'package:fidl_fuchsia_ui_views/fidl_async.dart';
+import 'package:fuchsia_logger/logger.dart';
import 'package:fuchsia_scenic_flutter/fuchsia_view.dart';
import 'package:fuchsia_services/services.dart';
import 'package:zircon/zircon.dart';
@@ -21,8 +22,7 @@
typedef ViewDismissedCallback = void Function(ViewState viewState);
typedef ErrorCallback = void Function(String url, String error);
-/// Defines a [GraphicalPresenter] to present and dismiss application views.
-class PresenterService extends GraphicalPresenter {
+class PresenterService {
late ViewPresentedCallback onViewPresented;
late ViewDismissedCallback onViewDismissed;
late VoidCallback onPresenterDisposed;
@@ -30,18 +30,50 @@
PresenterService();
+ final _bindings = <_GraphicalPresenterImpl>{};
+
void advertise(Outgoing outgoing) {
outgoing.addPublicService(bind, GraphicalPresenter.$serviceName);
}
+ void bind(InterfaceRequest<GraphicalPresenter> request) {
+ final graphicalPresenter = _GraphicalPresenterImpl()
+ ..onPresenterDisposed = onPresenterDisposed
+ ..onViewPresented = onViewPresented
+ ..onViewDismissed = onViewDismissed
+ ..onError = onError
+ ..bind(request);
+ _bindings.add(graphicalPresenter);
+ }
+
+ void dispose() {
+ for (var binding in _bindings) {
+ binding.dispose();
+ }
+ }
+}
+
+/// Defines a [GraphicalPresenter] to present and dismiss application views.
+class _GraphicalPresenterImpl extends GraphicalPresenter {
+ late ViewPresentedCallback onViewPresented;
+ late ViewDismissedCallback onViewDismissed;
+ late VoidCallback onPresenterDisposed;
+ late ErrorCallback onError;
+
+ _GraphicalPresenterImpl();
+
@override
Future<void> presentView(
ViewSpec viewSpec,
InterfaceHandle<AnnotationController>? annotationController,
InterfaceRequest<ViewController>? viewControllerRequest) async {
late ViewStateImpl viewState;
- final viewController = _ViewControllerImpl(() => onViewDismissed(viewState))
- ..bind(viewControllerRequest);
+
+ _ViewControllerImpl? viewController;
+ if (viewControllerRequest != null) {
+ viewController = _ViewControllerImpl(() => onViewDismissed(viewState))
+ ..bind(viewControllerRequest);
+ }
// Check to see if we have an id that we included in the annotation.
final id = _getAnnotation(viewSpec.annotations, 'id') ??
@@ -61,7 +93,7 @@
final viewRef = viewSpec.viewRef;
if (!_validateViewSpec(viewSpec, url)) {
- viewController.close();
+ viewController?.close();
throw MethodException(PresentViewError.invalidArgs);
}
@@ -95,7 +127,7 @@
id: id,
title: title!,
url: url,
- onClose: viewController.close,
+ onClose: viewController?.close ?? () {},
);
onViewPresented(viewState);
}
@@ -137,6 +169,7 @@
if ((viewSpec.viewHolderToken == null &&
viewSpec.viewportCreationToken == null) ||
viewSpec.viewRef == null) {
+ log.warning('ViewSpec has null ViewportCreationToken, ViewHolderToken or ViewRef');
if (url != null) {
onError(url,
'ViewSpec has null ViewportCreationToken, ViewHolderToken or ViewRef');
@@ -145,7 +178,8 @@
}
if (viewSpec.viewportCreationToken != null &&
- viewSpec.viewHolderToken != null) {
+ viewSpec.viewHolderToken != null) {
+ log.warning('ViewSpec has both ViewportCreationToken and ViewHolderToken set');
if (url != null) {
onError(url,
'ViewSpec has both ViewportCreationToken and ViewHolderToken set');
diff --git a/session_shells/ermine/shell/lib/src/services/settings/battery_watcher_service.dart b/session_shells/ermine/shell/lib/src/services/settings/battery_watcher_service.dart
index 537a790..0a7e98b 100644
--- a/session_shells/ermine/shell/lib/src/services/settings/battery_watcher_service.dart
+++ b/session_shells/ermine/shell/lib/src/services/settings/battery_watcher_service.dart
@@ -3,7 +3,7 @@
// found in the LICENSE file.
import 'package:ermine/src/services/settings/task_service.dart';
-import 'package:fidl_fuchsia_power/fidl_async.dart';
+import 'package:fidl_fuchsia_power_battery/fidl_async.dart';
import 'package:flutter/material.dart';
import 'package:fuchsia_services/services.dart';
diff --git a/session_shells/ermine/shell/lib/src/services/settings/volume_service.dart b/session_shells/ermine/shell/lib/src/services/settings/volume_service.dart
index 666518f..b9b17e8 100644
--- a/session_shells/ermine/shell/lib/src/services/settings/volume_service.dart
+++ b/session_shells/ermine/shell/lib/src/services/settings/volume_service.dart
@@ -55,6 +55,11 @@
volume = (volume - 0.1).clamp(0, 1);
}
+ // Toggles mute for volume on/off.
+ void toggleMute() {
+ muted = !muted;
+ }
+
IconData get icon => _muted ? Icons.volume_off : Icons.volume_up;
bool get muted => _muted;
diff --git a/session_shells/ermine/shell/lib/src/services/settings/wifi_service.dart b/session_shells/ermine/shell/lib/src/services/settings/wifi_service.dart
index 50606df..c7426c0 100644
--- a/session_shells/ermine/shell/lib/src/services/settings/wifi_service.dart
+++ b/session_shells/ermine/shell/lib/src/services/settings/wifi_service.dart
@@ -28,12 +28,16 @@
StreamSubscription? _connectToWPA2NetworkSubscription;
StreamSubscription? _savedNetworksSubscription;
StreamSubscription? _removeNetworkSubscription;
+ StreamSubscription? _startClientConnectionsSubscription;
+ StreamSubscription? _stopClientConnectionsSubscription;
Timer? _timer;
int scanIntervalInSeconds = 20;
final _scannedNetworks = <policy.ScanResult>{};
- String _targetNetwork = '';
+ NetworkInformation _targetNetwork = NetworkInformation();
final _savedNetworks = <policy.NetworkConfig>{};
+ bool _clientConnectionsEnabled = false;
+ final _networksWithFailedCredentials = <policy.NetworkConfig>{};
WiFiService();
@@ -41,7 +45,8 @@
Future<void> start() async {
_clientProvider = policy.ClientProviderProxy();
_clientController = policy.ClientControllerProxy();
- _monitor = ClientStateUpdatesMonitor(onChanged);
+ _monitor = ClientStateUpdatesMonitor(
+ onChanged, _pollNetworksWithFailedCredentials);
Incoming.fromSvcPath().connectToService(_clientProvider);
@@ -49,11 +54,7 @@
InterfaceRequest(_clientController?.ctrl.request().passChannel()),
_monitor.getInterfaceHandle());
- final requestStatus = await _clientController?.startClientConnections();
- if (requestStatus != RequestStatus.acknowledged) {
- log.warning(
- 'Failed to start wlan client connection. Request status: $requestStatus');
- }
+ clientConnectionsEnabled = true;
await getSavedNetworks();
@@ -68,6 +69,8 @@
await _connectToWPA2NetworkSubscription?.cancel();
await _savedNetworksSubscription?.cancel();
await _removeNetworkSubscription?.cancel();
+ await _startClientConnectionsSubscription?.cancel();
+ await _stopClientConnectionsSubscription?.cancel();
dispose();
}
@@ -81,29 +84,71 @@
_scanResultIteratorProvider = policy.ScanResultIteratorProxy();
}
- String get targetNetwork => _targetNetwork;
- set targetNetwork(String network) {
+ NetworkInformation get targetNetwork => _targetNetwork;
+ set targetNetwork(NetworkInformation network) {
_targetNetwork = network;
onChanged();
}
+ bool get clientConnectionsEnabled => _clientConnectionsEnabled;
+ set clientConnectionsEnabled(bool enabled) {
+ _clientConnectionsEnabled = enabled;
+ if (enabled) {
+ _startClientConnections();
+ } else {
+ _stopClientConnections();
+ }
+ onChanged();
+ }
+
+ Future<void> _startClientConnections() async {
+ if (_stopClientConnectionsSubscription != null) {
+ await _stopClientConnectionsSubscription!.cancel();
+ }
+ _startClientConnectionsSubscription = () async {
+ final requestStatus = await _clientController?.startClientConnections();
+ if (requestStatus != RequestStatus.acknowledged) {
+ log.warning(
+ 'Failed to start wlan client connection. Request status: $requestStatus');
+ }
+ }()
+ .asStream()
+ .listen((_) {});
+ }
+
+ Future<void> _stopClientConnections() async {
+ if (_startClientConnectionsSubscription != null) {
+ await _startClientConnectionsSubscription!.cancel();
+ }
+ _stopClientConnectionsSubscription = () async {
+ final requestStatus = await _clientController?.stopClientConnections();
+ if (requestStatus != RequestStatus.acknowledged) {
+ log.warning(
+ 'Failed to stop wlan client connection. Request status: $requestStatus');
+ }
+ }()
+ .asStream()
+ .listen((_) {});
+ }
+
Future<void> scanForNetworks() async {
_scanForNetworksSubscription = () async {
_scanResultIteratorProvider = policy.ScanResultIteratorProxy();
await _clientController?.scanForNetworks(InterfaceRequest(
_scanResultIteratorProvider?.ctrl.request().passChannel()));
- _scannedNetworks.clear();
List<policy.ScanResult>? scanResults;
try {
scanResults = await _scanResultIteratorProvider?.getNext();
+ _scannedNetworks.clear();
while (scanResults != null && scanResults.isNotEmpty) {
_scannedNetworks.addAll(scanResults);
scanResults = await _scanResultIteratorProvider?.getNext();
}
} on Exception catch (e) {
log.warning('Error encountered during scan: $e');
- return;
+ // TODO(cwhitten): uncomment once fxb/87664 fixed
+ // return;
}
onChanged();
}()
@@ -111,34 +156,35 @@
.listen((_) {});
}
- List<NetworkInformation> get scannedNetworks => _scannedNetworks
- .map((network) => NetworkInformation(
- name: nameFromScannedNetwork(network),
- compatible: compatibleFromScannedNetwork(network),
- icon: iconFromScannedNetwork(network)))
- .toList();
+ // TODO(cwhitten): simplify to _scannedNetworks.map(NetworkInformation.fromScanResult).toList();
+ // once passing named contructors is supported by dart.
+ List<NetworkInformation> get scannedNetworks =>
+ networkInformationFromScannedNetworks(_scannedNetworks);
- String nameFromScannedNetwork(policy.ScanResult network) {
- return utf8.decode(network.id!.ssid.toList());
+ List<NetworkInformation> networkInformationFromScannedNetworks(
+ Set<policy.ScanResult> networks) {
+ var networkInformationList = <NetworkInformation>[];
+ for (var network in networks) {
+ networkInformationList.add(NetworkInformation.fromScanResult(network));
+ }
+ return networkInformationList;
}
- IconData iconFromScannedNetwork(policy.ScanResult network) {
- return network.id!.type == policy.SecurityType.none
- ? Icons.signal_wifi_4_bar
- : Icons.wifi_lock;
+ Uint8List ssidFromScannedNetwork(policy.ScanResult network) {
+ return network.id!.ssid;
}
- bool compatibleFromScannedNetwork(policy.ScanResult network) {
- return network.compatibility == policy.Compatibility.supported;
- }
-
- Future<void> connectToWPA2Network(String password) async {
+ Future<void> connectToNetwork(String password) async {
try {
_connectToWPA2NetworkSubscription = () async {
- final utf8password = Uint8List.fromList(password.codeUnits);
- final credential = policy.Credential.withPassword(utf8password);
+ final credential = _targetNetwork.isOpen
+ ? policy.Credential.withNone(policy.Empty())
+ : policy.Credential.withPassword(
+ Uint8List.fromList(password.codeUnits));
+
policy.ScanResult? network = _scannedNetworks.firstWhereOrNull(
- (network) => nameFromScannedNetwork(network) == _targetNetwork);
+ (network) =>
+ ssidFromScannedNetwork(network) == _targetNetwork.ssid);
if (network == null) {
throw Exception(
@@ -148,7 +194,6 @@
final networkConfig =
policy.NetworkConfig(id: network.id, credential: credential);
- // TODO(fxb/79885): Separate save and connect functionality.
await _clientController?.saveNetwork(networkConfig);
final requestStatus = await _clientController?.connect(network.id!);
@@ -171,7 +216,19 @@
bool get connectionsEnabled => _monitor.connectionsEnabled();
- bool get incorrectPassword => _monitor.incorrectPassword();
+ void _pollNetworksWithFailedCredentials(List<policy.NetworkIdentifier>? ids) {
+ if (ids == null) {
+ return;
+ }
+ for (var id in ids) {
+ final foundNetwork = _savedNetworks.firstWhereOrNull((savedNetwork) =>
+ listEquals(savedNetwork.id?.ssid, id.ssid) &&
+ savedNetwork.id?.type == id.type);
+ if (foundNetwork != null) {
+ _networksWithFailedCredentials.add(foundNetwork);
+ }
+ }
+ }
Future<void> getSavedNetworks() async {
_savedNetworksSubscription = () async {
@@ -210,6 +267,10 @@
// Refresh list of saved networks
await getSavedNetworks();
+
+ _networksWithFailedCredentials.removeWhere((networkConfig) =>
+ listEquals(networkConfig.id?.ssid, foundNetwork.id?.ssid) &&
+ networkConfig.id?.type == foundNetwork.id?.type);
}()
.asStream()
.listen((_) {});
@@ -218,21 +279,19 @@
}
}
- List<NetworkInformation> get savedNetworks => _savedNetworks
- .map((network) => NetworkInformation(
- name: nameFromNetworkConfig(network),
- compatible: true,
- icon: iconFromNetworkConfig(network)))
- .toList();
+ // TODO(cwhitten): simplify to _savedNetworks.map(NetworkInformation.fromNetworkConfig).toList();
+ // once passing named contructors is supported by dart.
+ List<NetworkInformation> get savedNetworks =>
+ networkInformationFromSavedNetworks(_savedNetworks);
- String nameFromNetworkConfig(policy.NetworkConfig network) {
- return utf8.decode(network.id!.ssid.toList());
- }
-
- IconData iconFromNetworkConfig(policy.NetworkConfig network) {
- return network.id!.type == policy.SecurityType.none
- ? Icons.signal_wifi_4_bar
- : Icons.wifi_lock;
+ List<NetworkInformation> networkInformationFromSavedNetworks(
+ Set<policy.NetworkConfig> networks) {
+ var networkInformationList = <NetworkInformation>[];
+ for (var network in networks) {
+ networkInformationList.add(NetworkInformation.fromNetworkConfig(
+ network, _networksWithFailedCredentials));
+ }
+ return networkInformationList;
}
}
@@ -240,8 +299,11 @@
final _binding = policy.ClientStateUpdatesBinding();
policy.ClientStateSummary? _summary;
late final VoidCallback _onChanged;
+ late final void Function(List<policy.NetworkIdentifier>?)
+ _pollNetworksWithFailedCredentials;
- ClientStateUpdatesMonitor(this._onChanged);
+ ClientStateUpdatesMonitor(
+ this._onChanged, this._pollNetworksWithFailedCredentials);
InterfaceHandle<policy.ClientStateUpdates> getInterfaceHandle() =>
_binding.wrap(this);
@@ -263,28 +325,93 @@
return foundNetwork == null ? '' : utf8.decode(foundNetwork);
}
- // TODO(fxb/79855): ensure that failed password status is for target network
- bool incorrectPassword() {
- return _summary?.networks?.firstWhereOrNull((network) =>
- network.status == policy.DisconnectStatus.credentialsFailed) !=
- null;
+ // Check for failed credentials and poll networks with failed credentials
+ void _checkForFailedCredentials() {
+ var foundNetworks = _summary?.networks?.where((network) =>
+ network.status == policy.DisconnectStatus.credentialsFailed);
+ var networkIDs = foundNetworks?.map((network) => network.id!).toList();
+ if (networkIDs != null && networkIDs.isNotEmpty) {
+ _pollNetworksWithFailedCredentials(networkIDs);
+ }
}
@override
Future<void> onClientStateUpdate(policy.ClientStateSummary summary) async {
_summary = summary;
+ _checkForFailedCredentials();
_onChanged();
}
}
/// Network information needed for UI
class NetworkInformation {
- String name;
- bool compatible;
- IconData icon;
+ // SSID used for lookup
+ Uint8List? _ssid;
+ // String representation of SSID in UI
+ String? _name;
+ // If network is able to be connected to
+ bool _compatible = false;
+ // Security type of network
+ policy.SecurityType? _securityType;
+ // If network has a failed connection attempt due to bad credentials
+ // Only set true if failed credentials found
+ bool credentialsFailed = false;
- NetworkInformation(
- {this.name = '',
- this.compatible = false,
- this.icon = Icons.signal_wifi_4_bar});
+ NetworkInformation();
+
+ // Constructor for network config
+ NetworkInformation.fromNetworkConfig(policy.NetworkConfig networkConfig,
+ [Set<policy.NetworkConfig> networksWithFailedCredentials = const {}]) {
+ _ssid = networkConfig.id?.ssid;
+ _name = networkConfig.id?.ssid.toList() != null
+ ? utf8.decode(networkConfig.id!.ssid.toList())
+ : null;
+ _compatible = true;
+ _securityType = networkConfig.id?.type;
+ if (networksWithFailedCredentials.isNotEmpty) {
+ if (networksWithFailedCredentials
+ .map((networkConfig) => networkConfig.id!)
+ .firstWhereOrNull((networkIdentifier) =>
+ listEquals(networkIdentifier.ssid, _ssid) &&
+ networkIdentifier.type == _securityType) !=
+ null) {
+ credentialsFailed = true;
+ }
+ }
+ }
+
+ // Constructor for scan result
+ NetworkInformation.fromScanResult(policy.ScanResult scanResult) {
+ _ssid = scanResult.id?.ssid;
+ _name = scanResult.id?.ssid.toList() != null
+ ? utf8.decode(scanResult.id!.ssid.toList())
+ : null;
+ // Only allow valid characters in UI representation of SSID
+ // TODO(fxb/92668): Allow special characters, such as emojis, in network names
+ if (_name != null) {
+ _name = _name!.replaceAll(RegExp(r'[^A-Za-z0-9()\[\]\s+.,;?&_-]'), '');
+ }
+ _compatible = scanResult.compatibility == policy.Compatibility.supported;
+ _securityType = scanResult.id?.type;
+ }
+
+ Uint8List? get ssid => _ssid;
+
+ String get name => _name ?? '';
+
+ bool get compatible => _compatible;
+
+ IconData get icon => _securityType == policy.SecurityType.none
+ ? Icons.signal_wifi_4_bar
+ : Icons.wifi_lock;
+
+ bool get isOpen => _securityType == policy.SecurityType.none;
+
+ bool get isWEP => _securityType == policy.SecurityType.wep;
+
+ bool get isWPA => _securityType == policy.SecurityType.wpa;
+
+ bool get isWPA2 => _securityType == policy.SecurityType.wpa2;
+
+ bool get isWPA3 => _securityType == policy.SecurityType.wpa3;
}
diff --git a/session_shells/ermine/shell/lib/src/services/shortcuts_service.dart b/session_shells/ermine/shell/lib/src/services/shortcuts_service.dart
index 645bffb..3984bc7 100644
--- a/session_shells/ermine/shell/lib/src/services/shortcuts_service.dart
+++ b/session_shells/ermine/shell/lib/src/services/shortcuts_service.dart
@@ -6,6 +6,7 @@
import 'dart:io';
import 'package:fidl_fuchsia_ui_views/fidl_async.dart';
+import 'package:fuchsia_logger/logger.dart';
import 'package:internationalization/strings.dart';
import 'package:keyboard_shortcuts/keyboard_shortcuts.dart';
@@ -17,6 +18,9 @@
final ViewRef hostViewRef;
+ /// Returns the last shortcut received by the application.
+ String lastShortcutAction = '';
+
ShortcutsService(this.hostViewRef);
late final KeyboardShortcuts _keyboardShortcuts;
@@ -30,12 +34,17 @@
_keyboardShortcuts = KeyboardShortcuts.withViewRef(
hostViewRef,
- actions: actions.map((k, v) => MapEntry(k, () => v())),
+ actions: actions.map((k, v) => MapEntry(k, () {
+ log.info('Received shortcut action: $k');
+ lastShortcutAction = k;
+ v();
+ })),
bindings: bindings,
);
// Hook up actions to flutter driver handler.
flutterDriverHandler = (command) async {
+ log.info('Received flutter driver command: $command');
return actions[command]?.call();
};
}
diff --git a/session_shells/ermine/shell/lib/src/services/startup_service.dart b/session_shells/ermine/shell/lib/src/services/startup_service.dart
index 9e8b747..96c474b 100644
--- a/session_shells/ermine/shell/lib/src/services/startup_service.dart
+++ b/session_shells/ermine/shell/lib/src/services/startup_service.dart
@@ -5,10 +5,11 @@
import 'dart:async';
import 'dart:convert' show json;
import 'dart:io';
+import 'dart:isolate';
import 'package:ermine_utils/ermine_utils.dart';
import 'package:fidl_fuchsia_buildinfo/fidl_async.dart' as buildinfo;
-import 'package:fidl_fuchsia_device_manager/fidl_async.dart';
+import 'package:fidl_fuchsia_hardware_power_statecontrol/fidl_async.dart';
import 'package:fidl_fuchsia_intl/fidl_async.dart';
import 'package:fidl_fuchsia_ui_activity/fidl_async.dart' as activity;
import 'package:flutter/services.dart';
@@ -68,7 +69,7 @@
final _inspect = Inspect();
final _intl = PropertyProviderProxy();
- final _deviceManager = AdministratorProxy();
+ final _hardwareAdmin = AdminProxy();
final _provider = buildinfo.ProviderProxy();
final _activity = activity.ProviderProxy();
final _activityBinding = activity.ListenerBinding();
@@ -79,8 +80,8 @@
StartupService()
: componentContext = ComponentContext.create(),
hostView = ViewHandle(ScenicContext.hostViewRef()) {
+ Incoming.fromSvcPath().connectToService(_hardwareAdmin);
Incoming.fromSvcPath().connectToService(_intl);
- Incoming.fromSvcPath().connectToService(_deviceManager);
Incoming.fromSvcPath().connectToService(_provider);
Incoming.fromSvcPath().connectToService(_activity);
Incoming.fromSvcPath().connectToService(_activityTracker);
@@ -95,15 +96,19 @@
RawKeyboard.instance.addListener((event) {
// We use Alt key release event to dismiss app switching UI.
final data = event.data as RawKeyEventDataFuchsia;
- final fuchsiaKey = FuchsiaKeyboard.kHidUsagePageMask | data.hidUsage;
+ final fuchsiaKey = data.hidUsage;
if (fuchsiaKey == PhysicalKeyboardKey.altLeft.usbHidUsage ||
fuchsiaKey == PhysicalKeyboardKey.altRight.usbHidUsage) {
if (!event.isAltPressed) {
onAltReleased();
}
}
- // Notify activity service to user input.
- onActivity('keyboard');
+ // Notify activity service of user input. This is used to dismiss the
+ // screen saver if it is active. We do this only for key presses
+ // because key release from screensaver shortcut itself might cancel it.
+ if (event is RawKeyDownEvent) {
+ onActivity('keyboard');
+ }
});
});
@@ -153,7 +158,7 @@
}
void dispose() {
- _deviceManager.ctrl.close();
+ _hardwareAdmin.ctrl.close();
_intl.ctrl.close();
_provider.ctrl.close();
_activityBinding.close();
@@ -197,10 +202,20 @@
String get buildVersion => _buildVersion;
/// Reboot the device.
- void restartDevice() => _deviceManager.suspend(suspendFlagReboot);
+ void restartDevice() => _hardwareAdmin.reboot(RebootReason.userRequest);
/// Shutdown the device.
- void shutdownDevice() => _deviceManager.suspend(suspendFlagPoweroff);
+ void shutdownDevice() => _hardwareAdmin.poweroff();
+
+ /// Logout of the user shell.
+ void logout() {
+ // Exit the current isolate, which allows the parent to treat it as a logout
+ // action.
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ Isolate.current.setErrorsFatal(true);
+ Isolate.current.kill(priority: Isolate.beforeNextEvent);
+ });
+ }
Stream<Locale> get stream => LocaleSource(_intl).stream();
diff --git a/session_shells/ermine/shell/lib/src/states/app_state.dart b/session_shells/ermine/shell/lib/src/states/app_state.dart
index 4e3796c..6de8e3c 100644
--- a/session_shells/ermine/shell/lib/src/states/app_state.dart
+++ b/session_shells/ermine/shell/lib/src/states/app_state.dart
@@ -12,7 +12,6 @@
import 'package:ermine/src/states/app_state_impl.dart';
import 'package:ermine/src/states/settings_state.dart';
import 'package:ermine/src/states/view_state.dart';
-import 'package:ermine/src/widgets/app_bar.dart';
import 'package:ermine/src/widgets/dialogs/dialog.dart' as ermine;
import 'package:flutter/material.dart' hide Action, AppBar;
import 'package:fuchsia_scenic/views.dart';
@@ -36,7 +35,7 @@
bool get viewsVisible;
ViewState get topView;
ViewState? get switchTarget;
- List<ermine.Dialog> get dialogs;
+ List<ermine.DialogInfo> get dialogs;
List<ViewState> get views;
Map<String, List<String>> get errors;
Locale? get locale;
@@ -54,10 +53,11 @@
void switchView(ViewState view);
void cancel();
void closeView();
- void launch(String title, String url);
+ void launch(String title, String url, {String? alternateServiceName});
void setTheme({bool darkTheme});
void restart();
void shutdown();
+ void logout();
void launchFeedback();
void launchLicense();
void checkingForUpdatesAlert();
@@ -72,7 +72,7 @@
preferencesService: PreferencesService(),
pointerEventsService: PointerEventsService(
ScenicContext.hostViewRef(),
- insets: EdgeInsets.only(left: AppBar.kWidth),
+ insets: EdgeInsets.zero,
),
) as AppState;
}
diff --git a/session_shells/ermine/shell/lib/src/states/app_state_impl.dart b/session_shells/ermine/shell/lib/src/states/app_state_impl.dart
index 07830f8..0b441af 100644
--- a/session_shells/ermine/shell/lib/src/states/app_state_impl.dart
+++ b/session_shells/ermine/shell/lib/src/states/app_state_impl.dart
@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+// ignore_for_file: unnecessary_lambdas
import 'dart:convert';
import 'package:ermine/src/services/focus_service.dart';
@@ -15,8 +16,7 @@
import 'package:ermine/src/states/settings_state.dart';
import 'package:ermine/src/states/view_state.dart';
import 'package:ermine/src/states/view_state_impl.dart';
-import 'package:ermine/src/widgets/dialogs/dialog.dart' as ermine;
-import 'package:ermine/src/widgets/dialogs/text_only_dialog.dart';
+import 'package:ermine/src/widgets/dialogs/dialog.dart';
import 'package:ermine_utils/ermine_utils.dart';
import 'package:flutter/material.dart' hide Action;
import 'package:fuchsia_inspect/inspect.dart';
@@ -62,7 +62,8 @@
// Register keyboard shortcuts and then initialize SettingsState with it.
shortcutsService.register(_actions);
- settingsState = SettingsState.from(shortcutsService: shortcutsService);
+ settingsState =
+ SettingsState.from(shortcutBindings: shortcutsService.keyboardBindings);
pointerEventsService
..onPeekBegin = _onPeekBegin
@@ -173,7 +174,7 @@
}).asComputed();
@override
- final dialogs = <ermine.Dialog>[].asObservable();
+ final dialogs = <DialogInfo>[].asObservable();
@override
final errors = <String, List<String>>{}.asObservable();
@@ -306,7 +307,18 @@
}
@override
- void cancel() => hideOverlay();
+ void cancel() {
+ if (!dialogsVisible) {
+ hideOverlay();
+ }
+ // If top view is a screensaver, dismiss it.
+ // TODO(fxb/80131): Use cancel action associated with Esc keyboard shortcut
+ // to dismiss the screensaver, since mouse and keyboard input is not
+ // available to the shell when a child view is fullscreen.
+ if (views.isNotEmpty && topView.url == kScreenSaverUrl) {
+ _onIdle(idle: false);
+ }
+ }
@override
void closeView() => _closeView();
@@ -326,11 +338,14 @@
}.asAction();
@override
- void launch(String title, String url) => _launch([title, url]);
- late final _launch = (String title, String url) async {
+ void launch(String title, String url, {String? alternateServiceName}) =>
+ _launch([title, url, alternateServiceName]);
+ late final _launch =
+ (String title, String url, String? alternateServiceName) async {
try {
_clearError(url, 'ProposeElementError');
- await launchService.launch(title, url);
+ await launchService.launch(title, url,
+ alternateServiceName: alternateServiceName);
// Hide app launcher unless we had an error presenting the view.
if (!_isLaunchError(url)) {
runInAction(() {
@@ -357,31 +372,67 @@
}.asAction();
@override
- void restart() => runInAction(startupService.restartDevice);
+ void restart() {
+ _displayDialog(AlertDialogInfo(
+ title: Strings.confirmRestartAlertTitle,
+ body: Strings.confirmToSaveWorkAlertBody,
+ actions: [Strings.cancel, Strings.restart],
+ defaultAction: Strings.restart,
+ onAction: (action) {
+ if (action == Strings.restart) {
+ startupService.restartDevice();
+ // Clean up.
+ dispose();
+ }
+ },
+ ));
+ }
@override
- void shutdown() => runInAction(startupService.shutdownDevice);
+ void shutdown() {
+ _displayDialog(AlertDialogInfo(
+ title: Strings.confirmShutdownAlertTitle,
+ body: Strings.confirmToSaveWorkAlertBody,
+ actions: [Strings.cancel, Strings.shutdown],
+ defaultAction: Strings.shutdown,
+ onAction: (action) {
+ if (action == Strings.shutdown) {
+ startupService.shutdownDevice();
+ // Clean up.
+ dispose();
+ }
+ },
+ ));
+ }
+
+ @override
+ void logout() {
+ _displayDialog(AlertDialogInfo(
+ title: Strings.confirmLogoutAlertTitle,
+ body: Strings.confirmToSaveWorkAlertBody,
+ actions: [Strings.cancel, Strings.logout],
+ defaultAction: Strings.logout,
+ onAction: (action) {
+ if (action == Strings.logout) {
+ dispose();
+ startupService.logout();
+ }
+ },
+ ));
+ }
@override
void checkingForUpdatesAlert() {
- runInAction(() {
- final key = Key(
- 'checkingforupdatesalert_${DateTime.now().millisecondsSinceEpoch}');
- dialogs.add(TextOnlyDialog(
- key: key,
- title: Strings.channelUpdateAlertTitle,
- body: Strings.channelUpdateAlertBody,
- buttons: {
- Strings.close: () {
- dialogs.removeWhere((dialog) => dialog.key == key);
- },
- Strings.continueLabel: () {
- settingsState.checkForUpdates();
- dialogs.removeWhere((dialog) => dialog.key == key);
- },
- },
- ));
- });
+ _displayDialog(AlertDialogInfo(
+ title: Strings.channelUpdateAlertTitle,
+ body: Strings.channelUpdateAlertBody,
+ actions: [Strings.close, Strings.continueLabel],
+ onAction: (action) {
+ if (action == Strings.continueLabel) {
+ settingsState.checkForUpdates();
+ }
+ },
+ ));
}
late final showScreenSaver = () {
@@ -408,10 +459,12 @@
settingsState.showAllSettings();
}
},
- // ignore: unnecessary_lambdas
'increaseBrightness': () => settingsState.increaseBrightness(),
- // ignore: unnecessary_lambdas
'decreaseBrightness': () => settingsState.decreaseBrightness(),
+ 'increaseVolume': () => settingsState.increaseVolume(),
+ 'decreaseVolume': () => settingsState.decreaseVolume(),
+ 'muteVolume': () => settingsState.toggleMute(),
+ 'logout': logout,
};
final _focusedView = Observable<ViewHandle?>(null);
@@ -464,10 +517,6 @@
views.add(view);
topView = view;
- // If the child view is the screen saver, make it non-focusable in order
- // for keyboard input to get routed to the shell and dismiss it.
- viewState.focusable = viewState.url != kScreenSaverUrl;
-
// If any, remove previously cached launch errors for the app.
if (viewState.url != null) {
_clearError(viewState.url!, 'ViewControllerEpitaph');
@@ -482,10 +531,11 @@
}));
// Update view hittestability based on overlay visibility.
- view.reactions.add(reaction<bool>((_) => overlaysVisible, (overlay) {
+ view.reactions.add(reaction<bool>(
+ (_) => overlaysVisible || switcherVisible || dialogsVisible, (overlay) {
// Don't reset hittest flag when showing app switcher, because the
// app switcher does not react to pointer events.
- view.hitTestable = !overlay || switcherVisible;
+ view.hitTestable = !overlay;
}));
// Remove view from views when it is closed.
@@ -499,13 +549,13 @@
void _onViewDismissed(ViewState viewState) {
runInAction(() {
final view = viewState as ViewStateImpl;
- // Switch to next view before closing this view if it was the top view
+ // Switch to previous view before closing this view if it was the top view
// and there are other views.
if (view == topView && views.length > 1) {
- final nextView = topView == views.last
- ? views.first
- : views[views.indexOf(topView) + 1];
- topView = nextView;
+ final prevView = view != views.first
+ ? views[views.indexOf(topView) - 1]
+ : views.last;
+ topView = prevView;
setFocusToChildView();
}
@@ -541,23 +591,17 @@
? Strings.viewPresentRejectedDesc
: Strings.defaultPresentErrorDesc;
const referenceLink =
- 'https://fuchsia.dev/reference/fidl/fuchsia.session#ViewControllerEpitaph';
+ 'https://fuchsia.dev/reference/fidl/fuchsia.element#GraphicalPresenter.PresentView';
if (_isPrelistedApp(url)) {
errors[url] = [description, '$error\n$referenceLink'];
} else {
- final key = Key('presenterr_${DateTime.now().millisecondsSinceEpoch}');
- dialogs.add(TextOnlyDialog(
- key: key,
+ _displayDialog(AlertDialogInfo(
title: description,
body: '${Strings.errorWhilePresenting},\n$url\n\n'
'${Strings.errorType}: $error\n\n'
'${Strings.moreErrorInformation}\n$referenceLink',
- buttons: {
- Strings.close: () {
- dialogs.removeWhere((dialog) => dialog.key == key);
- }
- },
+ actions: [Strings.close],
));
}
});
@@ -572,10 +616,12 @@
: errorSpec == 'rejected'
? Strings.launchRejectedDesc
: Strings.defaultProposeErrorDesc;
+ const referenceLink =
+ 'https://fuchsia.dev/reference/fidl/fuchsia.element#Manager.ProposeElement';
errors[url] = [
description,
- '$proposeError\nhttps://fuchsia.dev/reference/fidl/fuchsia.session#ProposeElementError'
+ '$proposeError\n\n${Strings.moreErrorInformation}\n$referenceLink'
];
}
@@ -628,17 +674,11 @@
return;
}
final description = Strings.applicationFailedToStart(view.title);
- final key = Key('startfail_${DateTime.now().millisecondsSinceEpoch}');
- dialogs.add(TextOnlyDialog(
- key: key,
+ _displayDialog(AlertDialogInfo(
title: description,
body: 'Url: ${view.url}',
- buttons: {
- Strings.close: () {
- dialogs.removeWhere((dialog) => dialog.key == key);
- view.close();
- }
- },
+ actions: [Strings.close],
+ onClose: view.close,
));
}
}
@@ -649,6 +689,8 @@
data['appBarVisible'] = appBarVisible;
data['sideBarVisible'] = sideBarVisible;
data['overlaysVisible'] = overlaysVisible;
+ data['lastAction'] = shortcutsService.lastShortcutAction;
+ data['darkMode'] = hasDarkTheme;
// Number of running component views.
data['numViews'] = views.length;
@@ -668,7 +710,7 @@
final viewData = data['view-$i'];
viewData['title'] = view.title;
viewData['url'] = view.url;
- viewData['focused'] = view == topView;
+ viewData['focused'] = view.view == _focusedView.value;
final viewport = view.viewport;
if (viewport != null) {
@@ -684,6 +726,16 @@
return data;
}
+ void _displayDialog(DialogInfo dialog) {
+ runInAction(() {
+ dialogs.add(dialog);
+ if (viewsVisible) {
+ // Set focus to shell view so that we can receive the esc key press.
+ setFocusToShellView();
+ }
+ });
+ }
+
// Adds inspect data when requested by [Inspect].
void _onInspect(Node node, [Map<String, dynamic>? inspectData]) {
final data = inspectData ?? _getInspectData();
diff --git a/session_shells/ermine/shell/lib/src/states/settings_state.dart b/session_shells/ermine/shell/lib/src/states/settings_state.dart
index 780c213..889c2c0 100644
--- a/session_shells/ermine/shell/lib/src/states/settings_state.dart
+++ b/session_shells/ermine/shell/lib/src/states/settings_state.dart
@@ -12,7 +12,6 @@
import 'package:ermine/src/services/settings/timezone_service.dart';
import 'package:ermine/src/services/settings/volume_service.dart';
import 'package:ermine/src/services/settings/wifi_service.dart';
-import 'package:ermine/src/services/shortcuts_service.dart';
import 'package:ermine/src/states/settings_state_impl.dart';
import 'package:ermine/src/widgets/quick_settings.dart';
import 'package:ermine/src/widgets/settings/setting_details.dart';
@@ -80,12 +79,16 @@
double? get volumeLevel;
bool? get volumeMuted;
List<NetworkInformation> get availableNetworks;
- String get targetNetwork;
+ NetworkInformation get targetNetwork;
List<NetworkInformation> get savedNetworks;
+ TextEditingController get networkPasswordTextController;
+ String get currentNetwork;
+ bool get clientConnectionsEnabled;
- factory SettingsState.from({required ShortcutsService shortcutsService}) {
+ factory SettingsState.from(
+ {required Map<String, Set<String>> shortcutBindings}) {
return SettingsStateImpl(
- shortcutsService: shortcutsService,
+ shortcutBindings: shortcutBindings,
timezoneService: TimezoneService(),
dateTimeService: DateTimeService(),
networkService: NetworkAddressService(),
@@ -113,7 +116,11 @@
void setVolumeLevel(double value);
void setVolumeMute({bool muted});
void showWiFiSettings();
- void connectToWPA2Network(String password);
- void setTargetNetwork(String network);
+ void connectToNetwork([String password]);
+ void setTargetNetwork(NetworkInformation network);
void removeNetwork(String network);
+ void increaseVolume();
+ void decreaseVolume();
+ void toggleMute();
+ void setClientConnectionsEnabled({bool enabled});
}
diff --git a/session_shells/ermine/shell/lib/src/states/settings_state_impl.dart b/session_shells/ermine/shell/lib/src/states/settings_state_impl.dart
index dfb7bc1..dd0c779 100644
--- a/session_shells/ermine/shell/lib/src/states/settings_state_impl.dart
+++ b/session_shells/ermine/shell/lib/src/states/settings_state_impl.dart
@@ -14,7 +14,6 @@
import 'package:ermine/src/services/settings/timezone_service.dart';
import 'package:ermine/src/services/settings/volume_service.dart';
import 'package:ermine/src/services/settings/wifi_service.dart';
-import 'package:ermine/src/services/shortcuts_service.dart';
import 'package:ermine/src/states/settings_state.dart';
import 'package:ermine_utils/ermine_utils.dart';
import 'package:flutter/material.dart' hide Action;
@@ -161,9 +160,16 @@
final Observable<bool?> _volumeMuted = Observable<bool?>(null);
@override
- String get targetNetwork => _targetNetwork.value;
- set targetNetwork(String value) => _targetNetwork.value = value;
- final Observable<String> _targetNetwork = Observable<String>('');
+ NetworkInformation get targetNetwork => _targetNetwork.value;
+ set targetNetwork(NetworkInformation value) => _targetNetwork.value = value;
+ final Observable<NetworkInformation> _targetNetwork =
+ Observable<NetworkInformation>(NetworkInformation());
+
+ @override
+ TextEditingController get networkPasswordTextController =>
+ _networkPasswordTextController;
+ final TextEditingController _networkPasswordTextController =
+ TextEditingController();
@override
final List<NetworkInformation> availableNetworks =
@@ -173,6 +179,17 @@
final List<NetworkInformation> savedNetworks =
ObservableList<NetworkInformation>();
+ @override
+ String get currentNetwork => _currentNetwork.value;
+ set currentNetwork(String value) => _currentNetwork.value = value;
+ final Observable<String> _currentNetwork = ''.asObservable();
+
+ @override
+ bool get clientConnectionsEnabled => _clientConnectionsEnabled.value;
+ set clientConnectionsEnabled(bool value) =>
+ _clientConnectionsEnabled.value = value;
+ final Observable<bool> _clientConnectionsEnabled = Observable<bool>(true);
+
final List<String> _timezones;
@override
@@ -199,7 +216,7 @@
final WiFiService wifiService;
SettingsStateImpl({
- required ShortcutsService shortcutsService,
+ required this.shortcutBindings,
required this.timezoneService,
required this.dateTimeService,
required this.networkService,
@@ -209,8 +226,7 @@
required this.channelService,
required this.volumeService,
required this.wifiService,
- }) : shortcutBindings = shortcutsService.keyboardBindings,
- _timezones = _loadTimezones(),
+ }) : _timezones = _loadTimezones(),
_selectedTimezone = timezoneService.timezone.asObservable() {
dateTimeService.onChanged = updateDateTime;
timezoneService.onChanged =
@@ -310,6 +326,8 @@
..clear()
..addAll(wifiService.savedNetworks)
..removeWhere((network) => network.name.isEmpty);
+ currentNetwork = wifiService.currentNetwork;
+ clientConnectionsEnabled = wifiService.clientConnectionsEnabled;
});
};
}
@@ -430,14 +448,27 @@
runInAction(() => settingsPage.value = SettingsPage.wifi);
@override
- void connectToWPA2Network(String password) =>
- runInAction(() => wifiService.connectToWPA2Network(password));
+ void connectToNetwork([String password = '']) =>
+ runInAction(() => wifiService.connectToNetwork(password));
@override
- void setTargetNetwork(String network) =>
+ void setTargetNetwork(NetworkInformation network) =>
runInAction(() => wifiService.targetNetwork = network);
@override
void removeNetwork(String network) =>
runInAction(() => wifiService.remove(network));
+
+ @override
+ void increaseVolume() => runInAction(volumeService.increaseVolume);
+
+ @override
+ void decreaseVolume() => runInAction(volumeService.decreaseVolume);
+
+ @override
+ void toggleMute() => runInAction(volumeService.toggleMute);
+
+ @override
+ void setClientConnectionsEnabled({bool enabled = true}) =>
+ runInAction(() => wifiService.clientConnectionsEnabled = enabled);
}
diff --git a/session_shells/ermine/shell/lib/src/states/view_state_impl.dart b/session_shells/ermine/shell/lib/src/states/view_state_impl.dart
index 7073d24..d9c9efd 100644
--- a/session_shells/ermine/shell/lib/src/states/view_state_impl.dart
+++ b/session_shells/ermine/shell/lib/src/states/view_state_impl.dart
@@ -131,7 +131,10 @@
int retry = _kMaxRetries,
Duration backOff = _kBackoffDuration,
}) {
- if (loaded && !visible) {
+ // Flatland does not send viewStateChanged events per frame, so setting
+ // _connected to false would be a one way transition and would prevent
+ // this view from ever becoming focusable again.
+ if (!viewConnection.useFlatland && (loaded && !visible)) {
// Reset connected flag to retry setFocus on next viewStateChanged.
_connected.value = false;
} else {
@@ -139,7 +142,7 @@
return;
}
_requestFocusPending = true;
- FocusState.instance.requestFocus(view.handle).then((_) {
+ viewConnection.requestFocus().then((_) {
_requestFocusPending = false;
}).catchError((e) {
if (retry > 0) {
diff --git a/session_shells/ermine/shell/lib/src/widgets/app.dart b/session_shells/ermine/shell/lib/src/widgets/app.dart
index 2da77d9..4244477 100644
--- a/session_shells/ermine/shell/lib/src/widgets/app.dart
+++ b/session_shells/ermine/shell/lib/src/widgets/app.dart
@@ -6,6 +6,7 @@
import 'package:ermine/src/states/app_state.dart';
import 'package:ermine/src/widgets/app_view.dart';
+import 'package:ermine/src/widgets/dialogs/dialogs.dart';
import 'package:ermine/src/widgets/overlays.dart';
import 'package:ermine_utils/ermine_utils.dart';
import 'package:flutter/material.dart' hide AppBar;
@@ -37,11 +38,10 @@
locale: locale,
localizationsDelegates: [
localizations.delegate(),
- GlobalMaterialLocalizations.delegate,
+ ...GlobalMaterialLocalizations.delegates,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: supported_locales.locales,
- shortcuts: FuchsiaKeyboard.defaultShortcuts,
scrollBehavior: MaterialScrollBehavior().copyWith(
dragDevices: {PointerDeviceKind.mouse, PointerDeviceKind.touch},
),
@@ -61,6 +61,10 @@
// Show scrim and overlay layers if an overlay is visible.
if (app.overlaysVisible)
WidgetFactory.create(() => Overlays(app)),
+
+ // Show dialogs above all.
+ if (app.dialogsVisible)
+ WidgetFactory.create(() => Dialogs(app)),
],
);
}),
diff --git a/session_shells/ermine/shell/lib/src/widgets/app_launcher.dart b/session_shells/ermine/shell/lib/src/widgets/app_launcher.dart
index 35eb917..263df3b 100644
--- a/session_shells/ermine/shell/lib/src/widgets/app_launcher.dart
+++ b/session_shells/ermine/shell/lib/src/widgets/app_launcher.dart
@@ -58,7 +58,8 @@
enabled: _isEnabled(item),
onTap: () {
if (!_isLoading(item)) {
- app.launch(item['title']!, item['url']!);
+ app.launch(item['title']!, item['url']!,
+ alternateServiceName: item['element_manager_name']);
}
},
);
diff --git a/session_shells/ermine/shell/lib/src/widgets/dialogs/dialog.dart b/session_shells/ermine/shell/lib/src/widgets/dialogs/dialog.dart
index d207602..93ed72b 100644
--- a/session_shells/ermine/shell/lib/src/widgets/dialogs/dialog.dart
+++ b/session_shells/ermine/shell/lib/src/widgets/dialogs/dialog.dart
@@ -2,10 +2,81 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-import 'package:flutter/material.dart';
+/// An base class for information for aler dialogs.
+class DialogInfo {
+ /// The title of the alert dialog box.
+ final String? title;
-/// An abstract class for all Dialog widgets that will be carried by
-/// [AppState.dialogs].
-abstract class Dialog extends StatelessWidget {
- const Dialog({Key? key}) : super(key: key);
+ /// Optional. The default action to invoke if the user presses submit button.
+ /// This action MUST be present in the list of [actions].
+ final String? defaultAction;
+
+ /// The list of actions that are presented as buttons. The default action is
+ /// drawn using [ElevatedButton] to emphasize it's default nature.
+ final List<String> actions;
+
+ /// Optional. Callback when the dialog is closed.
+ final void Function()? onClose;
+
+ /// Optional. Callback when a specific action is invoked.
+ final void Function(String action)? onAction;
+
+ const DialogInfo({
+ required this.actions,
+ this.defaultAction,
+ this.onAction,
+ this.onClose,
+ this.title,
+ });
+}
+
+/// A class for holding information for alert dialogs widgets that will be
+/// carried by [AppState.dialogs].
+class AlertDialogInfo extends DialogInfo {
+ /// Optional. The content body of the dialog box.
+ final String? body;
+
+ const AlertDialogInfo({
+ required String title,
+ required List<String> actions,
+ String? defaultAction,
+ void Function(String action)? onAction,
+ void Function()? onClose,
+ this.body,
+ }) : super(
+ title: title,
+ actions: actions,
+ defaultAction: defaultAction,
+ onAction: onAction,
+ onClose: onClose,
+ );
+}
+
+/// A class for holding information for password capture dialogs widgets that
+/// will be carried by [AppState.dialogs].
+class PasswordDialogInfo extends DialogInfo {
+ /// The password prompt to show above the password text field.
+ final String prompt;
+
+ /// The callback to receive the entered password.
+ final void Function(String? password) onSubmit;
+
+ /// Optional. Callback to validate the password. Returns an error text on
+ /// validation fail or [null] for success.
+ final String? Function(String? password)? validator;
+
+ PasswordDialogInfo({
+ required this.prompt,
+ required this.onSubmit,
+ required List<String> actions,
+ String? defaultAction,
+ void Function(String action)? onAction,
+ void Function()? onClose,
+ this.validator,
+ }) : super(
+ actions: actions,
+ defaultAction: defaultAction,
+ onAction: onAction,
+ onClose: onClose,
+ );
}
diff --git a/session_shells/ermine/shell/lib/src/widgets/dialogs/dialogs.dart b/session_shells/ermine/shell/lib/src/widgets/dialogs/dialogs.dart
index 00c9944..36bd170 100644
--- a/session_shells/ermine/shell/lib/src/widgets/dialogs/dialogs.dart
+++ b/session_shells/ermine/shell/lib/src/widgets/dialogs/dialogs.dart
@@ -3,23 +3,104 @@
// found in the LICENSE file.
import 'package:ermine/src/states/app_state.dart';
+import 'package:ermine/src/widgets/dialogs/dialog.dart';
+import 'package:ermine/src/widgets/dialogs/password_prompt.dart';
import 'package:flutter/material.dart';
-import 'package:flutter_mobx/flutter_mobx.dart';
+import 'package:mobx/mobx.dart';
-/// A stack of [Dialog] widgets.
-///
-/// The last added dialog comes to the top of the stack.
+/// Displays dialogs sequentially.
class Dialogs extends StatelessWidget {
- final AppState _app;
+ final AppState app;
- const Dialogs(this._app);
+ const Dialogs(this.app);
@override
- Widget build(BuildContext context) => RepaintBoundary(child: Observer(
+ Widget build(BuildContext context) {
+ // Display queued up dialogs if none are being displayed currently.
+ if (!Navigator.canPop(context)) {
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ _showAllDialogs(context);
+ });
+ }
+ return Offstage();
+ }
+
+ // Shows all dialogs sequentially.
+ void _showAllDialogs(BuildContext context, [int index = 0]) async {
+ if (index >= app.dialogs.length) {
+ runInAction(() {
+ app.dialogs.clear();
+ // Hiding overlays should restore focus to child views.
+ app.hideOverlay();
+ });
+ return;
+ }
+ final _formState = GlobalKey<FormState>();
+ bool validate() => _formState.currentState?.validate() ?? false;
+
+ final dialog = app.dialogs[index];
+ final result = await showDialog<String?>(
+ context: context,
builder: (context) {
- return Stack(
- children: [for (final dialog in _app.dialogs) dialog],
+ return Form(
+ key: _formState,
+ child: AlertDialog(
+ title: _titleFromDialogInfo(dialog),
+ content: _contentFromDialogInfo(dialog),
+ actions: [
+ for (final label in dialog.actions)
+ if (label == dialog.defaultAction)
+ ElevatedButton(
+ autofocus: dialog is AlertDialogInfo,
+ child: Text(label.toUpperCase()),
+ onPressed: () {
+ if (validate()) {
+ _formState.currentState?.save();
+ Navigator.pop(context, label);
+ }
+ },
+ )
+ else
+ OutlinedButton(
+ child: Text(label.toUpperCase()),
+ onPressed: () => Navigator.pop(context, label),
+ )
+ ],
+ insetPadding: EdgeInsets.symmetric(horizontal: 240),
+ titlePadding: EdgeInsets.fromLTRB(40, 40, 40, 24),
+ contentPadding: EdgeInsets.fromLTRB(40, 0, 40, 24),
+ actionsPadding: EdgeInsets.only(right: 40, bottom: 24),
+ titleTextStyle: Theme.of(context).textTheme.headline5,
+ ),
);
- },
- ));
+ });
+ if (result != null) {
+ dialog.onAction?.call(result);
+ }
+ dialog.onClose?.call();
+
+ _showAllDialogs(context, index + 1);
+ }
+
+ Widget? _titleFromDialogInfo(DialogInfo info) {
+ switch (info.runtimeType) {
+ case AlertDialogInfo:
+ return Text(info.title!);
+ default:
+ return null;
+ }
+ }
+
+ Widget? _contentFromDialogInfo(DialogInfo info) {
+ switch (info.runtimeType) {
+ case AlertDialogInfo:
+ final dialog = info as AlertDialogInfo;
+ return (dialog.body != null) ? Text(dialog.body!) : null;
+ case PasswordDialogInfo:
+ final dialog = info as PasswordDialogInfo;
+ return PasswordPrompt(dialog);
+ default:
+ return null;
+ }
+ }
}
diff --git a/session_shells/ermine/shell/lib/src/widgets/dialogs/password_prompt.dart b/session_shells/ermine/shell/lib/src/widgets/dialogs/password_prompt.dart
new file mode 100644
index 0000000..b59a3f2
--- /dev/null
+++ b/session_shells/ermine/shell/lib/src/widgets/dialogs/password_prompt.dart
@@ -0,0 +1,71 @@
+// Copyright 2022 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 'package:ermine_utils/ermine_utils.dart';
+import 'package:ermine/src/widgets/dialogs/dialog.dart';
+import 'package:internationalization/strings.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_mobx/flutter_mobx.dart';
+import 'package:mobx/mobx.dart';
+
+/// Defines a widget to collect password in a TextFormField and used as the
+/// content of an AlertDialog.
+class PasswordPrompt extends StatelessWidget {
+ final PasswordDialogInfo info;
+ final _passwordController = TextEditingController();
+ final _showPassword = false.asObservable();
+
+ PasswordPrompt(this.info);
+
+ @override
+ Widget build(BuildContext context) {
+ return Observer(builder: (context) {
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Password prompt.
+ SizedBox(height: 52, width: 440),
+ Text(info.prompt),
+ SizedBox(height: 40),
+
+ // Password text field.
+ TextFormField(
+ key: ValueKey('password'),
+ autofocus: true,
+ autovalidateMode: AutovalidateMode.onUserInteraction,
+ controller: _passwordController,
+ obscureText: !_showPassword.value,
+ decoration: InputDecoration(
+ border: OutlineInputBorder(),
+ labelText: Strings.passwordHint,
+ ),
+ validator: info.validator,
+ onFieldSubmitted: (text) {
+ if (info.validator?.call(text) == null) {
+ Form.of(context)?.save();
+ Navigator.pop(context, info.defaultAction ?? info.actions.last);
+ }
+ },
+ onSaved: info.onSubmit,
+ ),
+ Container(
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Checkbox(
+ onChanged: (value) =>
+ runInAction(() => _showPassword.value = value == true),
+ value: _showPassword.value,
+ ),
+ Text(Strings.showPassword)
+ ],
+ ),
+ ),
+ ],
+ );
+ });
+ }
+}
diff --git a/session_shells/ermine/shell/lib/src/widgets/dialogs/text_only_dialog.dart b/session_shells/ermine/shell/lib/src/widgets/dialogs/text_only_dialog.dart
deleted file mode 100644
index d9ea907..0000000
--- a/session_shells/ermine/shell/lib/src/widgets/dialogs/text_only_dialog.dart
+++ /dev/null
@@ -1,35 +0,0 @@
-// 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 'package:ermine/src/widgets/dialogs/dialog.dart' as ermine;
-import 'package:flutter/material.dart';
-
-/// A dialog consists of only text and action buttons.
-class TextOnlyDialog extends ermine.Dialog {
- final String? title;
- final String? body;
- final Map<String, VoidCallback> buttons;
-
- const TextOnlyDialog({required this.buttons, this.title, this.body, Key? key})
- : assert(title != null || body != null),
- super(key: key);
-
- @override
- Widget build(BuildContext context) => AlertDialog(
- title: (title != null) ? Text(title!) : null,
- content: (body != null) ? Text(body!) : null,
- actions: [
- for (final label in buttons.keys)
- TextButton(
- onPressed: buttons[label],
- child: Text(label.toUpperCase()),
- ),
- ],
- insetPadding: EdgeInsets.symmetric(horizontal: 240),
- titlePadding: EdgeInsets.fromLTRB(40, 40, 40, 24),
- contentPadding: EdgeInsets.fromLTRB(40, 0, 40, 24),
- actionsPadding: EdgeInsets.only(right: 40, bottom: 24),
- titleTextStyle: Theme.of(context).textTheme.headline5,
- );
-}
diff --git a/session_shells/ermine/shell/lib/src/widgets/dialogs/textfield_dialog.dart b/session_shells/ermine/shell/lib/src/widgets/dialogs/textfield_dialog.dart
deleted file mode 100644
index 8377f9c..0000000
--- a/session_shells/ermine/shell/lib/src/widgets/dialogs/textfield_dialog.dart
+++ /dev/null
@@ -1,102 +0,0 @@
-// 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 'package:ermine/src/widgets/dialogs/dialog.dart' as ermine;
-import 'package:flutter/material.dart';
-import 'package:internationalization/strings.dart';
-
-/// A dialog that has a [TextField], action buttons, and an optional text
-/// description.
-class TextfieldDialog extends ermine.Dialog {
- final TextEditingController textController;
- final bool isPassword;
- final String? description;
- final String? fieldLabel;
- final String? fieldHint;
- final Map<String, VoidCallback> buttons;
- final String? errorMessage;
- final bool autoFocus;
-
- final _isObscure = ValueNotifier(false);
-
- TextfieldDialog(
- {required this.buttons,
- required this.textController,
- this.isPassword = false,
- this.description,
- this.fieldLabel,
- this.fieldHint,
- this.errorMessage,
- this.autoFocus = true,
- Key? key})
- : super(key: key) {
- if (isPassword) {
- _isObscure.value = true;
- }
- }
-
- @override
- Widget build(BuildContext context) => AlertDialog(
- title: (description != null) ? Text(description!) : null,
- content: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- ValueListenableBuilder<bool>(
- valueListenable: _isObscure,
- builder: (context, isObscure, _) => Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- Container(
- width: 440,
- child: TextField(
- controller: textController,
- maxLines: 1,
- decoration: InputDecoration(
- labelText: fieldLabel,
- border: OutlineInputBorder(
- borderSide:
- BorderSide(color: Theme.of(context).dividerColor),
- borderRadius: BorderRadius.circular(0),
- ),
- hintText: fieldHint,
- errorText: errorMessage,
- ),
- autofocus: autoFocus,
- obscureText: isObscure,
- ),
- ),
- if (isPassword)
- Row(
- children: [
- Checkbox(
- value: !isObscure,
- onChanged: (value) =>
- _isObscure.value = value != true,
- ),
- SizedBox(width: 8),
- Text(
- Strings.showPassword,
- style: Theme.of(context).textTheme.bodyText1,
- ),
- ],
- ),
- ],
- ),
- ),
- ],
- ),
- actions: [
- for (final label in buttons.keys)
- TextButton(
- onPressed: buttons[label],
- child: Text(label.toUpperCase()),
- ),
- ],
- insetPadding: EdgeInsets.symmetric(horizontal: 240),
- titlePadding: EdgeInsets.fromLTRB(40, 40, 40, 24),
- contentPadding: EdgeInsets.fromLTRB(40, 0, 40, 0),
- actionsPadding: EdgeInsets.only(right: 40, bottom: 24),
- titleTextStyle: Theme.of(context).textTheme.subtitle1,
- );
-}
diff --git a/session_shells/ermine/shell/lib/src/widgets/overlays.dart b/session_shells/ermine/shell/lib/src/widgets/overlays.dart
index 42953cb..224bb98 100644
--- a/session_shells/ermine/shell/lib/src/widgets/overlays.dart
+++ b/session_shells/ermine/shell/lib/src/widgets/overlays.dart
@@ -5,7 +5,6 @@
import 'package:ermine/src/states/app_state.dart';
import 'package:ermine/src/widgets/app_bar.dart';
import 'package:ermine/src/widgets/app_switcher.dart';
-import 'package:ermine/src/widgets/dialogs/dialogs.dart';
import 'package:ermine/src/widgets/scrim.dart';
import 'package:ermine/src/widgets/side_bar.dart';
import 'package:flutter/widgets.dart';
@@ -46,9 +45,6 @@
// App Switcher.
if (app.switcherVisible) AppSwitcher(app),
-
- // Dialogs.
- if (app.dialogsVisible) Dialogs(app),
],
),
);
diff --git a/session_shells/ermine/shell/lib/src/widgets/quick_settings.dart b/session_shells/ermine/shell/lib/src/widgets/quick_settings.dart
index 39c50e5..1a815db 100644
--- a/session_shells/ermine/shell/lib/src/widgets/quick_settings.dart
+++ b/session_shells/ermine/shell/lib/src/widgets/quick_settings.dart
@@ -8,6 +8,7 @@
import 'package:ermine/src/widgets/settings/shortcut_settings.dart';
import 'package:ermine/src/widgets/settings/timezone_settings.dart';
import 'package:ermine/src/widgets/settings/wifi_settings.dart';
+import 'package:ermine_utils/ermine_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:internationalization/strings.dart';
@@ -49,8 +50,7 @@
onChange: state.setTargetChannel,
updateAlert: appState.checkingForUpdatesAlert,
),
- if (state.wifiPageVisible)
- WiFiSettings(state: state, onChange: state.setTargetNetwork)
+ if (state.wifiPageVisible) WiFiSettings(state: state)
],
),
);
@@ -79,20 +79,40 @@
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
+ // Logout button.
+ OutlinedButton(
+ child: Icon(Icons.logout),
+ onPressed: appState.logout,
+ style: ErmineButtonStyle.outlinedButton(Theme.of(context))
+ .copyWith(
+ padding: MaterialStateProperty.all(EdgeInsets.zero),
+ minimumSize: MaterialStateProperty.all(Size(40, 40)),
+ ),
+ ).tooltip(Strings.logout),
+ SizedBox(width: 8),
+
// Restart button.
- OutlinedButton.icon(
+ OutlinedButton(
+ child: Icon(Icons.restart_alt),
onPressed: appState.restart,
- icon: Icon(Icons.restart_alt),
- label: Text(Strings.restart.toUpperCase()),
- ),
+ style: ErmineButtonStyle.outlinedButton(Theme.of(context))
+ .copyWith(
+ padding: MaterialStateProperty.all(EdgeInsets.zero),
+ minimumSize: MaterialStateProperty.all(Size(36, 36)),
+ ),
+ ).tooltip(Strings.restart),
SizedBox(width: 8),
// Power off button.
- OutlinedButton.icon(
+ OutlinedButton(
+ child: Icon(Icons.power_settings_new_rounded),
onPressed: appState.shutdown,
- icon: Icon(Icons.power_settings_new_rounded),
- label: Text(Strings.shutdown.toUpperCase()),
- ),
+ style: ErmineButtonStyle.outlinedButton(Theme.of(context))
+ .copyWith(
+ padding: MaterialStateProperty.all(EdgeInsets.zero),
+ minimumSize: MaterialStateProperty.all(Size(36, 36)),
+ ),
+ ).tooltip(Strings.powerOff),
Spacer(),
// Date time.
@@ -115,6 +135,7 @@
children: [
// Switch Theme
SwitchListTile(
+ key: ValueKey('darkMode'),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
secondary: Icon(Icons.dark_mode),
title: Text(Strings.darkMode),
@@ -173,6 +194,8 @@
trailing: appState.settingsState.brightnessAuto == true
? Text(Strings.auto.toUpperCase())
: OutlinedButton(
+ style: ErmineButtonStyle.outlinedButton(
+ Theme.of(context)),
onPressed: appState.settingsState.setBrightnessAuto,
child: Text(Strings.auto.toUpperCase()),
),
@@ -198,11 +221,15 @@
),
trailing: appState.settingsState.volumeMuted == true
? OutlinedButton(
+ style: ErmineButtonStyle.outlinedButton(
+ Theme.of(context)),
onPressed: () => appState.settingsState
.setVolumeMute(muted: false),
child: Text(Strings.unmute.toUpperCase()),
)
: OutlinedButton(
+ style: ErmineButtonStyle.outlinedButton(
+ Theme.of(context)),
onPressed: () => appState.settingsState
.setVolumeMute(muted: true),
child: Text(Strings.mute.toUpperCase()),
@@ -210,20 +237,29 @@
);
}),
// Wi-Fi
- ListTile(
- contentPadding: EdgeInsets.symmetric(horizontal: 24),
- leading: Icon(Icons.wifi),
- title: Text(Strings.wifi),
- trailing: Wrap(
- alignment: WrapAlignment.end,
- crossAxisAlignment: WrapCrossAlignment.center,
- spacing: 8,
- children: [
- Icon(Icons.arrow_right),
- ],
- ),
- onTap: appState.settingsState.showWiFiSettings,
- ),
+ Observer(builder: (_) {
+ return ListTile(
+ contentPadding: EdgeInsets.symmetric(horizontal: 24),
+ leading: Icon(Icons.wifi),
+ title: Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Text(Strings.wifi),
+ SizedBox(width: 48),
+ Expanded(
+ child: Text(
+ appState.settingsState.currentNetwork,
+ overflow: TextOverflow.ellipsis,
+ textAlign: TextAlign.right,
+ maxLines: 1,
+ ),
+ ),
+ ],
+ ),
+ trailing: Icon(Icons.arrow_right),
+ onTap: appState.settingsState.showWiFiSettings,
+ );
+ }),
// Channel
ListTile(
enabled: true,
@@ -254,6 +290,7 @@
leading: Icon(Icons.feedback_outlined),
title: Text(Strings.feedback),
trailing: OutlinedButton(
+ style: ErmineButtonStyle.outlinedButton(Theme.of(context)),
onPressed: appState.launchFeedback,
child: Text(Strings.open.toUpperCase()),
),
@@ -265,6 +302,7 @@
leading: Icon(Icons.info_outline),
title: Text(Strings.openSource),
trailing: OutlinedButton(
+ style: ErmineButtonStyle.outlinedButton(Theme.of(context)),
onPressed: appState.launchLicense,
child: Text(Strings.open.toUpperCase()),
),
diff --git a/session_shells/ermine/shell/lib/src/widgets/settings/channel_settings.dart b/session_shells/ermine/shell/lib/src/widgets/settings/channel_settings.dart
index dbb67bd..e6b111e 100644
--- a/session_shells/ermine/shell/lib/src/widgets/settings/channel_settings.dart
+++ b/session_shells/ermine/shell/lib/src/widgets/settings/channel_settings.dart
@@ -4,6 +4,7 @@
import 'package:ermine/src/states/settings_state.dart';
import 'package:ermine/src/widgets/settings/setting_details.dart';
+import 'package:ermine_utils/ermine_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:internationalization/strings.dart';
@@ -110,6 +111,7 @@
Padding(
padding: EdgeInsets.fromLTRB(8, 12, 24, 12),
child: ElevatedButton(
+ style: ErmineButtonStyle.elevatedButton(Theme.of(context)),
onPressed: state.targetChannel == '' ? null : updateAlert,
child: Text(Strings.update.toUpperCase()),
),
diff --git a/session_shells/ermine/shell/lib/src/widgets/settings/wifi_settings.dart b/session_shells/ermine/shell/lib/src/widgets/settings/wifi_settings.dart
index 250f89e..10c65da 100644
--- a/session_shells/ermine/shell/lib/src/widgets/settings/wifi_settings.dart
+++ b/session_shells/ermine/shell/lib/src/widgets/settings/wifi_settings.dart
@@ -11,9 +11,8 @@
/// Defines a widget to control WiFi in [SettingDetails] widget.
class WiFiSettings extends StatelessWidget {
final SettingsState state;
- final ValueChanged<String> onChange;
- const WiFiSettings({required this.state, required this.onChange});
+ const WiFiSettings({required this.state});
@override
Widget build(BuildContext context) {
@@ -25,19 +24,22 @@
Expanded(
child: SettingDetails(
title: Strings.wifi,
+ // TODO(cwhitten): Uncomment switch when fxb/90428 is fixed.
+ /*trailing: Switch(
+ value: state.clientConnectionsEnabled,
+ onChanged: (value) =>
+ state.setClientConnectionsEnabled(enabled: value),
+ ),*/
onBack: state.showAllSettings,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Padding(
- padding:
- EdgeInsets.symmetric(horizontal: 24, vertical: 16),
- child: Text(Strings.savedNetworks),
- ),
- if (savedNetworks.isEmpty)
- ListTile(
- title: Text(Strings.loading.toLowerCase()),
+ if (savedNetworks.isNotEmpty)
+ Padding(
+ padding:
+ EdgeInsets.symmetric(horizontal: 24, vertical: 16),
+ child: Text(Strings.savedNetworks),
),
ListView.builder(
shrinkWrap: true,
@@ -45,9 +47,21 @@
itemCount: savedNetworks.length,
itemBuilder: (context, index) {
final networkName = savedNetworks[index].name;
+ bool currentNetwork =
+ networkName == state.currentNetwork;
final networkIcon = savedNetworks[index].icon;
final networkCompatible =
savedNetworks[index].compatible;
+ final networkHasFailedCredentials =
+ savedNetworks[index].credentialsFailed;
+ // Add 'connnected' or 'credentials failed' subtitle if applicable
+ String? networkSubtitle;
+ if (currentNetwork) {
+ networkSubtitle = Strings.connected;
+ }
+ if (networkHasFailedCredentials) {
+ networkSubtitle = Strings.incorrectPassword;
+ }
return ListTile(
title: Text(networkName,
maxLines: 1,
@@ -61,7 +75,8 @@
color: networkCompatible
? null
: Theme.of(context).disabledColor),
- onTap: () => onChange(savedNetworks[index].name),
+ onTap: () =>
+ state.setTargetNetwork(savedNetworks[index]),
trailing: PopupMenuButton(
itemBuilder: (context) {
return [
@@ -74,6 +89,9 @@
onSelected: state.removeNetwork,
tooltip: Strings.forget,
),
+ subtitle: networkSubtitle != null
+ ? Text(networkSubtitle)
+ : null,
);
}),
Padding(
@@ -107,10 +125,10 @@
color: networkCompatible
? null
: Theme.of(context).disabledColor),
- onTap: () =>
- onChange(availableNetworks[index].name),
- trailing: ((state.targetNetwork != '') &&
- (state.targetNetwork == networkName))
+ onTap: () => state
+ .setTargetNetwork(availableNetworks[index]),
+ trailing: ((state.targetNetwork.name != '') &&
+ (state.targetNetwork.name == networkName))
? Icon(Icons.check_outlined)
: null,
);
@@ -120,46 +138,82 @@
),
),
),
- _buildPasswordPrompt(context),
+ _buildNetworkSelection(context),
],
);
});
}
- Widget _buildPasswordPrompt(BuildContext context) {
- TextEditingController textController = TextEditingController();
- bool networkSelected = state.targetNetwork != '';
+ Widget _buildNetworkSelection(BuildContext context) {
+ if (state.targetNetwork.name == '') {
+ return _buildSelectNetworkPrompt(context);
+ } else {
+ if (state.targetNetwork.isOpen) {
+ return _buildOpenNetworkPrompt(context);
+ } else {
+ return _buildPasswordEntryPrompt(context);
+ }
+ }
+ }
+
+ Widget _buildSelectNetworkPrompt(BuildContext context) {
return AppBar(
elevation: 0,
- title: networkSelected
- ? TextField(
- controller: textController,
- maxLines: 1,
- decoration: InputDecoration(
- border: InputBorder.none,
- hintText: Strings.enterPasswordForNetwork(state.targetNetwork),
- ),
- )
- : Text(
- Strings.selectNetwork,
- style: Theme.of(context).textTheme.bodyText2,
- ),
+ title: Text(
+ Strings.selectNetwork,
+ style: Theme.of(context).textTheme.bodyText2,
+ ),
+ shape: Border(top: BorderSide(color: Theme.of(context).indicatorColor)),
+ );
+ }
+
+ Widget _buildOpenNetworkPrompt(BuildContext context) {
+ return AppBar(
+ elevation: 0,
+ title: Text(
+ Strings.connectToNetwork(state.targetNetwork.name),
+ style: Theme.of(context).textTheme.bodyText2,
+ ),
shape: Border(top: BorderSide(color: Theme.of(context).indicatorColor)),
actions: [
- if (networkSelected)
- Padding(
- padding: EdgeInsets.fromLTRB(8, 12, 24, 12),
- child: ElevatedButton(
- onPressed: () => _enterPassword(textController),
- child: Text(Strings.connect),
- ),
+ Padding(
+ padding: EdgeInsets.fromLTRB(8, 12, 24, 12),
+ child: ElevatedButton(
+ onPressed: state.connectToNetwork,
+ child: Text(Strings.connect),
),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildPasswordEntryPrompt(BuildContext context) {
+ return AppBar(
+ elevation: 0,
+ title: TextField(
+ controller: state.networkPasswordTextController,
+ maxLines: 1,
+ decoration: InputDecoration(
+ border: InputBorder.none,
+ hintText: Strings.enterPasswordForNetwork(state.targetNetwork.name),
+ ),
+ ),
+ shape: Border(top: BorderSide(color: Theme.of(context).indicatorColor)),
+ actions: [
+ Padding(
+ padding: EdgeInsets.fromLTRB(8, 12, 24, 12),
+ child: ElevatedButton(
+ onPressed: () =>
+ _enterPassword(state.networkPasswordTextController),
+ child: Text(Strings.connect),
+ ),
+ ),
],
);
}
void _enterPassword(TextEditingController textController) {
- state.connectToWPA2Network(textController.text);
+ state.connectToNetwork(textController.text);
textController.clear();
}
}
diff --git a/session_shells/ermine/shell/meta/ermine.cml b/session_shells/ermine/shell/meta/ermine.cml
new file mode 100644
index 0000000..164b053
--- /dev/null
+++ b/session_shells/ermine/shell/meta/ermine.cml
@@ -0,0 +1,191 @@
+{
+ include: [
+ "//sdk/lib/inspect/client.shard.cml",
+
+ // Enable system logging.
+ "syslog/client.shard.cml",
+ ],
+ program: {
+ data: "data/ermine",
+ },
+ children: [
+ {
+ name: "memfs",
+ url: "fuchsia-pkg://fuchsia.com/ermine#meta/memfs.cm",
+ },
+ {
+ name: "element_manager",
+ url: "fuchsia-pkg://fuchsia.com/element_manager#meta/element_manager.cm",
+ },
+ {
+ name: "chrome",
+ url: "fuchsia-pkg://fuchsia.com/chrome#meta/chrome.cm",
+ },
+ ],
+ capabilities: [
+ {
+ protocol: [
+ "fuchsia.element.GraphicalPresenter",
+ "fuchsia.ui.app.ViewProvider",
+ ],
+ },
+ {
+ storage: "tmp",
+ from: "#memfs",
+ subdir: "tmp",
+ backing_dir: "memfs",
+ storage_id: "static_instance_id_or_moniker",
+ },
+ ],
+ use: [
+ {
+ protocol: [
+ "fuchsia.accessibility.semantics.SemanticsManager",
+ "fuchsia.buildinfo.Provider",
+ "fuchsia.cobalt.LoggerFactory",
+ "fuchsia.feedback.CrashReporter",
+ "fuchsia.fonts.Provider",
+ "fuchsia.hardware.power.statecontrol.Admin",
+ "fuchsia.intl.PropertyProvider",
+ "fuchsia.media",
+ "fuchsia.media.Audio",
+ "fuchsia.media.AudioCore",
+ "fuchsia.memory.Monitor",
+ "fuchsia.net.interfaces.State",
+ "fuchsia.power.battery.BatteryManager",
+ "fuchsia.settings.Intl",
+ "fuchsia.settings.Privacy",
+ "fuchsia.ssh.AuthorizedKeys",
+ "fuchsia.ui.activity.Provider",
+ "fuchsia.ui.activity.Tracker",
+ "fuchsia.ui.brightness.Control",
+ "fuchsia.ui.composition.Allocator",
+ "fuchsia.ui.composition.Flatland",
+ "fuchsia.ui.focus.FocusChainListenerRegistry",
+ "fuchsia.ui.input.ImeService",
+ "fuchsia.ui.input.InputDeviceRegistry",
+ "fuchsia.ui.input.PointerCaptureListenerRegistry",
+ "fuchsia.ui.input3.Keyboard",
+ "fuchsia.ui.scenic.Scenic",
+ "fuchsia.ui.shortcut.Registry",
+ "fuchsia.update.channelcontrol.ChannelControl",
+ "fuchsia.update.Manager",
+ "fuchsia.wlan.common",
+ "fuchsia.wlan.policy",
+ "fuchsia.wlan.policy.ClientProvider",
+ ],
+ },
+ {
+ directory: "config-data",
+ from: "parent",
+ rights: [ "r*" ],
+ path: "/config/data",
+ },
+ {
+ storage: "account",
+ path: "/data",
+ },
+ {
+ protocol: "fuchsia.element.Manager",
+ from: "#element_manager",
+ path: "/svc/fuchsia.element.Manager",
+ },
+ {
+ protocol: "fuchsia.element.Manager",
+ from: "#chrome",
+ path: "/svc/fuchsia.element.Manager-chrome",
+ },
+ ],
+ offer: [
+ {
+ protocol: [ "fuchsia.element.GraphicalPresenter" ],
+ from: "self",
+ to: "#element_manager",
+ dependency: "weak",
+ },
+ {
+ protocol: [
+ "fuchsia.logger.LogSink",
+ "fuchsia.media.Audio",
+ "fuchsia.sys.Launcher",
+ "fuchsia.sysmem.Allocator",
+ "fuchsia.tracing.provider.Registry",
+ "fuchsia.ui.composition.Allocator",
+ "fuchsia.ui.composition.Flatland",
+ "fuchsia.ui.input3.Keyboard",
+ "fuchsia.ui.scenic.Scenic",
+ ],
+ from: "parent",
+ to: "#element_manager",
+ },
+
+ // Support Chrome as a static component.
+ {
+ protocol: [
+ "fuchsia.buildinfo.Provider",
+ "fuchsia.fonts.Provider",
+ "fuchsia.intl.PropertyProvider",
+ "fuchsia.logger.LogSink",
+ "fuchsia.media.Audio",
+ "fuchsia.media.AudioDeviceEnumerator",
+ "fuchsia.media.ProfileProvider",
+ "fuchsia.mediacodec.CodecFactory",
+ "fuchsia.memorypressure.Provider",
+ "fuchsia.net.interfaces.State",
+ "fuchsia.net.name.Lookup",
+ "fuchsia.posix.socket.Provider",
+ "fuchsia.process.Launcher",
+ "fuchsia.sysmem.Allocator",
+ "fuchsia.tracing.provider.Registry",
+ "fuchsia.ui.composition.Allocator",
+ "fuchsia.ui.composition.Flatland",
+ "fuchsia.ui.input3.Keyboard",
+ "fuchsia.ui.scenic.Scenic",
+ "fuchsia.vulkan.loader.Loader",
+ ],
+ from: "parent",
+ to: "#chrome",
+ },
+ {
+ protocol: [ "fuchsia.element.GraphicalPresenter" ],
+ from: "self",
+ to: "#chrome",
+ dependency: "weak",
+ },
+ {
+ directory: "root-ssl-certificates",
+ from: "parent",
+ to: [ "#chrome" ],
+ },
+ {
+ storage: [ "account_cache" ],
+ from: "parent",
+ as: "cache",
+ to: "#chrome",
+ },
+ {
+ storage: [ "tmp" ],
+ from: "self",
+ to: "#chrome",
+ },
+ {
+ storage: "account",
+ from: "parent",
+ as: "data",
+ to: "#chrome",
+ },
+ ],
+ expose: [
+ {
+ protocol: [
+ "fuchsia.element.GraphicalPresenter",
+ "fuchsia.ui.app.ViewProvider",
+ ],
+ from: "self",
+ },
+ {
+ protocol: "fuchsia.element.Manager",
+ from: "#element_manager",
+ },
+ ],
+}
diff --git a/session_shells/ermine/shell/meta/ermine.cmx b/session_shells/ermine/shell/meta/ermine.cmx
deleted file mode 100644
index b5d8b1d..0000000
--- a/session_shells/ermine/shell/meta/ermine.cmx
+++ /dev/null
@@ -1,50 +0,0 @@
-{
- "program": {
- "data": "data/ermine"
- },
- "sandbox": {
- "features": [
- "config-data",
- "isolated-persistent-storage"
- ],
- "pkgfs": [
- "packages"
- ],
- "services": [
- "fuchsia.accessibility.semantics.SemanticsManager",
- "fuchsia.buildinfo.Provider",
- "fuchsia.cobalt.LoggerFactory",
- "fuchsia.device.manager.Administrator",
- "fuchsia.element.Manager",
- "fuchsia.feedback.CrashReporter",
- "fuchsia.fonts.Provider",
- "fuchsia.intl.PropertyProvider",
- "fuchsia.logger.LogSink",
- "fuchsia.media",
- "fuchsia.media.Audio",
- "fuchsia.media.AudioCore",
- "fuchsia.memory.Monitor",
- "fuchsia.net.interfaces.State",
- "fuchsia.power.BatteryManager",
- "fuchsia.settings.Intl",
- "fuchsia.settings.Privacy",
- "fuchsia.ssh.AuthorizedKeys",
- "fuchsia.sys.Environment",
- "fuchsia.ui.activity.Provider",
- "fuchsia.ui.activity.Tracker",
- "fuchsia.ui.brightness.Control",
- "fuchsia.ui.focus.FocusChainListenerRegistry",
- "fuchsia.ui.input.ImeService",
- "fuchsia.ui.input.InputDeviceRegistry",
- "fuchsia.ui.input.PointerCaptureListenerRegistry",
- "fuchsia.ui.input3.Keyboard",
- "fuchsia.ui.scenic.Scenic",
- "fuchsia.ui.shortcut.Registry",
- "fuchsia.update.Manager",
- "fuchsia.update.channelcontrol.ChannelControl",
- "fuchsia.wlan.common",
- "fuchsia.wlan.policy",
- "fuchsia.wlan.policy.ClientProvider"
- ]
- }
-}
diff --git a/session_shells/ermine/shell/test/app_widget_test.dart b/session_shells/ermine/shell/test/app_widget_test.dart
index 19932c1..4ffdb2b 100644
--- a/session_shells/ermine/shell/test/app_widget_test.dart
+++ b/session_shells/ermine/shell/test/app_widget_test.dart
@@ -11,6 +11,7 @@
import 'package:ermine/src/widgets/app.dart';
import 'package:ermine/src/widgets/app_view.dart';
import 'package:ermine/src/widgets/overlays.dart';
+import 'package:ermine/src/widgets/dialogs/dialogs.dart';
import 'package:ermine_utils/ermine_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -40,8 +41,9 @@
final controller = StreamController<Locale>();
final stream = controller.stream.asObservable();
when(state.locale).thenAnswer((_) => stream.value);
- when(state.views).thenAnswer((_) => <ViewState>[].asObservable());
+ when(state.views).thenAnswer((_) => <ViewState>[]);
when(state.overlaysVisible).thenAnswer((_) => false);
+ when(state.dialogsVisible).thenAnswer((_) => false);
await tester.pumpWidget(app);
// app should be OffStage until locale is pushed.
@@ -70,9 +72,10 @@
when(state.locale).thenAnswer((_) => stream.value);
// Create one view.
- when(state.views).thenAnswer((_) => [MockViewState()].asObservable());
+ when(state.views).thenAnswer((_) => [MockViewState()]);
// Show overlays.
when(state.overlaysVisible).thenAnswer((_) => true);
+ when(state.dialogsVisible).thenAnswer((_) => false);
await tester.pumpWidget(app);
await tester.pumpAndSettle();
@@ -80,6 +83,29 @@
expect(find.byKey(ValueKey(Overlays)), findsOneWidget);
await controller.close();
});
+
+ testWidgets('Dialogs are visible', (tester) async {
+ final controller = StreamController<Locale>();
+ final stream =
+ controller.stream.asObservable(initialValue: Locale('en', 'US'));
+ when(state.locale).thenAnswer((_) => stream.value);
+ when(state.views).thenAnswer((_) => []);
+
+ // Show dialogs.
+ final dialogsVisible = true.asObservable();
+ when(state.overlaysVisible).thenAnswer((_) => false);
+ when(state.dialogsVisible).thenAnswer((_) => dialogsVisible.value);
+
+ await tester.pumpWidget(app);
+ await tester.pumpAndSettle();
+ expect(find.byKey(ValueKey(Dialogs)), findsOneWidget);
+
+ runInAction(() => dialogsVisible.value = false);
+ await tester.pumpAndSettle();
+ expect(find.byKey(ValueKey(Dialogs)), findsNothing);
+
+ await controller.close();
+ });
}
class MockAppState extends Mock implements AppState {}
diff --git a/session_shells/ermine/utils/BUILD.gn b/session_shells/ermine/utils/BUILD.gn
index 83f7295..47188c6 100644
--- a/session_shells/ermine/utils/BUILD.gn
+++ b/session_shells/ermine/utils/BUILD.gn
@@ -11,11 +11,11 @@
sources = [
"ermine_utils.dart",
"src/crash.dart",
- "src/fuchsia_keyboard.dart",
"src/mobx_disposable.dart",
"src/mobx_extensions.dart",
"src/themes.dart",
"src/view_handle.dart",
+ "src/widget_extension.dart",
"src/widget_factory.dart",
]
deps = [
diff --git a/session_shells/ermine/utils/lib/ermine_utils.dart b/session_shells/ermine/utils/lib/ermine_utils.dart
index 572eef5..3a8c5a9 100644
--- a/session_shells/ermine/utils/lib/ermine_utils.dart
+++ b/session_shells/ermine/utils/lib/ermine_utils.dart
@@ -3,9 +3,9 @@
// found in the LICENSE file.
export 'src/crash.dart';
-export 'src/fuchsia_keyboard.dart';
export 'src/mobx_disposable.dart';
export 'src/mobx_extensions.dart';
export 'src/themes.dart';
export 'src/view_handle.dart';
+export 'src/widget_extension.dart';
export 'src/widget_factory.dart';
diff --git a/session_shells/ermine/utils/lib/src/crash.dart b/session_shells/ermine/utils/lib/src/crash.dart
index 8bfdd37..83db8e5 100644
--- a/session_shells/ermine/utils/lib/src/crash.dart
+++ b/session_shells/ermine/utils/lib/src/crash.dart
@@ -68,7 +68,7 @@
// Generate [CrashReport] from errorType, errorMessage and stackTrace.
final report = CrashReport(
- programName: 'ermine.cmx',
+ programName: 'ermine.cm',
programUptime: uptime.inMilliseconds * 1000,
specificReport: SpecificCrashReport.withDart(
RuntimeCrashReport(
diff --git a/session_shells/ermine/utils/lib/src/fuchsia_keyboard.dart b/session_shells/ermine/utils/lib/src/fuchsia_keyboard.dart
deleted file mode 100644
index 1c2ee80..0000000
--- a/session_shells/ermine/utils/lib/src/fuchsia_keyboard.dart
+++ /dev/null
@@ -1,104 +0,0 @@
-// 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 'package:flutter/services.dart';
-import 'package:flutter/widgets.dart';
-
-/// Defines the mapping of Fuchsia keys to application [Intent]s.
-///
-/// This is needed because currently the key mapping for Fuchsia in Flutter
-/// Framework is broken.
-class FuchsiaKeyboard {
- // Fuchsia keyboard HID usage values are defined in (page 53):
- // https://www.usb.org/sites/default/files/documents/hut1_12v2.pdf
-
- static const int kHidUsagePageMask = 0x70000;
- static const int kFuchsiaKeyIdPlane = LogicalKeyboardKey.fuchsiaPlane;
-
- static const kEnter = LogicalKeyboardKey(40 | kFuchsiaKeyIdPlane);
- static const kBackspace = LogicalKeyboardKey(42 | kFuchsiaKeyIdPlane);
- static const kDelete = LogicalKeyboardKey(76 | kFuchsiaKeyIdPlane);
- static const kEscape = LogicalKeyboardKey(41 | kFuchsiaKeyIdPlane);
- static const kTab = LogicalKeyboardKey(43 | kFuchsiaKeyIdPlane);
- static const kArrowLeft = LogicalKeyboardKey(80 | kFuchsiaKeyIdPlane);
- static const kArrowRight = LogicalKeyboardKey(79 | kFuchsiaKeyIdPlane);
- static const kArrowDown = LogicalKeyboardKey(81 | kFuchsiaKeyIdPlane);
- static const kArrowUp = LogicalKeyboardKey(82 | kFuchsiaKeyIdPlane);
- static const kPageUp = LogicalKeyboardKey(75 | kFuchsiaKeyIdPlane);
- static const kPageDown = LogicalKeyboardKey(78 | kFuchsiaKeyIdPlane);
-
- static const Map<ShortcutActivator, Intent> defaultShortcuts =
- <ShortcutActivator, Intent>{
- // Activation
- SingleActivator(kEnter): ActivateIntent(),
- SingleActivator(LogicalKeyboardKey.space): ActivateIntent(),
-
- // Dismissal
- SingleActivator(kEscape): DismissIntent(),
-
- // Keyboard traversal.
- SingleActivator(kTab): NextFocusIntent(),
- SingleActivator(kTab, shift: true): PreviousFocusIntent(),
- SingleActivator(kArrowLeft):
- DirectionalFocusIntent(TraversalDirection.left),
- SingleActivator(kArrowRight):
- DirectionalFocusIntent(TraversalDirection.right),
- SingleActivator(kArrowDown):
- DirectionalFocusIntent(TraversalDirection.down),
- SingleActivator(kArrowUp): DirectionalFocusIntent(TraversalDirection.up),
-
- // Scrolling
- SingleActivator(kArrowUp, control: true):
- ScrollIntent(direction: AxisDirection.up),
- SingleActivator(kArrowDown, control: true):
- ScrollIntent(direction: AxisDirection.down),
- SingleActivator(kArrowLeft, control: true):
- ScrollIntent(direction: AxisDirection.left),
- SingleActivator(kArrowRight, control: true):
- ScrollIntent(direction: AxisDirection.right),
- SingleActivator(kPageUp): ScrollIntent(
- direction: AxisDirection.up, type: ScrollIncrementType.page),
- SingleActivator(kPageDown): ScrollIntent(
- direction: AxisDirection.down, type: ScrollIncrementType.page),
- };
-
- static const Map<ShortcutActivator, Intent> defaultEditingShortcuts =
- <ShortcutActivator, Intent>{
- SingleActivator(kBackspace): DeleteTextIntent(),
- SingleActivator(kBackspace, control: true): DeleteByWordTextIntent(),
- SingleActivator(kBackspace, alt: true): DeleteByLineTextIntent(),
- SingleActivator(kDelete): DeleteForwardTextIntent(),
- SingleActivator(kDelete, control: true): DeleteForwardByWordTextIntent(),
- SingleActivator(kDelete, alt: true): DeleteForwardByLineTextIntent(),
- SingleActivator(kArrowDown, alt: true): MoveSelectionToEndTextIntent(),
- SingleActivator(kArrowLeft, alt: true): MoveSelectionLeftByLineTextIntent(),
- SingleActivator(kArrowRight, alt: true):
- MoveSelectionRightByLineTextIntent(),
- SingleActivator(kArrowUp, alt: true): MoveSelectionToStartTextIntent(),
- SingleActivator(kArrowDown, shift: true, alt: true):
- ExpandSelectionToEndTextIntent(),
- SingleActivator(kArrowLeft, shift: true, alt: true):
- ExpandSelectionLeftByLineTextIntent(),
- SingleActivator(kArrowRight, shift: true, alt: true):
- ExpandSelectionRightByLineTextIntent(),
- SingleActivator(kArrowUp, shift: true, alt: true):
- ExpandSelectionToStartTextIntent(),
- SingleActivator(kArrowDown): MoveSelectionDownTextIntent(),
- SingleActivator(kArrowLeft): MoveSelectionLeftTextIntent(),
- SingleActivator(kArrowRight): MoveSelectionRightTextIntent(),
- SingleActivator(kArrowUp): MoveSelectionUpTextIntent(),
- SingleActivator(kArrowLeft, control: true):
- MoveSelectionLeftByWordTextIntent(),
- SingleActivator(kArrowRight, control: true):
- MoveSelectionRightByWordTextIntent(),
- SingleActivator(kArrowLeft, shift: true, control: true):
- ExtendSelectionLeftByWordTextIntent(),
- SingleActivator(kArrowRight, shift: true, control: true):
- ExtendSelectionRightByWordTextIntent(),
- SingleActivator(kArrowDown, shift: true): ExtendSelectionDownTextIntent(),
- SingleActivator(kArrowLeft, shift: true): ExtendSelectionLeftTextIntent(),
- SingleActivator(kArrowRight, shift: true): ExtendSelectionRightTextIntent(),
- SingleActivator(kArrowUp, shift: true): ExtendSelectionUpTextIntent(),
- };
-}
diff --git a/session_shells/ermine/utils/lib/src/themes.dart b/session_shells/ermine/utils/lib/src/themes.dart
index cbf5eae..13193a9 100644
--- a/session_shells/ermine/utils/lib/src/themes.dart
+++ b/session_shells/ermine/utils/lib/src/themes.dart
@@ -48,6 +48,7 @@
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
primary: Colors.black,
+ onSurface: Colors.grey,
side: BorderSide(color: Colors.black),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
shape:
@@ -133,6 +134,7 @@
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
primary: Colors.white,
+ onSurface: Colors.grey,
side: BorderSide(color: Colors.white),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
shape:
@@ -178,3 +180,55 @@
),
);
}
+
+class ErmineButtonStyle {
+ const ErmineButtonStyle._();
+
+ static ButtonStyle elevatedButton(ThemeData theme) => ButtonStyle(
+ overlayColor: MaterialStateProperty.resolveWith<Color?>(
+ (Set<MaterialState> states) {
+ if (states.contains(MaterialState.hovered)) {
+ return theme.bottomAppBarColor.withOpacity(0.24);
+ }
+ if (states.contains(MaterialState.focused)) {
+ return theme.bottomAppBarColor.withOpacity(0.38);
+ }
+ if (states.contains(MaterialState.pressed)) {
+ return theme.colorScheme.primary.withOpacity(0.58);
+ }
+ return null;
+ }),
+ );
+
+ static ButtonStyle outlinedButton(ThemeData theme) => ButtonStyle(
+ overlayColor: MaterialStateProperty.resolveWith<Color?>(
+ (Set<MaterialState> states) {
+ if (states.contains(MaterialState.hovered)) {
+ return theme.dividerColor.withOpacity(0.12);
+ }
+ if (states.contains(MaterialState.focused)) {
+ return theme.dividerColor.withOpacity(0.24);
+ }
+ if (states.contains(MaterialState.pressed)) {
+ return theme.colorScheme.primary.withOpacity(0.58);
+ }
+ return null;
+ }),
+ );
+
+ static ButtonStyle textButton(ThemeData theme) => ButtonStyle(
+ overlayColor: MaterialStateProperty.resolveWith<Color?>(
+ (Set<MaterialState> states) {
+ if (states.contains(MaterialState.hovered)) {
+ return theme.dividerColor.withOpacity(0.12);
+ }
+ if (states.contains(MaterialState.focused)) {
+ return theme.dividerColor.withOpacity(0.24);
+ }
+ if (states.contains(MaterialState.pressed)) {
+ return theme.colorScheme.primary.withOpacity(0.58);
+ }
+ return null;
+ }),
+ );
+}
diff --git a/session_shells/ermine/utils/lib/src/view_handle.dart b/session_shells/ermine/utils/lib/src/view_handle.dart
index 4658949..6c836fa 100644
--- a/session_shells/ermine/utils/lib/src/view_handle.dart
+++ b/session_shells/ermine/utils/lib/src/view_handle.dart
@@ -2,11 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-import 'dart:async';
import 'package:fidl_fuchsia_ui_views/fidl_async.dart';
-import 'package:fuchsia_logger/logger.dart';
-import 'package:fuchsia_scenic_flutter/fuchsia_view.dart' as fuchsia_view;
/// A helper class that wraps [ViewRef] and provides convenient accessors.
class ViewHandle {
@@ -30,13 +27,4 @@
int get handle => viewRef.reference.handle?.handle ?? -1;
- /// Request focus to be transfered to the view with [handle].
- Future<void> focus() async {
- try {
- await fuchsia_view.FocusState.instance.requestFocus(handle);
- // ignore: avoid_catches_without_on_clauses
- } catch (e) {
- log.warning('Exception on requestFocus: $e ${StackTrace.current}');
- }
- }
}
diff --git a/session_shells/ermine/utils/lib/src/widget_extension.dart b/session_shells/ermine/utils/lib/src/widget_extension.dart
new file mode 100644
index 0000000..7c11453
--- /dev/null
+++ b/session_shells/ermine/utils/lib/src/widget_extension.dart
@@ -0,0 +1,16 @@
+// Copyright 2022 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 'package:flutter/material.dart';
+
+/// Helper widget extensions to help flatten widget trees.
+extension WidgetExtension on Widget {
+ Widget tooltip(String message) {
+ return Tooltip(message: message, child: this);
+ }
+
+ Widget padding(EdgeInsetsGeometry value) {
+ return Padding(padding: value, child: this);
+ }
+}
diff --git a/tests/BUILD.gn b/tests/BUILD.gn
index 0d93392..f1424e3 100644
--- a/tests/BUILD.gn
+++ b/tests/BUILD.gn
@@ -11,10 +11,7 @@
source_dir = "lib"
- sources = [
- "ermine_driver.dart",
- "simple_browser_driver.dart",
- ]
+ sources = [ "ermine_driver.dart" ]
deps = [
"//sdk/fidl/fuchsia.input",
@@ -23,6 +20,7 @@
"//sdk/testing/sl4f/client",
"//sdk/testing/sl4f/flutter_driver_sl4f",
"//third_party/dart-pkg/git/flutter/packages/flutter_driver",
+ "//third_party/dart-pkg/git/flutter/packages/fuchsia_remote_debug_protocol",
"//third_party/dart-pkg/pub/image",
"//third_party/dart-pkg/pub/test",
]
diff --git a/tests/chrome/BUILD.gn b/tests/chrome/BUILD.gn
index 9a21cfb..793e95a 100644
--- a/tests/chrome/BUILD.gn
+++ b/tests/chrome/BUILD.gn
@@ -5,6 +5,15 @@
import("//build/dart/test.gni")
import("//build/testing/environments.gni")
+# E2E product test runtime dependencies specific to end to end product tests for
+# products in src/experiences.
+#
+# This is pulled from workstation_pro.gni.
+group("end_to_end_deps") {
+ testonly = true
+ public_deps = [ "//src/experiences/bin/ermine_testserver" ]
+}
+
dart_test("workstation_chrome_smoke_test") {
null_safe = true
sources = [ "workstation_chrome_smoke_test.dart" ]
@@ -14,28 +23,62 @@
"//sdk/testing/sl4f/client",
"//sdk/testing/sl4f/flutter_driver_sl4f",
"//src/experiences/tests:ermine_driver",
+ "//third_party/dart-pkg/git/flutter/packages/flutter_driver",
"//third_party/dart-pkg/pub/test",
]
- environments = [ atlas_env ]
+ environments = [
+ # TODO(fxbug.dev/91950): Reenable on AEMU after Screenshots on Flatland is not flaky.
+ {
+ dimensions = {
+ device_type = "Intel NUC Kit NUC7i5DNHE"
+ }
+ tags = [ "e2e-fyi" ]
+ },
+ atlas_env,
+ ]
}
-copy("runtime_deps") {
- _data_dir = "$target_gen_dir/runtime_deps"
+dart_test("workstation_chrome_advanced_smoke_test") {
+ null_safe = true
+ sources = [ "workstation_chrome_advanced_smoke_test.dart" ]
- sources = [ "//prebuilt/third_party/chromedriver/linux-x64/chromedriver" ]
+ deps = [
+ "//sdk/fidl/fuchsia.input",
+ "//sdk/testing/sl4f/client",
+ "//sdk/testing/sl4f/flutter_driver_sl4f",
+ "//src/experiences/tests:ermine_driver",
+ "//third_party/dart-pkg/git/flutter/packages/flutter_driver",
+ "//third_party/dart-pkg/pub/test",
+ ]
- outputs = [ "$_data_dir/{{source_file_part}}" ]
+ environments = [
+ # TODO(fxbug.dev/91950): Reenable on AEMU after Screenshots on Flatland is not flaky.
+ {
+ dimensions = {
+ device_type = "Intel NUC Kit NUC7i5DNHE"
+ }
+ tags = [ "e2e-fyi" ]
+ },
- metadata = {
- test_runtime_deps = [ "$_data_dir/chromedriver" ]
- }
+ # TODO(fxbug.dev/94042): Reenable on Atlas after non-hermetic interactions
+ # with experiences_ermine_smoke_e2e_test are resolved.
+ {
+ dimensions = {
+ device_type = "Atlas"
+ }
+ tags = [ "e2e-fyi" ]
+ },
+ ]
}
group("test") {
testonly = true
if (is_host && is_linux) {
# Chromedriver prebuilt is only available for linux-x64
- deps = [ ":workstation_chrome_smoke_test($host_toolchain)" ]
+ deps = [
+ ":workstation_chrome_advanced_smoke_test($host_toolchain)",
+ ":workstation_chrome_smoke_test($host_toolchain)",
+ ]
}
}
diff --git a/tests/chrome/test/workstation_chrome_advanced_smoke_test.dart b/tests/chrome/test/workstation_chrome_advanced_smoke_test.dart
new file mode 100644
index 0000000..c4e8a52
--- /dev/null
+++ b/tests/chrome/test/workstation_chrome_advanced_smoke_test.dart
@@ -0,0 +1,112 @@
+// 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.
+
+// ignore_for_file: import_of_legacy_library_into_null_safe
+import 'dart:math';
+
+import 'package:ermine_driver/ermine_driver.dart';
+import 'package:fidl_fuchsia_input/fidl_async.dart';
+import 'package:flutter_driver/flutter_driver.dart';
+import 'package:sl4f/sl4f.dart';
+import 'package:test/test.dart';
+
+const chromiumUrl = 'fuchsia-pkg://fuchsia.com/chrome#meta/chrome.cm';
+const testserverUrl =
+ 'fuchsia-pkg://fuchsia.com/ermine_testserver#meta/ermine_testserver.cmx';
+
+void main() {
+ late Sl4f sl4f;
+ late ErmineDriver ermine;
+ late Input input;
+
+ setUpAll(() async {
+ sl4f = Sl4f.fromEnvironment();
+ await sl4f.startServer();
+
+ ermine = ErmineDriver(sl4f);
+ await ermine.setUp();
+
+ input = Input(sl4f);
+ print('Set up Input');
+ });
+
+ tearDownAll(() async {
+ await ermine.tearDown();
+ print('Tore down Ermine flutter driver');
+ await sl4f.stopServer();
+ print('Stopped sl4f server');
+ sl4f.close();
+ print('Closed sl4f');
+ });
+
+ test('Chrome browser should be able to access and render web pages.',
+ () async {
+ // Launches test server app
+ expect(await ermine.launch(testserverUrl), isTrue);
+ await ermine.driver.waitUntilNoTransientCallbacks();
+ print('Launched the test server.');
+
+ // TODO(fxb/94441): Launch Chromium using [ErmineDriver.launch] once the blocker is fixed.
+ // Opens the app launcher and find the Chromium app entry
+ print('Opening the app launcher');
+ await ermine.driver.requestData('launcher');
+ await ermine.driver.waitUntilNoTransientCallbacks();
+ final chromiumEntry = find.text('Chromium');
+ await ermine.driver.waitFor(chromiumEntry);
+ print('Found Chromium app entry on the app launcher');
+
+ // Launch Chromium app
+ await ermine.driver.tap(chromiumEntry);
+ print('Tapped Chromium app entry');
+ await ermine.driver.waitUntilNoTransientCallbacks();
+ print('Launched Chromium');
+
+ final snapshot = await ermine.waitForView(chromiumUrl, testForFocus: true);
+ expect(snapshot.url, chromiumUrl);
+ print('A Chromium view is presented');
+
+ const blueUrl = 'http://127.0.0.1:8080/blue.html';
+ await input.text(blueUrl, keyEventDuration: Duration(milliseconds: 50));
+ print('Typed in $blueUrl to the browser');
+ await input.keyPress(kEnterKey);
+ print('Pressed Enter');
+
+ const blue = 0xffff0000; // (0xAABBGGRR)
+ Map<int, int> histogram;
+
+ await Future.delayed(Duration(seconds: 3));
+ final isBlue = await ermine.waitFor(() async {
+ print('Take a screenshot...');
+ final screenshot = await ermine.screenshot(Rectangle(500, 500, 100, 100));
+ histogram = ermine.histogram(screenshot);
+ print('Color key: ${histogram.keys.first}');
+ print('Color value: ${histogram.values.first}');
+ if (histogram.keys.length == 1 && histogram[blue] == 10000) {
+ return true;
+ }
+ return false;
+ }, timeout: Duration(minutes: 2));
+
+ expect(isBlue, isTrue);
+ print('Verified the expected background color');
+
+ // Close Chromium
+ print('Closing the Chromium View');
+ await ermine.threeKeyShortcut(Key.leftCtrl, Key.leftShift, Key.w);
+ await ermine.driver.waitUntilNoTransientCallbacks();
+ await ermine.waitForAction('close');
+ print('Verified that Ermine took CLOSE action');
+ expect(await ermine.waitForViewAbsent(chromiumUrl), true);
+ print('Closed Chromium');
+
+ // Close the test server.
+ print('Closing the test server');
+ await ermine.threeKeyShortcut(Key.leftCtrl, Key.leftShift, Key.w);
+ await ermine.driver.waitUntilNoTransientCallbacks();
+ await ermine.waitForAction('close');
+ print('Verified that Ermine took CLOSE action');
+ expect(await ermine.waitForViewAbsent(testserverUrl), true);
+ print('Closed test server');
+ }, timeout: Timeout(Duration(minutes: 3)));
+}
diff --git a/tests/chrome/test/workstation_chrome_smoke_test.dart b/tests/chrome/test/workstation_chrome_smoke_test.dart
index 49d20fa..b1cfab6 100644
--- a/tests/chrome/test/workstation_chrome_smoke_test.dart
+++ b/tests/chrome/test/workstation_chrome_smoke_test.dart
@@ -3,12 +3,15 @@
// found in the LICENSE file.
// ignore_for_file: import_of_legacy_library_into_null_safe
+import 'dart:math';
+
import 'package:ermine_driver/ermine_driver.dart';
import 'package:fidl_fuchsia_input/fidl_async.dart';
+import 'package:flutter_driver/flutter_driver.dart';
import 'package:sl4f/sl4f.dart';
import 'package:test/test.dart';
-const chromeUrl = 'fuchsia-pkg://fuchsia.com/chrome#meta/chrome_v1.cmx';
+const chromiumUrl = 'fuchsia-pkg://fuchsia.com/chrome#meta/chrome.cm';
void main() {
late Sl4f sl4f;
@@ -32,19 +35,46 @@
});
test('Should be able to launch Chrome browser.', () async {
- await ermine.launch(chromeUrl);
+ // Launches Chromium app
+ // TODO(fxb/94441): Launch Chromium using [ErmineDriver.launch] once the blocker is fixed.
+ final chromiumEntry = find.text('Chromium');
+ await ermine.driver.waitFor(chromiumEntry);
+ print('Found Chromium app entry');
+ await ermine.driver.tap(chromiumEntry);
+ print('Tapped Chromium app entry');
await ermine.driver.waitUntilNoTransientCallbacks();
- print('Launched Chrome');
+ print('Launched Chromium');
- final snapshot = await ermine.waitForView(chromeUrl);
- expect(snapshot.focused, true);
- expect(snapshot.url, chromeUrl);
- print('A Chrome view is presented');
+ final snapshot = await ermine.waitForView(chromiumUrl, testForFocus: true);
+ expect(snapshot.url, chromiumUrl);
+ print('A Chromium view is presented');
+
+ // Takes a screenshot and checks the color
+ const white = 0xffffffff; // (0xAABBGGRR)
+ Map<int, int> histogram;
+
+ await Future.delayed(Duration(seconds: 3));
+ final isWhite = await ermine.waitFor(() async {
+ print('Take a screenshot...');
+ final screenshot = await ermine.screenshot(Rectangle(500, 500, 100, 100));
+ histogram = ermine.histogram(screenshot);
+ print('Color key: ${histogram.keys.first}');
+ print('Color value: ${histogram.values.first}');
+ if (histogram.keys.length == 1 && histogram[white] == 10000) {
+ return true;
+ }
+ return false;
+ }, timeout: Duration(minutes: 2));
+
+ expect(isWhite, isTrue);
+ print('Verified the expected background color');
// Close the Chrome view.
await ermine.threeKeyShortcut(Key.leftCtrl, Key.leftShift, Key.w);
await ermine.driver.waitUntilNoTransientCallbacks();
- expect(await ermine.waitForViewAbsent(chromeUrl), true);
- print('Closed Chrome');
- });
+ await ermine.waitForAction('close');
+ print('Verified that Ermine took CLOSE action');
+ expect(await ermine.waitForViewAbsent(chromiumUrl), true);
+ print('Closed Chromium');
+ }, timeout: Timeout(Duration(minutes: 2)));
}
diff --git a/tests/e2e/BUILD.gn b/tests/e2e/BUILD.gn
index 8145cc4..298732a 100644
--- a/tests/e2e/BUILD.gn
+++ b/tests/e2e/BUILD.gn
@@ -11,18 +11,20 @@
# This is pulled from workstation.gni.
group("end_to_end_deps") {
testonly = true
- public_deps = [ "//src/experiences/bin/ermine_testserver" ]
+ public_deps = [
+ "//src/experiences/bin/ermine_testserver",
+ "//src/sys/tools/stash_ctl",
+ ]
}
-_service_account =
- "fuchsia-e2e-auth@fuchsia-cloud-api-for-test.iam.gserviceaccount.com"
-
-host_test_data("scuba_goldens") {
- sources = [
- "//src/experiences/tests/e2e/test/scuba_goldens/simple_browser_rearranging_tab_after.png",
- "//src/experiences/tests/e2e/test/scuba_goldens/simple_browser_rearranging_tab_before.png",
- ]
- outputs = [ "$root_out_dir/scuba_goldens/{{source_file_part}}" ]
+if (is_host) {
+ host_test_data("scuba_goldens") {
+ sources = [
+ "//src/experiences/tests/e2e/test/scuba_goldens/simple_browser_rearranging_tab_after.png",
+ "//src/experiences/tests/e2e/test/scuba_goldens/simple_browser_rearranging_tab_before.png",
+ ]
+ outputs = [ "$root_out_dir/scuba_goldens/{{source_file_part}}" ]
+ }
}
dart_test("experiences_ermine_session_shell_e2e_test") {
@@ -107,52 +109,6 @@
]
}
-dart_test("experiences_ermine_simple_browser_e2e_test") {
- null_safe = true
- sources = [ "ermine_session_shell_simple_browser_test.dart" ]
-
- deps = [
- ":scuba_goldens",
- "//sdk/fidl/fuchsia.input",
- "//sdk/fidl/fuchsia.ui.input",
- "//sdk/fidl/fuchsia.ui.input3",
- "//sdk/testing/gcloud_lib",
- "//sdk/testing/sl4f/client",
- "//sdk/testing/sl4f/flutter_driver_sl4f",
- "//src/experiences/tests:ermine_driver",
- "//third_party/dart-pkg/git/flutter/packages/flutter_driver",
- "//third_party/dart-pkg/pub/image",
- "//third_party/dart-pkg/pub/test",
- "//third_party/dart-pkg/pub/webdriver",
- ]
-
- non_dart_deps = [ ":runtime_deps" ]
-
- environments = [
- {
- dimensions = {
- device_type = "AEMU"
- }
- service_account = _service_account
- tags = [ "e2e-fyi" ]
- },
- {
- dimensions = {
- device_type = "Intel NUC Kit NUC7i5DNHE"
- }
- service_account = _service_account
- tags = [ "e2e-fyi" ]
- },
- {
- dimensions = {
- device_type = "Atlas"
- }
- service_account = _service_account
- tags = [ "e2e-fyi" ]
- },
- ]
-}
-
dart_test("experiences_ermine_smoke_e2e_test") {
null_safe = true
sources = [ "ermine_smoke_test.dart" ]
@@ -170,11 +126,7 @@
]
environments = [
- {
- dimensions = {
- device_type = "AEMU"
- }
- },
+ # TODO(fxbug.dev/91950): Reenable on AEMU after Screenshots on Flatland is not flaky.
{
dimensions = {
device_type = "Intel NUC Kit NUC7i5DNHE"
@@ -184,6 +136,9 @@
dimensions = {
device_type = "Atlas"
}
+
+ # TODO(fxbug.dev/650923): De-flake and re-enable this test.
+ tags = [ "e2e-fyi" ]
},
]
}
@@ -206,7 +161,6 @@
# Chromedriver prebuilt is only available for linux-x64
deps = [
":experiences_ermine_session_shell_e2e_test($host_toolchain)",
- ":experiences_ermine_simple_browser_e2e_test($host_toolchain)",
":experiences_ermine_smoke_e2e_test($host_toolchain)",
":experiences_ermine_terminal_e2e_test($host_toolchain)",
]
diff --git a/tests/e2e/test/ermine_session_shell_ask_test.dart b/tests/e2e/test/ermine_session_shell_ask_test.dart
index e27cedc..9da7eb6 100644
--- a/tests/e2e/test/ermine_session_shell_ask_test.dart
+++ b/tests/e2e/test/ermine_session_shell_ask_test.dart
@@ -38,8 +38,7 @@
// The inspect data should show that the view has focus.
const componentUrl = 'fuchsia-pkg://fuchsia.com/terminal#meta/terminal.cmx';
- final inspect = await ermine.waitForView(componentUrl);
- expect(inspect.focused, isTrue);
+ await ermine.waitForView(componentUrl, testForFocus: true);
// Close the terminal view.
await ermine.driver.requestData('close');
diff --git a/tests/e2e/test/ermine_session_shell_simple_browser_test.dart b/tests/e2e/test/ermine_session_shell_simple_browser_test.dart
deleted file mode 100644
index f60b6d1..0000000
--- a/tests/e2e/test/ermine_session_shell_simple_browser_test.dart
+++ /dev/null
@@ -1,771 +0,0 @@
-// Copyright 2020 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.
-
-// ignore_for_file: import_of_legacy_library_into_null_safe
-import 'dart:async';
-import 'dart:math';
-
-import 'package:ermine_driver/ermine_driver.dart';
-import 'package:ermine_driver/simple_browser_driver.dart';
-import 'package:fidl_fuchsia_input/fidl_async.dart';
-import 'package:fidl_fuchsia_ui_input3/fidl_async.dart' hide KeyEvent;
-import 'package:flutter_driver/flutter_driver.dart';
-import 'package:gcloud_lib/gcloud_lib.dart';
-import 'package:sl4f/sl4f.dart';
-import 'package:test/test.dart';
-import 'package:webdriver/sync_io.dart';
-
-const _timeoutOneSec = Duration(seconds: 1);
-const _timeoutThreeSec = Duration(seconds: 3);
-const _timeoutTenSec = Duration(seconds: 10);
-const _timeoutPerTest = Timeout(Duration(seconds: 60));
-const _sampleViewRect = Rectangle(100, 200, 100, 100);
-const testserverUrl =
- 'fuchsia-pkg://fuchsia.com/ermine_testserver#meta/ermine_testserver.cmx';
-
-// Flags to enable/disable each test in order of
-// 0: Web pages & history navigation test
-// 1: Video test
-// 2: Tab control test
-// 3: Text input field test
-// 4: Audio test
-// 5: Keyboard shortcut test.
-const skipTests = [true, true, false, true, true, true];
-
-void main() {
- late Sl4f sl4f;
- late ErmineDriver ermine;
- late WebDriverConnector webDriverConnector;
- late Input input;
-
- // TODO(fxb/69334): Get rid of the space in the hint text.
- const newTabHintText = ' SEARCH';
- const indexUrl = 'http://127.0.0.1:8080/index.html';
- const stopUrl = 'http://127.0.0.1:8080/stop';
- final newTabFinder = find.text('NEW TAB');
- final indexTabFinder = find.text('Localhost');
- final nextTabFinder = find.text('Next Page');
- final popupTabFinder = find.text('Popup Page');
- final videoTabFinder = find.text('Video Test');
- final redTabFinder = find.text('Red Page');
- final greenTabFinder = find.text('Green Page');
- final blueTabFinder = find.text('Blue Page');
- final audioTabFinder = find.text('Audio Test');
-
- setUpAll(() async {
- sl4f = Sl4f.fromEnvironment();
- await sl4f.startServer();
- print('Started Sl4f server');
-
- ermine = ErmineDriver(sl4f);
- await ermine.setUp();
- print('Set up Ermine driver');
-
- input = Input(sl4f);
- print('Set up Input');
-
- webDriverConnector = WebDriverConnector('runtime_deps/chromedriver', sl4f);
- await webDriverConnector.initialize();
- print('Set up and initialized a web driver');
- });
-
- tearDownAll(() async {
- await webDriverConnector.tearDown();
- print('Tore down Web driver');
- await Future.delayed(Duration(seconds: 1));
- await ermine.tearDown();
- print('Tore down Ermine flutter driver');
- await sl4f.stopServer();
- print('Stopped sl4f server');
- sl4f.close();
- print('Closed sl4f');
- });
-
- Future<bool> _waitForTabArrangement(FlutterDriver browser,
- SerializableFinder leftTabFinder, SerializableFinder rightTabFinder,
- {Duration timeout = const Duration(seconds: 30)}) async {
- return ermine.waitFor(() async {
- final leftTabX = (await browser.getCenter(leftTabFinder)).dx;
- final rightTabX = (await browser.getCenter(rightTabFinder)).dx;
- return leftTabX < rightTabX;
- }, timeout: timeout);
- }
-
- /// Keeps finding a web element that satisfies the given [by] condition until
- /// the timeout expires. [NoSuchElementException] thrown by [findElement]
- /// in the meantime is ignored.
- /// Returns the [WebElement] if it finds one. Otherwise, returns null.
- Future<WebElement?> _waitForWebElement(WebDriver web, By by) async {
- return await ermine.waitFor<WebElement?>(() async {
- try {
- final element = web.findElement(by);
- return element;
- } on NoSuchElementException {
- return null;
- }
- }, timeout: _timeoutTenSec);
- }
-
- /// Keeps calling the given action until it gets the expected result within
- /// a fixed time. Errors thrown by the action in the meantime is ignored.
- /// e.g. [DriverError], thrown in case the driver fails to locate a [Finder].
- /// Returns true if it gets the expected result. Otherwise, returns false.
- Future<bool> _repeatActionUntilGetResult(
- dynamic action(), Future<void> result()) async {
- return await ermine.waitFor(() async {
- action.call();
- try {
- await result.call();
- return true;
- // ignore: avoid_catches_without_on_clauses
- } catch (e) {
- print('$e. Keep repeating the action until timeout expires...');
- return false;
- }
- }, timeout: _timeoutTenSec);
- }
-
- /// Keeps calling `waitFor(finder)` until the timeout expires. Returns true
- /// if it locates one. Otherwise, returns false.
- Future<bool> _repeatActionWaitingFor(
- FlutterDriver browser,
- dynamic action(),
- SerializableFinder finder, {
- Duration waitForTimeout = _timeoutOneSec,
- }) async {
- return await _repeatActionUntilGetResult(
- action, () => browser.waitFor(finder, timeout: waitForTimeout));
- }
-
- /// Keeps calling `waitForAbsent(finder)` until the timeout expires. Returns
- /// true if it locates one. Otherwise, returns false.
- Future<bool> _repeatActionWaitingForAbsent(
- FlutterDriver browser,
- dynamic action(),
- SerializableFinder finder, {
- Duration waitForAbsentTimeout = _timeoutOneSec,
- }) async {
- return await _repeatActionUntilGetResult(action,
- () => browser.waitForAbsent(finder, timeout: waitForAbsentTimeout));
- }
-
- Future<void> _invokeShortcut(List<Key> keys) async {
- const pressDuration = 100;
- final releaseDuration = pressDuration * keys.length + 100;
- var pressDurations = [
- for (var i = 0; i < keys.length; i++) pressDuration + (i * 100)
- ];
- var releaseDurations = [
- for (var i = 0; i < keys.length; i++) releaseDuration + (i * 100)
- ];
-
- await input.keyEvents([
- for (var i = 0; i < keys.length; i++)
- KeyEvent(keys[i], Duration(milliseconds: pressDurations[i]),
- KeyEventType.pressed),
- // Releases the key in reverse order.
- for (var i = 0; i < keys.length; i++)
- KeyEvent(keys[keys.length - i - 1],
- Duration(milliseconds: releaseDurations[i]), KeyEventType.released)
- ]);
-
- await ermine.driver
- .waitUntilNoTransientCallbacks(timeout: Duration(seconds: 2));
- }
-
- // TODO(fxb/68689): Transition pointer interactions to Sl4f.Input once it is
- // ready.
- test('Should be able to do page and history navigation.', () async {
- // Starts hosting a local http website.
- // ignore: unawaited_futures
- ermine.component.launch(testserverUrl);
- print('Launched the test server .');
-
- FlutterDriver browser;
- browser = await ermine.launchAndWaitForSimpleBrowser();
-
- // Access to the website.
- await input.text(indexUrl);
- print('Typed in $indexUrl to the browser');
- await input.keyPress(kEnterKey);
- print('Pressed Enter');
- await browser.waitUntilFirstFrameRasterized();
- await browser.waitUntilNoTransientCallbacks(timeout: _timeoutTenSec);
- await browser.waitFor(indexTabFinder, timeout: _timeoutTenSec);
- print('Index tab found.');
-
- final webdriver =
- (await webDriverConnector.webDriversForHost('127.0.0.1')).single;
- print('Connected a web driver to the localhost');
-
- expect(await browser.getText(indexTabFinder), isNotNull);
- print('Opened $indexUrl');
-
- final nextLink = await _waitForWebElement(webdriver, By.linkText('Next'));
- expect(nextLink, isNotNull);
-
- // Clicks the text link that opens next.html (page navigation)
- expect(
- await _repeatActionWaitingForAbsent(
- browser, nextLink!.click, indexTabFinder),
- isTrue,
- reason: 'Failed to click the Next link.');
- expect(await browser.getText(newTabFinder), isNotNull);
- expect(await browser.getText(nextTabFinder), isNotNull);
- print('Clicked the next.html link');
-
- final prevLink = await _waitForWebElement(webdriver, By.linkText('Prev'));
- expect(prevLink, isNotNull);
-
- // Clicks the text link that opens index.html (page navigation)
- expect(
- await _repeatActionWaitingForAbsent(
- browser, prevLink!.click, nextTabFinder),
- isTrue,
- reason: 'Failed to click the Prev link.');
-
- expect(await browser.getText(newTabFinder), isNotNull);
- expect(await browser.getText(indexTabFinder), isNotNull);
- print('Clicked the index.html link');
-
- // Goes back to next.html by tapping the BCK button (history navigation)
- expect(
- await _repeatActionWaitingForAbsent(browser, () async {
- final back = find.byValueKey('back');
- await browser.tap(back);
- }, indexTabFinder),
- isTrue,
- reason: 'Failed to hit the BCK button.',
- );
-
- expect(await browser.getText(newTabFinder), isNotNull);
- expect(await browser.getText(nextTabFinder), isNotNull);
- print('Hit BCK');
-
- // Goes forward to index.html by tapping the FWD button (history navigation)
- expect(
- await _repeatActionWaitingForAbsent(browser, () async {
- final forward = find.byValueKey('forward');
- await browser.tap(forward);
- }, nextTabFinder),
- isTrue,
- reason: 'Failed to hit the FWD button.',
- );
-
- expect(await browser.getText(newTabFinder), isNotNull);
- expect(await browser.getText(indexTabFinder), isNotNull);
- print('Hit FWD');
-
- // Clicks + button to increase the number
- var digitLink = await _waitForWebElement(webdriver, By.id('target'));
- final addButton = await _waitForWebElement(webdriver, By.id('increase'));
- expect(digitLink!.text, '0');
- addButton!.click();
- await ermine.waitFor(() async {
- return digitLink!.text == '1';
- });
- addButton.click();
- await ermine.waitFor(() async {
- return digitLink!.text == '2';
- });
- print('Clicked the + button next to the digit three times');
-
- // Refreshes the page
- final refresh = find.byValueKey('refresh');
- await browser.tap(refresh);
- await browser.waitUntilNoTransientCallbacks(timeout: _timeoutTenSec);
- digitLink = await _waitForWebElement(webdriver, By.id('target'));
- await ermine.waitFor(() async {
- return digitLink!.text == '0';
- });
- print('Hit RFRSH');
-
- final popupLink = await _waitForWebElement(webdriver, By.linkText('Popup'));
- expect(popupLink, isNotNull);
-
- // Clicks the text link that opens popup.html (popup page navigation)
- expect(
- await _repeatActionWaitingFor(browser, popupLink!.click, popupTabFinder,
- waitForTimeout: _timeoutThreeSec),
- isTrue,
- reason: 'Failed to click the Popup link.');
- expect(await browser.getText(newTabFinder), isNotNull);
- expect(await browser.getText(indexTabFinder), isNotNull);
- expect(await browser.getText(popupTabFinder), isNotNull);
- print('Clicked the popup.html link');
-
- // Stops the local http server.
- await browser.requestData(stopUrl);
- await browser.waitUntilNoTransientCallbacks(timeout: _timeoutTenSec);
- await browser.waitFor(find.text(stopUrl), timeout: _timeoutTenSec);
-
- // Closes the flutter driver connected to the browser.
- await browser.close();
-
- // Close the simple browser view.
- await ermine.threeKeyShortcut(Key.leftCtrl, Key.leftShift, Key.w);
- await ermine.driver.waitUntilNoTransientCallbacks(timeout: _timeoutTenSec);
- await ermine.waitForViewAbsent(simpleBrowserUrl);
- print('Closed the browser');
- }, timeout: _timeoutPerTest, skip: skipTests[0]);
-
- test('Should be able to play videos on web pages.', () async {
- // Starts hosting a local http website.
- // ignore: unawaited_futures
- ermine.component.launch(testserverUrl);
- print('Launched the test server.');
-
- FlutterDriver browser;
- browser = await ermine.launchAndWaitForSimpleBrowser();
-
- // Access to video.html where the following video is played:
- // experiences/bin/ermine_testserver/public/simple_browser_test/sample_video.mp4
- // It shows the violet-colored background for the first 3 seconds then shows
- // the fuchsia-colored background for another 3 seconds.
- await input.text('http://127.0.0.1:8080/video.html');
- await input.keyPress(kEnterKey);
- await browser.waitFor(videoTabFinder, timeout: _timeoutTenSec);
-
- expect(await browser.getText(videoTabFinder), isNotNull);
- print('Opened http://127.0.0.1:8080/video.html');
-
- // Waits for a while for the video to be loaded before taking a screenshot.
- await Future.delayed(Duration(seconds: 2));
- final earlyScreenshot = await ermine.screenshot(_sampleViewRect);
-
- // Takes another screenshot after 3 seconds.
- await Future.delayed(Duration(seconds: 3));
-
- final isVideoPlayed = await ermine.waitFor(() async {
- final lateScreenshot = await ermine.screenshot(_sampleViewRect);
- final diff = ermine.screenshotsDiff(earlyScreenshot, lateScreenshot);
- return diff == 1;
- }, timeout: _timeoutTenSec);
-
- expect(isVideoPlayed, isTrue);
- print('The video was played');
-
- // Stops the local http server.
- await browser.requestData(stopUrl);
- await browser.waitUntilNoTransientCallbacks(timeout: _timeoutTenSec);
- await browser.waitFor(find.text(stopUrl), timeout: _timeoutTenSec);
-
- // Closes the flutter driver connected to the browser.
- await browser.close();
- await ermine.driver.requestData('close');
- await ermine.driver.waitForAbsent(find.text('simple-browser.cmx'));
- expect(await ermine.isStopped(simpleBrowserUrl), isTrue);
- print('Closed the browser');
- }, timeout: _timeoutPerTest, skip: skipTests[1]);
-
- test('Should be able to switch, rearrange, and close tabs', () async {
- // Starts hosting a local http website.
- expect(await ermine.launch(testserverUrl), isTrue);
- await ermine.driver.waitUntilNoTransientCallbacks();
- print('Launched the test server.');
-
- final browser = SimpleBrowserDriver(ermine);
- await browser.launchAndWaitForSimpleBrowser();
-
- /// Tab Switching Test
- const redUrl = 'http://127.0.0.1:8080/red.html';
- const greenUrl = 'http://127.0.0.1:8080/green.html';
- const blueUrl = 'http://127.0.0.1:8080/blue.html';
-
- // Opens red.html in the second tab leaving the first tab as an empty tab.
- await input.text(redUrl, keyEventDuration: Duration(milliseconds: 50));
- print('Typed in $redUrl to the browser');
- await input.keyPress(kEnterKey);
- print('Pressed Enter');
- await browser.driver.waitUntilFirstFrameRasterized();
- await browser.driver.waitUntilNoTransientCallbacks(timeout: _timeoutTenSec);
- await browser.driver.waitFor(redTabFinder, timeout: _timeoutTenSec);
- print('Opened red.html');
-
- // Opens green.html in the third tab.
- await browser.driver.tap(find.byValueKey('new_tab'));
- await browser.driver
- .waitFor(find.text(newTabHintText), timeout: _timeoutTenSec);
-
- await input.text(greenUrl, keyEventDuration: Duration(milliseconds: 50));
- print('Typed in $greenUrl to the browser');
- await input.keyPress(kEnterKey);
- print('Pressed Enter');
- await browser.driver.waitUntilFirstFrameRasterized();
- await browser.driver.waitUntilNoTransientCallbacks(timeout: _timeoutTenSec);
- await browser.driver.waitFor(greenTabFinder, timeout: _timeoutTenSec);
- print('Opened green.html');
-
- // Opens blue.html in the forth tab.
- await browser.driver.tap(find.byValueKey('new_tab'));
- await browser.driver
- .waitFor(find.text(newTabHintText), timeout: _timeoutTenSec);
-
- await input.text(blueUrl, keyEventDuration: Duration(milliseconds: 50));
- print('Typed in $blueUrl to the browser');
- await input.keyPress(kEnterKey);
- print('Pressed Enter');
- await browser.driver.waitUntilFirstFrameRasterized();
- await browser.driver.waitUntilNoTransientCallbacks(timeout: _timeoutTenSec);
- await browser.driver.waitFor(blueTabFinder, timeout: _timeoutTenSec);
- print('Opened blue.html');
-
- // Should have 4 tabs and the forth tab should be focused.
- expect(await browser.driver.getText(newTabFinder), isNotNull);
- expect(await browser.driver.getText(redTabFinder), isNotNull);
- expect(await browser.driver.getText(greenTabFinder), isNotNull);
- expect(await browser.driver.getText(blueTabFinder), isNotNull);
- expect(await browser.driver.getText(find.text(blueUrl)), isNotNull);
- print('The Blue tab is focused');
-
- // The second tab should be focused when tapped.
- await browser.driver.tap(redTabFinder);
- await browser.driver.waitFor(find.text(redUrl));
- expect(await browser.driver.getText(find.text(redUrl)), isNotNull);
- print('Clicked the Red tab');
-
- // The thrid tab should be focused when tapped.
- await browser.driver.tap(greenTabFinder);
- await browser.driver.waitFor(find.text(greenUrl));
- expect(await browser.driver.getText(find.text(greenUrl)), isNotNull);
- print('Clicked the Green tab');
-
- /// Tab Rearranging Test
-
- // Checks the current order of tabs before rearranging tabs.
- expect(
- await _waitForTabArrangement(
- browser.driver, newTabFinder, redTabFinder),
- isTrue,
- reason: 'The New tab is not on the left side of the Red tab:');
- expect(
- await _waitForTabArrangement(
- browser.driver, redTabFinder, greenTabFinder),
- isTrue,
- reason: 'The Red tab is not on the left side of the Green tab');
- expect(
- await _waitForTabArrangement(
- browser.driver, greenTabFinder, blueTabFinder),
- isTrue,
- reason: 'The Green tab is not on the left side of the Blue tab');
- print('The tabs are in the order of New > Red > Green > Blue');
-
- // Drags the second tab to the right end of the tab list.
- await browser.driver.scroll(redTabFinder, 600, 0, Duration(seconds: 1));
-
- // The order of tabs after rearranging tabs.
- expect(
- await _waitForTabArrangement(
- browser.driver, newTabFinder, greenTabFinder),
- isTrue,
- reason: 'The New tab is not on the left side of the Green tab.');
- expect(
- await _waitForTabArrangement(
- browser.driver, greenTabFinder, blueTabFinder),
- isTrue,
- reason: 'The Green tab is not on the left side of the Blue tab');
- expect(
- await _waitForTabArrangement(
- browser.driver, blueTabFinder, redTabFinder),
- isTrue,
- reason: 'The Blue tab is not on the left side of the Red tab');
- print('Moved the Red tab to the right end');
-
- /// Tab closing test
- final tabCloseFinder = find.byValueKey('tab_close');
- await browser.driver.tap(tabCloseFinder);
-
- // The red page should be gone and the last tab should be focused.
- await browser.driver.waitForAbsent(redTabFinder);
- print('Closed the Red tab');
-
- expect(await browser.driver.getText(newTabFinder), isNotNull);
- expect(await browser.driver.getText(greenTabFinder), isNotNull);
- expect(await browser.driver.getText(blueTabFinder), isNotNull);
- expect(await browser.driver.getText(find.text(blueUrl)), isNotNull);
- print('The Blue tab is focused');
-
- // TODO(fxb/70265): Test closing an unfocused tab once fxb/68689 is done.
-
- await ermine.threeKeyShortcut(Key.leftCtrl, Key.leftShift, Key.w);
- await ermine.driver.waitUntilNoTransientCallbacks();
- await ermine.waitForViewAbsent(simpleBrowserUrl);
- print('Closed the browser');
-
- await ermine.threeKeyShortcut(Key.leftCtrl, Key.leftShift, Key.w);
- await ermine.driver.waitUntilNoTransientCallbacks();
- await ermine.waitForViewAbsent(testserverUrl);
- print('Closed the test server');
-
- // Closes the flutter driver connected to the browser.
- await browser.tearDown();
- print('Tore down the browser driver');
- }, timeout: _timeoutPerTest, skip: skipTests[2]);
-
- test('Should be able enter text into web text fields', () async {
- // Starts hosting a local http website.
- expect(await ermine.launch(testserverUrl), isTrue);
- await ermine.driver.waitUntilNoTransientCallbacks();
- print('Launched the test server.');
-
- FlutterDriver browser;
- browser = await ermine.launchAndWaitForSimpleBrowser();
-
- const testInputPage = 'http://127.0.0.1:8080/input.html';
- final textInputTabFinder = find.text('Text Input');
-
- // Access to the website.
- await input.text(testInputPage);
- await input.keyPress(kEnterKey);
- await browser.waitUntilNoTransientCallbacks(timeout: _timeoutTenSec);
- await browser.waitFor(textInputTabFinder, timeout: _timeoutTenSec);
- print('Opened $testInputPage');
-
- final webdriver =
- (await webDriverConnector.webDriversForHost('127.0.0.1')).single;
-
- final textField = await _waitForWebElement(webdriver, By.id('text-input'));
- print('The textfield is found.');
-
- expect(textField, isNotNull);
-
- textField!.click();
- await ermine.waitFor(() async {
- return webdriver.activeElement!.equals(textField);
- }, timeout: _timeoutTenSec);
-
- print('The textfield is now focused.');
-
- // TODO(fxb/74070): Sl4f.Input currently does not work for web elements.
- // Replace the following line with Sl4f.Input once that is fixed.
- const testText = 'hello fuchsia';
- textField.sendKeys(testText);
- await ermine.waitFor(() async {
- return textField.properties['value'] == testText;
- }, timeout: _timeoutTenSec);
- print('Text is entered into the textfield.');
-
- // Closes the flutter driver connected to the browser.
- await browser.close();
-
- await ermine.driver.requestData('closeAll');
- print('Closed all views');
- }, timeout: _timeoutPerTest, skip: skipTests[3]);
-
- test('Should be able to play audios on web', () async {
- // Starts hosting a local http website.
- // ignore: unawaited_futures
- ermine.component.launch(testserverUrl);
- print('Launched the test server.');
-
- FlutterDriver browser;
- browser = await ermine.launchAndWaitForSimpleBrowser();
-
- final record = Audio(sl4f);
- final gcloud = GCloud();
-
- // Access to audio.html where the following audio is played:
- // experiences/bin/ermine_testserver/public/simple_browser_test/sample_audio.mp3
- // It plays human voice saying "How old is Obama".
- await input.text('http://127.0.0.1:8080/audio.html');
- await input.keyPress(kEnterKey);
- await browser.waitFor(audioTabFinder, timeout: _timeoutTenSec);
-
- expect(await browser.getText(audioTabFinder), isNotNull);
- print('Opened http://127.0.0.1:8080/audio.html');
-
- final webdriver =
- (await webDriverConnector.webDriversForHost('127.0.0.1')).single;
-
- final audio = await _waitForWebElement(webdriver, By.id('audio'));
- expect(audio, isNotNull);
- print('The textfield is found.');
-
- final playButton = await _waitForWebElement(webdriver, By.id('play'));
- expect(playButton, isNotNull);
- print('The PLAY button is found.');
-
- // Note that it doesn't work locally. You should create GCloud using
- // `GCloud.withClientViaApiKey()` with an API key for local testing.
- await gcloud.setClientFromMetadata();
- print('Set an authenticated gcloud client');
-
- // Plays the audio, records it, sends it to gcloud for speech-to-text, and
- // verifies if the text result is what we expect.
- // Retries this process for a few more times if it fails since the audio
- // sometimes sounds janky.
- final ttsResult = await ermine.waitFor(() async {
- print('Start recording audio.');
- await record.startOutputSave();
- await Future.delayed(_timeoutOneSec);
- playButton!.click();
-
- // Waits for the audio being played to the end.
- await Future.delayed(Duration(seconds: 5));
-
- await record.stopOutputSave();
- final audioOutput = await record.getOutputAudio();
- print('Stopped recording audio.');
-
- final ttsList =
- await speechToText(gcloud.speech, audioOutput.audioData, 'en-us');
- final tts = ttsList.first.toLowerCase();
- print('STT result: $tts');
- return tts == 'how old is obama';
- });
-
- expect(ttsResult, isTrue);
-
- gcloud.close();
-
- // Stops the local http server.
- await browser.requestData(stopUrl);
- await browser.waitUntilNoTransientCallbacks(timeout: _timeoutTenSec);
- await browser.waitFor(find.text(stopUrl), timeout: _timeoutTenSec);
-
- // Closes the flutter driver connected to the browser.
- await browser.close();
- await ermine.driver.requestData('close');
- await ermine.driver.waitForAbsent(find.text('simple-browser.cmx'));
- expect(await ermine.isStopped(simpleBrowserUrl), isTrue);
- print('Closed the browser');
- }, timeout: _timeoutPerTest, skip: skipTests[4]);
-
- test('Should be able to control the browser with keyboard shortcuts',
- () async {
- // Starts hosting a local http website.
- // ignore: unawaited_futures
- ermine.component.launch(testserverUrl);
- print('Launched the test server.');
-
- FlutterDriver browser;
- browser = await ermine.launchAndWaitForSimpleBrowser();
-
- // Opens index.html
- await input.text(indexUrl, keyEventDuration: Duration(milliseconds: 10));
- await input.keyPress(kEnterKey);
- await browser.waitUntilNoTransientCallbacks(timeout: _timeoutTenSec);
- await browser.waitFor(indexTabFinder, timeout: _timeoutTenSec);
-
- final webdriver =
- (await webDriverConnector.webDriversForHost('127.0.0.1')).single;
- expect(await browser.getText(newTabFinder), isNotNull);
- expect(await browser.getText(indexTabFinder), isNotNull);
- print('Opened $indexUrl');
-
- // Clicks the + buttons
- var digitLink = await _waitForWebElement(webdriver, By.id('target'));
- final addButton = await _waitForWebElement(webdriver, By.id('increase'));
- expect(digitLink!.text, '0');
- addButton!.click();
- await ermine.waitFor(() async {
- return digitLink!.text == '1';
- });
- addButton.click();
- await ermine.waitFor(() async {
- return digitLink!.text == '2';
- });
- print('Clicked the + button next to the digit three times');
-
- // Shortcut for refresh (Ctrl + r)
- await _invokeShortcut([Key.leftCtrl, Key.r]);
- digitLink = await _waitForWebElement(webdriver, By.id('target'));
- await ermine.waitFor(() async {
- return digitLink!.text == '0';
- });
- print('Refreshed the page');
-
- // Clicks the 'Next' link
- final nextLink = await _waitForWebElement(webdriver, By.linkText('Next'));
- expect(nextLink, isNotNull);
- expect(
- await _repeatActionWaitingForAbsent(
- browser, nextLink!.click, indexTabFinder),
- isTrue,
- reason: 'Failed to click the Next link.');
- expect(await browser.getText(newTabFinder), isNotNull);
- expect(await browser.getText(nextTabFinder), isNotNull);
- print('Clicked the next.html link');
-
- // Shortcut for backward (Alt + ←)
- expect(
- await _repeatActionWaitingForAbsent(
- browser,
- () async => await _invokeShortcut([Key.leftAlt, Key.left]),
- nextTabFinder),
- isTrue,
- reason: 'Failed to invoke the shortcut for navigating back.');
- expect(await browser.getText(newTabFinder), isNotNull);
- expect(await browser.getText(indexTabFinder), isNotNull);
- print('Navigated back to index.html');
-
- // Shortcut for forward (Alt + →)
- expect(
- await _repeatActionWaitingForAbsent(
- browser,
- () async => await _invokeShortcut([Key.leftAlt, Key.right]),
- nextTabFinder),
- isTrue,
- reason: 'Failed to invoke the shortcut for navigating forward.');
- expect(await browser.getText(newTabFinder), isNotNull);
- expect(await browser.getText(indexTabFinder), isNotNull);
- print('Navigated forward to next.html');
-
- // Shortcut for opening a new tab (Ctrl + t)
- await _invokeShortcut([Key.leftCtrl, Key.t]);
- await browser.waitFor(find.text(newTabHintText), timeout: _timeoutTenSec);
-
- // Opens blue.html
- const blueUrl = 'http://127.0.0.1:8080/blue.html';
- const nextUrl = 'http://127.0.0.1:8080/next.html';
- await input.text(blueUrl, keyEventDuration: Duration(milliseconds: 10));
- await input.keyPress(kEnterKey);
- await browser.waitUntilNoTransientCallbacks(timeout: _timeoutTenSec);
- await browser.waitFor(blueTabFinder, timeout: _timeoutTenSec);
- print('Opened blue.html');
-
- // Shortcut for selecting the next tab (Ctrl + Tab) x 2
- await _invokeShortcut([Key.leftCtrl, Key.tab]);
- await browser.waitFor(find.text(newTabHintText));
- expect(await browser.getText(find.text(newTabHintText)), isNotNull);
- print('The new tab is now selected');
-
- await _invokeShortcut([Key.leftCtrl, Key.tab]);
- await browser.waitFor(find.text(nextUrl));
- expect(await browser.getText(find.text(nextUrl)), isNotNull);
- print('The next tab is now selected');
-
- // Shortcut for selecting the previous tab (Ctrl + Shift +Tab) x 2
- await _invokeShortcut([Key.leftCtrl, Key.leftShift, Key.tab]);
- await browser.waitFor(find.text(newTabHintText));
- expect(await browser.getText(find.text(newTabHintText)), isNotNull);
- print('The new tab is now selected');
-
- await _invokeShortcut([Key.leftCtrl, Key.leftShift, Key.tab]);
- await browser.waitFor(find.text(blueUrl));
- expect(await browser.getText(find.text(blueUrl)), isNotNull);
- print('The blue tab is now selected');
-
- // Shortcut for closing a current tab (Ctrl + w)
- await _invokeShortcut([Key.leftCtrl, Key.w]);
- await browser.waitForAbsent(blueTabFinder);
- print('Closed the blue tab');
-
- expect(await browser.getText(newTabFinder), isNotNull);
- expect(await browser.getText(find.text(nextUrl)), isNotNull);
- print('The index tab is focused');
-
- // Stops the local http server.
- await browser.requestData(stopUrl);
- await browser.waitUntilNoTransientCallbacks(timeout: _timeoutTenSec);
- await browser.waitFor(find.text(stopUrl), timeout: _timeoutTenSec);
-
- // Closes the flutter driver connected to the browser.
- await browser.close();
- await ermine.threeKeyShortcut(Key.leftCtrl, Key.leftShift, Key.w);
- await ermine.driver.waitUntilNoTransientCallbacks(timeout: _timeoutTenSec);
- await ermine.waitForViewAbsent(simpleBrowserUrl);
- print('Closed the browser');
- }, timeout: _timeoutPerTest, skip: skipTests[5]);
-}
diff --git a/tests/e2e/test/ermine_shell_test.dart b/tests/e2e/test/ermine_shell_test.dart
index 097bca9..37c0175 100644
--- a/tests/e2e/test/ermine_shell_test.dart
+++ b/tests/e2e/test/ermine_shell_test.dart
@@ -63,14 +63,13 @@
// Launch terminal.
const terminalUrl = 'fuchsia-pkg://fuchsia.com/terminal#meta/terminal.cmx';
await ermine.launch(terminalUrl);
- await ermine.waitForView(terminalUrl);
+ await ermine.waitForView(terminalUrl, testForFocus: true);
// Launch spinning_square_view, it should have focus.
const spinningSquareViewUrl =
'fuchsia-pkg://fuchsia.com/spinning_square_view#meta/spinning_square_view.cmx';
await ermine.launch(spinningSquareViewUrl);
- var view = await ermine.waitForView(spinningSquareViewUrl);
- expect(view.focused, isTrue);
+ await ermine.waitForView(spinningSquareViewUrl, testForFocus: true);
// Tap on terminal to switch focus to it. Terminal view should be left half
// of the screen. [input.tap] assumes screen resolution as 1000 x 1000.
@@ -78,7 +77,6 @@
await ermine.driver.waitUntilNoTransientCallbacks();
// Terminal should now have focus.
- view = await ermine.waitForView(terminalUrl);
- expect(view.focused, isTrue);
+ await ermine.waitForView(terminalUrl, testForFocus: true);
}, skip: true);
}
diff --git a/tests/e2e/test/ermine_smoke_test.dart b/tests/e2e/test/ermine_smoke_test.dart
index f4a312a..fd74831 100644
--- a/tests/e2e/test/ermine_smoke_test.dart
+++ b/tests/e2e/test/ermine_smoke_test.dart
@@ -5,6 +5,7 @@
// ignore_for_file: import_of_legacy_library_into_null_safe
@Retry(2)
+@Timeout(Duration(minutes: 2))
import 'package:ermine_driver/ermine_driver.dart';
import 'package:fidl_fuchsia_input/fidl_async.dart';
@@ -28,7 +29,6 @@
});
tearDownAll(() async {
- // Any of these may end up being null if the test fails in setup.
await ermine.tearDown();
await sl4f.stopServer();
sl4f.close();
@@ -49,6 +49,7 @@
test('Text input, pointer input and keyboard shortcut', () async {
print('Launching terminal...');
final terminalFinder = find.text('Terminal');
+ await ermine.driver.waitUntilNoTransientCallbacks();
final appResult = await ermine.driver.getText(terminalFinder);
expect(appResult, 'Terminal');
@@ -66,6 +67,21 @@
print('Verifying Ctrl+Shift+w shortcut is closing terminal');
await ermine.threeKeyShortcut(Key.leftCtrl, Key.leftShift, Key.w);
await ermine.driver.waitUntilNoTransientCallbacks();
+ await ermine.waitForAction('close');
expect(await ermine.isStopped(terminalUrl), isTrue);
- });
+
+ // Get the current value of dark mode.
+ bool darkMode = (await ermine.snapshot).darkMode;
+
+ // Toggle it.
+ await ermine.driver.tap(find.byValueKey('darkMode'));
+ expect((await ermine.snapshot).darkMode, !darkMode);
+
+ // Logout from ermine.
+ print('Logging out and back in');
+ await ermine.logoutAndLogin();
+
+ // Dark mode toggle should have persisted across auth flows.
+ expect((await ermine.snapshot).darkMode, !darkMode);
+ }, timeout: Timeout(Duration(minutes: 2)));
}
diff --git a/tests/e2e/test/ermine_terminal_test.dart b/tests/e2e/test/ermine_terminal_test.dart
index 7df0a04..b0594a9 100644
--- a/tests/e2e/test/ermine_terminal_test.dart
+++ b/tests/e2e/test/ermine_terminal_test.dart
@@ -53,7 +53,10 @@
var views = await ermine.launchedViews(filterByUrl: componentUrl);
if (views.length == instances) {
if (testForFocus) {
- expect(views.any((view) => view.focused), isTrue);
+ // Wait for a view with focus.
+ if (!views.any((view) => view.focused)) {
+ return null;
+ }
}
return views;
}
diff --git a/tests/lib/ermine_driver.dart b/tests/lib/ermine_driver.dart
index 189385c..f277297 100644
--- a/tests/lib/ermine_driver.dart
+++ b/tests/lib/ermine_driver.dart
@@ -17,10 +17,14 @@
import 'package:sl4f/sl4f.dart';
import 'package:test/test.dart';
-const ermineUrl = 'fuchsia-pkg://fuchsia.com/ermine#meta/ermine.cmx';
const simpleBrowserUrl =
'fuchsia-pkg://fuchsia.com/simple-browser#meta/simple-browser.cmx';
const terminalUrl = 'fuchsia-pkg://fuchsia.com/terminal#meta/terminal.cmx';
+const stashCtlUrl = 'fuchsia-pkg://fuchsia.com/stash_ctl#meta/stash_ctl.cmx';
+const kLoginInspectSelector =
+ 'core/session-manager/session\\:session/workstation_session/login_shell';
+const kErmineInspectSelector =
+ 'core/session-manager/session\\:session/workstation_session/login_shell/ermine_shell';
// USB HID code for ENTER key.
// See <https://www.usb.org/sites/default/files/documents/hut1_12v2.pdf>
@@ -41,6 +45,7 @@
final Component _component;
FlutterDriver? _driver;
+ FlutterDriver? _login;
final FlutterDriverConnector _connector;
/// Constructor.
@@ -51,6 +56,9 @@
/// The instance of [FlutterDriver] that is connected to Ermine flutter app.
FlutterDriver get driver => _driver!;
+ /// The instance of [FlutterDriver] that is connected to Login flutter app.
+ FlutterDriver get login => _login!;
+
/// The instance of [Component] that is connected to the DUT.
Component get component => _component;
@@ -70,20 +78,17 @@
await _connector.initialize();
print('Flutter driver connector initialized');
- // Now connect to ermine.
- _driver = await _connector.driverForIsolate('ermine');
- if (_driver == null) {
- fail('Unable to connect to ermine.');
+ // Connect to login shell's flutter driver.
+ _login = await connectToFlutterDriver('login', kLoginInspectSelector,
+ (snapshot) => snapshot['ready'] == true);
+ if (_login == null) {
+ fail('Unable to connect to login shell.');
}
+ print('Connected to login shell');
+
+ // Now connect to ermine.
+ _driver = await authenticate();
print('Driver is connected to Ermine');
-
- // Wait for shell to draw first frame.
- await driver.waitUntilFirstFrameRasterized();
- print('The first frame has been rasterized');
-
- // Wait until rendering stabilizes and animations settle.
- await driver.waitUntilNoTransientCallbacks();
- print('No further transient callbacks. ErmineDriver is ready.');
}
/// Closes [FlutterDriverConnector] and performs cleanup.
@@ -279,8 +284,8 @@
// Connects to the browser.
// TODO(fxb/66577): Get the driver of the last isolate once it's supported by
// [FlutterDriverConnector] in flutter_driver_sl4f.dart
- final browserDriver =
- await browserConnector.driverForIsolate('simple-browser');
+ final browserDriver = await browserConnector.driverForIsolateBySelector(
+ 'simple-browser', 'simple-browser.cmx');
// ignore: unnecessary_null_comparison
if (browserDriver == null) {
fail('unable to connect to simple browser.');
@@ -333,17 +338,24 @@
Future<Rectangle> getViewRect(String viewUrl,
[Duration timeout = waitForTimeout]) async {
- final view = await waitForView(viewUrl, timeout);
+ final view = await waitForView(viewUrl, timeout: timeout);
return view.viewport;
}
/// Finds the first launched component given its [viewUrl] and returns it's
- /// Inspect data. Waits for [timeout] duration for view to launch.
+ /// Inspect data. Waits for [timeout] duration for view to launch. If
+ /// [testForFocus] is true, waits for focused signal.
Future<ViewSnapshot> waitForView(String viewUrl,
- [Duration timeout = waitForTimeout]) async {
+ {bool testForFocus = false, Duration timeout = waitForTimeout}) async {
return waitFor(() async {
final views = await launchedViews(filterByUrl: viewUrl);
- return views.isNotEmpty ? views.first : null;
+ if (views.isEmpty) {
+ return null;
+ }
+ if (testForFocus) {
+ return views.first.focused ? views.first : null;
+ }
+ return views.first;
}, timeout: timeout);
}
@@ -355,12 +367,19 @@
});
}
- Future<Map<String, dynamic>> inspectSnapshot(String componentSelector,
- {Duration timeout = waitForTimeout}) {
+ Future<Map<String, dynamic>> inspectSnapshot(
+ String componentSelector, {
+ Duration timeout = waitForTimeout,
+ bool predicate(Map<String, dynamic> snapshot)?,
+ }) {
return waitFor(() async {
final snapshot = await Inspect(sl4f).snapshotRoot(componentSelector);
+ // Supply a default predicate that simply returns true.
+ predicate ??= (_) => true;
// ignore: unnecessary_null_comparison
- return snapshot == null || snapshot.isEmpty ? null : snapshot;
+ return snapshot == null || snapshot.isEmpty || !predicate!(snapshot)
+ ? null
+ : snapshot;
}, timeout: timeout);
}
@@ -370,6 +389,17 @@
return ShellSnapshot(json.decode(data));
}
+ /// Returns the last keyboard shortcut action received by ermine shell.
+ Future<String> get lastAction async => (await snapshot).lastAction;
+
+ /// Waits for last action to match the supplied value.
+ Future<bool> waitForAction(String action,
+ {Duration timeout = waitForTimeout}) async {
+ return waitFor(() async {
+ return (await lastAction) == action;
+ }, timeout: timeout);
+ }
+
/// Returns the list of launched views from inspect data.
Future<List<ViewSnapshot>> get views async => (await snapshot).views;
@@ -517,6 +547,141 @@
// We ran out of time.
throw TimeoutException('waitFor timeout expired', timeout);
}
+
+ /// Connects to [FlutterDriver] for isolate [name] with [selector];
+ Future<FlutterDriver> connectToFlutterDriver(String name, String selector,
+ [bool predicate(Map<String, dynamic> snapshot)?]) async {
+ // Connect to Login flutter app.
+ print('Connecting to $name flutter isolate');
+ await inspectSnapshot(selector, predicate: predicate);
+ final driver = await _connector.driverForIsolateBySelector(name, selector);
+
+ // Wait for shell to draw first frame.
+ await driver.waitUntilFirstFrameRasterized();
+ print('The first frame has been rasterized');
+
+ // Wait until rendering stabilizes and animations settle.
+ await driver.waitUntilNoTransientCallbacks();
+ print('No further transient callbacks. $name is ready.');
+ return driver;
+ }
+
+ /// If the workstation build is enabled for authentication through the login
+ /// shell, go through the create password or login flow.
+ ///
+ /// Returns [true] is successfully performed authentication flows.
+ Future<FlutterDriver> authenticate() async {
+ // Check if login shell's inspect data is published and is 'ready'.
+ final snapshot = await inspectSnapshot(kLoginInspectSelector,
+ predicate: (snapshot) => snapshot['ready'] == true);
+
+ // Check if OOBE is skipped or shown.
+ if (snapshot['launchOOBE'] == true) {
+ // Check if auth status is authenticated. If no, start auth flow.
+ if (snapshot['authenticated'] != true) {
+ // Check if create password is visible.
+ print('Performing authentication...');
+
+ const testPassword = '11223344';
+ if (snapshot['screen'] == 'password') {
+ print('Creating password...');
+
+ // Tap on the first password text field.
+ var passwordTextField = find.byValueKey('password1');
+ await enterText(testPassword,
+ driver: login, textField: passwordTextField);
+ print('password 1 done');
+
+ // Tap on the second password text field.
+ passwordTextField = find.byValueKey('password2');
+ await enterText(testPassword,
+ driver: login, textField: passwordTextField);
+ print('password 2 done');
+
+ // Tap the Set Password button and wait for auth to succeed.
+ await login.tap(find.byValueKey('setPassword'));
+ await login.waitForAbsent(find.byType('Password'),
+ timeout: Duration(minutes: 2));
+ print('Password set');
+
+ // Tap through the 'Start Workstation' screen.
+ await login.tap(find.byValueKey('startWorkstation'));
+ await login.waitForAbsent(find.byType('Ready'));
+ } else {
+ print('Adding login credentials...');
+ // Tap on the password text field.
+ var passwordTextField = find.byValueKey('password');
+ await enterText(testPassword,
+ driver: login, textField: passwordTextField);
+ print('password entered');
+
+ // Tap the Login button and wait for auth to succeed.
+ await login.tap(find.byValueKey('login'));
+ await login.waitForAbsent(find.byType('Login'),
+ timeout: Duration(minutes: 2));
+ print('password entered');
+ }
+ }
+ }
+
+ print('Starting ermine shell');
+ await inspectSnapshot(kLoginInspectSelector,
+ predicate: (snapshot) => snapshot['ermineReady'] == true);
+ // We should land on the Ermine shell.
+ await login.waitUntilNoTransientCallbacks();
+ await login.waitFor(find.byType('ErmineApp'));
+
+ return connectToFlutterDriver('ermine', kErmineInspectSelector);
+ }
+
+ /// Performs a logout followed by login authentication flow.
+ ///
+ /// This is done in one flow to ensure ermine's flutter [_driver] is valid at
+ /// the end of the flow.
+ Future<void> logoutAndLogin() async {
+ // Ensure that ermine shell is running.
+ await login.waitFor(find.byType('ErmineApp'));
+ await inspectSnapshot(kErmineInspectSelector);
+ await driver.waitUntilNoTransientCallbacks();
+
+ // Send logout action
+ await driver.requestData('logout');
+ await driver.waitUntilNoTransientCallbacks();
+
+ // Tap 'Log out' button to confirm logout dialog, but don't wait for it to
+ // complete because ermine is killed.
+ // ignore: unawaited_futures
+ driver.tap(find.text('LOGOUT')).catchError((_) {});
+ // ignore: unawaited_futures
+ driver.close();
+
+ // If launchOOBE is true, wait for the login screen.
+ final snapshot = await inspectSnapshot(kLoginInspectSelector,
+ predicate: (snapshot) => snapshot['ready'] == true);
+ if (snapshot['launchOOBE'] == true) {
+ await login.waitUntilNoTransientCallbacks();
+ await login.waitFor(find.byType('Login'));
+ }
+
+ // Now log back in and set the new flutter driver connection to ermine.
+ _driver = await authenticate();
+ }
+
+ /// Enters text using [Input] to [textField] using [driver].
+ Future<void> enterText(
+ String text, {
+ required FlutterDriver driver,
+ required SerializableFinder textField,
+ }) async {
+ final input = Input(sl4f);
+ // Tap on the text field.
+ await driver.tap(textField);
+ await input.text(text);
+ // Wait for input to make to the text field.
+ await waitFor(() async {
+ return (await driver.getText(textField)) == text;
+ });
+ }
}
/// Holds Ermine shell state which is derived from inspect data.
@@ -529,6 +694,8 @@
bool get appBarVisible => inspectData['appBarVisible'] == true;
bool get sideBarVisible => inspectData['sideBarVisible'] == true;
bool get overlaysVisible => inspectData['overlaysVisible'] == true;
+ String get lastAction => inspectData['lastAction'] ?? '';
+ bool get darkMode => inspectData['darkMode'] ?? true;
ViewSnapshot? get activeView =>
numViews > 0 ? views[inspectData['activeView'] ?? 0] : null;
diff --git a/tests/lib/simple_browser_driver.dart b/tests/lib/simple_browser_driver.dart
deleted file mode 100644
index f32a6d6..0000000
--- a/tests/lib/simple_browser_driver.dart
+++ /dev/null
@@ -1,98 +0,0 @@
-// 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:async';
-
-// ignore_for_file: import_of_legacy_library_into_null_safe
-
-import 'package:ermine_driver/ermine_driver.dart';
-import 'package:flutter_driver/flutter_driver.dart';
-import 'package:flutter_driver_sl4f/flutter_driver_sl4f.dart';
-import 'package:test/test.dart';
-
-class SimpleBrowserDriver {
- final ErmineDriver _ermine;
- final FlutterDriverConnector _connector;
- FlutterDriver? _browser;
-
- FlutterDriver get driver => _browser!;
-
- SimpleBrowserDriver(this._ermine)
- : _connector = FlutterDriverConnector(_ermine.sl4f);
-
- /// Launches a simple browser and returns a [FlutterDriver] connected to it.
- Future<void> launchSimpleBrowser() async {
- expect(await _ermine.launch(simpleBrowserUrl), isTrue);
- print('Launched a browser');
-
- // Initializes the browser's flutter driver connector.
- await _connector.initialize();
- print('Initialized a flutter driver connector for the browser.');
-
- // Checks if Simple Browser is running.
- // TODO(fxb/66577): Get the last isolate once it's supported by
- // [FlutterDriverConnector] in flutter_driver_sl4f.dart
- final browserIsolate = await _connector.isolate('simple-browser');
- // ignore: unnecessary_null_comparison
- if (browserIsolate == null) {
- fail('couldn\'t find simple browser.');
- }
- print('Checked that the browser is running.');
-
- // Connects to the browser.
- // TODO(fxb/66577): Get the driver of the last isolate once it's supported by
- // [FlutterDriverConnector] in flutter_driver_sl4f.dart
- _browser = await _connector.driverForIsolate('simple-browser');
- // ignore: unnecessary_null_comparison
- if (_browser == null) {
- fail('unable to connect to simple browser.');
- }
- print('Connected the browser to a flutter driver.');
- }
-
- /// Launches a simple browser and sets up options for test convenience.
- ///
- /// Opens another new tab as soon as the browser is launched, unless you set
- /// [openNewTab] to false. Contrarily, set [fullscreen] to true if you want
- /// the browser to expand its size to full-screen upon its launch.
- /// Also, you can set the text entry emulation of the browser's flutter driver
- /// using [enableTextEntryEmulation], which has false by default.
- Future<void> launchAndWaitForSimpleBrowser({
- bool openNewTab = true,
- bool enableTextEntryEmulation = false,
- }) async {
- await launchSimpleBrowser();
-
- if (_browser != null) {
- // Set the flutter driver's text entry emulation.
- await _browser!.setTextEntryEmulation(enabled: enableTextEntryEmulation);
- print('Text entry emulation is enabled for the browser.');
-
- // Opens another tab other than the tab opened on browser's launch,
- // if required.
- if (openNewTab) {
- final addTab = find.byValueKey('new_tab');
- await _browser!.waitFor(addTab);
-
- await _browser!.tap(addTab);
- await _browser!
- .waitFor(find.text('NEW TAB'), timeout: Duration(seconds: 10));
- print('Opened a new tab');
- } else {
- await _browser!
- .waitFor(find.text(' SEARCH'), timeout: Duration(seconds: 10));
- print('The first tab is ready.');
- }
-
- await _browser!.waitUntilFirstFrameRasterized();
- await _browser!.waitUntilNoTransientCallbacks();
- print('No further transient callbacks.');
- }
- }
-
- Future<void> tearDown() async {
- await _browser?.close();
- await _connector.tearDown();
- }
-}