Jelajahi Sumber

Merge remote-tracking branch 'origin/master' into v9b

Paul Beusterien 4 tahun lalu
induk
melakukan
99275370a2
100 mengubah file dengan 1819 tambahan dan 531 penghapusan
  1. 6 3
      .github/workflows/firestore.yml
  2. 1 0
      .github/workflows/release.yml
  3. 1 0
      .github/workflows/storage.yml
  4. 1 1
      CONTRIBUTING.md
  5. 3 0
      Crashlytics/Crashlytics/Components/FIRCLSContext.h
  6. 1 0
      Crashlytics/Crashlytics/Components/FIRCLSContext.m
  7. 2 0
      Crashlytics/Crashlytics/Components/FIRCLSUserLogging.h
  8. 4 0
      Crashlytics/Crashlytics/Components/FIRCLSUserLogging.m
  9. 20 0
      Crashlytics/Crashlytics/Controllers/FIRCLSExistingReportManager.m
  10. 6 1
      Crashlytics/Crashlytics/Controllers/FIRCLSManagerData.h
  11. 5 1
      Crashlytics/Crashlytics/Controllers/FIRCLSManagerData.m
  12. 1 0
      Crashlytics/Crashlytics/Controllers/FIRCLSReportUploader.m
  13. 15 1
      Crashlytics/Crashlytics/FIRCrashlytics.m
  14. 2 0
      Crashlytics/Crashlytics/FIRExceptionModel.m
  15. 10 0
      Crashlytics/Crashlytics/Handlers/FIRCLSException.h
  16. 128 3
      Crashlytics/Crashlytics/Handlers/FIRCLSException.mm
  17. 1 0
      Crashlytics/Crashlytics/Models/FIRCLSInternalReport.h
  18. 2 1
      Crashlytics/Crashlytics/Models/FIRCLSInternalReport.m
  19. 22 0
      Crashlytics/Crashlytics/Models/FIRCLSOnDemandModel.h
  20. 261 0
      Crashlytics/Crashlytics/Models/FIRCLSOnDemandModel.m
  21. 15 0
      Crashlytics/Crashlytics/Models/FIRCLSSettings.h
  22. 32 0
      Crashlytics/Crashlytics/Models/FIRCLSSettings.m
  23. 3 0
      Crashlytics/Crashlytics/Private/FIRCLSExistingReportManager_Private.h
  24. 49 0
      Crashlytics/Crashlytics/Private/FIRCLSOnDemandModel_Private.h
  25. 2 0
      Crashlytics/Crashlytics/Private/FIRExceptionModel_Private.h
  26. 2 1
      Crashlytics/UnitTests/FIRCLSExistingReportManagerTests.m
  27. 6 1
      Crashlytics/UnitTests/FIRCLSMetricKitManagerTests.m
  28. 257 0
      Crashlytics/UnitTests/FIRCLSOnDemandModelTests.m
  29. 6 1
      Crashlytics/UnitTests/FIRCLSReportManagerTests.m
  30. 6 1
      Crashlytics/UnitTests/FIRCLSReportUploaderTests.m
  31. 26 3
      Crashlytics/UnitTests/FIRCLSSettingsTests.m
  32. 30 0
      Crashlytics/UnitTests/Mocks/FIRCLSMockOnDemandModel.h
  33. 51 0
      Crashlytics/UnitTests/Mocks/FIRCLSMockOnDemandModel.m
  34. 1 1
      FirebaseAnalytics.podspec.json
  35. 2 2
      FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestKeyIDStorage.h
  36. 2 2
      FirebaseAppCheck/Sources/Core/Storage/FIRAppCheckStorage.h
  37. 1 1
      FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheck.h
  38. 1 1
      FirebaseAuth/Sources/Auth/FIRAuthDispatcher.h
  39. 1 1
      FirebaseAuth/Sources/Utilities/FIRAuthErrorUtils.h
  40. 3 31
      FirebaseAuth/Tests/Sample/README.md
  41. 1 1
      FirebaseAuth/Tests/Sample/Sample/AppManager.m
  42. 3 0
      FirebaseDynamicLinks/CHANGELOG.md
  43. 36 27
      FirebaseDynamicLinks/Sources/Utilities/FDLUtilities.m
  44. 41 11
      FirebaseDynamicLinks/Tests/Unit/FIRDynamicLinksTest.m
  45. 2 2
      FirebaseFirestore.podspec
  46. 4 0
      FirebaseFunctions/CHANGELOG.md
  47. 4 4
      FirebaseFunctions/Tests/Integration/IntegrationTests.swift
  48. 6 0
      FirebaseFunctions/Tests/ObjCIntegration/FIRFunctions+Internal.h
  49. 2 2
      FirebaseInAppMessaging/Sources/Runtime/FIRIAMActionURLFollower.m
  50. 2 2
      FirebaseInstallations/Source/Library/Public/FirebaseInstallations/FIRInstallations.h
  51. 1 1
      FirebaseMessaging/Sources/FIRMessagingRmqManager.h
  52. 3 0
      FirebasePerformance/CHANGELOG.md
  53. 1 29
      FirebasePerformance/Sources/AppActivity/FPRAppActivityTracker.m
  54. 1 5
      FirebasePerformance/Sources/Configurations/FPRConfigurations.h
  55. 0 80
      FirebasePerformance/Tests/Unit/FPRAppActivityTrackerTest.m
  56. 4 0
      FirebaseStorage/CHANGELOG.md
  57. 1 0
      FirebaseStorage/Sources/FIRStorageConstants.m
  58. 2 0
      FirebaseStorage/Sources/FIRStorageErrors.h
  59. 4 2
      FirebaseStorage/Sources/FIRStorageErrors.m
  60. 1 1
      FirebaseStorage/Sources/FIRStorageUploadTask.m
  61. 1 1
      FirebaseStorage/Sources/FIRStorageUtils.m
  62. 0 5
      FirebaseStorage/Sources/Public/FirebaseStorage/FIRStorageConstants.h
  63. 3 1
      Firestore/CHANGELOG.md
  64. 14 0
      Firestore/Example/Firestore.xcodeproj/project.pbxproj
  65. 1 1
      Firestore/Example/Tests/API/FSTAPIHelpers.h
  66. 1 0
      Firestore/Protos/CMakeLists.txt
  67. 15 15
      Firestore/Source/Public/FirebaseFirestore/FIRCollectionReference.h
  68. 7 7
      Firestore/Source/Public/FirebaseFirestore/FIRDocumentChange.h
  69. 39 38
      Firestore/Source/Public/FirebaseFirestore/FIRDocumentReference.h
  70. 14 14
      Firestore/Source/Public/FirebaseFirestore/FIRDocumentSnapshot.h
  71. 15 15
      Firestore/Source/Public/FirebaseFirestore/FIRFieldValue.h
  72. 44 45
      Firestore/Source/Public/FirebaseFirestore/FIRFirestore.h
  73. 1 1
      Firestore/Source/Public/FirebaseFirestore/FIRFirestoreErrors.h
  74. 4 4
      Firestore/Source/Public/FirebaseFirestore/FIRFirestoreSettings.h
  75. 1 1
      Firestore/Source/Public/FirebaseFirestore/FIRListenerRegistration.h
  76. 3 4
      Firestore/Source/Public/FirebaseFirestore/FIRLoadBundleTask.h
  77. 82 83
      Firestore/Source/Public/FirebaseFirestore/FIRQuery.h
  78. 6 6
      Firestore/Source/Public/FirebaseFirestore/FIRQuerySnapshot.h
  79. 7 7
      Firestore/Source/Public/FirebaseFirestore/FIRSnapshotMetadata.h
  80. 4 4
      Firestore/Source/Public/FirebaseFirestore/FIRTimestamp.h
  81. 15 15
      Firestore/Source/Public/FirebaseFirestore/FIRTransaction.h
  82. 15 15
      Firestore/Source/Public/FirebaseFirestore/FIRWriteBatch.h
  83. 31 6
      Firestore/core/src/bundle/bundle_serializer.cc
  84. 4 0
      Firestore/core/src/bundle/bundle_serializer.h
  85. 2 1
      Firestore/core/src/core/field_filter.cc
  86. 1 1
      Firestore/core/src/core/key_field_not_in_filter.cc
  87. 1 1
      Firestore/core/src/core/order_by.cc
  88. 1 2
      Firestore/core/src/index/index_entry.h
  89. 1 1
      Firestore/core/src/local/leveldb_index_manager.h
  90. 8 0
      Firestore/core/src/local/local_documents_view.h
  91. 5 2
      Firestore/core/src/local/local_store.cc
  92. 7 0
      Firestore/core/src/local/local_store.h
  93. 154 0
      Firestore/core/src/model/target_index_matcher.cc
  94. 103 0
      Firestore/core/src/model/target_index_matcher.h
  95. 1 1
      Firestore/core/src/nanopb/fields_array.h
  96. 1 1
      Firestore/core/src/util/filesystem.h
  97. 3 0
      Firestore/core/test/unit/bundle/bundle_reader_test.cc
  98. 15 8
      Firestore/core/test/unit/bundle/bundle_serializer_test.cc
  99. 45 1
      Firestore/core/test/unit/local/counting_query_engine.cc
  100. 37 0
      Firestore/core/test/unit/local/counting_query_engine.h

+ 6 - 3
.github/workflows/firestore.yml

@@ -199,10 +199,12 @@ jobs:
       run: ./scripts/setup_bundler.sh
 
     - name: Pod lib lint
+      # TODO(#9565, b/227461966): Remove --no-analyze when absl is fixed.
       run: |
         scripts/third_party/travis/retry.sh scripts/pod_lib_lint.rb ${{ matrix.podspec }} \
             --platforms=ios \
-            --allow-warnings
+            --allow-warnings \
+            --no-analyze
 
   # `pod lib lint` takes a long time so only run the other platforms and static frameworks build in the cron.
   pod-lib-lint-cron:
@@ -231,11 +233,13 @@ jobs:
       run: ./scripts/setup_bundler.sh
 
     - name: Pod lib lint
+      # TODO(#9565, b/227461966): Remove --no-analyze when absl is fixed.
       run: |
         scripts/third_party/travis/retry.sh scripts/pod_lib_lint.rb ${{ matrix.podspec }}\
             ${{ matrix.flags }} \
             --platforms=${{ matrix.platforms }} \
-            --allow-warnings
+            --allow-warnings \
+            --no-analyze
 
   spm:
     # Don't run on private repo unless it is a PR.
@@ -273,7 +277,6 @@ jobs:
     - name: Swift Build
       run: scripts/third_party/travis/retry.sh ./scripts/build.sh FirebaseFirestoreSwift-Beta ${{ matrix.target }} spmbuildonly
 
-# Restore when FirebaseUI works with Firebase 7 (#6646)
   quickstart:
     # Don't run on private repo unless it is a PR.
     if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request'

+ 1 - 0
.github/workflows/release.yml

@@ -433,6 +433,7 @@ jobs:
       bot_token_secret: ${{ secrets.GHASecretsGPGPassphrase1 }}
       testing_repo_dir: "/tmp/test/"
       testing_repo: "firebase-ios-sdk"
+      LEGACY: true
     runs-on: macos-11
     steps:
     - uses: actions/checkout@v2

+ 1 - 0
.github/workflows/storage.yml

@@ -98,6 +98,7 @@ jobs:
   #   env:
   #     plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }}
   #     signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }}
+  #     LEGACY: true
   #   runs-on: macos-11
   #   steps:
   #   - uses: actions/checkout@v2

+ 1 - 1
CONTRIBUTING.md

@@ -130,7 +130,7 @@ To develop Firebase software, **install**:
    To install [clang-format] and [mint] using [Homebrew]:
 
     ```console
-    brew install clang-format@13
+    brew install clang-format@14
     brew install mint
     ```
 

+ 3 - 0
Crashlytics/Crashlytics/Components/FIRCLSContext.h

@@ -45,6 +45,9 @@ typedef struct {
   volatile bool debuggerAttached;
   const char* previouslyCrashedFileFullPath;
   const char* logPath;
+  // Initial report path represents the report path used to initialized the context;
+  // where non-on-demand exceptions and other crashes will be written.
+  const char* initialReportPath;
 #if CLS_USE_SIGALTSTACK
   void* signalStack;
 #endif

+ 1 - 0
Crashlytics/Crashlytics/Components/FIRCLSContext.m

@@ -100,6 +100,7 @@ bool FIRCLSContextInitialize(FIRCLSInternalReport* report,
 
   // setup our SDK log file synchronously, because other calls may depend on it
   _firclsContext.readonly->logPath = FIRCLSContextAppendToRoot(rootPath, @"sdk.log");
+  _firclsContext.readonly->initialReportPath = FIRCLSDupString([report.path UTF8String]);
   if (!FIRCLSUnlinkIfExists(_firclsContext.readonly->logPath)) {
     FIRCLSErrorLog(@"Unable to write initialize SDK write paths %s", strerror(errno));
   }

+ 2 - 0
Crashlytics/Crashlytics/Components/FIRCLSUserLogging.h

@@ -31,6 +31,8 @@ extern NSString* const FIRCLSUserNameKey;
 extern NSString* const FIRCLSUserEmailKey;
 extern NSString* const FIRCLSDevelopmentPlatformNameKey;
 extern NSString* const FIRCLSDevelopmentPlatformVersionKey;
+extern NSString* const FIRCLSOnDemandRecordedExceptionsKey;
+extern NSString* const FIRCLSOnDemandDroppedExceptionsKey;
 #endif
 
 extern const uint32_t FIRCLSUserLoggingMaxKVEntries;

+ 4 - 0
Crashlytics/Crashlytics/Components/FIRCLSUserLogging.m

@@ -32,6 +32,10 @@ NSString *const FIRCLSUserIdentifierKey = @"com.crashlytics.user-id";
 NSString *const FIRCLSDevelopmentPlatformNameKey = @"com.crashlytics.development-platform-name";
 NSString *const FIRCLSDevelopmentPlatformVersionKey =
     @"com.crashlytics.development-platform-version";
+NSString *const FIRCLSOnDemandRecordedExceptionsKey =
+    @"com.crashlytics.on-demand.recorded-exceptions";
+NSString *const FIRCLSOnDemandDroppedExceptionsKey =
+    @"com.crashlytics.on-demand.dropped-exceptions";
 
 // Empty string object synchronized on to prevent a race condition when accessing AB file path
 NSString *const FIRCLSSynchronizedPathKey = @"";

+ 20 - 0
Crashlytics/Crashlytics/Controllers/FIRCLSExistingReportManager.m

@@ -22,6 +22,7 @@
 #import "Crashlytics/Crashlytics/Models/FIRCLSFileManager.h"
 #import "Crashlytics/Crashlytics/Models/FIRCLSInternalReport.h"
 #import "Crashlytics/Crashlytics/Models/FIRCLSSettings.h"
+#import "Crashlytics/Crashlytics/Private/FIRCLSOnDemandModel_Private.h"
 #import "Crashlytics/Crashlytics/Private/FIRCrashlyticsReport_Private.h"
 #import "Crashlytics/Crashlytics/Public/FirebaseCrashlytics/FIRCrashlyticsReport.h"
 
@@ -35,6 +36,7 @@ NSUInteger const FIRCLSMaxUnsentReports = 4;
 @property(nonatomic, strong) NSOperationQueue *operationQueue;
 @property(nonatomic, strong) FIRCLSSettings *settings;
 @property(nonatomic, strong) FIRCLSDataCollectionArbiter *dataArbiter;
+@property(nonatomic, strong) FIRCLSOnDemandModel *onDemandModel;
 
 // This list of active reports excludes the brand new active report that will be created this run of
 // the app.
@@ -60,6 +62,7 @@ NSUInteger const FIRCLSMaxUnsentReports = 4;
   _operationQueue = managerData.operationQueue;
   _dataArbiter = managerData.dataArbiter;
   _reportUploader = reportUploader;
+  _onDemandModel = managerData.onDemandModel;
 
   return self;
 }
@@ -178,6 +181,13 @@ NSInteger compareNewer(FIRCLSInternalReport *reportA,
                                  asUrgent:urgent];
   }
 
+  for (NSString *path in self.onDemandModel.storedActiveReportPaths) {
+    [self processExistingActiveReportPath:path
+                      dataCollectionToken:dataCollectionToken
+                                 asUrgent:urgent];
+  }
+  [self.onDemandModel.storedActiveReportPaths removeAllObjects];
+
   // deal with stuff in processing more carefully - do not process again
   [self.operationQueue addOperationWithBlock:^{
     for (NSString *path in self.processingReportPaths) {
@@ -248,4 +258,14 @@ NSInteger compareNewer(FIRCLSInternalReport *reportA,
   }];
 }
 
+- (void)handleOnDemandReportUpload:(NSString *)path
+               dataCollectionToken:(FIRCLSDataCollectionToken *)dataCollectionToken
+                          asUrgent:(BOOL)urgent {
+  dispatch_async(self.operationQueue.underlyingQueue, ^{
+    [self processExistingActiveReportPath:path
+                      dataCollectionToken:dataCollectionToken
+                                 asUrgent:YES];
+  });
+}
+
 @end

+ 6 - 1
Crashlytics/Crashlytics/Controllers/FIRCLSManagerData.h

@@ -22,6 +22,7 @@ NS_ASSUME_NONNULL_BEGIN
 @class FIRCLSApplicationIdentifierModel;
 @class FIRCLSInstallIdentifierModel;
 @class FIRCLSExecutionIdentifierModel;
+@class FIRCLSOnDemandModel;
 @class FIRCLSSettings;
 @class FIRCLSLaunchMarkerModel;
 @class GDTCORTransport;
@@ -46,7 +47,8 @@ NS_ASSUME_NONNULL_BEGIN
                           analytics:(nullable id<FIRAnalyticsInterop>)analytics
                         fileManager:(FIRCLSFileManager *)fileManager
                         dataArbiter:(FIRCLSDataCollectionArbiter *)dataArbiter
-                           settings:(FIRCLSSettings *)settings NS_DESIGNATED_INITIALIZER;
+                           settings:(FIRCLSSettings *)settings
+                      onDemandModel:(FIRCLSOnDemandModel *)onDemandModel NS_DESIGNATED_INITIALIZER;
 
 - (instancetype)init NS_UNAVAILABLE;
 + (instancetype)new NS_UNAVAILABLE;
@@ -72,6 +74,9 @@ NS_ASSUME_NONNULL_BEGIN
 // Uniquely identifies a run of the app
 @property(nonatomic, strong) FIRCLSExecutionIdentifierModel *executionIDModel;
 
+// Handles storing and uploading of on-demand events
+@property(nonatomic, readonly) FIRCLSOnDemandModel *onDemandModel;
+
 // Settings fetched from the server
 @property(nonatomic, strong) FIRCLSSettings *settings;
 

+ 5 - 1
Crashlytics/Crashlytics/Controllers/FIRCLSManagerData.m

@@ -17,6 +17,8 @@
 #import "Crashlytics/Crashlytics/Components/FIRCLSApplication.h"
 #import "Crashlytics/Crashlytics/Models/FIRCLSExecutionIdentifierModel.h"
 #import "Crashlytics/Crashlytics/Models/FIRCLSInstallIdentifierModel.h"
+#import "Crashlytics/Crashlytics/Models/FIRCLSSettings.h"
+#import "Crashlytics/Crashlytics/Private/FIRCLSOnDemandModel_Private.h"
 #import "Crashlytics/Crashlytics/Settings/Models/FIRCLSApplicationIdentifierModel.h"
 
 @implementation FIRCLSManagerData
@@ -27,7 +29,8 @@
                           analytics:(nullable id<FIRAnalyticsInterop>)analytics
                         fileManager:(FIRCLSFileManager *)fileManager
                         dataArbiter:(FIRCLSDataCollectionArbiter *)dataArbiter
-                           settings:(FIRCLSSettings *)settings {
+                           settings:(FIRCLSSettings *)settings
+                      onDemandModel:(FIRCLSOnDemandModel *)onDemandModel {
   self = [super init];
   if (!self) {
     return nil;
@@ -40,6 +43,7 @@
   _fileManager = fileManager;
   _dataArbiter = dataArbiter;
   _settings = settings;
+  _onDemandModel = onDemandModel;
 
   _appIDModel = [[FIRCLSApplicationIdentifierModel alloc] init];
   _installIDModel = [[FIRCLSInstallIdentifierModel alloc] initWithInstallations:installations];

+ 1 - 0
Crashlytics/Crashlytics/Controllers/FIRCLSReportUploader.m

@@ -23,6 +23,7 @@
 #import "Crashlytics/Crashlytics/Models/FIRCLSFileManager.h"
 #import "Crashlytics/Crashlytics/Models/FIRCLSInstallIdentifierModel.h"
 #import "Crashlytics/Crashlytics/Models/FIRCLSInternalReport.h"
+#import "Crashlytics/Crashlytics/Models/FIRCLSSettings.h"
 #import "Crashlytics/Crashlytics/Models/FIRCLSSymbolResolver.h"
 #import "Crashlytics/Crashlytics/Models/Record/FIRCLSReportAdapter.h"
 #import "Crashlytics/Crashlytics/Operations/Reports/FIRCLSProcessReportOperation.h"

+ 15 - 1
Crashlytics/Crashlytics/FIRCrashlytics.m

@@ -25,6 +25,7 @@
 #import "Crashlytics/Crashlytics/Components/FIRCLSHost.h"
 #include "Crashlytics/Crashlytics/Components/FIRCLSUserLogging.h"
 #import "Crashlytics/Crashlytics/DataCollection/FIRCLSDataCollectionArbiter.h"
+#import "Crashlytics/Crashlytics/DataCollection/FIRCLSDataCollectionToken.h"
 #import "Crashlytics/Crashlytics/FIRCLSUserDefaults/FIRCLSUserDefaults.h"
 #include "Crashlytics/Crashlytics/Handlers/FIRCLSException.h"
 #import "Crashlytics/Crashlytics/Helpers/FIRCLSDefines.h"
@@ -45,6 +46,9 @@
 #import "Crashlytics/Crashlytics/Controllers/FIRCLSNotificationManager.h"
 #import "Crashlytics/Crashlytics/Controllers/FIRCLSReportManager.h"
 #import "Crashlytics/Crashlytics/Controllers/FIRCLSReportUploader.h"
+#import "Crashlytics/Crashlytics/Private/FIRCLSExistingReportManager_Private.h"
+#import "Crashlytics/Crashlytics/Private/FIRCLSOnDemandModel_Private.h"
+#import "Crashlytics/Crashlytics/Private/FIRExceptionModel_Private.h"
 
 #import "FirebaseCore/Extension/FirebaseCoreInternal.h"
 #import "FirebaseInstallations/Source/Library/Private/FirebaseInstallationsInternal.h"
@@ -126,13 +130,16 @@ NSString *const FIRCLSGoogleTransportMappingID = @"1206";
     FIRCLSSettings *settings = [[FIRCLSSettings alloc] initWithFileManager:_fileManager
                                                                 appIDModel:appModel];
 
+    FIRCLSOnDemandModel *onDemandModel =
+        [[FIRCLSOnDemandModel alloc] initWithFIRCLSSettings:settings];
     _managerData = [[FIRCLSManagerData alloc] initWithGoogleAppID:_googleAppID
                                                   googleTransport:googleTransport
                                                     installations:installations
                                                         analytics:analytics
                                                       fileManager:_fileManager
                                                       dataArbiter:_dataArbiter
-                                                         settings:settings];
+                                                         settings:settings
+                                                    onDemandModel:onDemandModel];
 
     _reportUploader = [[FIRCLSReportUploader alloc] initWithManagerData:_managerData];
 
@@ -351,4 +358,11 @@ NSString *const FIRCLSGoogleTransportMappingID = @"1206";
   FIRCLSExceptionRecordModel(exceptionModel);
 }
 
+- (void)recordOnDemandExceptionModel:(FIRExceptionModel *)exceptionModel {
+  [self.managerData.onDemandModel
+      recordOnDemandExceptionIfQuota:exceptionModel
+           withDataCollectionEnabled:[self.dataArbiter isCrashlyticsCollectionEnabled]
+          usingExistingReportManager:self.existingReportManager];
+}
+
 @end

+ 2 - 0
Crashlytics/Crashlytics/FIRExceptionModel.m

@@ -18,6 +18,8 @@
 
 @property(nonatomic, copy) NSString *name;
 @property(nonatomic, copy) NSString *reason;
+@property(nonatomic) BOOL isFatal;
+@property(nonatomic) BOOL onDemand;
 
 @end
 

+ 10 - 0
Crashlytics/Crashlytics/Handlers/FIRCLSException.h

@@ -61,11 +61,21 @@ void FIRCLSExceptionRaiseTestCppException(void) __attribute((noreturn));
 
 #ifdef __OBJC__
 void FIRCLSExceptionRecordModel(FIRExceptionModel* exceptionModel);
+NSString* FIRCLSExceptionRecordOnDemandModel(FIRExceptionModel* exceptionModel,
+                                             int previousRecordedOnDemandExceptions,
+                                             int previousDroppedOnDemandExceptions);
 void FIRCLSExceptionRecordNSException(NSException* exception);
 void FIRCLSExceptionRecord(FIRCLSExceptionType type,
                            const char* name,
                            const char* reason,
                            NSArray<FIRStackFrame*>* frames);
+NSString* FIRCLSExceptionRecordOnDemand(FIRCLSExceptionType type,
+                                        const char* name,
+                                        const char* reason,
+                                        NSArray<FIRStackFrame*>* frames,
+                                        BOOL fatal,
+                                        int previousRecordedOnDemandExceptions,
+                                        int previousDroppedOnDemandExceptions);
 #endif
 
 __END_DECLS

+ 128 - 3
Crashlytics/Crashlytics/Handlers/FIRCLSException.mm

@@ -20,15 +20,20 @@
 #import "Crashlytics/Crashlytics/Private/FIRStackFrame_Private.h"
 
 #include "Crashlytics/Crashlytics/Components/FIRCLSApplication.h"
+#include "Crashlytics/Crashlytics/Components/FIRCLSContext.h"
 #include "Crashlytics/Crashlytics/Components/FIRCLSGlobals.h"
 #include "Crashlytics/Crashlytics/Components/FIRCLSProcess.h"
 #import "Crashlytics/Crashlytics/Components/FIRCLSUserLogging.h"
+
 #include "Crashlytics/Crashlytics/Handlers/FIRCLSHandler.h"
 #include "Crashlytics/Crashlytics/Helpers/FIRCLSFile.h"
 #import "Crashlytics/Crashlytics/Helpers/FIRCLSLogger.h"
 #import "Crashlytics/Crashlytics/Helpers/FIRCLSUtility.h"
 
 #import "Crashlytics/Crashlytics/Controllers/FIRCLSReportManager_Private.h"
+#import "Crashlytics/Crashlytics/Models/FIRCLSExecutionIdentifierModel.h"
+#import "Crashlytics/Crashlytics/Models/FIRCLSFileManager.h"
+#import "Crashlytics/Crashlytics/Models/FIRCLSInternalReport.h"
 #include "Crashlytics/Crashlytics/Operations/Symbolication/FIRCLSDemangleOperation.h"
 
 // C++/Objective-C exception handling
@@ -84,6 +89,18 @@ void FIRCLSExceptionRecordModel(FIRExceptionModel *exceptionModel) {
   FIRCLSExceptionRecord(FIRCLSExceptionTypeCustom, name, reason, [exceptionModel.stackTrace copy]);
 }
 
+NSString *FIRCLSExceptionRecordOnDemandModel(FIRExceptionModel *exceptionModel,
+                                             int previousRecordedOnDemandExceptions,
+                                             int previousDroppedOnDemandExceptions) {
+  const char *name = [[exceptionModel.name copy] UTF8String];
+  const char *reason = [[exceptionModel.reason copy] UTF8String] ?: "";
+
+  return FIRCLSExceptionRecordOnDemand(FIRCLSExceptionTypeCustom, name, reason,
+                                       [exceptionModel.stackTrace copy], exceptionModel.isFatal,
+                                       previousRecordedOnDemandExceptions,
+                                       previousDroppedOnDemandExceptions);
+}
+
 void FIRCLSExceptionRecordNSException(NSException *exception) {
   FIRCLSSDKLog("Recording an NSException\n");
 
@@ -213,8 +230,6 @@ void FIRCLSExceptionRecord(FIRCLSExceptionType type,
       FIRCLSHandler(&file, mach_thread_self(), NULL);
 
       FIRCLSFileClose(&file);
-
-      // disallow immediate delivery for non-native exceptions
     });
   } else {
     FIRCLSUserLoggingWriteAndCheckABFiles(
@@ -227,12 +242,122 @@ void FIRCLSExceptionRecord(FIRCLSExceptionType type,
   FIRCLSSDKLog("Finished recording an exception structure\n");
 }
 
+// Prepares a new active report for on-demand delivery and returns the path to the report.
+// Should only be used for platforms in which exceptions do not crash the app (flutter, Unity, etc).
+NSString *FIRCLSExceptionRecordOnDemand(FIRCLSExceptionType type,
+                                        const char *name,
+                                        const char *reason,
+                                        NSArray<FIRStackFrame *> *frames,
+                                        BOOL fatal,
+                                        int previousRecordedOnDemandExceptions,
+                                        int previousDroppedOnDemandExceptions) {
+  if (!FIRCLSContextIsInitialized()) {
+    return nil;
+  }
+
+  FIRCLSSDKLog("Recording an exception structure on demand\n");
+
+  FIRCLSFileManager *fileManager = [[FIRCLSFileManager alloc] init];
+
+  // Create paths for new report.
+  NSString *currentReportPath =
+      [NSString stringWithUTF8String:_firclsContext.readonly->initialReportPath];
+  NSString *newReportID = [[[FIRCLSExecutionIdentifierModel alloc] init] executionID];
+  NSString *newReportPath = [fileManager.activePath stringByAppendingPathComponent:newReportID];
+  NSString *customFatalIndicatorFilePath =
+      [newReportPath stringByAppendingPathComponent:FIRCLSCustomFatalIndicatorFile];
+  NSString *newKVPath =
+      [newReportPath stringByAppendingPathComponent:FIRCLSReportInternalIncrementalKVFile];
+
+  // Create new report and copy into it the current state of custom keys and log and the sdk.log,
+  // binary_images.clsrecord, and metadata.clsrecord files.
+  NSError *error = nil;
+  BOOL copied = [fileManager.underlyingFileManager copyItemAtPath:currentReportPath
+                                                           toPath:newReportPath
+                                                            error:&error];
+  if (error || !copied) {
+    FIRCLSSDKLog("Unable to create a new report to record on-demand exeption.");
+    return nil;
+  }
+
+  // Once the report is copied, remove non-fatal events from current report.
+  if ([fileManager
+          fileExistsAtPath:[NSString stringWithUTF8String:_firclsContext.readonly->logging
+                                                              .customExceptionStorage.aPath]]) {
+    [fileManager
+        removeItemAtPath:[NSString stringWithUTF8String:_firclsContext.readonly->logging
+                                                            .customExceptionStorage.aPath]];
+  }
+  if ([fileManager
+          fileExistsAtPath:[NSString stringWithUTF8String:_firclsContext.readonly->logging
+                                                              .customExceptionStorage.bPath]]) {
+    [fileManager
+        removeItemAtPath:[NSString stringWithUTF8String:_firclsContext.readonly->logging
+                                                            .customExceptionStorage.bPath]];
+  }
+  *_firclsContext.readonly->logging.customExceptionStorage.entryCount = 0;
+  _firclsContext.writable->exception.customExceptionCount = 0;
+
+  // Record how many on-demand exceptions occurred before this one as well as how many were dropped.
+  FIRCLSFile kvFile;
+  if (!FIRCLSFileInitWithPath(&kvFile, [newKVPath UTF8String], true)) {
+    FIRCLSSDKLogError("Unable to open k-v file\n");
+    return nil;
+  }
+  FIRCLSFileWriteSectionStart(&kvFile, "kv");
+  FIRCLSFileWriteHashStart(&kvFile);
+  FIRCLSFileWriteHashEntryHexEncodedString(&kvFile, "key",
+                                           [FIRCLSOnDemandRecordedExceptionsKey UTF8String]);
+  FIRCLSFileWriteHashEntryHexEncodedString(
+      &kvFile, "value",
+      [[[NSNumber numberWithInt:previousRecordedOnDemandExceptions] stringValue] UTF8String]);
+  FIRCLSFileWriteHashEnd(&kvFile);
+  FIRCLSFileWriteSectionEnd(&kvFile);
+  FIRCLSFileWriteSectionStart(&kvFile, "kv");
+  FIRCLSFileWriteHashStart(&kvFile);
+  FIRCLSFileWriteHashEntryHexEncodedString(&kvFile, "key",
+                                           [FIRCLSOnDemandDroppedExceptionsKey UTF8String]);
+  FIRCLSFileWriteHashEntryHexEncodedString(
+      &kvFile, "value",
+      [[[NSNumber numberWithInt:previousDroppedOnDemandExceptions] stringValue] UTF8String]);
+  FIRCLSFileWriteHashEnd(&kvFile);
+  FIRCLSFileWriteSectionEnd(&kvFile);
+  FIRCLSFileClose(&kvFile);
+
+  // If the event was fatal, write out an empty file to indicate that the report contains a fatal
+  // event. This is used to report events to Analytics for CFU calculations.
+  if (fatal && ![fileManager createFileAtPath:customFatalIndicatorFilePath
+                                     contents:nil
+                                   attributes:nil]) {
+    FIRCLSSDKLog("Unable to create custom exception file. On demand exception will not be logged "
+                 "with analytics.");
+  }
+
+  // Write out the exception in the new report.
+  const char *newActiveCustomExceptionPath =
+      fatal ? [[newReportPath stringByAppendingPathComponent:FIRCLSReportExceptionFile] UTF8String]
+            : [[newReportPath stringByAppendingPathComponent:FIRCLSReportCustomExceptionAFile]
+                  UTF8String];
+  FIRCLSFile file;
+  if (!FIRCLSFileInitWithPath(&file, newActiveCustomExceptionPath, true)) {
+    FIRCLSSDKLog("Unable to open log file for on demand custom exception\n");
+    return nil;
+  }
+  FIRCLSExceptionWrite(&file, type, name, reason, frames);
+  FIRCLSHandler(&file, mach_thread_self(), NULL);
+  FIRCLSFileClose(&file);
+
+  // Return the path to the new report.
+  FIRCLSSDKLog("Finished recording on demand exception structure\n");
+  return newReportPath;
+}
+
 // Ignore this message here, because we know that this call will not leak.
 #pragma clang diagnostic push
 #pragma clang diagnostic ignored "-Winvalid-noreturn"
 void FIRCLSExceptionRaiseTestObjCException(void) {
   [NSException raise:@"CrashlyticsTestException"
-              format:@"This is an Objective-C exception using for testing."];
+              format:@"This is an Objective-C exception used for testing."];
 }
 
 void FIRCLSExceptionRaiseTestCppException(void) {

+ 1 - 0
Crashlytics/Crashlytics/Models/FIRCLSInternalReport.h

@@ -16,6 +16,7 @@
 
 #include "Crashlytics/Crashlytics/Helpers/FIRCLSFeatures.h"
 
+extern NSString *const FIRCLSCustomFatalIndicatorFile;
 extern NSString *const FIRCLSReportBinaryImageFile;
 extern NSString *const FIRCLSReportExceptionFile;
 extern NSString *const FIRCLSReportCustomExceptionAFile;

+ 2 - 1
Crashlytics/Crashlytics/Models/FIRCLSInternalReport.m

@@ -21,6 +21,7 @@
 #import "Crashlytics/Crashlytics/Helpers/FIRCLSLogger.h"
 #import "Crashlytics/Crashlytics/Models/FIRCLSFileManager.h"
 
+NSString *const FIRCLSCustomFatalIndicatorFile = @"custom_fatal.clsrecord";
 NSString *const FIRCLSReportBinaryImageFile = @"binary_images.clsrecord";
 NSString *const FIRCLSReportExceptionFile = @"exception.clsrecord";
 NSString *const FIRCLSReportCustomExceptionAFile = @"custom_exception_a.clsrecord";
@@ -135,7 +136,7 @@ NSString *const FIRCLSReportUserCompactedKVFile = @"user_compacted_kv.clsrecord"
 #if CLS_MACH_EXCEPTION_SUPPORTED
       FIRCLSReportMachExceptionFile,
 #endif
-      FIRCLSReportSignalFile, FIRCLSMetricKitFatalReportFile
+      FIRCLSReportSignalFile, FIRCLSMetricKitFatalReportFile, FIRCLSCustomFatalIndicatorFile
     ];
   });
   return files;

