Jelajahi Sumber

[Sessions] Add ApplicationInfo for filling in the proto (#10376)

Sam Edson 3 tahun lalu
induk
melakukan
00f25acfbe

+ 2 - 0
FirebaseSessions.podspec

@@ -39,10 +39,12 @@ Pod::Spec.new do |s|
     base_dir + 'Protogen/**/*.{c,h,m,mm}',
   ]
 
+  s.ios.framework = 'CoreTelephony'
   s.dependency 'FirebaseCore', '~> 10.0'
   s.dependency 'FirebaseCoreExtension', '~> 10.0'
   s.dependency 'FirebaseInstallations', '~> 10.0'
   s.dependency 'GoogleDataTransport', '~> 9.2'
+  s.dependency 'GoogleUtilities/Environment', '~> 7.8'
   s.dependency 'nanopb', '>= 2.30908.0', '< 2.30910.0'
 
   s.pod_target_xcconfig = {

+ 63 - 0
FirebaseSessions/Sources/ApplicationInfo.swift

@@ -0,0 +1,63 @@
+//
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import Foundation
+
+@_implementationOnly import FirebaseCore
+@_implementationOnly import GoogleUtilities
+
+protocol ApplicationInfoProtocol {
+  /// Google App ID / GMP App ID
+  var appID: String { get }
+
+  /// App's bundle ID / bundle short version
+  var bundleID: String { get }
+
+  /// Version of the Firebase SDK
+  var sdkVersion: String { get }
+
+  /// Crashlytics-specific device / OS filter values.
+  var osName: String { get }
+
+  /// Validated Mobile Country Code and Mobile Network Code
+  var mccMNC: String { get }
+}
+
+class ApplicationInfo: ApplicationInfoProtocol {
+  let appID: String
+
+  init(appID: String) {
+    self.appID = appID
+  }
+
+  var bundleID: String {
+    return Bundle.main.bundleIdentifier ?? ""
+  }
+
+  var sdkVersion: String {
+    return FirebaseVersion()
+  }
+
+  var osName: String {
+    // TODO: Update once https://github.com/google/GoogleUtilities/pull/89 is released
+    // to production, update this to GULAppEnvironmentUtil.appleDevicePlatform() and update
+    // the podfile to depend on the newest version of GoogleUtilities
+    return GULAppEnvironmentUtil.applePlatform()
+  }
+
+  var mccMNC: String {
+    return FIRSESGetMccMnc() ?? ""
+  }
+}

+ 7 - 3
FirebaseSessions/Sources/FirebaseSessions.swift

@@ -39,6 +39,7 @@ protocol SessionsProvider {
   private let coordinator: SessionCoordinator
   private let initiator: SessionInitiator
   private let identifiers: Identifiers
+  private let appInfo: ApplicationInfo
 
   // MARK: - Initializers
 
@@ -55,27 +56,30 @@ protocol SessionsProvider {
     let identifiers = Identifiers(installations: installations)
     let coordinator = SessionCoordinator(identifiers: identifiers, fireLogger: fireLogger)
     let initiator = SessionInitiator()
+    let appInfo = ApplicationInfo(appID: appID)
 
     self.init(appID: appID,
               identifiers: identifiers,
               coordinator: coordinator,
-              initiator: initiator)
+              initiator: initiator,
+              appInfo: appInfo)
   }
 
   // Initializes the SDK and begines the process of listening for lifecycle events and logging events
   init(appID: String, identifiers: Identifiers, coordinator: SessionCoordinator,
-       initiator: SessionInitiator) {
+       initiator: SessionInitiator, appInfo: ApplicationInfo) {
     self.appID = appID
 
     self.identifiers = identifiers
     self.coordinator = coordinator
     self.initiator = initiator
+    self.appInfo = appInfo
 
     super.init()
 
     self.initiator.beginListening {
       self.identifiers.generateNewSessionID()
-      let event = SessionStartEvent(identifiers: self.identifiers)
+      let event = SessionStartEvent(identifiers: self.identifiers, appInfo: self.appInfo)
       DispatchQueue.global().async {
         self.coordinator.attemptLoggingSessionStart(event: event) { result in
         }

+ 11 - 0
FirebaseSessions/Sources/NanoPB/FIRSESNanoPBHelpers.h

@@ -16,6 +16,14 @@
 #ifndef FIRSESNanoPBHelpers_h
 #define FIRSESNanoPBHelpers_h
 
+#import <TargetConditionals.h>
+#if __has_include("CoreTelephony/CTTelephonyNetworkInfo.h") && !TARGET_OS_MACCATALYST && \
+                  !TARGET_OS_OSX && !TARGET_OS_TV
+#define TARGET_HAS_MOBILE_CONNECTIVITY
+#import <CoreTelephony/CTCarrier.h>
+#import <CoreTelephony/CTTelephonyNetworkInfo.h>
+#endif
+
 #import <nanopb/pb.h>
 #import <nanopb/pb_decode.h>
 #import <nanopb/pb_encode.h>
@@ -57,6 +65,9 @@ BOOL FIRSESIsPBStringEqual(pb_bytes_array_t* _Nullable pbString, NSString* _Null
 /// @param data NSData that's expected
 BOOL FIRSESIsPBDataEqual(pb_bytes_array_t* _Nullable pbArray, NSData* _Nullable data);
 
+/// Returns the validated MccMnc if it is available, or nil if the device does not support telephone
+NSString* _Nullable FIRSESGetMccMnc(void);
+
 NS_ASSUME_NONNULL_END
 
 #endif /* FIRSESNanoPBHelpers_h */

+ 34 - 1
FirebaseSessions/Sources/NanoPB/FIRSESNanoPBHelpers.m

@@ -15,7 +15,7 @@
 
 #import <Foundation/Foundation.h>
 
-#import "FirebaseSessions/Sources/NanoPB/FIRSESNanoPBHelpers.m"
+#import "FirebaseSessions/Sources/NanoPB/FIRSESNanoPBHelpers.h"
 
 #import <nanopb/pb.h>
 #import <nanopb/pb_decode.h>
@@ -126,4 +126,37 @@ BOOL FIRSESIsPBDataEqual(pb_bytes_array_t *_Nullable pbArray, NSData *_Nullable
   return equal;
 }
 
+#ifdef TARGET_HAS_MOBILE_CONNECTIVITY
+CTTelephonyNetworkInfo *_Nullable FIRSESNetworkInfo(void) {
+  static CTTelephonyNetworkInfo *networkInfo;
+  static dispatch_once_t onceToken;
+  dispatch_once(&onceToken, ^{
+    networkInfo = [[CTTelephonyNetworkInfo alloc] init];
+  });
+  return networkInfo;
+}
+
+NSString *FIRSESValidatedMccMnc(NSString *mcc, NSString *mnc) {
+  if ([mcc length] != 3 || [mnc length] < 2 || [mnc length] > 3) return nil;
+
+  static NSCharacterSet *notDigits;
+  static dispatch_once_t token;
+  dispatch_once(&token, ^{
+    notDigits = [[NSCharacterSet decimalDigitCharacterSet] invertedSet];
+  });
+  NSString *mccMnc = [mcc stringByAppendingString:mnc];
+  if ([mccMnc rangeOfCharacterFromSet:notDigits].location != NSNotFound) return nil;
+  return mccMnc;
+}
+#endif
+
+NSString *_Nullable FIRSESGetMccMnc(void) {
+#ifdef TARGET_HAS_MOBILE_CONNECTIVITY
+  CTTelephonyNetworkInfo *networkInfo = FIRSESNetworkInfo();
+  CTCarrier *provider = networkInfo.subscriberCellularProvider;
+  return FIRSESValidatedMccMnc(provider.mobileCountryCode, provider.mobileNetworkCode);
+#endif
+  return nil;
+}
+
 NS_ASSUME_NONNULL_END

+ 41 - 5
FirebaseSessions/Sources/SessionStartEvent.swift

@@ -25,7 +25,8 @@ import Foundation
 class SessionStartEvent: NSObject, GDTCOREventDataObject {
   var proto: firebase_appquality_sessions_SessionEvent
 
-  init(identifiers: IdentifierProvider, time: TimeProvider = Time()) {
+  init(identifiers: IdentifierProvider, appInfo: ApplicationInfoProtocol,
+       time: TimeProvider = Time()) {
     proto = firebase_appquality_sessions_SessionEvent()
 
     super.init()
@@ -34,16 +35,23 @@ class SessionStartEvent: NSObject, GDTCOREventDataObject {
     proto.session_data.session_id = makeProtoString(identifiers.sessionID)
     proto.session_data.previous_session_id = makeProtoString(identifiers.previousSessionID)
     proto.session_data.event_timestamp_us = time.timestampUS
+
+    proto.application_info.app_id = makeProtoString(appInfo.appID)
+    proto.application_info.session_sdk_version = makeProtoString(appInfo.sdkVersion)
+//    proto.application_info.device_model = makeProtoString(appInfo.deviceModel)
+//    proto.application_info.development_platform_name;
+//    proto.application_info.development_platform_version;
+
+    proto.application_info.apple_app_info.bundle_short_version = makeProtoString(appInfo.bundleID)
+//    proto.application_info.apple_app_info.network_connection_info
+    proto.application_info.apple_app_info.os_name = convertOSName(osName: appInfo.osName)
+    proto.application_info.apple_app_info.mcc_mnc = makeProtoString(appInfo.mccMNC)
   }
 
   func setInstallationID(identifiers: IdentifierProvider) {
     proto.session_data.firebase_installation_id = makeProtoString(identifiers.installationID)
   }
 
-  private func makeProtoString(_ string: String) -> UnsafeMutablePointer<pb_bytes_array_t>? {
-    return FIRSESEncodeString(string)
-  }
-
   // MARK: - GDTCOREventDataObject
 
   func transportBytes() -> Data {
@@ -59,4 +67,32 @@ class SessionStartEvent: NSObject, GDTCOREventDataObject {
     }
     return data
   }
+
+  // MARK: - Data Conversion
+
+  private func makeProtoString(_ string: String) -> UnsafeMutablePointer<pb_bytes_array_t>? {
+    return FIRSESEncodeString(string)
+  }
+
+  private func convertOSName(osName: String) -> firebase_appquality_sessions_OsName {
+    switch osName.lowercased() {
+    case "macos":
+      return firebase_appquality_sessions_OsName_MACOS
+    case "maccatalyst":
+      return firebase_appquality_sessions_OsName_MACCATALYST
+    case "ios_on_mac":
+      return firebase_appquality_sessions_OsName_IOS_ON_MAC
+    case "ios":
+      return firebase_appquality_sessions_OsName_IOS
+    case "tvos":
+      return firebase_appquality_sessions_OsName_TVOS
+    case "watchos":
+      return firebase_appquality_sessions_OsName_WATCHOS
+    case "ipados":
+      return firebase_appquality_sessions_OsName_IPADOS
+    default:
+      Logger.logWarning("Found unknown OSName: \"\(osName)\" while converting.")
+      return firebase_appquality_sessions_OsName_UNKNOWN_OSNAME
+    }
+  }
 }

+ 44 - 0
FirebaseSessions/Tests/Unit/Mocks/MockApplicationInfo.swift

@@ -0,0 +1,44 @@
+//
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import Foundation
+
+@testable import FirebaseSessions
+
+class MockApplicationInfo: ApplicationInfoProtocol {
+  var appID: String = ""
+
+  var bundleID: String = ""
+
+  var sdkVersion: String = ""
+
+  var osName: String = ""
+
+  var mccMNC: String = ""
+
+  static let testAppID = "testAppID"
+  static let testBundleID = "testBundleID"
+  static let testSDKVersion = "testSDKVersion"
+  static let testOSName = "ios"
+  static let testMCCMNC = "testMCCMNC"
+
+  func mockAllInfo() {
+    appID = MockApplicationInfo.testAppID
+    bundleID = MockApplicationInfo.testBundleID
+    sdkVersion = MockApplicationInfo.testSDKVersion
+    osName = MockApplicationInfo.testOSName
+    mccMNC = MockApplicationInfo.testMCCMNC
+  }
+}

+ 3 - 2
FirebaseSessions/Tests/Unit/SessionCoordinatorTests.swift

@@ -21,6 +21,7 @@ class SessionCoordinatorTests: XCTestCase {
   var identifiers = MockIdentifierProvider()
   var time = MockTimeProvider()
   var fireLogger = MockGDTLogger()
+  var appInfo = MockApplicationInfo()
 
   var coordinator: SessionCoordinator!
 
@@ -33,7 +34,7 @@ class SessionCoordinatorTests: XCTestCase {
   func test_attemptLoggingSessionStart_logsToGDT() throws {
     identifiers.mockAllValidIDs()
 
-    let event = SessionStartEvent(identifiers: identifiers, time: time)
+    let event = SessionStartEvent(identifiers: identifiers, appInfo: appInfo, time: time)
     var resultSuccess = false
     coordinator.attemptLoggingSessionStart(event: event) { result in
       switch result {
@@ -59,7 +60,7 @@ class SessionCoordinatorTests: XCTestCase {
     identifiers.mockAllValidIDs()
     fireLogger.result = .failure(NSError(domain: "TestError", code: -1))
 
-    let event = SessionStartEvent(identifiers: identifiers, time: time)
+    let event = SessionStartEvent(identifiers: identifiers, appInfo: appInfo, time: time)
 
     // Start success so it must be set to false
     var resultSuccess = true

+ 58 - 2
FirebaseSessions/Tests/Unit/SessionStartEventTests.swift

@@ -20,18 +20,20 @@ import XCTest
 class SessionStartEventTests: XCTestCase {
   var identifiers: MockIdentifierProvider!
   var time: MockTimeProvider!
+  var appInfo: MockApplicationInfo!
 
   override func setUp() {
     super.setUp()
 
     identifiers = MockIdentifierProvider()
     time = MockTimeProvider()
+    appInfo = MockApplicationInfo()
   }
 
   func test_init_setsSessionIDs() {
     identifiers.mockAllValidIDs()
 
-    let event = SessionStartEvent(identifiers: identifiers, time: time)
+    let event = SessionStartEvent(identifiers: identifiers, appInfo: appInfo, time: time)
     assertEqualProtoString(
       event.proto.session_data.session_id,
       expected: MockIdentifierProvider.testSessionID,
@@ -46,10 +48,43 @@ class SessionStartEventTests: XCTestCase {
     XCTAssertEqual(event.proto.session_data.event_timestamp_us, 123)
   }
 
+  func test_init_setsApplicationInfo() {
+    appInfo.mockAllInfo()
+
+    let event = SessionStartEvent(identifiers: identifiers, appInfo: appInfo, time: time)
+
+    assertEqualProtoString(
+      event.proto.application_info.app_id,
+      expected: MockApplicationInfo.testAppID,
+      fieldName: "app_id"
+    )
+    assertEqualProtoString(
+      event.proto.application_info.session_sdk_version,
+      expected: MockApplicationInfo.testSDKVersion,
+      fieldName: "session_sdk_version"
+    )
+    assertEqualProtoString(
+      event.proto.application_info.apple_app_info.bundle_short_version,
+      expected: MockApplicationInfo.testBundleID,
+      fieldName: "bundle_short_version"
+    )
+    assertEqualProtoString(
+      event.proto.application_info.apple_app_info.mcc_mnc,
+      expected: MockApplicationInfo.testMCCMNC,
+      fieldName: "mcc_mnc"
+    )
+
+    // Ensure we convert the test OS name into the enum.
+    XCTAssertEqual(
+      event.proto.application_info.apple_app_info.os_name,
+      firebase_appquality_sessions_OsName_IOS
+    )
+  }
+
   func test_setInstallationID_setsInstallationID() {
     identifiers.mockAllValidIDs()
 
-    let event = SessionStartEvent(identifiers: identifiers, time: time)
+    let event = SessionStartEvent(identifiers: identifiers, appInfo: appInfo, time: time)
     event.setInstallationID(identifiers: identifiers)
     assertEqualProtoString(
       event.proto.session_data.firebase_installation_id,
@@ -57,4 +92,25 @@ class SessionStartEventTests: XCTestCase {
       fieldName: "firebase_installation_id"
     )
   }
+
+  func test_convertOSName_convertsCorrectly() {
+    let expectations: [(given: String, expected: firebase_appquality_sessions_OsName)] = [
+      ("macos", firebase_appquality_sessions_OsName_MACOS),
+      ("maccatalyst", firebase_appquality_sessions_OsName_MACCATALYST),
+      ("ios_on_mac", firebase_appquality_sessions_OsName_IOS_ON_MAC),
+      ("ios", firebase_appquality_sessions_OsName_IOS),
+      ("tvos", firebase_appquality_sessions_OsName_TVOS),
+      ("watchos", firebase_appquality_sessions_OsName_WATCHOS),
+      ("ipados", firebase_appquality_sessions_OsName_IPADOS),
+      ("something unknown", firebase_appquality_sessions_OsName_UNKNOWN_OSNAME),
+    ]
+
+    expectations.forEach { (given: String, expected: firebase_appquality_sessions_OsName) in
+      appInfo.osName = given
+
+      let event = SessionStartEvent(identifiers: identifiers, appInfo: appInfo, time: time)
+
+      XCTAssertEqual(event.proto.application_info.apple_app_info.os_name, expected)
+    }
+  }
 }