196 lines
6.4 KiB
Dart
196 lines
6.4 KiB
Dart
/*
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
*/
|
|
|
|
import 'dart:async';
|
|
import 'dart:developer' as developer;
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter/scheduler.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_mozilla_components/src/domain/services/gecko_browser.dart';
|
|
|
|
class GeckoView extends StatefulWidget {
|
|
/// Whether the native container backing this platform view is attached to the
|
|
/// window, as reported by `NativeFragmentView`.
|
|
///
|
|
/// The browser fragment can only be attached while this holds `true`, and the
|
|
/// container is only inserted into the Flutter view hierarchy once the
|
|
/// platform-view layer is first composited — which an ancestor that lays the
|
|
/// view out without painting it (`Offstage`) defers for as long as it stays
|
|
/// offstage. Every attach attempt is therefore driven by this stream rather
|
|
/// than by a single burst of retries after creation, which would otherwise
|
|
/// expire while the container is still unreachable and never run again.
|
|
/// See https://github.com/FaFre/WebLibre/issues/557.
|
|
final Stream<bool> viewReadyEvents;
|
|
|
|
final Future<void> Function()? postInitializationStep;
|
|
|
|
const GeckoView({
|
|
super.key,
|
|
required this.viewReadyEvents,
|
|
this.postInitializationStep,
|
|
});
|
|
|
|
@override
|
|
State<GeckoView> createState() => _GeckoViewState();
|
|
}
|
|
|
|
class _GeckoViewState extends State<GeckoView> {
|
|
static const platform = MethodChannel(
|
|
'eu.weblibre.flutter_mozilla_components/trim_memory',
|
|
);
|
|
|
|
final browserService = GeckoBrowserService();
|
|
late final AppLifecycleListener _listener;
|
|
StreamSubscription<bool>? _viewReadySubscription;
|
|
|
|
/// Serialises attach attempts.
|
|
///
|
|
/// The container can be reported attached while an earlier attempt is still
|
|
/// retrying, and two concurrent attempts would both find no usable fragment
|
|
/// and race to replace each other's.
|
|
Future<void> _attachQueue = Future<void>.value();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_setupMethodCallHandler();
|
|
_listener = AppLifecycleListener(
|
|
onResume: () {
|
|
//Make sure fragment visible after resuming the app in case native resources have been disposed
|
|
unawaited(_enqueueShowNativeFragment());
|
|
},
|
|
);
|
|
|
|
_viewReadySubscription = widget.viewReadyEvents
|
|
.where((ready) => ready)
|
|
.listen((_) => unawaited(_enqueueShowNativeFragment()));
|
|
}
|
|
|
|
/// Queues an attach attempt behind any that is still running.
|
|
///
|
|
/// Returns when this attempt is done, so callers that need to sequence work
|
|
/// after it can await it; failures are contained so one bad attempt cannot
|
|
/// poison the queue for the ones the ready stream triggers later.
|
|
Future<void> _enqueueShowNativeFragment() {
|
|
final attempt = _attachQueue.then((_) async {
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await _showNativeFragment();
|
|
} catch (error, stackTrace) {
|
|
developer.log(
|
|
'Fragment attach attempt failed',
|
|
name: 'GeckoView',
|
|
level: 900,
|
|
error: error,
|
|
stackTrace: stackTrace,
|
|
);
|
|
}
|
|
});
|
|
|
|
_attachQueue = attempt;
|
|
|
|
return attempt;
|
|
}
|
|
|
|
void _setupMethodCallHandler() {
|
|
platform.setMethodCallHandler((MethodCall call) async {
|
|
if (call.method == 'onTrimMemory') {
|
|
await browserService.onTrimMemory(call.arguments as int);
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Attaches the browser fragment to the native container.
|
|
///
|
|
/// The retries cover the transient reasons an attach can fail once the
|
|
/// container is reachable — a saved fragment-manager state, a frame in which
|
|
/// the fragment's view has no size yet. They deliberately do *not* cover
|
|
/// waiting for the container to appear in the first place: that wait is
|
|
/// unbounded, and [viewReadyEvents] reports it instead.
|
|
Future<bool> _showNativeFragment({
|
|
int maxRetries = 10,
|
|
|
|
/// Default ist about one frame
|
|
Duration retryDelay = const Duration(milliseconds: 1000 ~/ 60),
|
|
}) async {
|
|
for (int attempt = 0; attempt < maxRetries; attempt++) {
|
|
final result = await browserService.showNativeFragment();
|
|
|
|
if (result) {
|
|
developer.log(
|
|
'Fragment ATTACHED after $attempt tries',
|
|
name: 'GeckoView',
|
|
);
|
|
return true;
|
|
}
|
|
|
|
if (attempt < maxRetries - 1) {
|
|
await Future.delayed(retryDelay);
|
|
}
|
|
}
|
|
|
|
developer.log(
|
|
'Fragment FAILED after $maxRetries tries',
|
|
name: 'GeckoView',
|
|
level: 900,
|
|
);
|
|
return false;
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
platform.setMethodCallHandler(null);
|
|
unawaited(_viewReadySubscription?.cancel());
|
|
_listener.dispose();
|
|
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return PlatformViewLink(
|
|
viewType: 'eu.weblibre/gecko',
|
|
surfaceFactory: (context, controller) {
|
|
return AndroidViewSurface(
|
|
controller: controller as AndroidViewController,
|
|
gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
|
|
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
|
|
);
|
|
},
|
|
onCreatePlatformView: (PlatformViewCreationParams params) {
|
|
return PlatformViewsService.initExpensiveAndroidView(
|
|
id: params.id,
|
|
viewType: 'eu.weblibre/gecko',
|
|
layoutDirection: TextDirection.ltr,
|
|
creationParams: {},
|
|
creationParamsCodec: const StandardMessageCodec(),
|
|
)
|
|
..addOnPlatformViewCreatedListener((value) {
|
|
params.onPlatformViewCreated(value);
|
|
|
|
SchedulerBinding.instance.addPostFrameCallback((_) async {
|
|
// A first attempt for the common case where the view is painted
|
|
// from the frame it is created in, so the container is already
|
|
// attached by now. When it is not, this attempt is cheap and the
|
|
// ready subscription takes over as soon as it becomes attached.
|
|
await _enqueueShowNativeFragment();
|
|
await widget.postInitializationStep?.call();
|
|
});
|
|
})
|
|
// ignore: discarded_futures that hos it is done in docs
|
|
..create();
|
|
},
|
|
);
|
|
}
|
|
}
|