+ 22 - 0
Crashlytics/Crashlytics/Models/FIRCLSOnDemandModel.h

@@ -0,0 +1,22 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+NS_SWIFT_NAME(OnDemandModel)
+@interface FIRCLSOnDemandModel : NSObject
+
+- (instancetype)init NS_UNAVAILABLE;
+
+@end

+ 261 - 0
Crashlytics/Crashlytics/Models/FIRCLSOnDemandModel.m

@@ -0,0 +1,261 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <Foundation/Foundation.h>
+
+#import "Crashlytics/Crashlytics/Components/FIRCLSApplication.h"
+#import "Crashlytics/Crashlytics/DataCollection/FIRCLSDataCollectionArbiter.h"
+#import "Crashlytics/Crashlytics/DataCollection/FIRCLSDataCollectionToken.h"
+#import "Crashlytics/Crashlytics/Handlers/FIRCLSException.h"
+#import "Crashlytics/Crashlytics/Helpers/FIRCLSLogger.h"
+#include "Crashlytics/Crashlytics/Helpers/FIRCLSUtility.h"
+#import "Crashlytics/Crashlytics/Models/FIRCLSOnDemandModel.h"
+#import "Crashlytics/Crashlytics/Private/FIRCLSOnDemandModel_Private.h"
+
+#include <math.h>
+
+@interface FIRCLSOnDemandModel ()
+
+@property(nonatomic, readonly) int recordedOnDemandExceptionCount;
+@property(nonatomic, readonly) int droppedOnDemandExceptionCount;
+@property(nonatomic, readonly) int queuedOperationsCount;
+
+@property(nonatomic, strong) FIRCLSSettings *settings;
+@property(nonatomic, strong) NSOperationQueue *operationQueue;
+@property(nonatomic, strong) dispatch_queue_t dispatchQueue;
+
+@property(nonatomic) double lastUpdated;
+@property(nonatomic) double currentStep;
+
+@property(nonatomic, strong) NSFileManager *fileManager;
+@property(nonatomic, strong) NSMutableArray *storedActiveReportPaths;
+
+@end
+
+@implementation FIRCLSOnDemandModel
+
+@synthesize recordedOnDemandExceptionCount = _recordedOnDemandExceptionCount;
+@synthesize droppedOnDemandExceptionCount = _droppedOnDemandExceptionCount;
+@synthesize queuedOperationsCount = _queuedOperationsCount;
+
+static const double MAX_DELAY_SEC = 3600;
+static const double SEC_PER_MINUTE = 60;
+
+- (instancetype)initWithFIRCLSSettings:(FIRCLSSettings *)settings {
+  self = [super init];
+  if (!self) {
+    return nil;
+  }
+
+  _settings = settings;
+
+  NSString *sdkBundleID = FIRCLSApplicationGetSDKBundleID();
+  _operationQueue = [NSOperationQueue new];
+  [_operationQueue setMaxConcurrentOperationCount:1];
+  [_operationQueue setName:[sdkBundleID stringByAppendingString:@".on-demand-queue"]];
+  _dispatchQueue = dispatch_queue_create("com.google.firebase.crashlytics.on.demand", 0);
+  _operationQueue.underlyingQueue = _dispatchQueue;
+
+  _queuedOperationsCount = 0;
+
+  _recordedOnDemandExceptionCount = 0;
+  _droppedOnDemandExceptionCount = 0;
+
+  _lastUpdated = [NSDate timeIntervalSinceReferenceDate];
+  _currentStep = -1;
+  _fileManager = [[NSFileManager alloc] init];
+
+  self.storedActiveReportPaths = [NSMutableArray array];
+
+  return self;
+}
+
+/*
+ * Called from FIRCrashlytics whenever the on-demand record exception method is called. Handles
+ * rate limiting and exponential backoff.
+ */
+- (BOOL)recordOnDemandExceptionIfQuota:(FIRExceptionModel *)exceptionModel
+             withDataCollectionEnabled:(BOOL)dataCollectionEnabled
+            usingExistingReportManager:(FIRCLSExistingReportManager *)existingReportManager {
+  // Record the exception model into a new report if there is unused on-demand quota. Otherwise,
+  // log the occurence but drop the event.
+  @synchronized(self) {
+    if ([self isQueueFull]) {
+      FIRCLSDebugLog(@"No available on-demand quota, dropping report");
+      [self incrementDroppedExceptionCount];
+      [self incrementRecordedExceptionCount];
+      return NO;  // Didn't record or submit the exception because no quota was available.
+    }
+
+    FIRCLSDataCollectionToken *dataCollectionToken = [FIRCLSDataCollectionToken validToken];
+    NSString *activeReportPath = FIRCLSExceptionRecordOnDemandModel(
+        exceptionModel, self.recordedOnDemandExceptionCount, self.droppedOnDemandExceptionCount);
+
+    if (!activeReportPath) {
+      FIRCLSErrorLog(@"Error recording on-demand exception");
+      return NO;  // Something went wrong when recording the exception, so we don't have a valid
+                  // path.
+    }
+
+    // Only submit an exception report if data collection is enabled. Otherwise, the report
+    // is stored until send or delete unsent reports is called.
+    [self incrementQueuedOperationCount];
+    [self incrementRecordedExceptionCount];
+    [self resetDroppedExceptionCount];
+
+    [self.operationQueue addOperationWithBlock:^{
+      double uploadDelay = [self calculateUploadDelay];
+
+      if (dataCollectionEnabled) {
+        [existingReportManager handleOnDemandReportUpload:activeReportPath
+                                      dataCollectionToken:dataCollectionToken
+                                                 asUrgent:YES];
+        FIRCLSDebugLog(@"Submitted an on-demand exception, starting delay %.20f", uploadDelay);
+      } else {
+        [self.storedActiveReportPaths insertObject:activeReportPath atIndex:0];
+        if ([self.storedActiveReportPaths count] > FIRCLSMaxUnsentReports) {
+          [self.fileManager removeItemAtPath:[self.storedActiveReportPaths lastObject] error:nil];
+          [self.storedActiveReportPaths removeLastObject];
+          [self decrementRecordedExceptionCount];
+          [self incrementDroppedExceptionCount];
+        }
+        FIRCLSDebugLog(@"Stored an on-demand exception, starting delay %.20f", uploadDelay);
+      }
+      [self implementOnDemandUploadDelay:uploadDelay];
+      [self decrementQueuedOperationCount];
+    }];
+    return YES;  // Recorded and submitted the exception.
+  }
+}
+
+- (double)calculateUploadDelay {
+  double calculatedStepDuration = [self calculateStepDuration];
+  double power = pow(self.settings.onDemandBackoffBase, calculatedStepDuration);
+  NSNumber *calculatedUploadDelay =
+      [NSNumber numberWithDouble:(SEC_PER_MINUTE / self.settings.onDemandUploadRate) * power];
+  NSComparisonResult result =
+      [[NSNumber numberWithDouble:MAX_DELAY_SEC] compare:calculatedUploadDelay];
+  return (result == NSOrderedAscending) ? MAX_DELAY_SEC : [calculatedUploadDelay doubleValue];
+}
+
+- (double)calculateStepDuration {
+  double currentTime = [NSDate timeIntervalSinceReferenceDate];
+  BOOL queueIsFull = [self isQueueFull];
+
+  if (self.currentStep == -1) {
+    self.currentStep = 0;
+    self.lastUpdated = currentTime;
+  }
+
+  double delta =
+      (currentTime - self.lastUpdated) / (double)self.settings.onDemandBackoffStepDuration;
+  double queueFullDuration = (self.currentStep + delta) > 100 ? 100 : (self.currentStep + delta);
+  double queueNotFullDuration = (self.currentStep - delta) < 0 ? 0 : (self.currentStep - delta);
+  double calculatedDuration = queueIsFull ? queueFullDuration : queueNotFullDuration;
+
+  if (self.currentStep != calculatedDuration) {
+    self.currentStep = calculatedDuration;
+    self.lastUpdated = currentTime;
+  }
+
+  return calculatedDuration;
+}
+
+- (void)implementOnDemandUploadDelay:(int)delay {
+  sleep(delay);
+}
+
+- (int)droppedOnDemandExceptionCount {
+  @synchronized(self) {
+    return _droppedOnDemandExceptionCount;
+  }
+}
+
+- (void)setDroppedOnDemandExceptionCount:(int)count {
+  @synchronized(self) {
+    _droppedOnDemandExceptionCount = count;
+  }
+}
+
+- (void)incrementDroppedExceptionCount {
+  @synchronized(self) {
+    [self setDroppedOnDemandExceptionCount:[self droppedOnDemandExceptionCount] + 1];
+  }
+}
+
+- (void)decrementDroppedExceptionCount {
+  @synchronized(self) {
+    [self setDroppedOnDemandExceptionCount:[self droppedOnDemandExceptionCount] - 1];
+  }
+}
+
+- (void)resetDroppedExceptionCount {
+  @synchronized(self) {
+    [self setDroppedOnDemandExceptionCount:0];
+  }
+}
+
+- (int)recordedOnDemandExceptionCount {
+  @synchronized(self) {
+    return _recordedOnDemandExceptionCount;
+  }
+}
+
+- (void)setRecordedOnDemandExceptionCount:(int)count {
+  @synchronized(self) {
+    _recordedOnDemandExceptionCount = count;
+  }
+}
+
+- (void)incrementRecordedExceptionCount {
+  @synchronized(self) {
+    [self setRecordedOnDemandExceptionCount:[self recordedOnDemandExceptionCount] + 1];
+  }
+}
+
+- (void)decrementRecordedExceptionCount {
+  @synchronized(self) {
+    [self setRecordedOnDemandExceptionCount:[self recordedOnDemandExceptionCount] - 1];
+  }
+}
+
+- (int)getQueuedOperationsCount {
+  @synchronized(self) {
+    return _queuedOperationsCount;
+  }
+}
+
+- (void)setQueuedOperationsCount:(int)count {
+  @synchronized(self) {
+    _queuedOperationsCount = count;
+  }
+}
+
+- (void)incrementQueuedOperationCount {
+  @synchronized(self) {
+    [self setQueuedOperationsCount:[self getQueuedOperationsCount] + 1];
+  }
+}
+
+- (void)decrementQueuedOperationCount {
+  @synchronized(self) {
+    [self setQueuedOperationsCount:[self getQueuedOperationsCount] - 1];
+  }
+}
+
+- (BOOL)isQueueFull {
+  return ([self getQueuedOperationsCount] >= self.settings.onDemandUploadRate);
+}
+
+@end

+ 15 - 0
Crashlytics/Crashlytics/Models/FIRCLSSettings.h

@@ -106,6 +106,21 @@ NS_ASSUME_NONNULL_BEGIN
  */
 @property(nonatomic, readonly) uint32_t maxCustomKeys;
 
+/**
+ * Returns the initial upload rate for on-demand exception reporting.
+ */
+@property(nonatomic, readonly) double onDemandUploadRate;
+
+/**
+ * Base exponent used when exponential backoff is triggered for on-demand reporting.
+ */
+@property(nonatomic, readonly) double onDemandBackoffBase;
+
+/**
+ * Step duration to use with exponential backoff for on-demand reporting.
+ */
+@property(nonatomic, readonly) uint32_t onDemandBackoffStepDuration;
+
 @end
 
 NS_ASSUME_NONNULL_END

+ 32 - 0
Crashlytics/Crashlytics/Models/FIRCLSSettings.m

@@ -325,4 +325,36 @@ NSString *const AppVersion = @"app_version";
   return 64;
 }
 
+#pragma mark - On Demand Reporting Parameters
+
+- (double)onDemandUploadRate {
+  NSNumber *value = self.settingsDictionary[@"on_demand_upload_rate_per_minute"];
+
+  if (value != nil) {
+    return value.doubleValue;
+  }
+
+  return 10;  // on-demand uploads allowed per minute
+}
+
+- (double)onDemandBackoffBase {
+  NSNumber *value = self.settingsDictionary[@"on_demand_backoff_base"];
+
+  if (value != nil) {
+    return [value doubleValue];
+  }
+
+  return 1.5;  // base of exponent for exponential backoff
+}
+
+- (uint32_t)onDemandBackoffStepDuration {
+  NSNumber *value = self.settingsDictionary[@"on_demand_backoff_step_duration_seconds"];
+
+  if (value != nil) {
+    return value.unsignedIntValue;
+  }
+
+  return 6;  // step duration for exponential backoff
+}
+
 @end

+ 3 - 0
Crashlytics/Crashlytics/Private/FIRCLSExistingReportManager_Private.h

@@ -28,6 +28,9 @@
 @property(nonatomic, strong) NSArray *processingReportPaths;
 @property(nonatomic, strong) NSArray *preparedReportPaths;
 
+- (void)handleOnDemandReportUpload:(NSString *)path
+               dataCollectionToken:(FIRCLSDataCollectionToken *)dataCollectionToken
+                          asUrgent:(BOOL)urgent;
 @end
 
 #endif /* FIRCLSExistingReportManager_Private_h */

+ 49 - 0
Crashlytics/Crashlytics/Private/FIRCLSOnDemandModel_Private.h

@@ -0,0 +1,49 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef FIRCLSOnDemandModel_Private_h
+#define FIRCLSOnDemandModel_Private_h
+
+#import <Foundation/Foundation.h>
+
+#import "Crashlytics/Crashlytics/Models/FIRCLSOnDemandModel.h"
+#import "Crashlytics/Crashlytics/Models/FIRCLSSettings.h"
+#import "Crashlytics/Crashlytics/Private/FIRCLSExistingReportManager_Private.h"
+#import "Crashlytics/Crashlytics/Private/FIRExceptionModel_Private.h"
+
+@interface FIRCLSOnDemandModel (Private)
+
+- (instancetype)initWithFIRCLSSettings:(FIRCLSSettings *)settings;
+
+- (BOOL)recordOnDemandExceptionIfQuota:(FIRExceptionModel *)exceptionModel
+             withDataCollectionEnabled:(BOOL)dataCollectionEnabled
+            usingExistingReportManager:(FIRCLSExistingReportManager *)existingReportManager;
+
+- (int)getQueuedOperationsCount;
+- (void)setQueuedOperationsCount:(int)count;
+
+// When data collection is off, stores active paths that have been recorded but not dispatched for
+// upload. Kept sorted (newest at front) so that we can limit on-device reports to the newest
+// `FIRCLSMaxUnsentReports` reports.
+@property(nonatomic, strong) NSMutableArray *storedActiveReportPaths;
+
+@property(nonatomic, readonly) int recordedOnDemandExceptionCount;
+@property(nonatomic, readonly) int droppedOnDemandExceptionCount;
+@property(nonatomic, readonly) int queuedOperationsCount;
+
+@property(nonatomic, strong) NSOperationQueue *operationQueue;
+
+@end
+
+#endif /* FIRCLSOnDemandModel_Private_h */

+ 2 - 0
Crashlytics/Crashlytics/Private/FIRExceptionModel_Private.h

@@ -25,6 +25,8 @@ NS_ASSUME_NONNULL_BEGIN
 
 @property(nonatomic, copy) NSString *name;
 @property(nonatomic, copy) NSString *reason;
+@property(nonatomic) BOOL isFatal;
+@property(nonatomic) BOOL onDemand;
 
 @end
 

+ 2 - 1
Crashlytics/UnitTests/FIRCLSExistingReportManagerTests.m

@@ -63,7 +63,8 @@
                                                           analytics:nil
                                                         fileManager:self.fileManager
                                                         dataArbiter:nil
-                                                           settings:nil];
+                                                           settings:nil
+                                                      onDemandModel:nil];
 #pragma clang diagnostic pop
 
   self.mockReportUploader = [[FIRCLSMockReportUploader alloc] initWithManagerData:self.managerData];

+ 6 - 1
Crashlytics/UnitTests/FIRCLSMetricKitManagerTests.m

@@ -94,13 +94,18 @@ API_AVAILABLE(ios(14))
   FIRCLSMockSettings *mockSettings =
       [[FIRCLSMockSettings alloc] initWithFileManager:self.fileManager appIDModel:appIDModel];
 
+  // Allow nil values only in tests
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wnonnull"
   _managerData = [[FIRCLSManagerData alloc] initWithGoogleAppID:TEST_GOOGLE_APP_ID
                                                 googleTransport:mockGoogleTransport
                                                   installations:iid
                                                       analytics:nil
                                                     fileManager:self.fileManager
                                                     dataArbiter:self.dataArbiter
-                                                       settings:mockSettings];
+                                                       settings:mockSettings
+                                                  onDemandModel:nil];
+#pragma clang diagnostic pop
 
   self.mockReportUploader = [[FIRCLSMockReportUploader alloc] initWithManagerData:self.managerData];
 

+ 257 - 0
Crashlytics/UnitTests/FIRCLSOnDemandModelTests.m

