Procházet zdrojové kódy

Merge branch 'master' into combine-main

Peter Friese před 5 roky
rodič
revize
de304ad16f
100 změnil soubory, kde provedl 1482 přidání a 991 odebrání
  1. 2 2
      .github/workflows/abtesting.yml
  2. 3 3
      .github/workflows/analytics.yml
  3. 2 3
      .github/workflows/appdistribution.yml
  4. 2 2
      .github/workflows/auth.yml
  5. 2 2
      .github/workflows/cocoapods-integration.yml
  6. 2 2
      .github/workflows/core-diagnostics.yml
  7. 2 2
      .github/workflows/core.yml
  8. 2 2
      .github/workflows/crashlytics.yml
  9. 2 2
      .github/workflows/database.yml
  10. 2 2
      .github/workflows/dynamiclinks.yml
  11. 2 2
      .github/workflows/firebasepod.yml
  12. 2 2
      .github/workflows/firestore.yml
  13. 2 2
      .github/workflows/functions.yml
  14. 2 2
      .github/workflows/google-utilities-components.yml
  15. 2 2
      .github/workflows/inappmessaging.yml
  16. 2 2
      .github/workflows/installations.yml
  17. 2 2
      .github/workflows/instanceid.yml
  18. 2 2
      .github/workflows/messaging.yml
  19. 1 1
      .github/workflows/prerelease.yml
  20. 2 2
      .github/workflows/remoteconfig.yml
  21. 2 2
      .github/workflows/segmentation.yml
  22. 2 2
      .github/workflows/spm.yml
  23. 2 2
      .github/workflows/storage.yml
  24. 2 2
      .github/workflows/symbolcollision.yml
  25. 11 7
      .github/workflows/test_coverage.yml
  26. 4 0
      .github/workflows/zip.yml
  27. 2 0
      .gitignore
  28. 0 6
      CoreOnly/Sources/Firebase.h
  29. 1 0
      CoreOnly/Tests/FirebasePodTest/Podfile
  30. 3 0
      Crashlytics/CHANGELOG.md
  31. 5 0
      Crashlytics/Crashlytics/Components/FIRCLSUserLogging.h
  32. 32 10
      Crashlytics/Crashlytics/Components/FIRCLSUserLogging.m
  33. 40 6
      Crashlytics/Crashlytics/Controllers/FIRCLSExistingReportManager.h
  34. 88 75
      Crashlytics/Crashlytics/Controllers/FIRCLSExistingReportManager.m
  35. 1 1
      Crashlytics/Crashlytics/Controllers/FIRCLSReportManager.h
  36. 42 64
      Crashlytics/Crashlytics/Controllers/FIRCLSReportManager.m
  37. 14 5
      Crashlytics/Crashlytics/FIRCrashlytics.m
  38. 197 0
      Crashlytics/Crashlytics/FIRCrashlyticsReport.m
  39. 1 1
      Crashlytics/Crashlytics/Models/FIRCLSInternalReport.h
  40. 4 4
      Crashlytics/Crashlytics/Models/FIRCLSInternalReport.m
  41. 0 110
      Crashlytics/Crashlytics/Models/FIRCLSReport.h
  42. 0 241
      Crashlytics/Crashlytics/Models/FIRCLSReport.m
  43. 0 27
      Crashlytics/Crashlytics/Models/FIRCLSReport_Private.h
  44. 14 7
      Crashlytics/Crashlytics/Private/FIRCrashlyticsReport_Private.h
  45. 29 0
      Crashlytics/Crashlytics/Public/FirebaseCrashlytics/FIRCrashlytics.h
  46. 108 0
      Crashlytics/Crashlytics/Public/FirebaseCrashlytics/FIRCrashlyticsReport.h
  47. 1 0
      Crashlytics/Crashlytics/Public/FirebaseCrashlytics/FirebaseCrashlytics.h
  48. 4 5
      Crashlytics/UnitTests/FIRCLSInternalReportTests.m
  49. 41 24
      Crashlytics/UnitTests/FIRCLSReportManagerTests.m
  50. 0 130
      Crashlytics/UnitTests/FIRCLSReportTests.m
  51. 260 0
      Crashlytics/UnitTests/FIRCrashlyticsReportTests.m
  52. 48 41
      Example/InstanceID/Tests/FIRInstanceIDTest.m
  53. 1 0
      Example/watchOSSample/Podfile
  54. 79 0
      FIRDynamicLinkTest.m
  55. 18 18
      Firebase.podspec
  56. 1 1
      FirebaseABTesting.podspec
  57. 2 2
      FirebaseABTesting/CHANGELOG.md
  58. 2 2
      FirebaseAnalytics.podspec.json
  59. 1 1
      FirebaseAppDistribution.podspec
  60. 1 1
      FirebaseAuth.podspec
  61. 1 0
      FirebaseAuth/Tests/Sample/Podfile
  62. 1 1
      FirebaseCore.podspec
  63. 3 1
      FirebaseCore/CHANGELOG.md
  64. 0 2
      FirebaseCore/Sources/FIRApp.m
  65. 1 1
      FirebaseCoreDiagnostics.podspec
  66. 1 1
      FirebaseCrashlytics.podspec
  67. 1 1
      FirebaseDatabase.podspec
  68. 1 1
      FirebaseDynamicLinks.podspec
  69. 3 0
      FirebaseDynamicLinks/CHANGELOG.md
  70. 15 1
      FirebaseDynamicLinks/Sources/FIRDynamicLink.m
  71. 6 0
      FirebaseDynamicLinks/Sources/Public/FirebaseDynamicLinks/FIRDynamicLink.h
  72. 6 4
      FirebaseDynamicLinks/Tests/Sample/FDLBuilderTestAppObjC/AppDelegate.m
  73. 6 4
      FirebaseDynamicLinks/Tests/Sample/FDLBuilderTestAppObjC/SceneDelegate.m
  74. 1 0
      FirebaseDynamicLinks/Tests/Sample/Podfile
  75. 1 1
      FirebaseFirestore.podspec
  76. 1 1
      FirebaseFirestoreSwift.podspec
  77. 1 1
      FirebaseFunctions.podspec
  78. 1 1
      FirebaseInAppMessaging.podspec
  79. 1 0
      FirebaseInAppMessaging/Tests/Integration/DefaultUITestApp/Podfile
  80. 1 0
      FirebaseInAppMessaging/Tests/Integration/FunctionalTestApp/Podfile
  81. 1 1
      FirebaseInstallations.podspec
  82. 1 1
      FirebaseInstanceID.podspec
  83. 1 2
      FirebaseMLModelDownloader.podspec
  84. 52 0
      FirebaseMLModelDownloader/Sources/ModelDownloadTask.swift
  85. 2 0
      FirebaseMLModelDownloader/Sources/ModelDownloader.swift
  86. 113 0
      FirebaseMLModelDownloader/Sources/ModelInfoRetriever.swift
  87. 39 17
      FirebaseMLModelDownloader/Sources/TelemetryLogger.swift
  88. 34 22
      FirebaseMLModelDownloader/Tests/Integration/ModelDownloaderIntegrationTests.swift
  89. 68 1
      FirebaseMLModelDownloader/Tests/Unit/ModelDownloaderUnitTests.swift
  90. 1 1
      FirebaseMessaging.podspec
  91. 1 0
      FirebaseMessaging/Apps/AdvancedSample/Podfile
  92. 1 0
      FirebaseMessaging/Apps/Sample/Podfile
  93. 2 2
      FirebaseMessaging/CHANGELOG.md
  94. 3 4
      FirebasePerformance.podspec
  95. 6 0
      FirebasePerformance/CHANGELOG.md
  96. 0 7
      FirebasePerformance/Sources/Configurations/FPRConfigurations.h
  97. 0 32
      FirebasePerformance/Sources/Configurations/FPRConfigurations.m
  98. 0 20
      FirebasePerformance/Sources/Configurations/FPRRemoteConfigFlags.h
  99. 0 20
      FirebasePerformance/Sources/Configurations/FPRRemoteConfigFlags.m
  100. 2 2
      FirebasePerformance/Sources/FPRClient+Private.h

+ 2 - 2
.github/workflows/abtesting.yml

@@ -8,8 +8,8 @@ on:
     - '.github/workflows/abtesting.yml'
     - 'Gemfile'
   schedule:
-    # Run every day at 11pm (PST) - cron uses UTC times
-    - cron:  '0 7 * * *'
+    # Run every day at 7pm (PST) - cron uses UTC times
+    - cron:  '0 3 * * *'
 
 jobs:
   pod-lib-lint:

+ 3 - 3
.github/workflows/analytics.yml

@@ -7,8 +7,8 @@ on:
     - 'GoogleAppMeasurement.podspec.json'
     - 'Gemfile'
   schedule:
-    # Run every day at 11pm (PST) - cron uses UTC times
-    - cron:  '0 7 * * *'
+    # Run every day at 7pm (PST) - cron uses UTC times
+    - cron:  '0 3 * * *'
 
 jobs:
   pod-lib-lint:
@@ -23,5 +23,5 @@ jobs:
     - name: GoogleAppMeasurement
       run: scripts/third_party/travis/retry.sh pod spec lint GoogleAppMeasurement.podspec.json
 
-# TODO: Consider pushing GoogleAppMeasurement.podspec.json to SpecsStaging to enable similar test
+# TODO: Consider pushing GoogleAppMeasurement.podspec.json to SpecsDev to enable similar test
 # for FirebaseAnalytics.podspec.json

+ 2 - 3
.github/workflows/appdistribution.yml

@@ -7,9 +7,8 @@ on:
     - '.github/workflows/appdistribution.yml'
     - 'Gemfile'
   schedule:
-    # Run every day at 3am (PST) - cron uses UTC times
-    # This is set to 3 hours after zip workflow finishes so zip testing can run after.
-    - cron:  '0 11 * * *'
+    # Run every day at 7pm (PST) - cron uses UTC times
+    - cron:  '0 3 * * *'
 
 jobs:
   pod-lib-lint:

+ 2 - 2
.github/workflows/auth.yml

@@ -8,8 +8,8 @@ on:
     - '.github/workflows/auth.yml'
     - 'Gemfile'
   schedule:
-    # Run every day at 11pm (PST) - cron uses UTC times
-    - cron:  '0 7 * * *'
+    # Run every day at 7pm (PST) - cron uses UTC times
+    - cron:  '0 3 * * *'
 
 jobs:
 

+ 2 - 2
.github/workflows/cocoapods-integration.yml

@@ -7,8 +7,8 @@ on:
     - '.github/workflows/cocoapods-integration.yml'
     - 'Gemfile'
   schedule:
-    # Run every day at 11pm (PST) - cron uses UTC times
-    - cron:  '0 7 * * *'
+    # Run every day at 7pm (PST) - cron uses UTC times
+    - cron:  '0 3 * * *'
 
 jobs:
   tests:

+ 2 - 2
.github/workflows/core-diagnostics.yml

@@ -9,8 +9,8 @@ on:
     - '.github/workflows/core-diagnostics.yml'
     - 'Gemfile'
   schedule:
-    # Run every day at 11pm (PST) - cron uses UTC times
-    - cron:  '0 7 * * *'
+    # Run every day at 8pm (PST) - cron uses UTC times
+    - cron:  '0 4 * * *'
 
 jobs:
   pod-lib-lint:

+ 2 - 2
.github/workflows/core.yml

@@ -8,8 +8,8 @@ on:
     - '.github/workflows/core.yml'
     - 'Gemfile'
   schedule:
-    # Run every day at 11pm (PST) - cron uses UTC times
-    - cron:  '0 7 * * *'
+    # Run every day at 8pm (PST) - cron uses UTC times
+    - cron:  '0 4 * * *'
 
 jobs:
   pod-lib-lint:

+ 2 - 2
.github/workflows/crashlytics.yml

@@ -9,8 +9,8 @@ on:
     - 'Interop/Analytics/Public/*.h'
     - 'Gemfile'
   schedule:
-    # Run every day at 11pm (PST) - cron uses UTC times
-    - cron:  '0 7 * * *'
+    # Run every day at 8pm (PST) - cron uses UTC times
+    - cron:  '0 4 * * *'
 
 jobs:
 

+ 2 - 2
.github/workflows/database.yml

@@ -11,8 +11,8 @@ on:
     - 'Gemfile'
     - 'scripts/run_database_emulator.sh'
   schedule:
-    # Run every day at 11pm (PST) - cron uses UTC times
-    - cron:  '0 7 * * *'
+    # Run every day at 8pm (PST) - cron uses UTC times
+    - cron:  '0 4 * * *'
 
 jobs:
   unit:

+ 2 - 2
.github/workflows/dynamiclinks.yml

@@ -8,8 +8,8 @@ on:
     - 'Interop/Analytics/Public/*.h'
     - 'Gemfile'
   schedule:
-    # Run every day at 11pm (PST) - cron uses UTC times
-    - cron:  '0 7 * * *'
+    # Run every day at 9pm (PST) - cron uses UTC times
+    - cron:  '0 5 * * *'
 
 jobs:
   pod_lib_lint:

+ 2 - 2
.github/workflows/firebasepod.yml

@@ -10,8 +10,8 @@ on:
     - '.github/workflows/firebasepod.yml'
     - 'Gemfile'
   schedule:
-    # Run every day at 11pm (PST) - cron uses UTC times
-    - cron:  '0 7 * * *'
+    # Run every day at 9pm (PST) - cron uses UTC times
+    - cron:  '0 5 * * *'
 
 jobs:
   installation-test:

+ 2 - 2
.github/workflows/firestore.yml

@@ -56,8 +56,8 @@ on:
     - 'Gemfile'
 
   schedule:
-    # Run every day at 11pm (PST) - cron uses UTC times
-    - cron:  '0 7 * * *'
+    # Run every day at 9pm (PST) - cron uses UTC times
+    - cron:  '0 5 * * *'
 
 jobs:
   check:

+ 2 - 2
.github/workflows/functions.yml

@@ -9,8 +9,8 @@ on:
     - 'FirebaseMessaging/Sources/Interop/*.h'
     - 'Gemfile'
   schedule:
-    # Run every day at 11pm (PST) - cron uses UTC times
-    - cron:  '0 7 * * *'
+    # Run every day at 10pm (PST) - cron uses UTC times
+    - cron:  '0 6 * * *'
 
 jobs:
 

+ 2 - 2
.github/workflows/google-utilities-components.yml

@@ -7,8 +7,8 @@ on:
     - '.github/workflows/google-utilities-components.yml'
     - 'Gemfile'
   schedule:
-    # Run every day at 11pm (PST) - cron uses UTC times
-    - cron:  '0 7 * * *'
+    # Run every day at 10pm (PST) - cron uses UTC times
+    - cron:  '0 6 * * *'
 
 jobs:
   pod-lib-lint:

+ 2 - 2
.github/workflows/inappmessaging.yml

@@ -8,8 +8,8 @@ on:
     - '.github/workflows/inappmessaging.yml'
     - 'Gemfile'
   schedule:
-    # Run every day at 11pm (PST) - cron uses UTC times
-    - cron:  '0 7 * * *'
+    # Run every day at 10pm (PST) - cron uses UTC times
+    - cron:  '0 6 * * *'
 
 jobs:
 

+ 2 - 2
.github/workflows/installations.yml

@@ -7,8 +7,8 @@ on:
     - '.github/workflows/installations.yml'
     - 'Gemfile'
   schedule:
-    # Run every day at 11pm (PST) - cron uses UTC times
-    - cron:  '0 7 * * *'
+    # Run every day at 10pm (PST) - cron uses UTC times
+    - cron:  '0 6 * * *'
 
 jobs:
   pod-lib-lint:

+ 2 - 2
.github/workflows/instanceid.yml

@@ -9,8 +9,8 @@ on:
     - '.github/workflows/instanceid.yml'
     - 'Gemfile'
   schedule:
-    # Run every day at 11pm (PST) - cron uses UTC times
-    - cron:  '0 7 * * *'
+    # Run every day at 10pm (PST) - cron uses UTC times
+    - cron:  '0 6 * * *'
 
 jobs:
   pod-lib-lint:

+ 2 - 2
.github/workflows/messaging.yml

@@ -14,8 +14,8 @@ on:
     # Rebuild on Ruby infrastructure changes
     - 'Gemfile'
   schedule:
-    # Run every day at 11pm (PST) - cron uses UTC times
-    - cron:  '0 7 * * *'
+    # Run every day at 10pm (PST) - cron uses UTC times
+    - cron:  '0 6 * * *'
 
 jobs:
 

+ 1 - 1
.github/workflows/prerelease.yml

@@ -37,7 +37,7 @@ jobs:
         cd scripts/create_spec_repo/
         swift build
         pod repo add --silent "${local_repo}" https://"$botaccess"@github.com/FirebasePrivate/SpecsReleasing.git
-        BOT_TOKEN="${botaccess}"  .build/debug/spec-repo-builder --sdk-repo "${local_sdk_repo_dir}" --local-spec-repo-name "${local_repo}" --sdk-repo-name SpecsReleasing --pod-sources 'https://${BOT_TOKEN}@github.com/FirebasePrivate/SpecsReleasing' --pod-sources "https://github.com/firebase/SpecsStaging.git" --pod-sources "https://cdn.cocoapods.org/"
+        BOT_TOKEN="${botaccess}"  .build/debug/spec-repo-builder --sdk-repo "${local_sdk_repo_dir}" --local-spec-repo-name "${local_repo}" --sdk-repo-name SpecsReleasing --pod-sources 'https://${BOT_TOKEN}@github.com/FirebasePrivate/SpecsReleasing' --pod-sources "https://github.com/firebase/SpecsDev.git" --pod-sources "https://github.com/firebase/SpecsStaging.git" --pod-sources "https://cdn.cocoapods.org/"
     - name: Clean Artifacts
       if: ${{ always() }}
       run: |

+ 2 - 2
.github/workflows/remoteconfig.yml

@@ -9,8 +9,8 @@ on:
     - 'Gemfile'
     - 'scripts/generate_access_token.sh'
   schedule:
-    # Run every day at 11pm (PST) - cron uses UTC times
-    - cron:  '0 7 * * *'
+    # Run every day at 12am (PST) - cron uses UTC times
+    - cron:  '0 8 * * *'
 
 jobs:
 

+ 2 - 2
.github/workflows/segmentation.yml

@@ -7,8 +7,8 @@ on:
     - '.github/workflows/segmentation.yml'
     - 'Gemfile'
   schedule:
-    # Run every day at 11pm (PST) - cron uses UTC times
-    - cron:  '0 7 * * *'
+    # Run every day at 12am (PST) - cron uses UTC times
+    - cron:  '0 8 * * *'
 
 jobs:
   pod-lib-lint:

+ 2 - 2
.github/workflows/spm.yml

@@ -7,8 +7,8 @@ on:
     - 'Package.swift'
     - 'Gemfile'
   schedule:
-    # Run every day at 11pm (PST) - cron uses UTC times
-    - cron:  '0 7 * * *'
+    # Run every day at 12am (PST) - cron uses UTC times
+    - cron:  '0 8 * * *'
 
 # This workflow builds and tests the Swift Package Manager. Only iOS runs on PRs
 # because each platform takes 15-20 minutes after adding Firestore.

+ 2 - 2
.github/workflows/storage.yml

@@ -10,8 +10,8 @@ on:
     # Rebuild on Ruby infrastructure changes.
     - 'Gemfile'
   schedule:
-    # Run every day at 11pm (PST) - cron uses UTC times
-    - cron:  '0 7 * * *'
+    # Run every day at 12am (PST) - cron uses UTC times
+    - cron:  '0 8 * * *'
 
 jobs:
   storage:

+ 2 - 2
.github/workflows/symbolcollision.yml

@@ -9,8 +9,8 @@ on:
     - 'SymbolCollisionTest/**'
     - 'Gemfile'
   schedule:
-    # Run every day at 11pm (PST) - cron uses UTC times
-    - cron:  '0 7 * * *'
+    # Run every day at 12am (PST) - cron uses UTC times
+    - cron:  '0 8 * * *'
 
 jobs:
   installation-test:

+ 11 - 7
.github/workflows/test_coverage.yml

@@ -7,6 +7,9 @@ on:
     # closed will be triggered when a pull request is closed.
     types: [opened, synchronize, closed]
 
+env:
+  METRICS_SERVICE_SECRET: ${{ secrets.GHASecretsGPGPassphrase1 }}
+
 jobs:
   check:
     if: github.repository == 'Firebase/firebase-ios-sdk' && (github.event.action == 'opened' || github.event.action == 'synchronize')
@@ -35,7 +38,10 @@ jobs:
         id: check_files
         env:
           pr_branch: ${{ github.event.pull_request.head.ref }}
-        run: ./scripts/code_coverage_report/get_updated_files.sh
+        run: |
+          if [ ! -z "${{ env.METRICS_SERVICE_SECRET }}"  ]; then
+          ./scripts/code_coverage_report/get_updated_files.sh
+          fi
 
   pod-lib-lint-database:
     needs: check
@@ -269,14 +275,12 @@ jobs:
 
   create_report:
     needs: [check, pod-lib-lint-abtesting, pod-lib-lint-auth, pod-lib-lint-database, pod-lib-lint-dynamiclinks, pod-lib-lint-firestore, pod-lib-lint-functions, pod-lib-lint-inappmessaging, pod-lib-lint-instanceid, pod-lib-lint-messaging, pod-lib-lint-performance, pod-lib-lint-remoteconfig, pod-lib-lint-storage]
-    env:
-      metrics_service_secret: ${{ secrets.GHASecretsGPGPassphrase1 }}
     if: always()
     runs-on: macOS-latest
     steps:
       - uses: actions/checkout@v2
       - name: Access to Metrics Service
-        if:  env.metrics_service_secret
+        if:  ${{ env.METRICS_SERVICE_SECRET }}
         run: |
           # Install gcloud sdk
           curl https://sdk.cloud.google.com > install.sh
@@ -286,14 +290,14 @@ jobs:
 
           # Activate the service account for Metrics Service.
           scripts/decrypt_gha_secret.sh scripts/gha-encrypted/metrics_service_access.json.gpg \
-          metrics-access.json "$metrics_service_secret"
+          metrics-access.json "${{ env.METRICS_SERVICE_SECRET }}"
           gcloud auth activate-service-account --key-file metrics-access.json
       - uses: actions/download-artifact@v2
         id: download
         with:
           path: /Users/runner/test
       - name: Compare Diff and Post a Report
-        if: github.event_name == 'pull_request' && env.metrics_service_secret
+        if: github.event_name == 'pull_request' && ${{ env.METRICS_SERVICE_SECRET }}
         env:
           base_commit: ${{ needs.check.outputs.base_commit }}
         run: |
@@ -304,7 +308,7 @@ jobs:
           swift run CoverageReportGenerator --presubmit "firebase/firebase-ios-sdk" --commit "${GITHUB_SHA}" --token $(gcloud auth print-identity-token) --xcresult-dir "/Users/runner/test/codecoverage" --log-link "https://github.com/firebase/firebase-ios-sdk/actions/runs/${GITHUB_RUN_ID}" --pull-request-num ${{github.event.pull_request.number}} --base-commit "$base_commit"
           fi
       - name: Update New Coverage Data
-        if: github.event.pull_request.merged == true && env.metrics_service_secret
+        if: github.event.pull_request.merged == true && ${{ env.METRICS_SERVICE_SECRET }}
         run: |
           if [ -d "${{steps.download.outputs.download-path}}" ]; then
           cd scripts/code_coverage_report/generate_code_coverage_report

