From d4aed2b327510024753edd5328452a3211d9bb0d Mon Sep 17 00:00:00 2001 From: timo <44401485+Timo972@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:28:50 +0200 Subject: [PATCH] fix: hand the foreground back after every broadcast start attempt The broadcast start restored the previously active app only on success. Every failure after the runner was brought forward (foregrounding timeout, picker error, confirmation tap error, extension connect timeout) left the runner's blank screen in front. The next attempt then found the runner already frontmost, had no app to restore, and never left it - even on success. That blank runner is what the screenshot fallback and a later broadcast capture, and on a phone lying on its side it rotates to landscape, so viewers see a black, sideways frame (DRO-3369). Restore on every exit path. When no previous app is known and the runner is still in front, activate SpringBoard instead (a HOME press does not reach SpringBoard on iOS 26). Co-Authored-By: Claude Opus 5.5 --- .../Utilities/FBBroadcastManager.m | 53 +++++++++++++++++-- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/WebDriverAgentLib/Utilities/FBBroadcastManager.m b/WebDriverAgentLib/Utilities/FBBroadcastManager.m index ec348c52b..cb01ff5ed 100644 --- a/WebDriverAgentLib/Utilities/FBBroadcastManager.m +++ b/WebDriverAgentLib/Utilities/FBBroadcastManager.m @@ -25,6 +25,7 @@ #import "XCUIApplication+FBTouchAction.h" #import "XCUIApplication.h" #import "XCUIApplication+FBHelpers.h" +#import "XCUIDevice+FBHelpers.h" NSErrorDomain const FBBroadcastManagerErrorDomain = @"com.facebook.WebDriverAgent.FBBroadcastManager"; @@ -411,6 +412,54 @@ - (BOOL)performBroadcastStartWithTimeout:(NSTimeInterval)timeout [FBLogger logFmt:@"broadcast/start: active-app lookup finished after %llums", FBBroadcastNowMs() - startedMs]; } + // Every exit from here on - success or any failure - hands the foreground back. Restoring only + // on success stranded the runner's blank screen in front after a failed dance, and the next + // attempt then found the runner already frontmost and had nothing left to restore. + @try { + return [self runBroadcastDanceWithRunner:runner + runnerIsActive:runnerIsActive + startedMs:startedMs + timeout:timeout + confirmButtonLabels:confirmButtonLabels + dismissLabels:dismissLabels + goToAppLabels:goToAppLabels + error:error]; + } @finally { + if (restoreForegroundApp) { + [self handBackForegroundToApp:previousApp]; + } + } +} + +// Leaves the screen on whatever was in front before the dance. When nothing is known (the +// runner itself was frontmost, e.g. stranded there by an earlier failed start), the home screen +// stands in: the runner's blank view must never stay in front, where it is what gets captured and +// it follows the device sideways when the phone lies on its side. SpringBoard is activated +// rather than sent a HOME button press, which WDA cannot deliver on iOS 26. +- (void)handBackForegroundToApp:(nullable XCUIApplication *)previousApp +{ + if (nil != previousApp) { + [previousApp activate]; + return; + } + if (UIApplication.sharedApplication.applicationState != UIApplicationStateActive) { + return; + } + NSError *homeError; + if (![XCUIDevice.sharedDevice fb_goToHomescreenWithError:&homeError]) { + [FBLogger logFmt:@"broadcast/start: cannot move the runner out of the foreground: %@", homeError.localizedDescription]; + } +} + +- (BOOL)runBroadcastDanceWithRunner:(XCUIApplication *)runner + runnerIsActive:(BOOL)runnerIsActive + startedMs:(uint64_t)startedMs + timeout:(NSTimeInterval)timeout + confirmButtonLabels:(NSArray *)confirmButtonLabels + dismissLabels:(NSArray *)dismissLabels + goToAppLabels:(NSArray *)goToAppLabels + error:(NSError **)error +{ // The picker can only present from a foreground app, so bring the runner up first. The // LaunchServices route is used instead of XCUIApplication.activate because activating the // runner from inside itself blocks on a self-quiescence wait that can only ever time out: @@ -548,10 +597,6 @@ - (BOOL)performBroadcastStartWithTimeout:(NSTimeInterval)timeout } return NO; } - - if (nil != previousApp) { - [previousApp activate]; - } return YES; }