ソースを参照

Roll forward for protobuf investigation (#8460)

Chen Liang 4 年 前
コミット
8827d24ab7
25 ファイル変更1184 行追加52 行削除
  1. 8 2
      FirebaseMessaging.podspec
  2. 4 8
      FirebaseMessaging/Apps/AdvancedSample/AdvancedSample.xcodeproj/project.pbxproj
  3. 5 0
      FirebaseMessaging/Apps/AdvancedSample/NotificationServiceExtension/NotificationService.swift
  4. 1 0
      FirebaseMessaging/Apps/AdvancedSample/Podfile
  5. 1 5
      FirebaseMessaging/Apps/Sample/Sample.xcodeproj/project.pbxproj
  6. 4 0
      FirebaseMessaging/Apps/Sample/Sample/Info.plist
  7. 13 0
      FirebaseMessaging/Apps/Shared/AppDelegate.swift
  8. 3 0
      FirebaseMessaging/CHANGELOG.md
  9. 22 0
      FirebaseMessaging/ProtoSupport/Protos/me.options
  10. 66 0
      FirebaseMessaging/ProtoSupport/Protos/me.proto
  11. 56 0
      FirebaseMessaging/ProtoSupport/generate_protos.sh
  12. 221 0
      FirebaseMessaging/ProtoSupport/nanopb_objc_generator.py
  13. 324 0
      FirebaseMessaging/ProtoSupport/proto_generator.py
  14. 9 21
      FirebaseMessaging/Sources/FIRMessagingAnalytics.m
  15. 4 0
      FirebaseMessaging/Sources/FIRMessagingCode.h
  16. 6 0
      FirebaseMessaging/Sources/FIRMessagingConstants.h
  17. 20 2
      FirebaseMessaging/Sources/FIRMessagingConstants.m
  18. 3 2
      FirebaseMessaging/Sources/FIRMessagingContextManagerService.m
  19. 156 1
      FirebaseMessaging/Sources/FIRMessagingExtensionHelper.m
  20. 52 0
      FirebaseMessaging/Sources/Protogen/nanopb/me.nanopb.c
  21. 119 0
      FirebaseMessaging/Sources/Protogen/nanopb/me.nanopb.h
  22. 8 0
      FirebaseMessaging/Sources/Public/FirebaseMessaging/FIRMessagingExtensionHelper.h
  23. 74 7
      FirebaseMessaging/Tests/UnitTests/FIRMessagingExtensionHelperTest.m
  24. 0 4
      FirebaseMessaging/Tests/UnitTests/FIRMessagingUtilitiesTest.m
  25. 5 0
      Package.swift

+ 8 - 2
FirebaseMessaging.podspec

@@ -35,7 +35,8 @@ device, and it is completely free.
 
   base_dir = "FirebaseMessaging/"
   s.source_files = [
-    base_dir + 'Sources/**/*.[mh]',
+    base_dir + 'Sources/**/*',
+    base_dir + 'Sources/Protogen/nanopb/*.h',
     'Interop/Analytics/Public/*.h',
     'FirebaseCore/Sources/Private/*.h',
     'FirebaseInstallations/Source/Library/Private/*.h',
@@ -44,9 +45,12 @@ device, and it is completely free.
   s.library = 'sqlite3'
   s.pod_target_xcconfig = {
     'GCC_C_LANGUAGE_STANDARD' => 'c99',
+    'GCC_PREPROCESSOR_DEFINITIONS' =>
+      # for nanopb:
+      'PB_FIELD_32BIT=1 PB_NO_PACKED_STRUCTS=1 PB_ENABLE_MALLOC=1',
     # Unit tests do library imports using repo-root relative paths.
     'HEADER_SEARCH_PATHS' => '"${PODS_TARGET_SRCROOT}"',
- }
+  }
   s.ios.framework = 'SystemConfiguration'
   s.tvos.framework = 'SystemConfiguration'
   s.osx.framework = 'SystemConfiguration'
@@ -57,6 +61,8 @@ device, and it is completely free.
   s.dependency 'GoogleUtilities/Reachability', '~> 7.4'
   s.dependency 'GoogleUtilities/Environment', '~> 7.4'
   s.dependency 'GoogleUtilities/UserDefaults', '~> 7.4'
+  s.dependency 'GoogleDataTransport', '~> 9.0'
+  s.dependency 'nanopb', '~> 2.30908.0'
 
   s.test_spec 'unit' do |unit_tests|
     unit_tests.scheme = { :code_coverage => true }

+ 4 - 8
FirebaseMessaging/Apps/AdvancedSample/AdvancedSample.xcodeproj/project.pbxproj

@@ -31,7 +31,6 @@
 		5194496E258AF2D000297021 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5194496D258AF2D000297021 /* Preview Assets.xcassets */; };
 		51944B28258B091A00297021 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 51944B27258B091A00297021 /* GoogleService-Info.plist */; };
 		51944B9D258B136000297021 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 51944B9C258B136000297021 /* Assets.xcassets */; };
-		51944BE1258BE42F00297021 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 51944BE0258BE42F00297021 /* GoogleService-Info.plist */; };
 		51A1F3A525883DCF0025932B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 51A1F3A425883DCF0025932B /* Preview Assets.xcassets */; };
 		51A1F3B125883E370025932B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A1F3B025883E370025932B /* AppDelegate.swift */; };
 		51A1F3B525883E630025932B /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A1F3B425883E630025932B /* SceneDelegate.swift */; };
@@ -39,7 +38,7 @@
 		51A1F3C625883EF60025932B /* Identity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A1F3C525883EF60025932B /* Identity.swift */; };
 		51A1F3CA25883EFF0025932B /* UserSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A1F3C825883EFF0025932B /* UserSettings.swift */; };
 		51A1F3CB25883EFF0025932B /* TopicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A1F3C925883EFF0025932B /* TopicView.swift */; };
-		51A1F3CE25883FAD0025932B /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 51A1F3CD25883FAD0025932B /* GoogleService-Info.plist */; };
+		51C21D212667FBCE0079AEEE /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 51C21D202667FBCE0079AEEE /* GoogleService-Info.plist */; };
 		51C24C622589603800236F25 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 51C24C602589603800236F25 /* LaunchScreen.storyboard */; };
 		51C24C652589606B00236F25 /* logo.png in Resources */ = {isa = PBXBuildFile; fileRef = 51C24C642589606B00236F25 /* logo.png */; };
 /* End PBXBuildFile section */
@@ -149,7 +148,6 @@
 		51944A86258B012A00297021 /* SampleWatchWatchKitExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SampleWatchWatchKitExtension.entitlements; sourceTree = "<group>"; };
 		51944B27258B091A00297021 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
 		51944B9C258B136000297021 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = ../../Shared/Assets.xcassets; sourceTree = "<group>"; };
-		51944BE0258BE42F00297021 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
 		51A1F39825883DCE0025932B /* AdvancedSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AdvancedSample.app; sourceTree = BUILT_PRODUCTS_DIR; };
 		51A1F3A425883DCF0025932B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
 		51A1F3A925883DCF0025932B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -159,7 +157,7 @@
 		51A1F3C525883EF60025932B /* Identity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Identity.swift; path = ../../Shared/Identity.swift; sourceTree = "<group>"; };
 		51A1F3C825883EFF0025932B /* UserSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = UserSettings.swift; path = ../../Shared/UserSettings.swift; sourceTree = "<group>"; };
 		51A1F3C925883EFF0025932B /* TopicView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TopicView.swift; path = ../../Shared/TopicView.swift; sourceTree = "<group>"; };
-		51A1F3CD25883FAD0025932B /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "../../Shared/GoogleService-Info.plist"; sourceTree = "<group>"; };
+		51C21D202667FBCE0079AEEE /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "../../Shared/GoogleService-Info.plist"; sourceTree = "<group>"; };
 		51C24C612589603800236F25 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = ../../Shared/Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
 		51C24C642589606B00236F25 /* logo.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = logo.png; path = ../../Shared/logo.png; sourceTree = "<group>"; };
 /* End PBXFileReference section */