+ 4 - 0
.github/workflows/zip.yml

@@ -27,6 +27,8 @@ jobs:
     runs-on: macOS-latest
     steps:
     - uses: actions/checkout@v2
+    - name: Xcode 12.2
+      run: sudo xcode-select -s /Applications/Xcode_12.2.app/Contents/Developer
     - name: Build
       run: |
         cd ReleaseTooling
@@ -39,6 +41,8 @@ jobs:
     runs-on: macOS-latest
     steps:
     - uses: actions/checkout@v2
+    - name: Xcode 12.2
+      run: sudo xcode-select -s /Applications/Xcode_12.2.app/Contents/Developer
     - name: Setup Bundler
       run: ./scripts/setup_bundler.sh
     - name: ZipBuildingTest

+ 2 - 0
.gitignore

@@ -9,6 +9,8 @@ FirebaseAuth/Tests/Sample/SwiftApiTests/Credentials.swift
 FirebaseDatabase/Tests/Resources/GoogleService-Info.plist
 
 FirebaseRemoteConfig/Tests/SwiftAPI/Resources/GoogleService-Info.plist
+FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/GoogleService-Info.plist
+FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/SecondApp-GoogleService-Info.plist
 
 # FirebaseStorage integration tests GoogleService-Info.plist
 FirebaseStorage/Tests/Integration/Resources/GoogleService-Info.plist

+ 0 - 6
CoreOnly/Sources/Firebase.h

@@ -99,12 +99,6 @@ FirebaseAnalytics dependency to your project to ensure Messaging works as intend
 
   #if __has_include(<FirebasePerformance/FirebasePerformance.h>)
     #import <FirebasePerformance/FirebasePerformance.h>
-    #if TARGET_OS_IOS && !__has_include(<FirebaseAnalytics/FirebaseAnalytics.h>)
-      #ifndef FIREBASE_ANALYTICS_SUPPRESS_WARNING
-        #warning "FirebaseAnalytics.framework is not included in your target. Please add the \
-FirebaseAnalytics dependency to your project to ensure Firebase Performance works as intended."
-      #endif // #ifndef FIREBASE_ANALYTICS_SUPPRESS_WARNING
-    #endif
   #endif
 
   #if __has_include(<FirebaseRemoteConfig/FirebaseRemoteConfig.h>)

+ 1 - 0
CoreOnly/Tests/FirebasePodTest/Podfile

@@ -1,6 +1,7 @@
 # Uncomment the next line to define a global platform for your project
 platform :ios, '10.0'
 
+source 'https://github.com/firebase/SpecsDev.git'
 source 'https://github.com/firebase/SpecsStaging.git'
 source 'https://cdn.cocoapods.org/'
 

+ 3 - 0
Crashlytics/CHANGELOG.md

