From 495de5a68475b5525290934262c068999bddd676 Mon Sep 17 00:00:00 2001 From: timo <44401485+Timo972@users.noreply.github.com> Date: Thu, 24 Sep 2026 18:53:13 +0200 Subject: [PATCH 1/2] fix: match broadcast system UI labels in any device language MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The broadcast start dance located the confirmation sheet's button by its English label ("Start Broadcast", with a "Start" prefix fallback) and the stale "Screen Broadcasting" alert by "OK" / "Go to Application". On devices running another system language (e.g. German: "Übertragung starten") the confirm button was never found and the start timed out. Resolve these labels at runtime from ReplayKit's own localization tables, covering every localization it ships, so matching no longer depends on the system language. Request-supplied label arguments are now matched in addition to the system labels instead of replacing them. Co-Authored-By: Claude Opus 5.5 (1M context) --- WebDriverAgent.xcodeproj/project.pbxproj | 4 + .../Utilities/FBBroadcastManager.h | 27 +++- .../Utilities/FBBroadcastManager.m | 135 +++++++++++++++--- .../UnitTests/FBBroadcastLabelsTests.m | 43 ++++++ docs/broadcast-extension.md | 37 ++--- 5 files changed, 202 insertions(+), 44 deletions(-) create mode 100644 WebDriverAgentTests/UnitTests/FBBroadcastLabelsTests.m diff --git a/WebDriverAgent.xcodeproj/project.pbxproj b/WebDriverAgent.xcodeproj/project.pbxproj index fe3c81ab0..506a63132 100644 --- a/WebDriverAgent.xcodeproj/project.pbxproj +++ b/WebDriverAgent.xcodeproj/project.pbxproj @@ -1332,6 +1332,7 @@ FBCAFE000000000000006035 /* FBAudioCaptureCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = FBCAFE000000000000006031 /* FBAudioCaptureCommands.m */; }; FBCAFE000000000000006042 /* FBExtAudioPipeline.m in Sources */ = {isa = PBXBuildFile; fileRef = FBCAFE000000000000006041 /* FBExtAudioPipeline.m */; }; FBCAFE000000000000006051 /* FBAudioStreamTests.m in Sources */ = {isa = PBXBuildFile; fileRef = FBCAFE000000000000006050 /* FBAudioStreamTests.m */; }; + FBCAFE000000000000008051 /* FBBroadcastLabelsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = FBCAFE000000000000008050 /* FBBroadcastLabelsTests.m */; }; FBCAFE000000000000007002 /* FBSocks5TunnelProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = FBCAFE000000000000007000 /* FBSocks5TunnelProtocol.h */; }; FBCAFE000000000000007003 /* FBSocks5TunnelProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = FBCAFE000000000000007000 /* FBSocks5TunnelProtocol.h */; }; FBCAFE000000000000007004 /* FBSocks5TunnelProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = FBCAFE000000000000007001 /* FBSocks5TunnelProtocol.m */; }; @@ -2109,6 +2110,7 @@ FBCAFE000000000000006040 /* FBExtAudioPipeline.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBExtAudioPipeline.h; sourceTree = ""; }; FBCAFE000000000000006041 /* FBExtAudioPipeline.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBExtAudioPipeline.m; sourceTree = ""; }; FBCAFE000000000000006050 /* FBAudioStreamTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBAudioStreamTests.m; sourceTree = ""; }; + FBCAFE000000000000008050 /* FBBroadcastLabelsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBBroadcastLabelsTests.m; sourceTree = ""; }; FBCAFE000000000000007000 /* FBSocks5TunnelProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBSocks5TunnelProtocol.h; sourceTree = ""; }; FBCAFE000000000000007001 /* FBSocks5TunnelProtocol.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBSocks5TunnelProtocol.m; sourceTree = ""; }; FBCAFE000000000000007010 /* FBSocks5URI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBSocks5URI.h; sourceTree = ""; }; @@ -2955,6 +2957,7 @@ FBCAFE00000000000000001B /* FBVideoEncoderTests.m */, FBCAFE000000000000004001 /* FBVideoStreamSessionTests.m */, FBCAFE000000000000006050 /* FBAudioStreamTests.m */, + FBCAFE000000000000008050 /* FBBroadcastLabelsTests.m */, FBCAFE000000000000002001 /* FBImageUtilsTests.m */, ); path = UnitTests; @@ -5272,6 +5275,7 @@ FBCAFE00000000000000001C /* FBVideoEncoderTests.m in Sources */, FBCAFE000000000000004002 /* FBVideoStreamSessionTests.m in Sources */, FBCAFE000000000000006051 /* FBAudioStreamTests.m in Sources */, + FBCAFE000000000000008051 /* FBBroadcastLabelsTests.m in Sources */, FBCAFE000000000000002002 /* FBImageUtilsTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/WebDriverAgentLib/Utilities/FBBroadcastManager.h b/WebDriverAgentLib/Utilities/FBBroadcastManager.h index b2737c5c6..27a0425b7 100644 --- a/WebDriverAgentLib/Utilities/FBBroadcastManager.h +++ b/WebDriverAgentLib/Utilities/FBBroadcastManager.h @@ -39,6 +39,18 @@ typedef NS_ERROR_ENUM(FBBroadcastManagerErrorDomain, FBBroadcastManagerError) { /** YES while the broadcast extension is connected to the control server. */ @property (nonatomic, readonly) BOOL isExtensionConnected; +/** + Every localized variant ReplayKit ships for one of its UI strings, e.g. + "CONTROL_CENTER_START_BROADCAST" ("Start Broadcast", "Übertragung starten", ...). The device's + preferred localizations come first; the set covers all languages so that matching does not + depend on which language the system UI is currently rendered in. Resolved from the ReplayKit + framework's own localization tables once per key and cached. + + @param key The key in ReplayKit's Localizable strings table + @return The distinct non-empty labels, or an empty array when ReplayKit or the key is unavailable + */ ++ (NSArray *)replayKitLabelsForKey:(NSString *)key; + /** Starts the loopback control server. Safe to call multiple times. */ - (void)startListening; @@ -54,14 +66,16 @@ typedef NS_ERROR_ENUM(FBBroadcastManagerErrorDomain, FBBroadcastManagerError) { for the extension to connect. Must be called on the main thread. Idempotent while connected. @param timeout The overall time budget in seconds for the broadcast to reach the connected state - @param confirmButtonLabels Labels to look for on the system confirmation sheet + @param confirmButtonLabels Extra labels to look for on the system confirmation sheet, in + addition to the "Start Broadcast" label in every language ReplayKit is localized for @param dismissButtonLabels Labels for dismissing the system's stale "Screen Broadcasting" alert that SpringBoard posts whenever a broadcast ends, which otherwise blocks the picker dance from - completing. Defaults to ["OK"] when empty/nil + completing. Added to the alert's "OK" label in every language ReplayKit is localized for @param goToApplicationButtonLabels Labels for the alert's other button. Together with dismissButtonLabels this anchors the alert's identity: it is only treated as the Screen Broadcasting alert, and auto-dismissed, when one button matches dismissButtonLabels and the - other matches this list. Defaults to ["Go to Application"] when empty/nil + other matches this list. Added to the alert's "Go to Application" label in every language + ReplayKit is localized for @param restoreForegroundApp YES to re-activate the previously active application afterwards @param error If there is an error, upon return contains an NSError describing the problem @return NO in case of a failure @@ -89,12 +103,13 @@ typedef NS_ERROR_ENUM(FBBroadcastManagerErrorDomain, FBBroadcastManagerError) { best-effort attempt to dismiss the system's stale "Screen Broadcasting" alert that SpringBoard posts once the broadcast ends. Idempotent when no broadcast is running. - @param dismissButtonLabels Labels for dismissing the system's stale "Screen Broadcasting" alert. - Defaults to ["OK"] when empty/nil + @param dismissButtonLabels Extra labels for dismissing the system's stale "Screen Broadcasting" + alert, added to its "OK" label in every language ReplayKit is localized for @param goToApplicationButtonLabels Labels for the alert's other button. Together with dismissButtonLabels this anchors the alert's identity: it is only treated as the Screen Broadcasting alert, and auto-dismissed, when one button matches dismissButtonLabels and the - other matches this list. Defaults to ["Go to Application"] when empty/nil + other matches this list. Added to the alert's "Go to Application" label in every language + ReplayKit is localized for @param error If there is an error, upon return contains an NSError describing the problem @return NO in case of a failure. The alert-dismissal attempt is best-effort and never causes this to return NO by itself diff --git a/WebDriverAgentLib/Utilities/FBBroadcastManager.m b/WebDriverAgentLib/Utilities/FBBroadcastManager.m index b821d82e8..115a45cb4 100644 --- a/WebDriverAgentLib/Utilities/FBBroadcastManager.m +++ b/WebDriverAgentLib/Utilities/FBBroadcastManager.m @@ -29,6 +29,26 @@ NSErrorDomain const FBBroadcastManagerErrorDomain = @"com.facebook.WebDriverAgent.FBBroadcastManager"; #if !TARGET_OS_SIMULATOR && !TARGET_OS_TV +// Keys of the ReplayKit strings the system broadcast UI renders. The confirmation sheet's button +// and the stale "Screen Broadcasting" alert are system UI rendered in the SYSTEM language, so +// their labels are resolved from ReplayKit's own localization tables rather than hardcoded. +static NSString *const FBReplayKitKeyStartBroadcast = @"CONTROL_CENTER_START_BROADCAST"; +static NSString *const FBReplayKitKeyAlertOK = @"BROADCAST_FAILED_ALERT_OK_BUTTON"; +static NSString *const FBReplayKitKeyAlertGoToApp = @"BROADCAST_FAILED_ALERT_GO_TO_APP_BUTTON"; + +// Caller-supplied labels first, then the system's own label in every ReplayKit localization, +// then the English label as a last resort for iOS versions whose tables lack the key. +static NSArray *FBBroadcastLabels(NSArray *_Nullable callerLabels, + NSString *replayKitKey, + NSString *englishLabel) +{ + NSMutableOrderedSet *labels = [NSMutableOrderedSet orderedSet]; + [labels addObjectsFromArray:callerLabels ?: @[]]; + [labels addObjectsFromArray:[FBBroadcastManager replayKitLabelsForKey:replayKitKey]]; + [labels addObject:englishLabel]; + return labels.array; +} + static const NSTimeInterval FOREGROUND_TIMEOUT = 5.0; static const NSTimeInterval CONFIRM_BUTTON_TIMEOUT = 10.0; // The picker press is dropped silently by the system when it fires before the scene is fully @@ -108,10 +128,11 @@ - (BOOL)performBroadcastStartWithTimeout:(NSTimeInterval)timeout // structurally, not by its (localized) title: exactly two buttons, of which exactly one matches // dismissLabels and the OTHER matches goToAppLabels - the second button anchors the alert's // identity, since "exactly one of two buttons matches the dismiss labels" alone still matches -// unrelated two-button prompts (e.g. "Settings" / "OK"). Both label lists are localizable via -// the request arguments. Anything else - including two-button alerts whose second button is -// unrecognized - is left alone; misfiring on an unrelated system dialog would silently -// acknowledge it, which is worse than letting the dance time out. +// unrelated two-button prompts (e.g. "Settings" / "OK"). Both label lists cover every ReplayKit +// localization (see FBBroadcastLabels) plus any request-supplied labels. Anything else - +// including two-button alerts whose second button is unrecognized - is left alone; misfiring on +// an unrelated system dialog would silently acknowledge it, which is worse than letting the +// dance time out. - (nullable XCUIElement *)matchingDismissButtonForAlertWithDismissLabels:(NSArray *)dismissLabels goToApplicationLabels:(NSArray *)goToAppLabels; // Dismisses the alert matched by matchingDismissButtonForAlertWithDismissLabels:goToApplicationLabels: @@ -144,6 +165,76 @@ + (instancetype)sharedInstance return instance; } ++ (NSArray *)replayKitLabelsForKey:(NSString *)key +{ + static NSMutableDictionary *> *cache; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + cache = [NSMutableDictionary dictionary]; + }); + @synchronized (cache) { + NSArray *cached = cache[key]; + if (nil != cached) { + return cached; + } + NSArray *labels = [self loadReplayKitLabelsForKey:key]; + cache[key] = labels; + return labels; + } +} + ++ (NSArray *)loadReplayKitLabelsForKey:(NSString *)key +{ + // RPScreenRecorder exists on every platform ReplayKit does, unlike the picker view. + Class anchorClass = NSClassFromString(@"RPScreenRecorder"); + NSBundle *bundle = nil == anchorClass ? nil : [NSBundle bundleForClass:anchorClass]; + if (nil == bundle || bundle == NSBundle.mainBundle) { + [FBLogger logFmt:@"broadcast: the ReplayKit bundle is unavailable; cannot resolve the localized '%@' labels", key]; + return @[]; + } + // Probe the device's preferred localizations first, then every other one: the system UI can + // be rendered in a different language than the runner's own preferences (e.g. a per-app + // language override), and every localization's string is distinct UI text anyway. + NSMutableOrderedSet *localizations = [NSMutableOrderedSet orderedSet]; + [localizations addObjectsFromArray:[NSBundle preferredLocalizationsFromArray:bundle.localizations + forPreferences:NSLocale.preferredLanguages]]; + [localizations addObjectsFromArray:bundle.localizations]; + + NSMutableOrderedSet *labels = [NSMutableOrderedSet orderedSet]; + for (NSString *localization in localizations) { + NSString *path = [bundle pathForResource:@"Localizable" + ofType:@"strings" + inDirectory:nil + forLocalization:localization]; + NSDictionary *table = nil == path ? nil : [NSDictionary dictionaryWithContentsOfFile:path]; + id label = table[key]; + if ([label isKindOfClass:NSString.class] && [(NSString *)label length] > 0) { + [labels addObject:label]; + } + } + // Newer system frameworks may ship a single .loctable (localization -> table) instead of + // per-language .lproj directories. + NSString *loctablePath = [bundle pathForResource:@"Localizable" ofType:@"loctable"]; + NSDictionary *loctable = nil == loctablePath ? nil : [NSDictionary dictionaryWithContentsOfFile:loctablePath]; + if (nil != loctable) { + NSMutableOrderedSet *loctableLocalizations = [NSMutableOrderedSet orderedSet]; + [loctableLocalizations addObjectsFromArray:[NSBundle preferredLocalizationsFromArray:loctable.allKeys + forPreferences:NSLocale.preferredLanguages]]; + [loctableLocalizations addObjectsFromArray:loctable.allKeys]; + for (NSString *localization in loctableLocalizations) { + id table = loctable[localization]; + id label = [table isKindOfClass:NSDictionary.class] ? ((NSDictionary *)table)[key] : nil; + if ([label isKindOfClass:NSString.class] && [(NSString *)label length] > 0) { + [labels addObject:label]; + } + } + } + if (0 == labels.count) { + [FBLogger logFmt:@"broadcast: ReplayKit has no localized '%@' labels", key]; + } + return labels.array; +} + - (BOOL)isExtensionConnected { return self.controlServer.isExtensionConnected; @@ -271,8 +362,8 @@ - (BOOL)performBroadcastStartWithTimeout:(NSTimeInterval)timeout restoreForegroundApp:(BOOL)restoreForegroundApp error:(NSError **)error { - NSArray *dismissLabels = dismissButtonLabels.count > 0 ? dismissButtonLabels : @[@"OK"]; - NSArray *goToAppLabels = goToApplicationButtonLabels.count > 0 ? goToApplicationButtonLabels : @[@"Go to Application"]; + NSArray *dismissLabels = FBBroadcastLabels(dismissButtonLabels, FBReplayKitKeyAlertOK, @"OK"); + NSArray *goToAppLabels = FBBroadcastLabels(goToApplicationButtonLabels, FBReplayKitKeyAlertGoToApp, @"Go to Application"); // The screen may already be captured by a live broadcast even though the extension is not // connected (it crashed, or it is between TCP reconnect attempts). Driving the picker on top @@ -383,7 +474,8 @@ - (BOOL)performBroadcastStartWithTimeout:(NSTimeInterval)timeout // The confirmation sheet is hosted by different processes depending on the iOS version, so // look for the confirm button in both the system app and the runner itself. - NSArray *labels = confirmButtonLabels.count > 0 ? confirmButtonLabels : @[@"Start Broadcast"]; + NSArray *labels = FBBroadcastLabels(confirmButtonLabels, FBReplayKitKeyStartBroadcast, @"Start Broadcast"); + NSPredicate *labelPredicate = [NSPredicate predicateWithFormat:@"label IN %@", labels]; NSArray *candidateApps = @[XCUIApplication.fb_systemApplication, runner]; __block BOOL confirmButtonFound = NO; __block CGRect confirmFrame = CGRectZero; @@ -394,14 +486,14 @@ - (BOOL)performBroadcastStartWithTimeout:(NSTimeInterval)timeout return NO; } for (XCUIApplication *app in candidateApps) { - for (NSString *label in labels) { - XCUIElement *candidate = app.buttons[label]; - if (candidate.exists) { - confirmFrame = candidate.frame; - confirmButtonFound = YES; - return YES; - } + // One query for all localized labels instead of one existence probe per label. + XCUIElement *candidate = [app.buttons matchingPredicate:labelPredicate].firstMatch; + if (candidate.exists) { + confirmFrame = candidate.frame; + confirmButtonFound = YES; + return YES; } + // Last resort for English UIs whose button text the tables above do not cover. XCUIElement *prefixMatch = [app.buttons matchingPredicate:[NSPredicate predicateWithFormat:@"label BEGINSWITH[c] 'Start'"]].firstMatch; if (prefixMatch.exists) { confirmFrame = prefixMatch.frame; @@ -422,7 +514,7 @@ - (BOOL)performBroadcastStartWithTimeout:(NSTimeInterval)timeout if (error) { *error = [NSError errorWithDomain:FBBroadcastManagerErrorDomain code:FBBroadcastManagerErrorTimeout - userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"The broadcast confirmation sheet did not show a button labeled %@ within %.0fs. Pass 'confirmButtonLabels' if the device language is not English", [labels componentsJoinedByString:@"/"], CONFIRM_BUTTON_TIMEOUT]}]; + userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"The broadcast confirmation sheet did not show a button labeled '%@' (or any of its %lu known variants) within %.0fs. Pass 'confirmButtonLabels' with the button's exact label if it differs", labels.firstObject, (unsigned long)labels.count, CONFIRM_BUTTON_TIMEOUT]}]; } return NO; } @@ -466,10 +558,11 @@ - (BOOL)performBroadcastStartWithTimeout:(NSTimeInterval)timeout // structurally, not by its (localized) title: exactly two buttons, of which exactly one matches // dismissLabels and the OTHER matches goToAppLabels - the second button anchors the alert's // identity, since "exactly one of two buttons matches the dismiss labels" alone still matches -// unrelated two-button prompts (e.g. "Settings" / "OK"). Both label lists are localizable via -// the request arguments. Anything else - including two-button alerts whose second button is -// unrecognized - is left alone; misfiring on an unrelated system dialog would silently -// acknowledge it, which is worse than letting the dance time out. +// unrelated two-button prompts (e.g. "Settings" / "OK"). Both label lists cover every ReplayKit +// localization (see FBBroadcastLabels) plus any request-supplied labels. Anything else - +// including two-button alerts whose second button is unrecognized - is left alone; misfiring on +// an unrelated system dialog would silently acknowledge it, which is worse than letting the +// dance time out. - (nullable XCUIElement *)matchingDismissButtonForAlertWithDismissLabels:(NSArray *)dismissLabels goToApplicationLabels:(NSArray *)goToAppLabels { @@ -580,8 +673,8 @@ - (BOOL)stopBroadcastWithDismissButtonLabels:(NSArray *)dismissButto error:(NSError **)error { #if !TARGET_OS_SIMULATOR && !TARGET_OS_TV - NSArray *dismissLabels = dismissButtonLabels.count > 0 ? dismissButtonLabels : @[@"OK"]; - NSArray *goToAppLabels = goToApplicationButtonLabels.count > 0 ? goToApplicationButtonLabels : @[@"Go to Application"]; + NSArray *dismissLabels = FBBroadcastLabels(dismissButtonLabels, FBReplayKitKeyAlertOK, @"OK"); + NSArray *goToAppLabels = FBBroadcastLabels(goToApplicationButtonLabels, FBReplayKitKeyAlertGoToApp, @"Go to Application"); #endif if (!self.isExtensionConnected) { #if !TARGET_OS_SIMULATOR && !TARGET_OS_TV diff --git a/WebDriverAgentTests/UnitTests/FBBroadcastLabelsTests.m b/WebDriverAgentTests/UnitTests/FBBroadcastLabelsTests.m new file mode 100644 index 000000000..e99c2ebe0 --- /dev/null +++ b/WebDriverAgentTests/UnitTests/FBBroadcastLabelsTests.m @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2026-present, Droidrun. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "FBBroadcastManager.h" + +@interface FBBroadcastLabelsTests : XCTestCase +@end + +@implementation FBBroadcastLabelsTests + +- (void)testStartBroadcastLabelCoversNonEnglishLanguages +{ + NSArray *labels = [FBBroadcastManager replayKitLabelsForKey:@"CONTROL_CENTER_START_BROADCAST"]; + XCTAssertTrue([labels containsObject:@"Start Broadcast"]); + // The German label does not start with "Start" - the case that broke the old prefix matching. + XCTAssertTrue([labels containsObject:@"Übertragung starten"]); + XCTAssertTrue([labels containsObject:@"开始直播"]); + XCTAssertEqual(labels.count, [NSSet setWithArray:labels].count); +} + +- (void)testScreenBroadcastingAlertLabelsCoverNonEnglishLanguages +{ + NSArray *okLabels = [FBBroadcastManager replayKitLabelsForKey:@"BROADCAST_FAILED_ALERT_OK_BUTTON"]; + XCTAssertTrue([okLabels containsObject:@"OK"]); + XCTAssertTrue([okLabels containsObject:@"Tamam"]); + NSArray *goToAppLabels = [FBBroadcastManager replayKitLabelsForKey:@"BROADCAST_FAILED_ALERT_GO_TO_APP_BUTTON"]; + XCTAssertTrue([goToAppLabels containsObject:@"Go to Application"]); + XCTAssertTrue([goToAppLabels containsObject:@"Zur App gehen"]); +} + +- (void)testUnknownKeyYieldsNoLabels +{ + XCTAssertEqualObjects([FBBroadcastManager replayKitLabelsForKey:@"FB_NO_SUCH_REPLAYKIT_KEY"], @[]); +} + +@end diff --git a/docs/broadcast-extension.md b/docs/broadcast-extension.md index 572b7ff18..f1bc687e1 100644 --- a/docs/broadcast-extension.md +++ b/docs/broadcast-extension.md @@ -26,28 +26,31 @@ the legacy screenshot pipeline; each session reports its current origin via the ```json { "timeout": 30, - "confirmButtonLabels": ["Start Broadcast"], - "dismissButtonLabels": ["OK"], - "goToApplicationButtonLabels": ["Go to Application"], + "confirmButtonLabels": [], + "dismissButtonLabels": [], + "goToApplicationButtonLabels": [], "restoreForegroundApp": true } ``` +The system UI driven by the start dance is matched in any device language: WDA reads the button +labels ("Start Broadcast", and the "Screen Broadcasting" alert's "OK" / "Go to Application") +from ReplayKit's own localization tables at runtime, in every language iOS ships. The label +arguments below are only needed if a future iOS renames those strings; they are matched in +addition to the system labels, never instead of them. + - `timeout` — seconds to wait for the extension to connect (covers the system's 3-2-1 countdown). -- `confirmButtonLabels` — labels to look for on the system confirmation sheet. Pass the - localized label when the device language is not English (a button starting with "Start" is - used as fallback). -- `dismissButtonLabels` — labels of the button that dismisses the system's "Screen Broadcasting" - alert left behind by a previous broadcast's end (SpringBoard posts this on iOS 26 whenever a - broadcast terminates, and it otherwise blocks the picker dance). Defaults to `["OK"]`. Pass the - localized label when the device language is not English. Also accepted by `broadcast/stop`. -- `goToApplicationButtonLabels` — labels of the alert's other button. Defaults to - `["Go to Application"]`. Together, `dismissButtonLabels` and `goToApplicationButtonLabels` - identify the system's "Screen Broadcasting" alert: it is only auto-dismissed when one button - matches `dismissButtonLabels` and the other matches `goToApplicationButtonLabels` — this - second-button check is what keeps the auto-dismiss from firing on an unrelated two-button - system prompt. Pass the localized label when the device language is not English. Also accepted - by `broadcast/stop`. +- `confirmButtonLabels` — extra labels to look for on the system confirmation sheet (an + English button starting with "Start" is used as a last-resort fallback). +- `dismissButtonLabels` — extra labels of the button that dismisses the system's "Screen + Broadcasting" alert left behind by a previous broadcast's end (SpringBoard posts this whenever + a broadcast terminates, and it otherwise blocks the picker dance). Also accepted by + `broadcast/stop`. +- `goToApplicationButtonLabels` — extra labels of the alert's other button. Together, the + dismiss and go-to-application labels identify the system's "Screen Broadcasting" alert: it is + only auto-dismissed when one button matches a dismiss label and the other a go-to-application + label — this second-button check is what keeps the auto-dismiss from firing on an unrelated + two-button system prompt. Also accepted by `broadcast/stop`. - `restoreForegroundApp` — re-activate the previously active app after the broadcast starts (the start dance briefly foregrounds the runner app, ~2-3 s). From 54ab79d1b18d2931106f0adfb402a13c65dd4e88 Mon Sep 17 00:00:00 2001 From: timo <44401485+Timo972@users.noreply.github.com> Date: Thu, 24 Sep 2026 18:55:30 +0200 Subject: [PATCH 2/2] ci: pin stable Xcode 27.0 now that the beta left the runner image Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/functional-test.yml | 6 +++--- .github/workflows/wda-tests.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/functional-test.yml b/.github/workflows/functional-test.yml index 58544b72c..36ad8f092 100644 --- a/.github/workflows/functional-test.yml +++ b/.github/workflows/functional-test.yml @@ -30,7 +30,7 @@ jobs: test_targets: # iOS - HOST_OS: 'xcode-27' - XCODE_VERSION: '27.0-beta' + XCODE_VERSION: '27.0' PLATFORM_NAME: 'iOS' PLATFORM_VERSION: '27.0' DEVICE_NAME: 'iPhone 17' @@ -42,7 +42,7 @@ jobs: # tvOS - HOST_OS: 'xcode-27' - XCODE_VERSION: '27.0-beta' + XCODE_VERSION: '27.0' PLATFORM_NAME: 'tvOS' PLATFORM_VERSION: '27.0' DEVICE_NAME: 'Apple TV 4K (3rd generation)' @@ -54,7 +54,7 @@ jobs: # WatchOS - HOST_OS: 'xcode-27' - XCODE_VERSION: '27.0-beta' + XCODE_VERSION: '27.0' PLATFORM_NAME: 'watchOS' PLATFORM_VERSION: '27.0' DEVICE_NAME: 'Apple Watch Series 11 (46mm)' diff --git a/.github/workflows/wda-tests.yml b/.github/workflows/wda-tests.yml index f1a497097..0fe704823 100644 --- a/.github/workflows/wda-tests.yml +++ b/.github/workflows/wda-tests.yml @@ -39,7 +39,7 @@ env: MIN_IPHONE_DEVICE_NAME: "iPhone 16" MIN_IPAD_DEVICE_NAME: "iPad Air 13-inch (M2)" MAX_VM_IMAGE: xcode-27 - MAX_XCODE_VERSION: "27.0-beta" + MAX_XCODE_VERSION: "27.0" MAX_PLATFORM_VERSION: "27.0" MAX_TV_PLATFORM_VERSION: "27.0" MAX_IPHONE_DEVICE_NAME: "iPhone 17"