@@ -0,0 +1,257 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <XCTest/XCTest.h>
+
+#include "Crashlytics/Crashlytics/Components/FIRCLSContext.h"
+#import "Crashlytics/Crashlytics/Controllers/FIRCLSManagerData.h"
+#import "Crashlytics/Crashlytics/Models/FIRCLSExecutionIdentifierModel.h"
+#import "Crashlytics/Crashlytics/Models/FIRCLSInternalReport.h"
+#import "Crashlytics/Crashlytics/Private/FIRCLSOnDemandModel_Private.h"
+#import "Crashlytics/UnitTests/Mocks/FIRAppFake.h"
+#import "Crashlytics/UnitTests/Mocks/FIRCLSMockExistingReportManager.h"
+#import "Crashlytics/UnitTests/Mocks/FIRCLSMockOnDemandModel.h"
+#import "Crashlytics/UnitTests/Mocks/FIRCLSMockReportUploader.h"
+#import "Crashlytics/UnitTests/Mocks/FIRCLSMockSettings.h"
+#import "Crashlytics/UnitTests/Mocks/FIRCLSTempMockFileManager.h"
+#import "Crashlytics/UnitTests/Mocks/FIRMockGDTCoreTransport.h"
+#import "Crashlytics/UnitTests/Mocks/FIRMockInstallations.h"
+
+#import "Crashlytics/Crashlytics/DataCollection/FIRCLSDataCollectionArbiter.h"
+#import "Crashlytics/Crashlytics/DataCollection/FIRCLSDataCollectionToken.h"
+#import "Crashlytics/Crashlytics/Settings/Models/FIRCLSApplicationIdentifierModel.h"
+
+#define TEST_GOOGLE_APP_ID (@"1:632950151350:ios:d5b0d08d4f00f4b1")
+
+@interface FIRCLSOnDemandModelTests : XCTestCase
+
+@property(nonatomic, retain) FIRCLSMockOnDemandModel *onDemandModel;
+@property(nonatomic, strong) FIRCLSExistingReportManager *existingReportManager;
+@property(nonatomic, strong) FIRCLSManagerData *managerData;
+@property(nonatomic, strong) FIRCLSDataCollectionArbiter *dataArbiter;
+@property(nonatomic, strong) FIRCLSTempMockFileManager *fileManager;
+@property(nonatomic, strong) FIRCLSMockReportUploader *mockReportUploader;
+@property(nonatomic, strong) FIRCLSMockSettings *mockSettings;
+
+@end
+
+@implementation FIRCLSOnDemandModelTests
+
+- (void)setUp {
+  [super setUp];
+  FIRSetLoggerLevel(FIRLoggerLevelMax);
+
+  FIRCLSContextBaseInit();
+
+  id fakeApp = [[FIRAppFake alloc] init];
+  self.dataArbiter = [[FIRCLSDataCollectionArbiter alloc] initWithApp:fakeApp withAppInfo:@{}];
+
+  self.fileManager = [[FIRCLSTempMockFileManager alloc] init];
+
+  FIRCLSApplicationIdentifierModel *appIDModel = [[FIRCLSApplicationIdentifierModel alloc] init];
+  _mockSettings = [[FIRCLSMockSettings alloc] initWithFileManager:self.fileManager
+                                                       appIDModel:appIDModel];
+  _onDemandModel = [[FIRCLSMockOnDemandModel alloc] initWithFIRCLSSettings:_mockSettings
+                                                                sleepBlock:^(int delay){
+                                                                }];
+
+  FIRMockInstallations *iid = [[FIRMockInstallations alloc] initWithFID:@"test_token"];
+
+  FIRMockGDTCORTransport *mockGoogleTransport =
+      [[FIRMockGDTCORTransport alloc] initWithMappingID:@"id" transformers:nil target:0];
+
+  _managerData = [[FIRCLSManagerData alloc] initWithGoogleAppID:TEST_GOOGLE_APP_ID
+                                                googleTransport:mockGoogleTransport
+                                                  installations:iid
+                                                      analytics:nil
+                                                    fileManager:self.fileManager
+                                                    dataArbiter:self.dataArbiter
+                                                       settings:self.mockSettings
+                                                  onDemandModel:_onDemandModel];
+  _mockReportUploader = [[FIRCLSMockReportUploader alloc] initWithManagerData:self.managerData];
+  _existingReportManager =
+      [[FIRCLSExistingReportManager alloc] initWithManagerData:self.managerData
+                                                reportUploader:self.mockReportUploader];
+  [self.fileManager createReportDirectories];
+  [self.fileManager
+      setupNewPathForExecutionIdentifier:self.managerData.executionIDModel.executionID];
+
+  NSString *name = @"exception_model_report";
+  NSString *reportPath = [self.fileManager.rootPath stringByAppendingPathComponent:name];
+  [self.fileManager createDirectoryAtPath:reportPath];
+
+  FIRCLSInternalReport *report =
+      [[FIRCLSInternalReport alloc] initWithPath:reportPath
+                             executionIdentifier:@"TEST_EXECUTION_IDENTIFIER"];
+  FIRCLSContextInitialize(report, self.mockSettings, self.fileManager);
+}
+
+- (void)tearDown {
+  self.onDemandModel = nil;
+  [[NSFileManager defaultManager] removeItemAtPath:self.fileManager.rootPath error:nil];
+  [super tearDown];
+}
+
+- (void)setSleepBlock:(void (^)(int))sleepBlock {
+  ((FIRCLSMockOnDemandModel *)self.managerData.onDemandModel).sleepBlock = sleepBlock;
+}
+
+- (void)testIncrementsQueueWhenEventRecorded {
+  FIRExceptionModel *exceptionModel = [self getTestExceptionModel];
+  XCTestExpectation *testComplete =
+      [[XCTestExpectation alloc] initWithDescription:@"complete test"];
+
+  // Put an expectation in the sleep block so we can test the state of the queue.
+  __weak FIRCLSOnDemandModelTests *weakSelf = self;
+  [self setSleepBlock:^(int delay) {
+    XCTAssertEqual(delay, 60 / self.mockSettings.onDemandUploadRate);
+    [weakSelf waitForExpectations:@[ testComplete ] timeout:1.0];
+  }];
+
+  BOOL success = [self.onDemandModel recordOnDemandExceptionIfQuota:exceptionModel
+                                          withDataCollectionEnabled:YES
+                                         usingExistingReportManager:self.existingReportManager];
+  // Should record but not submit a report.
+  XCTAssertTrue(success);
+  XCTAssertEqual([self.onDemandModel recordedOnDemandExceptionCount], 1);
+  XCTAssertEqual(self.onDemandModel.getQueuedOperationsCount, 1);
+
+  // Fulfill the expectation so the sleep block completes.
+  [testComplete fulfill];
+}
+
+- (void)testCompliesWithDataCollectionOff {
+  FIRExceptionModel *exceptionModel = [self getTestExceptionModel];
+  XCTestExpectation *testComplete =
+      [[XCTestExpectation alloc] initWithDescription:@"complete test"];
+
+  // Put an expectation in the sleep block so we can test the state of the queue.
+  __weak FIRCLSOnDemandModelTests *weakSelf = self;
+  [self setSleepBlock:^(int delay) {
+    XCTAssertEqual(delay, 60 / self.mockSettings.onDemandUploadRate);
+    [weakSelf waitForExpectations:@[ testComplete ] timeout:1.0];
+  }];
+
+  BOOL success = [self.onDemandModel recordOnDemandExceptionIfQuota:exceptionModel
+                                          withDataCollectionEnabled:NO
+                                         usingExistingReportManager:self.existingReportManager];
+
+  // Should record but not submit a report.
+  XCTAssertTrue(success);
+  // We still count this as a recorded event if it was recorded but not submitted.
+  XCTAssertEqual([self.onDemandModel recordedOnDemandExceptionCount], 1);
+  XCTAssertEqual([self contentsOfActivePath].count, 2);
+  XCTAssertEqual(self.onDemandModel.getQueuedOperationsCount, 1);
+  XCTAssertEqual([self.onDemandModel.storedActiveReportPaths count], 1);
+
+  // Fulfill the expectation so the sleep block completes.
+  [testComplete fulfill];
+}
+
+- (void)testQuotaWithDataCollectionOff {
+  FIRExceptionModel *exceptionModel = [self getTestExceptionModel];
+
+  for (int i = 0; i < 10; i++) {
+    BOOL success =
+        [self.managerData.onDemandModel recordOnDemandExceptionIfQuota:exceptionModel
+                                             withDataCollectionEnabled:NO
+                                            usingExistingReportManager:self.existingReportManager];
+
+    XCTAssertTrue(success);
+  }
+
+  // Once we've finished processing, there should be only FIRCLSMaxUnsentReports recorded with the
+  // rest considered dropped. The recorded events should be stored in storedActiveReportPaths which
+  // is kept in sync with the contents of the active path.
+  [self.managerData.onDemandModel.operationQueue waitUntilAllOperationsAreFinished];
+  XCTAssertEqual([self.managerData.onDemandModel.operationQueue operationCount], 0);
+
+  XCTAssertEqual([self.managerData.onDemandModel recordedOnDemandExceptionCount],
+                 FIRCLSMaxUnsentReports);
+  XCTAssertEqual([self contentsOfActivePath].count, FIRCLSMaxUnsentReports + 1);
+  XCTAssertEqual([self.managerData.onDemandModel.storedActiveReportPaths count],
+                 FIRCLSMaxUnsentReports);
+
+  // Once we call sendUnsentReports, stored reports should be sent immediately.
+  [self.existingReportManager sendUnsentReportsWithToken:[FIRCLSDataCollectionToken validToken]
+                                                asUrgent:YES];
+  XCTAssertEqual([self.managerData.onDemandModel recordedOnDemandExceptionCount],
+                 FIRCLSMaxUnsentReports);
+  XCTAssertEqual([self contentsOfActivePath].count, 1);
+  XCTAssertEqual([self.managerData.onDemandModel.storedActiveReportPaths count], 0);
+}
+
+- (void)testDropsEventIfNoQuota {
+  [self.onDemandModel setQueueToFull];
+  FIRExceptionModel *exceptionModel = [self getTestExceptionModel];
+  BOOL success = [self.onDemandModel recordOnDemandExceptionIfQuota:exceptionModel
+                                          withDataCollectionEnabled:NO
+                                         usingExistingReportManager:self.existingReportManager];
+
+  // Should return false when attempting to record an event and increment the count of dropped
+  // events.
+  XCTAssertFalse(success);
+  XCTAssertEqual(self.onDemandModel.getQueuedOperationsCount, [self.onDemandModel getQueueMax]);
+  XCTAssertEqual([self.onDemandModel droppedOnDemandExceptionCount], 1);
+}
+
+- (void)testDroppedEventCountResets {
+  [self.onDemandModel setQueueToFull];
+
+  FIRExceptionModel *exceptionModel = [self getTestExceptionModel];
+  BOOL success = [self.onDemandModel recordOnDemandExceptionIfQuota:exceptionModel
+                                          withDataCollectionEnabled:NO
+                                         usingExistingReportManager:self.existingReportManager];
+
+  // Should return false when attempting to record an event and increment the count of dropped
+  // events.
+  XCTAssertFalse(success);
+  XCTAssertEqual(self.onDemandModel.getQueuedOperationsCount, [self.onDemandModel getQueueMax]);
+  XCTAssertEqual([self.onDemandModel droppedOnDemandExceptionCount], 1);
+
+  // Reset the queue to empty
+  [self.onDemandModel setQueuedOperationsCount:0];
+  success = [self.onDemandModel recordOnDemandExceptionIfQuota:exceptionModel
+                                     withDataCollectionEnabled:NO
+                                    usingExistingReportManager:self.existingReportManager];
+
+  // Now have room in the queue to record the event
+  XCTAssertTrue(success);
+  // droppedOnDemandExceptionCount should be reset once we record the event
+  XCTAssertEqual([self.onDemandModel droppedOnDemandExceptionCount], 0);
+}
+
+#pragma mark - Helpers
+- (NSArray *)contentsOfActivePath {
+  return [[NSFileManager defaultManager] contentsOfDirectoryAtPath:self.fileManager.activePath
+                                                             error:nil];
+}
+
+- (FIRExceptionModel *)getTestExceptionModel {
+  NSArray *stackTrace = @[
+    [FIRStackFrame stackFrameWithSymbol:@"CrashyFunc" file:@"AppLib.m" line:504],
+    [FIRStackFrame stackFrameWithSymbol:@"ApplicationMain" file:@"AppleLib" line:1],
+    [FIRStackFrame stackFrameWithSymbol:@"main()" file:@"main.m" line:201],
+  ];
+  NSString *name = @"FIRCLSOnDemandModelTestCrash";
+  NSString *reason = @"Programmer made an error";
+
+  FIRExceptionModel *exceptionModel = [FIRExceptionModel exceptionModelWithName:name reason:reason];
+  exceptionModel.stackTrace = stackTrace;
+  exceptionModel.isFatal = YES;
+  exceptionModel.onDemand = YES;
+  return exceptionModel;
+}
+
+@end

+ 6 - 1
Crashlytics/UnitTests/FIRCLSReportManagerTests.m

@@ -89,6 +89,9 @@
   self.mockSettings = [[FIRCLSMockSettings alloc] initWithFileManager:self.fileManager
                                                            appIDModel:self.appIDModel];
 
+  // Allow nil values only in tests
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wnonnull"
   FIRCLSManagerData *managerData =
       [[FIRCLSManagerData alloc] initWithGoogleAppID:TEST_GOOGLE_APP_ID
                                      googleTransport:mockGoogleTransport
@@ -96,7 +99,9 @@
                                            analytics:nil
                                          fileManager:self.fileManager
                                          dataArbiter:self.dataArbiter
-                                            settings:self.mockSettings];
+                                            settings:self.mockSettings
+                                       onDemandModel:nil];
+#pragma clang diagnostic pop
 
   self.mockReportUploader = [[FIRCLSMockReportUploader alloc] initWithManagerData:managerData];
 

+ 6 - 1
Crashlytics/UnitTests/FIRCLSReportUploaderTests.m

@@ -67,13 +67,18 @@ NSString *const TestEndpoint = @"https://reports.crashlytics.com";
   FIRMockInstallations *mockInstallations =
       [[FIRMockInstallations alloc] initWithFID:@"test_token"];
 
+  // Allow nil values only in tests
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wnonnull"
   self.managerData = [[FIRCLSManagerData alloc] initWithGoogleAppID:@"someGoogleAppId"
                                                     googleTransport:self.mockDataTransport
                                                       installations:mockInstallations
                                                           analytics:nil
                                                         fileManager:self.fileManager
                                                         dataArbiter:dataArbiter
-                                                           settings:self.mockSettings];
+                                                           settings:self.mockSettings
+                                                      onDemandModel:nil];
+#pragma clang diagnostic pop
 
   self.uploader = [[FIRCLSReportUploader alloc] initWithManagerData:_managerData];
 }

+ 26 - 3
Crashlytics/UnitTests/FIRCLSSettingsTests.m

@@ -29,17 +29,18 @@
 
 const NSString *FIRCLSTestSettingsActivated =
     @"{\"settings_version\":3,\"cache_duration\":60,\"features\":{\"collect_logged_exceptions\":"
-    @"true,\"collect_reports\":true},"
+    @"true,\"collect_reports\":true, \"collect_metric_kit\":true},"
     @"\"fabric\":{\"org_id\":\"010101000000111111111111\",\"bundle_id\":\"com.lets.test."
     @"crashlytics\"}}";
 
 const NSString *FIRCLSTestSettingsInverse =
     @"{\"settings_version\":3,\"cache_duration\":12345,\"features\":{\"collect_logged_exceptions\":"
-    @"false,\"collect_reports\":false},"
+    @"false,\"collect_reports\":false, \"collect_metric_kit\":false},"
     @"\"fabric\":{\"org_id\":\"01e101a0000011b113115111\",\"bundle_id\":\"im.from.the.server\"},"
     @"\"session\":{\"log_buffer_size\":128000,\"max_chained_exception_depth\":32,\"max_complete_"
     @"sessions_count\":4,\"max_custom_exception_events\":1000,\"max_custom_key_value_pairs\":2000,"
-    @"\"identifier_mask\":255}}";
+    @"\"identifier_mask\":255}, \"on_demand_upload_rate_per_minute\":15.0, "
+    @"\"on_demand_backoff_base\":3.0, \"on_demand_backoff_step_duration_seconds\":9}";
 
 const NSString *FIRCLSTestSettingsCorrupted = @"{{{{ non_key: non\"value {}";
 
@@ -94,11 +95,15 @@ NSString *const TestChangedGoogleAppID = @"2:changed:google:app:id";
   XCTAssertTrue(self.settings.collectReportsEnabled);
   XCTAssertTrue(self.settings.errorReportingEnabled);
   XCTAssertTrue(self.settings.customExceptionsEnabled);
+  XCTAssertFalse(self.settings.metricKitCollectionEnabled);
 
   XCTAssertEqual(self.settings.errorLogBufferSize, 64 * 1000);
   XCTAssertEqual(self.settings.logBufferSize, 64 * 1000);
   XCTAssertEqual(self.settings.maxCustomExceptions, 8);
   XCTAssertEqual(self.settings.maxCustomKeys, 64);
+  XCTAssertEqual(self.settings.onDemandUploadRate, 10);
+  XCTAssertEqual(self.settings.onDemandBackoffBase, 1.5);
+  XCTAssertEqual(self.settings.onDemandBackoffStepDuration, 6);
 }
 
 - (BOOL)writeSettings:(const NSString *)settings error:(NSError **)error {
@@ -159,11 +164,15 @@ NSString *const TestChangedGoogleAppID = @"2:changed:google:app:id";
   XCTAssertTrue(self.settings.collectReportsEnabled);
   XCTAssertTrue(self.settings.errorReportingEnabled);
   XCTAssertTrue(self.settings.customExceptionsEnabled);
+  XCTAssertTrue(self.settings.metricKitCollectionEnabled);
 
   XCTAssertEqual(self.settings.errorLogBufferSize, 64 * 1000);
   XCTAssertEqual(self.settings.logBufferSize, 64 * 1000);
   XCTAssertEqual(self.settings.maxCustomExceptions, 8);
   XCTAssertEqual(self.settings.maxCustomKeys, 64);
+  XCTAssertEqual(self.settings.onDemandUploadRate, 10);
+  XCTAssertEqual(self.settings.onDemandBackoffBase, 1.5);
+  XCTAssertEqual(self.settings.onDemandBackoffStepDuration, 6);
 }
 
 - (void)testInverseDefaultSettingsCached {
@@ -180,11 +189,15 @@ NSString *const TestChangedGoogleAppID = @"2:changed:google:app:id";
   XCTAssertFalse(self.settings.collectReportsEnabled);
   XCTAssertFalse(self.settings.errorReportingEnabled);
   XCTAssertFalse(self.settings.customExceptionsEnabled);
+  XCTAssertFalse(self.settings.metricKitCollectionEnabled);
 
   XCTAssertEqual(self.settings.errorLogBufferSize, 128000);
   XCTAssertEqual(self.settings.logBufferSize, 128000);
   XCTAssertEqual(self.settings.maxCustomExceptions, 1000);
   XCTAssertEqual(self.settings.maxCustomKeys, 2000);
+  XCTAssertEqual(self.settings.onDemandUploadRate, 15);
+  XCTAssertEqual(self.settings.onDemandBackoffBase, 3);
+  XCTAssertEqual(self.settings.onDemandBackoffStepDuration, 9);
 }
 
 - (void)testCacheExpiredFromTTL {
@@ -340,11 +353,15 @@ NSString *const TestChangedGoogleAppID = @"2:changed:google:app:id";
   XCTAssertTrue(self.settings.collectReportsEnabled);
   XCTAssertTrue(self.settings.errorReportingEnabled);
   XCTAssertTrue(self.settings.customExceptionsEnabled);
+  XCTAssertFalse(self.settings.metricKitCollectionEnabled);
 
   XCTAssertEqual(self.settings.errorLogBufferSize, 64 * 1000);
   XCTAssertEqual(self.settings.logBufferSize, 64 * 1000);
   XCTAssertEqual(self.settings.maxCustomExceptions, 8);
   XCTAssertEqual(self.settings.maxCustomKeys, 64);
+  XCTAssertEqual(self.settings.onDemandUploadRate, 10);
+  XCTAssertEqual(self.settings.onDemandBackoffBase, 1.5);
+  XCTAssertEqual(self.settings.onDemandBackoffStepDuration, 6);
 }
 
 // These tests are partially to make sure the SDK doesn't crash when it
@@ -391,6 +408,9 @@ NSString *const TestChangedGoogleAppID = @"2:changed:google:app:id";
   XCTAssertEqual(self.settings.isCacheExpired, NO);
   XCTAssertEqual(self.settings.cacheDurationSeconds, 12345);
   XCTAssertEqual(self.settings.errorLogBufferSize, 128000);
+  XCTAssertEqual(self.settings.onDemandUploadRate, 15);
+  XCTAssertEqual(self.settings.onDemandBackoffBase, 3);
+  XCTAssertEqual(self.settings.onDemandBackoffStepDuration, 9);
 
   // Then pretend we wrote a corrupted cache key and just reload it
   [self writeSettings:FIRCLSTestSettingsCorrupted error:&error isCacheKey:YES];
@@ -405,6 +425,9 @@ NSString *const TestChangedGoogleAppID = @"2:changed:google:app:id";
   XCTAssertEqual(self.settings.isCacheExpired, YES);
   XCTAssertEqual(self.settings.cacheDurationSeconds, 3600);
   XCTAssertEqual(self.settings.errorLogBufferSize, 64 * 1000);
