blob: 212ab652aa2d6e43dbe7acb84927f1548847681a [file] [log] [blame]
// 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:flutter/foundation.dart';
import 'package:fuchsia_logger/logger.dart';
import 'package:fuchsia_scenic_flutter/child_view.dart'
show ChildViewConnection;
import 'package:fidl_fuchsia_ui_views/fidl_async.dart' as views;
import 'package:fidl_fuchsia_web/fidl_async.dart' as web;
import 'package:zircon/zircon.dart';
import '../models/webpage_action.dart';
import '../utils/sanitize_url.dart';
enum PageType { empty, normal, error }
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;
}
}
// 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 extends web.NavigationEventListener {
/// Used to present webpage in Flutter ChildView
ChildViewConnection get childViewConnection => _childViewConnection;
ChildViewConnection _childViewConnection;
final web.FrameProxy _frame;
final _navigationController = web.NavigationControllerProxy();
final _navigationEventObserverBinding = web.NavigationEventListenerBinding();
final _popupFrameCreationObserverBinding =
web.PopupFrameCreationListenerBinding();
// 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 ?? PageType.empty;
// Sinks
final _webPageActionController = StreamController<WebPageAction>();
Sink<WebPageAction> get request => _webPageActionController.sink;
// Constructors
/// Create a new [WebPageBloc] with a new page from [context]
WebPageBloc({
String homePage,
web.ContextProxy context,
void Function(WebPageBloc popup) popupHandler,
}) : _frame = web.FrameProxy() {
context.createFrame(_frame.ctrl.request());
_setup(popupHandler: popupHandler);
if (homePage != null) {
_handleAction(NavigateToAction(url: homePage));
}
}
/// Create a new [WebPageBloc] with [frame]
WebPageBloc.withFrame({
@required web.FrameProxy frame,
void Function(WebPageBloc popup) popupHandler,
}) : _frame = frame {
_setup(popupHandler: popupHandler);
}
/// Common setup for all constructors
void _setup({
void Function(WebPageBloc popup) popupHandler,
}) {
_frame
// setup listeners
..setNavigationEventListener(_navigationEventObserverBinding.wrap(this))
..setPopupFrameCreationListener(
_popupFrameCreationObserverBinding.wrap(_PopupListener(popupHandler)))
// attach navigation controller
..getNavigationController(_navigationController.ctrl.request());
// Create a token pair for the newly-created View.
final tokenPair = EventPairPair();
assert(tokenPair.status == ZX.OK);
final viewHolderToken = views.ViewHolderToken(value: tokenPair.first);
final viewToken = views.ViewToken(value: tokenPair.second);
_frame.createView(viewToken);
_childViewConnection = ChildViewConnection(viewHolderToken);
// begin handling action requests
_webPageActionController.stream.listen(_handleAction);
}
void dispose() {
_navigationController.ctrl.close();
_frame.ctrl.close();
_webPageActionController.close();
}
@override
Future<Null> onNavigationStateChanged(web.NavigationState event) async {
if (event.url != null) {
log.info('url loaded: ${event.url}');
_url.value = event.url;
}
if (event.canGoForward != null) {
_forwardState.value = event.canGoForward;
}
if (event.canGoBack != null) {
_backState.value = event.canGoBack;
}
if (event.isMainDocumentLoaded != null) {
_isLoadedState.value = event.isMainDocumentLoaded;
}
if (event.title != null) {
_pageTitle.value = event.title;
}
if (event.pageType != null) {
_pageType.value = pageTypeForWebPageType(event.pageType);
}
}
Future<void> _handleAction(WebPageAction action) async {
switch (action.op) {
case WebPageActionType.navigateTo:
final NavigateToAction navigate = action;
await _navigationController.loadUrl(
sanitizeUrl(navigate.url),
web.LoadUrlParams(type: web.LoadUrlReason.typed),
);
break;
case WebPageActionType.goBack:
await _navigationController.goBack();
break;
case WebPageActionType.goForward:
await _navigationController.goForward();
break;
case WebPageActionType.refresh:
await _navigationController.reload(web.ReloadType.partialCache);
}
}
}
class _PopupListener extends web.PopupFrameCreationListener {
final void Function(WebPageBloc popup) _handler;
_PopupListener(this._handler);
@override
Future<void> onPopupFrameCreated(
InterfaceHandle<web.Frame> frame,
web.PopupFrameCreationInfo info,
) async {
_handler(
WebPageBloc.withFrame(
frame: web.FrameProxy()..ctrl.bind(frame),
popupHandler: _handler,
),
);
}
}