[simple_browser] Add simple browser
TESTED=ran on device
Change-Id: I1222a7e1ae38368316cbf0172a354f0c572575f1
diff --git a/bin/simple_browser/BUILD.gn b/bin/simple_browser/BUILD.gn
new file mode 100644
index 0000000..cfce1b5
--- /dev/null
+++ b/bin/simple_browser/BUILD.gn
@@ -0,0 +1,33 @@
+# 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/fidl/fidl.gni")
+import("//topaz/runtime/flutter_runner/flutter_app.gni")
+
+flutter_app("simple_browser") {
+ main_dart = "lib/main.dart"
+
+ meta = [
+ {
+ path = rebase_path("meta/simple_browser.cmx")
+ dest = "simple_browser.cmx"
+ },
+ ]
+
+ package_name = "simple_browser"
+
+ manifest = "pubspec.yaml"
+
+ deps = [
+ "//third_party/dart-pkg/git/flutter/packages/flutter",
+ "//third_party/dart-pkg/pub/html_unescape",
+ "//third_party/dart-pkg/pub/http",
+ "//topaz/public/dart/fuchsia_logger",
+ "//topaz/public/dart/fuchsia_modular",
+ "//topaz/public/dart/fuchsia_scenic_flutter",
+ "//topaz/public/dart/widgets:lib.widgets",
+ "//topaz/runtime/chromium:chromium.web",
+ "//topaz/public/lib/webview",
+ ]
+}
diff --git a/bin/simple_browser/OWNERS b/bin/simple_browser/OWNERS
new file mode 100644
index 0000000..3b6fdfc
--- /dev/null
+++ b/bin/simple_browser/OWNERS
@@ -0,0 +1,4 @@
+set noparent
+
+ahetzroni@google.com
+miguelfrde@google.com
diff --git a/bin/simple_browser/README.md b/bin/simple_browser/README.md
new file mode 100644
index 0000000..c668ae9
--- /dev/null
+++ b/bin/simple_browser/README.md
@@ -0,0 +1,4 @@
+# Browser
+
+Minimal traditional web browser with navigation and a text field for
+entering the URL.
diff --git a/bin/simple_browser/analysis_options.yaml b/bin/simple_browser/analysis_options.yaml
new file mode 100644
index 0000000..73da07f
--- /dev/null
+++ b/bin/simple_browser/analysis_options.yaml
@@ -0,0 +1,5 @@
+# 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.
+
+include: ../analysis_options.yaml
diff --git a/bin/simple_browser/lib/app.dart b/bin/simple_browser/lib/app.dart
new file mode 100644
index 0000000..b027e39
--- /dev/null
+++ b/bin/simple_browser/lib/app.dart
@@ -0,0 +1,56 @@
+// 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:fuchsia_services/services.dart';
+import 'package:fuchsia_scenic_flutter/child_view.dart' show ChildView;
+import 'package:webview/webview.dart';
+import 'src/blocs/browser_bloc.dart';
+import 'src/widgets/navigation_bar.dart';
+
+class App extends StatefulWidget {
+ @override
+ State<App> createState() => AppState();
+}
+
+class AppState extends State<App> {
+ BrowserBloc _browserBloc;
+ ChromiumWebView _webView;
+
+ AppState() {
+ _webView = ChromiumWebView(
+ StartupContext.fromStartupInfo().environmentServices,
+ );
+ _browserBloc = BrowserBloc(webView: _webView);
+ }
+
+ @override
+ void dispose() {
+ _browserBloc.dispose();
+ _webView.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ title: 'Browser',
+ home: Scaffold(
+ backgroundColor: Colors.grey,
+ body: Container(
+ child: Column(
+ children: <Widget>[
+ NavigationBar(bloc: _browserBloc),
+ Expanded(
+ child: ChildView(
+ connection: _webView.childViewConnection,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/bin/simple_browser/lib/main.dart b/bin/simple_browser/lib/main.dart
new file mode 100644
index 0000000..1136e4e
--- /dev/null
+++ b/bin/simple_browser/lib/main.dart
@@ -0,0 +1,13 @@
+// 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:fuchsia_logger/logger.dart';
+
+import 'app.dart';
+
+void main() {
+ setupLogger(name: 'Browser');
+ runApp(App());
+}
diff --git a/bin/simple_browser/lib/src/blocs/browser_bloc.dart b/bin/simple_browser/lib/src/blocs/browser_bloc.dart
new file mode 100644
index 0000000..507ae47
--- /dev/null
+++ b/bin/simple_browser/lib/src/blocs/browser_bloc.dart
@@ -0,0 +1,135 @@
+// 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:io';
+import 'dart:math';
+import 'package:fuchsia_logger/logger.dart';
+import 'package:fidl_chromium_web/fidl_async.dart' as web;
+import 'package:meta/meta.dart';
+import 'package:webview/webview.dart';
+import '../models/browse_action.dart';
+
+// Business logic for the browser.
+// Sinks:
+// BrowseAction: a browsing action - url request, prev/next page, etc.
+// Streams:
+// Url: streams the url in case of an in page navigation.
+class BrowserBloc extends web.NavigationEventObserver {
+ final ChromiumWebView webView;
+
+ // State storage for browsing with back/forward buttons.
+ // Stores the browsing history.
+ final _urlList = <String>[];
+ // Stores the current location of the browser WRT history.
+ int _urlListHead = -1;
+ // True when the most recent navigation used back/forward buttons, false
+ // otherwise.
+ bool _navigationOngoing = false;
+
+ // Streams
+ final _urlController = StreamController<String>.broadcast();
+ Stream<String> get url => _urlController.stream;
+ final _forwardController = StreamController<bool>.broadcast();
+ Stream<bool> get forwardState => _forwardController.stream;
+ final _backController = StreamController<bool>.broadcast();
+ Stream<bool> get backState => _backController.stream;
+
+ // Sinks
+ final _browseActionController = StreamController<BrowseAction>();
+ Sink<BrowseAction> get request => _browseActionController.sink;
+
+ BrowserBloc({
+ @required this.webView,
+ String homePage,
+ }) : assert(webView != null) {
+ webView.setNavigationEventObserver(this);
+
+ if (homePage != null) {
+ _handleAction(NavigateToAction(url: homePage));
+ }
+ _browseActionController.stream.listen(_handleAction);
+ }
+
+ @override
+ Future<Null> onNavigationStateChanged(web.NavigationEvent event) async {
+ log.info('url loaded: ${event.url}');
+ if (!_navigationOngoing) {
+ // The event was triggered by a navigation inside the page.
+ await _addUrl(event.url);
+ }
+ _navigationOngoing = false;
+ }
+
+ Future<void> _handleAction(BrowseAction action) async {
+ switch (action.op) {
+ case BrowseActionType.navigateTo:
+ final NavigateToAction navigate = action;
+ _navigationOngoing = true;
+ await _addUrl(navigate.url, needsRedirect: true);
+ await webView.controller.loadUrl(
+ navigate.url, web.LoadUrlParams(type: web.LoadUrlReason.typed));
+ break;
+ case BrowseActionType.goBack:
+ _navigateInHistory(delta: -1);
+ _navigationOngoing = true;
+ await webView.controller.goBack();
+ break;
+ case BrowseActionType.goForward:
+ _navigateInHistory(delta: 1);
+ _navigationOngoing = true;
+ await webView.controller.goForward();
+ break;
+ }
+ }
+
+ void _navigateInHistory({@required delta}) {
+ final state = _urlListHead;
+ final newStateIndex = max(0, min(state + delta, _urlList.length - 1));
+ final oldUrl = state == -1 ? null : _urlList[state],
+ newUrl = _urlList[newStateIndex];
+ _urlListHead = newStateIndex;
+ _updateControllers();
+ _notifyNavigationUpdate(oldUrl, newUrl);
+ }
+
+ Future<Null> _addUrl(String newUrl, {bool needsRedirect = false}) async {
+ var url = newUrl;
+ if (needsRedirect) {
+ url = await _followRedirects(newUrl);
+ }
+ _urlList
+ ..removeRange(_urlListHead + 1, _urlList.length)
+ ..add(url);
+ _navigateInHistory(delta: 1);
+ }
+
+ void _updateControllers() {
+ _forwardController.add(_urlListHead < _urlList.length - 1);
+ _backController.add(_urlListHead > 0);
+ }
+
+ void _notifyNavigationUpdate(String oldUrl, String newUrl) {
+ _urlController.add(newUrl);
+ }
+
+ Future<String> _followRedirects(String url) async {
+ final request = await HttpClient().getUrl(Uri.parse(url));
+ request.followRedirects = true;
+ final response = await request.close();
+ final uri = response.redirects
+ .map((r) => r.location)
+ .toList()
+ .reversed
+ .firstWhere((url) => url.isAbsolute, orElse: () => null);
+ return uri?.toString() ?? url;
+ }
+
+ void dispose() {
+ _urlController.close();
+ _browseActionController.close();
+ _forwardController.close();
+ _backController.close();
+ }
+}
diff --git a/bin/simple_browser/lib/src/models/browse_action.dart b/bin/simple_browser/lib/src/models/browse_action.dart
new file mode 100644
index 0000000..c07770b
--- /dev/null
+++ b/bin/simple_browser/lib/src/models/browse_action.dart
@@ -0,0 +1,30 @@
+// 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:meta/meta.dart';
+
+// Base class for actions handled by the application's BLOC
+class BrowseAction {
+ final BrowseActionType op;
+ const BrowseAction(this.op);
+}
+
+// Operations allowed for browsing
+enum BrowseActionType { goForward, goBack, navigateTo }
+
+// Instructs to go to the next page.
+class GoForwardAction extends BrowseAction {
+ const GoForwardAction() : super(BrowseActionType.goForward);
+}
+
+// Instructs to go to the previous page.
+class GoBackAction extends BrowseAction {
+ const GoBackAction() : super(BrowseActionType.goBack);
+}
+
+// Instructs to navigate to some url.
+class NavigateToAction extends BrowseAction {
+ final String url;
+ NavigateToAction({@required this.url}) : super(BrowseActionType.navigateTo);
+}
diff --git a/bin/simple_browser/lib/src/widgets/navigation_bar.dart b/bin/simple_browser/lib/src/widgets/navigation_bar.dart
new file mode 100644
index 0000000..78daa70
--- /dev/null
+++ b/bin/simple_browser/lib/src/widgets/navigation_bar.dart
@@ -0,0 +1,99 @@
+// 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/browser_bloc.dart';
+import '../models/browse_action.dart';
+
+class NavigationBar extends StatelessWidget {
+ final BrowserBloc bloc;
+
+ const NavigationBar({this.bloc});
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: EdgeInsets.all(8.0),
+ child: Row(
+ children: [
+ Padding(
+ padding: EdgeInsets.only(right: 8.0),
+ child: StreamBuilder<bool>(
+ stream: bloc.backState,
+ initialData: false,
+ builder: (context, snapshot) => RaisedButton(
+ padding: EdgeInsets.all(4),
+ child: const Text('BCK'),
+ color: Colors.grey[350],
+ disabledColor: Colors.grey[700],
+ onPressed: snapshot.data
+ ? () => bloc.request.add(GoBackAction())
+ : null,
+ ),
+ ),
+ ),
+ Padding(
+ padding: EdgeInsets.only(right: 8.0),
+ child: StreamBuilder<bool>(
+ stream: bloc.forwardState,
+ initialData: false,
+ builder: (context, snapshot) => RaisedButton(
+ padding: EdgeInsets.all(4),
+ child: const Text('FWD'),
+ color: Colors.grey[350],
+ disabledColor: Colors.grey[700],
+ onPressed: snapshot.data
+ ? () => bloc.request.add(GoForwardAction())
+ : null,
+ ),
+ ),
+ ),
+ NavigationBox(bloc: bloc),
+ ],
+ ));
+ }
+}
+
+class NavigationBox extends StatefulWidget {
+ final BrowserBloc bloc;
+
+ const NavigationBox({this.bloc});
+
+ @override
+ NavigationBoxState createState() => NavigationBoxState(bloc);
+}
+
+class NavigationBoxState extends State<NavigationBox> {
+ final TextEditingController _controller;
+
+ NavigationBoxState(BrowserBloc bloc) : _controller = TextEditingController() {
+ bloc.url.listen((url) {
+ _controller.text = url;
+ });
+ }
+
+ @override
+ void dispose() {
+ _controller.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Expanded(
+ child: TextField(
+ controller: _controller,
+ decoration: InputDecoration(
+ filled: true,
+ border: InputBorder.none,
+ fillColor: Colors.white,
+ hintText: 'Enter an address...',
+ ),
+ onSubmitted: (value) =>
+ widget.bloc.request.add(NavigateToAction(url: value)),
+ textInputAction: TextInputAction.go,
+ ),
+ );
+ }
+}
diff --git a/bin/simple_browser/meta/simple_browser.cmx b/bin/simple_browser/meta/simple_browser.cmx
new file mode 100644
index 0000000..e9f51bb
--- /dev/null
+++ b/bin/simple_browser/meta/simple_browser.cmx
@@ -0,0 +1,37 @@
+{
+ "facets": {
+ "fuchsia.modular": {
+ "@version": 2,
+ "binary": "simple_browser",
+ "intent_filters": []
+ }
+ },
+ "program": {
+ "data": "data/simple_browser"
+ },
+ "sandbox": {
+ "services": [
+ "chromium.web.ContextProvider",
+ "fuchsia.cobalt.LoggerFactory",
+ "fuchsia.fonts.Provider",
+ "fuchsia.logger.LogSink",
+ "fuchsia.modular.Clipboard",
+ "fuchsia.modular.ContextWriter",
+ "fuchsia.modular.ModuleContext",
+ "fuchsia.net.SocketProvider",
+ "fuchsia.netstack.Netstack",
+ "fuchsia.process.Launcher",
+ "fuchsia.sys.Environment",
+ "fuchsia.sys.Launcher",
+ "fuchsia.ui.input.ImeService",
+ "fuchsia.ui.input.ImeVisibilityService",
+ "fuchsia.ui.scenic.Scenic",
+ "fuchsia.ui.policy.Presenter",
+ "fuchsia.ui.viewsv1.ViewManager",
+ "fuchsia.modular.ComponentContext"
+ ],
+ "system": [
+ "data/modules"
+ ]
+ }
+}
diff --git a/bin/simple_browser/pubspec.yaml b/bin/simple_browser/pubspec.yaml
new file mode 100644
index 0000000..e4eaad8
--- /dev/null
+++ b/bin/simple_browser/pubspec.yaml
@@ -0,0 +1,8 @@
+# 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:
diff --git a/packages/prod/BUILD.gn b/packages/prod/BUILD.gn
index 8017175..9cddb5e 100644
--- a/packages/prod/BUILD.gn
+++ b/packages/prod/BUILD.gn
@@ -270,6 +270,14 @@
]
}
+group("simple_browser") {
+ testonly = true
+ public_deps = [
+ "//topaz/bin/simple_browser:simple_browser",
+ "//topaz/public/lib/webview:webview",
+ ]
+}
+
group("cast_runner") {
testonly = true
public_deps = [