+  XCTAssertEqual(self.settings.onDemandUploadRate, 10);
+  XCTAssertEqual(self.settings.onDemandBackoffBase, 1.5);
+  XCTAssertEqual(self.settings.onDemandBackoffStepDuration, 6);
 }
 
 - (void)testNewReportEndpointSettings {

+ 30 - 0
Crashlytics/UnitTests/Mocks/FIRCLSMockOnDemandModel.h

@@ -0,0 +1,30 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "Crashlytics/Crashlytics/Models/FIRCLSOnDemandModel.h"
+#import "Crashlytics/Crashlytics/Private/FIRCLSOnDemandModel_Private.h"
+
+@interface FIRCLSMockOnDemandModel : FIRCLSOnDemandModel
+
+- (instancetype)initWithFIRCLSSettings:(FIRCLSSettings *)settings
+                            sleepBlock:(void (^)(int))sleepBlock;
+
+// Public for testing purposes
+- (void)setQueueToFull;
+- (void)setQueueToEmpty;
+- (int)getQueueMax;
+
+@property(nonatomic, copy) void (^sleepBlock)(int);
+
+@end

+ 51 - 0
Crashlytics/UnitTests/Mocks/FIRCLSMockOnDemandModel.m

@@ -0,0 +1,51 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "Crashlytics/UnitTests/Mocks/FIRCLSMockOnDemandModel.h"
+
+@interface FIRCLSMockOnDemandModel ()
+
+@property(nonatomic, readonly) FIRCLSSettings *settings;
+
+@end
+
+@implementation FIRCLSMockOnDemandModel
+
+- (instancetype)initWithFIRCLSSettings:(FIRCLSSettings *)settings
+                            sleepBlock:(void (^)(int))sleepBlock {
+  self = [super initWithFIRCLSSettings:settings];
+  if (!self) {
+    return nil;
+  }
+  _settings = settings;
+  _sleepBlock = sleepBlock;
+  return self;
+}
+- (void)setQueueToFull {
+  [self setQueuedOperationsCount:self.settings.onDemandUploadRate];
+}
+
+- (void)setQueueToEmpty {
+  [self setQueuedOperationsCount:0];
+}
+
+- (int)getQueueMax {
+  return self.settings.onDemandUploadRate;
+}
+
+- (void)implementOnDemandUploadDelay:(int)delay {
+  _sleepBlock(delay);
+}
+
+@end

+ 1 - 1
FirebaseAnalytics.podspec.json

@@ -32,7 +32,7 @@
         "tvos": "12.0"
     },
     "source": {
-        "http": "https://dl.google.com/firebase/ios/analytics/52eebd4fc62b3d8c/FirebaseAnalytics-8.14.0.tar.gz"
+        "http": "https://dl.google.com/firebase/ios/analytics/dcdcf15b4c4831d9/FirebaseAnalytics-8.15.0.tar.gz"
     },
     "subspecs": [
         {

+ 2 - 2
FirebaseAppCheck/Sources/AppAttestProvider/Storage/FIRAppAttestKeyIDStorage.h

@@ -25,13 +25,13 @@ NS_ASSUME_NONNULL_BEGIN
 
 /** Manages storage of an app attest key ID.
  *  @param keyID The app attest key ID to store or `nil` to remove the existing app attest key ID.
- *  @returns A promise that is resolved with a stored app attest key ID or `nil` if the existing app
+ *  @return A promise that is resolved with a stored app attest key ID or `nil` if the existing app
  * attest key ID has been removed.
  */
 - (FBLPromise<NSString *> *)setAppAttestKeyID:(nullable NSString *)keyID;
 
 /** Reads a stored app attest key ID.
- *  @returns A promise that is resolved with a stored app attest key ID or `nil` if there is not a
+ *  @return A promise that is resolved with a stored app attest key ID or `nil` if there is not a
  * stored app attest key ID. The promise is rejected with an error in the case of a missing app
  * attest key ID .
  */

+ 2 - 2
FirebaseAppCheck/Sources/Core/Storage/FIRAppCheckStorage.h

@@ -26,13 +26,13 @@ NS_ASSUME_NONNULL_BEGIN
 
 /** Manages storage of the FAA token.
  *  @param token A token object to store or `nil` to remove existing token.
- *  @returns A promise that is resolved with the stored object in the case of success or is rejected
+ *  @return A promise that is resolved with the stored object in the case of success or is rejected
  * with a specific error otherwise.
  */
 - (FBLPromise<FIRAppCheckToken *> *)setToken:(nullable FIRAppCheckToken *)token;
 
 /** Reads a stored FAA token.
- *  @returns A promise that is resolved with a stored token or `nil` if there is not a stored token.
+ *  @return A promise that is resolved with a stored token or `nil` if there is not a stored token.
  * The promise is rejected with an error in the case of a failure.
  */
 - (FBLPromise<FIRAppCheckToken *> *)getToken;

+ 1 - 1
FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheck.h

@@ -47,7 +47,7 @@ NS_SWIFT_NAME(AppCheck)
 
 /// Returns an instance of `AppCheck` for an application.
 /// @param firebaseApp A configured `FirebaseApp` instance if exists.
-/// @returns An instance of `AppCheck` corresponding to the passed application.
+/// @return An instance of `AppCheck` corresponding to the passed application.
 /// @throw Throws an exception if required `FirebaseApp` options are missing.
 + (nullable instancetype)appCheckWithApp:(FIRApp *)firebaseApp NS_SWIFT_NAME(appCheck(app:));
 

+ 1 - 1
FirebaseAuth/Sources/Auth/FIRAuthDispatcher.h

@@ -54,7 +54,7 @@ typedef void (^FIRAuthDispatcherImplBlock)(NSTimeInterval delay,
 
 /** @fn sharedInstance
     @brief Gets the shared instance of this class.
-    @returns The shared instance of this clss
+    @return The shared instance of this clss
  */
 + (instancetype)sharedInstance;
 

+ 1 - 1
FirebaseAuth/Sources/Utilities/FIRAuthErrorUtils.h

@@ -110,7 +110,7 @@ NS_ASSUME_NONNULL_BEGIN
     @param underlyingError The error that caused this error. If this parameter is nil, the
         NSUnderlyingErrorKey value will not be set.
     @remarks This error is returned when JWT parsing fails.
-    @returns An @c FIRAuthErrorCodeMalformedJWT error wrapping an underlying error, if available.
+    @return An @c FIRAuthErrorCodeMalformedJWT error wrapping an underlying error, if available.
  */
 + (NSError *)malformedJWTErrorWithToken:(NSString *)token
                         underlyingError:(NSError *_Nullable)underlyingError;

+ 3 - 31
FirebaseAuth/Tests/Sample/README.md

@@ -10,8 +10,8 @@ You'll need valid `GoogleService-Info.plist` files for those samples. To get you
 2. Create a new Firebase project, if you don't already have one
 3. For each sample app you want to test, create a new Firebase app with the sample app's bundle
 identifier (e.g. `com.google.FirebaseExperimental1.dev`)
-4. Download the resulting `GoogleService-Info.plist` and place it in
-[Sample/GoogleService-Info.plist](Sample/GoogleService-Info.plist)
+4. Download the resulting `GoogleService-Info.plist` and place it in the
+[`Sample/` directory](Sample/)
 
 #### GoogleService-Info\_multi.plist files
 
@@ -19,7 +19,7 @@ identifier (e.g. `com.google.FirebaseExperimental1.dev`)
 same Firebase project as the one above, or a different one.  Use a different app bundle identifier
 (e.g.  `com.google.FirebaseExperimental2.dev`).
 2. Save this plist file as `GoogleService-Info_multi.plist` in
-[Sample/GoogleService-Info\_multi.plist](Sample/GoogleService-Info_multi.plist).
+the [`Sample/` directory](Sample/).
 This enables testing that FirebaseAuth continues to work after switching the Firebase App in the
 runtime.
 
@@ -59,34 +59,6 @@ relevant appLinks domain. Your appLinks domains are domains that your app will h
 links, in this particular case you can obtain this domain from the aforementioned Dynamic Links
 section of the Firebase Console.
 
-
-### Running SwiftSample Application
-
-In order to run this application, you'll need to follow the following steps!
-
-#### GoogleService-Info.plist files
-
-You'll need valid `GoogleService-Info.plist` files for those samples. To get your own
-`GoogleService-Info.plist` files:
-1. Go to the [Firebase Console](https://console.firebase.google.com/)
-2. Create a new Firebase project, if you don't already have one
-3. For each sample app you want to test, create a new Firebase app with the sample app's bundle
-identifier (e.g. `com.google.FirebaseExperimental2.dev`)
-4. Download the resulting `GoogleService-Info.plist` and place it in
-[SwiftSample/GoogleService-Info.plist](SwiftSample/GoogleService-Info.plist)
-
-#### Info.plist file
-
-Please follow the instructions in
-[SwiftSample/InfoTemplate.plist](SwiftSample/InfoTemplate.plist)
-to generate the right Info.plist file
-
-#### Getting your own Credential files
-
-Please follow the instructions in
-[SwiftSample/AuthCredentialsTemplate.swift](SwiftSample/AuthCredentialsTemplate.swift)
-to generate the AuthCredentials.swift file.
-
 ### Running API tests
 
 In order to run the API tests, you'll need to follow the following steps!

+ 1 - 1
FirebaseAuth/Tests/Sample/Sample/AppManager.m

@@ -97,7 +97,7 @@ NS_ASSUME_NONNULL_BEGIN
 /** @fn appNameWithIndex:
     @brief Gets the app name for the given index.
     @param index The index of the app managed by this instance.
-    @returns The app name for the FIRApp instance.
+    @return The app name for the FIRApp instance.
  */
 - (NSString *)appNameWithIndex:(int)index {
   return [NSString stringWithFormat:@"APP_%02d", index];

+ 3 - 0
FirebaseDynamicLinks/CHANGELOG.md

@@ -1,3 +1,6 @@
+# v8.15.0
+- [fixed] Fixed Custom domain long url validation logic. (#6978)
+
 # v8.9.0
 - [fixed] Fixed Shortlink regression involving underscores and dashes introduced in 8.8.0. (#8786)
 - [fixed] Reduce memory stress on `WebKit` API. (#8847)

+ 36 - 27
FirebaseDynamicLinks/Sources/Utilities/FDLUtilities.m

@@ -201,37 +201,46 @@ NSString *FIRDLDeviceTimezone() {
   return timeZoneName;
 }
 
-BOOL FIRDLIsURLForAllowedCustomDomain(NSURL *_Nullable URL) {
-  BOOL customDomainMatchFound = false;
-  for (NSURL *allowedCustomDomain in FIRDLCustomDomains) {
-    // At least one custom domain host name should match at a minimum.
-    if ([allowedCustomDomain.host isEqualToString:URL.host]) {
-      NSString *urlStr = URL.absoluteString;
-      NSString *domainURIPrefixStr = allowedCustomDomain.absoluteString;
-
-      // Next, do a string compare to check if entire domainURIPrefix matches as well.
-      if (([urlStr rangeOfString:domainURIPrefixStr
-                         options:NSCaseInsensitiveSearch | NSAnchoredSearch]
-               .location) == 0) {
-        NSString *urlWithoutDomainURIPrefix = [urlStr substringFromIndex:domainURIPrefixStr.length];
-
-        // For a valid custom domain DL Suffix:
-        // 1. At least one path exists OR
-        // 2. Should have a link query param with an http/https link
-        BOOL matchesRegularExpression =
-            ([urlWithoutDomainURIPrefix
-                 rangeOfString:@"((\\/[A-Za-z0-9]+)|((\\?|\\/\\?)link=https?.*))"
-                       options:NSRegularExpressionSearch]
-                 .location != NSNotFound);
-
-        if (matchesRegularExpression) {
-          customDomainMatchFound = true;
-          break;
+BOOL FIRDLIsURLForAllowedCustomDomain(NSURL *URL) {
+  if (URL) {
+    for (NSURL *allowedCustomDomain in FIRDLCustomDomains) {
+      // At least one custom domain host name should match at a minimum.
+      if ([URL.absoluteString hasPrefix:allowedCustomDomain.absoluteString]) {
+        NSString *urlWithoutDomainURIPrefix =
+            [URL.absoluteString substringFromIndex:allowedCustomDomain.absoluteString.length];
+
+        // The urlWithoutDomainURIPrefix should be starting with '/' or '?' otherwise it means the
+        // allowed domain is not exactly matching the incoming URL domain prefix.
+        if ([urlWithoutDomainURIPrefix hasPrefix:@"/"] ||
+            [urlWithoutDomainURIPrefix hasPrefix:@"?"]) {
+          //  For a valid custom domain DL Suffix the urlWithoutDomainURIPrefix should have:
+          //  1. At least one path exists OR
+          //  2. Should have a link query param with an http/https link
+
+          NSURLComponents *components =
+              [[NSURLComponents alloc] initWithString:urlWithoutDomainURIPrefix];
+          if (components.path && components.path.length > 1) {
+            // Have a path exists. So valid custom domain.
+            return true;
+          }
+
+          if (components.queryItems && components.queryItems.count > 0) {
+            for (NSURLQueryItem *queryItem in components.queryItems) {
+              // Checks whether we have a link query param
+              if ([queryItem.name caseInsensitiveCompare:@"link"] == NSOrderedSame) {
+                // Checks whether link query param value starts with http/https
+                if (queryItem.value && ([queryItem.value hasPrefix:@"http://"] ||
+                                        [queryItem.value hasPrefix:@"https://"])) {
+                  return true;
+                }
+              }
+            }
+          }
         }
       }
     }
   }
-  return customDomainMatchFound;
+  return false;
 }
 
 /* We are validating following domains in proper format.

+ 41 - 11
FirebaseDynamicLinks/Tests/Unit/FIRDynamicLinksTest.m

@@ -1614,7 +1614,20 @@ static NSString *const kInfoPlistCustomDomainsKey = @"FirebaseDynamicLinksCustom
   NSArray<NSString *> *longFDLURLStrings = @[
     @"https://a.firebase.com/mypath/?link=https://abcd&test=1",  // Long FDL starting with
                                                                  // https://a.firebase.com/mypath
+    @"https://google.com?link=http://abcd",   // Long FDL starting with  'https://google.com'
     @"https://google.com/?link=http://abcd",  // Long FDL starting with  'https://google.com'
+    @"https://google.com?link=https://somedomain&some=qry",   // Long FDL with link param as another
+                                                              // argument.
+    @"https://google.com/?link=https://somedomain&some=qry",  // Long FDL with link param as another
+                                                              // argument.
+    @"https://google.com?some=qry&link=https://somedomain",   // Long FDL with link param as second
+                                                              // argument.
+    @"https://google.com/?some=qry&link=https://somedomain",  // Long FDL with link param as second
+                                                              // argument
+    @"https://google.com/?a=b&c=d&link=https://somedomain&y=z",    // Long FDL with link param as
+                                                                   // middle argument argument
+    @"https://google.com?some=qry&link=https%3A%2F%2Fsomedomain",  // Long FDL with Url encoded link
+                                                                   // param
   ];
   for (NSString *urlString in urlStrings) {
     NSURL *url = [NSURL URLWithString:urlString];
@@ -1637,22 +1650,39 @@ static NSString *const kInfoPlistCustomDomainsKey = @"FirebaseDynamicLinksCustom
   //  https://a.firebase.com/mypath
 
   NSArray<NSString *> *urlStrings = @[
-    @"google.com",                        // Valid domain. No scheme.
-    @"https://google.com",                // Valid domain. No path after domainURIPrefix.
-    @"https://google.com/",               // Valid domain. No path after domainURIPrefix.
-    @"https://google.co.in/mylink",       // No matching domainURIPrefix.
-    @"https://firebase.com/mypath",       // No matching domainURIPrefix: Invalid (sub)domain.
-    @"https://b.firebase.com/mypath",     // No matching domainURIPrefix: Invalid subdomain.
-    @"https://a.firebase.com/mypathabc",  // No matching domainURIPrefix: Invalid subdomain.
-    @"mydomain.com",                      // https scheme not specified for domainURIPrefix.
-    @"http://mydomain",                   // Domain not in plist. No path after domainURIPrefix.
+    @"google.com",                             // Valid domain. No scheme.
+    @"https://google.com",                     // Valid domain. No path after domainURIPrefix.
+    @"https://google.com/",                    // Valid domain. No path after domainURIPrefix.
+    @"https://google.co.in/mylink",            // No matching domainURIPrefix.
+    @"https://google.com/?some=qry",           // Valid domain with no path and link param
+    @"https://google.com/?some=qry&link=bla",  // Valid domain with no path and no valid link param
+    @"https://firebase.com/mypath",            // No matching domainURIPrefix: Invalid (sub)domain.
+    @"https://b.firebase.com/mypath",          // No matching domainURIPrefix: Invalid subdomain.
+    @"https://a.firebase.com/mypathabc",       // No matching domainURIPrefix: Invalid subdomain.
+    @"mydomain.com",                           // https scheme not specified for domainURIPrefix.
+    @"http://mydomain",  // Domain not in plist. No path after domainURIPrefix.
     @"https://somecustom.com?", @"https://somecustom.com/?",
-    @"https://somecustom.com?somekey=someval"
+    @"https://somecustom.com?somekey=someval",
+    @"https://google.com?some=qry&somelink=https%3A%2F%2Fsomedomain",  // Having somelink param
+                                                                       // instead of link param to
+                                                                       // confuse validation.
+    @"https://a.firebase.com/mypaths?some=qry&link=https%3A%2F%2Fsomedomain",  // Additional 's' in
+                                                                               // path param
+    @"https://a.firebase.com/mypath/?some=qry#other=b&link=https://somedomain",  // link param comes
+                                                                                 // in fragmentation
+    @"https://a.firebase.com/mypath/?some=qry#other=b&link=https%3A%2F%2Fsomedomain",  // link param
+                                                                                       // which is
+                                                                                       // url
+                                                                                       // encoded
+                                                                                       // and comes
+                                                                                       // in
+                                                                                       // fragmentation.
+    @"https://google.com?link=https1://abcd",  // link query param is not a valid http link
   ];
 
   for (NSString *urlString in urlStrings) {
     NSURL *url = [NSURL URLWithString:urlString];
-    BOOL matchesShortLinkFormat = [self.service matchesShortLinkFormat:url];
+    BOOL matchesShortLinkFormat = [self.service canParseUniversalLinkURL:url];
 
     XCTAssertFalse(matchesShortLinkFormat,
                    @"Non-DDL domain URL matched short link format with URL: %@", url);

+ 2 - 2
FirebaseFirestore.podspec

@@ -90,7 +90,7 @@ Google Cloud Firestore is a NoSQL document database built for automatic scaling,
 
   s.dependency 'FirebaseCore', '~> 8.0'
 
-  abseil_version = '0.20200225.0'
+  abseil_version = '~> 1.20211102.0'
   s.dependency 'abseil/algorithm', abseil_version
   s.dependency 'abseil/base', abseil_version
   s.dependency 'abseil/container/flat_hash_map', abseil_version
@@ -100,7 +100,7 @@ Google Cloud Firestore is a NoSQL document database built for automatic scaling,
   s.dependency 'abseil/time', abseil_version
   s.dependency 'abseil/types', abseil_version
 
-  s.dependency 'gRPC-C++', '~> 1.28.0'
+  s.dependency 'gRPC-C++', '~> 1.44.0'
   s.dependency 'leveldb-library', '~> 1.22'
   s.dependency 'nanopb', '~> 2.30908.0'
 

+ 4 - 0
FirebaseFunctions/CHANGELOG.md

@@ -1,6 +1,10 @@
 # v9.0.0
 - [changed] Backported Callable async/await APIs to iOS 13, etc. (#9483).
 
+# v8.15.0
+- [deprecated] The global variables `FIRFunctionsErrorDomain` and `FIRFunctionsErrorDetailsKey` are
+  deprecated and will be removed in v9.0.0. (#9569)
+
 # v8.9.0
 - [fixed] Add watchOS support for Swift Package Manager (#8864).
 

+ 4 - 4
FirebaseFunctions/Tests/Integration/IntegrationTests.swift

@@ -433,7 +433,7 @@ class IntegrationTests: XCTestCase {
         XCTAssertEqual(FunctionsErrorCode.outOfRange.rawValue, error.code)
         XCTAssertEqual("explicit nope", error.localizedDescription)
         XCTAssertEqual(["start": 10 as Int32, "end": 20 as Int32, "long": 30],
-                       error.userInfo[FunctionsErrorDetailsKey] as! [String: Int32])
+                       error.userInfo["details"] as! [String: Int32])
         expectation.fulfill()
         return
       }
@@ -458,7 +458,7 @@ class IntegrationTests: XCTestCase {
         XCTAssertEqual(FunctionsErrorCode.outOfRange.rawValue, error.code)
         XCTAssertEqual("explicit nope", error.localizedDescription)
         XCTAssertEqual(["start": 10 as Int32, "end": 20 as Int32, "long": 30],
-                       error.userInfo[FunctionsErrorDetailsKey] as! [String: Int32])
+                       error.userInfo["details"] as! [String: Int32])
       }
     }
   #endif
@@ -518,7 +518,7 @@ class IntegrationTests: XCTestCase {
         let error = error as NSError
         XCTAssertEqual(FunctionsErrorCode.deadlineExceeded.rawValue, error.code)
         XCTAssertEqual("DEADLINE EXCEEDED", error.localizedDescription)
-        XCTAssertNil(error.userInfo[FunctionsErrorDetailsKey])
+        XCTAssertNil(error.userInfo["details"])
         expectation.fulfill()
         return
       }
@@ -543,7 +543,7 @@ class IntegrationTests: XCTestCase {
         let error = error as NSError
         XCTAssertEqual(FunctionsErrorCode.deadlineExceeded.rawValue, error.code)
         XCTAssertEqual("DEADLINE EXCEEDED", error.localizedDescription)
-        XCTAssertNil(error.userInfo[FunctionsErrorDetailsKey])
+        XCTAssertNil(error.userInfo["details"])
       }
     }
   #endif

+ 6 - 0
FirebaseFunctions/Tests/ObjCIntegration/FIRFunctions+Internal.h

@@ -70,4 +70,10 @@ NS_ASSUME_NONNULL_BEGIN
 
 @end
 
+// The error domain for codes in the FIRFunctionsErrorCode enum.
+FOUNDATION_EXPORT NSString *const FIRFunctionsErrorDomainInternal;
+
+// The key for finding error details in the NSError userInfo.
+FOUNDATION_EXPORT NSString *const FIRFunctionsErrorDetailsKeyInternal;
+
 NS_ASSUME_NONNULL_END

+ 2 - 2
FirebaseInAppMessaging/Sources/Runtime/FIRIAMActionURLFollower.m

@@ -153,7 +153,7 @@ NS_EXTENSION_UNAVAILABLE("Firebase In App Messaging is not supported for iOS ext
 
 // Try to handle the url as a custom scheme url link by triggering
 // application:openURL:options: on App's delegate object directly.
-// @returns YES if that delegate method is defined and returns YES.
+// @return YES if that delegate method is defined and returns YES.
 - (BOOL)followURLWithAppDelegateOpenURLActivity:(NSURL *)url {
   if (self.isNewAppDelegateOpenURLDefined) {
     FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM210008",
@@ -168,7 +168,7 @@ NS_EXTENSION_UNAVAILABLE("Firebase In App Messaging is not supported for iOS ext
 
 // Try to handle the url as a universal link by triggering
 // application:continueUserActivity:restorationHandler: on App's delegate object directly.
-// @returns YES if that delegate method is defined and seeing a YES being returned from
+// @return YES if that delegate method is defined and seeing a YES being returned from
 // trigging it
 - (BOOL)followURLWithContinueUserActivity:(NSURL *)url {
   if (self.isContinueUserActivityMethodDefined) {

+ 2 - 2
FirebaseInstallations/Source/Library/Public/FirebaseInstallations/FIRInstallations.h

@@ -64,7 +64,7 @@ NS_SWIFT_NAME(Installations)
 
 /**
  * Returns a default instance of `Installations`.
- * @returns An instance of `Installations` for `FirebaseApp.defaultApp().
+ * @return An instance of `Installations` for `FirebaseApp.defaultApp().
  * @throw Throws an exception if the default app is not configured yet or required  `FirebaseApp`
  * options are missing.
  */
@@ -73,7 +73,7 @@ NS_SWIFT_NAME(Installations)
 /**
  * Returns an instance of `Installations` for an application.
  * @param application A configured `FirebaseApp` instance.
- * @returns An instance of `Installations` corresponding to the passed application.
+ * @return An instance of `Installations` corresponding to the passed application.
  * @throw Throws an exception if required `FirebaseApp` options are missing.
  */
 + (FIRInstallations *)installationsWithApp:(FIRApp *)application NS_SWIFT_NAME(installations(app:));

+ 1 - 1
FirebaseMessaging/Sources/FIRMessagingRmqManager.h

@@ -81,7 +81,7 @@
 /**
  * Returns path for database with specified name.
  * @param databaseName The database name without extension: "<databaseName>.sqlite".
- * @returns Path to the database with the specified name.
+ * @return Path to the database with the specified name.
  */
 + (NSString *)pathForDatabaseWithName:(NSString *)databaseName;
 

+ 3 - 0
FirebasePerformance/CHANGELOG.md

@@ -1,3 +1,6 @@
+# Pending
+* Remove the unused code for pre-warm detection.
+
 # Version 8.14.0
 * [fixed] Record the request payload size for POST/PUT requests.
 

+ 1 - 29
FirebasePerformance/Sources/AppActivity/FPRAppActivityTracker.m

@@ -43,7 +43,6 @@ NSString *const kFPRAppCounterNameTraceEventsRateLimited = @"_fstec";
 NSString *const kFPRAppCounterNameNetworkTraceEventsRateLimited = @"_fsntc";
 NSString *const kFPRAppCounterNameTraceNotStopped = @"_tsns";
 NSString *const kFPRAppCounterNameActivePrewarm = @"_fsapc";
-NSString *const kFPRAppCounterNameDoubleDispatch = @"_fsddc";
 
 @interface FPRAppActivityTracker ()
 
@@ -76,16 +75,6 @@ NSString *const kFPRAppCounterNameDoubleDispatch = @"_fsddc";
   // This is an approximation of the app start time.
   appStartTime = [NSDate date];
 
-  // Double dispatch is used to detect prewarming, but if it causes hang or crash in the future
-  // developers can disable it by setting a plist flag "fireperf_disable_dd" to true
-  if ([[[NSBundle mainBundle] objectForInfoDictionaryKey:@"fireperf_disable_dd"] boolValue] == NO) {
-    dispatch_async(dispatch_get_main_queue(), ^{
-      dispatch_async(dispatch_get_main_queue(), ^{
-        doubleDispatchTime = [NSDate date];
-      });
-    });
-  }
-
   // When an app is prewarmed, Apple sets env variable ActivePrewarm to 1, then the env variable is
   // deleted after didFinishLaunching
   isActivePrewarm = [NSProcessInfo.processInfo.environment[@"ActivePrewarm"] isEqualToString:@"1"];
@@ -185,17 +174,7 @@ NSString *const kFPRAppCounterNameDoubleDispatch = @"_fsddc";
  */
 - (BOOL)isActivePrewarmEnabled {
   PrewarmDetectionMode mode = [self.configurations prewarmDetectionMode];
-  return (mode == PrewarmDetectionModeActivePrewarm ||
-          mode == PrewarmDetectionModeActivePrewarmOrDoubleDispatch);
-}
-
-/**
- RC flag for enabling prewarm-detection using double dispatch method
- */
-- (BOOL)isDoubleDispatchEnabled {
-  PrewarmDetectionMode mode = [self.configurations prewarmDetectionMode];
-  return (mode == PrewarmDetectionModeDoubleDispatch ||
-          mode == PrewarmDetectionModeActivePrewarmOrDoubleDispatch);
+  return (mode == PrewarmDetectionModeActivePrewarm);
 }
 
 /**
@@ -215,13 +194,6 @@ NSString *const kFPRAppCounterNameDoubleDispatch = @"_fsddc";
     [self.activeTrace incrementMetric:kFPRAppCounterNameActivePrewarm byInt:0];
   }
 
-  if ([doubleDispatchTime compare:applicationDidFinishLaunchTime] == NSOrderedAscending) {
-    isPrewarmed = isPrewarmed || [self isDoubleDispatchEnabled];
-    [self.activeTrace incrementMetric:kFPRAppCounterNameDoubleDispatch byInt:1];
-  } else if (doubleDispatchTime) {
-    [self.activeTrace incrementMetric:kFPRAppCounterNameDoubleDispatch byInt:0];
-  }
-
   return isPrewarmed;
 }
 

+ 1 - 5
FirebasePerformance/Sources/Configurations/FPRConfigurations.h

@@ -20,16 +20,12 @@ NS_ASSUME_NONNULL_BEGIN
  * Different modes of prewarm-detection
  * KeepNone = No app start events are allowed
  * ActivePrewarm = Only detect prewarming using ActivePrewarm environment
- * DoubleDispatch = Only detect prewarming using double dispatch method
- * ActivePrewarmOrDoubleDispatch = Detect prewarming using both ActivePrewarm and double dispatch
  * KeepAll = All app start events are allowed
  */
 typedef NS_ENUM(NSInteger, PrewarmDetectionMode) {
   PrewarmDetectionModeKeepNone = 0,
   PrewarmDetectionModeActivePrewarm = 1,
-  PrewarmDetectionModeDoubleDispatch = 2,
-  PrewarmDetectionModeActivePrewarmOrDoubleDispatch = 3,
-  PrewarmDetectionModeKeepAll = 4
+  PrewarmDetectionModeKeepAll = 2
 };
 
 /** A typedef for ensuring that config names are one of the below specified strings. */

+ 0 - 80
FirebasePerformance/Tests/Unit/FPRAppActivityTrackerTest.m

@@ -205,28 +205,11 @@
   XCTAssertEqual(appTracker.applicationState, FPRApplicationStateBackground);
 }
 
-/** Validates double dispatch returns true when +load occurs before didFinishLaunching
- */
-- (void)test_isApplicationPrewarmed_doubleDispatch_returnsYes {
-  id mockAppTracker = OCMPartialMock([FPRAppActivityTracker sharedInstance]);
-  OCMStub([mockAppTracker isPrewarmAvailable]).andReturn(YES);
-  OCMStub([mockAppTracker isDoubleDispatchEnabled]).andReturn(YES);
-  OCMStub([mockAppTracker isActivePrewarmEnabled]).andReturn(NO);
-
-  [FPRAppActivityTracker load];
-  [[NSNotificationCenter defaultCenter]
-      postNotificationName:UIApplicationDidFinishLaunchingNotification
-                    object:[UIApplication sharedApplication]];
-
-  XCTAssertTrue([mockAppTracker isApplicationPreWarmed]);
-}
-
 /** Validates ActivePrewarm environment variable set to true is detected by prewarm-detection
  */
 - (void)test_isApplicationPrewarmed_activePrewarm_returnsYes {
   id mockAppTracker = OCMPartialMock([FPRAppActivityTracker sharedInstance]);
   OCMStub([mockAppTracker isPrewarmAvailable]).andReturn(YES);
-  OCMStub([mockAppTracker isDoubleDispatchEnabled]).andReturn(NO);
   OCMStub([mockAppTracker isActivePrewarmEnabled]).andReturn(YES);
 
   setenv("ActivePrewarm", "1", 1);
@@ -239,7 +222,6 @@
 - (void)test_isApplicationPrewarmed_activePrewarm_returnsNo {
   id mockAppTracker = OCMPartialMock([FPRAppActivityTracker sharedInstance]);
   OCMStub([mockAppTracker isPrewarmAvailable]).andReturn(YES);
-  OCMStub([mockAppTracker isDoubleDispatchEnabled]).andReturn(NO);
   OCMStub([mockAppTracker isActivePrewarmEnabled]).andReturn(YES);
 
   XCTAssertFalse([mockAppTracker isApplicationPreWarmed]);
@@ -281,66 +263,4 @@
   XCTAssertTrue([appTracker isActivePrewarmEnabled]);
 }
 
-/** Validates ActivePrewarm filtering is disabled when RC flag fpr_prewarm_detection is
- * PrewarmDetectionModeDoubleDispatch
- */
-- (void)test_isActivePrewarmEnabled_PrewarmDetectionModeDoubleDispatch_returnsNo {
-  FPRAppActivityTracker *appTracker = [FPRAppActivityTracker sharedInstance];
-  id mockConfigurations = OCMClassMock([FPRConfigurations class]);
-  appTracker.configurations = mockConfigurations;
-
-  OCMStub([mockConfigurations prewarmDetectionMode]).andReturn(PrewarmDetectionModeDoubleDispatch);
-  XCTAssertFalse([appTracker isActivePrewarmEnabled]);
-}
-
-/** Validates double dispatch filtering is disabled when RC flag fpr_prewarm_detection is
- * PrewarmDetectionModeActivePrewarm
- */
-- (void)test_isDoubleDispatchEnabled_PrewarmDetectionModeActivePrewarm_returnsNo {
-  FPRAppActivityTracker *appTracker = [FPRAppActivityTracker sharedInstance];
-  id mockConfigurations = OCMClassMock([FPRConfigurations class]);
-  appTracker.configurations = mockConfigurations;
-
-  OCMStub([mockConfigurations prewarmDetectionMode]).andReturn(PrewarmDetectionModeActivePrewarm);
-  XCTAssertFalse([appTracker isDoubleDispatchEnabled]);
-}
-
-/** Validates double dispatch filtering is enabled when RC flag fpr_prewarm_detection is
- * PrewarmDetectionModeDoubleDispatch
- */
-- (void)test_isDoubleDispatchEnabled_PrewarmDetectionModeDoubleDispatch_returnsYes {
-  FPRAppActivityTracker *appTracker = [FPRAppActivityTracker sharedInstance];
-  id mockConfigurations = OCMClassMock([FPRConfigurations class]);
-  appTracker.configurations = mockConfigurations;
-
-  OCMStub([mockConfigurations prewarmDetectionMode]).andReturn(PrewarmDetectionModeDoubleDispatch);
-  XCTAssertTrue([appTracker isDoubleDispatchEnabled]);
-}
-
-/** Validates ActivePrewarm filtering is enabled when RC flag fpr_prewarm_detection is
- * PrewarmDetectionModeActivePrewarmOrDoubleDispatch
- */
-- (void)test_isActivePrewarmEnabled_PrewarmDetectionModeActivePrewarmOrDoubleDispatch_returnsYes {
-  FPRAppActivityTracker *appTracker = [FPRAppActivityTracker sharedInstance];
-  id mockConfigurations = OCMClassMock([FPRConfigurations class]);
-  appTracker.configurations = mockConfigurations;
-
-  OCMStub([mockConfigurations prewarmDetectionMode])
-      .andReturn(PrewarmDetectionModeActivePrewarmOrDoubleDispatch);
-  XCTAssertTrue([appTracker isActivePrewarmEnabled]);
-}
-
-/** Validates double dispatch filtering is enabled when RC flag fpr_prewarm_detection is
- * PrewarmDetectionModeActivePrewarmOrDoubleDispatch
- */
-- (void)test_isDoubleDispatchEnabled_PrewarmDetectionModeActivePrewarmOrDoubleDispatch_returnsYes {
-  FPRAppActivityTracker *appTracker = [FPRAppActivityTracker sharedInstance];
-  id mockConfigurations = OCMClassMock([FPRConfigurations class]);
-  appTracker.configurations = mockConfigurations;
-
-  OCMStub([mockConfigurations prewarmDetectionMode])
-      .andReturn(PrewarmDetectionModeActivePrewarmOrDoubleDispatch);
-  XCTAssertTrue([appTracker isDoubleDispatchEnabled]);
-}
-
 @end

+ 4 - 0
FirebaseStorage/CHANGELOG.md

@@ -1,6 +1,10 @@
 # 9.0.0
 - [changed] Backported `StorageReference` async/await APIs to iOS 13, etc. (#9483).
 
+# 8.15.0
+- [deprecated] The global variable `FIRStorageErrorDomain` is deprecated and will
+  be removed in a future release (#9569).
+
 # 8.5.0
 - [fixed] Fixed an issue where Storage could not connect to local emulators using
   http (#8389).

+ 1 - 0
FirebaseStorage/Sources/FIRStorageConstants.m

@@ -39,6 +39,7 @@ NSString *const kFIRStorageResponseErrorCode = @"ResponseErrorCode";
 NSString *const kFIRStorageResponseBody = @"ResponseBody";
 
 NSString *const FIRStorageErrorDomain = @"FIRStorageErrorDomain";
+NSString *const FIRStorageErrorDomainInternal = @"FIRStorageErrorDomain";
 
 NSString *const kFIRStorageInvalidDataFormat = @"Invalid data returned from the server: %@";
 NSString *const kFIRStorageInvalidObserverStatus =

+ 2 - 0
FirebaseStorage/Sources/FIRStorageErrors.h

@@ -67,4 +67,6 @@ NS_ASSUME_NONNULL_BEGIN
 
 @end
 
+FOUNDATION_EXPORT NSString *const FIRStorageErrorDomainInternal;
+
 NS_ASSUME_NONNULL_END

+ 4 - 2
FirebaseStorage/Sources/FIRStorageErrors.m

@@ -113,7 +113,9 @@
 
   errorDictionary[NSLocalizedDescriptionKey] = errorMessage;
 
-  NSError *err = [NSError errorWithDomain:FIRStorageErrorDomain code:code userInfo:errorDictionary];
+  NSError *err = [NSError errorWithDomain:FIRStorageErrorDomainInternal
+                                     code:code
+                                 userInfo:errorDictionary];
   return err;
 }
 
@@ -182,7 +184,7 @@
 }
 
 + (NSError *)errorWithCustomMessage:(NSString *)errorMessage {
-  return [NSError errorWithDomain:FIRStorageErrorDomain
+  return [NSError errorWithDomain:FIRStorageErrorDomainInternal
                              code:FIRIMPLStorageErrorCodeUnknown
                          userInfo:@{NSLocalizedDescriptionKey : errorMessage}];
 }

+ 1 - 1
FirebaseStorage/Sources/FIRStorageUploadTask.m

@@ -213,7 +213,7 @@
         userInfo[NSUnderlyingErrorKey] = fileReachabilityError;
       }
 
-      *outError = [NSError errorWithDomain:FIRStorageErrorDomain
+      *outError = [NSError errorWithDomain:FIRStorageErrorDomainInternal
                                       code:FIRIMPLStorageErrorCodeUnknown
                                   userInfo:userInfo];
     }

+ 1 - 1
FirebaseStorage/Sources/FIRStorageUtils.m

@@ -133,7 +133,7 @@ NSString *const kGCSObjectAllowedCharacterSet =
 }
 
 + (NSError *)storageErrorWithDescription:(NSString *)description code:(NSInteger)code {
-  return [NSError errorWithDomain:FIRStorageErrorDomain
+  return [NSError errorWithDomain:FIRStorageErrorDomainInternal
                              code:code
                          userInfo:@{NSLocalizedDescriptionKey : description}];
 }

+ 0 - 5
FirebaseStorage/Sources/Public/FirebaseStorage/FIRStorageConstants.h

@@ -106,11 +106,6 @@ typedef NS_ENUM(NSInteger, FIRIMPLStorageTaskStatus) {
   FIRIMPLStorageTaskStatusFailure
 };
 
-/**
- * Firebase Storage error domain.
- */
-FOUNDATION_EXPORT NSString *const FIRStorageErrorDomain;
-
 /**
  * Enum representing the errors raised by Firebase Storage.
  */

+ 3 - 1
Firestore/CHANGELOG.md

@@ -1,4 +1,6 @@
-# Unreleased
+# v8.15.0
+- [changed] Potentially fixed a crash during application exit caused by an
+  assertion about ordering documents by missing fields (#9258).
 - [changed] Add more details to the assertion failure in Query::Comparator() to
   help with future debugging (#9258).
 

+ 14 - 0
Firestore/Example/Firestore.xcodeproj/project.pbxproj

@@ -119,6 +119,7 @@
 		146C140B254F3837A4DD7AE8 /* bits_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB380D01201BC69F00D97691 /* bits_test.cc */; };
 		152543FD706D5E8851C8DA92 /* precondition_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 549CCA5520A36E1F00BCEB75 /* precondition_test.cc */; };
 		153F3E4E9E3A0174E29550B4 /* mutation.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE8220B89AAC00B5BCE7 /* mutation.pb.cc */; };
+		15A5DEC8430E71D64424CBFD /* target_index_matcher_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 63136A2371C0C013EC7A540C /* target_index_matcher_test.cc */; };
 		15A5F95DA733FD89A1E4147D /* limit_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA129F1F315EE100DD57A1 /* limit_spec_test.json */; };
 		15BF63DFF3A7E9A5376C4233 /* transform_operation_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 33607A3AE91548BD219EC9C6 /* transform_operation_test.cc */; };
 		15F54E9538839D56A40C5565 /* watch_change_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2D7472BC70C024D736FF74D9 /* watch_change_test.cc */; };
@@ -184,6 +185,7 @@
 		22A00AC39CAB3426A943E037 /* query.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 544129D621C2DDC800EFB9CC /* query.pb.cc */; };
 		23C04A637090E438461E4E70 /* latlng.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE9220B89AAC00B5BCE7 /* latlng.pb.cc */; };
 		23EFC681986488B033C2B318 /* leveldb_opener_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 75860CD13AF47EB1EA39EC2F /* leveldb_opener_test.cc */; };
+		2428E92E063EBAEA44BA5913 /* target_index_matcher_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 63136A2371C0C013EC7A540C /* target_index_matcher_test.cc */; };
 		248DE4F56DD938F4DBCCF39B /* bundle_reader_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 6ECAF7DE28A19C69DF386D88 /* bundle_reader_test.cc */; };
 		24CB39421C63CD87242B31DF /* bundle_reader_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 6ECAF7DE28A19C69DF386D88 /* bundle_reader_test.cc */; };
 		254CD651CB621D471BC5AC12 /* target_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B5C37696557C81A6C2B7271A /* target_cache_test.cc */; };
@@ -767,6 +769,7 @@
 		84285C3F63D916A4786724A8 /* field_index_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = BF76A8DA34B5B67B4DD74666 /* field_index_test.cc */; };
 		843EE932AA9A8F43721F189E /* leveldb_local_store_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5FF903AEFA7A3284660FA4C5 /* leveldb_local_store_test.cc */; };
 		8460C97C9209D7DAF07090BD /* FIRFieldsTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E06A202154D500B64F25 /* FIRFieldsTests.mm */; };
