mParticle / mparticle-apple-sdk

mParticle Apple SDK
Apache License 2.0
45 stars 65 forks source link

iPadOS 17 NSInternalInconsistencyException: `Class MPAppDelegateProxy implements _wantsPriorityOverFocusUpdates, which is no longer supported.` #237

Closed djs-code closed 8 months ago

djs-code commented 11 months ago

We're seeing crashes on iPadOS 17 which seem to be related to the UIFocus family of APIs:

Class MPAppDelegateProxy implements _wantsPriorityOverFocusUpdates, which is no longer supported.

Fatal Exception: NSInternalInconsistencyException
0  CoreFoundation                 0xec870 __exceptionPreprocess
1  libobjc.A.dylib                0x2bc00 objc_exception_throw
2  Foundation                     0x6b6e54 -[NSMutableDictionary(NSMutableDictionary) initWithContentsOfFile:]
3  UIKitCore                      0x4ce918 -[UIResponder(UIFocusAdditions) _shouldSkipKeyCommand:forMovement:]
4  UIKitCore                      0x1304ce0 -[_UIFocusEventDelivery shouldSkipKeyCommand:whenDeliveringFocusKeyboardEvent:toResponder:]
5  UIKitCore                      0xe1e24c -[UIApplication _handleKeyboardPressEvent:]
6  UIKitCore                      0xe0fa7c -[UIApplication pressesBegan:withEvent:]
7  UIKitCore                      0x1cd154 forwardTouchMethod
8  UIKitCore                      0x1cd154 forwardTouchMethod
9  UIKitCore                      0x1cd154 forwardTouchMethod
10 UIKitCore                      0x1cd154 forwardTouchMethod
11 UIKitCore                      0x1cd154 forwardTouchMethod
12 UIKitCore                      0x1cd154 forwardTouchMethod
13 UIKitCore                      0x1cd154 forwardTouchMethod
14 UIKitCore                      0x1cd154 forwardTouchMethod
15 UIKitCore                      0x1cd154 forwardTouchMethod
16 UIKitCore                      0x1cd154 forwardTouchMethod
17 UIKitCore                      0x1cd154 forwardTouchMethod
18 UIKitCore                      0x1cd154 forwardTouchMethod
19 UIKitCore                      0x1cd154 forwardTouchMethod
20 UIKitCore                      0x1cd154 forwardTouchMethod
21 UIKitCore                      0x1cd154 forwardTouchMethod
22 UIKitCore                      0x1cd154 forwardTouchMethod
23 UIKitCore                      0x1cd154 forwardTouchMethod
24 UIKitCore                      0x1cd154 forwardTouchMethod
25 UIKitCore                      0x1cd154 forwardTouchMethod
26 UIKitCore                      0x120d4c8 -[UIScrollView pressesBegan:withEvent:]
27 UIKitCore                      0x1cd154 forwardTouchMethod
28 UIKitCore                      0x1cd154 forwardTouchMethod
29 UIKitCore                      0x1cd154 forwardTouchMethod
30 UIKitCore                      0x1cd154 forwardTouchMethod
31 UIKitCore                      0x10cdbb0 -[UITextField pressesBegan:withEvent:]
32 UIKitCore                      0xe33a64 -[UIWindow _sendButtonsForEvent:]
33 UIKitCore                      0x2020a4 -[UIWindow sendEvent:]
34 UIKitCore                      0x2015c4 -[UIApplication sendEvent:]
35 UIKitCore                      0xe1d764 -[UIApplication _handleKeyUIEvent:]
36 UIKitCore                      0xe2e1bc -[UIResponder _handleKeyUIEvent:]
37 UIKitCore                      0xe2e1bc -[UIResponder _handleKeyUIEvent:]
38 UIKitCore                      0xe2e1bc -[UIResponder _handleKeyUIEvent:]
39 UIKitCore                      0xe2e1bc -[UIResponder _handleKeyUIEvent:]
40 UIKitCore                      0xe2e1bc -[UIResponder _handleKeyUIEvent:]
41 UIKitCore                      0xe2e1bc -[UIResponder _handleKeyUIEvent:]
42 UIKitCore                      0xe2e1bc -[UIResponder _handleKeyUIEvent:]
43 UIKitCore                      0xe2e1bc -[UIResponder _handleKeyUIEvent:]
44 UIKitCore                      0xe2e1bc -[UIResponder _handleKeyUIEvent:]
45 UIKitCore                      0xe2e1bc -[UIResponder _handleKeyUIEvent:]
46 UIKitCore                      0xe2e1bc -[UIResponder _handleKeyUIEvent:]
47 UIKitCore                      0xe2e1bc -[UIResponder _handleKeyUIEvent:]
48 UIKitCore                      0xe2e1bc -[UIResponder _handleKeyUIEvent:]
49 UIKitCore                      0xe2e1bc -[UIResponder _handleKeyUIEvent:]
50 UIKitCore                      0xe2e1bc -[UIResponder _handleKeyUIEvent:]
51 UIKitCore                      0xe2e1bc -[UIResponder _handleKeyUIEvent:]
52 UIKitCore                      0xe2e1bc -[UIResponder _handleKeyUIEvent:]
53 UIKitCore                      0xe2e1bc -[UIResponder _handleKeyUIEvent:]
54 UIKitCore                      0xe2e1bc -[UIResponder _handleKeyUIEvent:]
55 UIKitCore                      0xe2e1bc -[UIResponder _handleKeyUIEvent:]
56 UIKitCore                      0xe2e1bc -[UIResponder _handleKeyUIEvent:]
57 UIKitCore                      0xe2e1bc -[UIResponder _handleKeyUIEvent:]
58 UIKitCore                      0xe2e1bc -[UIResponder _handleKeyUIEvent:]
59 UIKitCore                      0xe1d7e8 -[UIApplication handleKeyUIEvent:]
60 UIKitCore                      0xe1d6ac -[UIApplication _handleKeyHIDEvent:usingSyntheticEvent:]
61 UIKitCore                      0x1c33a4 __dispatchPreprocessedEventFromEventQueue
62 UIKitCore                      0x1c1ed0 __processEventQueue
63 UIKitCore                      0x1c08dc updateCycleEntry
64 UIKitCore                      0xaa468 _UIUpdateSequenceRun
65 UIKitCore                      0xa9b58 schedulerStepScheduledMainSection
66 UIKitCore                      0xa9c14 runloopSourceCallback
67 CoreFoundation                 0x3731c __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
68 CoreFoundation                 0x36598 __CFRunLoopDoSource0
69 CoreFoundation                 0x34d4c __CFRunLoopDoSources0
70 CoreFoundation                 0x33a88 __CFRunLoopRun
71 CoreFoundation                 0x33668 CFRunLoopRunSpecific
72 GraphicsServices               0x35ec GSEventRunModal
73 UIKitCore                      0x22c294 -[UIApplication _run]
74 UIKitCore                      0x22b8d0 UIApplicationMain
75 <redacted>                       0x12278f8 main + 13 (main.m:13)
76 ???                            0x1ad67edcc (Missing)

Our AppDelegate implementation inherits from UIResponder, and a cursory look into MPAppDelegateProxy.m and MPSurrogateAppDelegate.h suggests we might be able to work around this by switching it to NSObject. That said, I don't like the idea of cutting AppDelegate out of the responder chain in this day and age.

djs-code commented 11 months ago

For additional context, the UIFocus family of APIs is related to the Full Keyboard Access accessibility setting on iPadOS.

https://mcmw.abilitynet.org.uk/how-to-control-your-device-using-an-external-keyboard-in-ios-16-on-your-iphone-or-ipad

https://developer.apple.com/videos/play/wwdc2021/10260/

einsteinx2 commented 11 months ago

Hi thanks for reporting this, from a cursory look I believe that the exception description may be misleading... We don't do anything with the UIFocus API nor do we implement _wantsPriorityOverFocusUpdates. In fact I searched the entire project for the word "focus" case-insensitive and we don't even have that word in the code base. Also I don't actually see the MPAppDelegateProxy class nor the MPSurrogateAppDelegate class that it forwards to in the stack trace at all. In fact I don't see the mParticle framework in the trace at all, which is strange given the exception message.

I do see the following line in the stack trace just before the exception is triggered:

-[UIResponder(UIFocusAdditions) _shouldSkipKeyCommand:forMovement:]

This looks like it might be an internal Apple category as it says it's inside UIKitCore.

My best guess is this is an iOS 17 bug. Can you try a couple of things:

  1. Disable the app delegate proxy by setting proxyAppDelegate to false in MParticleOptions and see if it still triggers but this time with a different class name in the exception message?
  2. Try the latest Xcode 15.1 Beta with latest iOS 17.x beta and see if it still triggers. Another customer had a strange issue seemingly with our code base that made no sense, but was fixed in the latest Xcode beta, indicating it was some kind of compiler error.

Otherwise, I'm not sure what we can do. We don't use the UIFocus API at all in any way that I can see, so there's nothing we can remove.

djs-code commented 11 months ago

Hi @einsteinx2, thanks for the response. Indeed, I do not see any usage of the UIFocus APIs in the mParticle iOS SDK. From my investigation, I've concluded that this is happening because our App Delegate is a subclass of UIResponder, whose respondsToSelector: returns YES for _wantsPriorityOverFocusUpdates.

The selector resolution machinery here is certainly acting in a way I do not expect, but I suspect a possible workaround for this might be modifying [MPAppDelegateProxy respondsToSelector:] as follows:

- (BOOL)respondsToSelector:(SEL)aSelector {
    BOOL respondsToSelector = NO;
    if (aSelector == originalAppDelegateSelector) {
        respondsToSelector = YES;
    } else if ([_originalAppDelegate respondsToSelector:aSelector] && !([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion >= 17 && aSelector == NSSelectorFromString(@"_wantsPriorityOverFocusUpdates"))) {
        respondsToSelector = YES;
    } else if ([self.surrogateAppDelegate implementsSelector:aSelector]) {
        respondsToSelector = YES;
    }

    return respondsToSelector;
}
einsteinx2 commented 11 months ago

@djs-code thanks for the quick follow up. I'm hesitant to add a workaround like this to the code, as this will likely be fixed by Apple shortly and having the App Delegate subclass UIResponder is not a common pattern afaik, but the workaround will end up in the codebase forever.

Since we're already calling [_originalAppDelegate respondsToSelector:aSelector], would you be able to implement the !([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion >= 17 && aSelector == NSSelectorFromString(@"_wantsPriorityOverFocusUpdates")) check in your App delegate's own - (BOOL)respondsToSelector:(SEL)aSelector method? If you return NO on that condition, we will then also return NO in the proxy, which should achieve the same result.

Since Apple seems to be indicating that _wantsPriorityOverFocusUpdates is no longer supported (even though they seem to still be using it, which is probably the real bug here) I assume you don't actually need to handle those selectors anyway. Also if we added it to the proxy, it would also not be handled by your App Delegate since we are responding to respondsToSelector for you, so if I'm understanding this correctly, putting it in your own app delegate should have the exact same result as modifying it in the SDK's proxy class.

Also that would be a great way to quickly confirm that it does fix the issue. If it does fix it, we can have some internal discussions about whether this belongs in the SDK or in customers' app code and in the meantime you can immediately release an app update with the fix.

arolan commented 2 months ago

Dear @einsteinx2 and mParticle Support team,

we are also experiencing the same issue / crashes on iOS as originally reported.

Our current SDK versions:

i also checked the source code for MPAppDelegateProxy.m class

Screenshot 2024-07-31 at 7 55 17 AM

I can see that original proposed solution is present now inside your mParticle SDK source code:

- (BOOL)respondsToSelector:(SEL)aSelector {
    BOOL respondsToSelector = NO;
    if (aSelector == originalAppDelegateSelector) {
        respondsToSelector = YES;
    } else if ([_originalAppDelegate respondsToSelector:aSelector]) {
        respondsToSelector = YES;
    } else if ([self.surrogateAppDelegate implementsSelector:aSelector]) {
        respondsToSelector = YES;
    }

    return respondsToSelector;
}

however we still saw the crashes.

Do you think you can investigate it further?

Here is the crash details:

SoFiApplication.sendEvent(_:)
NSInternalInconsistencyException - Class MPAppDelegateProxy implements _wantsPriorityOverFocusUpdates, which is no longer supported.

Thanks again!

iMostfa commented 1 month ago

the workaround mentioned here isn't working, and this's still affecting many ipad users

BrandonStalnaker commented 1 month ago

@arolan This workaround suggested here is not already included in the MPAppDelegateProxy.m class. Compare the 2nd conditional statement in the code you provided and the suggested workaround bellow.

- (BOOL)respondsToSelector:(SEL)aSelector {
    BOOL respondsToSelector = NO;
    if (aSelector == originalAppDelegateSelector) {
        respondsToSelector = YES;
    } else if ([_originalAppDelegate respondsToSelector:aSelector] && !([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion >= 17 && aSelector == NSSelectorFromString(@"_wantsPriorityOverFocusUpdates"))) {
        respondsToSelector = YES;
    } else if ([self.surrogateAppDelegate implementsSelector:aSelector]) {
        respondsToSelector = YES;
    }

    return respondsToSelector;
}
iMostfa commented 1 month ago

@BrandonStalnaker also the suggested workaround, isn't working. [assuming that we can put this on your lib]

- (BOOL)respondsToSelector:(SEL)aSelector {
    BOOL respondsToSelector = NO;
    if (aSelector == originalAppDelegateSelector) {
        respondsToSelector = YES;
    } else if ([_originalAppDelegate respondsToSelector:aSelector] && !([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion >= 17 && aSelector == NSSelectorFromString(@"_wantsPriorityOverFocusUpdates"))) {
        respondsToSelector = YES;
    } else if ([self.surrogateAppDelegate implementsSelector:aSelector]) {
        respondsToSelector = YES;
    }

    return respondsToSelector;
}