-
Notifications
You must be signed in to change notification settings - Fork 0
iOS: keep secure rectangles and the view tree on windows away from the screen's origin #30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
fbbce3a
f49bfca
4af4f55
99012d7
73460ac
282233b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| #import "BGSRNReactRootOriginTracker.h" | ||
|
|
||
| #import "BGSRNSecureRectangles.h" | ||
|
|
||
| @implementation BGSRNReactRootOriginTracker { | ||
| BGSRNSecureRectangles *_store; | ||
| UIView *_Nullable (^_findRoot)(void); | ||
| NSValue *_Nullable (^_readOrigin)(UIWindow *window); | ||
| /// The React root found last. Weak: a reload replaces it, and the tracker | ||
| /// must not keep the old one alive. | ||
| __weak UIView *_root; | ||
| } | ||
|
|
||
| - (instancetype)initWithStore:(BGSRNSecureRectangles *)store | ||
| findRoot:(UIView *_Nullable (^)(void))findRoot | ||
| readOrigin:(NSValue *_Nullable (^)(UIWindow *window))readOrigin { | ||
| self = [super init]; | ||
| if (self) { | ||
| _store = store; | ||
| _findRoot = [findRoot copy]; | ||
| _readOrigin = [readOrigin copy]; | ||
| } | ||
| return self; | ||
| } | ||
|
|
||
| - (void)refreshFindingTheRoot { | ||
| [self refreshFinding:YES]; | ||
| } | ||
|
|
||
| - (void)refresh { | ||
| [self refreshFinding:NO]; | ||
| } | ||
|
|
||
| - (BOOL)publishCoordinates:(NSData *)coordinates forDisplay:(NSInteger)display { | ||
| [self refreshFindingTheRoot]; | ||
| return [_store setCoordinates:(const int32_t *)coordinates.bytes | ||
| count:coordinates.length / sizeof(int32_t) | ||
| forDisplay:display]; | ||
| } | ||
|
|
||
| - (void)refreshFinding:(BOOL)find { | ||
| @try { | ||
| UIView *root = _root; | ||
| if (root.window == nil && find) { | ||
| root = _findRoot(); | ||
| _root = root; | ||
| } | ||
| UIWindow *window = [root isKindOfClass:UIWindow.class] ? (UIWindow *)root : root.window; | ||
| if (window == nil) { | ||
| return; | ||
| } | ||
| NSValue *origin = _readOrigin(window); | ||
| if (origin == nil) { | ||
| return; | ||
| } | ||
| [_store setOrigin:origin.CGPointValue forDisplay:0]; | ||
| } @catch (NSException *exception) { | ||
| NSLog(@"[Bugsee] secure rectangles: could not read the React root's place on the screen: %@", | ||
| exception); | ||
| } | ||
| } | ||
|
|
||
| @end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,11 +2,39 @@ | |
|
|
||
| const NSUInteger BGSRNReactRootSearchBudget = 2000; | ||
|
|
||
| /// Whether the SDK composes the app's windows on the screen: iOS, and not an | ||
| /// iPhone or iPad app running on a Mac (`+[BGSTrackerApplication | ||
| /// capturesAppScreen]`). | ||
| static BOOL BGSRNSdkComposesScreen(void) { | ||
| #if TARGET_OS_MACCATALYST | ||
| return NO; | ||
| #else | ||
| return !NSProcessInfo.processInfo.isiOSAppOnMac; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1 High — Still open after The declared pin is still 7.0.0-beta3 ( The PR body already says ship with cocoa #170–#172. That SDK is not what this branch resolves. Comments and the Stage Manager check (run against a locally built cocoa SDK) do not change what a consumer of main gets. Scenario: Consumer builds this wrapper against the declared 7.0.0-beta3 pin. iPad Stage Manager or the right-hand Split View window. Fix: Ship this origin only with the compositor SDK (bump the pin in this PR), or detect that SDK and keep
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed, this is the gate the PR body states: the screen origin is right only with an SDK that composes the app's windows on the screen (bugsee-cocoa #170–#172). This PR does not merge while the pin is 7.0.0-beta3; the pin moves to the release with #170–#172 in the same change. Detecting the SDK at runtime would be dead code: the wrapper vendors exactly the pinned SDK. Converted to draft until then. |
||
| #endif | ||
| } | ||
|
|
||
| /// A window scene the user can see: the SDK leaves background scenes out, as | ||
| /// they keep reporting their last place on a screen. | ||
| static BOOL BGSRNIsForeground(UIScene *scene) { | ||
| return scene.activationState == UISceneActivationStateForegroundActive || | ||
| scene.activationState == UISceneActivationStateForegroundInactive; | ||
| } | ||
|
|
||
| UIWindow *BGSRNSdkKeyWindow(void) { | ||
| UIApplication *application = UIApplication.sharedApplication; | ||
| if (application == nil) { | ||
| return nil; | ||
| } | ||
| // Since the iOS 15 SDK every scene's key window reports isKeyWindow, so with | ||
| // two of the app's scenes on screen the loop below cannot tell which one the | ||
| // user is in; the application's key window follows the one brought forward. | ||
| #pragma clang diagnostic push | ||
| #pragma clang diagnostic ignored "-Wdeprecated-declarations" | ||
| UIWindow *applicationKeyWindow = application.keyWindow; | ||
| #pragma clang diagnostic pop | ||
| if (applicationKeyWindow.windowScene != nil && BGSRNIsForeground(applicationKeyWindow.windowScene)) { | ||
| return applicationKeyWindow; | ||
| } | ||
| UIWindow *fallback = nil; | ||
| UIWindow *stableForeground = nil; | ||
| UIWindow *markedKey = nil; | ||
|
|
@@ -49,54 +77,98 @@ | |
| NSDictionary *manifest = [NSBundle.mainBundle objectForInfoDictionaryKey:@"UIApplicationSceneManifest"]; | ||
| hasSceneManifest = [manifest isKindOfClass:NSDictionary.class] && manifest.count > 0; | ||
| }); | ||
| UIApplication *application = UIApplication.sharedApplication; | ||
| UIScreen *screen = keyWindow.windowScene.screen; | ||
| if (hasSceneManifest && BGSRNSdkComposesScreen() && application.connectedScenes.count > 1 && | ||
| screen != nil) { | ||
| NSMutableArray<UIWindow *> *composed = [NSMutableArray array]; | ||
| for (UIScene *scene in application.connectedScenes) { | ||
| if ([scene isKindOfClass:UIWindowScene.class] && BGSRNIsForeground(scene) && | ||
| ((UIWindowScene *)scene).screen == screen) { | ||
| [composed addObjectsFromArray:((UIWindowScene *)scene).windows]; | ||
| } | ||
| } | ||
| if (composed.count > 0) { | ||
| return composed; | ||
| } | ||
| } | ||
| NSArray<UIWindow *> *windows = nil; | ||
| if (hasSceneManifest) { | ||
| windows = keyWindow.windowScene.windows; | ||
| } else { | ||
| #pragma clang diagnostic push | ||
| #pragma clang diagnostic ignored "-Wdeprecated-declarations" | ||
| windows = UIApplication.sharedApplication.windows; | ||
| windows = application.windows; | ||
| #pragma clang diagnostic pop | ||
| } | ||
| return windows ?: @[]; | ||
| } | ||
|
|
||
| /// Breadth-first: a React root is near the top of its window, and a deep | ||
| /// native subtree beside it should not be searched first. | ||
| static BOOL HostsReactRoot(UIWindow *window, BOOL (^isReactRoot)(UIView *), NSUInteger budget) { | ||
| static UIView *ReactRootInWindow(UIWindow *window, BOOL (^isReactRoot)(UIView *), NSUInteger budget) { | ||
| NSMutableArray<UIView *> *queue = [NSMutableArray arrayWithObject:window]; | ||
| NSUInteger head = 0; | ||
| while (head < queue.count && head < budget) { | ||
| UIView *view = queue[head++]; | ||
| if (isReactRoot(view)) { | ||
| return YES; | ||
| return view; | ||
| } | ||
| [queue addObjectsFromArray:view.subviews]; | ||
| } | ||
| return NO; | ||
| return nil; | ||
| } | ||
|
|
||
| UIWindow *BGSRNWindowHostingReactRoot(UIWindow *keyWindow, | ||
| NSArray<UIWindow *> *windows, | ||
| BOOL (^isReactRoot)(UIView *), | ||
| NSUInteger budget) { | ||
| if (keyWindow != nil && HostsReactRoot(keyWindow, isReactRoot, budget)) { | ||
| return keyWindow; | ||
| UIView *BGSRNReactRootView(UIWindow *keyWindow, | ||
| NSArray<UIWindow *> *windows, | ||
| BOOL (^isReactRoot)(UIView *), | ||
| NSUInteger budget) { | ||
| UIView *root = keyWindow != nil ? ReactRootInWindow(keyWindow, isReactRoot, budget) : nil; | ||
| if (root != nil) { | ||
| return root; | ||
| } | ||
| for (UIWindow *window in windows) { | ||
| if (window != keyWindow && HostsReactRoot(window, isReactRoot, budget)) { | ||
| return window; | ||
| if (window != keyWindow) { | ||
| root = ReactRootInWindow(window, isReactRoot, budget); | ||
| if (root != nil) { | ||
| return root; | ||
| } | ||
| } | ||
| } | ||
| return nil; | ||
| } | ||
|
|
||
| UIWindow *BGSRNWindowHostingReactRoot(UIWindow *keyWindow, | ||
| NSArray<UIWindow *> *windows, | ||
| BOOL (^isReactRoot)(UIView *), | ||
| NSUInteger budget) { | ||
| UIView *root = BGSRNReactRootView(keyWindow, windows, isReactRoot, budget); | ||
| // A root found in a window's tree is in that window, unless it is the window itself. | ||
| return [root isKindOfClass:UIWindow.class] ? (UIWindow *)root : root.window; | ||
| } | ||
|
|
||
| NSValue *BGSRNReactRootOrigin(UIWindow *keyWindow, NSArray<UIWindow *> *windows, BOOL (^isReactRoot)(UIView *)) { | ||
| UIWindow *window = BGSRNWindowHostingReactRoot(keyWindow, windows, isReactRoot, BGSRNReactRootSearchBudget); | ||
| if (window == nil) { | ||
| return nil; | ||
| } | ||
| // `frame.origin`, NOT the window's position in the screen's coordinate | ||
| // space: the SDK adds exactly this (BGSCaptureViewHierarchyEngine.m:335-336). | ||
| return [NSValue valueWithCGPoint:window.frame.origin]; | ||
| // Not `frame.origin` on iOS: that is the window's place in its scene, {0, 0} | ||
| // for a Stage Manager window or the right-hand one side by side, while the | ||
| // SDK places its nodes on the screen. | ||
| return BGSRNWindowRecordedOrigin(window); | ||
| } | ||
|
|
||
| NSValue *BGSRNWindowRecordedOrigin(UIWindow *window) { | ||
| UIScreen *screen = window.windowScene.screen; | ||
| if (screen == nil) { | ||
| return nil; | ||
| } | ||
| if (!BGSRNSdkComposesScreen()) { | ||
| return [NSValue valueWithCGPoint:window.frame.origin]; | ||
| } | ||
| id<UICoordinateSpace> fixedSpace = screen.fixedCoordinateSpace; | ||
| const CGRect inFixedSpace = [window convertRect:window.bounds toCoordinateSpace:fixedSpace]; | ||
| const CGRect onScreen = [fixedSpace convertRect:inFixedSpace | ||
| toCoordinateSpace:screen.coordinateSpace]; | ||
| return [NSValue valueWithCGPoint:onScreen.origin]; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1 High — This path assumes the iOS SDK composites every window onto the screen. That is not 7.0.0-beta3, which is still what
Impact: Beta3 records the key window’s scene and draws secure rectangles and native nodes in that scene’s points. Scenario: Consumer builds this wrapper against the declared 7.0.0-beta3 pin (CocoaPods / SPM). iPad Stage Manager or the right-hand Split View window. Fix: Ship this origin only with the compositor SDK (bump the pin in this PR), or detect that SDK and keep
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed, this is the gate the PR body states: the screen origin is right only with an SDK that composes the app's windows on the screen (bugsee-cocoa #170–#172). This PR does not merge while the pin is 7.0.0-beta3; the pin moves to the release with #170–#172 in the same change. Detecting the SDK at runtime would be dead code: the wrapper vendors exactly the pinned SDK. Converted to draft until then. |
||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.