Skip to content

Commit bb01903

Browse files
committed
fix(flutter): apply runtime capability changes
1 parent 01472f1 commit bb01903

5 files changed

Lines changed: 71 additions & 2 deletions

File tree

sdks/flutter/packages/tugboat/lib/src/lifecycle.dart

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ class TugboatLifecycleNotifier extends ChangeNotifier {
4747

4848
void deactivate() {
4949
if (_state == TugboatLifecycleState.dormant &&
50-
_captureOverride == false &&
5150
_activationRequestId == null) {
5251
return;
5352
}
@@ -91,6 +90,16 @@ class TugboatLifecycleNotifier extends ChangeNotifier {
9190
notifyListeners();
9291
}
9392

93+
/// Advances the session epoch while the gate rebuilds for changed grants.
94+
///
95+
/// The gate is already rebuilding, so [markActive] sends the next lifecycle
96+
/// notification after the replacement session starts.
97+
int beginCapabilityRemount() {
98+
_requestEpoch += 1;
99+
_state = TugboatLifecycleState.starting;
100+
return _requestEpoch;
101+
}
102+
94103
/// Called by the gate when capture machinery is fully up.
95104
void markActive(int epoch) {
96105
if (epoch != _requestEpoch) return;

sdks/flutter/packages/tugboat/lib/src/replay_config.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ TugboatViewportSemanticPolicy resolveViewportSemanticPolicy({
4747
engineEnabled: true,
4848
emitEvents: shouldEmitEvents,
4949
debugLogs: debugLogs,
50-
holdPersistentSemanticsHandle: emitEvents,
50+
holdPersistentSemanticsHandle: shouldEmitEvents,
5151
);
5252
}
5353

sdks/flutter/packages/tugboat/lib/src/tugboat.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,9 +367,21 @@ class _TugboatActivationGateState extends State<_TugboatActivationGate> {
367367
super.didUpdateWidget(oldWidget);
368368
if (oldWidget.config.enabled != widget.config.enabled) {
369369
_syncCaptureFlag();
370+
} else if (_captureMounted &&
371+
_capabilitiesChanged(oldWidget.config, widget.config)) {
372+
_mountedEpoch = TugboatReplay._lifecycle.beginCapabilityRemount();
370373
}
371374
}
372375

376+
bool _capabilitiesChanged(
377+
TugboatReplayConfig previous,
378+
TugboatReplayConfig current,
379+
) =>
380+
previous.emitSceneInventory != current.emitSceneInventory ||
381+
previous.emitViewportSemanticMap != current.emitViewportSemanticMap ||
382+
previous.emitCaptureDiagnostics != current.emitCaptureDiagnostics ||
383+
previous.acceptActionContext != current.acceptActionContext;
384+
373385
@override
374386
void dispose() {
375387
TugboatReplay._lifecycle.removeListener(_listener);

sdks/flutter/packages/tugboat/test/tugboat_replay_test.dart

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1399,6 +1399,15 @@ void main() {
13991399
expect(TugboatReplay.isActivated, isFalse);
14001400
});
14011401

1402+
test('deactivate is a no-op from the initial dormant state', () {
1403+
addTearDown(TugboatReplay.resetForTest);
1404+
TugboatReplay.resetForTest();
1405+
1406+
TugboatReplay.deactivate();
1407+
1408+
expect(TugboatReplay.lifecycleState, TugboatLifecycleState.dormant);
1409+
});
1410+
14021411
testWidgets('enabled config reaches the active lifecycle state', (
14031412
tester,
14041413
) async {
@@ -1417,6 +1426,44 @@ void main() {
14171426
expect(TugboatReplay.lifecycleState, TugboatLifecycleState.active);
14181427
});
14191428

1429+
testWidgets('capability changes remount the active capture session', (
1430+
tester,
1431+
) async {
1432+
addTearDown(TugboatReplay.resetForTest);
1433+
final inventoryGrant = ValueNotifier<bool>(false);
1434+
addTearDown(inventoryGrant.dispose);
1435+
1436+
await tester.pumpWidget(
1437+
ValueListenableBuilder<bool>(
1438+
valueListenable: inventoryGrant,
1439+
builder: (context, granted, _) => MaterialApp(
1440+
builder: (context, child) => TugboatReplay.wrapApp(
1441+
config: _testConfig.copyWith(emitSceneInventory: granted),
1442+
child: child!,
1443+
),
1444+
home: const Scaffold(body: Text('Capability grant')),
1445+
),
1446+
),
1447+
);
1448+
await _waitForCaptures(tester);
1449+
final initialController = TugboatReplay.controller!;
1450+
expect(initialController.config.emitSceneInventory, isFalse);
1451+
1452+
inventoryGrant.value = true;
1453+
await _waitForCaptures(tester);
1454+
final grantedController = TugboatReplay.controller!;
1455+
expect(grantedController, isNot(same(initialController)));
1456+
expect(grantedController.config.emitSceneInventory, isTrue);
1457+
expect(TugboatReplay.lifecycleState, TugboatLifecycleState.active);
1458+
1459+
inventoryGrant.value = false;
1460+
await _waitForCaptures(tester);
1461+
final revokedController = TugboatReplay.controller!;
1462+
expect(revokedController, isNot(same(grantedController)));
1463+
expect(revokedController.config.emitSceneInventory, isFalse);
1464+
expect(TugboatReplay.lifecycleState, TugboatLifecycleState.active);
1465+
});
1466+
14201467
testWidgets('disabled config stays inert until activated without rebuild', (
14211468
tester,
14221469
) async {

sdks/flutter/packages/tugboat/test/viewport_semantic_policy_test.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ void main() {
3030
);
3131
expect(tapOnly.engineEnabled, isTrue);
3232
expect(tapOnly.emitEvents, isFalse);
33+
expect(tapOnly.holdPersistentSemanticsHandle, isFalse);
3334

3435
final defaultPolicy = const TugboatReplayConfig(
3536
enabled: true,

0 commit comments

Comments
 (0)