+		84E75527F3739131C09BEAA5 /* target_index_matcher_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 63136A2371C0C013EC7A540C /* target_index_matcher_test.cc */; };
 		851346D66DEC223E839E3AA9 /* memory_mutation_queue_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 74FBEFA4FE4B12C435011763 /* memory_mutation_queue_test.cc */; };
 		856A1EAAD674ADBDAAEDAC37 /* bundle_builder.cc in Sources */ = {isa = PBXBuildFile; fileRef = 4F5B96F3ABCD2CA901DB1CD4 /* bundle_builder.cc */; };
 		85B8918FC8C5DC62482E39C3 /* resource_path_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B686F2B02024FFD70028D6BE /* resource_path_test.cc */; };
@@ -961,6 +964,7 @@
 		B235E260EA0DCB7BAC04F69B /* field_path_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B686F2AD2023DDB20028D6BE /* field_path_test.cc */; };
 		B28ACC69EB1F232AE612E77B /* async_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 872C92ABD71B12784A1C5520 /* async_testing.cc */; };
 		B371628DA91E80B64AE53085 /* FIRFieldPathTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04C202154AA00B64F25 /* FIRFieldPathTests.mm */; };
+		B384E0F90D4CCC15C88CAF30 /* target_index_matcher_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 63136A2371C0C013EC7A540C /* target_index_matcher_test.cc */; };
 		B3A309CCF5D75A555C7196E1 /* path_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 403DBF6EFB541DFD01582AA3 /* path_test.cc */; };
 		B3B8608727430210C4405AC0 /* FSTMemorySpecTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E02F20213FFC00B64F25 /* FSTMemorySpecTests.mm */; };
 		B3C87C635527A2E57944B789 /* ordered_code_benchmark.cc in Sources */ = {isa = PBXBuildFile; fileRef = 0473AFFF5567E667A125347B /* ordered_code_benchmark.cc */; };
@@ -1069,6 +1073,7 @@
 		C7F174164D7C55E35A526009 /* resource_path_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B686F2B02024FFD70028D6BE /* resource_path_test.cc */; };
 		C7F3C6F569BBA904477F011C /* memory_target_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2286F308EFB0534B1BDE05B9 /* memory_target_cache_test.cc */; };
 		C80B10E79CDD7EF7843C321E /* objc_type_traits_apple_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2A0CF41BA5AED6049B0BEB2C /* objc_type_traits_apple_test.mm */; };
+		C8722550B56CEB96F84DCE94 /* target_index_matcher_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 63136A2371C0C013EC7A540C /* target_index_matcher_test.cc */; };
 		C8A573895D819A92BF16B5E5 /* mutation_queue_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 3068AA9DFBBA86C1FE2A946E /* mutation_queue_test.cc */; };
 		C8BC50508337800E8B098F57 /* bundle_loader_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = A853C81A6A5A51C9D0389EDA /* bundle_loader_test.cc */; };
 		C8C4CB7B6E23FC340BEC6D7F /* load_bundle_task_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 8F1A7B4158D9DD76EE4836BF /* load_bundle_task_test.cc */; };
@@ -1247,6 +1252,7 @@
 		F10A3E4E164A5458DFF7EDE6 /* leveldb_remote_document_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 0840319686A223CC4AD3FAB1 /* leveldb_remote_document_cache_test.cc */; };
 		F19B749671F2552E964422F7 /* FIRListenerRegistrationTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E06B202154D500B64F25 /* FIRListenerRegistrationTests.mm */; };
 		F272A8C41D2353700A11D1FB /* field_mask_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 549CCA5320A36E1F00BCEB75 /* field_mask_test.cc */; };
+		F27347560A963E8162C56FF3 /* target_index_matcher_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 63136A2371C0C013EC7A540C /* target_index_matcher_test.cc */; };
 		F2AB7EACA1B9B1A7046D3995 /* FSTSyncEngineTestDriver.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E02E20213FFC00B64F25 /* FSTSyncEngineTestDriver.mm */; };
 		F3261CBFC169DB375A0D9492 /* FSTMockDatastore.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E02D20213FFC00B64F25 /* FSTMockDatastore.mm */; };
 		F3DEF2DB11FADAABDAA4C8BB /* bundle_builder.cc in Sources */ = {isa = PBXBuildFile; fileRef = 4F5B96F3ABCD2CA901DB1CD4 /* bundle_builder.cc */; };
@@ -1561,6 +1567,7 @@
 		61F72C5520BC48FD001A68CB /* serializer_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = serializer_test.cc; sourceTree = "<group>"; };
 		620C1427763BA5D3CCFB5A1F /* BridgingHeader.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = BridgingHeader.h; sourceTree = "<group>"; };
 		62E103B28B48A81D682A0DE9 /* Pods_Firestore_Example_tvOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Example_tvOS.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		63136A2371C0C013EC7A540C /* target_index_matcher_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = target_index_matcher_test.cc; sourceTree = "<group>"; };
 		64AA92CFA356A2360F3C5646 /* filesystem_testing.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = filesystem_testing.h; sourceTree = "<group>"; };
 		69E6C311558EC77729A16CF1 /* Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.debug.xcconfig"; sourceTree = "<group>"; };
 		6AE927CDFC7A72BF825BE4CB /* Pods-Firestore_Tests_tvOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests_tvOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Tests_tvOS/Pods-Firestore_Tests_tvOS.release.xcconfig"; sourceTree = "<group>"; };
@@ -2432,6 +2439,7 @@
 				549CCA5520A36E1F00BCEB75 /* precondition_test.cc */,
 				B686F2B02024FFD70028D6BE /* resource_path_test.cc */,
 				ABA495B9202B7E79008A7851 /* snapshot_version_test.cc */,
+				63136A2371C0C013EC7A540C /* target_index_matcher_test.cc */,
 				33607A3AE91548BD219EC9C6 /* transform_operation_test.cc */,
 				40F9D09063A07F710811A84F /* value_util_test.cc */,
 			);
@@ -3659,6 +3667,7 @@
 				4A3FF3B16A39A5DC6B7EBA51 /* target.pb.cc in Sources */,
 				6D7F70938662E8CA334F11C2 /* target_cache_test.cc in Sources */,
 				E764F0F389E7119220EB212C /* target_id_generator_test.cc in Sources */,
+				B384E0F90D4CCC15C88CAF30 /* target_index_matcher_test.cc in Sources */,
 				88929ED628DA8DD9592974ED /* task_test.cc in Sources */,
 				32A95242C56A1A230231DB6A /* testutil.cc in Sources */,
 				5497CB78229DECDE000FB92F /* time_testing.cc in Sources */,
@@ -3856,6 +3865,7 @@
 				81B23D2D4E061074958AF12F /* target.pb.cc in Sources */,
 				6AED40FF444F0ACFE3AE96E3 /* target_cache_test.cc in Sources */,
 				DA4303684707606318E1914D /* target_id_generator_test.cc in Sources */,
+				2428E92E063EBAEA44BA5913 /* target_index_matcher_test.cc in Sources */,
 				67CF9FAA890307780731E1DA /* task_test.cc in Sources */,
 				8388418F43042605FB9BFB92 /* testutil.cc in Sources */,
 				5497CB79229DECDE000FB92F /* time_testing.cc in Sources */,
@@ -4067,6 +4077,7 @@
 				EC3331B17394886A3715CFD8 /* target.pb.cc in Sources */,
 				7DB0915EF7C22C700A423F7C /* target_cache_test.cc in Sources */,
 				71E2B154C4FB63F7B7CC4B50 /* target_id_generator_test.cc in Sources */,
+				C8722550B56CEB96F84DCE94 /* target_index_matcher_test.cc in Sources */,
 				76A5447D76F060E996555109 /* task_test.cc in Sources */,
 				409C0F2BFC2E1BECFFAC4D32 /* testutil.cc in Sources */,
 				6300709ECDE8E0B5A8645F8D /* time_testing.cc in Sources */,
@@ -4278,6 +4289,7 @@
 				B3E6F4CDB1663407F0980C7A /* target.pb.cc in Sources */,
 				66CA091F8B610E0FB0A3F8A4 /* target_cache_test.cc in Sources */,
 				A05BC6BDA2ABE405009211A9 /* target_id_generator_test.cc in Sources */,
+				15A5DEC8430E71D64424CBFD /* target_index_matcher_test.cc in Sources */,
 				93C8F772F4DC5A985FA3D815 /* task_test.cc in Sources */,
 				A17DBC8F24127DA8A381F865 /* testutil.cc in Sources */,
 				A25FF76DEF542E01A2DF3B0E /* time_testing.cc in Sources */,
@@ -4485,6 +4497,7 @@
 				618BBEA620B89AAC00B5BCE7 /* target.pb.cc in Sources */,
 				254CD651CB621D471BC5AC12 /* target_cache_test.cc in Sources */,
 				AB380CFB2019388600D97691 /* target_id_generator_test.cc in Sources */,
+				F27347560A963E8162C56FF3 /* target_index_matcher_test.cc in Sources */,
 				662793139A36E5CFC935B949 /* task_test.cc in Sources */,
 				54A0352A20A3B3BD003E0143 /* testutil.cc in Sources */,
 				5497CB77229DECDE000FB92F /* time_testing.cc in Sources */,
@@ -4715,6 +4728,7 @@
 				6FAC16B7FBD3B40D11A6A816 /* target.pb.cc in Sources */,
 				FA90FA91F7381E5C678EFA30 /* target_cache_test.cc in Sources */,
 				306E762DC6B829CED4FD995D /* target_id_generator_test.cc in Sources */,
+				84E75527F3739131C09BEAA5 /* target_index_matcher_test.cc in Sources */,
 				C57B15CADD8C3E806B154C19 /* task_test.cc in Sources */,
 				CA989C0E6020C372A62B7062 /* testutil.cc in Sources */,
 				2D220B9ABFA36CD7AC43D0A7 /* time_testing.cc in Sources */,

+ 1 - 1
Firestore/Example/Tests/API/FSTAPIHelpers.h

@@ -58,7 +58,7 @@ FIRDocumentReference *FSTTestDocRef(const char *path);
  * contents.
  * @param hasPendingWrites Whether the query snapshot has pending writes to the server.
  * @param fromCache Whether the query snapshot is cache result.
