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" 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).