@@ -253,7 +251,6 @@
 		51944961258AF2D000297021 /* SampleWatchWatchKitExtension */ = {
 			isa = PBXGroup;
 			children = (
-				51944BE0258BE42F00297021 /* GoogleService-Info.plist */,
 				51944A86258B012A00297021 /* SampleWatchWatchKitExtension.entitlements */,
 				51944962258AF2D000297021 /* HostingController.swift */,
 				51944964258AF2D000297021 /* ContentView.swift */,
@@ -303,10 +300,10 @@
 		51A1F39A25883DCE0025932B /* AdvancedSample */ = {
 			isa = PBXGroup;
 			children = (
+				51C21D202667FBCE0079AEEE /* GoogleService-Info.plist */,
 				519448DE2589900400297021 /* AdvancedSample.entitlements */,
 				51C24C642589606B00236F25 /* logo.png */,
 				51C24C602589603800236F25 /* LaunchScreen.storyboard */,
-				51A1F3CD25883FAD0025932B /* GoogleService-Info.plist */,
 				51A1F3C925883EFF0025932B /* TopicView.swift */,
 				51A1F3C825883EFF0025932B /* UserSettings.swift */,
 				51A1F3C525883EF60025932B /* Identity.swift */,
@@ -539,7 +536,6 @@
 			files = (
 				5194496E258AF2D000297021 /* Preview Assets.xcassets in Resources */,
 				5194496B258AF2D000297021 /* Assets.xcassets in Resources */,
-				51944BE1258BE42F00297021 /* GoogleService-Info.plist in Resources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -547,9 +543,9 @@
 			isa = PBXResourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
-				51A1F3CE25883FAD0025932B /* GoogleService-Info.plist in Resources */,
 				51C24C652589606B00236F25 /* logo.png in Resources */,
 				51C24C622589603800236F25 /* LaunchScreen.storyboard in Resources */,
+				51C21D212667FBCE0079AEEE /* GoogleService-Info.plist in Resources */,
 				51A1F3A525883DCF0025932B /* Preview Assets.xcassets in Resources */,
 				51944B9D258B136000297021 /* Assets.xcassets in Resources */,
 			);

+ 5 - 0
FirebaseMessaging/Apps/AdvancedSample/NotificationServiceExtension/NotificationService.swift

@@ -29,6 +29,11 @@ class NotificationService: UNNotificationServiceExtension {
       // Modify the notification content here...
       bestAttemptContent.title = "\(bestAttemptContent.title) 👩🏻‍💻"
 
+      // Log Delivery signals and export to BigQuery.
+      Messaging.serviceExtension()
+        .exportDeliveryMetricsToBigQuery(withMessageInfo: request.content.userInfo)
+
+      // Add image, call this last to finish with the content handler.
       Messaging.serviceExtension()
         .populateNotificationContent(bestAttemptContent, withContentHandler: contentHandler)
     }

+ 1 - 0
FirebaseMessaging/Apps/AdvancedSample/Podfile

@@ -9,6 +9,7 @@ def shared_pods
   pod 'FirebaseMessaging', :path => '../../../'
   pod 'FirebaseCoreDiagnostics', :path => '../../../'
   pod 'FirebaseInstallations', :path => '../../../'
+  pod 'GoogleDataTransport'
 end
 
 target 'AdvancedSample' do

+ 1 - 5
FirebaseMessaging/Apps/Sample/Sample.xcodeproj/project.pbxproj

@@ -3,7 +3,7 @@
 	archiveVersion = 1;
 	classes = {
 	};
-	objectVersion = 51;
+	objectVersion = 50;
 	objects = {
 
 /* Begin PBXBuildFile section */
@@ -15,7 +15,6 @@
 		51A1F3DB2588405A0025932B /* TopicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A1F3D72588405A0025932B /* TopicView.swift */; };
 		51A1F3DC2588405A0025932B /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A1F3D82588405A0025932B /* SceneDelegate.swift */; };
 		51A1F3E12588406A0025932B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A1F3DE2588406A0025932B /* AppDelegate.swift */; };
-		51A1F3E22588406A0025932B /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 51A1F3DF2588406A0025932B /* GoogleService-Info.plist */; };
 		51A1F3E32588406A0025932B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A1F3E02588406A0025932B /* ContentView.swift */; };
 		51C24C5225894EDB00236F25 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 51C24C5025894EDB00236F25 /* LaunchScreen.storyboard */; };
 		51C24C742589614800236F25 /* logo.png in Resources */ = {isa = PBXBuildFile; fileRef = 51C24C732589614800236F25 /* logo.png */; };
@@ -33,7 +32,6 @@
 		51A1F3D72588405A0025932B /* TopicView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TopicView.swift; path = ../../Shared/TopicView.swift; sourceTree = "<group>"; };
 		51A1F3D82588405A0025932B /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SceneDelegate.swift; path = ../../Shared/SceneDelegate.swift; sourceTree = "<group>"; };
 		51A1F3DE2588406A0025932B /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = ../../Shared/AppDelegate.swift; sourceTree = "<group>"; };
-		51A1F3DF2588406A0025932B /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "../../Shared/GoogleService-Info.plist"; sourceTree = "<group>"; };
 		51A1F3E02588406A0025932B /* ContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ContentView.swift; path = ../../Shared/ContentView.swift; sourceTree = "<group>"; };
 		51C24C5125894EDB00236F25 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = ../../Shared/Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
 		51C24C732589614800236F25 /* logo.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = logo.png; path = ../../Shared/logo.png; sourceTree = "<group>"; };
@@ -81,7 +79,6 @@
 				51C24C5025894EDB00236F25 /* LaunchScreen.storyboard */,
 				51A1F3DE2588406A0025932B /* AppDelegate.swift */,
 				51A1F3E02588406A0025932B /* ContentView.swift */,
-				51A1F3DF2588406A0025932B /* GoogleService-Info.plist */,
 				51A1F3D52588405A0025932B /* Identity.swift */,
 				51A1F3D82588405A0025932B /* SceneDelegate.swift */,
 				51A1F3D72588405A0025932B /* TopicView.swift */,
@@ -162,7 +159,6 @@
 			isa = PBXResourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
-				51A1F3E22588406A0025932B /* GoogleService-Info.plist in Resources */,
 				51C24C742589614800236F25 /* logo.png in Resources */,
 				5125CCA92437F472006CA5D0 /* Preview Assets.xcassets in Resources */,
 				51C24C5225894EDB00236F25 /* LaunchScreen.storyboard in Resources */,

+ 4 - 0
FirebaseMessaging/Apps/Sample/Sample/Info.plist

@@ -37,6 +37,10 @@
 			</array>
 		</dict>
 	</dict>
+	<key>UIBackgroundModes</key>
+	<array>
+		<string>remote-notification</string>
+	</array>
 	<key>UILaunchStoryboardName</key>
 	<string>LaunchScreen</string>
 	<key>UIRequiredDeviceCapabilities</key>

+ 13 - 0
FirebaseMessaging/Apps/Shared/AppDelegate.swift

@@ -14,6 +14,7 @@
 
 import UIKit
 import FirebaseCore
+import FirebaseMessaging
 import FirebaseAnalytics
 
 @UIApplicationMain
@@ -22,6 +23,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
                    didFinishLaunchingWithOptions launchOptions: [UIApplication
                      .LaunchOptionsKey: Any]?) -> Bool {
     FirebaseApp.configure()
+    application.delegate = self
     FirebaseAnalytics.Analytics.logEvent("test", parameters: nil)
 
     let center = UNUserNotificationCenter.current()
@@ -33,6 +35,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
       }
     }
     application.registerForRemoteNotifications()
+
     return true
   }
 
@@ -49,4 +52,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
                               withCompletionHandler completionHandler: @escaping () -> Void) {
     completionHandler()
   }
+
+  func application(_ application: UIApplication,
+                   didReceiveRemoteNotification userInfo: [AnyHashable: Any],
+                   fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult)
+                     -> Void) {
+    print("Hidden message arrived:\n" + userInfo.debugDescription)
+    // Log delivery signal for data/hidden/background messages
+    Messaging.serviceExtension().exportDeliveryMetricsToBigQuery(withMessageInfo: userInfo)
+    completionHandler(.newData)
+  }
 }

+ 3 - 0
FirebaseMessaging/CHANGELOG.md

@@ -1,3 +1,6 @@
+# 2021-07 -- v8.5.0
+- [added] Added new API `FIRMessagingExtensionHelper exportDeliveryMetricsToBigQuery` that allows developers to enable notification delivery metrics that exports to BigQuery. (#6181)
+
 # 2021-06 -- v8.2.0
 - [fixed] Fixed an issue that local scheduled notification is not set correctly due to sound type. (#8172)
 

+ 22 - 0
FirebaseMessaging/ProtoSupport/Protos/me.options

@@ -0,0 +1,22 @@
+# 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.
+#
+
+# Options for mee.protos
+fm.MessagingClientEvent.message_id type:FT_POINTER
+fm.MessagingClientEvent.instance_id type:FT_POINTER
+fm.MessagingClientEvent.package_name type:FT_POINTER
+fm.MessagingClientEvent.analytics_label type:FT_POINTER
+fm.MessagingClientEvent.composer_label type:FT_POINTER
+fm.MessagingClientEventExtension.messaging_client_event type:FT_POINTER

+ 66 - 0
FirebaseMessaging/ProtoSupport/Protos/me.proto

@@ -0,0 +1,66 @@
+//
+// Copyright 2020 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.
+//
+
+syntax = "proto3";
+
+package fm;
+
+message MessagingClientEvent {
+
+  int64 project_number = 1;
+
+  string message_id = 2;
+
+  string instance_id = 3;
+
+  enum MessageType {
+    UNKNOWN = 0;
+    DATA_MESSAGE = 1;
+    TOPIC = 2;
+    DISPLAY_NOTIFICATION = 3;
+  }
+
+  MessageType message_type = 4;
+
+  enum SDKPlatform {
+    UNKNOWN_OS = 0;
+    ANDROID = 1;
+    IOS = 2;
+    WEB = 3;
+  }
+
+  SDKPlatform sdk_platform = 5;
+
+  string package_name = 6;
+
+  enum Event {
+    UNKNOWN_EVENT = 0;
+    MESSAGE_DELIVERED = 1;
+    MESSAGE_OPEN = 2;
+  }
+
+  Event event = 12;
+
+  string analytics_label = 13;
+
+  int64 campaign_id = 14;
+
+  string composer_label = 15;
+}
+
+message MessagingClientEventExtension {
+  MessagingClientEvent messaging_client_event = 1;
+}

+ 56 - 0
FirebaseMessaging/ProtoSupport/generate_protos.sh

@@ -0,0 +1,56 @@
+#!/bin/bash
+
+# Copyright 2020 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.
+#
+
+# Example usage:
+# ./generate_protos.sh <path to nanopb>
+
+# Dependencies: git, protobuf, python-protobuf, pyinstaller
+
+readonly DIR="$( git rev-parse --show-toplevel )"
+
+# Current release of nanopb being used  to build the CCT protos
+readonly NANOPB_VERSION="0.3.9.8"
+readonly NANOPB_TEMPDIR="${DIR}/FirebaseMessaging/nanopb_temp"
+
+readonly LIBRARY_DIR="${DIR}/FirebaseMessaging/Sources/"
+readonly PROTO_DIR="${DIR}/FirebaseMessaging/ProtoSupport/Protos/"
+readonly PROTOGEN_DIR="${LIBRARY_DIR}/Protogen/"
+
+rm -rf "${NANOPB_TEMPDIR}"
+
+echo "Downloading nanopb..."
+git clone --branch "${NANOPB_VERSION}" https://github.com/nanopb/nanopb.git "${NANOPB_TEMPDIR}"
+
+echo "Building nanopb..."
+pushd "${NANOPB_TEMPDIR}"
+./tools/make_mac_package.sh
+GIT_DESCRIPTION=`git describe --always`-macosx-x86
+NANOPB_BIN_DIR="dist/${GIT_DESCRIPTION}"
+popd
+
+echo "Removing existing ME protos..."
+rm -rf "${PROTOGEN_DIR}/*"
+
+echo "Generating ME protos..."
+python "${DIR}/FirebaseMessaging/ProtoSupport/proto_generator.py" \
+  --nanopb \
+  --protos_dir="${PROTO_DIR}" \
+  --pythonpath="${NANOPB_TEMPDIR}/${NANOPB_BIN_DIR}/generator" \
+  --output_dir="${PROTOGEN_DIR}" \
+  --include="${PROTO_DIR}"
+
+rm -rf "${NANOPB_TEMPDIR}"

+ 221 - 0
FirebaseMessaging/ProtoSupport/nanopb_objc_generator.py

@@ -0,0 +1,221 @@
+#!/usr/bin/env python
+
+# 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.
+
+"""Generates and massages protocol buffer outputs.
+"""
+
+from __future__ import print_function
+
+import sys
+
+import io
+import nanopb_generator as nanopb
+import os
+import os.path
+import shlex
+
+from google.protobuf.descriptor_pb2 import FieldDescriptorProto
+
+# The plugin_pb2 package loads descriptors on import, but doesn't defend
+# against multiple imports. Reuse the plugin package as loaded by the
+# nanopb_generator.
+plugin_pb2 = nanopb.plugin_pb2
+nanopb_pb2 = nanopb.nanopb_pb2
+
+
+def main():
+  # Parse request
+  data = io.open(sys.stdin.fileno(), 'rb').read()
+  request = plugin_pb2.CodeGeneratorRequest.FromString(data)
+
+  # Preprocess inputs, changing types and nanopb defaults
+  options = nanopb_parse_options(request)
+  use_anonymous_oneof(request)
+  use_bytes_for_strings(request)
+
+  # Generate code
+  parsed_files = nanopb_parse_files(request, options)
+  results = nanopb_generate(request, options, parsed_files)
+  response = nanopb_write(results)
+
+  # Write to stdout
+  io.open(sys.stdout.fileno(), 'wb').write(response.SerializeToString())
+
+
+def use_anonymous_oneof(request):
+  """Use anonymous unions for oneofs if they're the only one in a message.
+
+  Equivalent to setting this option on messages where it applies:
+
+    option (nanopb).anonymous_oneof = true;
+
+  Args:
+    request: A CodeGeneratorRequest from protoc. The descriptors are modified
+      in place.
+  """
+  for _, message_type in iterate_messages(request):
+    if len(message_type.oneof_decl) == 1:
+      ext = message_type.options.Extensions[nanopb_pb2.nanopb_msgopt]
+      ext.anonymous_oneof = True
+
+
+def use_bytes_for_strings(request):
+  """Always use the bytes type instead of string.
+
+  By default, nanopb renders proto strings as having the C type char* and does
+  not include a separate size field, getting the length of the string via
+  strlen(). Unfortunately this prevents using strings with embedded nulls,
+  which is something the wire format supports.
+
+  Fortunately, string and bytes proto fields are identical on the wire and
+  nanopb's bytes representation does have an explicit length, so this function
+  changes the types of all string fields to bytes. The generated code will now
+  contain pb_bytes_array_t.
+
+  There's no nanopb or proto option to control this behavior. The equivalent
+  would be to hand edit all the .proto files :-(.
+
+  Args:
+    request: A CodeGeneratorRequest from protoc. The descriptors are modified
+      in place.
+  """
+  for names, message_type in iterate_messages(request):
+    for field in message_type.field:
+      if field.type == FieldDescriptorProto.TYPE_STRING:
+        field.type = FieldDescriptorProto.TYPE_BYTES
+
+
+def iterate_messages(request):
+  """Iterates over all messages in all files in the request.
+
+  Args:
+    request: A CodeGeneratorRequest passed by protoc.
+
+  Yields:
+    names: a nanopb.Names object giving a qualified name for the message
+    message_type: a DescriptorProto for the message.
+  """
+  for fdesc in request.proto_file:
+    for names, message_type in nanopb.iterate_messages(fdesc):
+      yield names, message_type
+
+
+def nanopb_parse_options(request):
+  """Parses nanopb_generator command-line options from the given request.
+
+  Args:
+    request: A CodeGeneratorRequest passed by protoc.
+
+  Returns:
+    Nanopb's options object, obtained via optparser.
+  """
+  # Parse options the same as nanopb_generator.main_plugin() does.
+  args = shlex.split(request.parameter)
+  options, _ = nanopb.optparser.parse_args(args)
+
+  # Force certain options
+  options.extension = '.nanopb'
+  options.verbose = True
+
+  # Replicate options setup from nanopb_generator.main_plugin.
+  nanopb.Globals.verbose_options = options.verbose
+
+  # Google's protoc does not currently indicate the full path of proto files.
+  # Instead always add the main file path to the search dirs, that works for
+  # the common case.
+  options.options_path.append(os.path.dirname(request.file_to_generate[0]))
+  return options
+
+
+def nanopb_parse_files(request, options):
+  """Parses the files in the given request into nanopb ProtoFile objects.
+
+  Args:
+    request: A CodeGeneratorRequest, as passed by protoc.
+    options: The command-line options from nanopb_parse_options.
+
+  Returns:
+    A dictionary of filename to nanopb.ProtoFile objects, each one representing
+    the parsed form of a FileDescriptor in the request.
+  """
+  # Process any include files first, in order to have them
+  # available as dependencies
+  parsed_files = {}
+  for fdesc in request.proto_file:
+    parsed_files[fdesc.name] = nanopb.parse_file(fdesc.name, fdesc, options)
+
+  return parsed_files
+
+
+def nanopb_generate(request, options, parsed_files):
+  """Generates C sources from the given parsed files.
+
+  Args:
+    request: A CodeGeneratorRequest, as passed by protoc.
+    options: The command-line options from nanopb_parse_options.
+    parsed_files: A dictionary of filename to nanopb.ProtoFile, as returned by
+      nanopb_parse_files().
+
+  Returns:
+    A list of nanopb output dictionaries, each one representing the code
+    generation result for each file to generate. The output dictionaries have
+    the following form:
+
+        {
+          'headername': Name of header file, ending in .h,
+          'headerdata': Contents of the header file,
+          'sourcename': Name of the source code file, ending in .c,
+          'sourcedata': Contents of the source code file
+        }
+  """
+  output = []
+
+  for filename in request.file_to_generate:
+    for fdesc in request.proto_file:
+      if fdesc.name == filename:
+        results = nanopb.process_file(filename, fdesc, options, parsed_files)
+        output.append(results)
+
+  return output
+
+
+def nanopb_write(results):
+  """Translates nanopb output dictionaries to a CodeGeneratorResponse.
+
+  Args:
+    results: A list of generated source dictionaries, as returned by
+      nanopb_generate().
+
+  Returns:
+    A CodeGeneratorResponse describing the result of the code generation
+    process to protoc.
+  """
+  response = plugin_pb2.CodeGeneratorResponse()
+
+  for result in results:
+    f = response.file.add()
+    f.name = result['headername']
+    f.content = result['headerdata']
+
+    f = response.file.add()
+    f.name = result['sourcename']
+    f.content = result['sourcedata']
+
+  return response
+
+
+if __name__ == '__main__':
+  main()

+ 324 - 0
FirebaseMessaging/ProtoSupport/proto_generator.py

@@ -0,0 +1,324 @@
+#! /usr/bin/env python
+
+# 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.
+
+"""Generates and massages protocol buffer outputs.
+
+Example usage:
+
+python GoogleDataTransport/GoogleDataTransportCCTSupport/ProtoSupport/build_protos.py \
+  --nanopb \
+  --protos_dir=GoogleDataTransport/GoogleDataTransportCCTSupport/Classes/Protos/ \
+  --pythonpath=~/Downloads/nanopb-0.3.9.2-macosx-x86/generator/ \
+  --output_dir=GoogleDataTransport/GoogleDataTransportCCTSupport/Classes/Protogen/
+"""
+
+from __future__ import print_function
+
+import sys
+
+import argparse
+import os
+import os.path
+import re
+import subprocess
+
+OBJC_GENERATOR='nanopb_objc_generator.py'
+
+COPYRIGHT_NOTICE = '''
+/*
+ * 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.
+ */
+'''.lstrip()
+
+
+def main():
+  parser = argparse.ArgumentParser(
+      description='Generates proto messages.')
+  parser.add_argument(
+      '--nanopb', action='store_true',
+      help='Generates nanopb messages.')
+  parser.add_argument(
+      '--objc', action='store_true',
+      help='Generates Objective-C messages.')
+  parser.add_argument(
+      '--protos_dir',
+      help='Source directory containing .proto files.')
+  parser.add_argument(
+      '--output_dir', '-d',
+      help='Directory to write files; subdirectories will be created.')
+  parser.add_argument(
+      '--protoc', default='protoc',
+      help='Location of the protoc executable')
+  parser.add_argument(
+      '--pythonpath',
+      help='Location of the protoc python library.')
+  parser.add_argument(
+      '--include', '-I', action='append', default=[],
+      help='Adds INCLUDE to the proto path.')
+
+
+  args = parser.parse_args()
+  if args.nanopb is None and args.objc is None:
+    parser.print_help()
+    sys.exit(1)
+
+  if args.protos_dir is None:
+    root_dir = os.path.abspath(os.path.dirname(__file__))
+    args.protos_dir = os.path.join(root_dir, 'protos')
+
+  if args.output_dir is None:
+    root_dir = os.path.abspath(os.path.dirname(__file__))
+    args.output_dir = os.path.join(root_dir, 'protogen-please-supply-an-outputdir')
+
+  all_proto_files = collect_files(args.protos_dir, '.proto')
+  if args.nanopb:
+    NanopbGenerator(args, all_proto_files).run()
+
+  proto_files = remove_well_known_protos(all_proto_files)
+
+  if args.objc:
+    ObjcProtobufGenerator(args, proto_files).run()
+
+
+class NanopbGenerator(object):
+  """Builds and runs the nanopb plugin to protoc."""
+
+  def __init__(self, args, proto_files):
+    self.args = args
+    self.proto_files = proto_files
+
+  def run(self):
+    """Performs the action of the generator."""
+
+    nanopb_out = os.path.join(self.args.output_dir, 'nanopb')
+    mkdir(nanopb_out)
+
+    self.__run_generator(nanopb_out)
+
+    sources = collect_files(nanopb_out, '.nanopb.h', '.nanopb.c')
+    post_process_files(
+        sources,
+        add_copyright,
+        nanopb_remove_extern_c,
+        nanopb_rename_delete,
+        nanopb_use_module_import
+    )
+
+  def __run_generator(self, out_dir):
+    """Invokes protoc using the nanopb plugin."""
+    cmd = protoc_command(self.args)
+
+    gen = os.path.join(os.path.dirname(__file__), OBJC_GENERATOR)
+    cmd.append('--plugin=protoc-gen-nanopb=%s' % gen)
+
+    nanopb_flags = [
+        '--extension=.nanopb',
+        '--source-extension=.c',
+        '--no-timestamp'
+    ]
+    nanopb_flags.extend(['-I%s' % path for path in self.args.include])
+    cmd.append('--nanopb_out=%s:%s' % (' '.join(nanopb_flags), out_dir))
+
+    cmd.extend(self.proto_files)
+    run_protoc(self.args, cmd)
+
+
+def protoc_command(args):
+  """Composes the initial protoc command-line including its include path."""
+  cmd = [args.protoc]
+  if args.include is not None:
+    cmd.extend(['-I=%s' % path for path in args.include])
+  return cmd
+
+
+def run_protoc(args, cmd):
+  """Actually runs the given protoc command.
+
+  Args:
+    args: The command-line args (including pythonpath)
+    cmd: The command to run expressed as a list of strings
+  """
+
+  kwargs = {}
+  if args.pythonpath:
+    env = os.environ.copy()
+    old_path = env.get('PYTHONPATH')
+    env['PYTHONPATH'] = os.path.expanduser(args.pythonpath)
+    if old_path is not None:
+      env['PYTHONPATH'] += os.pathsep + old_path
+    kwargs['env'] = env
+
+  try:
+    print(subprocess.check_output(cmd, stderr=subprocess.STDOUT, **kwargs))
+  except subprocess.CalledProcessError as error:
+    print('command failed: ', ' '.join(cmd), '\nerror: ', error.output)
+
+
+def remove_well_known_protos(filenames):
+  """Remove "well-known" protos for objc.
+
+  On those platforms we get these for free as a part of the protobuf runtime.
+  We only need them for nanopb.
+
+  Args:
+    filenames: A list of filenames, each naming a .proto file.
+
+  Returns:
+    The filenames with members of google/protobuf removed.
+  """
+
+  return [f for f in filenames if 'protos/google/protobuf/' not in f]
+
+
+def post_process_files(filenames, *processors):
+  for filename in filenames:
+    lines = []
+    with open(filename, 'r') as fd:
+      lines = fd.readlines()
+
+    for processor in processors:
+      lines = processor(lines)
+
+    write_file(filename, lines)
+
+
+def write_file(filename, lines):
+  mkdir(os.path.dirname(filename))
+  with open(filename, 'w') as fd:
+    fd.write(''.join(lines))
+
+
+def add_copyright(lines):
+  """Adds a copyright notice to the lines."""
+  result = [COPYRIGHT_NOTICE, '\n']
+  result.extend(lines)
+  return result
+
+
+def nanopb_remove_extern_c(lines):
+  """Removes extern "C" directives from nanopb code.
+
+  Args:
+    lines: A nanobp-generated source file, split into lines.
+  Returns:
+    A list of strings, similar to the input but modified to remove extern "C".
+  """
+  result = []
+  state = 'initial'
+  for line in lines:
+    if state == 'initial':
+      if '#ifdef __cplusplus' in line:
+        state = 'in-ifdef'
+        continue
+
+      result.append(line)
+
+    elif state == 'in-ifdef':
+      if '#endif' in line:
+        state = 'initial'
+
+  return result
+
+
+def nanopb_rename_delete(lines):
+  """Renames a delete symbol to delete_.
+
+  If a proto uses a field named 'delete', nanopb happily uses that in the
+  message definition. Works fine for C; not so much for C++.
+
+  Args:
+    lines: The lines to fix.
+
+  Returns:
+    The lines, fixed.
+  """
+  delete_keyword = re.compile(r'\bdelete\b')
+  return [delete_keyword.sub('delete_', line) for line in lines]
+
+
+def nanopb_use_module_import(lines):
+  """Changes #include <pb.h> to include <nanopb/pb.h>""" # Don't let Copybara alter these lines.
+  return [line.replace('#include <pb.h>', '{}include <nanopb/pb.h>'.format("#")) for line in lines]
+
+
+def strip_trailing_whitespace(lines):
+  """Removes trailing whitespace from the given lines."""
+  return [line.rstrip() + '\n' for line in lines]
+
+
+def objc_flatten_imports(lines):
+  """Flattens the import statements for compatibility with CocoaPods."""
+
+  long_import = re.compile(r'#import ".*/')
+  return [long_import.sub('#import "', line) for line in lines]
+
+
+def objc_strip_extension_registry(lines):
+  """Removes extensionRegistry methods from the classes."""
+  skip = False
+  result = []
+  for line in lines:
+    if '+ (GPBExtensionRegistry*)extensionRegistry {' in line:
+      skip = True
+    if not skip:
+      result.append(line)
+    elif line == '}\n':
+      skip = False
+
+  return result
+
+
+def collect_files(root_dir, *extensions):
+  """Finds files with the given extensions in the root_dir.
+
+  Args:
+    root_dir: The directory from which to start traversing.
+    *extensions: Filename extensions (including the leading dot) to find.
+
+  Returns:
+    A list of filenames, all starting with root_dir, that have one of the given
+    extensions.
+  """
+  result = []
+  for root, _, files in os.walk(root_dir):
+    for basename in files:
+      for ext in extensions:
+        if basename.endswith(ext):
+          filename = os.path.join(root, basename)
+          result.append(filename)
+  return result
+
+
+def mkdir(dirname):
+  if not os.path.isdir(dirname):
+    os.makedirs(dirname)
+
+
+if __name__ == '__main__':
+  main()
+

+ 9 - 21
FirebaseMessaging/Sources/FIRMessagingAnalytics.m

@@ -21,6 +21,7 @@
 #import "Interop/Analytics/Public/FIRInteropEventNames.h"
 #import "Interop/Analytics/Public/FIRInteropParameterNames.h"
 
+#import "FirebaseMessaging/Sources/FIRMessagingConstants.h"
 #import "FirebaseMessaging/Sources/FIRMessagingLogger.h"
 
 static NSString *const kLogTag = @"FIRMessagingAnalytics";
@@ -35,29 +36,16 @@ static NSString *const kApsContentAvailableKey = @"badge";
 // Data Key
 static NSString *const kDataKey = @"data";
 
-// Messaging From Key
-static NSString *const kFIRMessagingFromKey = @"from";
-
 static NSString *const kFIRParameterLabel = @"label";
 
 static NSString *const kReengagementSource = @"Firebase";
 static NSString *const kReengagementMedium = @"notification";
 
 // Analytics
-static NSString *const kAnalyticsEnabled = @"google.c.a."
-                                           @"e";
-static NSString *const kAnalyticsComposerIdentifier = @"google.c.a."
-                                                      @"c_id";
-static NSString *const kAnalyticsComposerLabel = @"google.c.a."
-                                                 @"c_l";
-static NSString *const kAnalyticsMessageLabel = @"google.c.a."
-                                                @"m_l";
-static NSString *const kAnalyticsMessageTimestamp = @"google.c.a."
-                                                    @"ts";
-static NSString *const kAnalyticsMessageUseDeviceTime = @"google.c.a."
-                                                        @"udt";
-static NSString *const kAnalyticsTrackConversions = @"google.c.a."
-                                                    @"tc";
+static NSString *const kAnalyticsEnabled = @"google.c.a.e";
+static NSString *const kAnalyticsMessageTimestamp = @"google.c.a.ts";
+static NSString *const kAnalyticsMessageUseDeviceTime = @"google.c.a.udt";
+static NSString *const kAnalyticsTrackConversions = @"google.c.a.tc";
 
 @implementation FIRMessagingAnalytics
 
@@ -119,17 +107,17 @@ static NSString *const kAnalyticsTrackConversions = @"google.c.a."
   }
 
   NSMutableDictionary *params = [NSMutableDictionary dictionary];
-  NSString *composerIdentifier = analyticsDataMap[kAnalyticsComposerIdentifier];
+  NSString *composerIdentifier = analyticsDataMap[kFIRMessagingAnalyticsComposerIdentifier];
   if ([composerIdentifier isKindOfClass:[NSString class]] && composerIdentifier.length) {
     params[kFIRIParameterMessageIdentifier] = [composerIdentifier copy];
   }
 
-  NSString *composerLabel = analyticsDataMap[kAnalyticsComposerLabel];
+  NSString *composerLabel = analyticsDataMap[kFIRMessagingAnalyticsComposerLabel];
   if ([composerLabel isKindOfClass:[NSString class]] && composerLabel.length) {
     params[kFIRIParameterMessageName] = [composerLabel copy];
   }
 
-  NSString *messageLabel = analyticsDataMap[kAnalyticsMessageLabel];
+  NSString *messageLabel = analyticsDataMap[kFIRMessagingAnalyticsMessageLabel];
   if ([messageLabel isKindOfClass:[NSString class]] && messageLabel.length) {
     params[kFIRParameterLabel] = [messageLabel copy];
   }
@@ -161,7 +149,7 @@ static NSString *const kAnalyticsTrackConversions = @"google.c.a."
     return;
   }
 