- * @returns A query snapshot that consists of both sets of documents.
+ * @return A query snapshot that consists of both sets of documents.
  */
 FIRQuerySnapshot *FSTTestQuerySnapshot(
     const char *path,

+ 1 - 0
Firestore/Protos/CMakeLists.txt

@@ -38,6 +38,7 @@ set(
   firestore/bundle
   google/api/annotations
   google/api/http
+  google/firestore/admin/index
   google/firestore/v1/common
   google/firestore/v1/document
   google/firestore/v1/firestore

+ 15 - 15
Firestore/Source/Public/FirebaseFirestore/FIRCollectionReference.h

@@ -23,8 +23,8 @@ NS_ASSUME_NONNULL_BEGIN
 @class FIRDocumentReference;
 
 /**
- * A `FIRCollectionReference` object can be used for adding documents, getting document references,
- * and querying for documents (using the methods inherited from `FIRQuery`).
+ * A `CollectionReference` object can be used for adding documents, getting document references,
+ * and querying for documents (using the methods inherited from `Query`).
  */
 NS_SWIFT_NAME(CollectionReference)
 @interface FIRCollectionReference : FIRQuery
@@ -36,32 +36,32 @@ NS_SWIFT_NAME(CollectionReference)
 @property(nonatomic, strong, readonly) NSString *collectionID;
 
 /**
- * For subcollections, `parent` returns the containing `FIRDocumentReference`.  For root
- * collections, nil is returned.
+ * For subcollections, `parent` returns the containing `DocumentReference`.  For root collections,
+ * `nil` is returned.
  */
 @property(nonatomic, strong, nullable, readonly) FIRDocumentReference *parent;
 
 /**
- * A string containing the slash-separated path to this this `FIRCollectionReference` (relative to
- * the root of the database).
+ * A string containing the slash-separated path to this this `CollectionReference` (relative to the
+ * root of the database).
  */
 @property(nonatomic, strong, readonly) NSString *path;
 
 /**
- * Returns a FIRDocumentReference pointing to a new document with an auto-generated ID.
+ * Returns a `DocumentReference` pointing to a new document with an auto-generated ID.
  *
- * @return A FIRDocumentReference pointing to a new document with an auto-generated ID.
+ * @return A `DocumentReference` pointing to a new document with an auto-generated ID.
  */
 - (FIRDocumentReference *)documentWithAutoID NS_SWIFT_NAME(document());
 
 /**
- * Gets a `FIRDocumentReference` referring to the document at the specified path, relative to this
+ * Gets a `DocumentReference` referring to the document at the specified path, relative to this
  * collection's own path.
  *
  * @param documentPath The slash-separated relative path of the document for which to get a
- * `FIRDocumentReference`.
+ * `DocumentReference`.
  *
- * @return The `FIRDocumentReference` for the specified document path.
+ * @return The `DocumentReference` for the specified document path.
  */
 - (FIRDocumentReference *)documentWithPath:(NSString *)documentPath NS_SWIFT_NAME(document(_:));
 
@@ -69,9 +69,9 @@ NS_SWIFT_NAME(CollectionReference)
  * Adds a new document to this collection with the specified data, assigning it a document ID
  * automatically.
  *
- * @param data An `NSDictionary` containing the data for the new document.
+ * @param data A `Dictionary` containing the data for the new document.
  *
- * @return A `FIRDocumentReference` pointing to the newly created document.
+ * @return A `DocumentReference` pointing to the newly created document.
  */
 - (FIRDocumentReference *)addDocumentWithData:(NSDictionary<NSString *, id> *)data
     NS_SWIFT_NAME(addDocument(data:));
@@ -80,12 +80,12 @@ NS_SWIFT_NAME(CollectionReference)
  * Adds a new document to this collection with the specified data, assigning it a document ID
  * automatically.
  *
- * @param data An `NSDictionary` containing the data for the new document.
+ * @param data A `Dictionary` containing the data for the new document.
  * @param completion A block to execute once the document has been successfully written to
  *     the server. This block will not be called while the client is offline, though local
  *     changes will be visible immediately.
  *
- * @return A `FIRDocumentReference` pointing to the newly created document.
+ * @return A `DocumentReference` pointing to the newly created document.
  */
 // clang-format off
 // clang-format breaks the NS_SWIFT_NAME attribute

+ 7 - 7
Firestore/Source/Public/FirebaseFirestore/FIRDocumentChange.h

@@ -40,7 +40,7 @@ typedef NS_ENUM(NSInteger, FIRDocumentChangeType)
 } NS_SWIFT_NAME(DocumentChangeType);
 
 /**
- * A `FIRDocumentChange` represents a change to the documents matching a query. It contains the
+ * A `DocumentChange` represents a change to the documents matching a query. It contains the
  * document affected and the type of change that occurred (added, modified, or removed).
  */
 NS_SWIFT_NAME(DocumentChange)
@@ -56,16 +56,16 @@ NS_SWIFT_NAME(DocumentChange)
 @property(nonatomic, strong, readonly) FIRQueryDocumentSnapshot *document;
 
 /**
- * The index of the changed document in the result set immediately prior to this FIRDocumentChange
- * (i.e. supposing that all prior FIRDocumentChange objects have been applied). NSNotFound for
- * FIRDocumentChangeTypeAdded events.
+ * The index of the changed document in the result set immediately prior to this `DocumentChange`
+ * (i.e. supposing that all prior `DocumentChange` objects have been applied). `NSNotFound` for
+ * `DocumentChangeTypeAdded` events.
  */
 @property(nonatomic, readonly) NSUInteger oldIndex;
 
 /**
- * The index of the changed document in the result set immediately after this FIRDocumentChange
- * (i.e. supposing that all prior FIRDocumentChange objects and the current FIRDocumentChange object
- * have been applied). NSNotFound for FIRDocumentChangeTypeRemoved events.
+ * The index of the changed document in the result set immediately after this `DocumentChange`
+ * (i.e. supposing that all prior `DocumentChange` objects and the current `DocumentChange` object
+ * have been applied). `NSNotFound` for `DocumentChangeTypeRemoved` events.
  */
 @property(nonatomic, readonly) NSUInteger newIndex;
 

+ 39 - 38
Firestore/Source/Public/FirebaseFirestore/FIRDocumentReference.h

@@ -33,10 +33,10 @@ typedef void (^FIRDocumentSnapshotBlock)(FIRDocumentSnapshot *_Nullable snapshot
     NS_SWIFT_UNAVAILABLE("Use Swift's closure syntax instead.");
 
 /**
- * A `FIRDocumentReference` refers to a document location in a Firestore database and can be
+ * A `DocumentReference` refers to a document location in a Firestore database and can be
  * used to write, read, or listen to the location. The document at the referenced location
- * may or may not exist. A `FIRDocumentReference` can also be used to create a
- * `FIRCollectionReference` to a subcollection.
+ * may or may not exist. A `DocumentReference` can also be used to create a `CollectionReference` to
+ * a subcollection.
  */
 NS_SWIFT_NAME(DocumentReference)
 @interface FIRDocumentReference : NSObject
@@ -51,7 +51,7 @@ NS_SWIFT_NAME(DocumentReference)
 /** A reference to the collection to which this `DocumentReference` belongs. */
 @property(nonatomic, strong, readonly) FIRCollectionReference *parent;
 
-/** The `FIRFirestore` for the Firestore database (useful for performing transactions, etc.). */
+/** The `Firestore` for the Firestore database (useful for performing transactions, etc.). */
 @property(nonatomic, strong, readonly) FIRFirestore *firestore;
 
 /**
@@ -61,13 +61,13 @@ NS_SWIFT_NAME(DocumentReference)
 @property(nonatomic, strong, readonly) NSString *path;
 
 /**
- * Gets a `FIRCollectionReference` referring to the collection at the specified
- * path, relative to this document.
+ * Gets a `CollectionReference` referring to the collection at the specified path, relative to this
+ * document.
  *
  * @param collectionPath The slash-separated relative path of the collection for which to get a
- * `FIRCollectionReference`.
+ * `CollectionReference`.
  *
- * @return The `FIRCollectionReference` at the specified _collectionPath_.
+ * @return The `CollectionReference` at the specified _collectionPath_.
  */
 - (FIRCollectionReference *)collectionWithPath:(NSString *)collectionPath
     NS_SWIFT_NAME(collection(_:));
@@ -75,21 +75,21 @@ NS_SWIFT_NAME(DocumentReference)
 #pragma mark - Writing Data
 
 /**
- * Writes to the document referred to by `FIRDocumentReference`. If the document doesn't yet exist,
+ * Writes to the document referred to by `DocumentReference`. If the document doesn't yet exist,
  * this method creates it and then sets the data. If the document exists, this method overwrites
  * the document data with the new values.
  *
- * @param documentData An `NSDictionary` that contains the fields and data to write to the
+ * @param documentData A `Dictionary` that contains the fields and data to write to the
  * document.
  */
 - (void)setData:(NSDictionary<NSString *, id> *)documentData;
 
 /**
- * Writes to the document referred to by this DocumentReference. If the document does not yet
- * exist, it will be created. If you pass `merge:YES`, the provided data will be merged into
+ * Writes to the document referred to by this `DocumentReference`. If the document does not yet
+ * exist, it will be created. If you pass `merge:true`, the provided data will be merged into
  * any existing document.
  *
- * @param documentData An `NSDictionary` that contains the fields and data to write to the
+ * @param documentData A `Dictionary` that contains the fields and data to write to the
  * document.
  * @param merge Whether to merge the provided data into any existing document. If enabled,
  * all omitted fields remain untouched. If your input sets any field to an empty dictionary, any
@@ -106,9 +106,9 @@ NS_SWIFT_NAME(DocumentReference)
  * It is an error to include a field in `mergeFields` that does not have a corresponding
  * value in the `data` dictionary.
  *
- * @param documentData An `NSDictionary` containing the fields that make up the document
+ * @param documentData A `Dictionary` containing the fields that make up the document
  * to be written.
- * @param mergeFields An `NSArray` that contains a list of `NSString` or `FIRFieldPath` elements
+ * @param mergeFields An `Array` that contains a list of `String` or `FieldPath` elements
  * specifying which fields to merge. Fields can contain dots to reference nested fields within
  * the document. If your input sets any field to an empty dictionary, any nested field is
  * overwritten.
@@ -116,10 +116,10 @@ NS_SWIFT_NAME(DocumentReference)
 - (void)setData:(NSDictionary<NSString *, id> *)documentData mergeFields:(NSArray<id> *)mergeFields;
 
 /**
- * Overwrites the document referred to by this `FIRDocumentReference`. If no document exists, it
+ * Overwrites the document referred to by this `DocumentReference`. If no document exists, it
  * is created. If a document already exists, it is overwritten.
  *
- * @param documentData An `NSDictionary` containing the fields that make up the document
+ * @param documentData A `Dictionary` containing the fields that make up the document
  *     to be written.
  * @param completion A block to execute once the document has been successfully written to the
  *     server. This block will not be called while the client is offline, though local
@@ -129,11 +129,11 @@ NS_SWIFT_NAME(DocumentReference)
      completion:(nullable void (^)(NSError *_Nullable error))completion;
 
 /**
- * Writes to the document referred to by this DocumentReference. If the document does not yet
- * exist, it will be created. If you pass `merge:YES`, the provided data will be merged into
+ * Writes to the document referred to by this `DocumentReference`. If the document does not yet
+ * exist, it will be created. If you pass `merge:true`, the provided data will be merged into
  * any existing document.
  *
- * @param documentData An `NSDictionary` containing the fields that make up the document
+ * @param documentData A `Dictionary` containing the fields that make up the document
  * to be written.
  * @param merge Whether to merge the provided data into any existing document. If your input sets
  *     any field to an empty dictionary, any nested field is overwritten.
@@ -154,9 +154,9 @@ NS_SWIFT_NAME(DocumentReference)
  * It is an error to include a field in `mergeFields` that does not have a corresponding
  * value in the `data` dictionary.
  *
- * @param documentData An `NSDictionary` containing the fields that make up the document
+ * @param documentData A `Dictionary` containing the fields that make up the document
  * to be written.
- * @param mergeFields An `NSArray` that contains a list of `NSString` or `FIRFieldPath` elements
+ * @param mergeFields An `Array` that contains a list of `String` or `FieldPath` elements
  *     specifying which fields to merge. Fields can contain dots to reference nested fields within
  *     the document. If your input sets any field to an empty dictionary, any nested field is
  *     overwritten.
@@ -169,20 +169,20 @@ NS_SWIFT_NAME(DocumentReference)
      completion:(nullable void (^)(NSError *_Nullable error))completion;
 
 /**
- * Updates fields in the document referred to by this `FIRDocumentReference`.
+ * Updates fields in the document referred to by this `DocumentReference`.
  * If the document does not exist, the update fails (specify a completion block to be notified).
  *
- * @param fields An `NSDictionary` containing the fields (expressed as an `NSString` or
- *     `FIRFieldPath`) and values with which to update the document.
+ * @param fields A `Dictionary` containing the fields (expressed as an `String` or
+ *     `FieldPath`) and values with which to update the document.
  */
 - (void)updateData:(NSDictionary<id, id> *)fields;
 
 /**
- * Updates fields in the document referred to by this `FIRDocumentReference`. If the document
+ * Updates fields in the document referred to by this `DocumentReference`. If the document
  * does not exist, the update fails and the specified completion block receives an error.
  *
- * @param fields An `NSDictionary` containing the fields (expressed as an `NSString` or
- *     `FIRFieldPath`) and values with which to update the document.
+ * @param fields A `Dictionary` containing the fields (expressed as a `String` or
+ *     `FieldPath`) and values with which to update the document.
  * @param completion A block to execute when the update is complete. If the update is successful the
  *     error parameter will be nil, otherwise it will give an indication of how the update failed.
  *     This block will only execute when the client is online and the commit has completed against
@@ -192,14 +192,15 @@ NS_SWIFT_NAME(DocumentReference)
 - (void)updateData:(NSDictionary<id, id> *)fields
         completion:(nullable void (^)(NSError *_Nullable error))completion;
 
-// NOTE: this is named 'deleteDocument' because 'delete' is a keyword in Objective-C++.
-/** Deletes the document referred to by this `FIRDocumentReference`. */
+// NOTE: this method is named 'deleteDocument' in Objective-C because 'delete' is a keyword in
+// Objective-C++.
+/** Deletes the document referred to by this `DocumentReference`. */
 // clang-format off
 - (void)deleteDocument NS_SWIFT_NAME(delete());
 // clang-format on
 
 /**
- * Deletes the document referred to by this `FIRDocumentReference`.
+ * Deletes the document referred to by this `DocumentReference`.
  *
  * @param completion A block to execute once the document has been successfully written to the
  *     server. This block will not be called while the client is offline, though local
@@ -213,7 +214,7 @@ NS_SWIFT_NAME(DocumentReference)
 #pragma mark - Retrieving Data
 
 /**
- * Reads the document referenced by this `FIRDocumentReference`.
+ * Reads the document referenced by this `DocumentReference`.
  *
  * This method attempts to provide up-to-date data when possible by waiting for
  * data from the server, but it may return cached data or fail if you are
@@ -227,7 +228,7 @@ NS_SWIFT_NAME(DocumentReference)
     NS_SWIFT_NAME(getDocument(completion:));
 
 /**
- * Reads the document referenced by this `FIRDocumentReference`.
+ * Reads the document referenced by this `DocumentReference`.
  *
  * @param source indicates whether the results should be fetched from the cache
  *     only (`Source.cache`), the server only (`Source.server`), or to attempt
@@ -242,24 +243,24 @@ NS_SWIFT_NAME(DocumentReference)
 // clang-format on
 
 /**
- * Attaches a listener for DocumentSnapshot events.
+ * Attaches a listener for `DocumentSnapshot` events.
  *
  * @param listener The listener to attach.
  *
- * @return A FIRListenerRegistration that can be used to remove this listener.
+ * @return A `ListenerRegistration` that can be used to remove this listener.
  */
 - (id<FIRListenerRegistration>)addSnapshotListener:
     (void (^)(FIRDocumentSnapshot *_Nullable snapshot, NSError *_Nullable error))listener
     NS_SWIFT_NAME(addSnapshotListener(_:));
 
 /**
- * Attaches a listener for DocumentSnapshot events.
+ * Attaches a listener for `DocumentSnapshot` events.
  *
  * @param includeMetadataChanges Whether metadata-only changes (i.e. only
- *     `FIRDocumentSnapshot.metadata` changed) should trigger snapshot events.
+ *     `DocumentSnapshot.metadata` changed) should trigger snapshot events.
  * @param listener The listener to attach.
  *
- * @return A FIRListenerRegistration that can be used to remove this listener.
+ * @return A `ListenerRegistration` that can be used to remove this listener.
  */
 // clang-format off
 - (id<FIRListenerRegistration>)

+ 14 - 14
Firestore/Source/Public/FirebaseFirestore/FIRDocumentSnapshot.h

@@ -48,11 +48,11 @@ typedef NS_ENUM(NSInteger, FIRServerTimestampBehavior) {
 } NS_SWIFT_NAME(ServerTimestampBehavior);
 
 /**
- * A `FIRDocumentSnapshot` contains data read from a document in your Firestore database. The data
+ * A `DocumentSnapshot` contains data read from a document in your Firestore database. The data
  * can be extracted with the `data` property or by using subscript syntax to access a specific
  * field.
  *
- * For a `FIRDocumentSnapshot` that points to a non-existing document, any data access will return
+ * For a `DocumentSnapshot` that points to a non-existing document, any data access will return
  * `nil`. You can use the `exists` property to explicitly verify a documents existence.
  */
 NS_SWIFT_NAME(DocumentSnapshot)
@@ -65,23 +65,23 @@ NS_SWIFT_NAME(DocumentSnapshot)
 /** True if the document exists. */
 @property(nonatomic, assign, readonly) BOOL exists;
 
-/** A `FIRDocumentReference` to the document location. */
+/** A `DocumentReference` to the document location. */
 @property(nonatomic, strong, readonly) FIRDocumentReference *reference;
 
-/** The ID of the document for which this `FIRDocumentSnapshot` contains data. */
+/** The ID of the document for which this `DocumentSnapshot` contains data. */
 @property(nonatomic, copy, readonly) NSString *documentID;
 
 /** Metadata about this snapshot concerning its source and if it has local modifications. */
 @property(nonatomic, strong, readonly) FIRSnapshotMetadata *metadata;
 
 /**
- * Retrieves all fields in the document as an `NSDictionary`. Returns `nil` if the document doesn't
+ * Retrieves all fields in the document as a `Dictionary`. Returns `nil` if the document doesn't
  * exist.
  *
  * Server-provided timestamps that have not yet been set to their final value will be returned as
- * `NSNull`. You can use `dataWithServerTimestampBehavior()` to configure this behavior.
+ * `NSNull`. You can use the `data(with:)` method to configure this behavior.
  *
- * @return An `NSDictionary` containing all fields in the document or `nil` if the document doesn't
+ * @return A `Dictionary` containing all fields in the document or `nil` if the document doesn't
  *     exist.
  */
 - (nullable NSDictionary<NSString *, id> *)data;
@@ -102,7 +102,7 @@ NS_SWIFT_NAME(DocumentSnapshot)
  * Retrieves a specific field from the document. Returns `nil` if the document or the field doesn't
  * exist.
  *
- * The timestamps that have not yet been set to their final value will be returned as `NSNull`. The
+ * The timestamps that have not yet been set to their final value will be returned as `NSNull`. You
  * can use `get(_:serverTimestampBehavior:)` to configure this behavior.
  *
  * @param field The field to retrieve.
@@ -114,7 +114,7 @@ NS_SWIFT_NAME(DocumentSnapshot)
  * Retrieves a specific field from the document. Returns `nil` if the document or the field doesn't
  * exist.
  *
- * The timestamps that have not yet been set to their final value will be returned as `NSNull`. The
+ * The timestamps that have not yet been set to their final value will be returned as `NSNull`. You
  * can use `get(_:serverTimestampBehavior:)` to configure this behavior.
  *
  * @param field The field to retrieve.
@@ -140,11 +140,11 @@ NS_SWIFT_NAME(DocumentSnapshot)
 @end
 
 /**
- * A `FIRQueryDocumentSnapshot` contains data read from a document in your Firestore database as
+ * A `QueryDocumentSnapshot` contains data read from a document in your Firestore database as
  * part of a query. The document is guaranteed to exist and its data can be extracted with the
  * `data` property or by using subscript syntax to access a specific field.
  *
- * A `FIRQueryDocumentSnapshot` offers the same API surface as a `FIRDocumentSnapshot`. As
+ * A `QueryDocumentSnapshot` offers the same API surface as a `DocumentSnapshot`. As
  * deleted documents are not returned from queries, its `exists` property will always be true and
  * `data:` will never return `nil`.
  */
@@ -156,12 +156,12 @@ NS_SWIFT_NAME(QueryDocumentSnapshot)
     __attribute__((unavailable("FIRQueryDocumentSnapshot cannot be created directly.")));
 
 /**
- * Retrieves all fields in the document as an `NSDictionary`.
+ * Retrieves all fields in the document as a `Dictionary`.
  *
  * Server-provided timestamps that have not yet been set to their final value will be returned as
- * `NSNull`. You can use `dataWithServerTimestampBehavior()` to configure this behavior.
+ * `NSNull`. You can use the `data(with:)` method to configure this behavior.
  *
- * @return An `NSDictionary` containing all fields in the document.
+ * @return A `Dictionary` containing all fields in the document.
  */
 - (NSDictionary<NSString *, id> *)data;
 

+ 15 - 15
Firestore/Source/Public/FirebaseFirestore/FIRFieldValue.h

@@ -19,7 +19,7 @@
 NS_ASSUME_NONNULL_BEGIN
 
 /**
- * Sentinel values that can be used when writing document fields with setData() or updateData().
+ * Sentinel values that can be used when writing document fields with `setData()` or `updateData()`.
  */
 NS_SWIFT_NAME(FieldValue)
 @interface FIRFieldValue : NSObject
@@ -27,56 +27,56 @@ NS_SWIFT_NAME(FieldValue)
 /** :nodoc: */
 - (instancetype)init NS_UNAVAILABLE;
 
-/** Used with updateData() to mark a field for deletion. */
+/** Used with `updateData()` to mark a field for deletion. */
 // clang-format off
 + (instancetype)fieldValueForDelete NS_SWIFT_NAME(delete());
 // clang-format on
 
 /**
- * Used with setData() or updateData() to include a server-generated timestamp in the written
+ * Used with `setData()` or `updateData()` to include a server-generated timestamp in the written
  * data.
  */
 + (instancetype)fieldValueForServerTimestamp NS_SWIFT_NAME(serverTimestamp());
 
 /**
- * Returns a special value that can be used with setData() or updateData() that tells the server to
- * union the given elements with any array value that already exists on the server. Each
+ * Returns a special value that can be used with `setData()` or `updateData()` that tells the server
+ * to union the given elements with any array value that already exists on the server. Each
  * specified element that doesn't already exist in the array will be added to the end. If the
  * field being modified is not already an array it will be overwritten with an array containing
  * exactly the specified elements.
  *
  * @param elements The elements to union into the array.
- * @return The FieldValue sentinel for use in a call to setData() or updateData().
+ * @return The `FieldValue` sentinel for use in a call to `setData()` or `updateData()`.
  */
 + (instancetype)fieldValueForArrayUnion:(NSArray<id> *)elements NS_SWIFT_NAME(arrayUnion(_:));
 
 /**
- * Returns a special value that can be used with setData() or updateData() that tells the server to
- * remove the given elements from any array value that already exists on the server. All
+ * Returns a special value that can be used with `setData()` or `updateData()` that tells the server
+ * to remove the given elements from any array value that already exists on the server. All
  * instances of each element specified will be removed from the array. If the field being
  * modified is not already an array it will be overwritten with an empty array.
  *
  * @param elements The elements to remove from the array.
- * @return The FieldValue sentinel for use in a call to setData() or updateData().
+ * @return The `FieldValue` sentinel for use in a call to `setData()` or `updateData()`.
  */
 + (instancetype)fieldValueForArrayRemove:(NSArray<id> *)elements NS_SWIFT_NAME(arrayRemove(_:));
 
 /**
- * Returns a special value that can be used with setData() or updateData() that tells the server to
- * increment the field's current value by the given value.
+ * Returns a special value that can be used with `setData()` or `updateData()` that tells the server
+ * to increment the field's current value by the given value.
  *
  * If the current value is an integer or a double, both the current and the given value will be
  * interpreted as doubles and all arithmetic will follow IEEE 754 semantics. Otherwise, the
  * transformation will set the field to the given value.
  *
  * @param d The double value to increment by.
- * @return The FieldValue sentinel for use in a call to setData() or update().
+ * @return The `FieldValue` sentinel for use in a call to `setData()` or `updateData()`.
  */
 + (instancetype)fieldValueForDoubleIncrement:(double)d NS_SWIFT_NAME(increment(_:));
 
 /**
- * Returns a special value that can be used with setData() or updateData() that tells the server to
- * increment the field's current value by the given value.
+ * Returns a special value that can be used with `setData()` or `updateData()` that tells the server
+ * to increment the field's current value by the given value.
  *
  * If the current field value is an integer, possible integer overflows are resolved to LONG_MAX or
  * LONG_MIN. If the current field value is a double, both values will be interpreted as doubles and
@@ -86,7 +86,7 @@ NS_SWIFT_NAME(FieldValue)
  * will set the field to the given value.
  *
  * @param l The integer value to increment by.
- * @return The FieldValue sentinel for use in a call to setData() or updateData().
+ * @return The `FieldValue` sentinel for use in a call to `setData()` or `updateData()`.
  */
 + (instancetype)fieldValueForIntegerIncrement:(int64_t)l NS_SWIFT_NAME(increment(_:));
 

+ 44 - 45
Firestore/Source/Public/FirebaseFirestore/FIRFirestore.h

@@ -31,7 +31,7 @@
 NS_ASSUME_NONNULL_BEGIN
 
 /**
- * `FIRFirestore` represents a Firestore Database and is the entry point for all Firestore
+ * `Firestore` represents a Firestore Database and is the entry point for all Firestore
  * operations.
  */
 NS_SWIFT_NAME(Firestore)
@@ -42,27 +42,27 @@ NS_SWIFT_NAME(Firestore)
 - (instancetype)init __attribute__((unavailable("Use a static constructor method.")));
 
 /**
- * Creates, caches, and returns a `FIRFirestore` using the default `FIRApp`. Each subsequent
- * invocation returns the same `FIRFirestore` object.
+ * Creates, caches, and returns a `Firestore` using the default `FirebaseApp`. Each subsequent
+ * invocation returns the same `Firestore` object.
  *
- * @return The `FIRFirestore` instance.
+ * @return The `Firestore` instance.
  */
 + (instancetype)firestore NS_SWIFT_NAME(firestore());
 
 /**
- * Creates, caches, and returns a `FIRFirestore` object for the specified _app_. Each subsequent
- * invocation returns the same `FIRFirestore` object.
+ * Creates, caches, and returns a `Firestore` object for the specified _app_. Each subsequent
+ * invocation returns the same `Firestore` object.
  *
- * @param app The `FIRApp` instance to use for authentication and as a source of the Google Cloud
- * Project ID for your Firestore Database. If you want the default instance, you should explicitly
- * set it to `[FIRApp defaultApp]`.
+ * @param app The `FirebaseApp` instance to use for authentication and as a source of the Google
+ * Cloud Project ID for your Firestore Database. If you want the default instance, you should
+ * explicitly set it to `FirebaseApp.app()`.
  *
- * @return The `FIRFirestore` instance.
+ * @return The `Firestore` instance.
  */
 + (instancetype)firestoreForApp:(FIRApp *)app NS_SWIFT_NAME(firestore(app:));
 
 /**
- * Custom settings used to configure this `FIRFirestore` object.
+ * Custom settings used to configure this `Firestore` object.
  */
 @property(nonatomic, copy) FIRFirestoreSettings *settings;
 
@@ -74,25 +74,25 @@ NS_SWIFT_NAME(Firestore)
 #pragma mark - Collections and Documents
 
 /**
- * Gets a `FIRCollectionReference` referring to the collection at the specified path within the
+ * Gets a `CollectionReference` referring to the collection at the specified path within the
  * database.
  *
  * @param collectionPath The slash-separated path of the collection for which to get a
- * `FIRCollectionReference`.
+ * `CollectionReference`.
  *
- * @return The `FIRCollectionReference` at the specified _collectionPath_.
+ * @return The `CollectionReference` at the specified _collectionPath_.
  */
 - (FIRCollectionReference *)collectionWithPath:(NSString *)collectionPath
     NS_SWIFT_NAME(collection(_:));
 
 /**
- * Gets a `FIRDocumentReference` referring to the document at the specified path within the
+ * Gets a `DocumentReference` referring to the document at the specified path within the
  * database.
  *
  * @param documentPath The slash-separated path of the document for which to get a
- * `FIRDocumentReference`.
+ * `DocumentReference`.
  *
- * @return The `FIRDocumentReference` for the specified _documentPath_.
+ * @return The `DocumentReference` for the specified _documentPath_.
  */
 - (FIRDocumentReference *)documentWithPath:(NSString *)documentPath NS_SWIFT_NAME(document(_:));
 
@@ -119,7 +119,7 @@ NS_SWIFT_NAME(Firestore)
  * `FieldValue.increment()` inside a transaction counts as an additional write.
  *
  * In the updateBlock, a set of reads and writes can be performed atomically using the
- * `FIRTransaction` object passed to the block. After the updateBlock is run, Firestore will attempt
+ * `Transaction` object passed to the block. After the updateBlock is run, Firestore will attempt
  * to apply the changes to the server. If any of the data read has been modified outside of this
  * transaction since being read, then the transaction will be retried by executing the updateBlock
  * again. If the transaction still fails after 5 retries, then the transaction will fail.
@@ -128,11 +128,11 @@ NS_SWIFT_NAME(Firestore)
  * would cause side effects.
  *
  * Any value maybe be returned from the updateBlock. If the transaction is successfully committed,
- * then the completion block will be passed that value. The updateBlock also has an `NSError` out
- * parameter. If this is set, then the transaction will not attempt to commit, and the given error
- * will be passed to the completion block.
+ * then the completion block will be passed that value. The updateBlock also has an `NSErrorPointer`
+ * out parameter. If this is set, then the transaction will not attempt to commit, and the given
+ * error will be passed to the completion block.
  *
- * The `FIRTransaction` object passed to the updateBlock contains methods for accessing documents
+ * The `Transaction` object passed to the updateBlock contains methods for accessing documents
  * and collections. Unlike other firestore access, data accessed with the transaction will not
  * reflect local changes that have not been committed. For this reason, it is required that all
  * reads are performed before any writes. Transactions must be performed while online. Otherwise,
@@ -174,16 +174,16 @@ NS_SWIFT_NAME(Firestore)
 
 /**
  * Re-enables usage of the network by this Firestore instance after a prior call to
- * `disableNetworkWithCompletion`. Completion block, if provided, will be called once network uasge
+ * `disableNetwork(completion:)`. Completion block, if provided, will be called once network uasge
  * has been enabled.
  */
 - (void)enableNetworkWithCompletion:(nullable void (^)(NSError *_Nullable error))completion;
 
 /**
  * Disables usage of the network by this Firestore instance. It can be re-enabled by via
- * `enableNetworkWithCompletion`. While the network is disabled, any snapshot listeners or get calls
- * will return results from cache and any write operations will be queued until the network is
- * restored. The completion block, if provided, will be called once network usage has been disabled.
+ * `enableNetwork`. While the network is disabled, any snapshot listeners or get calls will return
+ * results from cache and any write operations will be queued until the network is restored. The
+ * completion block, if provided, will be called once network usage has been disabled.
  */
 - (void)disableNetworkWithCompletion:(nullable void (^)(NSError *_Nullable error))completion;
 
@@ -192,7 +192,7 @@ NS_SWIFT_NAME(Firestore)
  *
  * Must be called while the firestore instance is not started (after the app is shutdown or when
  * the app is first initialized). On startup, this method must be called before other methods
- * (other than `FIRFirestore.settings`). If the firestore instance is still running, the function
+ * (other than `Firestore.settings`). If the firestore instance is still running, the function
  * will complete with an error code of `FailedPrecondition`.
  *
  * Note: `clearPersistence(completion:)` is primarily intended to help write reliable tests that
@@ -211,11 +211,10 @@ NS_SWIFT_NAME(Firestore)
  * Otherwise, the completion block is called when all previously issued writes (including those
  * written in a previous app session) have been acknowledged by the backend. The completion
  * block does not wait for writes that were added after the method is called. If you
- * wish to wait for additional writes, you have to call `waitForPendingWritesWithCompletion`
- * again.
+ * wish to wait for additional writes, you have to call `waitForPendingWrites` again.
  *
- * Any outstanding `waitForPendingWritesWithCompletion` completion blocks are called with an
- * error during user change.
+ * Any outstanding `waitForPendingWrites(completion:)` completion blocks are called with an error
+ * during user change.
  */
 - (void)waitForPendingWritesWithCompletion:(void (^)(NSError *_Nullable error))completion;
 
@@ -230,7 +229,7 @@ NS_SWIFT_NAME(Firestore)
  *
  * @param listener A callback to be called every time all snapshot listeners are in sync with each
  * other.
- * @return A FIRListenerRegistration object that can be used to remove the listener.
+ * @return A `ListenerRegistration` object that can be used to remove the listener.
  */
 - (id<FIRListenerRegistration>)addSnapshotsInSyncListener:(void (^)(void))listener
     NS_SWIFT_NAME(addSnapshotsInSyncListener(_:));
@@ -238,13 +237,13 @@ NS_SWIFT_NAME(Firestore)
 #pragma mark - Terminating
 
 /**
- * Terminates this `FIRFirestore` instance.
+ * Terminates this `Firestore` instance.
  *
  * After calling `terminate` only the `clearPersistence` method may be used. Any other method will
  * throw an error.
  *
- * To restart after termination, simply create a new instance of FIRFirestore with `firestore` or
- * `firestoreForApp` methods.
+ * To restart after termination, simply create a new instance of `Firestore` with the `firestore`
+ * method.
  *
  * Termination does not cancel any pending writes and any tasks that are awaiting a response from
  * the server will not be resolved. The next time you start this instance, it will resume attempting
@@ -265,7 +264,7 @@ NS_SWIFT_NAME(Firestore)
  * Loads a Firestore bundle into the local cache.
  *
  * @param bundleData Data from the bundle to be loaded.
- * @return A `FIRLoadBundleTask` (`LoadBundleTask` in Swift) which allows registered observers
+ * @return A `LoadBundleTask` which allows registered observers
  * to receive progress updates and completion or error events.
  */
 - (FIRLoadBundleTask *)loadBundle:(NSData *)bundleData NS_SWIFT_NAME(loadBundle(_:));
@@ -276,9 +275,9 @@ NS_SWIFT_NAME(Firestore)
  * @param bundleData Data from the bundle to be loaded.
  * @param completion A block to execute when loading is in a final state. The `error` parameter
  * will be set if the block is invoked due to an error. If observers are registered to the
- * `FIRLoadBundleTask`, this block will be called after all observers are notified.
- * @return A `FIRLoadBundleTask` (`LoadBundleTask` in Swift) which allows registered observers
- * to receive progress updates and completion or error events.
+ * `LoadBundleTask`, this block will be called after all observers are notified.
+ * @return A `LoadBundleTask` which allows registered observers to receive progress updates and
+ * completion or error events.
  */
 - (FIRLoadBundleTask *)loadBundle:(NSData *)bundleData
                        completion:(nullable void (^)(FIRLoadBundleTaskProgress *_Nullable progress,
@@ -289,8 +288,8 @@ NS_SWIFT_NAME(Firestore)
  * Loads a Firestore bundle into the local cache.
  *
  * @param bundleStream An input stream from which the bundle can be read.
- * @return A `FIRLoadBundleTask` (`LoadBundleTask` in Swift) which allows registered observers
- * to receive progress updates and completion or error events.
+ * @return A `LoadBundleTask` which allows registered observers to receive progress updates and
+ * completion or error events.
  */
 - (FIRLoadBundleTask *)loadBundleStream:(NSInputStream *)bundleStream NS_SWIFT_NAME(loadBundle(_:));
 
@@ -300,9 +299,9 @@ NS_SWIFT_NAME(Firestore)
  * @param bundleStream An input stream from which the bundle can be read.
  * @param completion A block to execute when the loading is in a final state. The `error` parameter
  * of the block will be set if it is due to an error. If observers are registered to the returning
- * `FIRLoadBundleTask`, this block will be called after all observers are notified.
- * @return A `FIRLoadBundleTask` (`LoadBundleTask` in Swift), which allow registering observers
- * to receive progress updates, and completion or error events.
+ * `LoadBundleTask`, this block will be called after all observers are notified.
+ * @return A `LoadBundleTask` which allow registering observers to receive progress updates, and
+ * completion or error events.
  */
 - (FIRLoadBundleTask *)loadBundleStream:(NSInputStream *)bundleStream
                              completion:
@@ -311,7 +310,7 @@ NS_SWIFT_NAME(Firestore)
     NS_SWIFT_NAME(loadBundle(_:completion:));
 
 /**
- * Reads a `FIRQuery` (`Query` in Swift) from the local cache, identified by the given name.
+ * Reads a `Query` from the local cache, identified by the given name.
  *
  * Named queries are packaged into bundles on the server side (along with the resulting documents)
  * and loaded into local cache using `loadBundle`. Once in the local cache, you can use this method

+ 1 - 1
Firestore/Source/Public/FirebaseFirestore/FIRFirestoreErrors.h

@@ -24,7 +24,7 @@ FOUNDATION_EXPORT NSString *const FIRFirestoreErrorDomain NS_SWIFT_NAME(Firestor
 /** Error codes used by Cloud Firestore. */
 typedef NS_ERROR_ENUM(FIRFirestoreErrorDomain, FIRFirestoreErrorCode){
     /**
-     * The operation completed successfully. NSError objects will never have a code with this
+     * The operation completed successfully. `NSError` objects will never have a code with this
      * value.
      */
     FIRFirestoreErrorCodeOK = 0,

+ 4 - 4
Firestore/Source/Public/FirebaseFirestore/FIRFirestoreSettings.h

@@ -22,14 +22,14 @@ NS_ASSUME_NONNULL_BEGIN
 FOUNDATION_EXTERN const int64_t
     kFIRFirestoreCacheSizeUnlimited NS_SWIFT_NAME(FirestoreCacheSizeUnlimited);
 
-/** Settings used to configure a `FIRFirestore` instance. */
+/** Settings used to configure a `Firestore` instance. */
 NS_SWIFT_NAME(FirestoreSettings)
 @interface FIRFirestoreSettings : NSObject <NSCopying>
 
 /**
- * Creates and returns an empty `FIRFirestoreSettings` object.
+ * Creates and returns an empty `FirestoreSettings` object.
  *
- * @return The created `FIRFirestoreSettings` object.
+ * @return The created `FirestoreSettings` object.
  */
 - (instancetype)init NS_DESIGNATED_INITIALIZER;
 
@@ -53,7 +53,7 @@ NS_SWIFT_NAME(FirestoreSettings)
  * documents. The size is not a guarantee that the cache will stay below that size, only that if
  * the cache exceeds the given size, cleanup will be attempted. Cannot be set lower than 1MB.
  *
- * Set to kFIRFirestoreCacheSizeUnlimited to disable garbage collection entirely.
+ * Set to `FirestoreCacheSizeUnlimited` to disable garbage collection entirely.
  */
 @property(nonatomic, assign) int64_t cacheSizeBytes;
 

+ 1 - 1
Firestore/Source/Public/FirebaseFirestore/FIRListenerRegistration.h

@@ -23,7 +23,7 @@ NS_SWIFT_NAME(ListenerRegistration)
 @protocol FIRListenerRegistration <NSObject>
 
 /**
- * Removes the listener being tracked by this FIRListenerRegistration. After the initial call,
+ * Removes the listener being tracked by this `ListenerRegistration`. After the initial call,
  * subsequent calls have no effect.
  */
 - (void)remove;

+ 3 - 4
Firestore/Source/Public/FirebaseFirestore/FIRLoadBundleTask.h

@@ -21,9 +21,8 @@ NS_ASSUME_NONNULL_BEGIN
 /**
  * Represents the state of bundle loading tasks.
  *
- * Both `FIRLoadBundleTaskStateError` and `FIRLoadBundleTaskStateSuccess` are final states: task
- * will be in either aborted or completed state and there will be no more updates after they are
- * reported.
+ * Both `error` and `inProgress` are final states: task will be in either aborted or completed state
+ * and there will be no more updates after they are reported.
  */
 typedef NS_ENUM(NSInteger, FIRLoadBundleTaskState) {
 
@@ -51,7 +50,7 @@ NS_SWIFT_NAME(LoadBundleTaskProgress)
 /** The total number of bytes in the bundle. 0 if the bundle failed to parse. */
 @property(readonly, nonatomic) NSInteger totalBytes;
 
-/** The current state of `FIRLoadBundleTask` (`LoadBundleTask` in Swift). */
+/** The current state of `LoadBundleTask`. */
 @property(readonly, nonatomic) FIRLoadBundleTaskState state;
 
 @end

+ 82 - 83
Firestore/Source/Public/FirebaseFirestore/FIRQuery.h

@@ -34,15 +34,15 @@ typedef void (^FIRQuerySnapshotBlock)(FIRQuerySnapshot *_Nullable snapshot,
     NS_SWIFT_UNAVAILABLE("Use Swift's closure syntax instead.");
 
 /**
- * A `FIRQuery` refers to a Query which you can read or listen to. You can also construct
- * refined `FIRQuery` objects by adding filters and ordering.
+ * A `Query` refers to a query which you can read or listen to. You can also construct
+ * refined `Query` objects by adding filters and ordering.
  */
 NS_SWIFT_NAME(Query)
 @interface FIRQuery : NSObject
 /** :nodoc: */
 - (id)init __attribute__((unavailable("FIRQuery cannot be created directly.")));
 
-/** The `FIRFirestore` for the Firestore database (useful for performing transactions, etc.). */
+/** The `Firestore` for the Firestore database (useful for performing transactions, etc.). */
 @property(nonatomic, strong, readonly) FIRFirestore *firestore;
 
 #pragma mark - Retrieving Data
@@ -76,24 +76,24 @@ NS_SWIFT_NAME(Query)
     NS_SWIFT_NAME(getDocuments(source:completion:));
 
 /**
- * Attaches a listener for QuerySnapshot events.
+ * Attaches a listener for `QuerySnapshot` events.
  *
  * @param listener The listener to attach.
  *
- * @return A FIRListenerRegistration that can be used to remove this listener.
+ * @return A `ListenerRegistration` that can be used to remove this listener.
  */
 - (id<FIRListenerRegistration>)addSnapshotListener:
     (void (^)(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error))listener
     NS_SWIFT_NAME(addSnapshotListener(_:));
 
 /**
- * Attaches a listener for QuerySnapshot events.
+ * Attaches a listener for `QuerySnapshot` events.
  *
  * @param includeMetadataChanges Whether metadata-only changes (i.e. only
- *     `FIRDocumentSnapshot.metadata` changed) should trigger snapshot events.
+ *     `DocumentSnapshot.metadata` changed) should trigger snapshot events.
  * @param listener The listener to attach.
  *
- * @return A FIRListenerRegistration that can be used to remove this listener.
+ * @return A `ListenerRegistration` that can be used to remove this listener.
  */
 - (id<FIRListenerRegistration>)
     addSnapshotListenerWithIncludeMetadataChanges:(BOOL)includeMetadataChanges
@@ -103,179 +103,179 @@ NS_SWIFT_NAME(Query)
 
 #pragma mark - Filtering Data
 /**
- * Creates and returns a new `FIRQuery` with the additional filter that documents must
+ * Creates and returns a new `Query` with the additional filter that documents must
  * contain the specified field and the value must be equal to the specified value.
  *
  * @param field The name of the field to compare.
  * @param value The value the field must be equal to.
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryWhereField:(NSString *)field
                     isEqualTo:(id)value NS_SWIFT_NAME(whereField(_:isEqualTo:));
 
 /**
- * Creates and returns a new `FIRQuery` with the additional filter that documents must
+ * Creates and returns a new `Query` with the additional filter that documents must
  * contain the specified field and the value does not equal the specified value.
  *
  * @param path The path of the field to compare.
  * @param value The value the field must be equal to.
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path
                      isNotEqualTo:(id)value NS_SWIFT_NAME(whereField(_:isNotEqualTo:));
 
 /**
- * Creates and returns a new `FIRQuery` with the additional filter that documents must
+ * Creates and returns a new `Query` with the additional filter that documents must
  * contain the specified field and the value does not equal the specified value.
  *
  * @param field The name of the field to compare.
  * @param value The value the field must be equal to.
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryWhereField:(NSString *)field
                  isNotEqualTo:(id)value NS_SWIFT_NAME(whereField(_:isNotEqualTo:));
 
 /**
- * Creates and returns a new `FIRQuery` with the additional filter that documents must
+ * Creates and returns a new `Query` with the additional filter that documents must
  * contain the specified field and the value must be equal to the specified value.
  *
  * @param path The path of the field to compare.
  * @param value The value the field must be equal to.
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path
                         isEqualTo:(id)value NS_SWIFT_NAME(whereField(_:isEqualTo:));
 
 /**
- * Creates and returns a new `FIRQuery` with the additional filter that documents must
+ * Creates and returns a new `Query` with the additional filter that documents must
  * contain the specified field and the value must be less than the specified value.
  *
  * @param field The name of the field to compare.
  * @param value The value the field must be less than.
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryWhereField:(NSString *)field
                    isLessThan:(id)value NS_SWIFT_NAME(whereField(_:isLessThan:));
 
 /**
- * Creates and returns a new `FIRQuery` with the additional filter that documents must
+ * Creates and returns a new `Query` with the additional filter that documents must
  * contain the specified field and the value must be less than the specified value.
  *
  * @param path The path of the field to compare.
  * @param value The value the field must be less than.
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path
                        isLessThan:(id)value NS_SWIFT_NAME(whereField(_:isLessThan:));
 
 /**
- * Creates and returns a new `FIRQuery` with the additional filter that documents must
+ * Creates and returns a new `Query` with the additional filter that documents must
  * contain the specified field and the value must be less than or equal to the specified value.
  *
  * @param field The name of the field to compare
  * @param value The value the field must be less than or equal to.
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryWhereField:(NSString *)field
           isLessThanOrEqualTo:(id)value NS_SWIFT_NAME(whereField(_:isLessThanOrEqualTo:));
 
 /**
- * Creates and returns a new `FIRQuery` with the additional filter that documents must
+ * Creates and returns a new `Query` with the additional filter that documents must
  * contain the specified field and the value must be less than or equal to the specified value.
  *
  * @param path The path of the field to compare
  * @param value The value the field must be less than or equal to.
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path
               isLessThanOrEqualTo:(id)value NS_SWIFT_NAME(whereField(_:isLessThanOrEqualTo:));
 
 /**
- * Creates and returns a new `FIRQuery` with the additional filter that documents must
+ * Creates and returns a new `Query` with the additional filter that documents must
  * contain the specified field and the value must greater than the specified value.
  *
  * @param field The name of the field to compare
  * @param value The value the field must be greater than.
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryWhereField:(NSString *)field
                 isGreaterThan:(id)value NS_SWIFT_NAME(whereField(_:isGreaterThan:));
 
 /**
- * Creates and returns a new `FIRQuery` with the additional filter that documents must
+ * Creates and returns a new `Query` with the additional filter that documents must
  * contain the specified field and the value must greater than the specified value.
  *
  * @param path The path of the field to compare
  * @param value The value the field must be greater than.
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path
                     isGreaterThan:(id)value NS_SWIFT_NAME(whereField(_:isGreaterThan:));
 
 /**
- * Creates and returns a new `FIRQuery` with the additional filter that documents must
+ * Creates and returns a new `Query` with the additional filter that documents must
  * contain the specified field and the value must be greater than or equal to the specified value.
  *
  * @param field The name of the field to compare
  * @param value The value the field must be greater than.
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryWhereField:(NSString *)field
        isGreaterThanOrEqualTo:(id)value NS_SWIFT_NAME(whereField(_:isGreaterThanOrEqualTo:));
 
 /**
- * Creates and returns a new `FIRQuery` with the additional filter that documents must
+ * Creates and returns a new `Query` with the additional filter that documents must
  * contain the specified field and the value must be greater than or equal to the specified value.
  *
  * @param path The path of the field to compare
  * @param value The value the field must be greater than.
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path
            isGreaterThanOrEqualTo:(id)value NS_SWIFT_NAME(whereField(_:isGreaterThanOrEqualTo:));
 
 /**
- * Creates and returns a new `FIRQuery` with the additional filter that documents must contain
+ * Creates and returns a new `Query` with the additional filter that documents must contain
  * the specified field, it must be an array, and the array must contain the provided value.
  *
- * A query can have only one arrayContains filter.
+ * A query can have only one `arrayContains` filter.
  *
  * @param field The name of the field containing an array to search
  * @param value The value that must be contained in the array
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryWhereField:(NSString *)field
                 arrayContains:(id)value NS_SWIFT_NAME(whereField(_:arrayContains:));
 
 /**
- * Creates and returns a new `FIRQuery` with the additional filter that documents must contain
+ * Creates and returns a new `Query` with the additional filter that documents must contain
  * the specified field, it must be an array, and the array must contain the provided value.
  *
- * A query can have only one arrayContains filter.
+ * A query can have only one `arrayContains` filter.
  *
  * @param path The path of the field containing an array to search
  * @param value The value that must be contained in the array
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path
                     arrayContains:(id)value NS_SWIFT_NAME(whereField(_:arrayContains:));
 
 /**
- * Creates and returns a new `FIRQuery` with the additional filter that documents must contain
+ * Creates and returns a new `Query` with the additional filter that documents must contain
  * the specified field, the value must be an array, and that array must contain at least one value
  * from the provided array.
  *
@@ -285,13 +285,13 @@ NS_SWIFT_NAME(Query)
  * @param field The name of the field containing an array to search.
  * @param values The array that contains the values to match.
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryWhereField:(NSString *)field
              arrayContainsAny:(NSArray<id> *)values NS_SWIFT_NAME(whereField(_:arrayContainsAny:));
 
 /**
- * Creates and returns a new `FIRQuery` with the additional filter that documents must contain
+ * Creates and returns a new `Query` with the additional filter that documents must contain
  * the specified field, the value must be an array, and that array must contain at least one value
  * from the provided array.
  *
@@ -301,14 +301,14 @@ NS_SWIFT_NAME(Query)
  * @param path The path of the field containing an array to search.
  * @param values The array that contains the values to match.
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path
                  arrayContainsAny:(NSArray<id> *)values
     NS_SWIFT_NAME(whereField(_:arrayContainsAny:));
 
 /**
- * Creates and returns a new `FIRQuery` with the additional filter that documents must contain
+ * Creates and returns a new `Query` with the additional filter that documents must contain
  * the specified field and the value must equal one of the values from the provided array.
  *
  * A query can have only one `in` filter, and it cannot be combined with an `arrayContainsAny`
@@ -317,13 +317,13 @@ NS_SWIFT_NAME(Query)
  * @param field The name of the field to search.
  * @param values The array that contains the values to match.
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryWhereField:(NSString *)field
                            in:(NSArray<id> *)values NS_SWIFT_NAME(whereField(_:in:));
 
 /**
- * Creates and returns a new `FIRQuery` with the additional filter that documents must contain
+ * Creates and returns a new `Query` with the additional filter that documents must contain
  * the specified field and the value must equal one of the values from the provided array.
  *
  * A query can have only one `in` filter, and it cannot be combined with an `arrayContainsAny`
@@ -332,13 +332,13 @@ NS_SWIFT_NAME(Query)
  * @param path The path of the field to search.
  * @param values The array that contains the values to match.
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path
                                in:(NSArray<id> *)values NS_SWIFT_NAME(whereField(_:in:));
 
 /**
- * Creates and returns a new `FIRQuery` with the additional filter that documents must contain
+ * Creates and returns a new `Query` with the additional filter that documents must contain
  * the specified field and the value does not equal any of the values from the provided array.
  *
  * One special case is that `notIn` filters cannot match `nil` values. To query for documents
@@ -350,13 +350,13 @@ NS_SWIFT_NAME(Query)
  * @param field The name of the field to search.
  * @param values The array that contains the values to match.
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryWhereField:(NSString *)field
                         notIn:(NSArray<id> *)values NS_SWIFT_NAME(whereField(_:notIn:));
 
 /**
- * Creates and returns a new `FIRQuery` with the additional filter that documents must contain
+ * Creates and returns a new `Query` with the additional filter that documents must contain
  * the specified field and the value does not equal any of the values from the provided array.
  *
  * One special case is that `notIn` filters cannot match `nil` values. To query for documents
@@ -368,178 +368,177 @@ NS_SWIFT_NAME(Query)
  * @param path The path of the field to search.
  * @param values The array that contains the values to match.
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path
                             notIn:(NSArray<id> *)values NS_SWIFT_NAME(whereField(_:notIn:));
 
 /**
- * Creates and returns a new `FIRQuery` with the additional filter that documents must
+ * Creates and returns a new `Query` with the additional filter that documents must
  * satisfy the specified predicate.
  *
  * @param predicate The predicate the document must satisfy. Can be either comparison
  *     or compound of comparison. In particular, block-based predicate is not supported.
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryFilteredUsingPredicate:(NSPredicate *)predicate NS_SWIFT_NAME(filter(using:));
 
 #pragma mark - Sorting Data
 /**
- * Creates and returns a new `FIRQuery` that's additionally sorted by the specified field.
+ * Creates and returns a new `Query` that's additionally sorted by the specified field.
  *
  * @param field The field to sort by.
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryOrderedByField:(NSString *)field NS_SWIFT_NAME(order(by:));
 
 /**
- * Creates and returns a new `FIRQuery` that's additionally sorted by the specified field.
+ * Creates and returns a new `Query` that's additionally sorted by the specified field.
  *
  * @param path The field to sort by.
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryOrderedByFieldPath:(FIRFieldPath *)path NS_SWIFT_NAME(order(by:));
 
 /**
- * Creates and returns a new `FIRQuery` that's additionally sorted by the specified field,
+ * Creates and returns a new `Query` that's additionally sorted by the specified field,
  * optionally in descending order instead of ascending.
  *
  * @param field The field to sort by.
  * @param descending Whether to sort descending.
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryOrderedByField:(NSString *)field
                        descending:(BOOL)descending NS_SWIFT_NAME(order(by:descending:));
 
 /**
- * Creates and returns a new `FIRQuery` that's additionally sorted by the specified field,
+ * Creates and returns a new `Query` that's additionally sorted by the specified field,
  * optionally in descending order instead of ascending.
  *
  * @param path The field to sort by.
  * @param descending Whether to sort descending.
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryOrderedByFieldPath:(FIRFieldPath *)path
                            descending:(BOOL)descending NS_SWIFT_NAME(order(by:descending:));
 
 #pragma mark - Limiting Data
 /**
- * Creates and returns a new `FIRQuery` that only returns the first matching documents up to
+ * Creates and returns a new `Query` that only returns the first matching documents up to
  * the specified number.
  *
  * @param limit The maximum number of items to return.
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryLimitedTo:(NSInteger)limit NS_SWIFT_NAME(limit(to:));
 
 /**
- * Creates and returns a new `FIRQuery` that only returns the last matching documents up to
+ * Creates and returns a new `Query` that only returns the last matching documents up to
  * the specified number.
  *
- * A query with a `limit(ToLast:)` clause must have at least one `orderBy` clause.
+ * A query with a `limit(toLast:)` clause must have at least one `orderBy` clause.
  *
  * @param limit The maximum number of items to return.
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryLimitedToLast:(NSInteger)limit NS_SWIFT_NAME(limit(toLast:));
 
 #pragma mark - Choosing Endpoints
 /**
- * Creates and returns a new `FIRQuery` that starts at the provided document (inclusive). The
+ * Creates and returns a new `Query` that starts at the provided document (inclusive). The
  * starting position is relative to the order of the query. The document must contain all of the
  * fields provided in the orderBy of this query.
  *
  * @param document The snapshot of the document to start at.
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryStartingAtDocument:(FIRDocumentSnapshot *)document
     NS_SWIFT_NAME(start(atDocument:));
 
 /**
- * Creates and returns a new `FIRQuery` that starts at the provided fields relative to the order of
+ * Creates and returns a new `Query` that starts at the provided fields relative to the order of
  * the query. The order of the field values must match the order of the order by clauses of the
  * query.
  *
  * @param fieldValues The field values to start this query at, in order of the query's order by.
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryStartingAtValues:(NSArray *)fieldValues NS_SWIFT_NAME(start(at:));
 
 /**
- * Creates and returns a new `FIRQuery` that starts after the provided document (exclusive). The
+ * Creates and returns a new `Query` that starts after the provided document (exclusive). The
  * starting position is relative to the order of the query. The document must contain all of the
  * fields provided in the orderBy of this query.
  *
  * @param document The snapshot of the document to start after.
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryStartingAfterDocument:(FIRDocumentSnapshot *)document
     NS_SWIFT_NAME(start(afterDocument:));
 
 /**
- * Creates and returns a new `FIRQuery` that starts after the provided fields relative to the order
+ * Creates and returns a new `Query` that starts after the provided fields relative to the order
  * of the query. The order of the field values must match the order of the order by clauses of the
  * query.
  *
- * @param fieldValues The field values to start this query after, in order of the query's order
- *     by.
+ * @param fieldValues The field values to start this query after, in order of the query's orderBy.
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryStartingAfterValues:(NSArray *)fieldValues NS_SWIFT_NAME(start(after:));
 
 /**
- * Creates and returns a new `FIRQuery` that ends before the provided document (exclusive). The end
+ * Creates and returns a new `Query` that ends before the provided document (exclusive). The end
  * position is relative to the order of the query. The document must contain all of the fields
  * provided in the orderBy of this query.
  *
  * @param document The snapshot of the document to end before.
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryEndingBeforeDocument:(FIRDocumentSnapshot *)document
     NS_SWIFT_NAME(end(beforeDocument:));
 
 /**
- * Creates and returns a new `FIRQuery` that ends before the provided fields relative to the order
+ * Creates and returns a new `Query` that ends before the provided fields relative to the order
  * of the query. The order of the field values must match the order of the order by clauses of the
  * query.
  *
  * @param fieldValues The field values to end this query before, in order of the query's order by.
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryEndingBeforeValues:(NSArray *)fieldValues NS_SWIFT_NAME(end(before:));
 
 /**
- * Creates and returns a new `FIRQuery` that ends at the provided document (exclusive). The end
+ * Creates and returns a new `Query` that ends at the provided document (exclusive). The end
  * position is relative to the order of the query. The document must contain all of the fields
  * provided in the orderBy of this query.
  *
  * @param document The snapshot of the document to end at.
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryEndingAtDocument:(FIRDocumentSnapshot *)document NS_SWIFT_NAME(end(atDocument:));
 
 /**
- * Creates and returns a new `FIRQuery` that ends at the provided fields relative to the order of
+ * Creates and returns a new `Query` that ends at the provided fields relative to the order of
  * the query. The order of the field values must match the order of the order by clauses of the
  * query.
  *
  * @param fieldValues The field values to end this query at, in order of the query's order by.
  *
- * @return The created `FIRQuery`.
+ * @return The created `Query`.
  */
 - (FIRQuery *)queryEndingAtValues:(NSArray *)fieldValues NS_SWIFT_NAME(end(at:));
 

+ 6 - 6
Firestore/Source/Public/FirebaseFirestore/FIRQuerySnapshot.h

@@ -24,7 +24,7 @@ NS_ASSUME_NONNULL_BEGIN
 @class FIRSnapshotMetadata;
 
 /**
- * A `FIRQuerySnapshot` contains zero or more `FIRDocumentSnapshot` objects. It can be enumerated
+ * A `QuerySnapshot` contains zero or more `DocumentSnapshot` objects. It can be enumerated
  * using "for ... in documentSet.documents" and its size can be inspected with `isEmpty` and
  * `count`.
  */
@@ -36,20 +36,20 @@ NS_SWIFT_NAME(QuerySnapshot)
 
 /**
  * The query on which you called `getDocuments` or listened to in order to get this
- * `FIRQuerySnapshot`.
+ * `QuerySnapshot`.
  */
 @property(nonatomic, strong, readonly) FIRQuery *query;
 
 /** Metadata about this snapshot, concerning its source and if it has local modifications. */
 @property(nonatomic, strong, readonly) FIRSnapshotMetadata *metadata;
 
-/** Indicates whether this `FIRQuerySnapshot` is empty (contains no documents). */
+/** Indicates whether this `QuerySnapshot` is empty (contains no documents). */
 @property(nonatomic, readonly, getter=isEmpty) BOOL empty;
 
-/** The count of documents in this `FIRQuerySnapshot`. */
+/** The count of documents in this `QuerySnapshot`. */
 @property(nonatomic, readonly) NSInteger count;
 
-/** An Array of the `FIRDocumentSnapshots` that make up this document set. */
+/** An Array of the `DocumentSnapshots` that make up this document set. */
 @property(nonatomic, strong, readonly) NSArray<FIRQueryDocumentSnapshot *> *documents;
 
 /**
@@ -63,7 +63,7 @@ NS_SWIFT_NAME(QuerySnapshot)
  * snapshot, all documents will be in the list as Added changes.
  *
  * @param includeMetadataChanges Whether metadata-only changes (i.e. only
- *     `FIRDocumentSnapshot.metadata` changed) should be included.
+ *     `DocumentSnapshot.metadata` changed) should be included.
  */
 - (NSArray<FIRDocumentChange *> *)documentChangesWithIncludeMetadataChanges:
     (BOOL)includeMetadataChanges NS_SWIFT_NAME(documentChanges(includeMetadataChanges:));

+ 7 - 7
Firestore/Source/Public/FirebaseFirestore/FIRSnapshotMetadata.h

@@ -26,18 +26,18 @@ NS_SWIFT_NAME(SnapshotMetadata)
 - (instancetype)init NS_UNAVAILABLE;
 
 /**
- * Returns YES if the snapshot contains the result of local writes (e.g. set() or update() calls)
+ * Returns `true` if the snapshot contains the result of local writes (e.g. set() or update() calls)
  * that have not yet been committed to the backend. If your listener has opted into metadata updates
- * (via `includeMetadataChanges:YES`) you will receive another snapshot with `hasPendingWrites`
- * equal to NO once the writes have been committed to the backend.
+ * (via `includeMetadataChanges:true`) you will receive another snapshot with `hasPendingWrites`
+ * equal to `false` once the writes have been committed to the backend.
  */
 @property(nonatomic, assign, readonly, getter=hasPendingWrites) BOOL pendingWrites;
 
 /**
- * Returns YES if the snapshot was created from cached data rather than guaranteed up-to-date server
- * data. If your listener has opted into metadata updates (via `includeMetadataChanges:YES`) you
- * will receive another snapshot with `isFromCache` equal to NO once the client has received
- * up-to-date data from the backend.
+ * Returns `true` if the snapshot was created from cached data rather than guaranteed up-to-date
+ * server data. If your listener has opted into metadata updates (via `includeMetadataChanges:true`)
+ * you will receive another snapshot with `isFromCache` equal to `false` once the client has
+ * received up-to-date data from the backend.
  */
 @property(nonatomic, assign, readonly, getter=isFromCache) BOOL fromCache;
 

+ 4 - 4
Firestore/Source/Public/FirebaseFirestore/FIRTimestamp.h

@@ -59,15 +59,15 @@ NS_SWIFT_NAME(Timestamp)
 /** Creates a new timestamp with the current date / time. */
 + (instancetype)timestamp;
 
-/** Returns a new NSDate corresponding to this timestamp. This may lose precision. */
+/** Returns a new `Date` corresponding to this timestamp. This may lose precision. */
 - (NSDate *)dateValue;
 
 /**
  * Returns the result of comparing the receiver with another timestamp.
  * @param other the other timestamp to compare.
- * @return NSOrderedAscending if `other` is chronologically following self,
- *     NSOrderedDescending if `other` is chronologically preceding self,
- *     NSOrderedSame otherwise.
+ * @return `orderedAscending` if `other` is chronologically following self,
+ *     `orderedDescending` if `other` is chronologically preceding self,
+ *     `orderedSame` otherwise.
  */
 - (NSComparisonResult)compare:(FIRTimestamp *)other;
 

+ 15 - 15
Firestore/Source/Public/FirebaseFirestore/FIRTransaction.h

@@ -22,9 +22,9 @@ NS_ASSUME_NONNULL_BEGIN
 @class FIRDocumentSnapshot;
 
 /**
- * `FIRTransaction` provides methods to read and write data within a transaction.
+ * `Transaction` provides methods to read and write data within a transaction.
  *
- * @see FIRFirestore#transaction:completion:
+ * @see Firestore#transaction:completion:
  */
 NS_SWIFT_NAME(Transaction)
 @interface FIRTransaction : NSObject
@@ -37,9 +37,9 @@ NS_SWIFT_NAME(Transaction)
  * this method creates it and then sets the data. If the document exists, this method overwrites
  * the document data with the new values.
  *
- * @param data An `NSDictionary` that contains the fields and data to write to the document.
+ * @param data A `Dictionary` that contains the fields and data to write to the document.
  * @param document A reference to the document whose data should be overwritten.
- * @return This `FIRTransaction` instance. Used for chaining method calls.
+ * @return This `Transaction` instance. Used for chaining method calls.
  */
 // clang-format off
 - (FIRTransaction *)setData:(NSDictionary<NSString *, id> *)data
@@ -49,15 +49,15 @@ NS_SWIFT_NAME(Transaction)
 
 /**
  * Writes to the document referred to by `document`. If the document doesn't yet exist,
- * this method creates it and then sets the data. If you pass `merge:YES`, the provided data will be
- * merged into any existing document.
+ * this method creates it and then sets the data. If you pass `merge:true`, the provided data will
+ * be merged into any existing document.
  *
- * @param data An `NSDictionary` that contains the fields and data to write to the document.
+ * @param data A `Dictionary` that contains the fields and data to write to the document.
  * @param document A reference to the document whose data should be overwritten.
  * @param merge Whether to merge the provided data into any existing document. If enabled,
  * all omitted fields remain untouched. If your input sets any field to an empty dictionary, any
  * nested field is overwritten.
- * @return This `FIRTransaction` instance. Used for chaining method calls.
+ * @return This `Transaction` instance. Used for chaining method calls.
  */
 // clang-format off
 - (FIRTransaction *)setData:(NSDictionary<NSString *, id> *)data
@@ -75,14 +75,14 @@ NS_SWIFT_NAME(Transaction)
  * It is an error to include a field in `mergeFields` that does not have a corresponding
  * value in the `data` dictionary.
  *
- * @param data An `NSDictionary` containing the fields that make up the document
+ * @param data A `Dictionary` containing the fields that make up the document
  * to be written.
  * @param document A reference to the document whose data should be overwritten.
- * @param mergeFields An `NSArray` that contains a list of `NSString` or `FIRFieldPath` elements
+ * @param mergeFields An `Array` that contains a list of `String` or `FieldPath` elements
  * specifying which fields to merge. Fields can contain dots to reference nested fields within
  * the document. If your input sets any field to an empty dictionary, any nested field is
  * overwritten.
- * @return This `FIRTransaction` instance. Used for chaining method calls.
+ * @return This `Transaction` instance. Used for chaining method calls.
  */
 // clang-format off
 - (FIRTransaction *)setData:(NSDictionary<NSString *, id> *)data
@@ -95,10 +95,10 @@ NS_SWIFT_NAME(Transaction)
  * Updates fields in the document referred to by `document`.
  * If the document does not exist, the transaction will fail.
  *
- * @param fields An `NSDictionary` containing the fields (expressed as an `NSString` or
- * `FIRFieldPath`) and values with which to update the document.
+ * @param fields A `Dictionary` containing the fields (expressed as an `String` or
+ * `FieldPath`) and values with which to update the document.
  * @param document A reference to the document whose data should be updated.
- * @return This `FIRTransaction` instance. Used for chaining method calls.
+ * @return This `Transaction` instance. Used for chaining method calls.
  */
 // clang-format off
 - (FIRTransaction *)updateData:(NSDictionary<id, id> *)fields
@@ -110,7 +110,7 @@ NS_SWIFT_NAME(Transaction)
  * Deletes the document referred to by `document`.
  *
  * @param document A reference to the document that should be deleted.
- * @return This `FIRTransaction` instance. Used for chaining method calls.
+ * @return This `Transaction` instance. Used for chaining method calls.
  */
 - (FIRTransaction *)deleteDocument:(FIRDocumentReference *)document
     NS_SWIFT_NAME(deleteDocument(_:));

+ 15 - 15
Firestore/Source/Public/FirebaseFirestore/FIRWriteBatch.h

@@ -23,9 +23,9 @@ NS_ASSUME_NONNULL_BEGIN
 /**
  * A write batch is used to perform multiple writes as a single atomic unit.
  *
- * A WriteBatch object can be acquired by calling [FIRFirestore batch]. It provides methods for
+ * A WriteBatch object can be acquired by calling `Firestore.batch()`. It provides methods for
  * adding writes to the write batch. None of the writes will be committed (or visible locally)
- * until [FIRWriteBatch commit] is called.
+ * until `WriteBatch.commit()` is called.
  *
  * Unlike transactions, write batches are persisted offline and therefore are preferable when you
  * don't need to condition your writes on read data.
@@ -41,9 +41,9 @@ NS_SWIFT_NAME(WriteBatch)
  * this method creates it and then sets the data. If the document exists, this method overwrites
  * the document data with the new values.
  *
- * @param data An `NSDictionary` that contains the fields and data to write to the document.
+ * @param data A `Dictionary` that contains the fields and data to write to the document.
  * @param document A reference to the document whose data should be overwritten.
- * @return This `FIRWriteBatch` instance. Used for chaining method calls.
+ * @return This `WriteBatch` instance. Used for chaining method calls.
  */
 // clang-format off
 - (FIRWriteBatch *)setData:(NSDictionary<NSString *, id> *)data
@@ -52,15 +52,15 @@ NS_SWIFT_NAME(WriteBatch)
 
 /**
  * Writes to the document referred to by `document`. If the document doesn't yet exist,
- * this method creates it and then sets the data. If you pass `merge:YES`, the provided data will be
- * merged into  any existing document.
+ * this method creates it and then sets the data. If you pass `merge:true`, the provided data will
+ * be merged into  any existing document.
  *
- * @param data An `NSDictionary` that contains the fields and data to write to the document.
+ * @param data A `Dictionary` that contains the fields and data to write to the document.
  * @param document A reference to the document whose data should be overwritten.
  * @param merge Whether to merge the provided data into any existing document. If enabled,
  * all omitted fields remain untouched. If your input sets any field to an empty dictionary, any
  * nested field is overwritten.
- * @return This `FIRWriteBatch` instance. Used for chaining method calls.
+ * @return This `WriteBatch` instance. Used for chaining method calls.
  */
 // clang-format off
 - (FIRWriteBatch *)setData:(NSDictionary<NSString *, id> *)data
@@ -78,13 +78,13 @@ NS_SWIFT_NAME(WriteBatch)
  * It is an error to include a field in `mergeFields` that does not have a corresponding
  * value in the `data` dictionary.
  *
- * @param data An `NSDictionary` that contains the fields and data to write to the document.
+ * @param data A `Dictionary` that contains the fields and data to write to the document.
  * @param document A reference to the document whose data should be overwritten.
- * @param mergeFields An `NSArray` that contains a list of `NSString` or `FIRFieldPath` elements
+ * @param mergeFields An `Array` that contains a list of `String` or `FieldPath` elements
  * specifying which fields to merge. Fields can contain dots to reference nested fields within
  * the document. If your input sets any field to an empty dictionary, any nested field is
  * overwritten.
- * @return This `FIRWriteBatch` instance. Used for chaining method calls.
+ * @return This `WriteBatch` instance. Used for chaining method calls.
  */
 // clang-format off
 - (FIRWriteBatch *)setData:(NSDictionary<NSString *, id> *)data
@@ -97,10 +97,10 @@ NS_SWIFT_NAME(WriteBatch)
  * Updates fields in the document referred to by `document`.
  * If document does not exist, the write batch will fail.
  *
- * @param fields An `NSDictionary` containing the fields (expressed as an `NSString` or
- *     `FIRFieldPath`) and values with which to update the document.
+ * @param fields A `Dictionary` containing the fields (expressed as an `String` or
+ *     `FieldPath`) and values with which to update the document.
  * @param document A reference to the document whose data should be updated.
- * @return This `FIRWriteBatch` instance. Used for chaining method calls.
+ * @return This `WriteBatch` instance. Used for chaining method calls.
  */
 // clang-format off
 - (FIRWriteBatch *)updateData:(NSDictionary<id, id> *)fields
@@ -112,7 +112,7 @@ NS_SWIFT_NAME(WriteBatch)
  * Deletes the document referred to by `document`.
  *
  * @param document A reference to the document that should be deleted.
- * @return This `FIRWriteBatch` instance. Used for chaining method calls.
+ * @return This `WriteBatch` instance. Used for chaining method calls.
  */
 - (FIRWriteBatch *)deleteDocument:(FIRDocumentReference *)document
     NS_SWIFT_NAME(deleteDocument(_:));

+ 31 - 6
Firestore/core/src/bundle/bundle_serializer.cc

@@ -7,7 +7,7 @@
  *
  *      http://www.apache.org/licenses/LICENSE-2.0
  *
- * Unless Requiredd by applicable law or agreed to in writing, software
+ * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
@@ -393,6 +393,16 @@ const nlohmann::json& JsonReader::RequiredObject(const char* child_name,
   return json_object.at(child_name);
 }
 
+const nlohmann::json& JsonReader::OptionalObject(
+    const char* child_name,
+    const json& json_object,
+    const nlohmann::json& default_value) {
+  if (json_object.contains(child_name)) {
+    return json_object.at(child_name);
+  }
+  return default_value;
+}
+
 double JsonReader::RequiredDouble(const char* name, const json& json_object) {
   if (json_object.contains(name)) {
     double result = DecodeDouble(json_object.at(name));
@@ -614,11 +624,22 @@ FilterList BundleSerializer::DecodeCompositeFilter(JsonReader& reader,
     return {};
   }
 
-  auto filters = reader.RequiredArray("filters", filter);
+  const std::vector<json> default_filters;
+  const auto& filters =
+      reader.OptionalArray("filters", filter, default_filters);
+
+  const json default_objects;
   FilterList result;
   for (const auto& f : filters) {
-    result = result.push_back(
-        DecodeFieldFilter(reader, reader.RequiredObject("fieldFilter", f)));
+    const json& field_filter =
+        reader.OptionalObject("fieldFilter", f, default_objects);
+    if (!field_filter.empty()) {
+      result = result.push_back(DecodeFieldFilter(reader, field_filter));
+    } else {
+      result = result.push_back(DecodeUnaryFilter(
+          reader, reader.OptionalObject("unaryFilter", f, default_objects)));
+    }
+
     if (!reader.ok()) {
       return {};
     }
@@ -636,8 +657,10 @@ Bound BundleSerializer::DecodeBound(JsonReader& reader,
     return default_bound;
   }
 
+  std::vector<json> default_values;
   const json& bound_json = reader.RequiredObject(bound_name, query);
-  std::vector<json> values = reader.RequiredArray("values", bound_json);
+  std::vector<json> values =
+      reader.OptionalArray("values", bound_json, default_values);
   bool before = reader.OptionalBool("before", bound_json);
 
   auto positions = MakeSharedMessage<google_firestore_v1_ArrayValue>({});
@@ -737,7 +760,9 @@ Message<google_firestore_v1_MapValue> BundleSerializer::DecodeMapValue(
 
 Message<google_firestore_v1_ArrayValue> BundleSerializer::DecodeArrayValue(
     JsonReader& reader, const json& array_json) const {
-  const auto& values = reader.RequiredArray("values", array_json);
+  std::vector<json> default_values;
+  const auto& values =
+      reader.OptionalArray("values", array_json, default_values);
 
   Message<google_firestore_v1_ArrayValue> array_value;
   SetRepeatedField(

+ 4 - 0
Firestore/core/src/bundle/bundle_serializer.h

@@ -64,8 +64,12 @@ class JsonReader : public util::ReadContext {
       const char* name,
       const nlohmann::json& json_object,
       const std::vector<nlohmann::json>& default_value);
+
   const nlohmann::json& RequiredObject(const char* child_name,
                                        const nlohmann::json& json_object);
+  const nlohmann::json& OptionalObject(const char* child_name,
+                                       const nlohmann::json& json_object,
+                                       const nlohmann::json& default_value);
 
   double RequiredDouble(const char* name, const nlohmann::json& json_object);
   double OptionalDouble(const char* name,

+ 2 - 1
Firestore/core/src/core/field_filter.cc

@@ -30,6 +30,7 @@
 #include "Firestore/core/src/model/document.h"
 #include "Firestore/core/src/model/value_util.h"
 #include "Firestore/core/src/util/exception.h"
+#include "Firestore/core/src/util/hard_assert.h"
 #include "Firestore/core/src/util/hashing.h"
 #include "absl/algorithm/container.h"
 #include "absl/strings/str_cat.h"
@@ -115,7 +116,7 @@ FieldFilter FieldFilter::Create(
 }
 
 FieldFilter::FieldFilter(const Filter& other) : Filter(other) {
-  HARD_ASSERT(IsAFieldFilter());
+  HARD_ASSERT(other.IsAFieldFilter());
 }
 
 FieldFilter::FieldFilter(std::shared_ptr<const Filter::Rep> rep)

+ 1 - 1
Firestore/core/src/core/key_field_not_in_filter.cc

@@ -45,7 +45,7 @@ class KeyFieldNotInFilter::Rep : public FieldFilter::Rep {
   }
 
   Type type() const override {
-    return Type::kKeyFieldInFilter;
+    return Type::kKeyFieldNotInFilter;
   }
 
   bool Matches(const model::Document& doc) const override;

+ 1 - 1
Firestore/core/src/core/order_by.cc

@@ -67,7 +67,7 @@ void AssertBothOptionalsHaveValues(
 ComparisonResult OrderBy::Compare(const Document& lhs,
                                   const Document& rhs) const {
   ComparisonResult result;
-  if (field_ == FieldPath::KeyFieldPath()) {
+  if (field_.IsKeyFieldPath()) {
     result = lhs->key().CompareTo(rhs->key());
   } else {
     absl::optional<google_firestore_v1_Value> value1 = lhs->field(field_);

+ 1 - 2
Firestore/core/src/index/index_entry.h

@@ -62,8 +62,7 @@ class IndexEntry : public util::Comparable<IndexEntry> {
   size_t Hash() const;
 
   std::string ToString() const;
-  friend std::ostream& operator<<(std::ostream& out,
-                                  const IndexEntry& database_id);
+  friend std::ostream& operator<<(std::ostream& out, const IndexEntry& entry);
 
  private:
   int32_t index_id_;

+ 1 - 1
Firestore/core/src/local/leveldb_index_manager.h

@@ -65,7 +65,7 @@ class LevelDbIndexManager : public IndexManager {
   absl::optional<model::FieldIndex> GetFieldIndex(core::Target target) override;
 
   absl::optional<std::vector<model::DocumentKey>> GetDocumentsMatchingTarget(
-      model::FieldIndex fieldIndex, core::Target target) override;
+      model::FieldIndex field_index, core::Target target) override;
 
   absl::optional<std::string> GetNextCollectionGroupToUpdate() override;
 

+ 8 - 0
Firestore/core/src/local/local_documents_view.h

@@ -19,6 +19,7 @@
 
 #include <vector>
 
+#include "Firestore/core/src/local/document_overlay_cache.h"
 #include "Firestore/core/src/local/index_manager.h"
 #include "Firestore/core/src/local/mutation_queue.h"
 #include "Firestore/core/src/local/remote_document_cache.h"
@@ -47,9 +48,11 @@ class LocalDocumentsView {
  public:
   LocalDocumentsView(RemoteDocumentCache* remote_document_cache,
                      MutationQueue* mutation_queue,
+                     DocumentOverlayCache* document_overlay_cache,
                      IndexManager* index_manager)
       : remote_document_cache_{remote_document_cache},
         mutation_queue_{mutation_queue},
+        document_overlay_cache_{document_overlay_cache},
         index_manager_{index_manager} {
   }
 
@@ -136,6 +139,10 @@ class LocalDocumentsView {
     return mutation_queue_;
   }
 
+  DocumentOverlayCache* document_overlay_cache() {
+    return document_overlay_cache_;
+  }
+
   IndexManager* index_manager() {
     return index_manager_;
   }
@@ -143,6 +150,7 @@ class LocalDocumentsView {
  private:
   RemoteDocumentCache* remote_document_cache_;
   MutationQueue* mutation_queue_;
+  DocumentOverlayCache* document_overlay_cache_;
   IndexManager* index_manager_;
 };
 

+ 5 - 2
Firestore/core/src/local/local_store.cc

@@ -88,8 +88,10 @@ LocalStore::LocalStore(Persistence* persistence,
       query_engine_(query_engine) {
   index_manager_ = persistence->GetIndexManager(initial_user);
   mutation_queue_ = persistence->GetMutationQueue(initial_user, index_manager_);
+  document_overlay_cache_ = persistence->GetDocumentOverlayCache(initial_user);
   local_documents_ = absl::make_unique<LocalDocumentsView>(
-      remote_document_cache_, mutation_queue_, index_manager_);
+      remote_document_cache_, mutation_queue_, document_overlay_cache_,
+      index_manager_);
   remote_document_cache_->SetIndexManager(index_manager_);
 
   persistence->reference_delegate()->AddInMemoryPins(&local_view_references_);
@@ -130,7 +132,8 @@ DocumentMap LocalStore::HandleUserChange(const User& user) {
 
     // Recreate our LocalDocumentsView using the new MutationQueue.
     local_documents_ = absl::make_unique<LocalDocumentsView>(
-        remote_document_cache_, mutation_queue_, index_manager_);
+        remote_document_cache_, mutation_queue_, document_overlay_cache_,
+        index_manager_);
     query_engine_->SetLocalDocumentsView(local_documents_.get());
 
     // Union the old/new changed keys.

+ 7 - 0
Firestore/core/src/local/local_store.h

@@ -26,6 +26,7 @@
 #include "Firestore/core/src/bundle/bundle_metadata.h"
 #include "Firestore/core/src/bundle/named_query.h"
 #include "Firestore/core/src/core/target_id_generator.h"
+#include "Firestore/core/src/local/document_overlay_cache.h"
 #include "Firestore/core/src/local/reference_set.h"
 #include "Firestore/core/src/local/target_data.h"
 #include "Firestore/core/src/model/document.h"
@@ -339,6 +340,12 @@ class LocalStore : public bundle::BundleCallback {
    */
   MutationQueue* mutation_queue_ = nullptr;
 
+  /**
+   * The overlays that can be used to short circuit applying all mutations from
+   * mutation queue.
+   */
+  DocumentOverlayCache* document_overlay_cache_ = nullptr;
+
   /** The set of all cached remote documents. */
   RemoteDocumentCache* remote_document_cache_ = nullptr;
 

+ 154 - 0
Firestore/core/src/model/target_index_matcher.cc

@@ -0,0 +1,154 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "Firestore/core/src/model/target_index_matcher.h"
+
+#include "Firestore/core/src/util/hard_assert.h"
+
+namespace firebase {
+namespace firestore {
+namespace model {
+
+using core::FieldFilter;
+using core::Filter;
+using core::OrderBy;
+using core::Target;
+
+TargetIndexMatcher::TargetIndexMatcher(const core::Target& target) {
+  collection_id_ = target.collection_group() != nullptr
+                       ? *target.collection_group()
+                       : target.path().last_segment();
+  order_bys_ = target.order_bys();
+  inequality_filter_ = absl::nullopt;
+
+  for (const Filter& filter : target.filters()) {
+    FieldFilter field_filter(filter);
+    if (field_filter.IsInequality()) {
+      HARD_ASSERT(!inequality_filter_.has_value() ||
+                      inequality_filter_->field() == field_filter.field(),
+                  "Only a single inequality is supported");
+      inequality_filter_ = field_filter;
+    } else {
+      equality_filters_.push_back(field_filter);
+    }
+  }
+}
+
+bool TargetIndexMatcher::ServedByIndex(const model::FieldIndex& index) {
+  HARD_ASSERT(index.collection_group() == collection_id_,
+              "Collection IDs do not match");
+
+  // If there is an array element, find a matching filter.
+  const auto& array_segment = index.GetArraySegment();
+  if (array_segment.has_value() &&
+      !HasMatchingEqualityFilter(array_segment.value())) {
+    return false;
+  }
+
+  std::vector<Segment> segments = index.GetDirectionalSegments();
+  size_t segment_index = 0;
+  // Process all equalities first. Equalities can appear out of order.
+  for (; segment_index < segments.size(); ++segment_index) {
+    // We attempt to greedily match all segments to equality filters. If a
+    // filter matches an index segment, we can mark the segment as used. Since
+    // it is not possible to use the same field path in both an equality and
+    // inequality/oderBy clause, we do not have to consider the possibility that
+    // a matching equality segment should instead be used to map to an
+    // inequality filter or orderBy clause.
+    if (!HasMatchingEqualityFilter(segments[segment_index])) {
+      // If we cannot find a matching filter, we need to verify whether the
+      // remaining segments map to the target's inequality and its orderBy
+      // clauses.
+      break;
+    }
+  }
+
+  // If we already have processed all segments, all segments are used to serve
+  // the equality filters and we do not need to map any segments to the target's
+  // inequality and orderBy clauses.
+  if (segment_index == segments.size()) {
+    return true;
+  }
+
+  // `order_bys_` has at least one element.
+  auto order_by_iter = order_bys_.begin();
+
+  // If there is an inequality filter, the next segment must match both the
+  // filter and the first OrderBy clause.
+  if (inequality_filter_.has_value()) {
+    if (!MatchesFilter(inequality_filter_, segments[segment_index]) ||
+        !MatchesOrderBy(*order_by_iter, segments[segment_index])) {
+      return false;
+    }
+    ++order_by_iter;
+    ++segment_index;
+  }
+
+  // All remaining segments need to represent the prefix of the target's
+  // OrderBy.
+  for (; segment_index < segments.size(); ++segment_index) {
+    if (order_by_iter == order_bys_.end() ||
+        !MatchesOrderBy(*order_by_iter, segments[segment_index])) {
+      return false;
+    }
+    ++order_by_iter;
+  }
+
+  return true;
+}
+
+bool TargetIndexMatcher::HasMatchingEqualityFilter(const Segment& segment) {
+  for (const auto& filter : equality_filters_) {
+    if (MatchesFilter(filter, segment)) {
+      return true;
+    }
+  }
+  return false;
+}
+
+bool TargetIndexMatcher::MatchesFilter(
+    const absl::optional<core::FieldFilter>& filter, const Segment& segment) {
+  if (!filter.has_value()) {
+    return false;
+  }
+  return MatchesFilter(filter.value(), segment);
+}
+
+bool TargetIndexMatcher::MatchesFilter(const FieldFilter& filter,
+                                       const Segment& segment) {
+  if (filter.field() != segment.field_path()) {
+    return false;
+  }
+
+  bool is_array_op = filter.op() == FieldFilter::Operator::ArrayContains ||
+                     filter.op() == FieldFilter::Operator::ArrayContainsAny;
+  return (segment.kind() == Segment::kContains) == is_array_op;
+}
+
+bool TargetIndexMatcher::MatchesOrderBy(const OrderBy& order_by,
+                                        const Segment& segment) {
+  if (order_by.field() != segment.field_path()) {
+    return false;
+  }
+  return (segment.kind() == Segment::kAscending &&
+          order_by.direction() == core::Direction::Ascending) ||
+         (segment.kind() == Segment::kDescending &&
+          order_by.direction() == core::Direction::Descending);
+}
+
+}  // namespace model
+}  // namespace firestore
+}  // namespace firebase

+ 103 - 0
Firestore/core/src/model/target_index_matcher.h

@@ -0,0 +1,103 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef FIRESTORE_CORE_SRC_MODEL_TARGET_INDEX_MATCHER_H_
+#define FIRESTORE_CORE_SRC_MODEL_TARGET_INDEX_MATCHER_H_
+
+#include <string>
+#include <vector>
+
+#include "Firestore/core/src/core/field_filter.h"
+#include "Firestore/core/src/core/order_by.h"
+#include "Firestore/core/src/core/target.h"
+#include "Firestore/core/src/model/field_index.h"
+#include "absl/types/optional.h"
+
+namespace firebase {
+namespace firestore {
+namespace model {
+
+/**
+ * A light query planner for Firestore.
+ *
+ * This class matches a `FieldIndex` against a Firestore Query `Target`. It
+ * determines whether a given index can be used to serve the specified target.
+ *
+ * The following table showcases some possible index configurations:
+ *
+ * Query                                               | Index
+ * -----------------------------------------------------------------------------
+ * where('a', '==', 'a').where('b', '==', 'b')         | a ASC, b DESC
+ * where('a', '==', 'a').where('b', '==', 'b')         | a ASC
+ * where('a', '==', 'a').where('b', '==', 'b')         | b DESC
+ * where('a', '>=', 'a').orderBy('a')                  | a ASC
+ * where('a', '>=', 'a').orderBy('a', 'desc')          | a DESC
+ * where('a', '>=', 'a').orderBy('a').orderBy('b')     | a ASC, b ASC
+ * where('a', '>=', 'a').orderBy('a').orderBy('b')     | a ASC
+ * where('a', 'array-contains', 'a').orderBy('b')      | a CONTAINS, b ASCENDING
+ * where('a', 'array-contains', 'a').orderBy('b')      | a CONTAINS
+ */
+class TargetIndexMatcher {
+ public:
+  explicit TargetIndexMatcher(const core::Target& target);
+
+  /**
+   * Returns whether the index can be used to serve the TargetIndexMatcher's
+   * target.
+   *
+   * An index is considered capable of serving the target when:
+   * - The target uses all index segments for its filters and OrderBy clauses.
+   *   The target can have additional filter and OrderBy clauses, but not
+   *   fewer.
+   * - If an ArrayContains/ArrayContainsAny is used, the index must also
+   *   have a corresponding `kContains` segment.
+   * - All directional index segments can be mapped to the target as a series of
+   *   equality filters, a single inequality filter and a series of OrderBy
+   *   clauses.
+   * - The segments that represent the equality filters may appear out of order.
+   * - The optional segment for the inequality filter must appear after all
+   *   equality segments.
+   * - The segments that represent that OrderBy clause of the target must appear
+   *   in order after all equality and inequality segments. Single OrderBy
+   *   clauses cannot be skipped, but a continuous OrderBy suffix may be
+   *   omitted.
+   */
+  bool ServedByIndex(const model::FieldIndex& index);
+
+ private:
+  bool HasMatchingEqualityFilter(const model::Segment& segment);
+
+  bool MatchesFilter(const core::FieldFilter& filter,
+                     const model::Segment& segment);
+  bool MatchesFilter(const absl::optional<core::FieldFilter>& filter,
+                     const model::Segment& segment);
+
+  bool MatchesOrderBy(const core::OrderBy& order_by,
+                      const model::Segment& segment);
+
+  // The collection ID (or collection group) of the query target.
+  std::string collection_id_;
+
+  absl::optional<core::FieldFilter> inequality_filter_;
+  std::vector<core::FieldFilter> equality_filters_;
+  core::OrderByList order_bys_;
+};
+
+}  // namespace model
+}  // namespace firestore
+}  // namespace firebase
+
+#endif  // FIRESTORE_CORE_SRC_MODEL_TARGET_INDEX_MATCHER_H_

+ 1 - 1
Firestore/core/src/nanopb/fields_array.h

@@ -182,7 +182,7 @@ inline const pb_field_t* FieldsArray<firestore_NamedQuery>() {
 
 template <>
 inline const pb_field_t* FieldsArray<google_firestore_admin_v1_Index>() {
-  return google_protobuf_Empty_fields;
+  return google_firestore_admin_v1_Index_fields;
 }
 
 template <>

+ 1 - 1
Firestore/core/src/util/filesystem.h

@@ -90,7 +90,7 @@ class Filesystem {
    *
    * @param app_name The name of the application.
    *
-   * @returns The documents directory path or a status with
+   * @return The documents directory path or a status with
    * Error::kErrorUnimplemented if the current platform does not have a legacy
    * documents directory.
    */

+ 3 - 0
Firestore/core/test/unit/bundle/bundle_reader_test.cc

@@ -229,9 +229,12 @@ class BundleReaderTest : public ::testing::Test {
     value2.set_string_value("okok");
     ProtoValue value3;
     value3.set_null_value(google::protobuf::NULL_VALUE);
+    ProtoValue value4;
+    value4.mutable_array_value();
     document.mutable_fields()->insert({"\0\ud7ff\ue000\uffff\"", value1});
     document.mutable_fields()->insert({"\"(╯°□°)╯︵ ┻━┻\"", value2});
     document.mutable_fields()->insert({"nValue", value3});
+    document.mutable_fields()->insert({"emptyArray", value4});
 
     return document;
   }

+ 15 - 8
Firestore/core/test/unit/bundle/bundle_serializer_test.cc

@@ -826,14 +826,6 @@ TEST_F(BundleSerializerTest, DecodeInvalidFieldFilterOperatorFails) {
     EXPECT_NOT_OK(reader.status());
   }
 
-  {
-    auto json_copy =
-        ReplacedCopy(json_string, "\"stringValue\"", "\"arrayValue\"");
-    JsonReader reader;
-    bundle_serializer.DecodeNamedQuery(reader, Parse(json_copy));
-    EXPECT_NOT_OK(reader.status());
-  }
-
   {
     auto json_copy = ReplacedCopy(json_string, "\"op\"", "\"Op\"");
     JsonReader reader;
@@ -850,6 +842,14 @@ TEST_F(BundleSerializerTest, DecodeInvalidFieldFilterOperatorFails) {
 }
 
 TEST_F(BundleSerializerTest, DecodesCompositeFilter) {
+  core::Query original = testutil::Query("colls")
+                             .AddingFilter(Filter("f1", "==", nullptr))
+                             .AddingFilter(Filter("f2", "==", true))
+                             .AddingFilter(Filter("f3", "==", 50.3));
+  VerifyNamedQueryRoundtrip(original);
+}
+
+TEST_F(BundleSerializerTest, DecodesCompositeNotNullFilter) {
   core::Query original =
       testutil::Query("colls")
           .AddingFilter(Filter("f1", "not-in", Array(1, "2", 3.0)))
@@ -858,6 +858,13 @@ TEST_F(BundleSerializerTest, DecodesCompositeFilter) {
   VerifyNamedQueryRoundtrip(original);
 }
 
+TEST_F(BundleSerializerTest, DecodesCompositeNullFilter) {
+  core::Query original = testutil::Query("colls")
+                             .AddingFilter(Filter("f1", "==", nullptr))
+                             .AddingFilter(Filter("f2", "==", nullptr));
+  VerifyNamedQueryRoundtrip(original);
+}
+
 TEST_F(BundleSerializerTest, DecodeInvalidCompositeFilterOperatorFails) {
   std::string json_string = NamedQueryJsonString(
       testutil::Query("colls")

+ 45 - 1
Firestore/core/test/unit/local/counting_query_engine.cc

@@ -22,6 +22,7 @@
 #include "Firestore/core/src/model/mutable_document.h"
 #include "Firestore/core/src/model/mutation_batch.h"
 #include "Firestore/core/src/nanopb/byte_string.h"
+#include "Firestore/core/src/util/hard_assert.h"
 
 namespace firebase {
 namespace firestore {
@@ -43,9 +44,11 @@ void CountingQueryEngine::SetLocalDocumentsView(
       local_documents->remote_document_cache(), this);
   mutation_queue_ = absl::make_unique<WrappedMutationQueue>(
       local_documents->mutation_queue(), this);
+  document_overlay_cache_ = absl::make_unique<WrappedDocumentOverlayCache>(
+      local_documents->document_overlay_cache(), this);
   local_documents_ = absl::make_unique<LocalDocumentsView>(
       remote_documents_.get(), mutation_queue_.get(),
-      local_documents->index_manager());
+      document_overlay_cache_.get(), local_documents->index_manager());
   QueryEngine::SetLocalDocumentsView(local_documents_.get());
 }
 
@@ -54,6 +57,9 @@ void CountingQueryEngine::ResetCounts() {
   mutations_read_by_key_ = 0;
   documents_read_by_query_ = 0;
   documents_read_by_key_ = 0;
+  overlays_read_by_key_ = 0;
+  overlays_read_by_collection_ = 0;
+  overlays_read_by_collection_group_ = 0;
 }
 
 // MARK: - WrappedMutationQueue
@@ -173,6 +179,44 @@ model::MutableDocumentMap WrappedRemoteDocumentCache::GetMatching(
   return result;
 }
 
+// MARK: - WrappedDocumentOverlayCache
+
+absl::optional<model::Overlay> WrappedDocumentOverlayCache::GetOverlay(
+    const model::DocumentKey& key) const {
+  ++query_engine_->overlays_read_by_key_;
+  return subject_->GetOverlay(key);
+}
+
+void WrappedDocumentOverlayCache::SaveOverlays(
+    int largest_batch_id, const MutationByDocumentKeyMap& overlays) {
+  subject_->SaveOverlays(largest_batch_id, overlays);
+}
+
+void WrappedDocumentOverlayCache::RemoveOverlaysForBatchId(int batch_id) {
+  subject_->RemoveOverlaysForBatchId(batch_id);
+}
+
+DocumentOverlayCache::OverlayByDocumentKeyMap
+WrappedDocumentOverlayCache::GetOverlays(const model::ResourcePath& collection,
+                                         int since_batch_id) const {
+  auto result = subject_->GetOverlays(collection, since_batch_id);
+  query_engine_->overlays_read_by_collection_ += result.size();
+  return result;
+}
+
+DocumentOverlayCache::OverlayByDocumentKeyMap
+WrappedDocumentOverlayCache::GetOverlays(absl::string_view collection_group,
+                                         int since_batch_id,
+                                         std::size_t count) const {
+  auto result = subject_->GetOverlays(collection_group, since_batch_id, count);
+  query_engine_->overlays_read_by_collection_group_ += result.size();
+  return result;
+}
+
+int WrappedDocumentOverlayCache::GetOverlayCount() const {
+  HARD_FAIL("WrappedDocumentOverlayCache::GetOverlayCount() not implemented");
+}
+
 }  // namespace local
 }  // namespace firestore
 }  // namespace firebase

+ 37 - 0
Firestore/core/test/unit/local/counting_query_engine.h

@@ -21,6 +21,7 @@
 #include <utility>
 #include <vector>
 
+#include "Firestore/core/src/local/document_overlay_cache.h"
 #include "Firestore/core/src/local/mutation_queue.h"
 #include "Firestore/core/src/local/query_engine.h"
 #include "Firestore/core/src/local/remote_document_cache.h"
@@ -37,6 +38,7 @@ class Query;
 namespace local {
 
 class LocalDocumentsView;
+class WrappedDocumentOverlayCache;
 class WrappedMutationQueue;
 class WrappedRemoteDocumentCache;
 
@@ -90,17 +92,22 @@ class CountingQueryEngine : public QueryEngine {
   }
 
  private:
+  friend class WrappedDocumentOverlayCache;
   friend class WrappedMutationQueue;
   friend class WrappedRemoteDocumentCache;
 
   std::unique_ptr<LocalDocumentsView> local_documents_;
   std::unique_ptr<WrappedMutationQueue> mutation_queue_;
+  std::unique_ptr<WrappedDocumentOverlayCache> document_overlay_cache_;
   std::unique_ptr<WrappedRemoteDocumentCache> remote_documents_;
 
   size_t mutations_read_by_query_ = 0;
   size_t mutations_read_by_key_ = 0;
   size_t documents_read_by_query_ = 0;
   size_t documents_read_by_key_ = 0;
+  size_t overlays_read_by_key_ = 0;
+  size_t overlays_read_by_collection_ = 0;
+  size_t overlays_read_by_collection_group_ = 0;
 };
 
 /** A MutationQueue that counts document reads. */
@@ -186,6 +193,36 @@ class WrappedRemoteDocumentCache : public RemoteDocumentCache {
   CountingQueryEngine* query_engine_ = nullptr;
 };
 
+/** A DocumentOverlayCache that counts document reads. */
+class WrappedDocumentOverlayCache final : public DocumentOverlayCache {
+ public:
+  WrappedDocumentOverlayCache(DocumentOverlayCache* subject,
+                              CountingQueryEngine* query_engine)
+      : subject_(subject), query_engine_(query_engine) {
+  }
+
+  absl::optional<model::Overlay> GetOverlay(
+      const model::DocumentKey& key) const override;
+
+  void SaveOverlays(int largest_batch_id,
+                    const MutationByDocumentKeyMap& overlays) override;
+
+  void RemoveOverlaysForBatchId(int batch_id) override;
+
+  OverlayByDocumentKeyMap GetOverlays(const model::ResourcePath& collection,
+                                      int since_batch_id) const override;
+
+  OverlayByDocumentKeyMap GetOverlays(absl::string_view collection_group,
+                                      int since_batch_id,
+                                      std::size_t count) const override;
+
+ private:
+  int GetOverlayCount() const override;
+
+  DocumentOverlayCache* subject_ = nullptr;
+  CountingQueryEngine* query_engine_ = nullptr;
+};
+
 }  // namespace local
 }  // namespace firestore
 }  // namespace firebase

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini