소스 검색

add heartbeat feature for ios sdks (#4098)

* add heartbeat feature

* fix client logtype

* make class functions

* add subspec changes

* remove test file

* add heartbeat  tests

* use FileDataStorage instead of Storage

* add style

* use file data storage instead of storage

* fix test location

* add tests

* address comments

* update storage heartbeat with the new api

* update format

* update formatting and use correct enums

* rename

* update storage

* fix heartbeat

* update heartbeat tests

* fix styling

* Revert change

* heartbeat storage

* add comments

* update storage

* address comments

* add comments

* address comments

* fix style

* add style

* add style

* move heartbeat date storage

* move to environment

* update tests

* add tests

* small change

* add google utilities

* delete the header file

* update tests

* remove headers

* fix comment

* gul heartbeat date storage

* Update versions for heartbeat PR (#4311)

* update podfile

* Fix Firestore build under CMake

GoogleUtilities must now build SecureCoding
Vinay Guthal 6 년 전
부모
커밋
be17ade458

+ 61 - 0
Example/Core/Tests/FIRHeartbeatInfoTest.m

@@ -0,0 +1,61 @@
+// 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 <FirebaseCore/FIRHeartbeatInfo.h>
+#import <GoogleUtilities/GULHeartbeatDateStorage.h>
+#import <XCTest/XCTest.h>
+
+@interface FIRHeartbeatInfoTest : XCTestCase
+
+@property(nonatomic, strong) GULHeartbeatDateStorage *dataStorage;
+
+@property(nonatomic, strong) NSMutableDictionary *dictionary;
+
+@end
+
+@implementation FIRHeartbeatInfoTest
+
+- (void)setUp {
+  NSString *const kHeartbeatStorageFile = @"HEARTBEAT_INFO_STORAGE";
+  self.dataStorage = [[GULHeartbeatDateStorage alloc] initWithFileName:kHeartbeatStorageFile];
+  NSDate *pastTime = [NSDate dateWithTimeIntervalSinceNow:-996400];
+  [self.dataStorage setHearbeatDate:pastTime forTag:@"fire-iid"];
+  [self.dataStorage setHearbeatDate:pastTime forTag:@"GLOBAL"];
+}
+
+- (void)testCombinedHeartbeat {
+  FIRHeartbeatInfoCode heartbeatCode = [FIRHeartbeatInfo heartbeatCodeForTag:@"fire-iid"];
+  XCTAssertEqual(heartbeatCode, FIRHeartbeatInfoCodeCombined);
+}
+
+- (void)testSdkOnlyHeartbeat {
+  [self.dataStorage setHearbeatDate:[NSDate date] forTag:@"GLOBAL"];
+  FIRHeartbeatInfoCode heartbeatCode = [FIRHeartbeatInfo heartbeatCodeForTag:@"fire-iid"];
+  XCTAssertEqual(heartbeatCode, FIRHeartbeatInfoCodeSDK);
+}
+
+- (void)testGlobalOnlyHeartbeat {
+  [self.dataStorage setHearbeatDate:[NSDate date] forTag:@"fire-iid"];
+  FIRHeartbeatInfoCode heartbeatCode = [FIRHeartbeatInfo heartbeatCodeForTag:@"fire-iid"];
+  XCTAssertEqual(heartbeatCode, FIRHeartbeatInfoCodeGlobal);
+}
+
+- (void)testNoHeartbeat {
+  [self.dataStorage setHearbeatDate:[NSDate date] forTag:@"fire-iid"];
+  [self.dataStorage setHearbeatDate:[NSDate date] forTag:@"GLOBAL"];
+  FIRHeartbeatInfoCode heartbeatCode = [FIRHeartbeatInfo heartbeatCodeForTag:@"fire-iid"];
+  XCTAssertEqual(heartbeatCode, FIRHeartbeatInfoCodeNone);
+}
+
+@end

+ 32 - 29
Example/CoreDiagnostics/Tests/FIRCoreDiagnosticsTest.m

@@ -26,6 +26,7 @@
 #import <GoogleDataTransport/GDTCORTransport.h>
 #import <GoogleDataTransportCCTSupport/GDTCCTPrioritizer.h>
 #import <GoogleUtilities/GULAppEnvironmentUtil.h>
+#import <GoogleUtilities/GULHeartbeatDateStorage.h>
 #import <GoogleUtilities/GULUserDefaults.h>
 #import <OCMock/OCMock.h>
 #import <nanopb/pb_decode.h>
@@ -33,14 +34,13 @@
 
 #import "FIRCDLibrary/Protogen/nanopb/firebasecore.nanopb.h"
 
-#import "FIRCDLibrary/FIRCoreDiagnosticsDateFileStorage.h"
-
 extern NSString *const kFIRAppDiagnosticsNotification;
 extern NSString *const kFIRLastCheckinDateKey;
 
 static NSString *const kGoogleAppID = @"1:123:ios:123abc";
 static NSString *const kBundleID = @"com.google.FirebaseSDKTests";
 static NSString *const kLibraryVersionID = @"1.2.3";
+static NSString *const kFIRCoreDiagnosticsHeartbeatTag = @"FIRCoreDiagnostics";
 
 #pragma mark - Testing interfaces
 
@@ -48,17 +48,16 @@ static NSString *const kLibraryVersionID = @"1.2.3";
 // Initialization.
 + (instancetype)sharedInstance;
 - (instancetype)initWithTransport:(GDTCORTransport *)transport
-             heartbeatDateStorage:(FIRCoreDiagnosticsDateFileStorage *)heartbeatDateStorage;
+             heartbeatDateStorage:(GULHeartbeatDateStorage *)heartbeatDateStorage;
 
 // Properties.
 @property(nonatomic, readonly) dispatch_queue_t diagnosticsQueue;
 @property(nonatomic, readonly) GDTCORTransport *transport;
-@property(nonatomic, readonly) FIRCoreDiagnosticsDateFileStorage *heartbeatDateStorage;
+@property(nonatomic, readonly) GULHeartbeatDateStorage *heartbeatDateStorage;
 
 // Install string helpers.
 + (NSString *)installString;
 + (BOOL)writeString:(NSString *)string toURL:(NSURL *)filePathURL;
-+ (NSURL *)filePathURLWithName:(NSString *)fileName;
 + (NSString *)stringAtURL:(NSURL *)filePathURL;
 
 // Metadata helpers.
@@ -140,7 +139,7 @@ extern void FIRPopulateProtoWithInfoPlistValues(
   OCMStub([self.mockTransport eventForTransport])
       .andReturn([[GDTCOREvent alloc] initWithMappingID:@"111" target:2]);
 
-  self.mockDateStorage = OCMClassMock([FIRCoreDiagnosticsDateFileStorage class]);
+  self.mockDateStorage = OCMClassMock([GULHeartbeatDateStorage class]);
   self.diagnostics = [[FIRCoreDiagnostics alloc] initWithTransport:self.mockTransport
                                               heartbeatDateStorage:self.mockDateStorage];
 }
@@ -244,18 +243,20 @@ extern void FIRPopulateProtoWithInfoPlistValues(
 
   // Verify start of the day
   NSDate *startOfTheDay = [calendar dateFromComponents:dateComponents];
-  OCMExpect([self.mockDateStorage date]).andReturn(startOfTheDay);
-  OCMReject([self.mockDateStorage setDate:[self OCMArgToCheckDateEqualTo:[OCMArg any]]
-                                    error:[OCMArg anyObjectRef]]);
+  OCMExpect([self.mockDateStorage heartbeatDateForTag:kFIRCoreDiagnosticsHeartbeatTag])
+      .andReturn(startOfTheDay);
+  OCMReject([self.mockDateStorage setHearbeatDate:[self OCMArgToCheckDateEqualTo:[OCMArg any]]
+                                           forTag:kFIRCoreDiagnosticsHeartbeatTag]);
 
   [self assertEventSentWithHeartbeat:NO];
 
   // Verify middle of the day
   dateComponents.hour = 12;
   NSDate *middleOfTheDay = [calendar dateFromComponents:dateComponents];
-  OCMExpect([self.mockDateStorage date]).andReturn(middleOfTheDay);
-  OCMReject([self.mockDateStorage setDate:[self OCMArgToCheckDateEqualTo:[OCMArg any]]
-                                    error:[OCMArg anyObjectRef]]);
+  OCMExpect([self.mockDateStorage heartbeatDateForTag:kFIRCoreDiagnosticsHeartbeatTag])
+      .andReturn(middleOfTheDay);
+  OCMReject([self.mockDateStorage setHearbeatDate:[self OCMArgToCheckDateEqualTo:[OCMArg any]]
+                                           forTag:kFIRCoreDiagnosticsHeartbeatTag]);
 
   [self assertEventSentWithHeartbeat:NO];
 
@@ -264,17 +265,18 @@ extern void FIRPopulateProtoWithInfoPlistValues(
   dateComponents.day += 1;
   NSDate *startOfNextDay = [calendar dateFromComponents:dateComponents];
   NSDate *endOfTheDay = [startOfNextDay dateByAddingTimeInterval:-1];
-  OCMExpect([self.mockDateStorage date]).andReturn(endOfTheDay);
-  OCMReject([self.mockDateStorage setDate:[self OCMArgToCheckDateEqualTo:[OCMArg any]]
-                                    error:[OCMArg anyObjectRef]]);
-
+  OCMExpect([self.mockDateStorage heartbeatDateForTag:kFIRCoreDiagnosticsHeartbeatTag])
+      .andReturn(endOfTheDay);
+  OCMReject([self.mockDateStorage setHearbeatDate:[self OCMArgToCheckDateEqualTo:[OCMArg any]]
+                                           forTag:kFIRCoreDiagnosticsHeartbeatTag]);
   [self assertEventSentWithHeartbeat:NO];
 }
 
 - (void)testHeartbeatSentNoPreviousCheckin {
-  OCMExpect([self.mockDateStorage date]).andReturn(nil);
-  OCMExpect([self.mockDateStorage setDate:[self OCMArgToCheckDateEqualTo:[NSDate date]]
-                                    error:[OCMArg anyObjectRef]]);
+  OCMExpect([self.mockDateStorage heartbeatDateForTag:kFIRCoreDiagnosticsHeartbeatTag])
+      .andReturn(nil);
+  OCMExpect([self.mockDateStorage setHearbeatDate:[self OCMArgToCheckDateEqualTo:[NSDate date]]
+                                           forTag:kFIRCoreDiagnosticsHeartbeatTag]);
 
   [self assertEventSentWithHeartbeat:YES];
 }
@@ -283,9 +285,10 @@ extern void FIRPopulateProtoWithInfoPlistValues(
   NSDate *startOfToday = [[NSCalendar currentCalendar] startOfDayForDate:[NSDate date]];
   NSDate *endOfYesterday = [startOfToday dateByAddingTimeInterval:-1];
 
-  OCMExpect([self.mockDateStorage date]).andReturn(endOfYesterday);
-  OCMExpect([self.mockDateStorage setDate:[self OCMArgToCheckDateEqualTo:[NSDate date]]
-                                    error:[OCMArg anyObjectRef]]);
+  OCMExpect([self.mockDateStorage heartbeatDateForTag:kFIRCoreDiagnosticsHeartbeatTag])
+      .andReturn(endOfYesterday);
+  OCMExpect([self.mockDateStorage setHearbeatDate:[self OCMArgToCheckDateEqualTo:[NSDate date]]
+                                           forTag:kFIRCoreDiagnosticsHeartbeatTag]);
 
   [self assertEventSentWithHeartbeat:YES];
 }
@@ -295,16 +298,16 @@ extern void FIRPopulateProtoWithInfoPlistValues(
 - (void)testSharedInstanceDateStorageProperlyInitialized {
   FIRCoreDiagnostics *sharedInstance = [FIRCoreDiagnostics sharedInstance];
   XCTAssertNotNil(sharedInstance.heartbeatDateStorage);
-  XCTAssert([sharedInstance.heartbeatDateStorage
-      isKindOfClass:[FIRCoreDiagnosticsDateFileStorage class]]);
+  XCTAssert([sharedInstance.heartbeatDateStorage isKindOfClass:[GULHeartbeatDateStorage class]]);
 
   NSDate *date = [NSDate date];
 
-  NSError *error;
-  XCTAssertTrue([sharedInstance.heartbeatDateStorage setDate:date error:&error], @"Error %@",
-                error);
-
-  XCTAssertEqualObjects([sharedInstance.heartbeatDateStorage date], date);
+  XCTAssertTrue([sharedInstance.heartbeatDateStorage
+      setHearbeatDate:date
+               forTag:kFIRCoreDiagnosticsHeartbeatTag]);
+  XCTAssertEqualObjects(
+      [sharedInstance.heartbeatDateStorage heartbeatDateForTag:kFIRCoreDiagnosticsHeartbeatTag],
+      date);
 }
 
 #pragma mark - Helpers

+ 61 - 0
Firebase/Core/FIRHeartbeatInfo.m

@@ -0,0 +1,61 @@
+// 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 "FIRHeartbeatInfo.h"
+#import <GoogleUtilities/GULHeartbeatDateStorage.h>
+#import <GoogleUtilities/GULLogger.h>
+
+const static long secondsInDay = 864000;
+@implementation FIRHeartbeatInfo : NSObject
+
+/** Updates the storage with the heartbeat information corresponding to this tag.
+ * @param heartbeatTag Tag which could either be sdk specific tag or the global tag.
+ * @return Boolean representing whether the heartbeat needs to be sent for this tag or not.
+ */
++ (BOOL)updateIfNeededHeartbeatDateForTag:(NSString *)heartbeatTag {
+  @synchronized(self) {
+    NSString *const kHeartbeatStorageFile = @"HEARTBEAT_INFO_STORAGE";
+    GULHeartbeatDateStorage *dataStorage =
+        [[GULHeartbeatDateStorage alloc] initWithFileName:kHeartbeatStorageFile];
+    NSDate *heartbeatTime = [dataStorage heartbeatDateForTag:heartbeatTag];
+    NSDate *currentDate = [NSDate date];
+    if (heartbeatTime != nil) {
+      NSTimeInterval secondsBetween = [currentDate timeIntervalSinceDate:heartbeatTime];
+      if (secondsBetween < secondsInDay) {
+        return false;
+      }
+    }
+    return [dataStorage setHearbeatDate:currentDate forTag:heartbeatTag];
+  }
+}
+
++ (FIRHeartbeatInfoCode)heartbeatCodeForTag:(NSString *)heartbeatTag {
+  NSString *globalTag = @"GLOBAL";
+  BOOL isSdkHeartbeatNeeded = [FIRHeartbeatInfo updateIfNeededHeartbeatDateForTag:heartbeatTag];
+  BOOL isGlobalHeartbeatNeeded = [FIRHeartbeatInfo updateIfNeededHeartbeatDateForTag:globalTag];
+  if (!isSdkHeartbeatNeeded && !isGlobalHeartbeatNeeded) {
+    // Both sdk and global heartbeat not needed.
+    return FIRHeartbeatInfoCodeNone;
+  } else if (isSdkHeartbeatNeeded && !isGlobalHeartbeatNeeded) {
+    // Only SDK heartbeat needed.
+    return FIRHeartbeatInfoCodeSDK;
+  } else if (!isSdkHeartbeatNeeded && isGlobalHeartbeatNeeded) {
+    // Only global heartbeat needed.
+    return FIRHeartbeatInfoCodeGlobal;
+  } else {
+    // Both sdk and global heartbeat are needed.
+    return FIRHeartbeatInfoCodeCombined;
+  }
+}
+@end

+ 39 - 0
Firebase/Core/Private/FIRHeartbeatInfo.h

@@ -0,0 +1,39 @@
+// 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
+
+@interface FIRHeartbeatInfo : NSObject
+
+// Enum representing the different heartbeat codes.
+typedef NS_ENUM(NSInteger, FIRHeartbeatInfoCode) {
+  FIRHeartbeatInfoCodeNone = 0,
+  FIRHeartbeatInfoCodeSDK = 1,
+  FIRHeartbeatInfoCodeGlobal = 2,
+  FIRHeartbeatInfoCodeCombined = 3,
+};
+
+/**
+ * Get heartbeat code requred for the sdk.
+ * @param heartbeatTag String representing the sdk heartbeat tag.
+ * @return Heartbeat code indicating whether or not an sdk/global heartbeat
+ * needs to be sent
+ */
++ (FIRHeartbeatInfoCode)heartbeatCodeForTag:(NSString *)heartbeatTag;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 9 - 44
Firebase/CoreDiagnostics/FIRCDLibrary/FIRCoreDiagnostics.m

@@ -23,6 +23,7 @@
 #import <GoogleDataTransport/GDTCORTransport.h>
 
 #import <GoogleUtilities/GULAppEnvironmentUtil.h>
+#import <GoogleUtilities/GULHeartbeatDateStorage.h>
 #import <GoogleUtilities/GULLogger.h>
 
 #import <FirebaseCoreDiagnosticsInterop/FIRCoreDiagnosticsData.h>
@@ -34,8 +35,6 @@
 
 #import "FIRCDLibrary/Protogen/nanopb/firebasecore.nanopb.h"
 
-#import "FIRCDLibrary/FIRCoreDiagnosticsDateFileStorage.h"
-
 /** The logger service string to use when printing to the console. */
 static GULLoggerService kFIRCoreDiagnostics = @"[FirebaseCoreDiagnostics/FIRCoreDiagnostics]";
 
@@ -85,6 +84,7 @@ static NSString *const kFIRAppDiagnosticsConfigurationTypeKey =
 static NSString *const kFIRAppDiagnosticsFIRAppKey = @"FIRAppDiagnosticsFIRAppKey";
 static NSString *const kFIRAppDiagnosticsSDKNameKey = @"FIRAppDiagnosticsSDKNameKey";
 static NSString *const kFIRAppDiagnosticsSDKVersionKey = @"FIRAppDiagnosticsSDKVersionKey";
+static NSString *const kFIRCoreDiagnosticsHeartbeatTag = @"FIRCoreDiagnostics";
 
 /**
  * The file name to the recent heartbeat date.
@@ -153,7 +153,7 @@ NS_ASSUME_NONNULL_BEGIN
 @property(nonatomic, readonly) GDTCORTransport *transport;
 
 /** The storage to store the date of the last sent heartbeat. */
-@property(nonatomic, readonly) FIRCoreDiagnosticsDateFileStorage *heartbeatDateStorage;
+@property(nonatomic, readonly) GULHeartbeatDateStorage *heartbeatDateStorage;
 
 @end
 
@@ -175,8 +175,8 @@ NS_ASSUME_NONNULL_END
                                                              transformers:nil
                                                                    target:kGDTCORTargetCCT];
 
-  FIRCoreDiagnosticsDateFileStorage *dateStorage = [[FIRCoreDiagnosticsDateFileStorage alloc]
-      initWithFileURL:[[self class] filePathURLWithName:kFIRCoreDiagnosticsHeartbeatDateFileName]];
+  GULHeartbeatDateStorage *dateStorage =
+      [[GULHeartbeatDateStorage alloc] initWithFileName:kFIRCoreDiagnosticsHeartbeatDateFileName];
 
   return [self initWithTransport:transport heartbeatDateStorage:dateStorage];
 }
@@ -188,7 +188,7 @@ NS_ASSUME_NONNULL_END
  * @return Returns the initialized `FIRCoreDiagnostics` instance.
  */
 - (instancetype)initWithTransport:(GDTCORTransport *)transport
-             heartbeatDateStorage:(FIRCoreDiagnosticsDateFileStorage *)heartbeatDateStorage {
+             heartbeatDateStorage:(GULHeartbeatDateStorage *)heartbeatDateStorage {
   self = [super init];
   if (self) {
     _diagnosticsQueue =
@@ -199,37 +199,6 @@ NS_ASSUME_NONNULL_END
   return self;
 }
 
-#pragma mark - File path helpers
-
-/** Returns the URL path of the file with name fileName under the Application Support folder for
- * local logging. Creates the Application Support folder if the folder doesn't exist.
- *
- * @return the URL path of the file with the name fileName in Application Support.
- */
-+ (NSURL *)filePathURLWithName:(NSString *)fileName {
-  @synchronized(self) {
-    NSArray<NSString *> *paths =
-        NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
-    NSArray<NSString *> *components = @[ paths.lastObject, @"Google/FIRApp" ];
-    NSString *directoryString = [NSString pathWithComponents:components];
-    NSURL *directoryURL = [NSURL fileURLWithPath:directoryString];
-
-    NSError *error;
-    if (![directoryURL checkResourceIsReachableAndReturnError:&error]) {
-      // If fail creating the Application Support directory, return nil.
-      if (![[NSFileManager defaultManager] createDirectoryAtURL:directoryURL
-                                    withIntermediateDirectories:YES
-                                                     attributes:nil
-                                                          error:&error]) {
-        GULLogWarning(kFIRCoreDiagnostics, YES, @"I-COR100001",
-                      @"Unable to create internal state storage: %@", error);
-        return nil;
-      }
-    }
-    return [directoryURL URLByAppendingPathComponent:fileName];
-  }
-}
-
 #pragma mark - Metadata helpers
 
 /** Returns the model of iOS device. Sample platform strings are @"iPhone7,1" for iPhone 6 Plus,
@@ -648,7 +617,8 @@ void FIRPopulateProtoWithInfoPlistValues(logs_proto_mobilesdk_ios_ICoreConfigura
 - (void)setHeartbeatFlagIfNeededToConfig:(logs_proto_mobilesdk_ios_ICoreConfiguration *)config {
   // Check if need to send a heartbeat.
   NSDate *currentDate = [NSDate date];
-  NSDate *lastCheckin = [self.heartbeatDateStorage date];
+  NSDate *lastCheckin =
+      [self.heartbeatDateStorage heartbeatDateForTag:kFIRCoreDiagnosticsHeartbeatTag];
   if (lastCheckin) {
     // Ensure the previous checkin was on a different date in the past.
     if ([self isDate:currentDate inSameDayOrBeforeThan:lastCheckin]) {
@@ -657,12 +627,7 @@ void FIRPopulateProtoWithInfoPlistValues(logs_proto_mobilesdk_ios_ICoreConfigura
   }
 
   // Update heartbeat sent date.
-  NSError *error;
-  if (![self.heartbeatDateStorage setDate:currentDate error:&error]) {
-    GULLogError(kFIRCoreDiagnostics, NO, @"I-COR100004", @"Unable to persist internal state: %@",
-                error);
-  }
-
+  [self.heartbeatDateStorage setHearbeatDate:currentDate forTag:kFIRCoreDiagnosticsHeartbeatTag];
   // Set the flag.
   config->sdk_name = logs_proto_mobilesdk_ios_ICoreConfiguration_ServiceType_ICORE;
   config->has_sdk_name = 1;

+ 0 - 64
Firebase/CoreDiagnostics/FIRCDLibrary/FIRCoreDiagnosticsDateFileStorage.m

@@ -1,64 +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 "FIRCDLibrary/FIRCoreDiagnosticsDateFileStorage.h"
-
-@interface FIRCoreDiagnosticsDateFileStorage ()
-@property(nonatomic, readonly) NSURL *fileURL;
-@end
-
-@implementation FIRCoreDiagnosticsDateFileStorage
-
-- (instancetype)initWithFileURL:(NSURL *)fileURL {
-  if (fileURL == nil) {
-    return nil;
-  }
-
-  self = [super init];
-  if (self) {
-    _fileURL = fileURL;
-  }
-
-  return self;
-}
-
-- (BOOL)setDate:(nullable NSDate *)date error:(NSError **)outError {
-  NSString *stringToSave = @"";
-
-  if (date != nil) {
-    NSTimeInterval timestamp = [date timeIntervalSinceReferenceDate];
-    stringToSave = [NSString stringWithFormat:@"%f", timestamp];
-  }
-
-  return [stringToSave writeToURL:self.fileURL
-                       atomically:YES
-                         encoding:NSUTF8StringEncoding
-                            error:outError];
-}
-
-- (nullable NSDate *)date {
-  NSString *timestampString = [NSString stringWithContentsOfURL:self.fileURL
-                                                       encoding:NSUTF8StringEncoding
-                                                          error:nil];
-  if (timestampString.length == 0) {
-    return nil;
-  }
-
-  NSTimeInterval timestamp = timestampString.doubleValue;
-  return [NSDate dateWithTimeIntervalSinceReferenceDate:timestamp];
-}
-
-@end

+ 2 - 2
FirebaseCore.podspec

@@ -31,8 +31,8 @@ Firebase Core includes FIRApp and FIROptions which provide central configuration
   s.ios.framework = 'UIKit'
   s.osx.framework = 'AppKit'
   s.tvos.framework = 'UIKit'
-  s.dependency 'GoogleUtilities/Environment', '~> 6.2'
-  s.dependency 'GoogleUtilities/Logger', '~> 6.2'
+  s.dependency 'GoogleUtilities/Environment', '~> 6.4'
+  s.dependency 'GoogleUtilities/Logger', '~> 6.4'
   s.dependency 'FirebaseCoreDiagnosticsInterop', '~> 1.0'
   s.dependency 'FirebaseCoreDiagnostics', '~> 1.0'
 

+ 1 - 1
Firestore/Example/Podfile

@@ -57,6 +57,7 @@ end
 def configure_local_pods()
   # Firestore is always local; that's what's under development here.
   pod 'FirebaseFirestore', :path => '../../'
+  pod 'GoogleUtilities', :path => '../..'
 
   # FirebaseCore must always be a local pod so that CI builds that make changes
   # to its podspec can still function. See Firestore-*-xcodebuild in
@@ -66,7 +67,6 @@ def configure_local_pods()
   # Pull in local sources conditionally.
   maybe_local_pod 'FirebaseAuth'
   maybe_local_pod 'FirebaseAuthInterop'
-  maybe_local_pod 'GoogleUtilities'
 
   if xcode_major_version() >= 9
     # Firestore still compiles with Xcode 8 to help verify general conformance

+ 4 - 2
GoogleUtilities.podspec

@@ -1,10 +1,10 @@
 Pod::Spec.new do |s|
   s.name             = 'GoogleUtilities'
-  s.version          = '6.3.1'
+  s.version          = '6.4.0'
   s.summary          = 'Google Utilities for iOS (plus community support for macOS and tvOS)'
 
   s.description      = <<-DESC
-Internal Google Utilities including Network, Reachability Environment, Logger, and Swizzling for
+Internal Google Utilities including Network, Reachability Environment, Logger and Swizzling for
 other Google CocoaPods. They're not intended for direct public usage.
                        DESC
 
@@ -28,6 +28,7 @@ other Google CocoaPods. They're not intended for direct public usage.
     es.source_files = 'GoogleUtilities/Environment/third_party/*.[mh]'
     es.public_header_files = 'GoogleUtilities/Environment/third_party/*.h'
     es.private_header_files = 'GoogleUtilities/Environment/third_party/*.h'
+    es.dependency 'GoogleUtilities/SecureCoding'
   end
 
   s.subspec 'Logger' do |ls|
@@ -37,6 +38,7 @@ other Google CocoaPods. They're not intended for direct public usage.
     ls.dependency 'GoogleUtilities/Environment'
   end
 
+
   s.subspec 'Network' do |ns|
     ns.source_files = 'GoogleUtilities/Network/**/*.[mh]'
     ns.public_header_files = 'GoogleUtilities/Network/Private/*.h'

+ 8 - 0
GoogleUtilities/CMakeLists.txt

@@ -18,12 +18,14 @@ if(APPLE)
     GLOB sources
     Environment/third_party/*.m
     Logger/*.m
+    SecureCoding/*.m
   )
   file(
     GLOB headers
     Environment/third_party/*.h
     Logger/Private/*.h
     Logger/Public/*.h
+    SecureCoding/Public/*.h
   )
 
   podspec_version(version ${PROJECT_SOURCE_DIR}/GoogleUtilities.podspec)
@@ -40,4 +42,10 @@ if(APPLE)
       "-framework Foundation"
     EXCLUDE_FROM_ALL
   )
+
+  target_compile_options(
+    GoogleUtilities
+    PRIVATE
+      -Wno-deprecated-declarations
+  )
 endif()

+ 12 - 10
Firebase/CoreDiagnostics/FIRCDLibrary/FIRCoreDiagnosticsDateFileStorage.h → GoogleUtilities/Environment/third_party/GULHeartbeatDateStorage.h

@@ -18,29 +18,31 @@
 
 NS_ASSUME_NONNULL_BEGIN
 
-/// Stores a date to a specified file.
-@interface FIRCoreDiagnosticsDateFileStorage : NSObject
+/// Stores either a date or a dictionary to a specified file.
+@interface GULHeartbeatDateStorage : NSObject
 
 - (instancetype)init NS_UNAVAILABLE;
 
+@property(nonatomic, readonly) NSURL *fileURL;
+
 /**
  * Default initializer.
- * @param fileURL The URL of the file to store the date. The directory must exist, the file may not
+ * @param fileName The name of the file to store the date information.
  * exist, it will be created if needed.
  */
-- (instancetype)initWithFileURL:(NSURL *)fileURL;
+- (instancetype)initWithFileName:(NSString *)fileName;
 
 /**
- * Saves the date to the specified file.
- * @return YES on success, NO otherwise.
+ * Reads the date from the specified file for the given tag.
+ * @return Returns date if exists, otherwise `nil`.
  */
-- (BOOL)setDate:(nullable NSDate *)date error:(NSError **)outError;
+- (nullable NSDate *)heartbeatDateForTag:(NSString *)tag;
 
 /**
- * Reads the date to the specified file.
- * @return Returns date if exists, otherwise `nil`.
+ * Saves the date for the specified tag in the specified file.
+ * @return YES on success, NO otherwise.
  */
-- (nullable NSDate *)date;
+- (BOOL)setHearbeatDate:(NSDate *)date forTag:(NSString *)tag;
 
 @end
 

+ 140 - 0
GoogleUtilities/Environment/third_party/GULHeartbeatDateStorage.m

@@ -0,0 +1,140 @@
+/*
+ * 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 "GULHeartbeatDateStorage.h"
+#import <GoogleUtilities/GULSecureCoding.h>
+
+@interface GULHeartbeatDateStorage ()
+/** The storage to store the date of the last sent heartbeat. */
+@property(nonatomic, readonly) NSFileCoordinator *fileCoordinator;
+@end
+
+@implementation GULHeartbeatDateStorage
+
+- (instancetype)initWithFileName:(NSString *)fileName {
+  if (fileName == nil) {
+    return nil;
+  }
+
+  self = [super init];
+  if (self) {
+    _fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
+    NSURL *directoryURL = [[self class] directoryPathURL];
+    [[self class] checkAndCreateDirectory:directoryURL fileCoordinator:_fileCoordinator];
+    _fileURL = [directoryURL URLByAppendingPathComponent:fileName];
+  }
+  return self;
+}
+
+/** Returns the URL path of the Application Support folder.
+ * @return the URL path of Application Support.
+ */
++ (NSURL *)directoryPathURL {
+  NSArray<NSString *> *paths =
+      NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
+  NSArray<NSString *> *components = @[ paths.lastObject, @"Google/FIRApp" ];
+  NSString *directoryString = [NSString pathWithComponents:components];
+  NSURL *directoryURL = [NSURL fileURLWithPath:directoryString];
+  return directoryURL;
+}
+
+/** Checks and creates a directory for the directory specified by the
+ * directory url
+ * @param directoryPathURL The path to the directory which needs to be created.
+ * @param fileCoordinator The fileCoordinator object to coordinate writes to the directory.
+ */
++ (void)checkAndCreateDirectory:(NSURL *)directoryPathURL fileCoordinator:(NSFileCoordinator *)fileCoordinator {
+  NSError *fileCoordinatorError = nil;
+  [fileCoordinator
+      coordinateWritingItemAtURL:directoryPathURL
+                         options:0
+                           error:&fileCoordinatorError
+                      byAccessor:^(NSURL *writingDirectoryURL) {
+                        NSError *error;
+                        if (![writingDirectoryURL checkResourceIsReachableAndReturnError:&error]) {
+                          // If fail creating the Application Support directory, log warning.
+                          NSError *error;
+                          [[NSFileManager defaultManager]
+                                         createDirectoryAtURL:writingDirectoryURL
+                                  withIntermediateDirectories:YES
+                                                   attributes:nil
+                           error:&error];
+                        }
+                      }];
+}
+
+- (nullable NSMutableDictionary *)heartbeatDictionaryWithFileURL:(NSURL *)readingFileURL {
+  NSError *error;
+  NSMutableDictionary *dict;
+  NSData *objectData = [NSData dataWithContentsOfURL:readingFileURL options:0 error:&error];
+  if (objectData == nil || error != nil) {
+    dict = [NSMutableDictionary dictionary];
+  } else {
+    dict = [GULSecureCoding
+            unarchivedObjectOfClasses:[NSSet setWithArray:@[ NSDictionary.class, NSDate.class ]]
+            fromData:objectData
+            error:&error];
+    if (dict == nil || error != nil) {
+      dict = [NSMutableDictionary dictionary];
+    }
+  }
+  return dict;
+}
+
+- (nullable NSDate *)heartbeatDateForTag:(NSString *)tag {
+  __block NSMutableDictionary *dict;
+  NSError *error;
+  [self.fileCoordinator coordinateReadingItemAtURL:self.fileURL
+                                           options:0
+                                             error:&error
+                                        byAccessor:^(NSURL *readingURL) {
+                                          dict = [self heartbeatDictionaryWithFileURL:readingURL];
+                                        }];
+  return dict[tag];
+}
+
+- (BOOL)setHearbeatDate:(NSDate *)date forTag:(NSString *)tag {
+  NSError *error;
+  __block BOOL isSuccess = false;
+  [self.fileCoordinator
+      coordinateReadingItemAtURL:self.fileURL
+                         options:0
+                writingItemAtURL:self.fileURL
+                         options:0
+                           error:&error
+                      byAccessor:^(NSURL *readingURL, NSURL *writingURL) {
+                        NSMutableDictionary *dictionary = [self heartbeatDictionaryWithFileURL:readingURL];
+                        dictionary[tag] = date;
+                        NSError *error;
+                        isSuccess = [self writeDictionary:dictionary
+                                            forWritingURL:writingURL
+                                                    error:&error];
+                      }];
+  return isSuccess;
+}
+
+- (BOOL)writeDictionary:(NSMutableDictionary *)dictionary
+          forWritingURL:(NSURL *)writingFileURL
+                  error:(NSError **)outError {
+  NSData *data = [GULSecureCoding archivedDataWithRootObject:dictionary error:outError];
+  if (*outError != nil) {
+    return false;
+  } else {
+    return [data writeToURL:writingFileURL atomically:YES];
+  }
+}
+
+@end

+ 11 - 34
Example/CoreDiagnostics/Tests/FIRCoreDiagnosticsDateFileStorageTests.m → GoogleUtilities/Example/Tests/Environment/GULHeartbeatDateStorageTest.m

@@ -14,23 +14,21 @@
  * limitations under the License.
  */
 
+#import <GoogleUtilities/GULHeartbeatDateStorage.h>
 #import <XCTest/XCTest.h>
-#import "FIRCDLibrary/FIRCoreDiagnosticsDateFileStorage.h"
 
-@interface FIRCoreDiagnosticsDateFileStorageTests : XCTestCase
+@interface GULHeartbeatDateStorageTest : XCTestCase
 @property(nonatomic) NSURL *fileURL;
-@property(nonatomic) FIRCoreDiagnosticsDateFileStorage *storage;
+@property(nonatomic) GULHeartbeatDateStorage *storage;
 @end
 
-@implementation FIRCoreDiagnosticsDateFileStorageTests
+@implementation GULHeartbeatDateStorageTest
 
 - (void)setUp {
   NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(
       NSApplicationSupportDirectory, NSUserDomainMask, YES) firstObject];
   XCTAssertNotNil(documentsPath);
   NSURL *documentsURL = [NSURL fileURLWithPath:documentsPath];
-  self.fileURL = [documentsURL URLByAppendingPathComponent:@"FIRDiagnosticsDateFileStorageTests"
-                                               isDirectory:NO];
 
   NSError *error;
   if (![documentsURL checkResourceIsReachableAndReturnError:&error]) {
@@ -40,40 +38,19 @@
                                                              error:&error],
               @"Error: %@", error);
   }
-
-  self.storage = [[FIRCoreDiagnosticsDateFileStorage alloc] initWithFileURL:self.fileURL];
+  self.storage = [[GULHeartbeatDateStorage alloc] initWithFileName:@"GULStorageHeartbeatTest"];
 }
 
 - (void)tearDown {
-  [[NSFileManager defaultManager] removeItemAtURL:self.fileURL error:nil];
-  self.fileURL = nil;
+  [[NSFileManager defaultManager] removeItemAtURL:[self.storage fileURL] error:nil];
   self.storage = nil;
 }
 
-- (void)testDateStorage {
-  NSDate *dateToSave = [NSDate date];
-
-  XCTAssertNil([self.storage date]);
-
-  NSError *error;
-  XCTAssertTrue([self.storage setDate:dateToSave error:&error]);
-
-  XCTAssertEqualObjects([self.storage date], dateToSave);
-
-  XCTAssertTrue([self.storage setDate:nil error:&error]);
-  XCTAssertNil([self.storage date]);
-}
-
-- (void)testDateIsStoredToFileSystem {
-  NSDate *date = [NSDate date];
-
-  NSError *error;
-  XCTAssert([self.storage setDate:date error:&error], @"Error: %@", error);
-
-  FIRCoreDiagnosticsDateFileStorage *anotherStorage =
-      [[FIRCoreDiagnosticsDateFileStorage alloc] initWithFileURL:self.fileURL];
-
-  XCTAssertEqualObjects([anotherStorage date], date);
+- (void)testHeartbeatDateForTag {
+  NSDate *now = [NSDate date];
+  [self.storage setHearbeatDate:now forTag:@"fire-iid"];
+  XCTAssertEqual([now timeIntervalSinceReferenceDate],
+                 [[self.storage heartbeatDateForTag:@"fire-iid"] timeIntervalSinceReferenceDate]);
 }
 
 @end