blob: c8508256f9aa7793c736aaddb6bd7ca3fb15421e [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 'dart:io';
import 'dart:math';
import 'package:fuchsia_logger/logger.dart';
import 'package:fidl_fuchsia_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.NavigationEventListener {
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.setNavigationEventListener(this);
if (homePage != null) {
_handleAction(NavigateToAction(url: homePage));
}
_browseActionController.stream.listen(_handleAction);
}
@override
Future<Null> onNavigationStateChanged(web.NavigationState 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();
}
}