@@ -1,3 +1,6 @@
+# Unreleased
+- [added] Added a new API checkAndUpdateUnsentReportsWithCompletion for updating the crash report from the previous run of the app if, for example, the developer wants to implement a feedback dialog to ask end-users for more information. Unsent Crashlytics Reports have familiar methods like setting custom keys and logs.
+
 # v7.6.0
 - [fixed] Fixed an issue where some developers experienced a race condition involving binary image operations (#7459).
 

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

@@ -101,6 +101,11 @@ void FIRCLSUserLoggingWriteAndCheckABFiles(FIRCLSUserLoggingABStorage* storage,
 NSArray* FIRCLSUserLoggingStoredKeyValues(const char* path);
 
 OBJC_EXTERN void FIRCLSLog(NSString* format, ...) NS_FORMAT_FUNCTION(1, 2);
+OBJC_EXTERN void FIRCLSLogToStorage(FIRCLSUserLoggingABStorage* storage,
+                                    const char** activePath,
+                                    NSString* format,
+                                    ...) NS_FORMAT_FUNCTION(3, 4);
+
 #endif
 
 __END_DECLS

+ 32 - 10
Crashlytics/Crashlytics/Components/FIRCLSUserLogging.m

@@ -42,7 +42,9 @@ static void FIRCLSUserLoggingWriteKeysAndValues(NSDictionary *keysAndValues,
 static void FIRCLSUserLoggingCheckAndSwapABFiles(FIRCLSUserLoggingABStorage *storage,
                                                  const char **activePath,
                                                  off_t fileSize);
-void FIRCLSLogInternal(NSString *message);
+void FIRCLSLogInternal(FIRCLSUserLoggingABStorage *storage,
+                       const char **activePath,
+                       NSString *message);
 
 #pragma mark - Setup
 void FIRCLSUserLoggingInit(FIRCLSUserLoggingReadOnlyContext *roContext,
@@ -198,7 +200,8 @@ void FIRCLSUserLoggingCompactKVEntries(FIRCLSUserLoggingKVStorage *storage) {
     // but it's very uncommon to go down this path.
     NSArray *keys = [finalKVs allKeys];
 
-    FIRCLSSDKLogInfo("Truncating KV set, which is above max %d\n", maxCount);
+    FIRCLSSDKLogInfo("Truncating %d keys from KV set, which is above max %d\n",
+                     (uint32_t)(finalKVs.count - maxCount), maxCount);
 
     finalKVs =
         [finalKVs dictionaryWithValuesForKeys:[keys subarrayWithRange:NSMakeRange(0, maxCount)]];
@@ -396,7 +399,26 @@ void FIRCLSLog(NSString *format, ...) {
   NSString *msg = [[NSString alloc] initWithFormat:format arguments:args];
   va_end(args);
 
-  FIRCLSLogInternal(msg);
+  FIRCLSUserLoggingABStorage *currentStorage = &_firclsContext.readonly->logging.logStorage;
+  const char **activePath = &_firclsContext.writable->logging.activeUserLogPath;
+  FIRCLSLogInternal(currentStorage, activePath, msg);
+}
+
+void FIRCLSLogToStorage(FIRCLSUserLoggingABStorage *storage,
+                        const char **activePath,
+                        NSString *format,
+                        ...) {
+  // If the format is nil do nothing just like NSLog.
+  if (!format) {
+    return;
+  }
+
+  va_list args;
+  va_start(args, format);
+  NSString *msg = [[NSString alloc] initWithFormat:format arguments:args];
+  va_end(args);
+
+  FIRCLSLogInternal(storage, activePath, msg);
 }
 
 #pragma mark - Properties
@@ -524,7 +546,9 @@ void FIRCLSLogInternalWrite(FIRCLSFile *file, NSString *message, uint64_t time)
   FIRCLSFileWriteSectionEnd(file);
 }
 
-void FIRCLSLogInternal(NSString *message) {
+void FIRCLSLogInternal(FIRCLSUserLoggingABStorage *storage,
+                       const char **activePath,
+                       NSString *message) {
   if (!message) {
     return;
   }
@@ -538,7 +562,7 @@ void FIRCLSLogInternal(NSString *message) {
   struct timeval te;
 
   NSUInteger messageLength = [message length];
-  int maxLogSize = _firclsContext.readonly->logging.logStorage.maxSize;
+  int maxLogSize = storage->maxSize;
 
   if (messageLength > maxLogSize) {
     FIRCLSWarningLog(
@@ -555,9 +579,7 @@ void FIRCLSLogInternal(NSString *message) {
 
   const uint64_t time = te.tv_sec * 1000LL + te.tv_usec / 1000;
 
-  FIRCLSUserLoggingWriteAndCheckABFiles(&_firclsContext.readonly->logging.logStorage,
-                                        &_firclsContext.writable->logging.activeUserLogPath,
-                                        ^(FIRCLSFile *file) {
-                                          FIRCLSLogInternalWrite(file, message, time);
-                                        });
+  FIRCLSUserLoggingWriteAndCheckABFiles(storage, activePath, ^(FIRCLSFile *file) {
+    FIRCLSLogInternalWrite(file, message, time);
+  });
 }

+ 40 - 6
Crashlytics/Crashlytics/Controllers/FIRCLSExistingReportManager.h

@@ -19,25 +19,59 @@ NS_ASSUME_NONNULL_BEGIN
 @class FIRCLSManagerData;
 @class FIRCLSReportUploader;
 @class FIRCLSDataCollectionToken;
+@class FIRCrashlyticsReport;
 
 @interface FIRCLSExistingReportManager : NSObject
 
+/**
+ * Returns the number of unsent reports on the device, ignoring empty reports in
+ * the active folder, and ignoring any reports in "processing" or "prepared".
+ *
+ * In the past, this would count reports in the processed or prepared
+ * folders. This has been changed because reports in those paths have already
+ * been cleared for upload, so there isn't any point in asking for permission
+ * or possibly spamming end-users if a report gets stuck.
+ *
+ * The tricky part is, customers will NOT be alerted in checkForUnsentReports
+ * for reports in these paths, but when they choose sendUnsentReports / enable data
+ * collection, reports in those directories will be re-managed. This should be ok and
+ * just an edge case because reports should only be in processing or prepared for a split second as
+ * they do on-device symbolication and get converted into a GDTEvent. After a report is handed off
+ * to GoogleDataTransport, it is uploaded regardless of Crashlytics data collection.
+ */
+@property(nonatomic, readonly) NSUInteger unsentReportsCount;
+
+/**
+ * This value needs to stay in sync with numUnsentReports, so if there is > 0 numUnsentReports,
+ * newestUnsentReport needs to return a value. Otherwise it needs to return null.
+ *
+ * FIRCLSContext needs to be initialized before the FIRCrashlyticsReport is instantiated.
+ */
+@property(nonatomic, readonly) FIRCrashlyticsReport *_Nullable newestUnsentReport;
+
 - (instancetype)initWithManagerData:(FIRCLSManagerData *)managerData
                      reportUploader:(FIRCLSReportUploader *)reportUploader;
 
 - (instancetype)init NS_UNAVAILABLE;
 + (instancetype)new NS_UNAVAILABLE;
 
-- (int)unsentReportsCountWithPreexisting:(NSArray<NSString *> *)paths;
+/**
+ * This is important to call once, early in startup, before the
+ * new report for this run of the app has been created. Any
+ * reports in ExistingReportManager will be uploaded or deleted
+ * and we don't want to do that for the current run of the app.
+ */
+- (void)collectExistingReports;
 
-- (void)deleteUnsentReportsWithPreexisting:(NSArray *)preexistingReportPaths;
+/**
+ * This is the side-effect of calling deleteUnsentReports, or collect_reports setting
+ * being false.
+ */
+- (void)deleteUnsentReports;
 
-- (void)processExistingReportPaths:(NSArray *)reportPaths
-               dataCollectionToken:(FIRCLSDataCollectionToken *)dataCollectionToken
+- (void)sendUnsentReportsWithToken:(FIRCLSDataCollectionToken *)dataCollectionToken
                           asUrgent:(BOOL)urgent;
 
-- (void)handleContentsInOtherReportingDirectoriesWithToken:(FIRCLSDataCollectionToken *)token;
-
 @end
 
 NS_ASSUME_NONNULL_END

+ 88 - 75
Crashlytics/Crashlytics/Controllers/FIRCLSExistingReportManager.m

@@ -17,9 +17,10 @@
 #import "Crashlytics/Crashlytics/Controllers/FIRCLSManagerData.h"
 #import "Crashlytics/Crashlytics/Controllers/FIRCLSReportUploader.h"
 #import "Crashlytics/Crashlytics/DataCollection/FIRCLSDataCollectionToken.h"
-#import "Crashlytics/Crashlytics/Helpers/FIRCLSLogger.h"
 #import "Crashlytics/Crashlytics/Models/FIRCLSFileManager.h"
 #import "Crashlytics/Crashlytics/Models/FIRCLSInternalReport.h"
+#import "Crashlytics/Crashlytics/Private/FIRCrashlyticsReport_Private.h"
+#import "Crashlytics/Crashlytics/Public/FirebaseCrashlytics/FIRCrashlyticsReport.h"
 
 @interface FIRCLSExistingReportManager ()
 
@@ -27,6 +28,12 @@
 @property(nonatomic, strong) NSOperationQueue *operationQueue;
 @property(nonatomic, strong) FIRCLSReportUploader *reportUploader;
 
+// This list of active reports excludes the brand new active report that will be created this run of
+// the app.
+@property(nonatomic, strong) NSArray *existingUnemptyActiveReportPaths;
+@property(nonatomic, strong) NSArray *processingReportPaths;
+@property(nonatomic, strong) NSArray *preparedReportPaths;
+
 @end
 
 @implementation FIRCLSExistingReportManager
@@ -45,40 +52,95 @@
   return self;
 }
 
-/**
- * Returns the number of unsent reports on the device, including the ones passed in.
- */
-- (int)unsentReportsCountWithPreexisting:(NSArray<NSString *> *)paths {
-  int count = [self countSubmittableAndDeleteUnsubmittableReportPaths:paths];
+NSInteger compareOlder(FIRCLSInternalReport *reportA,
+                       FIRCLSInternalReport *reportB,
+                       void *context) {
+  return [reportA.dateCreated compare:reportB.dateCreated];
+}
+
+- (void)collectExistingReports {
+  self.existingUnemptyActiveReportPaths =
+      [self getUnemptyExistingActiveReportsAndDeleteEmpty:self.fileManager.activePathContents];
+  self.processingReportPaths = self.fileManager.processingPathContents;
+  self.preparedReportPaths = self.fileManager.preparedPathContents;
+}
+
+- (FIRCrashlyticsReport *)newestUnsentReport {
+  if (self.unsentReportsCount <= 0) {
+    return nil;
+  }
+
+  NSMutableArray<NSString *> *allReportPaths =
+      [NSMutableArray arrayWithArray:self.existingUnemptyActiveReportPaths];
 
-  count += self.fileManager.processingPathContents.count;
-  count += self.fileManager.preparedPathContents.count;
-  return count;
+  NSMutableArray<FIRCLSInternalReport *> *validReports = [NSMutableArray array];
+  for (NSString *path in allReportPaths) {
+    FIRCLSInternalReport *_Nullable report = [FIRCLSInternalReport reportWithPath:path];
+    if (!report) {
+      continue;
+    }
+    [validReports addObject:report];
+  }
+
+  [validReports sortUsingFunction:compareOlder context:nil];
+
+  FIRCLSInternalReport *_Nullable internalReport = [validReports lastObject];
+  return [[FIRCrashlyticsReport alloc] initWithInternalReport:internalReport];
+}
+
+- (NSUInteger)unsentReportsCount {
+  // There are nuances about why we only count active reports.
+  // See the header comment for more information.
+  return self.existingUnemptyActiveReportPaths.count;
 }
 
-- (int)countSubmittableAndDeleteUnsubmittableReportPaths:(NSArray *)reportPaths {
-  int count = 0;
+- (NSArray *)getUnemptyExistingActiveReportsAndDeleteEmpty:(NSArray *)reportPaths {
+  NSMutableArray *unemptyReports = [NSMutableArray array];
   for (NSString *path in reportPaths) {
     FIRCLSInternalReport *report = [FIRCLSInternalReport reportWithPath:path];
-    if ([report needsToBeSubmitted]) {
-      count++;
+    if ([report hasAnyEvents]) {
+      [unemptyReports addObject:path];
     } else {
       [self.operationQueue addOperationWithBlock:^{
-        [self->_fileManager removeItemAtPath:path];
+        [self.fileManager removeItemAtPath:path];
       }];
     }
   }
-  return count;
+  return unemptyReports;
 }
 
-- (void)processExistingReportPaths:(NSArray *)reportPaths
-               dataCollectionToken:(FIRCLSDataCollectionToken *)dataCollectionToken
+- (void)sendUnsentReportsWithToken:(FIRCLSDataCollectionToken *)dataCollectionToken
                           asUrgent:(BOOL)urgent {
-  for (NSString *path in reportPaths) {
+  for (NSString *path in self.existingUnemptyActiveReportPaths) {
     [self processExistingActiveReportPath:path
                       dataCollectionToken:dataCollectionToken
                                  asUrgent:urgent];
   }
+
+  // deal with stuff in processing more carefully - do not process again
+  [self.operationQueue addOperationWithBlock:^{
+    for (NSString *path in self.processingReportPaths) {
+      FIRCLSInternalReport *report = [FIRCLSInternalReport reportWithPath:path];
+      [self.reportUploader prepareAndSubmitReport:report
+                              dataCollectionToken:dataCollectionToken
+                                         asUrgent:NO
+                                   withProcessing:NO];
+    }
+  }];
+
+  // Because this could happen quite a bit after the inital set of files was
+  // captured, some could be completed (deleted). So, just double-check to make sure
+  // the file still exists.
+  [self.operationQueue addOperationWithBlock:^{
+    for (NSString *path in self.preparedReportPaths) {
+      if (![[self.fileManager underlyingFileManager] fileExistsAtPath:path]) {
+        continue;
+      }
+      [self.reportUploader uploadPackagedReportAtPath:path
+                                  dataCollectionToken:dataCollectionToken
+                                             asUrgent:NO];
+    }
+  }];
 }
 
 - (void)processExistingActiveReportPath:(NSString *)path
@@ -86,10 +148,10 @@
                                asUrgent:(BOOL)urgent {
   FIRCLSInternalReport *report = [FIRCLSInternalReport reportWithPath:path];
 
-  // TODO: needsToBeSubmitted should really be called on the background queue.
-  if (![report needsToBeSubmitted]) {
+  // TODO: hasAnyEvents should really be called on the background queue.
+  if (![report hasAnyEvents]) {
     [self.operationQueue addOperationWithBlock:^{
-      [self->_fileManager removeItemAtPath:path];
+      [self.fileManager removeItemAtPath:path];
     }];
 
     return;
@@ -104,11 +166,6 @@
     return;
   }
 
-  [self submitReport:report dataCollectionToken:dataCollectionToken];
-}
-
-- (void)submitReport:(FIRCLSInternalReport *)report
-    dataCollectionToken:(FIRCLSDataCollectionToken *)dataCollectionToken {
   [self.operationQueue addOperationWithBlock:^{
     [self.reportUploader prepareAndSubmitReport:report
                             dataCollectionToken:dataCollectionToken
@@ -117,15 +174,12 @@
   }];
 }
 
-// This is the side-effect of calling deleteUnsentReports, or collect_reports setting
-// being false
-- (void)deleteUnsentReportsWithPreexisting:(NSArray *)preexistingReportPaths {
-  [self removeExistingReportPaths:preexistingReportPaths];
-  [self removeExistingReportPaths:self.fileManager.processingPathContents];
-  [self removeExistingReportPaths:self.fileManager.preparedPathContents];
-}
+- (void)deleteUnsentReports {
+  NSArray<NSString *> *reportPaths = @[];
+  reportPaths = [reportPaths arrayByAddingObjectsFromArray:self.existingUnemptyActiveReportPaths];
+  reportPaths = [reportPaths arrayByAddingObjectsFromArray:self.processingReportPaths];
+  reportPaths = [reportPaths arrayByAddingObjectsFromArray:self.preparedReportPaths];
 
-- (void)removeExistingReportPaths:(NSArray *)reportPaths {
   [self.operationQueue addOperationWithBlock:^{
     for (NSString *path in reportPaths) {
       [self.fileManager removeItemAtPath:path];
@@ -133,45 +187,4 @@
   }];
 }
 
-- (void)handleContentsInOtherReportingDirectoriesWithToken:(FIRCLSDataCollectionToken *)token {
-  [self handleExistingFilesInProcessingWithToken:token];
-  [self handleExistingFilesInPreparedWithToken:token];
-}
-
-- (void)handleExistingFilesInProcessingWithToken:(FIRCLSDataCollectionToken *)token {
-  NSArray *processingPaths = _fileManager.processingPathContents;
-
-  // deal with stuff in processing more carefully - do not process again
-  [self.operationQueue addOperationWithBlock:^{
-    for (NSString *path in processingPaths) {
-      FIRCLSInternalReport *report = [FIRCLSInternalReport reportWithPath:path];
-      [self.reportUploader prepareAndSubmitReport:report
-                              dataCollectionToken:token
-                                         asUrgent:NO
-                                   withProcessing:NO];
-    }
-  }];
-}
-
-- (void)handleExistingFilesInPreparedWithToken:(FIRCLSDataCollectionToken *)token {
-  NSArray *preparedPaths = self.fileManager.preparedPathContents;
-  [self.operationQueue addOperationWithBlock:^{
-    [self uploadPreexistingFiles:preparedPaths withToken:token];
-  }];
-}
-
-- (void)uploadPreexistingFiles:(NSArray *)files withToken:(FIRCLSDataCollectionToken *)token {
-  // Because this could happen quite a bit after the inital set of files was
-  // captured, some could be completed (deleted). So, just double-check to make sure
-  // the file still exists.
-
-  for (NSString *path in files) {
-    if (![[_fileManager underlyingFileManager] fileExistsAtPath:path]) {
-      continue;
-    }
-
-    [self.reportUploader uploadPackagedReportAtPath:path dataCollectionToken:token asUrgent:NO];
-  }
-}
-
 @end

+ 1 - 1
Crashlytics/Crashlytics/Controllers/FIRCLSReportManager.h

@@ -36,7 +36,7 @@ NS_ASSUME_NONNULL_BEGIN
 
 - (FBLPromise<NSNumber *> *)startWithProfilingMark:(FIRCLSProfileMark)mark;
 
-- (FBLPromise<NSNumber *> *)checkForUnsentReports;
+- (FBLPromise<FIRCrashlyticsReport *> *)checkForUnsentReports;
 - (FBLPromise *)sendUnsentReports;
 - (FBLPromise *)deleteUnsentReports;
 

+ 42 - 64
Crashlytics/Crashlytics/Controllers/FIRCLSReportManager.m

@@ -93,11 +93,6 @@ typedef NSNumber FIRCLSWrappedReportAction;
 }
 @end
 
-/**
- * This is a helper to make code using NSNumber for bools more readable.
- */
-typedef NSNumber FIRCLSWrappedBool;
-
 @interface FIRCLSReportManager () {
   FIRCLSFileManager *_fileManager;
   dispatch_queue_t _dispatchQueue;
@@ -106,7 +101,7 @@ typedef NSNumber FIRCLSWrappedBool;
 
   // A promise that will be resolved when unsent reports are found on the device, and
   // processReports: can be called to decide how to deal with them.
-  FBLPromise<FIRCLSWrappedBool *> *_unsentReportsAvailable;
+  FBLPromise<FIRCrashlyticsReport *> *_unsentReportsAvailable;
 
   // A promise that will be resolved when the user has provided an action that they want to perform
   // for all the unsent reports.
@@ -197,8 +192,8 @@ typedef NSNumber FIRCLSWrappedBool;
 //    2. The developer uses the processCrashReports API to indicate whether the report
 //       should be sent or deleted, at which point the promise will be resolved with the action.
 - (FBLPromise<FIRCLSWrappedReportAction *> *)waitForReportAction {
-  FIRCLSDebugLog(@"[Crashlytics:Crash] Notifying that unsent reports are available.");
-  [_unsentReportsAvailable fulfill:@YES];
+  FIRCrashlyticsReport *unsentReport = self.existingReportManager.newestUnsentReport;
+  [_unsentReportsAvailable fulfill:unsentReport];
 
   // If data collection gets enabled while we are waiting for an action, go ahead and send the
   // reports, and any subsequent explicit response will be ignored.
@@ -208,16 +203,16 @@ typedef NSNumber FIRCLSWrappedBool;
             return @(FIRCLSReportActionSend);
           }];
 
-  FIRCLSDebugLog(@"[Crashlytics:Crash] Waiting for send/deleteUnsentReports to be called.");
   // Wait for either the processReports callback to be called, or data collection to be enabled.
   return [FBLPromise race:@[ collectionEnabled, _reportActionProvided ]];
 }
 
-- (FBLPromise<FIRCLSWrappedBool *> *)checkForUnsentReports {
+- (FBLPromise<FIRCrashlyticsReport *> *)checkForUnsentReports {
   bool expectedCalled = NO;
   if (!atomic_compare_exchange_strong(&_checkForUnsentReportsCalled, &expectedCalled, YES)) {
-    FIRCLSErrorLog(@"checkForUnsentReports should only be called once per execution.");
-    return [FBLPromise resolvedWith:@NO];
+    FIRCLSErrorLog(@"Either checkForUnsentReports or checkAndUpdateUnsentReports should be called "
+                   @"once per execution.");
+    return [FBLPromise resolvedWith:nil];
   }
   return _unsentReportsAvailable;
 }
@@ -239,6 +234,10 @@ typedef NSNumber FIRCLSWrappedBool;
   NSTimeInterval currentTimestamp = [NSDate timeIntervalSinceReferenceDate];
   [self.settings reloadFromCacheWithGoogleAppID:self.googleAppID currentTimestamp:currentTimestamp];
 
+  // This needs to be called before the new report is created for
+  // this run of the app.
+  [self.existingReportManager collectExistingReports];
+
   if (![self validateAppIdentifiers]) {
     return [FBLPromise resolvedWith:@NO];
   }
@@ -251,9 +250,7 @@ typedef NSNumber FIRCLSWrappedBool;
     return [FBLPromise resolvedWith:@NO];
   }
 
-  // Grab existing reports
   BOOL launchFailure = [self.launchMarker checkForAndCreateLaunchMarker];
-  NSArray *preexistingReportPaths = _fileManager.activePathContents;
 
   FIRCLSInternalReport *report = [self setupCurrentReport:executionIdentifier];
   if (!report) {
@@ -282,56 +279,41 @@ typedef NSNumber FIRCLSWrappedBool;
 
     [self beginSettingsWithToken:dataCollectionToken];
 
-    [self beginReportUploadsWithToken:dataCollectionToken
-               preexistingReportPaths:preexistingReportPaths
-                         blockingSend:launchFailure];
+    [self beginReportUploadsWithToken:dataCollectionToken blockingSend:launchFailure];
 
     // If data collection is enabled, the SDK will not notify the user
     // when unsent reports are available, or respect Send / DeleteUnsentReports
-    [_unsentReportsAvailable fulfill:@NO];
+    [_unsentReportsAvailable fulfill:nil];
 
   } else {
     FIRCLSDebugLog(@"Automatic data collection is disabled.");
-
-    // TODO: This counting of the file system happens on the main thread. Now that some of the other
-    // work below has been made async and moved to the dispatch queue, maybe we can move this code
-    // to the dispatch queue as well.
-    int unsentReportsCount =
-        [self.existingReportManager unsentReportsCountWithPreexisting:preexistingReportPaths];
-    if (unsentReportsCount > 0) {
-      FIRCLSDebugLog(
-          @"[Crashlytics:Crash] %d unsent reports are available. Checking for upload permission.",
-          unsentReportsCount);
-      // Wait for an action to get sent, either from processReports: or automatic data collection.
-      promise = [[self waitForReportAction]
-          onQueue:_dispatchQueue
-             then:^id _Nullable(FIRCLSWrappedReportAction *_Nullable wrappedAction) {
-               // Process the actions for the reports on disk.
-               FIRCLSReportAction action = [wrappedAction reportActionValue];
-               if (action == FIRCLSReportActionSend) {
-                 FIRCLSDebugLog(@"Sending unsent reports.");
-                 FIRCLSDataCollectionToken *dataCollectionToken =
-                     [FIRCLSDataCollectionToken validToken];
-
-                 [self beginSettingsWithToken:dataCollectionToken];
-
-                 [self beginReportUploadsWithToken:dataCollectionToken
-                            preexistingReportPaths:preexistingReportPaths
-                                      blockingSend:NO];
-
-               } else if (action == FIRCLSReportActionDelete) {
-                 FIRCLSDebugLog(@"Deleting unsent reports.");
-                 [self.existingReportManager
-                     deleteUnsentReportsWithPreexisting:preexistingReportPaths];
-               } else {
-                 FIRCLSErrorLog(@"Unknown report action: %d", action);
-               }
-               return @(report != nil);
-             }];
-    } else {
-      FIRCLSDebugLog(@"[Crashlytics:Crash] There are no unsent reports.");
-      [_unsentReportsAvailable fulfill:@NO];
-    }
+    FIRCLSDebugLog(@"[Crashlytics:Crash] %d unsent reports are available. Waiting for "
+                   @"send/deleteUnsentReports to be called.",
+                   self.existingReportManager.unsentReportsCount);
+
+    // Wait for an action to get sent, either from processReports: or automatic data collection.
+    promise = [[self waitForReportAction]
+        onQueue:_dispatchQueue
+           then:^id _Nullable(FIRCLSWrappedReportAction *_Nullable wrappedAction) {
+             // Process the actions for the reports on disk.
+             FIRCLSReportAction action = [wrappedAction reportActionValue];
+             if (action == FIRCLSReportActionSend) {
+               FIRCLSDebugLog(@"Sending unsent reports.");
+               FIRCLSDataCollectionToken *dataCollectionToken =
+                   [FIRCLSDataCollectionToken validToken];
+
+               [self beginSettingsWithToken:dataCollectionToken];
+
+               [self beginReportUploadsWithToken:dataCollectionToken blockingSend:NO];
+
+             } else if (action == FIRCLSReportActionDelete) {
+               FIRCLSDebugLog(@"Deleting unsent reports.");
+               [self.existingReportManager deleteUnsentReports];
+             } else {
+               FIRCLSErrorLog(@"Unknown report action: %d", action);
+             }
+             return @(report != nil);
+           }];
   }
 
   if (report != nil) {
@@ -388,17 +370,13 @@ typedef NSNumber FIRCLSWrappedBool;
 }
 
 - (void)beginReportUploadsWithToken:(FIRCLSDataCollectionToken *)token
-             preexistingReportPaths:(NSArray *)preexistingReportPaths
                        blockingSend:(BOOL)blockingSend {
   if (self.settings.collectReportsEnabled) {
-    [self.existingReportManager processExistingReportPaths:preexistingReportPaths
-                                       dataCollectionToken:token
-                                                  asUrgent:blockingSend];
-    [self.existingReportManager handleContentsInOtherReportingDirectoriesWithToken:token];
+    [self.existingReportManager sendUnsentReportsWithToken:token asUrgent:blockingSend];
 
   } else {
     FIRCLSInfoLog(@"Collect crash reports is disabled");
-    [self.existingReportManager deleteUnsentReportsWithPreexisting:preexistingReportPaths];
+    [self.existingReportManager deleteUnsentReports];
   }
 }
 

+ 14 - 5
Crashlytics/Crashlytics/FIRCrashlytics.m

@@ -31,7 +31,6 @@
 #include "Crashlytics/Crashlytics/Helpers/FIRCLSProfiling.h"
 #include "Crashlytics/Crashlytics/Helpers/FIRCLSUtility.h"
 #import "Crashlytics/Crashlytics/Models/FIRCLSFileManager.h"
-#import "Crashlytics/Crashlytics/Models/FIRCLSReport_Private.h"
 #import "Crashlytics/Crashlytics/Models/FIRCLSSettings.h"
 #import "Crashlytics/Crashlytics/Settings/Models/FIRCLSApplicationIdentifierModel.h"
 
@@ -271,10 +270,20 @@ NSString *const FIRCLSGoogleTransportMappingID = @"1206";
 #pragma mark - API: Accessors
 
 - (void)checkForUnsentReportsWithCompletion:(void (^)(BOOL))completion {
-  [[self.reportManager checkForUnsentReports] then:^id _Nullable(NSNumber *_Nullable value) {
-    completion([value boolValue]);
-    return nil;
-  }];
+  [[self.reportManager checkForUnsentReports]
+      then:^id _Nullable(FIRCrashlyticsReport *_Nullable value) {
+        completion(value ? true : false);
+        return nil;
+      }];
+}
+
+- (void)checkAndUpdateUnsentReportsWithCompletion:
+    (void (^)(FIRCrashlyticsReport *_Nonnull))completion {
+  [[self.reportManager checkForUnsentReports]
+      then:^id _Nullable(FIRCrashlyticsReport *_Nullable value) {
+        completion(value);
+        return nil;
+      }];
 }
 
 - (void)sendUnsentReports {

+ 197 - 0
Crashlytics/Crashlytics/FIRCrashlyticsReport.m

@@ -0,0 +1,197 @@
+// Copyright 2021 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/Public/FirebaseCrashlytics/FIRCrashlyticsReport.h"
+
+#import "Crashlytics/Crashlytics/Components/FIRCLSContext.h"
+#import "Crashlytics/Crashlytics/Components/FIRCLSGlobals.h"
+#import "Crashlytics/Crashlytics/Helpers/FIRCLSLogger.h"
+#import "Crashlytics/Crashlytics/Models/FIRCLSInternalReport.h"
+
+@interface FIRCrashlyticsReport () {
+  NSString *_reportID;
+  NSDate *_dateCreated;
+  BOOL _hasCrash;
+
+  FIRCLSUserLoggingABStorage _logStorage;
+  const char *_activeLogPath;
+
+  uint32_t _internalKVCounter;
+  FIRCLSUserLoggingKVStorage _internalKVStorage;
+
+  uint32_t _userKVCounter;
+  FIRCLSUserLoggingKVStorage _userKVStorage;
+}
+
+@property(nonatomic, strong) FIRCLSInternalReport *internalReport;
+
+@end
+
+@implementation FIRCrashlyticsReport
+
+- (instancetype)initWithInternalReport:(FIRCLSInternalReport *)internalReport {
+  self = [super init];
+  if (!self) {
+    return nil;
+  }
+
+  _internalReport = internalReport;
+  _reportID = [[internalReport identifier] copy];
+  _dateCreated = [[internalReport dateCreated] copy];
+  _hasCrash = [internalReport isCrash];
+
+  _logStorage.maxSize = _firclsContext.readonly->logging.logStorage.maxSize;
+  _logStorage.maxEntries = _firclsContext.readonly->logging.logStorage.maxEntries;
+  _logStorage.restrictBySize = _firclsContext.readonly->logging.logStorage.restrictBySize;
+  _logStorage.entryCount = _firclsContext.readonly->logging.logStorage.entryCount;
+  _logStorage.aPath = [FIRCrashlyticsReport filesystemPathForContentFile:FIRCLSReportLogAFile
+                                                        inInternalReport:internalReport];
+  _logStorage.bPath = [FIRCrashlyticsReport filesystemPathForContentFile:FIRCLSReportLogBFile
+                                                        inInternalReport:internalReport];
+
+  _activeLogPath = _logStorage.aPath;
+
+  // TODO: correct kv accounting
+  // The internal report will have non-zero compacted and incremental keys. The right thing to do
+  // is count them, so we can kick off compactions/pruning at the right times. By
+  // setting this value to zero, we're allowing more entries to be made than there really
+  // should be. Not the end of the world, but we should do better eventually.
+  _internalKVCounter = 0;
+  _userKVCounter = 0;
+
+  _userKVStorage.maxCount = _firclsContext.readonly->logging.userKVStorage.maxCount;
+  _userKVStorage.maxIncrementalCount =
+      _firclsContext.readonly->logging.userKVStorage.maxIncrementalCount;
+  _userKVStorage.compactedPath =
+      [FIRCrashlyticsReport filesystemPathForContentFile:FIRCLSReportUserCompactedKVFile
+                                        inInternalReport:internalReport];
+  _userKVStorage.incrementalPath =
+      [FIRCrashlyticsReport filesystemPathForContentFile:FIRCLSReportUserIncrementalKVFile
+                                        inInternalReport:internalReport];
+
+  _internalKVStorage.maxCount = _firclsContext.readonly->logging.internalKVStorage.maxCount;
+  _internalKVStorage.maxIncrementalCount =
+      _firclsContext.readonly->logging.internalKVStorage.maxIncrementalCount;
+  _internalKVStorage.compactedPath =
+      [FIRCrashlyticsReport filesystemPathForContentFile:FIRCLSReportInternalCompactedKVFile
+                                        inInternalReport:internalReport];
+  _internalKVStorage.incrementalPath =
+      [FIRCrashlyticsReport filesystemPathForContentFile:FIRCLSReportInternalIncrementalKVFile
+                                        inInternalReport:internalReport];
+
+  return self;
+}
+
++ (const char *)filesystemPathForContentFile:(NSString *)contentFile
+                            inInternalReport:(FIRCLSInternalReport *)internalReport {
+  if (!internalReport) {
+    return nil;
+  }
+
+  // We need to be defensive because strdup will crash
+  // if given a nil.
+  NSString *objCString = [internalReport pathForContentFile:contentFile];
+  const char *fileSystemString = [objCString fileSystemRepresentation];
+  if (!objCString || !fileSystemString) {
+    return nil;
+  }
+
+  // Paths need to be duplicated because fileSystemRepresentation returns C strings
+  // that are freed outside of this context.
+  return strdup(fileSystemString);
+}
+
+- (BOOL)checkContextForMethod:(NSString *)methodName {
+  if (!FIRCLSContextIsInitialized()) {
+    FIRCLSErrorLog(@"%@ failed for FIRCrashlyticsReport because Crashlytics context isn't "
+                   @"initialized.",
+                   methodName);
+    return false;
+  }
+  return true;
+}
+
+#pragma mark - API: Getters
+
+- (NSString *)reportID {
+  return _reportID;
+}
+
+- (NSDate *)dateCreated {
+  return _dateCreated;
+}
+
+- (BOOL)hasCrash {
+  return _hasCrash;
+}
+
+#pragma mark - API: Logging
+
+- (void)log:(NSString *)msg {
+  if (![self checkContextForMethod:@"log:"]) {
+    return;
+  }
+
+  FIRCLSLogToStorage(&_logStorage, &_activeLogPath, @"%@", msg);
+}
+
+- (void)logWithFormat:(NSString *)format, ... {
+  if (![self checkContextForMethod:@"logWithFormat:"]) {
+    return;
+  }
+
+  va_list args;
+  va_start(args, format);
+  [self logWithFormat:format arguments:args];
+  va_end(args);
+}
+
+- (void)logWithFormat:(NSString *)format arguments:(va_list)args {
+  if (![self checkContextForMethod:@"logWithFormat:arguments:"]) {
+    return;
+  }
+
+  [self log:[[NSString alloc] initWithFormat:format arguments:args]];
+}
+
+#pragma mark - API: setUserID
+
+- (void)setUserID:(NSString *)userID {
+  if (![self checkContextForMethod:@"setUserID:"]) {
+    return;
+  }
+
+  FIRCLSUserLoggingRecordKeyValue(FIRCLSUserIdentifierKey, userID, &_internalKVStorage,
+                                  &_internalKVCounter);
+}
+
+#pragma mark - API: setCustomValue
+
+- (void)setCustomValue:(id)value forKey:(NSString *)key {
+  if (![self checkContextForMethod:@"setCustomValue:forKey:"]) {
+    return;
+  }
+
+  FIRCLSUserLoggingRecordKeyValue(key, value, &_userKVStorage, &_userKVCounter);
+}
+
+- (void)setCustomKeysAndValues:(NSDictionary *)keysAndValues {
+  if (![self checkContextForMethod:@"setCustomKeysAndValues:"]) {
+    return;
+  }
+
+  FIRCLSUserLoggingRecordKeysAndValues(keysAndValues, &_userKVStorage, &_userKVCounter);
+}
+
+@end

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

@@ -49,7 +49,7 @@ extern NSString *const FIRCLSReportUserCompactedKVFile;
 
 @property(nonatomic, copy, readonly) NSString *directoryName;
 @property(nonatomic, copy) NSString *path;
-@property(nonatomic, assign, readonly) BOOL needsToBeSubmitted;
+@property(nonatomic, assign, readonly) BOOL hasAnyEvents;
 
 // content paths
 @property(nonatomic, copy, readonly) NSString *binaryImagePath;

+ 4 - 4
Crashlytics/Crashlytics/Models/FIRCLSInternalReport.m

@@ -105,7 +105,7 @@ NSString *const FIRCLSReportUserCompactedKVFile = @"user_compacted_kv.clsrecord"
 }
 
 #pragma mark - Processing Methods
-- (BOOL)needsToBeSubmitted {
+- (BOOL)hasAnyEvents {
   NSArray *reportFiles = @[
     FIRCLSReportExceptionFile, FIRCLSReportSignalFile, FIRCLSReportCustomExceptionAFile,
     FIRCLSReportCustomExceptionBFile,
@@ -114,7 +114,7 @@ NSString *const FIRCLSReportUserCompactedKVFile = @"user_compacted_kv.clsrecord"
 #endif
     FIRCLSReportErrorAFile, FIRCLSReportErrorBFile
   ];
-  return [self checkExistenceOfAtLeastOnceFileInArray:reportFiles];
+  return [self checkExistenceOfAtLeastOneFileInArray:reportFiles];
 }
 
 // These are purposefully in order of precedence. If duplicate data exists
@@ -140,10 +140,10 @@ NSString *const FIRCLSReportUserCompactedKVFile = @"user_compacted_kv.clsrecord"
 
 - (BOOL)isCrash {
   NSArray *crashFiles = [FIRCLSInternalReport crashFileNames];
-  return [self checkExistenceOfAtLeastOnceFileInArray:crashFiles];
+  return [self checkExistenceOfAtLeastOneFileInArray:crashFiles];
 }
 
-- (BOOL)checkExistenceOfAtLeastOnceFileInArray:(NSArray *)files {
+- (BOOL)checkExistenceOfAtLeastOneFileInArray:(NSArray *)files {
   NSFileManager *manager = [NSFileManager defaultManager];
 
   for (NSString *fileName in files) {

+ 0 - 110
Crashlytics/Crashlytics/Models/FIRCLSReport.h

@@ -1,110 +0,0 @@
-// Copyright 2019 Google
-//
-// 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_ASSUME_NONNULL_BEGIN
-
-/**
- * The CLSCrashReport protocol is deprecated. See the CLSReport class and the CrashyticsDelegate
- * changes for details.
- **/
-@protocol FIRCLSCrashReport <NSObject>
-
-@property(nonatomic, copy, readonly) NSString *identifier;
-@property(nonatomic, copy, readonly) NSDictionary *customKeys;
-@property(nonatomic, copy, readonly) NSString *bundleVersion;
-@property(nonatomic, copy, readonly) NSString *bundleShortVersionString;
-@property(nonatomic, readonly, nullable) NSDate *crashedOnDate;
-@property(nonatomic, copy, readonly) NSString *OSVersion;
-@property(nonatomic, copy, readonly) NSString *OSBuildVersion;
-
-@end
-
-/**
- * The CLSReport exposes an interface to the phsyical report that Crashlytics has created. You can
- * use this class to get information about the event, and can also set some values after the
- * event has occurred.
- **/
-@interface FIRCLSReport : NSObject <FIRCLSCrashReport>
-
-- (instancetype)init NS_UNAVAILABLE;
-+ (instancetype)new NS_UNAVAILABLE;
-
-/**
- * Returns the session identifier for the report.
- **/
-@property(nonatomic, copy, readonly) NSString *identifier;
-
-/**
- * Returns the custom key value data for the report.
- **/
-@property(nonatomic, copy, readonly) NSDictionary *customKeys;
-
-/**
- * Returns the CFBundleVersion of the application that generated the report.
- **/
-@property(nonatomic, copy, readonly) NSString *bundleVersion;
-
-/**
- * Returns the CFBundleShortVersionString of the application that generated the report.
- **/
-@property(nonatomic, copy, readonly) NSString *bundleShortVersionString;
-
-/**
- * Returns the date that the report was created.
- **/
-@property(nonatomic, copy, readonly) NSDate *dateCreated;
-
-/**
- * Returns the os version that the application crashed on.
- **/
-@property(nonatomic, copy, readonly) NSString *OSVersion;
-
-/**
- * Returns the os build version that the application crashed on.
- **/
-@property(nonatomic, copy, readonly) NSString *OSBuildVersion;
-
-/**
- * Returns YES if the report contains any crash information, otherwise returns NO.
- **/
-@property(nonatomic, assign, readonly) BOOL isCrash;
-
-/**
- * You can use this method to set, after the event, additional custom keys. The rules
- * and semantics for this method are the same as those documented in FIRCrashlytics.h. Be aware
- * that the maximum size and count of custom keys is still enforced, and you can overwrite keys
- * and/or cause excess keys to be deleted by using this method.
- **/
-- (void)setObjectValue:(nullable id)value forKey:(NSString *)key;
-
-/**
- * Record an application-specific user identifier. See FIRCrashlytics.h for details.
- **/
-@property(nonatomic, copy, nullable) NSString *userIdentifier;
-
-/**
- * Record a user name. See FIRCrashlytics.h for details.
- **/
-@property(nonatomic, copy, nullable) NSString *userName;
-
-/**
- * Record a user email. See FIRCrashlytics.h for details.
- **/
-@property(nonatomic, copy, nullable) NSString *userEmail;
-
-@end
-
-NS_ASSUME_NONNULL_END

+ 0 - 241
Crashlytics/Crashlytics/Models/FIRCLSReport.m

@@ -1,241 +0,0 @@
-// Copyright 2019 Google
-//
-// 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/Components/FIRCLSContext.h"
-#import "Crashlytics/Crashlytics/Components/FIRCLSGlobals.h"
-#import "Crashlytics/Crashlytics/Components/FIRCLSUserLogging.h"
-#import "Crashlytics/Crashlytics/Helpers/FIRCLSFile.h"
-#import "Crashlytics/Crashlytics/Models/FIRCLSInternalReport.h"
-#import "Crashlytics/Crashlytics/Models/FIRCLSReport_Private.h"
-
-@interface FIRCLSReport () {
-  FIRCLSInternalReport *_internalReport;
-  uint32_t _internalKVCounter;
-  uint32_t _userKVCounter;
-
-  NSString *_internalCompactedKVFile;
-  NSString *_internalIncrementalKVFile;
-  NSString *_userCompactedKVFile;
-  NSString *_userIncrementalKVFile;
-
-  BOOL _readOnly;
-
-  // cached values, to ensure that their contents remain valid
-  // even if the report is deleted
-  NSString *_identifer;
-  NSString *_bundleVersion;
-  NSString *_bundleShortVersionString;
-  NSDate *_dateCreated;
-  NSDate *_crashedOnDate;
-  NSString *_OSVersion;
-  NSString *_OSBuildVersion;
-  NSNumber *_isCrash;
-  NSDictionary *_customKeys;
-}
-
-@end
-
-@implementation FIRCLSReport
-
-- (instancetype)initWithInternalReport:(FIRCLSInternalReport *)report
-                          prefetchData:(BOOL)shouldPrefetch {
-  self = [super init];
-  if (!self) {
-    return nil;
-  }
-
-  _internalReport = report;
-
-  // TODO: correct kv accounting
-  // The internal report will have non-zero compacted and incremental keys. The right thing to do
-  // is count them, so we can kick off compactions/pruning at the right times. By
-  // setting this value to zero, we're allowing more entries to be made than there really
-  // should be. Not the end of the world, but we should do better eventually.
-  _internalKVCounter = 0;
-  _userKVCounter = 0;
-
-  _internalCompactedKVFile =
-      [self.internalReport pathForContentFile:FIRCLSReportInternalCompactedKVFile];
-  _internalIncrementalKVFile =
-      [self.internalReport pathForContentFile:FIRCLSReportInternalIncrementalKVFile];
-  _userCompactedKVFile = [self.internalReport pathForContentFile:FIRCLSReportUserCompactedKVFile];
-  _userIncrementalKVFile =
-      [self.internalReport pathForContentFile:FIRCLSReportUserIncrementalKVFile];
-
-  _readOnly = shouldPrefetch;
-
-  if (shouldPrefetch) {
-    _identifer = report.identifier;
-    _bundleVersion = report.bundleVersion;
-    _bundleShortVersionString = report.bundleShortVersionString;
-    _dateCreated = report.dateCreated;
-    _crashedOnDate = report.crashedOnDate;
-    _OSVersion = report.OSVersion;
-    _OSBuildVersion = report.OSBuildVersion;
-    _isCrash = [NSNumber numberWithBool:report.isCrash];
-
-    _customKeys = [self readCustomKeys];
-  }
-
-  return self;
-}
-
-- (instancetype)initWithInternalReport:(FIRCLSInternalReport *)report {
-  return [self initWithInternalReport:report prefetchData:NO];
-}
-
-#pragma mark - Helpers
-- (FIRCLSUserLoggingKVStorage)internalKVStorage {
-  FIRCLSUserLoggingKVStorage storage;
-
-  storage.maxCount = _firclsContext.readonly->logging.internalKVStorage.maxCount;
-  storage.maxIncrementalCount =
-      _firclsContext.readonly->logging.internalKVStorage.maxIncrementalCount;
-  storage.compactedPath = [_internalCompactedKVFile fileSystemRepresentation];
-  storage.incrementalPath = [_internalIncrementalKVFile fileSystemRepresentation];
-
-  return storage;
-}
-
-- (FIRCLSUserLoggingKVStorage)userKVStorage {
-  FIRCLSUserLoggingKVStorage storage;
-
-  storage.maxCount = _firclsContext.readonly->logging.userKVStorage.maxCount;
-  storage.maxIncrementalCount = _firclsContext.readonly->logging.userKVStorage.maxIncrementalCount;
-  storage.compactedPath = [_userCompactedKVFile fileSystemRepresentation];
-  storage.incrementalPath = [_userIncrementalKVFile fileSystemRepresentation];
-
-  return storage;
-}
-
-- (BOOL)canRecordNewValues {
-  return !_readOnly && FIRCLSContextIsInitialized();
-}
-
-- (void)recordValue:(id)value forInternalKey:(NSString *)key {
-  if (!self.canRecordNewValues) {
-    return;
-  }
-
-  FIRCLSUserLoggingKVStorage storage = [self internalKVStorage];
-
-  FIRCLSUserLoggingRecordKeyValue(key, value, &storage, &_internalKVCounter);
-}
-
-- (void)recordValue:(id)value forUserKey:(NSString *)key {
-  if (!self.canRecordNewValues) {
-    return;
-  }
-
-  FIRCLSUserLoggingKVStorage storage = [self userKVStorage];
-
-  FIRCLSUserLoggingRecordKeyValue(key, value, &storage, &_userKVCounter);
-}
-
-- (NSDictionary *)readCustomKeys {
-  FIRCLSUserLoggingKVStorage storage = [self userKVStorage];
-
-  // return decoded entries
-  return FIRCLSUserLoggingGetCompactedKVEntries(&storage, true);
-}
-
-#pragma mark - Metadata helpers
-
-- (NSString *)identifier {
-  if (!_identifer) {
-    _identifer = self.internalReport.identifier;
-  }
-
-  return _identifer;
-}
-
-- (NSDictionary *)customKeys {
-  if (!_customKeys) {
-    _customKeys = [self readCustomKeys];
-  }
-
-  return _customKeys;
-}
-
-- (NSString *)bundleVersion {
-  if (!_bundleVersion) {
-    _bundleVersion = self.internalReport.bundleVersion;
-  }
-
-  return _bundleVersion;
-}
-
-- (NSString *)bundleShortVersionString {
-  if (!_bundleShortVersionString) {
-    _bundleShortVersionString = self.internalReport.bundleShortVersionString;
-  }
-
-  return _bundleShortVersionString;
-}
-
-- (NSDate *)dateCreated {
-  if (!_dateCreated) {
-    _dateCreated = self.internalReport.dateCreated;
-  }
-
-  return _dateCreated;
-}
-
-// for compatibility with the CLSCrashReport Protocol
-- (NSDate *)crashedOnDate {
-  if (!_crashedOnDate) {
-    _crashedOnDate = self.internalReport.crashedOnDate;
-  }
-
-  return _crashedOnDate;
-}
-
-- (NSString *)OSVersion {
-  if (!_OSVersion) {
-    _OSVersion = self.internalReport.OSVersion;
-  }
-
-  return _OSVersion;
-}
-
-- (NSString *)OSBuildVersion {
-  if (!_OSBuildVersion) {
-    _OSBuildVersion = self.internalReport.OSBuildVersion;
-  }
-
-  return _OSBuildVersion;
-}
-
-- (BOOL)isCrash {
-  if (_isCrash == nil) {
-    _isCrash = [NSNumber numberWithBool:self.internalReport.isCrash];
-  }
-
-  return [_isCrash boolValue];
-}
-
-#pragma mark - Public Read/Write Methods
-- (void)setObjectValue:(id)value forKey:(NSString *)key {
-  [self recordValue:value forUserKey:key];
-}
-
-- (NSString *)userIdentifier {
-  return nil;
-}
-
-- (void)setUserIdentifier:(NSString *)userIdentifier {
-  [self recordValue:userIdentifier forInternalKey:FIRCLSUserIdentifierKey];
-}
-
-@end

+ 0 - 27
Crashlytics/Crashlytics/Models/FIRCLSReport_Private.h

@@ -1,27 +0,0 @@
-// Copyright 2019 Google
-//
-// 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/FIRCLSReport.h"
-
-@class FIRCLSInternalReport;
-
-@interface FIRCLSReport ()
-
-- (instancetype)initWithInternalReport:(FIRCLSInternalReport *)report
-                          prefetchData:(BOOL)shouldPrefetch NS_DESIGNATED_INITIALIZER;
-- (instancetype)initWithInternalReport:(FIRCLSInternalReport *)report;
-
-@property(nonatomic, strong, readonly) FIRCLSInternalReport *internalReport;
-
-@end

+ 14 - 7
FirebasePerformance/Tests/Unit/Configurations/FPRFakeRemoteConfigFlags.h → Crashlytics/Crashlytics/Private/FIRCrashlyticsReport_Private.h

@@ -1,4 +1,4 @@
-// Copyright 2020 Google LLC
+// Copyright 2021 Google LLC
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,17 +12,24 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#import <Foundation/Foundation.h>
-#import "FirebasePerformance/Sources/Configurations/FPRRemoteConfigFlags+Private.h"
-#import "FirebasePerformance/Sources/Configurations/FPRRemoteConfigFlags.h"
+#ifndef FIRCrashlyticsReport_Private_h
+#define FIRCrashlyticsReport_Private_h
+
+#import "Crashlytics/Crashlytics/Public/FirebaseCrashlytics/FIRCrashlyticsReport.h"
 
 NS_ASSUME_NONNULL_BEGIN
 
-@interface FPRFakeRemoteConfigFlags : FPRRemoteConfigFlags
+/**
+ * Internal initializer because this object is created by the SDK.
+ **/
+@interface FIRCrashlyticsReport (Private)
+
+- (instancetype)initWithInternalReport:(FIRCLSInternalReport *)internalReport;
 
-/** A fake representing whether any RC flag value exists. */
-@property(nonatomic) BOOL containsRemoteConfigFlagValues;
+@property(nonatomic, strong) FIRCLSInternalReport *internalReport;
 
 @end
 
 NS_ASSUME_NONNULL_END
+
+#endif /* FIRCrashlyticsReport_Private_h */

+ 29 - 0
Crashlytics/Crashlytics/Public/FirebaseCrashlytics/FIRCrashlytics.h

@@ -14,6 +14,7 @@
 
 #import <Foundation/Foundation.h>
 
+#import "FIRCrashlyticsReport.h"
 #import "FIRExceptionModel.h"
 
 #if __has_include(<Crashlytics/Crashlytics.h>)
@@ -179,6 +180,34 @@ NS_SWIFT_NAME(Crashlytics)
 - (void)checkForUnsentReportsWithCompletion:(void (^)(BOOL))completion
     NS_SWIFT_NAME(checkForUnsentReports(completion:));
 
+/**
+ * Determines whether there are any unsent crash reports cached on the device, then calls the given
+ * callback with a CrashlyticsReport object that you can use to update the unsent report.
+ * CrashlyticsReports have a lot of the familiar Crashlytics methods like setting custom keys and
+ * logs.
+ *
+ * The callback only executes if automatic data collection is disabled. You can use
+ * the callback to get one-time consent from a user upon a crash, and then call
+ * sendUnsentReports or deleteUnsentReports, depending on whether or not the user gives consent.
+ *
+ * Disable automatic collection by:
+ *  - Adding the FirebaseCrashlyticsCollectionEnabled: NO key to your App's Info.plist
+ *  - Calling [[FIRCrashlytics crashlytics] setCrashlyticsCollectionEnabled:NO] in your app
+ *  - Setting FIRApp's isDataCollectionDefaultEnabled to NO
+ *
+ * Not calling send/deleteUnsentReports will result in the report staying on disk, which means the
+ * same CrashlyticsReport can show up in multiple runs of the app. If you want avoid duplicates,
+ * ensure there was a crash on the last run of the app by checking the value of
+ * didCrashDuringPreviousExecution.
+ *
+ * @param completion The callback that's executed once Crashlytics finishes checking for unsent
+ * reports. The callback is called with the newest unsent Crashlytics Report, or nil if there are
+ * none cached on disk.
+ */
+- (void)checkAndUpdateUnsentReportsWithCompletion:
+    (void (^)(FIRCrashlyticsReport *_Nullable))completion
+    NS_SWIFT_NAME(checkAndUpdateUnsentReports(completion:));
+
 /**
  * Enqueues any unsent reports on the device to upload to Crashlytics.
  *

+ 108 - 0
Crashlytics/Crashlytics/Public/FirebaseCrashlytics/FIRCrashlyticsReport.h

@@ -0,0 +1,108 @@
+// Copyright 2021 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_ASSUME_NONNULL_BEGIN
+
+/**
+ * The Firebase Crashlytics Report provides a way to read and write information
+ * to a past Crashlytics reports. A common use case is gathering end-user feedback
+ * on the next run of the app.
+ *
+ * The CrashlyticsReport should be modified before calling send/deleteUnsentReports.
+ */
+NS_SWIFT_NAME(CrashlyticsReport)
+@interface FIRCrashlyticsReport : NSObject
+
+/** :nodoc: */
+- (instancetype)init NS_UNAVAILABLE;
+
+/**
+ * Returns the unique ID for the Crashlytics report.
+ */
+@property(nonatomic, readonly) NSString *reportID;
+
+/**
+ * Returns the date that the report was created.
+ */
+@property(nonatomic, readonly) NSDate *dateCreated;
+
+/**
+ * Returns true when one of the events in the Crashlytics report is a crash.
+ */
+@property(nonatomic, readonly) BOOL hasCrash;
+
+/**
+ * Adds logging that is sent with your crash data. The logging does not appear  in the
+ * system.log and is only visible in the Crashlytics dashboard.
+ *
+ * @param msg Message to log
+ */
+- (void)log:(NSString *)msg;
+
+/**
+ * Adds logging that is sent with your crash data. The logging does not appear  in the
+ * system.log and is only visible in the Crashlytics dashboard.
+ *
+ * @param format Format of string
+ * @param ... A comma-separated list of arguments to substitute into format
+ */
+- (void)logWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1, 2);
+
+/**
+ * Adds logging that is sent with your crash data. The logging does not appear  in the
+ * system.log and is only visible in the Crashlytics dashboard.
+ *
+ * @param format Format of string
+ * @param args Arguments to substitute into format
+ */
+- (void)logWithFormat:(NSString *)format
+            arguments:(va_list)args NS_SWIFT_NAME(log(format:arguments:));
+
+/**
+ * Sets a custom key and value to be associated with subsequent fatal and non-fatal reports.
+ * When setting an object value, the object is converted to a string. This is
+ * typically done by calling "-[NSObject description]".
+ *
+ * @param value The value to be associated with the key
+ * @param key A unique key
+ */
+- (void)setCustomValue:(id)value forKey:(NSString *)key;
+
+/**
+ * Sets custom keys and values to be associated with subsequent fatal and non-fatal reports.
+ * The objects in the dictionary are converted to strings. This is
+ * typically done by calling "-[NSObject description]".
+ *
+ * @param keysAndValues The values to be associated with the corresponding keys
+ */
+- (void)setCustomKeysAndValues:(NSDictionary *)keysAndValues;
+
+/**
+ * Records a user ID (identifier) that's associated with subsequent fatal and non-fatal reports.
+ *
+ * If you want to associate a crash with a specific user, we recommend specifying an arbitrary
+ * string (e.g., a database, ID, hash, or other value that you can index and query, but is
+ * meaningless to a third-party observer). This allows you to facilitate responses for support
+ * requests and reach out to users for more information.
+ *
+ * @param userID An arbitrary user identifier string that associates a user to a record in your
+ * system.
+ */
+- (void)setUserID:(NSString *)userID;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 1 - 0
Crashlytics/Crashlytics/Public/FirebaseCrashlytics/FirebaseCrashlytics.h

@@ -15,5 +15,6 @@
  */
 
 #import "FIRCrashlytics.h"
+#import "FIRCrashlyticsReport.h"
 #import "FIRExceptionModel.h"
 #import "FIRStackFrame.h"

+ 4 - 5
Crashlytics/UnitTests/FIRCLSInternalReportTests.m

@@ -47,24 +47,23 @@
   NSString *customAPath = [report pathForContentFile:FIRCLSReportCustomExceptionAFile];
   NSString *customBPath = [report pathForContentFile:FIRCLSReportCustomExceptionBFile];
 
-  XCTAssertFalse(report.needsToBeSubmitted, @"metadata only should not need to be submitted");
+  XCTAssertFalse(report.hasAnyEvents, @"metadata only should not need to be submitted");
 
   [[NSFileManager defaultManager] createFileAtPath:customAPath
                                           contents:[NSData data]
                                         attributes:nil];
 
-  XCTAssert(report.needsToBeSubmitted, @"with the A file present, needs to be submitted");
+  XCTAssert(report.hasAnyEvents, @"with the A file present, needs to be submitted");
 
   [[NSFileManager defaultManager] createFileAtPath:customBPath
                                           contents:[NSData data]
                                         attributes:nil];
 
   // with A and B, also needs
-  XCTAssert(report.needsToBeSubmitted,
-            @"with both the A and B files present, needs to be submitted");
+  XCTAssert(report.hasAnyEvents, @"with both the A and B files present, needs to be submitted");
 
   XCTAssert([[NSFileManager defaultManager] removeItemAtPath:customAPath error:nil]);
-  XCTAssert(report.needsToBeSubmitted, @"with the B file present, needs to be submitted");
+  XCTAssert(report.hasAnyEvents, @"with the B file present, needs to be submitted");
 }
 
 @end

+ 41 - 24
Crashlytics/UnitTests/FIRCLSReportManagerTests.m

@@ -230,17 +230,15 @@
   XCTestExpectation *processReportsComplete =
       [[XCTestExpectation alloc] initWithDescription:@"processReports: complete"];
   __block BOOL reportsAvailable = NO;
-  [[[self.reportManager checkForUnsentReports] then:^id _Nullable(NSNumber *_Nullable value) {
-    reportsAvailable = [value boolValue];
-    if (!reportsAvailable) {
-      return nil;
-    }
-    if (send) {
-      return [self->_reportManager sendUnsentReports];
-    } else {
-      return [self->_reportManager deleteUnsentReports];
-    }
-  }] then:^id _Nullable(id _Nullable ignored) {
+  [[[self.reportManager checkForUnsentReports]
+      then:^id _Nullable(FIRCrashlyticsReport *_Nullable report) {
+        reportsAvailable = report ? true : false;
+        if (send) {
+          return [self->_reportManager sendUnsentReports];
+        } else {
+          return [self->_reportManager deleteUnsentReports];
+        }
+      }] then:^id _Nullable(id _Nullable ignored) {
     [processReportsComplete fulfill];
     return nil;
   }];
@@ -257,12 +255,15 @@
 }
 
 - (void)testExistingUnimportantReportOnStart {
-  // create a report and put it in place
+  // Create a report representing the last run and put it in place
   [self createActiveReport];
 
-  // Report should get deleted, and nothing else specials should happen.
+  // Report from the last run should get deleted, and a new
+  // one should be created for this run.
   [self startReportManager];
 
+  // If this is > 1 it means we're not cleaning up reports from previous runs.
+  // If this == 0, it means we're not creating new reports.
   XCTAssertEqual([[self contentsOfActivePath] count], 1);
 
   XCTAssertEqual([self.prepareAndSubmitReportArray count], 0);
@@ -273,10 +274,8 @@
   // create a report and put it in place
   [self createActiveReport];
 
-  // Report should get deleted, and nothing else specials should happen.
-  FBLPromise<NSNumber *> *promise = [self startReportManagerWithDataCollectionEnabled:NO];
-  // It should not be necessary to call processReports, since there are no reports.
-  [self waitForPromise:promise];
+  // Starting with data collection disabled should report in nothing changing
+  [self startReportManagerWithDataCollectionEnabled:NO];
 
   XCTAssertEqual([[self contentsOfActivePath] count], 1);
 
@@ -466,6 +465,11 @@
   XCTAssertEqualObjects(self.prepareAndSubmitReportArray[0][@"urgent"], @(NO));
 }
 
+/*
+ * This tests an edge case where there is a report in processing. For the purposes of unsent
+ * reports these are not shown to the developer, but they are uploaded / deleted upon
+ * calling send / delete.
+ */
 - (void)testFilesLeftInProcessingWithDataCollectionDisabled {
   // Put report in processing.
   FIRCLSInternalReport *report = [self createActiveReport];
@@ -479,10 +483,14 @@
                  @"Processing should still have the report");
   XCTAssertEqual([self.prepareAndSubmitReportArray count], 0);
 
-  [self processReports:YES];
+  // We don't expect reports here because we don't consider processing or prepared
+  // reports as unsent as they need to be marked for sending before being placed
+  // in those directories.
+  [self processReports:YES andExpectReports:NO];
 
   // We should not process reports left over in processing.
   XCTAssertEqual([[self contentsOfProcessingPath] count], 0, @"Processing should be cleared");
+  XCTAssertEqual([[self contentsOfPreparedPath] count], 0, @"Prepared should be cleared");
 
   XCTAssertEqual([self.prepareAndSubmitReportArray count], 1);
   XCTAssertEqualObjects(self.prepareAndSubmitReportArray[0][@"process"], @(NO));
@@ -493,7 +501,7 @@
   // Drop a phony multipart-mime file in here, with non-zero contents.
   XCTAssert([_fileManager createDirectoryAtPath:_fileManager.preparedPath]);
   NSString *path = [_fileManager.preparedPath stringByAppendingPathComponent:@"phony-report"];
-  path = [path stringByAppendingPathExtension:@".multipart-mime"];
+  path = [path stringByAppendingPathExtension:@"multipart-mime"];
 
   XCTAssertTrue([[_fileManager underlyingFileManager]
       createFileAtPath:path
@@ -502,7 +510,7 @@
 
   [self startReportManager];
 
-  // We should not process reports left over in prepared.
+  // Reports should be moved out of prepared
   XCTAssertEqual([[self contentsOfPreparedPath] count], 0, @"Prepared should be cleared");
 
   XCTAssertEqual([self.prepareAndSubmitReportArray count], 0);
@@ -510,11 +518,16 @@
   XCTAssertEqualObjects(self.uploadReportArray[0][@"path"], path);
 }
 
+/*
+ * This tests an edge case where there is a report in prepared. For the purposes of unsent
+ * reports these are not shown to the developer, but they are uploaded / deleted upon
+ * calling send / delete.
+ */
 - (void)testFilesLeftInPreparedWithDataCollectionDisabled {
   // drop a phony multipart-mime file in here, with non-zero contents
   XCTAssert([_fileManager createDirectoryAtPath:_fileManager.preparedPath]);
   NSString *path = [_fileManager.preparedPath stringByAppendingPathComponent:@"phony-report"];
-  path = [path stringByAppendingPathExtension:@".multipart-mime"];
+  path = [path stringByAppendingPathExtension:@"multipart-mime"];
 
   XCTAssertTrue([[_fileManager underlyingFileManager]
       createFileAtPath:path
@@ -528,10 +541,14 @@
                  @"Prepared should still have the report");
   XCTAssertEqual([self.prepareAndSubmitReportArray count], 0);
 
-  [self processReports:YES];
+  // We don't expect reports here because we don't consider processing or prepared
+  // reports as unsent as they need to be marked for sending before being placed
+  // in those directories.
+  [self processReports:YES andExpectReports:NO];
 
-  // we should not process reports left over in processing
+  // Reports should be moved out of prepared
   XCTAssertEqual([[self contentsOfPreparedPath] count], 0, @"Prepared should be cleared");
+  XCTAssertEqual([[self contentsOfProcessingPath] count], 0, @"Processing should be cleared");
 
   XCTAssertEqual([self.prepareAndSubmitReportArray count], 0);
   XCTAssertEqual([self.uploadReportArray count], 1);
@@ -542,7 +559,7 @@
   // drop a phony multipart-mime file in here, with non-zero contents
   XCTAssert([_fileManager createDirectoryAtPath:_fileManager.preparedPath]);
   NSString *path = [_fileManager.preparedPath stringByAppendingPathComponent:@"phony-report"];
-  path = [path stringByAppendingPathExtension:@".multipart-mime"];
+  path = [path stringByAppendingPathExtension:@"multipart-mime"];
 
   XCTAssertTrue([[_fileManager underlyingFileManager]
       createFileAtPath:path

+ 0 - 130
Crashlytics/UnitTests/FIRCLSReportTests.m

@@ -1,130 +0,0 @@
-// Copyright 2019 Google
-//
-// 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 <XCTest/XCTest.h>
-
-#import "Crashlytics/Crashlytics/Components/FIRCLSContext.h"
-#import "Crashlytics/Crashlytics/Components/FIRCLSGlobals.h"
-#import "Crashlytics/Crashlytics/Helpers/FIRCLSFile.h"
-#import "Crashlytics/Crashlytics/Models/FIRCLSInternalReport.h"
-#import "Crashlytics/Crashlytics/Models/FIRCLSReport.h"
-#import "Crashlytics/Crashlytics/Models/FIRCLSReport_Private.h"
-
-@interface FIRCLSReportTests : XCTestCase
-
-@end
-
-@implementation FIRCLSReportTests
-
-- (void)setUp {
-  [super setUp];
-
-  FIRCLSContextBaseInit();
-
-  // these values must be set for the internals of logging to work
-  _firclsContext.readonly->logging.userKVStorage.maxCount = 16;
-  _firclsContext.readonly->logging.userKVStorage.maxIncrementalCount = 16;
-  _firclsContext.readonly->logging.internalKVStorage.maxCount = 32;
-  _firclsContext.readonly->logging.internalKVStorage.maxIncrementalCount = 16;
-
-  _firclsContext.readonly->initialized = true;
-}
-
-- (void)tearDown {
-  FIRCLSContextBaseDeinit();
-
-  [super tearDown];
-}
-
-- (NSString *)resourcePath {
-  return [[NSBundle bundleForClass:[self class]] resourcePath];
-}
-
-- (NSString *)pathForResource:(NSString *)name {
-  return [[self resourcePath] stringByAppendingPathComponent:name];
-}
-
-- (FIRCLSInternalReport *)createTempCopyOfInternalReportWithName:(NSString *)name {
-  NSString *tempPath = [NSTemporaryDirectory() stringByAppendingPathComponent:name];
-
-  // make sure to remove anything that was there previously
-  [[NSFileManager defaultManager] removeItemAtPath:tempPath error:nil];
-
-  NSString *resourcePath = [self pathForResource:name];
-
-  [[NSFileManager defaultManager] copyItemAtPath:resourcePath toPath:tempPath error:nil];
-
-  return [[FIRCLSInternalReport alloc] initWithPath:tempPath];
-}
-
-- (FIRCLSReport *)createTempCopyOfReportWithName:(NSString *)name {
-  FIRCLSInternalReport *internalReport = [self createTempCopyOfInternalReportWithName:name];
-
-  return [[FIRCLSReport alloc] initWithInternalReport:internalReport];
-}
-
-#pragma mark - Public Getter Methods
-- (void)testPropertiesFromMetadatFile {
-  FIRCLSReport *report = [self createTempCopyOfReportWithName:@"metadata_only_report"];
-
-  XCTAssertEqualObjects(@"772929a7f21f4ad293bb644668f257cd", report.identifier);
-  XCTAssertEqualObjects(@"3", report.bundleVersion);
-  XCTAssertEqualObjects(@"1.0", report.bundleShortVersionString);
-  XCTAssertEqualObjects([NSDate dateWithTimeIntervalSince1970:1423944888], report.dateCreated);
-  XCTAssertEqualObjects(@"14C109", report.OSBuildVersion);
-  XCTAssertEqualObjects(@"10.10.2", report.OSVersion);
-}
-
-#pragma mark - Public Setter Methods
-- (void)testSetUserProperties {
-  FIRCLSReport *report = [self createTempCopyOfReportWithName:@"metadata_only_report"];
-
-  [report setUserIdentifier:@"12345-6"];
-
-  NSArray *entries = FIRCLSFileReadSections(
-      [[report.internalReport pathForContentFile:FIRCLSReportInternalIncrementalKVFile]
-          fileSystemRepresentation],
-      false, nil);
-
-  XCTAssertEqual([entries count], 1, @"");
-
-  XCTAssertEqualObjects(entries[0][@"kv"][@"key"],
-                        FIRCLSFileHexEncodeString([FIRCLSUserIdentifierKey UTF8String]), @"");
-  XCTAssertEqualObjects(entries[0][@"kv"][@"value"], FIRCLSFileHexEncodeString("12345-6"), @"");
-}
-
-- (void)testSetKeyValuesWhenNoneWerePresent {
-  FIRCLSReport *report = [self createTempCopyOfReportWithName:@"metadata_only_report"];
-
-  [report setObjectValue:@"hello" forKey:@"mykey"];
-  [report setObjectValue:@"goodbye" forKey:@"anotherkey"];
-
-  NSArray *entries = FIRCLSFileReadSections(
-      [[report.internalReport pathForContentFile:FIRCLSReportUserIncrementalKVFile]
-          fileSystemRepresentation],
-      false, nil);
-
-  XCTAssertEqual([entries count], 2, @"");
-
-  // mykey = "..."
-  XCTAssertEqualObjects(entries[0][@"kv"][@"key"], FIRCLSFileHexEncodeString("mykey"), @"");
-  XCTAssertEqualObjects(entries[0][@"kv"][@"value"], FIRCLSFileHexEncodeString("hello"), @"");
-
-  // anotherkey = "..."
-  XCTAssertEqualObjects(entries[1][@"kv"][@"key"], FIRCLSFileHexEncodeString("anotherkey"), @"");
-  XCTAssertEqualObjects(entries[1][@"kv"][@"value"], FIRCLSFileHexEncodeString("goodbye"), @"");
-}
-
-@end

+ 260 - 0
Crashlytics/UnitTests/FIRCrashlyticsReportTests.m

@@ -0,0 +1,260 @@
+// Copyright 2019 Google
+//
+// 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 <XCTest/XCTest.h>
+
+#import "Crashlytics/Crashlytics/Components/FIRCLSContext.h"
+#import "Crashlytics/Crashlytics/Components/FIRCLSGlobals.h"
+#import "Crashlytics/Crashlytics/Helpers/FIRCLSFile.h"
+#import "Crashlytics/Crashlytics/Models/FIRCLSInternalReport.h"
+#import "Crashlytics/Crashlytics/Private/FIRCrashlyticsReport_Private.h"
+#import "Crashlytics/Crashlytics/Public/FirebaseCrashlytics/FIRCrashlyticsReport.h"
+
+@interface FIRCrashlyticsReportTests : XCTestCase
+
+@end
+
+@implementation FIRCrashlyticsReportTests
+
+- (void)setUp {
+  [super setUp];
+
+  FIRCLSContextBaseInit();
+
+  // these values must be set for the internals of logging to work
+  _firclsContext.readonly->logging.userKVStorage.maxCount = 64;
+  _firclsContext.readonly->logging.userKVStorage.maxIncrementalCount =
+      FIRCLSUserLoggingMaxKVEntries;
+  _firclsContext.readonly->logging.internalKVStorage.maxCount = 32;
+  _firclsContext.readonly->logging.internalKVStorage.maxIncrementalCount = 16;
+
+  _firclsContext.readonly->logging.logStorage.maxSize = 64 * 1000;
+  _firclsContext.readonly->logging.logStorage.maxEntries = 0;
+  _firclsContext.readonly->logging.logStorage.restrictBySize = true;
+  _firclsContext.readonly->logging.logStorage.entryCount = NULL;
+
+  _firclsContext.readonly->initialized = true;
+}
+
+- (void)tearDown {
+  FIRCLSContextBaseDeinit();
+
+  [super tearDown];
+}
+
+- (NSString *)resourcePath {
+  return [[NSBundle bundleForClass:[self class]] resourcePath];
+}
+
+- (NSString *)pathForResource:(NSString *)name {
+  return [[self resourcePath] stringByAppendingPathComponent:name];
+}
+
+- (FIRCLSInternalReport *)createTempCopyOfInternalReportWithName:(NSString *)name {
+  NSString *tempPath = [NSTemporaryDirectory() stringByAppendingPathComponent:name];
+
+  // make sure to remove anything that was there previously
+  [[NSFileManager defaultManager] removeItemAtPath:tempPath error:nil];
+
+  NSString *resourcePath = [self pathForResource:name];
+
+  [[NSFileManager defaultManager] copyItemAtPath:resourcePath toPath:tempPath error:nil];
+
+  return [[FIRCLSInternalReport alloc] initWithPath:tempPath];
+}
+
+- (FIRCrashlyticsReport *)createTempCopyOfReportWithName:(NSString *)name {
+  FIRCLSInternalReport *internalReport = [self createTempCopyOfInternalReportWithName:name];
+  return [[FIRCrashlyticsReport alloc] initWithInternalReport:internalReport];
+}
+
+#pragma mark - Public Getter Methods
+- (void)testPropertiesFromMetadatFile {
+  FIRCrashlyticsReport *report = [self createTempCopyOfReportWithName:@"metadata_only_report"];
+
+  XCTAssertEqualObjects(@"772929a7f21f4ad293bb644668f257cd", report.reportID);
+  XCTAssertEqualObjects([NSDate dateWithTimeIntervalSince1970:1423944888], report.dateCreated);
+}
+
+#pragma mark - Public Setter Methods
+- (void)testSetUserID {
+  FIRCrashlyticsReport *report = [self createTempCopyOfReportWithName:@"metadata_only_report"];
+
+  [report setUserID:@"12345-6"];
+
+  NSArray *entries = FIRCLSFileReadSections(
+      [[report.internalReport pathForContentFile:FIRCLSReportInternalIncrementalKVFile]
+          fileSystemRepresentation],
+      false, nil);
+
+  XCTAssertEqual([entries count], 1, @"");
+
+  XCTAssertEqualObjects(entries[0][@"kv"][@"key"],
+                        FIRCLSFileHexEncodeString([FIRCLSUserIdentifierKey UTF8String]), @"");
+  XCTAssertEqualObjects(entries[0][@"kv"][@"value"], FIRCLSFileHexEncodeString("12345-6"), @"");
+}
+
+- (void)testCustomKeysNoExisting {
+  FIRCrashlyticsReport *report = [self createTempCopyOfReportWithName:@"metadata_only_report"];
+
+  [report setCustomValue:@"hello" forKey:@"mykey"];
+  [report setCustomValue:@"goodbye" forKey:@"anotherkey"];
+
+  [report setCustomKeysAndValues:@{
+    @"is_test" : @(YES),
+    @"test_number" : @(10),
+  }];
+
+  NSArray *entries = FIRCLSFileReadSections(
+      [[report.internalReport pathForContentFile:FIRCLSReportUserIncrementalKVFile]
+          fileSystemRepresentation],
+      false, nil);
+
+  XCTAssertEqual([entries count], 4, @"");
+
+  XCTAssertEqualObjects(entries[0][@"kv"][@"key"], FIRCLSFileHexEncodeString("mykey"), @"");
+  XCTAssertEqualObjects(entries[0][@"kv"][@"value"], FIRCLSFileHexEncodeString("hello"), @"");
+
+  XCTAssertEqualObjects(entries[1][@"kv"][@"key"], FIRCLSFileHexEncodeString("anotherkey"), @"");
+  XCTAssertEqualObjects(entries[1][@"kv"][@"value"], FIRCLSFileHexEncodeString("goodbye"), @"");
+
+  XCTAssertEqualObjects(entries[2][@"kv"][@"key"], FIRCLSFileHexEncodeString("is_test"), @"");
+  XCTAssertEqualObjects(entries[2][@"kv"][@"value"], FIRCLSFileHexEncodeString("1"), @"");
+
+  XCTAssertEqualObjects(entries[3][@"kv"][@"key"], FIRCLSFileHexEncodeString("test_number"), @"");
+  XCTAssertEqualObjects(entries[3][@"kv"][@"value"], FIRCLSFileHexEncodeString("10"), @"");
+}
+
+- (void)testCustomKeysWithExisting {
+  FIRCrashlyticsReport *report = [self createTempCopyOfReportWithName:@"ios_all_files_crash"];
+
+  [report setCustomValue:@"hello" forKey:@"mykey"];
+  [report setCustomValue:@"goodbye" forKey:@"anotherkey"];
+
+  [report setCustomKeysAndValues:@{
+    @"is_test" : @(YES),
+    @"test_number" : @(10),
+  }];
+
+  NSArray *entries = FIRCLSFileReadSections(
+      [[report.internalReport pathForContentFile:FIRCLSReportUserIncrementalKVFile]
+          fileSystemRepresentation],
+      false, nil);
+
+  XCTAssertEqual([entries count], 5, @"");
+
+  XCTAssertEqualObjects(entries[1][@"kv"][@"key"], FIRCLSFileHexEncodeString("mykey"), @"");
+  XCTAssertEqualObjects(entries[1][@"kv"][@"value"], FIRCLSFileHexEncodeString("hello"), @"");
+
+  XCTAssertEqualObjects(entries[2][@"kv"][@"key"], FIRCLSFileHexEncodeString("anotherkey"), @"");
+  XCTAssertEqualObjects(entries[2][@"kv"][@"value"], FIRCLSFileHexEncodeString("goodbye"), @"");
+
+  XCTAssertEqualObjects(entries[3][@"kv"][@"key"], FIRCLSFileHexEncodeString("is_test"), @"");
+  XCTAssertEqualObjects(entries[3][@"kv"][@"value"], FIRCLSFileHexEncodeString("1"), @"");
+
+  XCTAssertEqualObjects(entries[4][@"kv"][@"key"], FIRCLSFileHexEncodeString("test_number"), @"");
+  XCTAssertEqualObjects(entries[4][@"kv"][@"value"], FIRCLSFileHexEncodeString("10"), @"");
+}
+
+- (void)testCustomKeysLimits {
+  FIRCrashlyticsReport *report = [self createTempCopyOfReportWithName:@"ios_all_files_crash"];
+
+  // Write a bunch of keys and values
+  for (int i = 0; i < 120; i++) {
+    NSString *key = [NSString stringWithFormat:@"key_%i", i];
+    [report setCustomValue:@"hello" forKey:key];
+  }
+
+  NSArray *entriesI = FIRCLSFileReadSections(
+      [[report.internalReport pathForContentFile:FIRCLSReportUserIncrementalKVFile]
+          fileSystemRepresentation],
+      false, nil);
+  NSArray *entriesC = FIRCLSFileReadSections(
+      [[report.internalReport pathForContentFile:FIRCLSReportUserCompactedKVFile]
+          fileSystemRepresentation],
+      false, nil);
+
+  // One of these should be the max (64), and one should be the number of written keys modulo 64
+  // (eg. 56 == (120 mod 64))
+  XCTAssertEqual(entriesI.count, 56, @"");
+  XCTAssertEqual(entriesC.count, 64, @"");
+}
+
+- (void)testLogsNoExisting {
+  FIRCrashlyticsReport *report = [self createTempCopyOfReportWithName:@"metadata_only_report"];
+
+  [report log:@"Normal log without formatting"];
+  [report logWithFormat:@"%@, %@", @"First", @"Second"];
+
+  NSArray *entries = FIRCLSFileReadSections(
+      [[report.internalReport pathForContentFile:FIRCLSReportLogAFile] fileSystemRepresentation],
+      false, nil);
+
+  XCTAssertEqual([entries count], 2, @"");
+
+  XCTAssertEqualObjects(entries[0][@"log"][@"msg"],
+                        FIRCLSFileHexEncodeString("Normal log without formatting"), @"");
+  XCTAssertEqualObjects(entries[1][@"log"][@"msg"], FIRCLSFileHexEncodeString("First, Second"),
+                        @"");
+}
+
+- (void)testLogsWithExisting {
+  FIRCrashlyticsReport *report = [self createTempCopyOfReportWithName:@"ios_all_files_crash"];
+
+  [report log:@"Normal log without formatting"];
+  [report logWithFormat:@"%@, %@", @"First", @"Second"];
+
+  NSArray *entries = FIRCLSFileReadSections(
+      [[report.internalReport pathForContentFile:FIRCLSReportLogAFile] fileSystemRepresentation],
+      false, nil);
+
+  XCTAssertEqual([entries count], 8, @"");
+
+  XCTAssertEqualObjects(entries[6][@"log"][@"msg"],
+                        FIRCLSFileHexEncodeString("Normal log without formatting"), @"");
+  XCTAssertEqualObjects(entries[7][@"log"][@"msg"], FIRCLSFileHexEncodeString("First, Second"),
+                        @"");
+}
+
+- (void)testLogLimits {
+  FIRCrashlyticsReport *report = [self createTempCopyOfReportWithName:@"metadata_only_report"];
+
+  for (int i = 0; i < 2000; i++) {
+    [report log:@"0123456789"];
+  }
+
+  unsigned long long sizeA = [[[NSFileManager defaultManager]
+      attributesOfItemAtPath:[report.internalReport pathForContentFile:FIRCLSReportLogAFile]
+                       error:nil] fileSize];
+  unsigned long long sizeB = [[[NSFileManager defaultManager]
+      attributesOfItemAtPath:[report.internalReport pathForContentFile:FIRCLSReportLogBFile]
+                       error:nil] fileSize];
+
+  NSArray *entriesA = FIRCLSFileReadSections(
+      [[report.internalReport pathForContentFile:FIRCLSReportLogAFile] fileSystemRepresentation],
+      false, nil);
+  NSArray *entriesB = FIRCLSFileReadSections(
+      [[report.internalReport pathForContentFile:FIRCLSReportLogBFile] fileSystemRepresentation],
+      false, nil);
+
+  // If these numbers have changed, the goal is to validate that the size of log_a and log_b are
+  // under the limit, logStorage.maxSize (64 * 1000). These numbers don't need to be exact so if
+  // they fluctuate then we might just need to accept a range in these tests.
+  XCTAssertEqual(entriesB.count + entriesA.count, 2000, @"");
+  XCTAssertEqual(sizeA, 64 * 1000 + 20, @"");
+  XCTAssertEqual(sizeB, 55980, @"");
+}
+
+@end

+ 48 - 41
Example/InstanceID/Tests/FIRInstanceIDTest.m

@@ -187,11 +187,10 @@ static NSString *const kGoogleAppID = @"1:123:ios:123abc";
   [[[self.mockInstanceID stub] andReturn:kToken] cachedTokenIfAvailable];
   [[[self.mockTokenManager stub] andReturnValue:@(YES)]
       checkTokenRefreshPolicyWithIID:[OCMArg any]];
-  [[[self.mockInstanceID stub] andDo:^(NSInvocation *invocation){
-  }] tokenWithAuthorizedEntity:[OCMArg any]
-                         scope:[OCMArg any]
-                       options:[OCMArg any]
-                       handler:[OCMArg any]];
+  [[self.mockInstanceID stub] tokenWithAuthorizedEntity:[OCMArg any]
+                                                  scope:[OCMArg any]
+                                                options:[OCMArg any]
+                                                handler:[OCMArg any]];
   [self expectInstallationsInstallationIDWithFID:kToken error:nil];
 
   [self.mockInstanceID didCompleteConfigure];
@@ -202,11 +201,10 @@ static NSString *const kGoogleAppID = @"1:123:ios:123abc";
 - (void)testTokenShouldBeRefreshedIfNoCacheTokenButAutoInitAllowed {
   [[[self.mockInstanceID stub] andReturn:nil] cachedTokenIfAvailable];
   [[[self.mockInstanceID stub] andReturnValue:@(YES)] isFCMAutoInitEnabled];
-  [[[self.mockInstanceID stub] andDo:^(NSInvocation *invocation){
-  }] tokenWithAuthorizedEntity:[OCMArg any]
-                         scope:[OCMArg any]
-                       options:[OCMArg any]
-                       handler:[OCMArg any]];
+  [[self.mockInstanceID stub] tokenWithAuthorizedEntity:[OCMArg any]
+                                                  scope:[OCMArg any]
+                                                options:[OCMArg any]
+                                                handler:[OCMArg any]];
 
   [self.mockInstanceID didCompleteConfigure];
 
@@ -254,11 +252,10 @@ static NSString *const kGoogleAppID = @"1:123:ios:123abc";
 
 - (void)testTokenIsDeletedAlongWithIdentity {
   [[[self.mockInstanceID stub] andReturnValue:@(YES)] isFCMAutoInitEnabled];
-  [[[self.mockInstanceID stub] andDo:^(NSInvocation *invocation){
-  }] tokenWithAuthorizedEntity:[OCMArg any]
-                         scope:[OCMArg any]
-                       options:[OCMArg any]
-                       handler:[OCMArg any]];
+  [[self.mockInstanceID stub] tokenWithAuthorizedEntity:[OCMArg any]
+                                                  scope:[OCMArg any]
+                                                options:[OCMArg any]
+                                                handler:[OCMArg any]];
 
   [self.mockInstanceID deleteIdentityWithHandler:^(NSError *_Nullable error) {
     XCTAssertNil([self.mockInstanceID token]);
@@ -452,17 +449,17 @@ static NSString *const kGoogleAppID = @"1:123:ios:123abc";
     serverTypeKey : @(NO),
   };
 
-  [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation){
-  }] fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity
-                                 scope:kScope
-                            instanceID:[OCMArg any]
-                               options:[OCMArg checkWithBlock:^BOOL(id obj) {
-                                 NSDictionary *options = (NSDictionary *)obj;
-                                 XCTAssertTrue([options[APNSKey] hasPrefix:@"p_"]);
-                                 XCTAssertFalse([options[serverTypeKey] boolValue]);
-                                 return YES;
-                               }]
-                               handler:OCMOCK_ANY];
+  [[self.mockTokenManager stub]
+      fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity
+                                  scope:kScope
+                             instanceID:[OCMArg any]
+                                options:[OCMArg checkWithBlock:^BOOL(id obj) {
+                                  NSDictionary *options = (NSDictionary *)obj;
+                                  XCTAssertTrue([options[APNSKey] hasPrefix:@"p_"]);
+                                  XCTAssertFalse([options[serverTypeKey] boolValue]);
+                                  return YES;
+                                }]
+                                handler:OCMOCK_ANY];
 
   [self.instanceID tokenWithAuthorizedEntity:kAuthorizedEntity
                                        scope:kScope
@@ -638,7 +635,8 @@ static NSString *const kGoogleAppID = @"1:123:ios:123abc";
   // will change. Normal stubbing will always return the initial pointer,
   // which in this case is 0x0 (nil).
   [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) {
-    [invocation setReturnValue:&cachedTokenInfo];
+    __autoreleasing FIRInstanceIDTokenInfo *tokenInfo = cachedTokenInfo;
+    [invocation setReturnValue:&tokenInfo];
   }] cachedTokenInfoWithAuthorizedEntity:kAuthorizedEntity scope:kFIRInstanceIDDefaultTokenScope];
 
   [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) {
@@ -705,7 +703,8 @@ static NSString *const kGoogleAppID = @"1:123:ios:123abc";
   // will change. Normal stubbing will always return the initial pointer,
   // which in this case is 0x0 (nil).
   [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) {
-    [invocation setReturnValue:&cachedTokenInfo];
+    __autoreleasing FIRInstanceIDTokenInfo *tokenInfo = cachedTokenInfo;
+    [invocation setReturnValue:&tokenInfo];
   }] cachedTokenInfoWithAuthorizedEntity:kAuthorizedEntity scope:kFIRInstanceIDDefaultTokenScope];
 
   [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) {
@@ -764,7 +763,8 @@ static NSString *const kGoogleAppID = @"1:123:ios:123abc";
   // will change. Normal stubbing will always return the initial pointer,
   // which in this case is 0x0 (nil).
   [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) {
-    [invocation setReturnValue:&cachedTokenInfo];
+    __autoreleasing FIRInstanceIDTokenInfo *tokenInfo = cachedTokenInfo;
+    [invocation setReturnValue:&tokenInfo];
   }] cachedTokenInfoWithAuthorizedEntity:kAuthorizedEntity scope:kFIRInstanceIDDefaultTokenScope];
 
   [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) {
@@ -841,7 +841,8 @@ static NSString *const kGoogleAppID = @"1:123:ios:123abc";
   // will change. Normal stubbing will always return the initial pointer,
   // which in this case is 0x0 (nil).
   [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) {
-    [invocation setReturnValue:&cachedTokenInfo];
+    __autoreleasing FIRInstanceIDTokenInfo *tokenInfo = cachedTokenInfo;
+    [invocation setReturnValue:&tokenInfo];
   }] cachedTokenInfoWithAuthorizedEntity:kAuthorizedEntity scope:kFIRInstanceIDDefaultTokenScope];
 
   [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) {
@@ -949,7 +950,9 @@ static NSString *const kGoogleAppID = @"1:123:ios:123abc";
   __block FIRInstanceIDTokenHandler tokenHandler;
 
   [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) {
-    [invocation getArgument:&tokenHandler atIndex:6];
+    __unsafe_unretained FIRInstanceIDTokenHandler handler;
+    [invocation getArgument:&handler atIndex:6];
+    tokenHandler = handler;
     [fetchNewTokenExpectation fulfill];
   }] fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity
                                  scope:kFIRInstanceIDDefaultTokenScope
@@ -1005,7 +1008,9 @@ static NSString *const kGoogleAppID = @"1:123:ios:123abc";
   __block FIRInstanceIDTokenHandler tokenHandler;
 
   [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) {
-    [invocation getArgument:&tokenHandler atIndex:6];
+    __unsafe_unretained FIRInstanceIDTokenHandler handler;
+    [invocation getArgument:&handler atIndex:6];
+    tokenHandler = handler;
     [fetchNewTokenExpectations[fetchNewTokenCallCount] fulfill];
     fetchNewTokenCallCount += 1;
   }] fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity
@@ -1070,7 +1075,9 @@ static NSString *const kGoogleAppID = @"1:123:ios:123abc";
   __block FIRInstanceIDTokenHandler tokenHandler;
 
   [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) {
-    [invocation getArgument:&tokenHandler atIndex:6];
+    __unsafe_unretained FIRInstanceIDTokenHandler handler;
+    [invocation getArgument:&handler atIndex:6];
+    tokenHandler = handler;
     [fetchNewTokenExpectations[fetchNewTokenCallCount] fulfill];
     fetchNewTokenCallCount += 1;
   }] fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity
@@ -1166,7 +1173,7 @@ static NSString *const kGoogleAppID = @"1:123:ios:123abc";
 
   [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) {
     // Inspect
-    NSDictionary *options;
+    __unsafe_unretained NSDictionary *options;
     [invocation getArgument:&options atIndex:5];
     if (options[kFIRInstanceIDTokenOptionsAPNSIsSandboxKey] != nil) {
       [apnsServerTypeExpectation fulfill];
@@ -1205,12 +1212,6 @@ static NSString *const kGoogleAppID = @"1:123:ios:123abc";
   // This token is |kToken|, but we will simulate that a fetch will return another token
   NSString *oldCachedToken = kToken;
   NSString *fetchedToken = @"abcd123_newtoken";
-  __block FIRInstanceIDTokenInfo *cachedTokenInfo =
-      [[FIRInstanceIDTokenInfo alloc] initWithAuthorizedEntity:kAuthorizedEntity
-                                                         scope:kFIRInstanceIDDefaultTokenScope
-                                                         token:oldCachedToken
-                                                    appVersion:@"1.0"
-                                                 firebaseAppID:@"firebaseAppID"];
 
   [self stubInstallationsToReturnValidID];
 
@@ -1223,7 +1224,13 @@ static NSString *const kGoogleAppID = @"1:123:ios:123abc";
   // will change. Normal stubbing will always return the initial pointer,
   // which in this case is 0x0 (nil).
   [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) {
-    [invocation setReturnValue:&cachedTokenInfo];
+    __autoreleasing FIRInstanceIDTokenInfo *tokenInfo =
+        [[FIRInstanceIDTokenInfo alloc] initWithAuthorizedEntity:kAuthorizedEntity
+                                                           scope:kFIRInstanceIDDefaultTokenScope
+                                                           token:oldCachedToken
+                                                      appVersion:@"1.0"
+                                                   firebaseAppID:@"firebaseAppID"];
+    [invocation setReturnValue:&tokenInfo];
   }] cachedTokenInfoWithAuthorizedEntity:kAuthorizedEntity scope:kFIRInstanceIDDefaultTokenScope];
 
   // Mock the network request to return |fetchedToken|, so we can clearly see if the token is

+ 1 - 0
Example/watchOSSample/Podfile

@@ -1,3 +1,4 @@
+source 'https://github.com/firebase/SpecsDev.git'
 source 'https://github.com/firebase/SpecsStaging.git'
 source 'https://cdn.cocoapods.org/'
 

+ 79 - 0
FIRDynamicLinkTest.m

@@ -0,0 +1,79 @@
+/*
+ * Copyright 2021 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 <XCTest/XCTest.h>
+#import "FirebaseDynamicLinks/Sources/FIRDynamicLink+Private.h"
+
+@interface FIRDynamicLinkTest : XCTestCase {
+}
+@end
+
+@implementation FIRDynamicLinkTest
+
+NSMutableDictionary<NSString *, NSString *> *fdlParameters = nil;
+NSDictionary<NSString *, NSString *> *linkParameters = nil;
+NSDictionary<NSString *, NSString *> *utmParameters = nil;
+
+- (void)setUp {
+  [super setUp];
+
+  linkParameters = @{
+    @"deep_link_id" : @"https://mmaksym.com/test-app1",
+    @"match_message" : @"Link is uniquely matched for this device.",
+    @"match_type" : @"unique",
+    @"a_parameter" : @"a_value"
+  };
+  utmParameters = @{
+    @"utm_campaign" : @"eldhosembabu Test",
+    @"utm_medium" : @"test_medium",
+    @"utm_source" : @"test_source",
+  };
+
+  fdlParameters = [[NSMutableDictionary alloc] initWithDictionary:linkParameters];
+  [fdlParameters addEntriesFromDictionary:utmParameters];
+}
+
+- (void)testDynamicLinkParameters_InitWithParameters {
+  FIRDynamicLink *dynamicLink = [[FIRDynamicLink alloc] initWithParametersDictionary:fdlParameters];
+  XCTAssertEqual([fdlParameters count], [[dynamicLink parametersDictionary] count]);
+  for (NSString *key in fdlParameters) {
+    NSString *expectedValue = [fdlParameters valueForKey:key];
+    NSString *derivedValue = [[dynamicLink parametersDictionary] valueForKey:key];
+    XCTAssertNotNil(derivedValue, @"Cannot be null!");
+    XCTAssertEqualObjects(derivedValue, expectedValue);
+  }
+}
+
+- (void)testDynamicLinkUtmParameters_InitWithParameters {
+  FIRDynamicLink *dynamicLink = [[FIRDynamicLink alloc] initWithParametersDictionary:fdlParameters];
+  XCTAssertEqual([[dynamicLink utmParametersDictionary] count], [utmParameters count]);
+  for (NSString *key in utmParameters) {
+    NSString *expectedValue = [utmParameters valueForKey:key];
+    NSString *derivedValue = [[dynamicLink utmParametersDictionary] valueForKey:key];
+    XCTAssertNotNil(derivedValue, @"Cannot be null!");
+    XCTAssertEqualObjects(derivedValue, expectedValue);
+  }
+}
+
+- (void)testDynamicLinkParameters_InitWithNoUtmParameters {
+  FIRDynamicLink *dynamicLink =
+      [[FIRDynamicLink alloc] initWithParametersDictionary:linkParameters];
+  XCTAssertEqual([[dynamicLink parametersDictionary] count], [linkParameters count]);
+  XCTAssertEqual([[dynamicLink utmParametersDictionary] count], 0);
+}
+
+@end

+ 18 - 18
Firebase.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'Firebase'
-  s.version          = '7.6.0'
+  s.version          = '7.7.0'
   s.summary          = 'Firebase'
 
   s.description      = <<-DESC
@@ -34,12 +34,12 @@ Simplify your app development, grow your user base, and monetize more effectivel
     ss.ios.deployment_target = '9.0'
     ss.osx.deployment_target = '10.12'
     ss.tvos.deployment_target = '10.0'
-    ss.ios.dependency 'FirebaseAnalytics', '7.6.0'
+    ss.ios.dependency 'FirebaseAnalytics', '7.7.0'
     ss.dependency 'Firebase/CoreOnly'
   end
 
   s.subspec 'CoreOnly' do |ss|
-    ss.dependency 'FirebaseCore', '7.6.0'
+    ss.dependency 'FirebaseCore', '7.7.0'
     ss.source_files = 'CoreOnly/Sources/Firebase.h'
     ss.preserve_paths = 'CoreOnly/Sources/module.modulemap'
     if ENV['FIREBASE_POD_REPO_FOR_DEV_POD'] then
@@ -64,7 +64,7 @@ Simplify your app development, grow your user base, and monetize more effectivel
 
   s.subspec 'ABTesting' do |ss|
     ss.dependency 'Firebase/CoreOnly'
-    ss.dependency 'FirebaseABTesting', '~> 7.6.0'
+    ss.dependency 'FirebaseABTesting', '~> 7.7.0'
   end
 
   s.subspec 'AdMob' do |ss|
@@ -75,12 +75,12 @@ Simplify your app development, grow your user base, and monetize more effectivel
 
   s.subspec 'AppDistribution' do |ss|
     ss.dependency 'Firebase/CoreOnly'
-    ss.ios.dependency 'FirebaseAppDistribution', '~> 7.6.0-beta'
+    ss.ios.dependency 'FirebaseAppDistribution', '~> 7.7.0-beta'
   end
 
   s.subspec 'Auth' do |ss|
     ss.dependency 'Firebase/CoreOnly'
-    ss.dependency 'FirebaseAuth', '~> 7.6.0'
+    ss.dependency 'FirebaseAuth', '~> 7.7.0'
     # Standard platforms PLUS watchOS.
     ss.ios.deployment_target = '10.0'
     ss.osx.deployment_target = '10.12'
@@ -90,7 +90,7 @@ Simplify your app development, grow your user base, and monetize more effectivel
 
   s.subspec 'Crashlytics' do |ss|
     ss.dependency 'Firebase/CoreOnly'
-    ss.dependency 'FirebaseCrashlytics', '~> 7.6.0'
+    ss.dependency 'FirebaseCrashlytics', '~> 7.7.0'
     # Standard platforms PLUS watchOS.
     ss.ios.deployment_target = '10.0'
     ss.osx.deployment_target = '10.12'
@@ -100,37 +100,37 @@ Simplify your app development, grow your user base, and monetize more effectivel
 
   s.subspec 'Database' do |ss|
     ss.dependency 'Firebase/CoreOnly'
-    ss.dependency 'FirebaseDatabase', '~> 7.6.0'
+    ss.dependency 'FirebaseDatabase', '~> 7.7.0'
   end
 
   s.subspec 'DynamicLinks' do |ss|
     ss.dependency 'Firebase/CoreOnly'
-    ss.ios.dependency 'FirebaseDynamicLinks', '~> 7.6.0'
+    ss.ios.dependency 'FirebaseDynamicLinks', '~> 7.7.0'
   end
 
   s.subspec 'Firestore' do |ss|
     ss.dependency 'Firebase/CoreOnly'
-    ss.dependency 'FirebaseFirestore', '~> 7.6.0'
+    ss.dependency 'FirebaseFirestore', '~> 7.7.0'
   end
 
   s.subspec 'Functions' do |ss|
     ss.dependency 'Firebase/CoreOnly'
-    ss.dependency 'FirebaseFunctions', '~> 7.6.0'
+    ss.dependency 'FirebaseFunctions', '~> 7.7.0'
   end
 
   s.subspec 'InAppMessaging' do |ss|
     ss.dependency 'Firebase/CoreOnly'
-    ss.ios.dependency 'FirebaseInAppMessaging', '~> 7.6.0-beta'
+    ss.ios.dependency 'FirebaseInAppMessaging', '~> 7.7.0-beta'
   end
 
   s.subspec 'Installations' do |ss|
     ss.dependency 'Firebase/CoreOnly'
-    ss.dependency 'FirebaseInstallations', '~> 7.6.0'
+    ss.dependency 'FirebaseInstallations', '~> 7.7.0'
   end
 
   s.subspec 'Messaging' do |ss|
     ss.dependency 'Firebase/CoreOnly'
-    ss.dependency 'FirebaseMessaging', '~> 7.6.0'
+    ss.dependency 'FirebaseMessaging', '~> 7.7.0'
     # Standard platforms PLUS watchOS.
     ss.ios.deployment_target = '10.0'
     ss.osx.deployment_target = '10.12'
@@ -140,22 +140,22 @@ Simplify your app development, grow your user base, and monetize more effectivel
 
   s.subspec 'MLModelDownloader' do |ss|
     ss.dependency 'Firebase/CoreOnly'
-    ss.ios.dependency 'FirebaseMLModelDownloader', '~> 7.6.0-beta'
+    ss.ios.dependency 'FirebaseMLModelDownloader', '~> 7.7.0-beta'
   end
 
   s.subspec 'Performance' do |ss|
     ss.dependency 'Firebase/CoreOnly'
-    ss.ios.dependency 'FirebasePerformance', '~> 7.6.0'
+    ss.ios.dependency 'FirebasePerformance', '~> 7.7.0'
   end
 
   s.subspec 'RemoteConfig' do |ss|
     ss.dependency 'Firebase/CoreOnly'
-    ss.dependency 'FirebaseRemoteConfig', '~> 7.6.0'
+    ss.dependency 'FirebaseRemoteConfig', '~> 7.7.0'
   end
 
   s.subspec 'Storage' do |ss|
     ss.dependency 'Firebase/CoreOnly'
-    ss.dependency 'FirebaseStorage', '~> 7.6.0'
+    ss.dependency 'FirebaseStorage', '~> 7.7.0'
     # Standard platforms PLUS watchOS.
     ss.ios.deployment_target = '10.0'
     ss.osx.deployment_target = '10.12'

+ 1 - 1
FirebaseABTesting.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseABTesting'
-  s.version          = '7.6.0'
+  s.version          = '7.7.0'
   s.summary          = 'Firebase ABTesting'
 
   s.description      = <<-DESC

+ 2 - 2
FirebaseABTesting/CHANGELOG.md

@@ -1,5 +1,5 @@
-# unreleased
-- [added] Added community support for watchOS. (#7481)
+# v7.7.0
+- [added] Added community support for watchOS. ABTesting can now build on watchOS, but some functions might not work yet. (#7481)
 
 # v7.0.0
 - [removed] Removed `FIRExperimentController.updateExperiments(serviceOrigin:events:policy:lastStartTime:payloads:)`, which was deprecated. (#6543)

+ 2 - 2
FirebaseAnalytics.podspec.json

@@ -4,7 +4,7 @@
     "dependencies": {
         "FirebaseCore": "~> 7.0",
         "FirebaseInstallations": "~> 7.0",
-        "GoogleAppMeasurement": "7.6.0",
+        "GoogleAppMeasurement": "7.7.0",
         "GoogleUtilities/AppDelegateSwizzler": "~> 7.0",
         "GoogleUtilities/MethodSwizzler": "~> 7.0",
         "GoogleUtilities/NSData+zlib": "~> 7.0",
@@ -36,5 +36,5 @@
     "vendored_frameworks": [
         "Frameworks/FirebaseAnalytics.xcframework"
     ],
-    "version": "7.6.0"
+    "version": "7.7.0"
 }

+ 1 - 1
FirebaseAppDistribution.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseAppDistribution'
-  s.version          = '7.6.0-beta'
+  s.version          = '7.7.0-beta'
   s.summary          = 'App Distribution for Firebase iOS SDK.'
 
   s.description      = <<-DESC

+ 1 - 1
FirebaseAuth.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseAuth'
-  s.version          = '7.6.0'
+  s.version          = '7.7.0'
   s.summary          = 'Apple platform client for Firebase Authentication'
 
   s.description      = <<-DESC

+ 1 - 0
FirebaseAuth/Tests/Sample/Podfile

@@ -2,6 +2,7 @@
 #source 'sso://cpdc-internal/firebase'
 #source 'https://cdn.cocoapods.org/'
 
+source 'https://github.com/firebase/SpecsDev.git'
 source 'https://github.com/firebase/SpecsStaging.git'
 source 'https://cdn.cocoapods.org/'
 

+ 1 - 1
FirebaseCore.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseCore'
-  s.version          = '7.6.0'
+  s.version          = '7.7.0'
   s.summary          = 'Firebase Core'
 
   s.description      = <<-DESC

+ 3 - 1
FirebaseCore/CHANGELOG.md

@@ -1,7 +1,9 @@
 # FirebaseCore 7.7.0
 - [changed] Deprecated FirebaseMLModelInterpreter and FirebaseMLVision.
 - [added] Introduced FirebaseMLModelDownloader.
-- [fixed] Fixed missing doc comment in `FirebaseVersion()` (#7506).
+- [fixed] Fixed missing doc comment in `FirebaseVersion()`. (#7506)
+- [changed] Minimum required Xcode version for Zip and Carthage distributions changed to 12.2 (was 12.0).
+- [added] The zip distribution now includes Catalyst arm64 simulator slices. (#7007)
 
 # FirebaseCore 7.6.0
 - [fixed] Fixed build warnings introduced with Xcode 12.5. (#7431)

+ 0 - 2
FirebaseCore/Sources/FIRApp.m

@@ -346,7 +346,6 @@ static FIRApp *sDefaultApp;
     return NO;
   }
 
-#if TARGET_OS_IOS
   // Initialize the Analytics once there is a valid options under default app. Analytics should
   // always initialize first by itself before the other SDKs.
   if ([self.name isEqualToString:kFIRDefaultAppName]) {
@@ -367,7 +366,6 @@ static FIRApp *sDefaultApp;
       }
     }
   }
-#endif
 
   [self subscribeForAppDidBecomeActiveNotifications];
 

+ 1 - 1
FirebaseCoreDiagnostics.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseCoreDiagnostics'
-  s.version          = '7.6.0'
+  s.version          = '7.7.0'
   s.summary          = 'Firebase Core Diagnostics'
 
   s.description      = <<-DESC

+ 1 - 1
FirebaseCrashlytics.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseCrashlytics'
-  s.version          = '7.6.0'
+  s.version          = '7.7.0'
   s.summary          = 'Best and lightest-weight crash reporting for mobile, desktop and tvOS.'
   s.description      = 'Firebase Crashlytics helps you track, prioritize, and fix stability issues that erode app quality.'
   s.homepage         = 'https://firebase.google.com/'

+ 1 - 1
FirebaseDatabase.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseDatabase'
-  s.version          = '7.6.0'
+  s.version          = '7.7.0'
   s.summary          = 'Firebase Realtime Database'
 
   s.description      = <<-DESC

+ 1 - 1
FirebaseDynamicLinks.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseDynamicLinks'
-  s.version          = '7.6.0'
+  s.version          = '7.7.0'
   s.summary          = 'Firebase Dynamic Links'
 
   s.description      = <<-DESC

+ 3 - 0
FirebaseDynamicLinks/CHANGELOG.md

@@ -1,3 +1,6 @@
+# v7.7.0
+- [added] Added `utmParametersDictionary` property to `DynamicLink`. (#6730)
+
 # v7.6.0
 - [fixed] Fixed build warnings introduced with Xcode 12.5. (#7434)
 

+ 15 - 1
FirebaseDynamicLinks/Sources/FIRDynamicLink.m

@@ -23,6 +23,8 @@
 
 @implementation FIRDynamicLink
 
+NSString *const FDLUTMParamPrefix = @"utm_";
+
 - (NSString *)description {
   return [NSString stringWithFormat:@"<%@: %p, url [%@], match type: %@, minimumAppVersion: %@, "
                                      "match message: %@>",
@@ -36,7 +38,7 @@
 
   if (self = [super init]) {
     _parametersDictionary = [parameters copy];
-
+    _utmParametersDictionary = [[self class] extractUTMParams:parameters];
     NSString *urlString = parameters[kFIRDLParameterDeepLinkIdentifier];
     _url = [NSURL URLWithString:urlString];
     _inviteId = parameters[kFIRDLParameterInviteId];
@@ -134,6 +136,18 @@
   return [matchMap[string] integerValue] ?: FIRDLMatchTypeNone;
 }
 
++ (NSDictionary<NSString *, id> *)extractUTMParams:(NSDictionary<NSString *, id> *)parameters {
+  NSMutableDictionary<NSString *, id> *utmParamsDictionary = [[NSMutableDictionary alloc] init];
+
+  for (NSString *key in parameters) {
+    if ([key hasPrefix:FDLUTMParamPrefix]) {
+      [utmParamsDictionary setObject:[parameters valueForKey:key] forKey:key];
+    }
+  }
+
+  return [[NSDictionary alloc] initWithDictionary:utmParamsDictionary];
+}
+
 @end
 
 #endif  // TARGET_OS_IOS

+ 6 - 0
FirebaseDynamicLinks/Sources/Public/FirebaseDynamicLinks/FIRDynamicLink.h

@@ -67,6 +67,12 @@ NS_SWIFT_NAME(DynamicLink)
  */
 @property(nonatomic, assign, readonly) FIRDLMatchType matchType;
 
+/**
+ * @property utmParametersDictionary
+ * @abstract UTM parameters associated with a Firebase Dynamic Link.
+ */
+@property(nonatomic, copy, readonly) NSDictionary<NSString *, id> *utmParametersDictionary;
+
 /**
  * @property minimumAppVersion
  * @abstract The minimum iOS application version that supports the Dynamic Link. This is retrieved

+ 6 - 4
FirebaseDynamicLinks/Tests/Sample/FDLBuilderTestAppObjC/AppDelegate.m

@@ -84,10 +84,12 @@
 
   UIAlertController *alertVC = [UIAlertController
       alertControllerWithTitle:@"Got Dynamic Link!"
-                       message:[NSString stringWithFormat:
-                                             @"URL [%@], matchType [%ld], minimumAppVersion [%@]",
-                                             dynamicLink.url, (unsigned long)dynamicLink.matchType,
-                                             dynamicLink.minimumAppVersion]
+                       message:[NSString stringWithFormat:@"URL [%@], matchType [%ld], "
+                                                          @"minimumAppVersion [%@], utmParams [%@]",
+                                                          dynamicLink.url,
+                                                          (unsigned long)dynamicLink.matchType,
+                                                          dynamicLink.minimumAppVersion,
+                                                          dynamicLink.utmParametersDictionary]
                 preferredStyle:UIAlertControllerStyleAlert];
   [alertVC addAction:[UIAlertAction actionWithTitle:@"Dismiss"
                                               style:UIAlertActionStyleCancel

+ 6 - 4
FirebaseDynamicLinks/Tests/Sample/FDLBuilderTestAppObjC/SceneDelegate.m

@@ -92,10 +92,12 @@
 
   UIAlertController *alertVC = [UIAlertController
       alertControllerWithTitle:@"Got Dynamic Link!"
-                       message:[NSString stringWithFormat:
-                                             @"URL [%@], matchType [%ld], minimumAppVersion [%@]",
-                                             dynamicLink.url, (unsigned long)dynamicLink.matchType,
-                                             dynamicLink.minimumAppVersion]
+                       message:[NSString stringWithFormat:@"URL [%@], matchType [%ld], "
+                                                          @"minimumAppVersion [%@], utmParams [%@]",
+                                                          dynamicLink.url,
+                                                          (unsigned long)dynamicLink.matchType,
+                                                          dynamicLink.minimumAppVersion,
+                                                          dynamicLink.utmParametersDictionary]
                 preferredStyle:UIAlertControllerStyleAlert];
   [alertVC addAction:[UIAlertAction actionWithTitle:@"Dismiss"
                                               style:UIAlertActionStyleCancel

+ 1 - 0
FirebaseDynamicLinks/Tests/Sample/Podfile

@@ -1,3 +1,4 @@
+source 'https://github.com/firebase/SpecsDev.git'
 source 'https://github.com/firebase/SpecsStaging.git'
 source 'https://cdn.cocoapods.org/'
 

+ 1 - 1
FirebaseFirestore.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseFirestore'
-  s.version          = '7.6.0'
+  s.version          = '7.7.0'
   s.summary          = 'Google Cloud Firestore'
 
   s.description      = <<-DESC

+ 1 - 1
FirebaseFirestoreSwift.podspec

@@ -5,7 +5,7 @@
 
 Pod::Spec.new do |s|
   s.name                    = 'FirebaseFirestoreSwift'
-  s.version                 = '7.6.0-beta'
+  s.version                 = '7.7.0-beta'
   s.summary                 = 'Swift Extensions for Google Cloud Firestore'
 
   s.description      = <<-DESC

+ 1 - 1
FirebaseFunctions.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseFunctions'
-  s.version          = '7.6.0'
+  s.version          = '7.7.0'
   s.summary          = 'Cloud Functions for Firebase'
 
   s.description      = <<-DESC

+ 1 - 1
FirebaseInAppMessaging.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseInAppMessaging'
-  s.version          = '7.6.0-beta'
+  s.version          = '7.7.0-beta'
   s.summary          = 'Firebase In-App Messaging for iOS'
 
   s.description      = <<-DESC

+ 1 - 0
FirebaseInAppMessaging/Tests/Integration/DefaultUITestApp/Podfile

@@ -1,3 +1,4 @@
+source 'https://github.com/firebase/SpecsDev.git'
 source 'https://github.com/firebase/SpecsStaging.git'
 source 'https://cdn.cocoapods.org/'
 

+ 1 - 0
FirebaseInAppMessaging/Tests/Integration/FunctionalTestApp/Podfile

@@ -1,5 +1,6 @@
 use_frameworks!
 
+source 'https://github.com/firebase/SpecsDev.git'
 source 'https://github.com/firebase/SpecsStaging.git'
 source 'https://cdn.cocoapods.org/'
 

+ 1 - 1
FirebaseInstallations.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseInstallations'
-  s.version          = '7.6.0'
+  s.version          = '7.7.0'
   s.summary          = 'Firebase Installations'
 
   s.description      = <<-DESC

+ 1 - 1
FirebaseInstanceID.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseInstanceID'
-  s.version          = '7.6.0'
+  s.version          = '7.7.0'
   s.summary          = 'Firebase InstanceID'
 
   s.description      = <<-DESC

+ 1 - 2
FirebaseMLModelDownloader.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseMLModelDownloader'
-  s.version          = '7.6.0-beta'
+  s.version          = '7.7.0-beta'
   s.summary          = 'Firebase ML Model Downloader'
 
   s.description      = <<-DESC
@@ -29,7 +29,6 @@ Pod::Spec.new do |s|
   s.watchos.deployment_target = watchos_deployment_target
 
   s.cocoapods_version = '>= 1.4.0'
-  s.static_framework = true
   s.prefix_header_file = false
 
   s.source_files = [

+ 52 - 0
FirebaseMLModelDownloader/Sources/ModelDownloadTask.swift

@@ -93,12 +93,20 @@ extension ModelDownloadTask {
                             messageCode: .anotherDownloadInProgressError)
       telemetryLogger?.logModelDownloadEvent(eventName: .modelDownload,
                                              status: .failed,
+                                             model: CustomModel(name: remoteModelInfo.name,
+                                                                size: remoteModelInfo.size,
+                                                                path: "",
+                                                                hash: remoteModelInfo.modelHash),
                                              downloadErrorCode: .downloadFailed)
       return
     }
     downloadStatus = .downloading
     telemetryLogger?.logModelDownloadEvent(eventName: .modelDownload,
                                            status: .downloading,
+                                           model: CustomModel(name: remoteModelInfo.name,
+                                                              size: remoteModelInfo.size,
+                                                              path: "",
+                                                              hash: remoteModelInfo.modelHash),
                                            downloadErrorCode: .noError)
     downloader.downloadFile(with: remoteModelInfo.downloadURL,
                             progressHandler: { downloadedBytes, totalBytes in
@@ -131,6 +139,10 @@ extension ModelDownloadTask {
           self.telemetryLogger?.logModelDownloadEvent(
             eventName: .modelDownload,
             status: .failed,
+            model: CustomModel(name: self.remoteModelInfo.name,
+                               size: self.remoteModelInfo.size,
+                               path: "",
+                               hash: self.remoteModelInfo.modelHash),
             downloadErrorCode: .noConnection
           )
         case FileDownloaderError.unexpectedResponseType:
@@ -142,6 +154,10 @@ extension ModelDownloadTask {
           self.telemetryLogger?.logModelDownloadEvent(
             eventName: .modelDownload,
             status: .failed,
+            model: CustomModel(name: self.remoteModelInfo.name,
+                               size: self.remoteModelInfo.size,
+                               path: "",
+                               hash: self.remoteModelInfo.modelHash),
             downloadErrorCode: .downloadFailed
           )
         default:
@@ -153,6 +169,10 @@ extension ModelDownloadTask {
           self.telemetryLogger?.logModelDownloadEvent(
             eventName: .modelDownload,
             status: .failed,
+            model: CustomModel(name: self.remoteModelInfo.name,
+                               size: self.remoteModelInfo.size,
+                               path: "",
+                               hash: self.remoteModelInfo.modelHash),
             downloadErrorCode: .downloadFailed
           )
         }
@@ -175,6 +195,10 @@ extension ModelDownloadTask {
           telemetryLogger?.logModelDownloadEvent(
             eventName: .modelDownload,
             status: .failed,
+            model: CustomModel(name: remoteModelInfo.name,
+                               size: remoteModelInfo.size,
+                               path: "",
+                               hash: remoteModelInfo.modelHash),
             downloadErrorCode: .httpError(code: response.statusCode)
           )
           completion(.failure(.invalidArgument))
@@ -186,6 +210,10 @@ extension ModelDownloadTask {
         telemetryLogger?.logModelDownloadEvent(
           eventName: .modelDownload,
           status: .failed,
+          model: CustomModel(name: remoteModelInfo.name,
+                             size: remoteModelInfo.size,
+                             path: "",
+                             hash: remoteModelInfo.modelHash),
           downloadErrorCode: .urlExpired
         )
         completion(.failure(.expiredDownloadURL))
@@ -196,6 +224,10 @@ extension ModelDownloadTask {
         telemetryLogger?.logModelDownloadEvent(
           eventName: .modelDownload,
           status: .failed,
+          model: CustomModel(name: remoteModelInfo.name,
+                             size: remoteModelInfo.size,
+                             path: "",
+                             hash: remoteModelInfo.modelHash),
           downloadErrorCode: .httpError(code: response.statusCode)
         )
         completion(.failure(.permissionDenied))
@@ -207,6 +239,10 @@ extension ModelDownloadTask {
         telemetryLogger?.logModelDownloadEvent(
           eventName: .modelDownload,
           status: .failed,
+          model: CustomModel(name: remoteModelInfo.name,
+                             size: remoteModelInfo.size,
+                             path: "",
+                             hash: remoteModelInfo.modelHash),
           downloadErrorCode: .httpError(code: response.statusCode)
         )
         completion(.failure(.notFound))
@@ -219,6 +255,10 @@ extension ModelDownloadTask {
         telemetryLogger?.logModelDownloadEvent(
           eventName: .modelDownload,
           status: .failed,
+          model: CustomModel(name: remoteModelInfo.name,
+                             size: remoteModelInfo.size,
+                             path: "",
+                             hash: remoteModelInfo.modelHash),
           downloadErrorCode: .httpError(code: response.statusCode)
         )
         completion(.failure(.internalError(description: description)))
@@ -239,6 +279,10 @@ extension ModelDownloadTask {
       // Downloading the file succeeding but saving failed.
       telemetryLogger?.logModelDownloadEvent(eventName: .modelDownload,
                                              status: .succeeded,
+                                             model: CustomModel(name: remoteModelInfo.name,
+                                                                size: remoteModelInfo.size,
+                                                                path: "",
+                                                                hash: remoteModelInfo.modelHash),
                                              downloadErrorCode: .downloadFailed)
       completion(.failure(.internalError(description: description)))
       return
@@ -296,6 +340,10 @@ extension ModelDownloadTask {
       // Downloading the file succeeding but saving failed.
       telemetryLogger?.logModelDownloadEvent(eventName: .modelDownload,
                                              status: .succeeded,
+                                             model: CustomModel(name: remoteModelInfo.name,
+                                                                size: remoteModelInfo.size,
+                                                                path: "",
+                                                                hash: remoteModelInfo.modelHash),
                                              downloadErrorCode: .downloadFailed)
       completion(.failure(error))
       return
@@ -306,6 +354,10 @@ extension ModelDownloadTask {
       // Downloading the file succeeding but saving failed.
       telemetryLogger?.logModelDownloadEvent(eventName: .modelDownload,
                                              status: .succeeded,
+                                             model: CustomModel(name: remoteModelInfo.name,
+                                                                size: remoteModelInfo.size,
+                                                                path: "",
+                                                                hash: remoteModelInfo.modelHash),
                                              downloadErrorCode: .downloadFailed)
       completion(.failure(.internalError(description: error.localizedDescription)))
       return

+ 2 - 0
FirebaseMLModelDownloader/Sources/ModelDownloader.swift

@@ -162,6 +162,7 @@ public class ModelDownloader {
         telemetryLogger?.logModelDownloadEvent(
           eventName: .modelDownload,
           status: .scheduled,
+          model: CustomModel(name: modelName, size: 0, path: "", hash: ""),
           downloadErrorCode: .noError
         )
         // Update local model in the background.
@@ -191,6 +192,7 @@ public class ModelDownloader {
                 self?.telemetryLogger?.logModelDownloadEvent(
                   eventName: .modelDownload,
                   status: .failed,
+                  model: CustomModel(name: modelName, size: 0, path: "", hash: ""),
                   downloadErrorCode: .downloadFailed
                 )
               }

+ 113 - 0
FirebaseMLModelDownloader/Sources/ModelInfoRetriever.swift

@@ -153,6 +153,10 @@ class ModelInfoRetriever {
                                 messageCode: .invalidModelInfoFetchURL)
           self.telemetryLogger?.logModelInfoRetrievalEvent(eventName: .modelDownload,
                                                            status: .modelInfoRetrievalFailed,
+                                                           model: CustomModel(name: self.modelName,
+                                                                              size: 0,
+                                                                              path: "",
+                                                                              hash: ""),
                                                            modelInfoErrorCode: .connectionFailed)
           completion(.failure(.internalError(description: ModelInfoRetriever.ErrorDescription
               .invalidModelInfoFetchURL)))
@@ -169,6 +173,12 @@ class ModelInfoRetriever {
                                   messageCode: .modelInfoRetrievalError)
             self.telemetryLogger?.logModelInfoRetrievalEvent(eventName: .modelDownload,
                                                              status: .modelInfoRetrievalFailed,
+                                                             model: CustomModel(
+                                                               name: self.modelName,
+                                                               size: 0,
+                                                               path: "",
+                                                               hash: ""
+                                                             ),
                                                              modelInfoErrorCode: .connectionFailed)
             completion(.failure(.internalError(description: description)))
           } else {
@@ -179,6 +189,12 @@ class ModelInfoRetriever {
                                     messageCode: .invalidHTTPResponse)
               self.telemetryLogger?.logModelInfoRetrievalEvent(eventName: .modelDownload,
                                                                status: .modelInfoRetrievalFailed,
+                                                               model: CustomModel(
+                                                                 name: self.modelName,
+                                                                 size: 0,
+                                                                 path: "",
+                                                                 hash: ""
+                                                               ),
                                                                modelInfoErrorCode: .connectionFailed)
               completion(.failure(.internalError(description: ModelInfoRetriever.ErrorDescription
                   .invalidHTTPResponse)))
@@ -197,6 +213,12 @@ class ModelInfoRetriever {
                                       messageCode: .missingModelHash)
                 self.telemetryLogger?.logModelInfoRetrievalEvent(eventName: .modelDownload,
                                                                  status: .modelInfoRetrievalFailed,
+                                                                 model: CustomModel(
+                                                                   name: self.modelName,
+                                                                   size: 0,
+                                                                   path: "",
+                                                                   hash: ""
+                                                                 ),
                                                                  modelInfoErrorCode: .noHash)
                 completion(.failure(.internalError(description: ModelInfoRetriever.ErrorDescription
                     .missingModelHash)))
@@ -207,6 +229,15 @@ class ModelInfoRetriever {
                                       message: ModelInfoRetriever.ErrorDescription
                                         .invalidHTTPResponse,
                                       messageCode: .invalidHTTPResponse)
+                self.telemetryLogger?.logModelInfoRetrievalEvent(eventName: .modelDownload,
+                                                                 status: .modelInfoRetrievalFailed,
+                                                                 model: CustomModel(
+                                                                   name: self.modelName,
+                                                                   size: 0,
+                                                                   path: "",
+                                                                   hash: ""
+                                                                 ),
+                                                                 modelInfoErrorCode: .unknown)
                 completion(.failure(.internalError(description: ModelInfoRetriever.ErrorDescription
                     .invalidHTTPResponse)))
                 return
@@ -220,6 +251,12 @@ class ModelInfoRetriever {
                                       messageCode: .modelInfoDownloaded)
                 self.telemetryLogger?.logModelInfoRetrievalEvent(eventName: .modelDownload,
                                                                  status: .modelInfoRetrievalSucceeded,
+                                                                 model: CustomModel(
+                                                                   name: self.modelName,
+                                                                   size: modelInfo.size,
+                                                                   path: "",
+                                                                   hash: modelInfo.modelHash
+                                                                 ),
                                                                  modelInfoErrorCode: .noError)
                 completion(.success(.modelInfo(modelInfo)))
               } catch {
@@ -228,6 +265,15 @@ class ModelInfoRetriever {
                 DeviceLogger.logEvent(level: .debug,
                                       message: description,
                                       messageCode: .invalidModelInfoJSON)
+                self.telemetryLogger?.logModelInfoRetrievalEvent(eventName: .modelDownload,
+                                                                 status: .modelInfoRetrievalFailed,
+                                                                 model: CustomModel(
+                                                                   name: self.modelName,
+                                                                   size: 0,
+                                                                   path: "",
+                                                                   hash: ""
+                                                                 ),
+                                                                 modelInfoErrorCode: .unknown)
                 completion(
                   .failure(.internalError(description: description))
                 )
@@ -239,6 +285,15 @@ class ModelInfoRetriever {
                                       message: ModelInfoRetriever.ErrorDescription
                                         .unexpectedModelInfoDeletion,
                                       messageCode: .modelInfoDeleted)
+                self.telemetryLogger?.logModelInfoRetrievalEvent(eventName: .modelDownload,
+                                                                 status: .modelInfoRetrievalFailed,
+                                                                 model: CustomModel(
+                                                                   name: self.modelName,
+                                                                   size: 0,
+                                                                   path: "",
+                                                                   hash: ""
+                                                                 ),
+                                                                 modelInfoErrorCode: .unknown)
                 completion(
                   .failure(.internalError(description: ModelInfoRetriever.ErrorDescription
                       .unexpectedModelInfoDeletion))
@@ -253,6 +308,12 @@ class ModelInfoRetriever {
                                       messageCode: .noModelHash)
                 self.telemetryLogger?.logModelInfoRetrievalEvent(eventName: .modelDownload,
                                                                  status: .modelInfoRetrievalFailed,
+                                                                 model: CustomModel(
+                                                                   name: self.modelName,
+                                                                   size: 0,
+                                                                   path: "",
+                                                                   hash: ""
+                                                                 ),
                                                                  modelInfoErrorCode: .noHash)
                 completion(.failure(.internalError(description: ModelInfoRetriever.ErrorDescription
                     .missingModelHash)))
@@ -266,6 +327,12 @@ class ModelInfoRetriever {
                                       messageCode: .modelHashMismatchError)
                 self.telemetryLogger?.logModelInfoRetrievalEvent(eventName: .modelDownload,
                                                                  status: .modelInfoRetrievalFailed,
+                                                                 model: CustomModel(
+                                                                   name: self.modelName,
+                                                                   size: 0,
+                                                                   path: "",
+                                                                   hash: modelHash
+                                                                 ),
                                                                  modelInfoErrorCode: .hashMismatch)
                 completion(.failure(.internalError(description: ModelInfoRetriever.ErrorDescription
                     .modelHashMismatch)))
@@ -277,6 +344,12 @@ class ModelInfoRetriever {
                                     messageCode: .modelInfoUnmodified)
               self.telemetryLogger?.logModelInfoRetrievalEvent(eventName: .modelDownload,
                                                                status: .modelInfoRetrievalSucceeded,
+                                                               model: CustomModel(
+                                                                 name: self.modelName,
+                                                                 size: localInfo.size,
+                                                                 path: "",
+                                                                 hash: localInfo.modelHash
+                                                               ),
                                                                modelInfoErrorCode: .noError)
               completion(.success(.notModified))
             case 400:
@@ -288,6 +361,12 @@ class ModelInfoRetriever {
                                     messageCode: .invalidArgument)
               self.telemetryLogger?.logModelInfoRetrievalEvent(eventName: .modelDownload,
                                                                status: .modelInfoRetrievalFailed,
+                                                               model: CustomModel(
+                                                                 name: self.modelName,
+                                                                 size: 0,
+                                                                 path: "",
+                                                                 hash: ""
+                                                               ),
                                                                modelInfoErrorCode: .httpError(code: httpResponse
                                                                  .statusCode))
               completion(.failure(.invalidArgument))
@@ -300,6 +379,12 @@ class ModelInfoRetriever {
                                     messageCode: .permissionDenied)
               self.telemetryLogger?.logModelInfoRetrievalEvent(eventName: .modelDownload,
                                                                status: .modelInfoRetrievalFailed,
+                                                               model: CustomModel(
+                                                                 name: self.modelName,
+                                                                 size: 0,
+                                                                 path: "",
+                                                                 hash: ""
+                                                               ),
                                                                modelInfoErrorCode: .httpError(code: httpResponse
                                                                  .statusCode))
               completion(.failure(.permissionDenied))
@@ -312,6 +397,12 @@ class ModelInfoRetriever {
                                     messageCode: .modelNotFound)
               self.telemetryLogger?.logModelInfoRetrievalEvent(eventName: .modelDownload,
                                                                status: .modelInfoRetrievalFailed,
+                                                               model: CustomModel(
+                                                                 name: self.modelName,
+                                                                 size: 0,
+                                                                 path: "",
+                                                                 hash: ""
+                                                               ),
                                                                modelInfoErrorCode: .httpError(code: httpResponse
                                                                  .statusCode))
               completion(.failure(.notFound))
@@ -324,6 +415,12 @@ class ModelInfoRetriever {
                                     messageCode: .resourceExhausted)
               self.telemetryLogger?.logModelInfoRetrievalEvent(eventName: .modelDownload,
                                                                status: .modelInfoRetrievalFailed,
+                                                               model: CustomModel(
+                                                                 name: self.modelName,
+                                                                 size: 0,
+                                                                 path: "",
+                                                                 hash: ""
+                                                               ),
                                                                modelInfoErrorCode: .httpError(code: httpResponse
                                                                  .statusCode))
               completion(.failure(.resourceExhausted))
@@ -336,6 +433,12 @@ class ModelInfoRetriever {
                                     messageCode: .modelInfoRetrievalError)
               self.telemetryLogger?.logModelInfoRetrievalEvent(eventName: .modelDownload,
                                                                status: .modelInfoRetrievalFailed,
+                                                               model: CustomModel(
+                                                                 name: self.modelName,
+                                                                 size: 0,
+                                                                 path: "",
+                                                                 hash: ""
+                                                               ),
                                                                modelInfoErrorCode: .httpError(code: httpResponse
                                                                  .statusCode))
               completion(.failure(.internalError(description: description)))
@@ -348,6 +451,15 @@ class ModelInfoRetriever {
                               message: ModelInfoRetriever.ErrorDescription
                                 .authTokenError,
                               messageCode: .authTokenError)
+        self.telemetryLogger?.logModelInfoRetrievalEvent(eventName: .modelDownload,
+                                                         status: .modelInfoRetrievalFailed,
+                                                         model: CustomModel(
+                                                           name: self.modelName,
+                                                           size: 0,
+                                                           path: "",
+                                                           hash: ""
+                                                         ),
+                                                         modelInfoErrorCode: .unknown)
         completion(.failure(.internalError(description: ModelInfoRetriever.ErrorDescription
             .authTokenError)))
         return
@@ -463,6 +575,7 @@ enum ModelInfoErrorCode {
   case httpError(code: Int)
   case connectionFailed
   case hashMismatch
+  case unknown
 }
 
 /// Possible error messages for model info retrieval.

+ 39 - 17
FirebaseMLModelDownloader/Sources/TelemetryLogger.swift

@@ -28,15 +28,23 @@ extension SystemInfo {
   }
 }
 
+/// Extension to set model info.
+extension ModelInfo {
+  mutating func setModelInfo(modelName: String, modelHash: String) {
+    name = modelName
+    if !modelHash.isEmpty {
+      hash = modelHash
+    }
+    modelType = .custom
+  }
+}
+
 /// Extension to set model options.
 extension ModelOptions {
-  mutating func setModelOptions(model: CustomModel, isModelUpdateEnabled: Bool? = nil) {
-    if let updateEnabled = isModelUpdateEnabled {
-      self.isModelUpdateEnabled = updateEnabled
-    }
-    modelInfo.name = model.name
-    modelInfo.hash = model.hash
-    modelInfo.modelType = .custom
+  mutating func setModelOptions(modelName: String, modelHash: String) {
+    var modelInfo = ModelInfo()
+    modelInfo.setModelInfo(modelName: modelName, modelHash: modelHash)
+    self.modelInfo = modelInfo
   }
 }
 
@@ -51,8 +59,8 @@ extension DeleteModelLogEvent {
 /// Extension to build model download log event.
 extension ModelDownloadLogEvent {
   mutating func setEvent(status: DownloadStatus, errorCode: ErrorCode,
-                         roughDownloadDuration: UInt64? = nil, exactDownloadDuration: UInt64? = nil,
-                         downloadFailureStatus: Int64? = nil, modelOptions: ModelOptions) {
+                         roughDownloadDuration: UInt64? = 0, exactDownloadDuration: UInt64? = 0,
+                         downloadFailureStatus: Int64? = 0, modelOptions: ModelOptions) {
     downloadStatus = status
     self.errorCode = errorCode
     if let roughDuration = roughDownloadDuration {
@@ -162,6 +170,7 @@ class TelemetryLogger {
   /// Log model info retrieval event to Firelog.
   func logModelInfoRetrievalEvent(eventName: EventName,
                                   status: ModelDownloadLogEvent.DownloadStatus,
+                                  model: CustomModel,
                                   modelInfoErrorCode: ModelInfoErrorCode) {
     guard app.isDataCollectionDefaultEnabled else { return }
     var systemInfo = SystemInfo()
@@ -169,6 +178,7 @@ class TelemetryLogger {
     let projectID = app.options.projectID
     systemInfo.setAppInfo(apiKey: apiKey, projectID: projectID)
     var errorCode = ErrorCode()
+    var failureCode: Int64?
     switch modelInfoErrorCode {
     case .noError:
       errorCode = .noError
@@ -179,13 +189,21 @@ class TelemetryLogger {
     case .hashMismatch:
       errorCode = .modelHashMismatch
     case let .httpError(code):
-      errorCode = ErrorCode(rawValue: code) ?? .modelInfoDownloadUnsuccessfulHTTPStatus
+      errorCode = .modelInfoDownloadUnsuccessfulHTTPStatus
+      failureCode = Int64(code)
+    case .unknown:
+      errorCode = .unknownError
     }
-    let modelOptions = ModelOptions()
+    var modelOptions = ModelOptions()
+    modelOptions.setModelOptions(
+      modelName: model.name,
+      modelHash: model.hash
+    )
     var modelDownloadLogEvent = ModelDownloadLogEvent()
     modelDownloadLogEvent.setEvent(
       status: status,
       errorCode: errorCode,
+      downloadFailureStatus: failureCode,
       modelOptions: modelOptions
     )
     var fbmlEvent = FirebaseMlLogEvent()
@@ -198,19 +216,21 @@ class TelemetryLogger {
   }
 
   /// Log model download event to Firelog.
-  func logModelDownloadEvent(eventName: EventName, status: ModelDownloadLogEvent.DownloadStatus,
-                             model: CustomModel? = nil, downloadErrorCode: ModelDownloadErrorCode) {
+  func logModelDownloadEvent(eventName: EventName,
+                             status: ModelDownloadLogEvent.DownloadStatus,
+                             model: CustomModel,
+                             downloadErrorCode: ModelDownloadErrorCode) {
     guard app.isDataCollectionDefaultEnabled else { return }
     var modelOptions = ModelOptions()
-    if let model = model {
-      modelOptions.setModelOptions(model: model)
-    }
+    modelOptions.setModelOptions(modelName: model.name, modelHash: model.hash)
     var systemInfo = SystemInfo()
     let apiKey = app.options.apiKey
     let projectID = app.options.projectID
     systemInfo.setAppInfo(apiKey: apiKey, projectID: projectID)
 
     var errorCode = ErrorCode()
+    var failureCode: Int64?
+
     switch downloadErrorCode {
     case .noError:
       errorCode = .noError
@@ -221,13 +241,15 @@ class TelemetryLogger {
     case .downloadFailed:
       errorCode = .downloadFailed
     case let .httpError(code):
-      errorCode = ErrorCode(rawValue: code) ?? .unknownError
+      errorCode = .unknownError
+      failureCode = Int64(code)
     }
 
     var modelDownloadLogEvent = ModelDownloadLogEvent()
     modelDownloadLogEvent.setEvent(
       status: status,
       errorCode: errorCode,
+      downloadFailureStatus: failureCode,
       modelOptions: modelOptions
     )
 

+ 34 - 22
FirebaseMLModelDownloader/Tests/Integration/ModelDownloaderIntegrationTests.swift

@@ -61,6 +61,7 @@ final class ModelDownloaderIntegrationTests: XCTestCase {
       XCTFail("Default app was not configured.")
       return
     }
+    testApp.isDataCollectionDefaultEnabled = false
 
     let testName = String(#function.dropLast(2))
     let testModelName = "pose-detection"
@@ -152,6 +153,8 @@ final class ModelDownloaderIntegrationTests: XCTestCase {
       XCTFail("Default app was not configured.")
       return
     }
+    testApp.isDataCollectionDefaultEnabled = false
+
     let testName = String(#function.dropLast(2))
     let testModelName = "\(testName)-test-model"
     let urlString =
@@ -208,6 +211,8 @@ final class ModelDownloaderIntegrationTests: XCTestCase {
       XCTFail("Default app was not configured.")
       return
     }
+    testApp.isDataCollectionDefaultEnabled = false
+
     let testName = String(#function.dropLast(2))
     let testModelName = "image-classification"
 
@@ -308,6 +313,8 @@ final class ModelDownloaderIntegrationTests: XCTestCase {
       XCTFail("Default app was not configured.")
       return
     }
+    testApp.isDataCollectionDefaultEnabled = false
+
     let testName = String(#function.dropLast(2))
     let emptyModelName = ""
 
@@ -351,6 +358,7 @@ final class ModelDownloaderIntegrationTests: XCTestCase {
       XCTFail("Default app was not configured.")
       return
     }
+    testApp.isDataCollectionDefaultEnabled = false
 
     let testName = String(#function.dropLast(2))
     let testModelName = "pose-detection"
@@ -410,6 +418,8 @@ final class ModelDownloaderIntegrationTests: XCTestCase {
       XCTFail("Default app was not configured.")
       return
     }
+    testApp.isDataCollectionDefaultEnabled = false
+
     let testName = String(#function.dropLast(2))
     let testModelName = "pose-detection"
 
@@ -464,8 +474,10 @@ final class ModelDownloaderIntegrationTests: XCTestCase {
       XCTFail("Default app was not configured.")
       return
     }
+    // Flip this to `true` to test logging.
+    testApp.isDataCollectionDefaultEnabled = false
 
-    let testModelName = "image-classification"
+    let testModelName = "digit-classification"
     let testName = String(#function.dropLast(2))
 
     let conditions = ModelDownloadConditions()
@@ -482,18 +494,7 @@ final class ModelDownloaderIntegrationTests: XCTestCase {
       conditions: conditions
     ) { result in
       switch result {
-      case let .success(model):
-        guard let telemetryLogger = TelemetryLogger(app: testApp) else {
-          XCTFail("Could not initialize logger.")
-          return
-        }
-        // TODO: Remove actual logging and stub out with mocks.
-        telemetryLogger.logModelDownloadEvent(
-          eventName: .modelDownload,
-          status: .succeeded,
-          model: model,
-          downloadErrorCode: .noError
-        )
+      case .success: break
       case let .failure(error):
         XCTFail("Failed to download model - \(error)")
       }
@@ -504,14 +505,7 @@ final class ModelDownloaderIntegrationTests: XCTestCase {
     let deleteModelExpectation = expectation(description: "Test delete model telemetry.")
     modelDownloader.deleteDownloadedModel(name: testModelName) { result in
       switch result {
-      case .success(()):
-        guard let telemetryLogger = TelemetryLogger(app: testApp) else {
-          XCTFail("Could not initialize logger.")
-          return
-        }
-        // TODO: Remove actual logging and stub out with mocks.
-        telemetryLogger.logModelDeletedEvent(eventName: .remoteModelDeleteOnDevice,
-                                             isSuccessful: true)
+      case .success(()): break
       case let .failure(error):
         XCTFail("Failed to delete model - \(error)")
       }
@@ -519,6 +513,24 @@ final class ModelDownloaderIntegrationTests: XCTestCase {
     }
 
     wait(for: [deleteModelExpectation], timeout: 5)
+
+    let notFoundModelName = "fakeModelName"
+    let notFoundModelExpectation =
+      expectation(description: "Test get model telemetry with unknown model.")
+
+    modelDownloader.getModel(
+      name: notFoundModelName,
+      downloadType: .latestModel,
+      conditions: conditions
+    ) { result in
+      switch result {
+      case .success: XCTFail("Unexpected model success.")
+      case let .failure(error):
+        XCTAssertEqual(error, .notFound)
+      }
+      notFoundModelExpectation.fulfill()
+    }
+    wait(for: [notFoundModelExpectation], timeout: 5)
   }
 
   func testGetModelWithConditions() {
@@ -526,11 +538,11 @@ final class ModelDownloaderIntegrationTests: XCTestCase {
       XCTFail("Default app was not configured.")
       return
     }
+    testApp.isDataCollectionDefaultEnabled = false
 
     let testModelName = "pose-detection"
     let testName = String(#function.dropLast(2))
 
-    // TODO: Figure out a better way to test this.
     let conditions = ModelDownloadConditions(allowsCellularAccess: false)
 
     let modelDownloader = ModelDownloader.modelDownloaderWithDefaults(

+ 68 - 1
FirebaseMLModelDownloader/Tests/Unit/ModelDownloaderUnitTests.swift

@@ -82,6 +82,8 @@ final class ModelDownloaderUnitTests: XCTestCase {
       XCTFail("Default app was not configured.")
       return
     }
+    testApp.isDataCollectionDefaultEnabled = false
+
     let modelDownloader1 = ModelDownloader.modelDownloader(app: testApp)
     let modelDownloader2 = ModelDownloader.modelDownloader()
     XCTAssert(modelDownloader1 === modelDownloader2)
@@ -93,6 +95,8 @@ final class ModelDownloaderUnitTests: XCTestCase {
       XCTFail("Default app was not configured.")
       return
     }
+    testApp.isDataCollectionDefaultEnabled = false
+
     let modelDownloader1 = ModelDownloader.modelDownloader()
     testApp.delete { success in
       XCTAssertTrue(success)
@@ -104,6 +108,12 @@ final class ModelDownloaderUnitTests: XCTestCase {
 
   /// Test invalid model file deletion.
   func testDeleteInvalidModel() {
+    guard let testApp = FirebaseApp.app() else {
+      XCTFail("Default app was not configured.")
+      return
+    }
+    testApp.isDataCollectionDefaultEnabled = false
+
     let modelDownloader = ModelDownloader.modelDownloader()
     modelDownloader.deleteDownloadedModel(name: fakeModelName) { result in
       switch result {
@@ -127,6 +137,8 @@ final class ModelDownloaderUnitTests: XCTestCase {
       XCTFail("Default app was not configured.")
       return
     }
+    testApp.isDataCollectionDefaultEnabled = false
+
     let testName = String(#function.dropLast(2))
     let modelDownloader = ModelDownloader.modelDownloaderWithDefaults(
       .createUnitTestInstance(testName: testName),
@@ -155,6 +167,12 @@ final class ModelDownloaderUnitTests: XCTestCase {
 
   /// Test invalid path in model directory.
   func testListModelsInvalidPath() {
+    guard let testApp = FirebaseApp.app() else {
+      XCTFail("Default app was not configured.")
+      return
+    }
+    testApp.isDataCollectionDefaultEnabled = false
+
     let modelDownloader = ModelDownloader.modelDownloader()
     let invalidTempModelFileURL = tempModelFile(invalid: true)
     let completionExpectation = expectation(description: "Completion handler")
@@ -183,6 +201,7 @@ final class ModelDownloaderUnitTests: XCTestCase {
       XCTFail("Default app was not configured.")
       return
     }
+    testApp.isDataCollectionDefaultEnabled = false
 
     let testName = String(#function.dropLast(2))
     let tempModelFileURL = tempModelFile()
@@ -242,7 +261,7 @@ final class ModelDownloaderUnitTests: XCTestCase {
       hash: fakeModelHash
     )
     var modelOptions = ModelOptions()
-    modelOptions.setModelOptions(model: fakeModel)
+    modelOptions.setModelOptions(modelName: fakeModel.name, modelHash: fakeModel.hash)
 
     guard let binaryData = try? modelOptions.serializedData(),
       let jsonData = try? modelOptions.jsonUTF8Data(),
@@ -681,6 +700,12 @@ final class ModelDownloaderUnitTests: XCTestCase {
 
   /// Get model if multiple duplicate requests are made (test at the API surface).
   func testGetModelWithMergeRequests() {
+    guard let testApp = FirebaseApp.app() else {
+      XCTFail("Default app was not configured.")
+      return
+    }
+    testApp.isDataCollectionDefaultEnabled = false
+
     let modelDownloader = ModelDownloader.modelDownloader()
     let conditions = ModelDownloadConditions()
     let session = fakeModelInfoSessionWithURL(fakeDownloadURL, statusCode: 200)
@@ -814,6 +839,12 @@ final class ModelDownloaderUnitTests: XCTestCase {
 
   /// Get model with a valid server response.
   func testGetModelSuccessful() {
+    guard let testApp = FirebaseApp.app() else {
+      XCTFail("Default app was not configured.")
+      return
+    }
+    testApp.isDataCollectionDefaultEnabled = false
+
     let modelDownloader = ModelDownloader.modelDownloader()
     let conditions = ModelDownloadConditions()
     let session = fakeModelInfoSessionWithURL(fakeDownloadURL, statusCode: 200)
@@ -860,6 +891,12 @@ final class ModelDownloaderUnitTests: XCTestCase {
 
   /// Get model if download url expired but retry was successful.
   func testGetModelSuccessfulRetry() {
+    guard let testApp = FirebaseApp.app() else {
+      XCTFail("Default app was not configured.")
+      return
+    }
+    testApp.isDataCollectionDefaultEnabled = false
+
     let modelDownloader = ModelDownloader.modelDownloader()
     let conditions = ModelDownloadConditions()
     let session = fakeModelInfoSessionWithURL(fakeDownloadURL, statusCode: 200)
@@ -923,6 +960,12 @@ final class ModelDownloaderUnitTests: XCTestCase {
 
   /// Get model if model doesn't exist.
   func testGetModelNotFound() {
+    guard let testApp = FirebaseApp.app() else {
+      XCTFail("Default app was not configured.")
+      return
+    }
+    testApp.isDataCollectionDefaultEnabled = false
+
     let modelDownloader = ModelDownloader.modelDownloader()
     let conditions = ModelDownloadConditions()
     let session = fakeModelInfoSessionWithURL(fakeDownloadURL, statusCode: 200)
@@ -971,6 +1014,12 @@ final class ModelDownloaderUnitTests: XCTestCase {
 
   /// Get model if invalid or insufficient permissions.
   func testGetModelUnauthorized() {
+    guard let testApp = FirebaseApp.app() else {
+      XCTFail("Default app was not configured.")
+      return
+    }
+    testApp.isDataCollectionDefaultEnabled = false
+
     let modelDownloader = ModelDownloader.modelDownloader()
     let conditions = ModelDownloadConditions()
     let session = fakeModelInfoSessionWithURL(fakeDownloadURL, statusCode: 200)
@@ -1018,6 +1067,12 @@ final class ModelDownloaderUnitTests: XCTestCase {
 
   /// Get model if download url expired and retry url also expired.
   func testGetModelFailedRetry() {
+    guard let testApp = FirebaseApp.app() else {
+      XCTFail("Default app was not configured.")
+      return
+    }
+    testApp.isDataCollectionDefaultEnabled = false
+
     let modelDownloader = ModelDownloader.modelDownloader()
     let conditions = ModelDownloadConditions()
     let session = fakeModelInfoSessionWithURL(fakeDownloadURL, statusCode: 200)
@@ -1078,6 +1133,12 @@ final class ModelDownloaderUnitTests: XCTestCase {
 
   /// Get model if multiple retries all returned expired urls.
   func testGetModelMultipleFailedRetries() {
+    guard let testApp = FirebaseApp.app() else {
+      XCTFail("Default app was not configured.")
+      return
+    }
+    testApp.isDataCollectionDefaultEnabled = false
+
     let modelDownloader = ModelDownloader.modelDownloader()
     modelDownloader.numberOfRetries = 2
     let conditions = ModelDownloadConditions()
@@ -1150,6 +1211,12 @@ final class ModelDownloaderUnitTests: XCTestCase {
 
   /// Get model if a retry finally returns an unexpired download url.
   func testGetModelMultipleRetriesWithSuccess() {
+    guard let testApp = FirebaseApp.app() else {
+      XCTFail("Default app was not configured.")
+      return
+    }
+    testApp.isDataCollectionDefaultEnabled = false
+
     let modelDownloader = ModelDownloader.modelDownloader()
     modelDownloader.numberOfRetries = 4
     let conditions = ModelDownloadConditions()

+ 1 - 1
FirebaseMessaging.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebaseMessaging'
-  s.version          = '7.6.0'
+  s.version          = '7.7.0'
   s.summary          = 'Firebase Messaging'
 
   s.description      = <<-DESC

+ 1 - 0
FirebaseMessaging/Apps/AdvancedSample/Podfile

@@ -1,5 +1,6 @@
 use_frameworks! :linkage => :static
 
+source 'https://github.com/firebase/SpecsDev.git'
 source 'https://github.com/firebase/SpecsStaging.git'
 source 'https://cdn.cocoapods.org/'
 

+ 1 - 0
FirebaseMessaging/Apps/Sample/Podfile

@@ -1,5 +1,6 @@
 use_frameworks!
 
+source 'https://github.com/firebase/SpecsDev.git'
 source 'https://github.com/firebase/SpecsStaging.git'
 source 'https://cdn.cocoapods.org/'
 

+ 2 - 2
FirebaseMessaging/CHANGELOG.md

@@ -1,5 +1,5 @@
-# unreleased
-- [fixed] Fixed an issue that when checking storage size before writing to the disk, the client was checking the document folder that is no longer used. (#7480)
+# 2021-02 -- v7.7.0
+- [fixed] Fixed an issue in which, when checking storage size before writing to disk, the client was checking document folders that were no longer used. (#7480)
 
 # 2021-02 -- v7.6.0
 - [fixed] Fixed build warnings introduced with Xcode 12.5. (#7433)

+ 3 - 4
FirebasePerformance.podspec

@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name             = 'FirebasePerformance'
-  s.version          = '7.6.0'
+  s.version          = '7.7.0'
   s.summary          = 'Firebase Performance'
 
   s.description      = <<-DESC
@@ -24,7 +24,6 @@ Firebase Performance library to measure performance of Mobile and Web Apps.
   s.tvos.deployment_target = tvos_deployment_target
 
   s.cocoapods_version = '>= 1.4.0'
-  s.static_framework = true
   s.prefix_header_file = false
 
   base_dir = "FirebasePerformance/"
@@ -56,8 +55,8 @@ Firebase Performance library to measure performance of Mobile and Web Apps.
   }
 
   s.ios.framework = 'CoreTelephony'
-  s.ios.framework = 'QuartzCore'
-  s.ios.framework = 'SystemConfiguration'
+  s.framework = 'QuartzCore'
+  s.framework = 'SystemConfiguration'
   s.dependency 'FirebaseCore', '~> 7.0'
   s.dependency 'FirebaseInstallations', '~> 7.0'
   s.dependency 'FirebaseRemoteConfig', '~> 7.0'

+ 6 - 0
FirebasePerformance/CHANGELOG.md

@@ -1,4 +1,10 @@
 # Unreleased
+* Deprecate Clearcut event transport mechanism.
+* Enable dynamic framework support. (#7569)
+* Remove the warning to include Firebase Analytics as Perf does not depend on Analytics (#7487)
+* Fix the crash on gauge manager due to race condition. (#7535)
+
+# Version 7.7.0
 * Add community supported tvOS.
 
 # Version 7.6.0

+ 0 - 7
FirebasePerformance/Sources/Configurations/FPRConfigurations.h

@@ -187,13 +187,6 @@ FOUNDATION_EXTERN FPRConfigName kFPRConfigInstrumentationEnabled;
  */
 - (uint32_t)memorySamplingFrequencyInBackgroundInMS;
 
-/**
- * Returns a float specifying the transport percentage for FLL. Range [0-100].
- *
- * @return The percentage of devices sending events to FLL.
- */
-- (float_t)fllTransportPercentage;
-
 @end
 
 NS_ASSUME_NONNULL_END

+ 0 - 32
FirebasePerformance/Sources/Configurations/FPRConfigurations.m

@@ -474,36 +474,4 @@ static dispatch_once_t gSharedInstanceToken;
   return samplingFrequency;
 }
 
-#pragma mark - Google Data Transport related configurations.
-
-- (float_t)fllTransportPercentage {
-  // Order of precedence is:
-  //
-  // Any RC config flags exists?
-  //   -> Yes
-  //     -> If Transport flag exists, honor the value (active rollout scenario)
-  //     -> Otherwise, send to Fll (deprecation scenario)
-  //   -> No
-  //     -> Send to clearcut (onboarding scenario)
-  //
-  // If a PList override also exists than that takes the priority
-
-  // By default send to Clearcut
-  float transportPercentage = 0.0f;  // Range [0 - 100]
-
-  if (self.remoteConfigFlags && [self.remoteConfigFlags containsRemoteConfigFlags]) {
-    // If Transport flag exists, honor the value (active rollout scenario)
-    // Otherwise, send to Fll (deprecation scenario)
-    transportPercentage = [self.remoteConfigFlags fllTransportPercentageWithDefaultValue:100.0f];
-  }
-
-  // If a PList override also exists than that takes the priority
-  id plistObject = [self objectForInfoDictionaryKey:@"fllTransportPercentage"];
-  if (plistObject) {
-    transportPercentage = [plistObject floatValue];
-  }
-
-  return transportPercentage;
-}
-
 @end

+ 0 - 20
FirebasePerformance/Sources/Configurations/FPRRemoteConfigFlags.h

@@ -40,13 +40,6 @@
  */
 - (void)update;
 
-/**
- * Returns if there was a successful fetch in the past and if any remote config flag exists.
- *
- * @return YES if any remote config flag exists; NO otherwise.
- */
-- (BOOL)containsRemoteConfigFlags;
-
 #pragma mark - General configs.
 
 /**
@@ -207,17 +200,4 @@
  */
 - (int)sessionMaxDurationWithDefaultValue:(int)maxDurationInMinutes;
 
-#pragma mark - Google Data Transport related configs.
-
-/**
- * Returns the fll event transport percentage. A value of 100 means all the events are sent to
- * Fll. A value of 0 means, event are not sent to FLL. Range [0-100]. A value of -1 means
- * the value is not found. Name in remote config: "fpr_log_transport_ios_percent"
- *
- * @param percentage Default value of the transport rate to be returned if value does not exist
- * in remote config.
- * @return FLL transport percentage.
- */
-- (float)fllTransportPercentageWithDefaultValue:(float)percentage;
-
 @end

+ 0 - 20
FirebasePerformance/Sources/Configurations/FPRRemoteConfigFlags.m

@@ -95,7 +95,6 @@ typedef NS_ENUM(NSInteger, FPRConfigValueType) {
     [keysToCache setObject:@(FPRConfigValueTypeInteger)
                     forKey:@"fpr_session_gauge_memory_capture_frequency_bg_ms"];
     [keysToCache setObject:@(FPRConfigValueTypeInteger) forKey:@"fpr_session_max_duration_min"];
-    [keysToCache setObject:@(FPRConfigValueTypeFloat) forKey:@"fpr_log_transport_ios_percent"];
     self.configKeys = [keysToCache copy];
 
     [self update];
@@ -136,19 +135,6 @@ typedef NS_ENUM(NSInteger, FPRConfigValueType) {
   }
 }
 
-- (BOOL)containsRemoteConfigFlags {
-  // Ideally this should not be tied to any specific flag but since "fpr_enabled" is and should
-  // always be available we simply check for its existence to validate that the RC flags exists
-  // in the cache or not.
-  id cachedValueObject = [self cachedValueForConfigFlag:@"fpr_enabled"];
-
-  if (cachedValueObject) {
-    return true;
-  }
-
-  return false;
-}
-
 #pragma mark - Util methods.
 
 - (void)resetCache {
@@ -344,10 +330,4 @@ typedef NS_ENUM(NSInteger, FPRConfigValueType) {
                      defaultValue:maxDurationInMinutes];
 }
 
-#pragma mark - Google Data Transport related methods
-
-- (float)fllTransportPercentageWithDefaultValue:(float)percentage {
-  return [self getFloatValueForFlag:@"fpr_log_transport_ios_percent" defaultValue:percentage];
-}
-
 @end

+ 2 - 2
FirebasePerformance/Sources/FPRClient+Private.h

@@ -15,7 +15,7 @@
 #import "FirebasePerformance/ProtoSupport/PerfMetric.pbobjc.h"
 #import "FirebasePerformance/Sources/FPRClient.h"
 
-@class FPRGDTCCLogger;
+@class FPRGDTLogger;
 @class FPRConfigurations;
 @class FIRInstallations;
 
@@ -28,7 +28,7 @@
 @property(nonatomic, getter=isConfigured, readwrite) BOOL configured;
 
 /** GDT Logger to transmit Fireperf events to Google Data Transport. */
-@property(nonatomic) FPRGDTCCLogger *gdtLogger;
+@property(nonatomic) FPRGDTLogger *gdtLogger;
 
 /** The queue group all FPRClient work will run on. Used for testing only. */
 @property(nonatomic, readonly) dispatch_group_t eventsQueueGroup;

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů