diff --git a/AGENTS.md b/AGENTS.md index 7da3ccf1b0..0e8160a46d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,9 @@ # Introduction +> **Note on Document Formatting:** This document (`AGENTS.md`) should be +> maintained with lines word-wrapped to a maximum of 80 characters to ensure +> readability across various editors and terminals. + This document provides context and guidance for AI agents (like Jules) when making changes to the Firebase C++ SDK repository. It covers essential information about the repository's structure, setup, testing procedures, API @@ -34,8 +38,8 @@ instructions for your specific platform. * **Android SDK & NDK**: Required for building Android libraries. `sdkmanager` can be used for installation. CMake for Android (version 3.10.2 recommended) is also needed. -* **(Windows Only) Strings**: From Microsoft Sysinternals, required for Android - builds on Windows. +* **(Windows Only) Strings**: From Microsoft Sysinternals, required for + Android builds on Windows. * **Cocoapods**: Required for building iOS or tvOS libraries. ## Building the SDK @@ -74,9 +78,10 @@ generated in each library's build directory (e.g., ### Desktop Platform Setup Details -When setting up for desktop, if you are using an iOS `GoogleService-Info.plist` -file, convert it to the required `google-services-desktop.json` using the -script: `python generate_xml_from_google_services_json.py --plist -i GoogleService-Info.plist` +When setting up for desktop, if you are using an iOS +`GoogleService-Info.plist` file, convert it to the required +`google-services-desktop.json` using the script: +`python generate_xml_from_google_services_json.py --plist -i GoogleService-Info.plist` (run this from the script's directory, ensuring the plist file is accessible). The desktop SDK searches for configuration files in the current working @@ -175,8 +180,9 @@ Database). parameters if not relying on a `google-services.json` or `GoogleService-Info.plist` file. 2. **Service Instances**: Once `firebase::App` is initialized, you generally - obtain instances of specific Firebase services using a static `GetInstance()` - method on the service's class, passing the `firebase::App` object. + obtain instances of specific Firebase services using a static + `GetInstance()` method on the service's class, passing the `firebase::App` + object. * Examples for services like Auth, Database, Storage, Firestore: * `firebase::auth::Auth* auth = firebase::auth::Auth::GetAuth(app, &init_result);` * `firebase::database::Database* database = firebase::database::Database::GetInstance(app, &init_result);` @@ -192,8 +198,8 @@ Database). called as global functions within the `firebase::analytics` namespace, rather than on an instance object obtained via `GetInstance()`. Refer to the specific product's header file for its exact - initialization mechanism if it deviates from the common `GetInstance(app, ...)` - pattern. + initialization mechanism if it deviates from the common + `GetInstance(app, ...)` pattern. ### Asynchronous Operations: `firebase::Future` @@ -205,8 +211,8 @@ where `T` is the type of the expected result. `kFutureStatusInvalid`. * **Getting Results**: Once `future.status() == kFutureStatusComplete`: * Check for errors: `future.error()`. A value of `0` (e.g., - `firebase::auth::kAuthErrorNone`, `firebase::database::kErrorNone`) - usually indicates success. + `firebase::auth::kAuthErrorNone`, + `firebase::database::kErrorNone`) usually indicates success. * Get the error message: `future.error_message()`. * Get the result: `future.result()`. This returns a pointer to the result object of type `T`. The result is only valid if `future.error()` @@ -218,8 +224,8 @@ where `T` is the type of the expected result. ### Core Classes and Operations (Examples from Auth and Database) -While each Firebase product has its own specific classes, the following examples -illustrate common API patterns: +While each Firebase product has its own specific classes, the following +examples illustrate common API patterns: * **`firebase::auth::Auth`**: The main entry point for Firebase Authentication. @@ -305,6 +311,12 @@ API documentation. as mentioned in `CONTRIBUTING.md`. * **Formatting**: Use `python3 scripts/format_code.py -git_diff -verbose` to format your code before committing. +* **Naming Precision for Dynamic Systems**: Function names should precisely + reflect their behavior, especially in systems with dynamic or asynchronous + interactions. For example, a function that processes a list of items should + be named differently from one that operates on a single, specific item + captured asynchronously. Regularly re-evaluate function names as + requirements evolve to maintain clarity. ## Comments @@ -328,8 +340,9 @@ API documentation. * **Check `Future` status and errors**: Always check `future.status()` and `future.error()` before attempting to use `future.result()`. * A common success code is `0` (e.g., - `firebase::auth::kAuthErrorNone`, `firebase::database::kErrorNone`). - Other specific error codes are defined per module (e.g., + `firebase::auth::kAuthErrorNone`, + `firebase::database::kErrorNone`). Other specific error codes are + defined per module (e.g., `firebase::auth::kAuthErrorUserNotFound`). * **Callback error parameters**: When using listeners or other callbacks, always check the provided error code and message before processing the @@ -362,6 +375,13 @@ API documentation. otherwise ensuring the `Future` completes its course, particularly for operations with side effects or those that allocate significant backend resources. +* **Lifecycle of Queued Callbacks/Blocks**: If blocks or callbacks are queued + to be run upon an asynchronous event (e.g., an App Delegate class being set + or a Future completing), clearly define and document their lifecycle. + Determine if they are one-shot (cleared after first execution) or + persistent (intended to run for multiple or future events). This impacts + how associated data and the blocks themselves are stored and cleared, + preventing memory leaks or unexpected multiple executions. ## Immutability @@ -397,6 +417,29 @@ API documentation. integration, it can occasionally be a factor to consider when debugging app delegate behavior or integrating with other libraries that also perform swizzling. + When implementing or interacting with swizzling, especially for App Delegate + methods like `[UIApplication setDelegate:]`: + * Be highly aware that `setDelegate:` can be called multiple times + with different delegate class instances, including proxy classes + from other libraries (e.g., GUL - Google Utilities). Swizzling + logic must be robust against being invoked multiple times for the + same effective method on the same class or on classes in a + hierarchy. An idempotency check (i.e., if the method's current IMP + is already the target swizzled IMP, do nothing more for that + specific swizzle attempt) in any swizzling utility can prevent + issues like recursion. + * When tracking unique App Delegate classes (e.g., for applying hooks + or callbacks via swizzling), consider the class hierarchy. If a + superclass has already been processed, processing a subclass for + the same inherited methods might be redundant or problematic. A + strategy to check if a newly set delegate is a subclass of an + already processed delegate can prevent such issues. + * For code that runs very early in the application lifecycle on + iOS/macOS (e.g., `+load` methods, static initializers involved in + swizzling), prefer using `NSLog` directly over custom logging + frameworks if there's any uncertainty about whether the custom + framework is fully initialized, to avoid crashes during logging + itself. ## Class and File Structure @@ -462,9 +505,9 @@ management: module, but the fundamental responsibility for creation and deletion in typical scenarios lies with the Pimpl class itself. -It's crucial to correctly implement all these aspects (constructors, destructor, -copy/move operators) when dealing with raw pointer Pimpls to prevent memory -leaks, dangling pointers, or double deletions. +It's crucial to correctly implement all these aspects (constructors, +destructor, copy/move operators) when dealing with raw pointer Pimpls to +prevent memory leaks, dangling pointers, or double deletions. ## Namespace Usage @@ -531,7 +574,7 @@ leaks, dangling pointers, or double deletions. This document is a living guide. As the Firebase C++ SDK evolves, new patterns may emerge, or existing practices might change. If you introduce a new common pattern, significantly alter a build process, or establish a new best practice -during your work, please take a moment to update this `Jules.md` file +during your work, please take a moment to update this `AGENTS.md` file accordingly. Keeping this document current will greatly benefit future AI agents and human @@ -541,7 +584,7 @@ developers working on this repository. ## Recommended General Prompt Instruction -When working on this task, please consistently refer to the `Jules.md` guide +When working on this task, please consistently refer to the `AGENTS.md` guide for all repository-specific conventions, including setup procedures, coding style, common architectural patterns, and API usage. Pay close attention to the testing strategies outlined, ensuring your implementation includes @@ -549,30 +592,35 @@ comprehensive integration tests with detailed test cases in your plan. Implement robust error handling for any new or modified public API methods, following the patterns for `firebase::Future` error reporting. If the feature involves platform-specific code, ensure the public API remains consistent across all -platforms, as discussed in `Jules.md`. Write clear, maintainable code, +platforms, as discussed in `AGENTS.md`. Write clear, maintainable code, adhering to the commenting guidelines, and if you need to add new third-party dependencies, document the rationale and update build configurations according to the established practices. Ensure your changes align with the overall best -practices detailed in `Jules.md`. +practices detailed in `AGENTS.md`. ## Key Directives for Jules AI -* **Prioritize `Jules.md`**: This document (`Jules.md`) contains +* **Prioritize `AGENTS.md`**: This document (`AGENTS.md`) contains repository-specific guidelines. Prioritize this information when making decisions about coding style, testing procedures, architectural patterns, and API usage. * **Adherence to Patterns**: Strive to follow the common patterns and best practices outlined here. This ensures consistency across the codebase. -* **Clarification for Deviations**: If the existing patterns in `Jules.md` do +* **Clarification for Deviations**: If the existing patterns in `AGENTS.md` do not seem suitable for a specific task, or if a deviation is necessary, please explicitly state this in your plan or request clarification before proceeding with implementation. -* **Updating `Jules.md`**: If you introduce a new, broadly applicable +* **Updating `AGENTS.md`**: If you introduce a new, broadly applicable pattern, or if a significant change to the build process or best practices occurs as part of your task, please include a step in your plan to update - `Jules.md` to reflect these changes. + `AGENTS.md` to reflect these changes. * **Testing**: Remember that integration tests are the primary method of testing. Ensure new features are accompanied by corresponding integration - tests as described in the 'Testing' section of `Jules.md`. + tests as described in the 'Testing' section of `AGENTS.md`. * **Commit Messages**: Follow standard commit message guidelines. A brief summary line, followed by a more detailed explanation if necessary. +* **Tool Limitations & Path Specificity**: If codebase search tools (like + `grep` or recursive `ls`) are limited or unavailable, and initial attempts + to locate files/modules based on common directory structures are + unsuccessful, explicitly ask for more specific paths rather than assuming a + module doesn't exist or contains no relevant code. diff --git a/app/src/invites/ios/invites_ios_startup.mm b/app/src/invites/ios/invites_ios_startup.mm index 4ae901d57a..a99602ba79 100644 --- a/app/src/invites/ios/invites_ios_startup.mm +++ b/app/src/invites/ios/invites_ios_startup.mm @@ -286,7 +286,7 @@ @implementation UIApplication (FIRFBI) + (void)load { // C++ constructors may not be called yet so call NSLog rather than LogDebug. NSLog(@"Loading UIApplication category for Firebase App"); - ::firebase::util::ForEachAppDelegateClass(^(Class clazz) { + ::firebase::util::RunOnAppDelegateClasses(^(Class clazz) { ::firebase::invites::HookAppDelegateMethods(clazz); }); } diff --git a/app/src/util_ios.h b/app/src/util_ios.h index 454fab09cd..1a69484979 100644 --- a/app/src/util_ios.h +++ b/app/src/util_ios.h @@ -185,10 +185,12 @@ typedef BOOL ( id self, SEL selector_value, UIApplication *application, NSUserActivity *user_activity, void (^restoration_handler)(NSArray *)); -// Call the given block once for every Objective-C class that exists that -// implements the UIApplicationDelegate protocol (except for those in a -// blacklist we keep). -void ForEachAppDelegateClass(void (^block)(Class)); +// Calls the given block for each unique Objective-C class that has been +// previously passed to [UIApplication setDelegate:]. The block is executed +// immediately for all currently known unique delegate classes. +// Additionally, the block is queued to be executed if any new, unique +// Objective-C class is passed to [UIApplication setDelegate:] in the future. +void RunOnAppDelegateClasses(void (^block)(Class)); // Convert a string array into an NSMutableArray. NSMutableArray *StringVectorToNSMutableArray( diff --git a/app/src/util_ios.mm b/app/src/util_ios.mm index 21a70e6c85..6286eeae4b 100644 --- a/app/src/util_ios.mm +++ b/app/src/util_ios.mm @@ -27,6 +27,103 @@ #import #import +using firebase::GetLogLevel; +using firebase::kLogLevelDebug; + +// Key used in Info.plist to specify a custom AppDelegate class name. +static NSString *const kFirebaseAppDelegateClassNameKey = @"FirebaseAppDelegateClassName"; + +#define MAX_PENDING_APP_DELEGATE_BLOCKS 8 +#define MAX_SEEN_DELEGATE_CLASSES 32 + +static IMP g_original_setDelegate_imp = NULL; +static void (^g_pending_app_delegate_blocks[MAX_PENDING_APP_DELEGATE_BLOCKS])(Class) = {nil}; +static int g_pending_block_count = 0; +static Class g_seen_delegate_classes[MAX_SEEN_DELEGATE_CLASSES] = {nil}; +static int g_seen_delegate_classes_count = 0; + +static void Firebase_setDelegate(id self, SEL _cmd, id delegate) { + Class new_class = nil; + if (delegate) { + new_class = [delegate class]; + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: UIApplication setDelegate: called with class %s (Swizzled)", + class_getName(new_class)); + } else { + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: UIApplication setDelegate: called with nil delegate (Swizzled)"); + } + + if (new_class) { + // 1. Superclass Check + bool superclass_already_seen = false; + Class current_super = class_getSuperclass(new_class); + while (current_super) { + for (int i = 0; i < g_seen_delegate_classes_count; i++) { + if (g_seen_delegate_classes[i] == current_super) { + superclass_already_seen = true; + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Delegate class %s has superclass %s which was already seen. Skipping " + @"processing for %s.", + class_getName(new_class), class_getName(current_super), class_getName(new_class)); + break; + } + } + if (superclass_already_seen) break; + current_super = class_getSuperclass(current_super); + } + + if (!superclass_already_seen) { + // 2. Direct Class Check (if no superclass was seen) + bool direct_class_already_seen = false; + for (int i = 0; i < g_seen_delegate_classes_count; i++) { + if (g_seen_delegate_classes[i] == new_class) { + direct_class_already_seen = true; + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Delegate class %s already seen directly. Skipping processing.", + class_getName(new_class)); + break; + } + } + + if (!direct_class_already_seen) { + // 3. Process as New Class + if (g_seen_delegate_classes_count < MAX_SEEN_DELEGATE_CLASSES) { + g_seen_delegate_classes[g_seen_delegate_classes_count] = new_class; + g_seen_delegate_classes_count++; + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Added new delegate class %s to seen list (total seen: %d).", + class_getName(new_class), g_seen_delegate_classes_count); + + if (g_pending_block_count > 0) { + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Executing %d pending block(s) for new delegate class: %s.", + g_pending_block_count, class_getName(new_class)); + for (int i = 0; i < g_pending_block_count; i++) { + if (g_pending_app_delegate_blocks[i]) { + g_pending_app_delegate_blocks[i](new_class); + // Pending blocks persist to run for future new delegate classes. + } + } + } + } else { + NSLog(@"Firebase Error: Exceeded MAX_SEEN_DELEGATE_CLASSES (%d). Cannot add new delegate " + @"class %s or run pending blocks for it.", + MAX_SEEN_DELEGATE_CLASSES, class_getName(new_class)); + } + } + } + } + + // Call the original setDelegate: implementation + if (g_original_setDelegate_imp) { + ((void (*)(id, SEL, id))g_original_setDelegate_imp)(self, _cmd, + delegate); + } else { + NSLog(@"Firebase Error: Original setDelegate: IMP not found, cannot call original method."); + } +} + @implementation FIRSAMAppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { @@ -76,41 +173,133 @@ - (BOOL)application:(UIApplication *)application #endif // defined(__IPHONE_12_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_12_0 @end +@implementation UIApplication (FirebaseAppDelegateSwizzling) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSString *appDelegateClassName = + [[NSBundle mainBundle] objectForInfoDictionaryKey:kFirebaseAppDelegateClassNameKey]; + + if (appDelegateClassName && [appDelegateClassName isKindOfClass:[NSString class]] && + appDelegateClassName.length > 0) { + Class specificClass = NSClassFromString(appDelegateClassName); + if (specificClass) { + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Info.plist key '%@' found. Targeting AppDelegate class: %@. Swizzling " + @"of [UIApplication setDelegate:] will be skipped.", + kFirebaseAppDelegateClassNameKey, appDelegateClassName); + + // Set this class as the sole "seen" delegate for Firebase processing. + // g_seen_delegate_classes_count should be 0 here in +load, but clear just in case. + for (int i = 0; i < g_seen_delegate_classes_count; i++) { + g_seen_delegate_classes[i] = nil; + } + g_seen_delegate_classes[0] = specificClass; + g_seen_delegate_classes_count = 1; + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: %@ is now the only delegate class Firebase will initially process.", + appDelegateClassName); + + // If there are already blocks pending (e.g., from other Firebase components' +load + // methods), execute them now for the specified delegate. These blocks will remain in the + // pending queue, mirroring the behavior of the original swizzled setDelegate: method which + // also does not clear pending blocks after execution (as they might apply to future + // delegates). In this Info.plist mode, however, Firebase won't process further setDelegate: + // calls. + if (g_pending_block_count > 0) { + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: +load (Info.plist Mode) - Executing %d PENDING block(s) for " + @"specified delegate: %@. (Blocks are not removed from queue).", + g_pending_block_count, NSStringFromClass(specificClass)); + for (int i = 0; i < g_pending_block_count; i++) { + if (g_pending_app_delegate_blocks[i]) { + g_pending_app_delegate_blocks[i](specificClass); + } + } + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: +load (Info.plist Mode) - Pending blocks executed for specific " + @"delegate."); + } + // Skip swizzling. g_original_setDelegate_imp remains NULL. + return; + } else { + NSLog(@"Firebase Error: Info.plist key '%@' specifies class '%@', which was not found. " + @"Proceeding with default swizzling.", + kFirebaseAppDelegateClassNameKey, appDelegateClassName); + } + } else { + if (appDelegateClassName) { // Key is present but value is invalid (e.g., empty string or + // wrong type). + NSLog(@"Firebase Error: Info.plist key '%@' has an invalid value ('%@'). Proceeding " + @"with default swizzling.", + kFirebaseAppDelegateClassNameKey, appDelegateClassName); + } else { // Key is not present. + // This is the default case, no special logging needed here beyond the swizzling log itself. + } + } + + // Standard behavior: Swizzle [UIApplication setDelegate:] + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Proceeding with swizzling of [UIApplication setDelegate:]."); + Class uiApplicationClass = [UIApplication class]; + SEL originalSelector = @selector(setDelegate:); + Method originalMethod = class_getInstanceMethod(uiApplicationClass, originalSelector); + + if (!originalMethod) { + NSLog( + @"Firebase Error: Original [UIApplication setDelegate:] method not found for swizzling."); + return; + } + + IMP previousImp = method_setImplementation(originalMethod, (IMP)Firebase_setDelegate); + if (previousImp) { + g_original_setDelegate_imp = previousImp; + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Successfully swizzled [UIApplication setDelegate:] and stored original " + @"IMP."); + } else { + NSLog(@"Firebase Error: Swizzled [UIApplication setDelegate:], but original IMP was NULL (or " + @"method_setImplementation failed to return the previous IMP)."); + } + }); +} + +@end + namespace firebase { namespace util { -void ForEachAppDelegateClass(void (^block)(Class)) { - unsigned int number_of_classes; - Class *classes = objc_copyClassList(&number_of_classes); - for (unsigned int i = 0; i < number_of_classes; i++) { - Class clazz = classes[i]; - if (class_conformsToProtocol(clazz, @protocol(UIApplicationDelegate))) { - const char *class_name = class_getName(clazz); - bool blacklisted = false; - static const char *kClassNameBlacklist[] = { - // Declared in Firebase Analytics: - // //googlemac/iPhone/Firebase/Analytics/Sources/ApplicationDelegate/ - // FIRAAppDelegateProxy.m - "FIRAAppDelegate", - // Declared here. - "FIRSAMAppDelegate"}; - for (size_t i = 0; i < FIREBASE_ARRAYSIZE(kClassNameBlacklist); ++i) { - if (strcmp(class_name, kClassNameBlacklist[i]) == 0) { - blacklisted = true; - break; - } - } - if (!blacklisted) { - if (GetLogLevel() <= kLogLevelDebug) { - // Call NSLog directly because we may be in a +load method, - // and C++ classes may not be constructed yet. - NSLog(@"Firebase: Found UIApplicationDelegate class %s", class_name); - } - block(clazz); +void RunOnAppDelegateClasses(void (^block)(Class)) { + if (g_seen_delegate_classes_count > 0) { + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: RunOnAppDelegateClasses executing block for %d already seen delegate " + @"class(es).", + g_seen_delegate_classes_count); + for (int i = 0; i < g_seen_delegate_classes_count; i++) { + if (g_seen_delegate_classes[i]) { // Safety check + block(g_seen_delegate_classes[i]); } } + } else { + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: RunOnAppDelegateClasses - no delegate classes seen yet. Block will be " + @"queued for future delegates."); + } + + // Always try to queue the block for any future new delegate classes. + if (g_pending_block_count < MAX_PENDING_APP_DELEGATE_BLOCKS) { + g_pending_app_delegate_blocks[g_pending_block_count] = [block copy]; + g_pending_block_count++; + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: RunOnAppDelegateClasses - added block to pending list (total pending: %d). " + @"This block will run on future new delegate classes.", + g_pending_block_count); + } else { + NSLog(@"Firebase Error: RunOnAppDelegateClasses - pending block queue is full (max %d). Cannot " + @"add new block for future execution. Discarding block.", + MAX_PENDING_APP_DELEGATE_BLOCKS); } - free(classes); } NSDictionary *StringMapToNSDictionary(const std::map &string_map) { @@ -354,8 +543,27 @@ void RunOnBackgroundThread(void (*function_ptr)(void *function_data), void *func const char *class_name = class_getName(clazz); Method method = class_getInstanceMethod(clazz, name); NSString *selector_name_nsstring = NSStringFromSelector(name); - const char *selector_name = selector_name_nsstring.UTF8String; - IMP original_method_implementation = method ? method_getImplementation(method) : nil; + const char *selector_name = selector_name_nsstring.UTF8String; // Used for logging later + + IMP current_actual_imp = method ? method_getImplementation(method) : nil; + + // === Begin idempotency check === + if (current_actual_imp == imp) { + // Assuming GetLogLevel() and kLogLevelDebug are available here. + // Based on previous file content, GetLogLevel is available in this file from util_ios.h. + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Method %s on class %s is already swizzled with the target IMP. Skipping " + @"re-swizzle.", + selector_name, class_name); + + return; // Already swizzled to the desired implementation + } + // === End idempotency check === + + // If we reach here, current_actual_imp is different from imp, or the method didn't exist. + // We now assign original_method_implementation to be current_actual_imp for the rest of the + // function. + IMP original_method_implementation = current_actual_imp; // Get the type encoding of the selector from a type_encoding_class (which is a class which // implements a stub for the method). @@ -364,9 +572,9 @@ void RunOnBackgroundThread(void (*function_ptr)(void *function_data), void *func assert(type_encoding); NSString *new_method_name_nsstring = nil; - if (GetLogLevel() <= kLogLevelDebug) { - NSLog(@"Registering method for %s selector %s", class_name, selector_name); - } + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Attempting to register method for %s selector %s", class_name, selector_name); + if (original_method_implementation) { // Try adding a method with randomized prefix on the name. int retry = kRandomNameGenerationRetries; @@ -381,32 +589,32 @@ void RunOnBackgroundThread(void (*function_ptr)(void *function_data), void *func } const char *new_method_name = new_method_name_nsstring.UTF8String; if (retry == 0) { - NSLog(@"Failed to add method %s on class %s as the %s method already exists on the class. To " - @"resolve this issue, change the name of the method %s on the class %s.", - new_method_name, class_name, new_method_name, new_method_name, class_name); + NSLog( + @"Firebase Error: Failed to add method %s on class %s as the %s method already exists on " + @"the class. To resolve this issue, change the name of the method %s on the class %s.", + new_method_name, class_name, new_method_name, new_method_name, class_name); return; } method_setImplementation(method, imp); // Save the selector name that points at the original method implementation. SetMethod(name, new_method_name_nsstring); - if (GetLogLevel() <= kLogLevelDebug) { - NSLog(@"Registered method for %s selector %s (original method %s 0x%08x)", class_name, - selector_name, new_method_name, + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Registered method for %s selector %s (original method %s 0x%08x)", + class_name, selector_name, new_method_name, static_cast(reinterpret_cast(original_method_implementation))); - } + } else if (add_method) { - if (GetLogLevel() <= kLogLevelDebug) { - NSLog(@"Adding method for %s selector %s", class_name, selector_name); - } + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Adding method for %s selector %s", class_name, selector_name); + // The class doesn't implement the selector so simply install our method implementation. if (!class_addMethod(clazz, name, imp, type_encoding)) { - NSLog(@"Failed to add new method %s on class %s.", selector_name, class_name); + NSLog(@"Firebase Error: Failed to add new method %s on class %s.", selector_name, class_name); } } else { - if (GetLogLevel() <= kLogLevelDebug) { - NSLog(@"Method implementation for %s selector %s not found, ignoring.", class_name, + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Method implementation for %s selector %s not found, ignoring.", class_name, selector_name); - } } } @@ -420,9 +628,9 @@ void RunOnBackgroundThread(void (*function_ptr)(void *function_data), void *func selector_implementation_names_per_selector_[selector_name_nsstring]; const char *class_name = class_getName(clazz); if (!selector_implementation_names) { - if (GetLogLevel() <= kLogLevelDebug) { - NSLog(@"Method not cached for class %s selector %s.", class_name, selector_name); - } + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Method not cached for class %s selector %s.", class_name, selector_name); + return nil; } @@ -440,10 +648,10 @@ void RunOnBackgroundThread(void (*function_ptr)(void *function_data), void *func search_class = clazz; for (; search_class; search_class = class_getSuperclass(search_class)) { const char *search_class_name = class_getName(search_class); - if (GetLogLevel() <= kLogLevelDebug) { - NSLog(@"Searching for selector %s (%s) on class %s", selector_name, + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Searching for selector %s (%s) on class %s", selector_name, selector_implementation_name, search_class_name); - } + Method method = class_getInstanceMethod(search_class, selector_implementation); method_implementation = method ? method_getImplementation(method) : nil; if (method_implementation) break; @@ -451,18 +659,18 @@ void RunOnBackgroundThread(void (*function_ptr)(void *function_data), void *func if (method_implementation) break; } if (!method_implementation) { - if (GetLogLevel() <= kLogLevelDebug) { - NSLog(@"Class %s does not respond to selector %s (%s)", class_name, selector_name, + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Class %s does not respond to selector %s (%s)", class_name, selector_name, selector_implementation_name_nsstring.UTF8String); - } + return nil; } - if (GetLogLevel() <= kLogLevelDebug) { - NSLog(@"Found %s (%s, 0x%08x) on class %s (%s)", selector_name, + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Found %s (%s, 0x%08x) on class %s (%s)", selector_name, selector_implementation_name_nsstring.UTF8String, static_cast(reinterpret_cast(method_implementation)), class_name, class_getName(search_class)); - } + return method_implementation; } diff --git a/messaging/src/ios/messaging.mm b/messaging/src/ios/messaging.mm index 3029bc21c0..680a29011e 100644 --- a/messaging/src/ios/messaging.mm +++ b/messaging/src/ios/messaging.mm @@ -870,7 +870,7 @@ @implementation UIApplication (FIRFCM) + (void)load { // C++ constructors may not be called yet so call NSLog rather than LogInfo. NSLog(@"FCM: Loading UIApplication FIRFCM category"); - ::firebase::util::ForEachAppDelegateClass(^(Class clazz) { + ::firebase::util::RunOnAppDelegateClasses(^(Class clazz) { FirebaseMessagingHookAppDelegate(clazz); }); } diff --git a/release_build_files/readme.md b/release_build_files/readme.md index 09b9644733..0ecbafb0d9 100644 --- a/release_build_files/readme.md +++ b/release_build_files/readme.md @@ -537,9 +537,30 @@ addition to any you may have implemented. The Firebase Cloud Messaging library needs to attach handlers to the application delegate using method swizzling. If you are using -these libraries, at load time, Firebase will identify your `AppDelegate` class -and swizzle the required methods onto it, chaining a call back to your existing -method implementation. +these libraries, at load time, Firebase will typically identify your `AppDelegate` +class and swizzle the required methods onto it. + +#### Specifying Your AppDelegate Class Directly (iOS) + +For a more direct approach, or if you encounter issues with the default +method swizzling, you can explicitly tell Firebase which class is your +application's `AppDelegate`. To do this, add the `FirebaseAppDelegateClassName` +key to your app's `Info.plist` file: + +* **Key:** `FirebaseAppDelegateClassName` +* **Type:** `String` +* **Value:** Your AppDelegate's class name (e.g., `MyCustomAppDelegate`) + +**Example `Info.plist` entry:** +```xml +FirebaseAppDelegateClassName +MyCustomAppDelegate +``` + +If this key is provided with a valid class name, Firebase will use that class +directly for its AppDelegate-related interactions. If the key is not present, +is invalid, or the class is not found, Firebase will use its standard method +swizzling approach. ### Custom Android Build Systems @@ -654,6 +675,14 @@ workflow use only during the development of your app, not for publicly shipping code. ## Release Notes +### Upcoming Release +- Changes + - iOS: Added an option to explicitly specify your app's `AppDelegate` class + name via the `FirebaseAppDelegateClassName` key in `Info.plist`. This + provides a more direct way for Firebase to interact with your specified + AppDelegate. See "Platform Notes > iOS Method Swizzling > + Specifying Your AppDelegate Class Directly (iOS)" for details. + ### 12.8.0 - Changes - General (iOS): Update to Firebase Cocoapods version 11.14.0.