-  NSString *composerIdentifier = notification[kAnalyticsComposerIdentifier];
+  NSString *composerIdentifier = notification[kFIRMessagingAnalyticsComposerIdentifier];
   if ([composerIdentifier isKindOfClass:[NSString class]] && composerIdentifier.length) {
     // Set user property for event.
     [analytics setUserPropertyWithOrigin:@"fcm"

+ 4 - 0
FirebaseMessaging/Sources/FIRMessagingCode.h

@@ -136,6 +136,10 @@ typedef NS_ENUM(NSInteger, FIRMessagingMessageCode) {
   kFIRMessagingServiceExtensionImageNotDownloaded = 20001,
   kFIRMessagingServiceExtensionLocalFileNotCreated = 20002,
   kFIRMessagingServiceExtensionImageNotAttached = 20003,
+  kFIRMessagingServiceExtensionTransportBytesError = 20004,
+  kFIRMessagingServiceExtensionInvalidProjectID = 2005,
+  kFIRMessagingServiceExtensionInvalidMessageID = 2006,
+  kFIRMessagingServiceExtensionInvalidInstanceID = 2007,
 
   kFIRMessagingMessageCodeFIRApp002 = 22002,
   kFIRMessagingMessageCodeInternal001 = 22001,

+ 6 - 0
FirebaseMessaging/Sources/FIRMessagingConstants.h

@@ -28,6 +28,12 @@ FOUNDATION_EXPORT NSString *const kFIRMessagingMessageIDKey;
 FOUNDATION_EXPORT NSString *const kFIRMessagingMessageAPNSContentAvailableKey;
 FOUNDATION_EXPORT NSString *const kFIRMessagingMessageSyncMessageTTLKey;
 FOUNDATION_EXPORT NSString *const kFIRMessagingMessageLinkKey;
+FOUNDATION_EXPORT NSString *const kFIRMessagingSenderID;
+FOUNDATION_EXPORT NSString *const kFIRMessagingFID;
+FOUNDATION_EXPORT NSString *const kFIRMessagingAnalyticsComposerIdentifier;
+FOUNDATION_EXPORT NSString *const kFIRMessagingAnalyticsMessageLabel;
+FOUNDATION_EXPORT NSString *const kFIRMessagingAnalyticsComposerLabel;
+
 FOUNDATION_EXPORT NSString *const kFIRMessagingRemoteNotificationsProxyEnabledInfoPlistKey;
 FOUNDATION_EXPORT NSString *const kFIRMessagingSubDirectoryName;
 

+ 20 - 2
FirebaseMessaging/Sources/FIRMessagingConstants.m

@@ -18,13 +18,31 @@
 
 NSString *const kFIRMessagingFromKey = @"from";
 
-NSString *const kFIRMessagingMessageIDKey = @"gcm."
-                                            @"message_id";
+NSString *const kFIRMessagingSendTo = @"google."
+                                      @"to";
+NSString *const kFIRMessagingSendTTL = @"google."
+                                       @"ttl";
+NSString *const kFIRMessagingSendDelay = @"google."
+                                         @"delay";
+NSString *const kFIRMessagingSendMessageID = @"google."
+                                             @"msg_id";
+NSString *const KFIRMessagingSendMessageAppData = @"google."
+                                                  @"data";
+
+NSString *const kFIRMessagingMessageInternalReservedKeyword = @"gcm.";
+NSString *const kFIRMessagingMessagePersistentIDKey = @"persistent_id";
+
+NSString *const kFIRMessagingMessageIDKey = @"gcm.message_id";
 NSString *const kFIRMessagingMessageAPNSContentAvailableKey = @"content-available";
 NSString *const kFIRMessagingMessageSyncMessageTTLKey = @"gcm."
                                                         @"ttl";
 NSString *const kFIRMessagingMessageLinkKey = @"gcm."
                                               @"app_link";
+NSString *const kFIRMessagingSenderID = @"google.c.sender.id";
+NSString *const kFIRMessagingFID = @"google.c.fid";
+NSString *const kFIRMessagingAnalyticsComposerIdentifier = @"google.c.a.c_id";
+NSString *const kFIRMessagingAnalyticsMessageLabel = @"google.c.a.m_l";
+NSString *const kFIRMessagingAnalyticsComposerLabel = @"google.c.a.c_l";
 
 NSString *const kFIRMessagingRemoteNotificationsProxyEnabledInfoPlistKey =
     @"FirebaseAppDelegateProxyEnabled";

+ 3 - 2
FirebaseMessaging/Sources/FIRMessagingContextManagerService.m

@@ -69,8 +69,9 @@ typedef NS_ENUM(NSUInteger, FIRMessagingContextManagerMessageType) {
 + (BOOL)isContextManagerMessage:(NSDictionary *)message {
   // For now we only support local time in ContextManager.
   if (![message[kFIRMessagingContextManagerLocalTimeStart] length]) {
-    FIRMessagingLoggerDebug(kFIRMessagingMessageCodeContextManagerService000,
-                            @"Received message missing local start time, dropped.");
+    FIRMessagingLoggerDebug(
+        kFIRMessagingMessageCodeContextManagerService000,
+        @"Received message missing local start time, not a contextual message.");
     return NO;
   }
 

+ 156 - 1
FirebaseMessaging/Sources/FIRMessagingExtensionHelper.m

@@ -14,16 +14,89 @@
  * limitations under the License.
  */
 
-#import "FirebaseMessaging/Sources/Public/FirebaseMessaging/FIRMessagingExtensionHelper.h"
+#import <nanopb/pb.h>
+#import <nanopb/pb_decode.h>
+#import <nanopb/pb_encode.h>
 
+#import <GoogleDataTransport/GoogleDataTransport.h>
+#import <GoogleUtilities/GULAppEnvironmentUtil.h>
 #import "FirebaseMessaging/Sources/FIRMessagingCode.h"
+#import "FirebaseMessaging/Sources/FIRMessagingConstants.h"
 #import "FirebaseMessaging/Sources/FIRMessagingLogger.h"
+#import "FirebaseMessaging/Sources/Protogen/nanopb/me.nanopb.h"
+#import "FirebaseMessaging/Sources/Public/FirebaseMessaging/FIRMessagingExtensionHelper.h"
 
 static NSString *const kPayloadOptionsName = @"fcm_options";
 static NSString *const kPayloadOptionsImageURLName = @"image";
 static NSString *const kNoExtension = @"";
 static NSString *const kImagePathPrefix = @"image/";
 
+#pragma mark - nanopb helper functions
+
+/** Callocs a pb_bytes_array and copies the given NSData bytes into the bytes array.
+ *
+ * @note Memory needs to be free manually, through pb_free or pb_release.
+ * @param data The data to copy into the new bytes array.
+ */
+pb_bytes_array_t *FIRMessagingEncodeData(NSData *data) {
+  pb_bytes_array_t *pbBytesArray = calloc(1, PB_BYTES_ARRAY_T_ALLOCSIZE(data.length));
+  if (pbBytesArray != NULL) {
+    [data getBytes:pbBytesArray->bytes length:data.length];
+    pbBytesArray->size = (pb_size_t)data.length;
+  }
+  return pbBytesArray;
+}
+/** Callocs a pb_bytes_array and copies the given NSString's bytes into the bytes array.
+ *
+ * @note Memory needs to be free manually, through pb_free or pb_release.
+ * @param string The string to encode as pb_bytes.
+ */
+pb_bytes_array_t *FIRMessagingEncodeString(NSString *string) {
+  NSData *stringBytes = [string dataUsingEncoding:NSUTF8StringEncoding];
+  return FIRMessagingEncodeData(stringBytes);
+}
+
+@interface FIRMessagingMetricsLog : NSObject <GDTCOREventDataObject>
+
+@property(nonatomic) fm_MessagingClientEventExtension eventExtension;
+
+@end
+
+@implementation FIRMessagingMetricsLog
+
+- (instancetype)initWithEventExtension:(fm_MessagingClientEventExtension)eventExtension {
+  self = [super init];
+  if (self) {
+    _eventExtension = eventExtension;
+  }
+  return self;
+}
+
+- (NSData *)transportBytes {
+  pb_ostream_t sizestream = PB_OSTREAM_SIZING;
+
+  // Encode 1 time to determine the size.
+  if (!pb_encode(&sizestream, fm_MessagingClientEventExtension_fields, &_eventExtension)) {
+    FIRMessagingLoggerError(kFIRMessagingServiceExtensionTransportBytesError,
+                            @"Error in nanopb encoding for size: %s", PB_GET_ERROR(&sizestream));
+  }
+
+  // Encode a 2nd time to actually get the bytes from it.
+  size_t bufferSize = sizestream.bytes_written;
+  CFMutableDataRef dataRef = CFDataCreateMutable(CFAllocatorGetDefault(), bufferSize);
+  CFDataSetLength(dataRef, bufferSize);
+  pb_ostream_t ostream = pb_ostream_from_buffer((void *)CFDataGetBytePtr(dataRef), bufferSize);
+  if (!pb_encode(&ostream, fm_MessagingClientEventExtension_fields, &_eventExtension)) {
+    FIRMessagingLoggerError(kFIRMessagingServiceExtensionTransportBytesError,
+                            @"Error in nanopb encoding for bytes: %s", PB_GET_ERROR(&ostream));
+  }
+  CFDataSetLength(dataRef, ostream.bytes_written);
+
+  return CFBridgingRelease(dataRef);
+}
+
+@end
+
 @interface FIRMessagingExtensionHelper ()
 @property(nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
 @property(nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;
@@ -129,4 +202,86 @@ static NSString *const kImagePathPrefix = @"image/";
   }
 }
 
+- (void)exportDeliveryMetricsToBigQueryWithMessageInfo:(NSDictionary *)info {
+  GDTCORTransport *transport = [[GDTCORTransport alloc] initWithMappingID:@"1249"
+                                                             transformers:nil
+                                                                   target:kGDTCORTargetFLL];
+
+  fm_MessagingClientEventExtension eventExtension = fm_MessagingClientEventExtension_init_default;
+
+  fm_MessagingClientEvent clientEvent = fm_MessagingClientEvent_init_default;
+  if (!info[kFIRMessagingSenderID]) {
+    FIRMessagingLoggerError(kFIRMessagingServiceExtensionInvalidProjectID,
+                            @"Delivery logging failed: Invalid project ID");
+    return;
+  }
+  clientEvent.project_number = (int64_t)[info[kFIRMessagingSenderID] longLongValue];
+
+  if (!info[kFIRMessagingMessageIDKey] ||
+      ![info[kFIRMessagingMessageIDKey] isKindOfClass:NSString.class]) {
+    FIRMessagingLoggerWarn(kFIRMessagingServiceExtensionInvalidMessageID,
+                           @"Delivery logging failed: Invalid Message ID");
+    return;
+  }
+  clientEvent.message_id = FIRMessagingEncodeString(info[kFIRMessagingMessageIDKey]);
+
+  if (!info[kFIRMessagingFID] || ![info[kFIRMessagingFID] isKindOfClass:NSString.class]) {
+    FIRMessagingLoggerWarn(kFIRMessagingServiceExtensionInvalidInstanceID,
+                           @"Delivery logging failed: Invalid Instance ID");
+    return;
+  }
+  clientEvent.instance_id = FIRMessagingEncodeString(info[kFIRMessagingFID]);
+
+  if ([info[@"aps"][kFIRMessagingMessageAPNSContentAvailableKey] intValue] == 1 &&
+      ![GULAppEnvironmentUtil isAppExtension]) {
+    clientEvent.message_type = fm_MessagingClientEvent_MessageType_DATA_MESSAGE;
+  } else {
+    clientEvent.message_type = fm_MessagingClientEvent_MessageType_DISPLAY_NOTIFICATION;
+  }
+  clientEvent.sdk_platform = fm_MessagingClientEvent_SDKPlatform_IOS;
+
+  NSString *bundleID = [NSBundle mainBundle].bundleIdentifier;
+  if ([GULAppEnvironmentUtil isAppExtension]) {
+    bundleID = [[self class] bundleIdentifierByRemovingLastPartFrom:bundleID];
+  }
+  if (bundleID) {
+    clientEvent.package_name = FIRMessagingEncodeString(bundleID);
+  }
+  clientEvent.event = fm_MessagingClientEvent_Event_MESSAGE_DELIVERED;
+
+  if (info[kFIRMessagingAnalyticsMessageLabel]) {
+    clientEvent.analytics_label =
+        FIRMessagingEncodeString(info[kFIRMessagingAnalyticsMessageLabel]);
+  }
+  if (info[kFIRMessagingAnalyticsComposerIdentifier]) {
+    clientEvent.campaign_id =
+        (int64_t)[info[kFIRMessagingAnalyticsComposerIdentifier] longLongValue];
+  }
+  if (info[kFIRMessagingAnalyticsComposerLabel]) {
+    clientEvent.composer_label =
+        FIRMessagingEncodeString(info[kFIRMessagingAnalyticsComposerLabel]);
+  }
+
+  eventExtension.messaging_client_event = &clientEvent;
+  FIRMessagingMetricsLog *log =
+      [[FIRMessagingMetricsLog alloc] initWithEventExtension:eventExtension];
+
+  GDTCOREvent *event = [transport eventForTransport];
+  event.dataObject = log;
+  event.qosTier = GDTCOREventQoSFast;
+
+  // Use this API for SDK service data events.
+  [transport sendDataEvent:event];
+}
+
++ (NSString *)bundleIdentifierByRemovingLastPartFrom:(NSString *)bundleIdentifier {
+  NSString *bundleIDComponentsSeparator = @".";
+
+  NSMutableArray<NSString *> *bundleIDComponents =
+      [[bundleIdentifier componentsSeparatedByString:bundleIDComponentsSeparator] mutableCopy];
+  [bundleIDComponents removeLastObject];
+
+  return [bundleIDComponents componentsJoinedByString:bundleIDComponentsSeparator];
+}
+
 @end

+ 52 - 0
FirebaseMessaging/Sources/Protogen/nanopb/me.nanopb.c

@@ -0,0 +1,52 @@
+/*
+ * 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.
+ */
+
+/* Automatically generated nanopb constant definitions */
+/* Generated by nanopb-0.3.9.8 */
+
+#include "FirebaseMessaging/Sources/Protogen/nanopb/me.nanopb.h"
+
+/* @@protoc_insertion_point(includes) */
+#if PB_PROTO_HEADER_VERSION != 30
+#error Regenerate this file with the current version of nanopb generator.
+#endif
+
+
+
+const pb_field_t fm_MessagingClientEvent_fields[11] = {
+    PB_FIELD(  1, INT64   , SINGULAR, STATIC  , FIRST, fm_MessagingClientEvent, project_number, project_number, 0),
+    PB_FIELD(  2, BYTES   , SINGULAR, POINTER , OTHER, fm_MessagingClientEvent, message_id, project_number, 0),
+    PB_FIELD(  3, BYTES   , SINGULAR, POINTER , OTHER, fm_MessagingClientEvent, instance_id, message_id, 0),
+    PB_FIELD(  4, UENUM   , SINGULAR, STATIC  , OTHER, fm_MessagingClientEvent, message_type, instance_id, 0),
+    PB_FIELD(  5, UENUM   , SINGULAR, STATIC  , OTHER, fm_MessagingClientEvent, sdk_platform, message_type, 0),
+    PB_FIELD(  6, BYTES   , SINGULAR, POINTER , OTHER, fm_MessagingClientEvent, package_name, sdk_platform, 0),
+    PB_FIELD( 12, UENUM   , SINGULAR, STATIC  , OTHER, fm_MessagingClientEvent, event, package_name, 0),
+    PB_FIELD( 13, BYTES   , SINGULAR, POINTER , OTHER, fm_MessagingClientEvent, analytics_label, event, 0),
+    PB_FIELD( 14, INT64   , SINGULAR, STATIC  , OTHER, fm_MessagingClientEvent, campaign_id, analytics_label, 0),
+    PB_FIELD( 15, BYTES   , SINGULAR, POINTER , OTHER, fm_MessagingClientEvent, composer_label, campaign_id, 0),
+    PB_LAST_FIELD
+};
+
+const pb_field_t fm_MessagingClientEventExtension_fields[2] = {
+    PB_FIELD(  1, MESSAGE , SINGULAR, POINTER , FIRST, fm_MessagingClientEventExtension, messaging_client_event, messaging_client_event, &fm_MessagingClientEvent_fields),
+    PB_LAST_FIELD
+};
+
+
+
+
+
+/* @@protoc_insertion_point(eof) */

+ 119 - 0
FirebaseMessaging/Sources/Protogen/nanopb/me.nanopb.h

@@ -0,0 +1,119 @@
+/*
+ * 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.
+ */
+
+/* Automatically generated nanopb header */
+/* Generated by nanopb-0.3.9.8 */
+
+#ifndef PB_FM_ME_NANOPB_H_INCLUDED
+#define PB_FM_ME_NANOPB_H_INCLUDED
+#include <nanopb/pb.h>
+
+/* @@protoc_insertion_point(includes) */
+#if PB_PROTO_HEADER_VERSION != 30
+#error Regenerate this file with the current version of nanopb generator.
+#endif
+
+
+/* Enum definitions */
+typedef enum _fm_MessagingClientEvent_MessageType {
+    fm_MessagingClientEvent_MessageType_UNKNOWN = 0,
+    fm_MessagingClientEvent_MessageType_DATA_MESSAGE = 1,
+    fm_MessagingClientEvent_MessageType_TOPIC = 2,
+    fm_MessagingClientEvent_MessageType_DISPLAY_NOTIFICATION = 3
+} fm_MessagingClientEvent_MessageType;
+#define _fm_MessagingClientEvent_MessageType_MIN fm_MessagingClientEvent_MessageType_UNKNOWN
+#define _fm_MessagingClientEvent_MessageType_MAX fm_MessagingClientEvent_MessageType_DISPLAY_NOTIFICATION
+#define _fm_MessagingClientEvent_MessageType_ARRAYSIZE ((fm_MessagingClientEvent_MessageType)(fm_MessagingClientEvent_MessageType_DISPLAY_NOTIFICATION+1))
+
+typedef enum _fm_MessagingClientEvent_SDKPlatform {
+    fm_MessagingClientEvent_SDKPlatform_UNKNOWN_OS = 0,
+    fm_MessagingClientEvent_SDKPlatform_ANDROID = 1,
+    fm_MessagingClientEvent_SDKPlatform_IOS = 2,
+    fm_MessagingClientEvent_SDKPlatform_WEB = 3
+} fm_MessagingClientEvent_SDKPlatform;
+#define _fm_MessagingClientEvent_SDKPlatform_MIN fm_MessagingClientEvent_SDKPlatform_UNKNOWN_OS
+#define _fm_MessagingClientEvent_SDKPlatform_MAX fm_MessagingClientEvent_SDKPlatform_WEB
+#define _fm_MessagingClientEvent_SDKPlatform_ARRAYSIZE ((fm_MessagingClientEvent_SDKPlatform)(fm_MessagingClientEvent_SDKPlatform_WEB+1))
+
+typedef enum _fm_MessagingClientEvent_Event {
+    fm_MessagingClientEvent_Event_UNKNOWN_EVENT = 0,
+    fm_MessagingClientEvent_Event_MESSAGE_DELIVERED = 1,
+    fm_MessagingClientEvent_Event_MESSAGE_OPEN = 2
+} fm_MessagingClientEvent_Event;
+#define _fm_MessagingClientEvent_Event_MIN fm_MessagingClientEvent_Event_UNKNOWN_EVENT
+#define _fm_MessagingClientEvent_Event_MAX fm_MessagingClientEvent_Event_MESSAGE_OPEN
+#define _fm_MessagingClientEvent_Event_ARRAYSIZE ((fm_MessagingClientEvent_Event)(fm_MessagingClientEvent_Event_MESSAGE_OPEN+1))
+
+/* Struct definitions */
+typedef struct _fm_MessagingClientEventExtension {
+    struct _fm_MessagingClientEvent *messaging_client_event;
+/* @@protoc_insertion_point(struct:fm_MessagingClientEventExtension) */
+} fm_MessagingClientEventExtension;
+
+typedef struct _fm_MessagingClientEvent {
+    int64_t project_number;
+    pb_bytes_array_t *message_id;
+    pb_bytes_array_t *instance_id;
+    fm_MessagingClientEvent_MessageType message_type;
+    fm_MessagingClientEvent_SDKPlatform sdk_platform;
+    pb_bytes_array_t *package_name;
+    fm_MessagingClientEvent_Event event;
+    pb_bytes_array_t *analytics_label;
+    int64_t campaign_id;
+    pb_bytes_array_t *composer_label;
+/* @@protoc_insertion_point(struct:fm_MessagingClientEvent) */
+} fm_MessagingClientEvent;
+
+/* Default values for struct fields */
+
+/* Initializer values for message structs */
+#define fm_MessagingClientEvent_init_default     {0, NULL, NULL, _fm_MessagingClientEvent_MessageType_MIN, _fm_MessagingClientEvent_SDKPlatform_MIN, NULL, _fm_MessagingClientEvent_Event_MIN, NULL, 0, NULL}
+#define fm_MessagingClientEventExtension_init_default {NULL}
+#define fm_MessagingClientEvent_init_zero        {0, NULL, NULL, _fm_MessagingClientEvent_MessageType_MIN, _fm_MessagingClientEvent_SDKPlatform_MIN, NULL, _fm_MessagingClientEvent_Event_MIN, NULL, 0, NULL}
+#define fm_MessagingClientEventExtension_init_zero {NULL}
+
+/* Field tags (for use in manual encoding/decoding) */
+#define fm_MessagingClientEventExtension_messaging_client_event_tag 1
+#define fm_MessagingClientEvent_project_number_tag 1
+#define fm_MessagingClientEvent_message_id_tag   2
+#define fm_MessagingClientEvent_instance_id_tag  3
+#define fm_MessagingClientEvent_message_type_tag 4
+#define fm_MessagingClientEvent_sdk_platform_tag 5
+#define fm_MessagingClientEvent_package_name_tag 6
+#define fm_MessagingClientEvent_event_tag        12
+#define fm_MessagingClientEvent_analytics_label_tag 13
+#define fm_MessagingClientEvent_campaign_id_tag  14
+#define fm_MessagingClientEvent_composer_label_tag 15
+
+/* Struct field encoding specification for nanopb */
+extern const pb_field_t fm_MessagingClientEvent_fields[11];
+extern const pb_field_t fm_MessagingClientEventExtension_fields[2];
+
+/* Maximum encoded size of messages (where known) */
+/* fm_MessagingClientEvent_size depends on runtime parameters */
+/* fm_MessagingClientEventExtension_size depends on runtime parameters */
+
+/* Message IDs (where set with "msgid" option) */
+#ifdef PB_MSGID
+
+#define ME_MESSAGES \
+
+
+#endif
+
+/* @@protoc_insertion_point(eof) */
+
+#endif

+ 8 - 0
FirebaseMessaging/Sources/Public/FirebaseMessaging/FIRMessagingExtensionHelper.h

@@ -34,6 +34,14 @@ __OSX_AVAILABLE(10.14) @interface FIRMessagingExtensionHelper : NSObject
 - (void)populateNotificationContent:(UNMutableNotificationContent *)content
                  withContentHandler:(void (^)(UNNotificationContent *_Nonnull))contentHandler;
 
+/// Exports delivery metrics to BigQuery. Call this API to enable logging delivery of alert
+/// notification or background notification and export to BigQuery.
+/// If you log alert notifications, enable Notification Service Extension and calls this API
+/// under `UNNotificationServiceExtension didReceiveNotificationRequest: withContentHandler:`.
+/// If you log background notifications, call the API under `UIApplicationDelegate
+/// application:didReceiveRemoteNotification:fetchCompletionHandler:`.
+- (void)exportDeliveryMetricsToBigQueryWithMessageInfo:(NSDictionary *)info;
+
 @end
 
 NS_ASSUME_NONNULL_END

+ 74 - 7
FirebaseMessaging/Tests/UnitTests/FIRMessagingExtensionHelperTest.m

@@ -17,10 +17,13 @@
 #import <OCMock/OCMock.h>
 #import <XCTest/XCTest.h>
 
+#import <GoogleUtilities/GULAppEnvironmentUtil.h>
+
+#import "FirebaseMessaging/Sources/FIRMessagingConstants.h"
 #import "FirebaseMessaging/Sources/Public/FirebaseMessaging/FIRMessaging.h"
 #import "FirebaseMessaging/Sources/Public/FirebaseMessaging/FIRMessagingExtensionHelper.h"
 
-API_AVAILABLE(macos(10.14), ios(10.0))
+API_AVAILABLE(macos(10.14), ios(10.0), watchos(3.0))
 typedef void (^FIRMessagingContentHandler)(UNNotificationContent *content);
 
 #if TARGET_OS_IOS || TARGET_OS_OSX || TARGET_OS_WATCH
@@ -34,11 +37,13 @@ static NSString *const kValidImageURL =
 
 - (void)loadAttachmentForURL:(NSURL *)attachmentURL
            completionHandler:(void (^)(UNNotificationAttachment *))completionHandler;
++ (NSString *)bundleIdentifierByRemovingLastPartFrom:(NSString *)bundleIdentifier;
 - (NSString *)fileExtensionForResponse:(NSURLResponse *)response;
 @end
 
 @interface FIRMessagingExtensionHelperTest : XCTestCase {
   id _mockExtensionHelper;
+  id _mockUtilClass;
   id _mockURLResponse;
 }
 @end
@@ -47,9 +52,10 @@ static NSString *const kValidImageURL =
 
 - (void)setUp {
   [super setUp];
-  if (@available(macOS 10.14, iOS 10.0, *)) {
+  if (@available(macOS 10.14, iOS 10.0, watchos 3.0, *)) {
     FIRMessagingExtensionHelper *extensionHelper = [FIRMessaging extensionHelper];
     _mockExtensionHelper = OCMPartialMock(extensionHelper);
+    _mockUtilClass = OCMClassMock([GULAppEnvironmentUtil class]);
     _mockURLResponse = OCMClassMock([NSURLResponse class]);
   } else {
     // Fallback on earlier versions
@@ -58,13 +64,14 @@ static NSString *const kValidImageURL =
 
 - (void)tearDown {
   [_mockExtensionHelper stopMocking];
+  [_mockUtilClass stopMocking];
   [_mockURLResponse stopMocking];
 }
 
 #ifdef COCOAPODS
 // This test requires internet access.
 - (void)testModifyNotificationWithValidPayloadData {
-  if (@available(macOS 10.14, iOS 10.0, *)) {
+  if (@available(macOS 10.14, iOS 10.0, watchos 3.0, *)) {
     XCTestExpectation *validPayloadExpectation =
         [self expectationWithDescription:@"Test payload is valid."];
     UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
@@ -79,10 +86,10 @@ static NSString *const kValidImageURL =
     [self waitForExpectationsWithTimeout:1.0 handler:nil];
   }
 }
-#endif
+#endif  // COCOAPODS
 
 - (void)testModifyNotificationWithInvalidPayloadData {
-  if (@available(macOS 10.14, iOS 10.0, *)) {
+  if (@available(macOS 10.14, iOS 10.0, watchos 3.0, *)) {
     XCTestExpectation *validPayloadExpectation =
         [self expectationWithDescription:@"Test payload is valid."];
     UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
@@ -100,7 +107,7 @@ static NSString *const kValidImageURL =
 }
 
 - (void)testModifyNotificationWithEmptyPayloadData {
-  if (@available(macOS 10.14, iOS 10.0, *)) {
+  if (@available(macOS 10.14, iOS 10.0, watchos 3.0, *)) {
     XCTestExpectation *validPayloadExpectation =
         [self expectationWithDescription:@"Test payload is valid."];
     UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
@@ -148,5 +155,65 @@ static NSString *const kValidImageURL =
     XCTAssertTrue([extension isEqualToString:kValidMIMETypeTestExtension]);
   }
 }
+
+- (void)testDeliveryMetricsLoggingWithEmptyPayload {
+  OCMStub([_mockUtilClass isAppExtension]).andReturn(YES);
+  NSDictionary *fakeMessageInfo = @{@"aps" : @{}};
+
+  [_mockExtensionHelper exportDeliveryMetricsToBigQueryWithMessageInfo:fakeMessageInfo];
+  OCMReject([_mockExtensionHelper bundleIdentifierByRemovingLastPartFrom:[OCMArg any]]);
+}
+
+- (void)testDeliveryMetricsLoggingWithInvalidMessageID {
+  OCMStub([_mockUtilClass isAppExtension]).andReturn(YES);
+  NSDictionary *fakeMessageInfo = @{
+    @"aps" : @{@"badge" : @9, @"mutable-content" : @1},
+    @"fcm_options" : @{@"image" : @"https://google.com"},
+    @"google.c.fid" : @"fakeFIDForTest",
+    @"google.c.sender.id" : @123456789
+  };
+  [_mockExtensionHelper exportDeliveryMetricsToBigQueryWithMessageInfo:fakeMessageInfo];
+  OCMReject([_mockExtensionHelper bundleIdentifierByRemovingLastPartFrom:[OCMArg any]]);
+}
+
+- (void)testDeliveryMetricsLoggingWithInvalidFID {
+  OCMStub([_mockUtilClass isAppExtension]).andReturn(YES);
+  NSDictionary *fakeMessageInfo = @{
+    @"aps" : @{@"badge" : @9, @"mutable-content" : @1},
+    @"fcm_options" : @{@"image" : @"https://google.com"},
+    @"google.c.sender.id" : @123456789
+  };
+  [_mockExtensionHelper exportDeliveryMetricsToBigQueryWithMessageInfo:fakeMessageInfo];
+  OCMReject([_mockExtensionHelper bundleIdentifierByRemovingLastPartFrom:[OCMArg any]]);
+}
+
+- (void)testDeliveryMetricsLoggingWithDisplayPayload {
+  OCMStub([_mockUtilClass isAppExtension]).andReturn(YES);
+  NSDictionary *fakeMessageInfo = @{
+    @"aps" : @{@"badge" : @9, @"mutable-content" : @1},
+    @"fcm_options" : @{@"image" : @"https://google.com"},
+    @"gcm.message_id" : @"1627428480762269",
+    @"google.c.fid" : @"fakeFIDForTest",
+    @"google.c.sender.id" : @123456789
+  };
+
+  [_mockExtensionHelper exportDeliveryMetricsToBigQueryWithMessageInfo:fakeMessageInfo];
+  OCMExpect([_mockExtensionHelper bundleIdentifierByRemovingLastPartFrom:[OCMArg any]]);
+}
+
+- (void)testDeliveryMetricsLoggingWithDataPayload {
+  OCMStub([_mockUtilClass isAppExtension]).andReturn(NO);
+  NSDictionary *fakeMessageInfo = @{
+    @"aps" : @{@"badge" : @9, @"content-available" : @1},
+    @"fcm_options" : @{@"image" : @"https://google.com"},
+    @"gcm.message_id" : @"1627428480762269",
+    @"google.c.fid" : @"fakeFIDForTest",
+    @"google.c.sender.id" : @123456789
+  };
+  [_mockExtensionHelper exportDeliveryMetricsToBigQueryWithMessageInfo:fakeMessageInfo];
+  OCMReject([_mockExtensionHelper bundleIdentifierByRemovingLastPartFrom:[OCMArg any]]);
+}
+
 @end
-#endif
+
+#endif  // TARGET_OS_IOS || TARGET_OS_OSX || TARGET_OS_WATCH

+ 0 - 4
FirebaseMessaging/Tests/UnitTests/FIRMessagingUtilitiesTest.m

@@ -78,11 +78,7 @@
 
   [[[_mainBundleMock stub] andReturn:bundleIdentifier] bundleIdentifier];
   NSString *appIdentifier = FIRMessagingAppIdentifier();
-#if TARGET_OS_WATCH
-  XCTAssertEqualObjects(appIdentifier, expectedIdentifier);
-#else
   XCTAssertEqualObjects(appIdentifier, expectedIdentifier);
-#endif
 }
 
 - (void)testAppIdentifierReturnsEmptyStringWhenNotFound {

+ 5 - 0
Package.swift

@@ -793,11 +793,16 @@ let package = Package(
         .product(name: "GULEnvironment", package: "GoogleUtilities"),
         .product(name: "GULReachability", package: "GoogleUtilities"),
         .product(name: "GULUserDefaults", package: "GoogleUtilities"),
+        .product(name: "GoogleDataTransport", package: "GoogleDataTransport"),
+        .product(name: "nanopb", package: "nanopb"),
       ],
       path: "FirebaseMessaging/Sources",
       publicHeadersPath: "Public",
       cSettings: [
         .headerSearchPath("../../"),
+        .define("PB_FIELD_32BIT", to: "1"),
+        .define("PB_NO_PACKED_STRUCTS", to: "1"),
+        .define("PB_ENABLE_MALLOC", to: "1"),
       ],
       linkerSettings: [
         .linkedFramework("SystemConfiguration", .when(platforms: [.iOS, .macOS, .tvOS])),